Merge branch 'master' into beacon
This commit is contained in:
commit
8bd5de72ea
371 changed files with 33138 additions and 12950 deletions
|
|
@ -1,2 +1,3 @@
|
|||
[alias]
|
||||
lint = "clippy --workspace --benches --all-features --no-deps -- -D warnings"
|
||||
lint-fix = "clippy --fix --allow-dirty --workspace --benches --all-features --no-deps -- -D warnings"
|
||||
|
|
|
|||
16
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
16
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
|
|
@ -6,6 +6,22 @@ body:
|
|||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is your issue REALLY a bug?
|
||||
description: |
|
||||
This issue tracker is for __BUG REPORTS ONLY__.
|
||||
|
||||
It's obvious, right? This is a bug report form, after all! Still, some crazy users seem to forcefully fill out this form just to ask questions and request features.
|
||||
|
||||
The core team does not appreciate that. Don't do it.
|
||||
|
||||
If you want to ask a question or request a feature, please [go back here](https://github.com/iced-rs/iced/issues/new/choose) and read carefully.
|
||||
options:
|
||||
- label: My issue is indeed a bug!
|
||||
required: true
|
||||
- label: I am not crazy! I will not fill out this form just to ask a question or request a feature. Pinky promise.
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
|
|
|
|||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
|
@ -3,7 +3,7 @@ contact_links:
|
|||
- name: I have a question
|
||||
url: https://discourse.iced.rs/c/learn/6
|
||||
about: Ask and learn from others in the Discourse forum.
|
||||
- name: I want to start a discussion
|
||||
- name: I want to request a feature or start a discussion
|
||||
url: https://discourse.iced.rs/c/request-feedback/7
|
||||
about: Share your idea and gather feedback in the Discourse forum.
|
||||
- name: I want to chat with other users of the library
|
||||
|
|
|
|||
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
- name: Build todos binary
|
||||
run: cargo build --verbose --profile release-opt --package todos
|
||||
- name: Archive todos binary
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: todos-x86_64-unknown-linux-gnu
|
||||
path: target/release-opt/todos
|
||||
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
- name: Rename todos .deb package
|
||||
run: mv target/debian/*.deb target/debian/iced_todos-x86_64-debian-linux-gnu.deb
|
||||
- name: Archive todos .deb package
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: todos-x86_64-debian-linux-gnu
|
||||
path: target/debian/iced_todos-x86_64-debian-linux-gnu.deb
|
||||
|
|
@ -48,7 +48,7 @@ jobs:
|
|||
- name: Build todos binary
|
||||
run: cargo build --verbose --profile release-opt --package todos
|
||||
- name: Archive todos binary
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: todos-x86_64-pc-windows-msvc
|
||||
path: target/release-opt/todos.exe
|
||||
|
|
@ -65,7 +65,7 @@ jobs:
|
|||
- name: Open binary via double-click
|
||||
run: chmod +x target/release-opt/todos
|
||||
- name: Archive todos binary
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: todos-x86_64-apple-darwin
|
||||
path: target/release-opt/todos
|
||||
|
|
@ -80,14 +80,14 @@ jobs:
|
|||
- name: Build todos binary for Raspberry Pi 3/4 (64 bits)
|
||||
run: cross build --verbose --profile release-opt --package todos --target aarch64-unknown-linux-gnu
|
||||
- name: Archive todos binary
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: todos-aarch64-unknown-linux-gnu
|
||||
path: target/aarch64-unknown-linux-gnu/release-opt/todos
|
||||
- name: Build todos binary for Raspberry Pi 2/3/4 (32 bits)
|
||||
run: cross build --verbose --profile release-opt --package todos --target armv7-unknown-linux-gnueabihf
|
||||
- name: Archive todos binary
|
||||
uses: actions/upload-artifact@v1
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: todos-armv7-unknown-linux-gnueabihf
|
||||
path: target/armv7-unknown-linux-gnueabihf/release-opt/todos
|
||||
|
|
|
|||
3
.github/workflows/document.yml
vendored
3
.github/workflows/document.yml
vendored
|
|
@ -8,12 +8,13 @@ jobs:
|
|||
steps:
|
||||
- uses: hecrj/setup-rust-action@v2
|
||||
with:
|
||||
rust-version: nightly-2023-12-11
|
||||
rust-version: nightly
|
||||
- uses: actions/checkout@v2
|
||||
- name: Generate documentation
|
||||
run: |
|
||||
RUSTDOCFLAGS="--cfg docsrs" \
|
||||
cargo doc --no-deps --all-features \
|
||||
-p futures-core \
|
||||
-p iced_core \
|
||||
-p iced_highlighter \
|
||||
-p iced_futures \
|
||||
|
|
|
|||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
|
@ -8,7 +8,7 @@ jobs:
|
|||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
rust: [stable, beta]
|
||||
rust: [stable, beta, "1.85"]
|
||||
steps:
|
||||
- uses: hecrj/setup-rust-action@v2
|
||||
with:
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,6 +1,5 @@
|
|||
target/
|
||||
pkg/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
dist/
|
||||
traces/
|
||||
|
|
|
|||
222
CHANGELOG.md
222
CHANGELOG.md
|
|
@ -5,12 +5,228 @@ 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).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.13.1] - 2024-09-19
|
||||
### Added
|
||||
- `fetch_position` command in `window` module. [#2280](https://github.com/iced-rs/iced/pull/2280)
|
||||
- Some `From` trait implementations for `text_input::Id`. [#2582](https://github.com/iced-rs/iced/pull/2582)
|
||||
- Custom `Executor` support for `Application` and `Daemon`. [#2580](https://github.com/iced-rs/iced/pull/2580)
|
||||
- `rust-version` metadata to `Cargo.toml`. [#2579](https://github.com/iced-rs/iced/pull/2579)
|
||||
- Widget examples to API reference. [#2587](https://github.com/iced-rs/iced/pull/2587)
|
||||
|
||||
### Fixed
|
||||
- Inverted scrolling direction with trackpad in `scrollable`. [#2583](https://github.com/iced-rs/iced/pull/2583)
|
||||
- `scrollable` transactions when `on_scroll` is not set. [#2584](https://github.com/iced-rs/iced/pull/2584)
|
||||
- Incorrect text color styling in `text_editor` widget. [#2586](https://github.com/iced-rs/iced/pull/2586)
|
||||
|
||||
Many thanks to...
|
||||
- @dcampbell24
|
||||
- @lufte
|
||||
- @mtkennerly
|
||||
|
||||
## [0.13.0] - 2024-09-18
|
||||
### Added
|
||||
- Introductory chapters to the [official guide book](https://book.iced.rs/).
|
||||
- [Pocket guide](https://docs.rs/iced/0.13.0/iced/#the-pocket-guide) in API reference.
|
||||
- `Program` API. [#2331](https://github.com/iced-rs/iced/pull/2331)
|
||||
- `Task` API. [#2463](https://github.com/iced-rs/iced/pull/2463)
|
||||
- `Daemon` API and Shell Runtime Unification. [#2469](https://github.com/iced-rs/iced/pull/2469)
|
||||
- `rich_text` and `markdown` widgets. [#2508](https://github.com/iced-rs/iced/pull/2508)
|
||||
- `stack` widget. [#2405](https://github.com/iced-rs/iced/pull/2405)
|
||||
- `hover` widget. [#2408](https://github.com/iced-rs/iced/pull/2408)
|
||||
- `row::Wrapping` widget. [#2539](https://github.com/iced-rs/iced/pull/2539)
|
||||
- `text` macro helper. [#2338](https://github.com/iced-rs/iced/pull/2338)
|
||||
- `text::Wrapping` support. [#2279](https://github.com/iced-rs/iced/pull/2279)
|
||||
- Functional widget styling. [#2312](https://github.com/iced-rs/iced/pull/2312)
|
||||
- Closure-based widget styling. [#2326](https://github.com/iced-rs/iced/pull/2326)
|
||||
- Class-based Theming. [#2350](https://github.com/iced-rs/iced/pull/2350)
|
||||
- Type-Driven Renderer Fallback. [#2351](https://github.com/iced-rs/iced/pull/2351)
|
||||
- Background styling to `rich_text` widget. [#2516](https://github.com/iced-rs/iced/pull/2516)
|
||||
- Underline support for `rich_text`. [#2526](https://github.com/iced-rs/iced/pull/2526)
|
||||
- Strikethrough support for `rich_text`. [#2528](https://github.com/iced-rs/iced/pull/2528)
|
||||
- Abortable `Task`. [#2496](https://github.com/iced-rs/iced/pull/2496)
|
||||
- `abort_on_drop` to `task::Handle`. [#2503](https://github.com/iced-rs/iced/pull/2503)
|
||||
- `Ferra` theme. [#2329](https://github.com/iced-rs/iced/pull/2329)
|
||||
- `auto-detect-theme` feature. [#2343](https://github.com/iced-rs/iced/pull/2343)
|
||||
- Custom key binding support for `text_editor`. [#2522](https://github.com/iced-rs/iced/pull/2522)
|
||||
- `align_x` for `text_input` widget. [#2535](https://github.com/iced-rs/iced/pull/2535)
|
||||
- `center` widget helper. [#2423](https://github.com/iced-rs/iced/pull/2423)
|
||||
- Rotation support for `image` and `svg` widgets. [#2334](https://github.com/iced-rs/iced/pull/2334)
|
||||
- Dynamic `opacity` support for `image` and `svg`. [#2424](https://github.com/iced-rs/iced/pull/2424)
|
||||
- Scroll transactions for `scrollable` widget. [#2401](https://github.com/iced-rs/iced/pull/2401)
|
||||
- `physical_key` and `modified_key` to `keyboard::Event`. [#2576](https://github.com/iced-rs/iced/pull/2576)
|
||||
- `fetch_position` command in `window` module. [#2280](https://github.com/iced-rs/iced/pull/2280)
|
||||
- `filter_method` property for `image::Viewer` widget. [#2324](https://github.com/iced-rs/iced/pull/2324)
|
||||
- Support for pre-multiplied alpha `wgpu` composite mode. [#2341](https://github.com/iced-rs/iced/pull/2341)
|
||||
- `text_size` and `line_height` properties for `text_editor` widget. [#2358](https://github.com/iced-rs/iced/pull/2358)
|
||||
- `is_focused` method for `text_editor::State`. [#2386](https://github.com/iced-rs/iced/pull/2386)
|
||||
- `canvas::Cache` Grouping. [#2415](https://github.com/iced-rs/iced/pull/2415)
|
||||
- `ICED_PRESENT_MODE` env var to pick a `wgpu::PresentMode`. [#2428](https://github.com/iced-rs/iced/pull/2428)
|
||||
- `SpecificWith` variant to `window::Position`. [#2435](https://github.com/iced-rs/iced/pull/2435)
|
||||
- `scale_factor` field to `window::Screenshot`. [#2449](https://github.com/iced-rs/iced/pull/2449)
|
||||
- Styling support for `overlay::Menu` of `pick_list` widget. [#2457](https://github.com/iced-rs/iced/pull/2457)
|
||||
- `window::Id` in `Event` subscriptions. [#2456](https://github.com/iced-rs/iced/pull/2456)
|
||||
- `FromIterator` implementation for `row` and `column`. [#2460](https://github.com/iced-rs/iced/pull/2460)
|
||||
- `content_fit` for `image::viewer` widget. [#2330](https://github.com/iced-rs/iced/pull/2330)
|
||||
- `Display` implementation for `Radians`. [#2446](https://github.com/iced-rs/iced/pull/2446)
|
||||
- Helper methods for `window::Settings` in `Application`. [#2470](https://github.com/iced-rs/iced/pull/2470)
|
||||
- `Copy` implementation for `canvas::Fill` and `canvas::Stroke`. [#2475](https://github.com/iced-rs/iced/pull/2475)
|
||||
- Clarification of `Border` alignment for `Quad`. [#2485](https://github.com/iced-rs/iced/pull/2485)
|
||||
- "Select All" functionality on `Ctrl+A` to `text_editor`. [#2321](https://github.com/iced-rs/iced/pull/2321)
|
||||
- `stream::try_channel` helper. [#2497](https://github.com/iced-rs/iced/pull/2497)
|
||||
- `iced` widget helper to display the iced logo :comet:. [#2498](https://github.com/iced-rs/iced/pull/2498)
|
||||
- `align_x` and `align_y` helpers to `scrollable`. [#2499](https://github.com/iced-rs/iced/pull/2499)
|
||||
- Built-in text styles for each `Palette` color. [#2500](https://github.com/iced-rs/iced/pull/2500)
|
||||
- Embedded `Scrollbar` support for `scrollable`. [#2269](https://github.com/iced-rs/iced/pull/2269)
|
||||
- `on_press_with` method for `button`. [#2502](https://github.com/iced-rs/iced/pull/2502)
|
||||
- `resize_events` subscription to `window` module. [#2505](https://github.com/iced-rs/iced/pull/2505)
|
||||
- `Link` support to `rich_text` widget. [#2512](https://github.com/iced-rs/iced/pull/2512)
|
||||
- `image` and `svg` support for `canvas` widget. [#2537](https://github.com/iced-rs/iced/pull/2537)
|
||||
- `Compact` variant for `pane_grid::Controls`. [#2555](https://github.com/iced-rs/iced/pull/2555)
|
||||
- `image-without-codecs` feature flag. [#2244](https://github.com/iced-rs/iced/pull/2244)
|
||||
- `container::background` styling helper. [#2261](https://github.com/iced-rs/iced/pull/2261)
|
||||
- `undecorated_shadow` window setting for Windows. [#2285](https://github.com/iced-rs/iced/pull/2285)
|
||||
- Tasks for setting mouse passthrough. [#2284](https://github.com/iced-rs/iced/pull/2284)
|
||||
- `*_maybe` helpers for `text_input` widget. [#2390](https://github.com/iced-rs/iced/pull/2390)
|
||||
- Wasm support for `download_progress` example. [#2419](https://github.com/iced-rs/iced/pull/2419)
|
||||
- `scrollable::scroll_by` widget operation. [#2436](https://github.com/iced-rs/iced/pull/2436)
|
||||
- Enhancements to `slider` widget styling. [#2444](https://github.com/iced-rs/iced/pull/2444)
|
||||
- `on_scroll` handler to `mouse_area` widget. [#2450](https://github.com/iced-rs/iced/pull/2450)
|
||||
- `stroke_rectangle` method to `canvas::Frame`. [#2473](https://github.com/iced-rs/iced/pull/2473)
|
||||
- `override_redirect` setting for X11 windows. [#2476](https://github.com/iced-rs/iced/pull/2476)
|
||||
- Disabled state support for `toggler` widget. [#2478](https://github.com/iced-rs/iced/pull/2478)
|
||||
- `Color::parse` helper for parsing color strings. [#2486](https://github.com/iced-rs/iced/pull/2486)
|
||||
- `rounded_rectangle` method to `canvas::Path`. [#2491](https://github.com/iced-rs/iced/pull/2491)
|
||||
- `width` method to `text_editor` widget. [#2513](https://github.com/iced-rs/iced/pull/2513)
|
||||
- `on_open` handler to `combo_box` widget. [#2534](https://github.com/iced-rs/iced/pull/2534)
|
||||
- Additional `mouse::Interaction` cursors. [#2551](https://github.com/iced-rs/iced/pull/2551)
|
||||
- Scroll wheel handling in `slider` widget. [#2565](https://github.com/iced-rs/iced/pull/2565)
|
||||
|
||||
### Changed
|
||||
- Use a `StagingBelt` in `iced_wgpu` for regular buffer uploads. [#2357](https://github.com/iced-rs/iced/pull/2357)
|
||||
- Use generic `Content` in `Text` to avoid reallocation in `fill_text`. [#2360](https://github.com/iced-rs/iced/pull/2360)
|
||||
- Use `Iterator::size_hint` to initialize `Column` and `Row` capacity. [#2362](https://github.com/iced-rs/iced/pull/2362)
|
||||
- Specialize `widget::text` helper. [#2363](https://github.com/iced-rs/iced/pull/2363)
|
||||
- Use built-in `[lints]` table in `Cargo.toml`. [#2377](https://github.com/iced-rs/iced/pull/2377)
|
||||
- Target `#iced` container by default on Wasm. [#2342](https://github.com/iced-rs/iced/pull/2342)
|
||||
- Improved architecture for `iced_wgpu` and `iced_tiny_skia`. [#2382](https://github.com/iced-rs/iced/pull/2382)
|
||||
- Make image `Cache` eviction strategy less aggressive in `iced_wgpu`. [#2403](https://github.com/iced-rs/iced/pull/2403)
|
||||
- Retain caches in `iced_wgpu` as long as `Rc` values are alive. [#2409](https://github.com/iced-rs/iced/pull/2409)
|
||||
- Use `bytes` crate for `image` widget. [#2356](https://github.com/iced-rs/iced/pull/2356)
|
||||
- Update `winit` to `0.30`. [#2427](https://github.com/iced-rs/iced/pull/2427)
|
||||
- Reuse `glyphon::Pipeline` state in `iced_wgpu`. [#2430](https://github.com/iced-rs/iced/pull/2430)
|
||||
- Ask for explicit `Length` in `center_*` methods. [#2441](https://github.com/iced-rs/iced/pull/2441)
|
||||
- Hide internal `Task` constructors. [#2492](https://github.com/iced-rs/iced/pull/2492)
|
||||
- Hide `Subscription` internals. [#2493](https://github.com/iced-rs/iced/pull/2493)
|
||||
- Improved `view` ergonomics. [#2504](https://github.com/iced-rs/iced/pull/2504)
|
||||
- Update `cosmic-text` and `resvg`. [#2416](https://github.com/iced-rs/iced/pull/2416)
|
||||
- Snap `Quad` lines to the pixel grid in `iced_wgpu`. [#2531](https://github.com/iced-rs/iced/pull/2531)
|
||||
- Update `web-sys` to `0.3.69`. [#2507](https://github.com/iced-rs/iced/pull/2507)
|
||||
- Allow disabled `TextInput` to still be interacted with. [#2262](https://github.com/iced-rs/iced/pull/2262)
|
||||
- Enable horizontal scrolling without shift modifier for `srollable` widget. [#2392](https://github.com/iced-rs/iced/pull/2392)
|
||||
- Add `mouse::Button` to `mouse::Click`. [#2414](https://github.com/iced-rs/iced/pull/2414)
|
||||
- Notify `scrollable::Viewport` changes. [#2438](https://github.com/iced-rs/iced/pull/2438)
|
||||
- Improved documentation of `Component` state management. [#2556](https://github.com/iced-rs/iced/pull/2556)
|
||||
|
||||
### Fixed
|
||||
- Fix `block_on` in `iced_wgpu` hanging Wasm builds. [#2313](https://github.com/iced-rs/iced/pull/2313)
|
||||
- Private `PaneGrid` style fields. [#2316](https://github.com/iced-rs/iced/pull/2316)
|
||||
- Some documentation typos. [#2317](https://github.com/iced-rs/iced/pull/2317)
|
||||
- Blurry input caret with non-integral scaling. [#2320](https://github.com/iced-rs/iced/pull/2320)
|
||||
- Scrollbar stuck in a `scrollable` under some circumstances. [#2322](https://github.com/iced-rs/iced/pull/2322)
|
||||
- Broken `wgpu` examples link in issue template. [#2327](https://github.com/iced-rs/iced/pull/2327)
|
||||
- Empty `wgpu` draw calls in `image` pipeline. [#2344](https://github.com/iced-rs/iced/pull/2344)
|
||||
- Layout invalidation for `Responsive` widget. [#2345](https://github.com/iced-rs/iced/pull/2345)
|
||||
- Incorrect shadows on quads with rounded corners. [#2354](https://github.com/iced-rs/iced/pull/2354)
|
||||
- Empty menu overlay on `combo_box`. [#2364](https://github.com/iced-rs/iced/pull/2364)
|
||||
- Copy / cut vulnerability in a secure `TextInput`. [#2366](https://github.com/iced-rs/iced/pull/2366)
|
||||
- Inadequate readability / contrast for built-in themes. [#2376](https://github.com/iced-rs/iced/pull/2376)
|
||||
- Fix `pkg-config` typo in `DEPENDENCIES.md`. [#2379](https://github.com/iced-rs/iced/pull/2379)
|
||||
- Unbounded memory consumption by `iced_winit::Proxy`. [#2389](https://github.com/iced-rs/iced/pull/2389)
|
||||
- Typo in `icon::Error` message. [#2393](https://github.com/iced-rs/iced/pull/2393)
|
||||
- Nested scrollables capturing all scroll events. [#2397](https://github.com/iced-rs/iced/pull/2397)
|
||||
- Content capturing scrollbar events in a `scrollable`. [#2406](https://github.com/iced-rs/iced/pull/2406)
|
||||
- Out of bounds caret and overflow when scrolling in `text_editor`. [#2407](https://github.com/iced-rs/iced/pull/2407)
|
||||
- Missing `derive(Default)` in overview code snippet. [#2412](https://github.com/iced-rs/iced/pull/2412)
|
||||
- `image::Viewer` triggering grab from outside the widget. [#2420](https://github.com/iced-rs/iced/pull/2420)
|
||||
- Different windows fighting over shared `image::Cache`. [#2425](https://github.com/iced-rs/iced/pull/2425)
|
||||
- Images not aligned to the (logical) pixel grid in `iced_wgpu`. [#2440](https://github.com/iced-rs/iced/pull/2440)
|
||||
- Incorrect local time in `clock` example under Unix systems. [#2421](https://github.com/iced-rs/iced/pull/2421)
|
||||
- `⌘ + ←` and `⌘ + →` behavior for `text_input` on macOS. [#2315](https://github.com/iced-rs/iced/pull/2315)
|
||||
- Wayland packages in `DEPENDENCIES.md`. [#2465](https://github.com/iced-rs/iced/pull/2465)
|
||||
- Typo in documentation. [#2487](https://github.com/iced-rs/iced/pull/2487)
|
||||
- Extraneous comment in `scrollable` module. [#2488](https://github.com/iced-rs/iced/pull/2488)
|
||||
- Top layer in `hover` widget hiding when focused. [#2544](https://github.com/iced-rs/iced/pull/2544)
|
||||
- Out of bounds text in `text_editor` widget. [#2536](https://github.com/iced-rs/iced/pull/2536)
|
||||
- Segfault on Wayland when closing the app. [#2547](https://github.com/iced-rs/iced/pull/2547)
|
||||
- `lazy` feature flag sometimes not present in documentation. [#2289](https://github.com/iced-rs/iced/pull/2289)
|
||||
- Border of `progress_bar` widget being rendered below the active bar. [#2443](https://github.com/iced-rs/iced/pull/2443)
|
||||
- `radii` typo in `iced_wgpu` shader. [#2484](https://github.com/iced-rs/iced/pull/2484)
|
||||
- Incorrect priority of `Binding::Delete` in `text_editor`. [#2514](https://github.com/iced-rs/iced/pull/2514)
|
||||
- Division by zero in `multitouch` example. [#2517](https://github.com/iced-rs/iced/pull/2517)
|
||||
- Invisible text in `svg` widget. [#2560](https://github.com/iced-rs/iced/pull/2560)
|
||||
- `wasm32` deployments not displaying anything. [#2574](https://github.com/iced-rs/iced/pull/2574)
|
||||
- Unnecessary COM initialization on Windows. [#2578](https://github.com/iced-rs/iced/pull/2578)
|
||||
|
||||
### Removed
|
||||
- Unnecessary struct from `download_progress` example. [#2380](https://github.com/iced-rs/iced/pull/2380)
|
||||
- Out of date comment from `custom_widget` example. [#2549](https://github.com/iced-rs/iced/pull/2549)
|
||||
- `Clone` bound for `graphics::Cache::clear`. [#2575](https://github.com/iced-rs/iced/pull/2575)
|
||||
|
||||
Many thanks to...
|
||||
- @Aaron-McGuire
|
||||
- @airstrike
|
||||
- @alex-ds13
|
||||
- @alliby
|
||||
- @Andrew-Schwartz
|
||||
- @ayeniswe
|
||||
- @B0ney
|
||||
- @Bajix
|
||||
- @blazra
|
||||
- @Brady-Simon
|
||||
- @breynard0
|
||||
- @bungoboingo
|
||||
- @casperstorm
|
||||
- @Davidster
|
||||
- @derezzedex
|
||||
- @DKolter
|
||||
- @dtoniolo
|
||||
- @dtzxporter
|
||||
- @fenhl
|
||||
- @Gigas002
|
||||
- @gintsgints
|
||||
- @henrispriet
|
||||
- @IsaacMarovitz
|
||||
- @ivanceras
|
||||
- @Jinderamarak
|
||||
- @JL710
|
||||
- @jquesada2016
|
||||
- @JustSoup312
|
||||
- @kiedtl
|
||||
- @kmoon2437
|
||||
- @Koranir
|
||||
- @lufte
|
||||
- @LuisLiraC
|
||||
- @m4rch3n1ng
|
||||
- @meithecatte
|
||||
- @mtkennerly
|
||||
- @myuujiku
|
||||
- @n1ght-hunter
|
||||
- @nrjais
|
||||
- @PgBiel
|
||||
- @PolyMeilex
|
||||
- @rustrover
|
||||
- @ryankopf
|
||||
- @saihaze
|
||||
- @shartrec
|
||||
- @skygrango
|
||||
- @SolidStateDj
|
||||
- @sundaram123krishnan
|
||||
- @tarkah
|
||||
- @vladh
|
||||
- @WailAbou
|
||||
- @wiiznokes
|
||||
- @woelfman
|
||||
- @Zaubentrucker
|
||||
|
||||
## [0.12.1] - 2024-02-22
|
||||
### Added
|
||||
|
|
@ -772,7 +988,9 @@ Many thanks to...
|
|||
### Added
|
||||
- First release! :tada:
|
||||
|
||||
[Unreleased]: https://github.com/iced-rs/iced/compare/0.12.1...HEAD
|
||||
[Unreleased]: https://github.com/iced-rs/iced/compare/0.13.1...HEAD
|
||||
[0.13.1]: https://github.com/iced-rs/iced/compare/0.13.0...0.13.1
|
||||
[0.13.0]: https://github.com/iced-rs/iced/compare/0.12.1...0.13.0
|
||||
[0.12.1]: https://github.com/iced-rs/iced/compare/0.12.0...0.12.1
|
||||
[0.12.0]: https://github.com/iced-rs/iced/compare/0.10.0...0.12.0
|
||||
[0.10.0]: https://github.com/iced-rs/iced/compare/0.9.0...0.10.0
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Contributing
|
||||
|
||||
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! Take a look at [the roadmap] to get an idea of the current state of the library.
|
||||
|
||||
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!
|
||||
|
||||
|
|
@ -15,7 +15,6 @@ Besides directly writing code, there are many other different ways you can contr
|
|||
- Submitting bug reports and use cases
|
||||
- Sharing, discussing, researching and exploring new ideas or crates
|
||||
|
||||
[the ecosystem overview]: ECOSYSTEM.md
|
||||
[the roadmap]: ROADMAP.md
|
||||
[our Discourse forum]: https://discourse.iced.rs/
|
||||
[Code is the Easy Part]: https://youtu.be/DSjbTC-hvqQ?t=1138
|
||||
|
|
|
|||
7654
Cargo.lock
generated
Normal file
7654
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
107
Cargo.toml
107
Cargo.toml
|
|
@ -9,6 +9,7 @@ repository.workspace = true
|
|||
homepage.workspace = true
|
||||
categories.workspace = true
|
||||
keywords.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
@ -21,19 +22,23 @@ all-features = true
|
|||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[features]
|
||||
default = ["wgpu", "tiny-skia", "fira-sans", "auto-detect-theme"]
|
||||
# Enable the `wgpu` GPU-accelerated renderer backend
|
||||
default = ["wgpu", "tiny-skia", "auto-detect-theme"]
|
||||
# Enables the `wgpu` GPU-accelerated renderer backend
|
||||
wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"]
|
||||
# Enable the `tiny-skia` software renderer backend
|
||||
# Enables the `tiny-skia` software renderer backend
|
||||
tiny-skia = ["iced_renderer/tiny-skia"]
|
||||
# Enables the `Image` widget
|
||||
image = ["iced_widget/image", "dep:image"]
|
||||
# Enables the `Svg` widget
|
||||
# Enables the `image` widget
|
||||
image = ["image-without-codecs", "image/default"]
|
||||
# Enables the `image` widget, without any built-in codecs of the `image` crate
|
||||
image-without-codecs = ["iced_widget/image", "dep:image"]
|
||||
# Enables the `svg` widget
|
||||
svg = ["iced_widget/svg"]
|
||||
# Enables the `Canvas` widget
|
||||
# Enables the `canvas` widget
|
||||
canvas = ["iced_widget/canvas"]
|
||||
# Enables the `QRCode` widget
|
||||
# Enables the `qr_code` widget
|
||||
qr_code = ["iced_widget/qr_code"]
|
||||
# Enables the `markdown` widget
|
||||
markdown = ["iced_widget/markdown"]
|
||||
# Enables lazy widgets
|
||||
lazy = ["iced_widget/lazy"]
|
||||
# Enables a debug view in native platforms (press F12)
|
||||
|
|
@ -48,18 +53,20 @@ smol = ["iced_futures/smol"]
|
|||
system = ["iced_winit/system"]
|
||||
# Enables broken "sRGB linear" blending to reproduce color management of the Web
|
||||
web-colors = ["iced_renderer/web-colors"]
|
||||
# Enables the WebGL backend, replacing WebGPU
|
||||
# Enables the WebGL backend
|
||||
webgl = ["iced_renderer/webgl"]
|
||||
# Enables the syntax `highlighter` module
|
||||
highlighter = ["iced_highlighter"]
|
||||
# Enables experimental multi-window support.
|
||||
multi-window = ["iced_winit/multi-window"]
|
||||
# Enables syntax highligthing
|
||||
highlighter = ["iced_highlighter", "iced_widget/highlighter"]
|
||||
# Enables the advanced module
|
||||
advanced = ["iced_core/advanced", "iced_widget/advanced"]
|
||||
# Enables embedding Fira Sans as the default font on Wasm builds
|
||||
# Embeds Fira Sans into the final application; useful for testing and Wasm builds
|
||||
fira-sans = ["iced_renderer/fira-sans"]
|
||||
# Enables auto-detecting light/dark mode for the built-in theme
|
||||
# Auto-detects light/dark mode for the built-in theme
|
||||
auto-detect-theme = ["iced_core/auto-detect-theme"]
|
||||
# Enables strict assertions for debugging purposes at the expense of performance
|
||||
strict-assertions = ["iced_renderer/strict-assertions"]
|
||||
# Redraws on every runtime event, and not only when a widget requests it
|
||||
unconditional-rendering = ["iced_winit/unconditional-rendering"]
|
||||
|
||||
[dependencies]
|
||||
iced_debug.workspace = true
|
||||
|
|
@ -67,7 +74,7 @@ iced_core.workspace = true
|
|||
iced_futures.workspace = true
|
||||
iced_renderer.workspace = true
|
||||
iced_widget.workspace = true
|
||||
iced_winit.features = ["application"]
|
||||
iced_winit.features = ["program"]
|
||||
iced_winit.workspace = true
|
||||
|
||||
iced_highlighter.workspace = true
|
||||
|
|
@ -99,6 +106,7 @@ strip = "debuginfo"
|
|||
|
||||
[workspace]
|
||||
members = [
|
||||
"beacon",
|
||||
"core",
|
||||
"debug",
|
||||
"futures",
|
||||
|
|
@ -106,7 +114,7 @@ members = [
|
|||
"highlighter",
|
||||
"renderer",
|
||||
"runtime",
|
||||
"beacon",
|
||||
"test",
|
||||
"tiny_skia",
|
||||
"wgpu",
|
||||
"widget",
|
||||
|
|
@ -115,79 +123,85 @@ members = [
|
|||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.13.0-dev"
|
||||
version = "0.14.0-dev"
|
||||
authors = ["Héctor Ramón Jiménez <hector@hecrj.dev>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/iced-rs/iced"
|
||||
homepage = "https://iced.rs"
|
||||
categories = ["gui"]
|
||||
keywords = ["gui", "ui", "graphics", "interface", "widgets"]
|
||||
rust-version = "1.85"
|
||||
|
||||
[workspace.dependencies]
|
||||
iced = { version = "0.13.0-dev", path = "." }
|
||||
iced_beacon = { version = "0.13.0-dev", path = "beacon" }
|
||||
iced_core = { version = "0.13.0-dev", path = "core" }
|
||||
iced_debug = { version = "0.13.0-dev", path = "debug" }
|
||||
iced_futures = { version = "0.13.0-dev", path = "futures" }
|
||||
iced_graphics = { version = "0.13.0-dev", path = "graphics" }
|
||||
iced_highlighter = { version = "0.13.0-dev", path = "highlighter" }
|
||||
iced_renderer = { version = "0.13.0-dev", path = "renderer" }
|
||||
iced_runtime = { version = "0.13.0-dev", path = "runtime" }
|
||||
iced_tiny_skia = { version = "0.13.0-dev", path = "tiny_skia" }
|
||||
iced_wgpu = { version = "0.13.0-dev", path = "wgpu" }
|
||||
iced_widget = { version = "0.13.0-dev", path = "widget" }
|
||||
iced_winit = { version = "0.13.0-dev", path = "winit" }
|
||||
iced = { version = "0.14.0-dev", path = "." }
|
||||
iced_beacon = { version = "0.14.0-dev", path = "beacon" }
|
||||
iced_core = { version = "0.14.0-dev", path = "core" }
|
||||
iced_debug = { version = "0.14.0-dev", path = "debug" }
|
||||
iced_futures = { version = "0.14.0-dev", path = "futures" }
|
||||
iced_graphics = { version = "0.14.0-dev", path = "graphics" }
|
||||
iced_highlighter = { version = "0.14.0-dev", path = "highlighter" }
|
||||
iced_renderer = { version = "0.14.0-dev", path = "renderer" }
|
||||
iced_runtime = { version = "0.14.0-dev", path = "runtime" }
|
||||
iced_test = { version = "0.14.0-dev", path = "test" }
|
||||
iced_tiny_skia = { version = "0.14.0-dev", path = "tiny_skia" }
|
||||
iced_wgpu = { version = "0.14.0-dev", path = "wgpu" }
|
||||
iced_widget = { version = "0.14.0-dev", path = "widget" }
|
||||
iced_winit = { version = "0.14.0-dev", path = "winit" }
|
||||
|
||||
async-std = "1.0"
|
||||
bincode = "1.3"
|
||||
bitflags = "2.0"
|
||||
bytemuck = { version = "1.0", features = ["derive"] }
|
||||
bytes = "1.6"
|
||||
cosmic-text = "0.10"
|
||||
dark-light = "1.0"
|
||||
cosmic-text = "0.12"
|
||||
dark-light = "2.0"
|
||||
futures = "0.3"
|
||||
glam = "0.25"
|
||||
glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "f07e7bab705e69d39a5e6e52c73039a93c4552f8" }
|
||||
glyphon = { git = "https://github.com/hecrj/glyphon.git", rev = "09712a70df7431e9a3b1ac1bbd4fb634096cb3b4" }
|
||||
guillotiere = "0.6"
|
||||
half = "2.2"
|
||||
image = "0.24"
|
||||
image = { version = "0.25", default-features = false }
|
||||
kamadak-exif = "0.5"
|
||||
kurbo = "0.10"
|
||||
lilt = "0.8"
|
||||
log = "0.4"
|
||||
lyon = "1.0"
|
||||
lyon_path = "1.0"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.0"
|
||||
ouroboros = "0.18"
|
||||
palette = "0.7"
|
||||
png = "0.17"
|
||||
pulldown-cmark = "0.12"
|
||||
qrcode = { version = "0.13", default-features = false }
|
||||
raw-window-handle = "0.6"
|
||||
resvg = "0.36"
|
||||
rustc-hash = "1.0"
|
||||
resvg = "0.42"
|
||||
rustc-hash = "2.0"
|
||||
serde = "1.0"
|
||||
semver = "1.0"
|
||||
sha2 = "0.10"
|
||||
sipper = "0.1"
|
||||
smol = "1.0"
|
||||
smol_str = "0.2"
|
||||
softbuffer = "0.4"
|
||||
syntect = "5.1"
|
||||
sysinfo = "0.30"
|
||||
sysinfo = "0.33"
|
||||
thiserror = "1.0"
|
||||
tiny-skia = "0.11"
|
||||
tokio = "1.0"
|
||||
tracing = "0.1"
|
||||
unicode-segmentation = "1.0"
|
||||
url = "2.5"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
wasm-timer = "0.2"
|
||||
web-sys = "=0.3.67"
|
||||
wasmtimer = "0.4.1"
|
||||
web-sys = "0.3.69"
|
||||
web-time = "1.1"
|
||||
wgpu = "0.19"
|
||||
winapi = "0.3"
|
||||
wgpu = "23.0"
|
||||
window_clipboard = "0.4.1"
|
||||
winit = { git = "https://github.com/iced-rs/winit.git", rev = "8affa522bc6dcc497d332a28c03491d22a22f5a7" }
|
||||
winit = { git = "https://github.com/iced-rs/winit.git", rev = "11414b6aa45699f038114e61b4ddf5102b2d3b4b" }
|
||||
|
||||
[workspace.lints.rust]
|
||||
rust_2018_idioms = "deny"
|
||||
rust_2018_idioms = { level = "deny", priority = -1 }
|
||||
missing_debug_implementations = "deny"
|
||||
missing_docs = "deny"
|
||||
unsafe_code = "deny"
|
||||
|
|
@ -195,6 +209,7 @@ unused_results = "deny"
|
|||
|
||||
[workspace.lints.clippy]
|
||||
type-complexity = "allow"
|
||||
map-entry = "allow"
|
||||
semicolon_if_nothing_returned = "deny"
|
||||
trivially-copy-pass-by-ref = "deny"
|
||||
default_trait_access = "deny"
|
||||
|
|
|
|||
|
|
@ -25,9 +25,57 @@ pkgs.mkShell rec {
|
|||
xorg.libXcursor
|
||||
xorg.libXi
|
||||
xorg.libXrandr
|
||||
wayland
|
||||
libxkbcommon
|
||||
];
|
||||
|
||||
LD_LIBRARY_PATH =
|
||||
builtins.foldl' (a: b: "${a}:${b}/lib") "${pkgs.vulkan-loader}/lib" buildInputs;
|
||||
}
|
||||
```
|
||||
|
||||
Alternatively, you can use this `flake.nix` to create a dev shell, activated by `nix develop`:
|
||||
|
||||
```nix
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = {
|
||||
nixpkgs,
|
||||
flake-utils,
|
||||
...
|
||||
}:
|
||||
flake-utils.lib.eachDefaultSystem (
|
||||
system: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
expat
|
||||
fontconfig
|
||||
freetype
|
||||
freetype.dev
|
||||
libGL
|
||||
pkg-config
|
||||
xorg.libX11
|
||||
xorg.libXcursor
|
||||
xorg.libXi
|
||||
xorg.libXrandr
|
||||
wayland
|
||||
libxkbcommon
|
||||
];
|
||||
in {
|
||||
devShells.default = pkgs.mkShell {
|
||||
inherit buildInputs;
|
||||
|
||||
LD_LIBRARY_PATH =
|
||||
builtins.foldl' (a: b: "${a}:${b}/lib") "${pkgs.vulkan-loader}/lib" buildInputs;
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
|
|
|||
91
ECOSYSTEM.md
91
ECOSYSTEM.md
|
|
@ -1,91 +0,0 @@
|
|||
# Ecosystem
|
||||
This document describes the Iced ecosystem and explains how the different crates relate to each other.
|
||||
|
||||
## Overview
|
||||
Iced is meant to be used by 2 different types of users:
|
||||
|
||||
- __End-users__. They should be able to:
|
||||
- get started quickly,
|
||||
- have many widgets available,
|
||||
- keep things simple,
|
||||
- and build applications that are __maintainable__ and __performant__.
|
||||
- __GUI toolkit developers / Ecosystem contributors__. They should be able to:
|
||||
- build new kinds of widgets,
|
||||
- implement custom runtimes,
|
||||
- integrate existing runtimes in their own system (like game engines),
|
||||
- and create their own custom renderers.
|
||||
|
||||
Iced consists of different crates which offer different layers of abstractions for our users. This modular architecture helps us keep implementation details hidden and decoupled, which should allow us to rewrite or change strategies in the future.
|
||||
|
||||
<p align="center">
|
||||
<img alt="The Iced Ecosystem" src="docs/graphs/ecosystem.png" width="60%">
|
||||
</p>
|
||||
|
||||
## The foundations
|
||||
There are a bunch of concepts that permeate the whole ecosystem. These concepts are considered __the foundations__, and they are provided by three different crates:
|
||||
|
||||
- [`iced_core`] contains many lightweight, reusable primitives (e.g. `Point`, `Rectangle`, `Color`).
|
||||
- [`iced_futures`] implements the concurrent concepts of [The Elm Architecture] on top of the [`futures`] ecosystem.
|
||||
- [`iced_style`] defines the default styling capabilities of built-in widgets.
|
||||
|
||||
<p align="center">
|
||||
<img alt="The foundations" src="docs/graphs/foundations.png" width="50%">
|
||||
</p>
|
||||
|
||||
## The native target
|
||||
The native side of the ecosystem is split into two different groups: __renderers__ and __shells__.
|
||||
|
||||
<p align="center">
|
||||
<img alt="The native target" src="docs/graphs/native.png" width="80%">
|
||||
</p>
|
||||
|
||||
### Renderers
|
||||
The widgets of a _graphical_ user interface produce some primitives that eventually need to be drawn on screen. __Renderers__ take care of this task, potentially leveraging GPU acceleration.
|
||||
|
||||
Currently, there are two different official renderers:
|
||||
|
||||
- [`iced_wgpu`] is powered by [`wgpu`] and supports Vulkan, DirectX 12, and Metal.
|
||||
- [`tiny-skia`] is used as a fallback software renderer when `wgpu` is not supported.
|
||||
|
||||
Additionally, the [`iced_graphics`] subcrate contains a bunch of backend-agnostic types that can be leveraged to build renderers. Both of the renderers rely on the graphical foundations provided by this crate.
|
||||
|
||||
### Shells
|
||||
The widgets of a graphical user _interface_ are interactive. __Shells__ gather and process user interactions in an event loop.
|
||||
|
||||
Normally, a shell will be responsible of creating a window and managing the lifecycle of a user interface, implementing a runtime of [The Elm Architecture].
|
||||
|
||||
As of now, there is one official shell: [`iced_winit`] implements a shell runtime on top of [`winit`].
|
||||
|
||||
## The web target
|
||||
The Web platform provides all the abstractions necessary to draw widgets and gather users interactions.
|
||||
|
||||
Therefore, unlike the native path, the web side of the ecosystem does not need to split renderers and shells. Instead, [`iced_web`] leverages [`dodrio`] to both render widgets and implement a proper runtime.
|
||||
|
||||
## Iced
|
||||
Finally, [`iced`] unifies everything into a simple abstraction to create cross-platform applications:
|
||||
|
||||
- On native, it uses __[shells](#shells)__ and __[renderers](#renderers)__.
|
||||
- On the web, it uses [`iced_web`].
|
||||
|
||||
<p align="center">
|
||||
<img alt="Iced" src="docs/graphs/iced.png" width="80%">
|
||||
</p>
|
||||
|
||||
[`iced_core`]: core
|
||||
[`iced_futures`]: futures
|
||||
[`iced_style`]: style
|
||||
[`iced_native`]: native
|
||||
[`iced_web`]: https://github.com/iced-rs/iced_web
|
||||
[`iced_graphics`]: graphics
|
||||
[`iced_wgpu`]: wgpu
|
||||
[`iced_glow`]: glow
|
||||
[`iced_winit`]: winit
|
||||
[`iced_glutin`]: glutin
|
||||
[`iced`]: ..
|
||||
[`futures`]: https://github.com/rust-lang/futures-rs
|
||||
[`glow`]: https://github.com/grovesNL/glow
|
||||
[`wgpu`]: https://github.com/gfx-rs/wgpu
|
||||
[`winit`]: https://github.com/rust-windowing/winit
|
||||
[`glutin`]: https://github.com/rust-windowing/glutin
|
||||
[`dodrio`]: https://github.com/fitzgen/dodrio
|
||||
[The Elm Architecture]: https://guide.elm-lang.org/architecture/
|
||||
54
README.md
54
README.md
|
|
@ -15,11 +15,11 @@
|
|||
A cross-platform GUI library for Rust focused on simplicity and type-safety.
|
||||
Inspired by [Elm].
|
||||
|
||||
<a href="https://iced.rs/examples/todos.mp4">
|
||||
<img src="https://iced.rs/examples/todos.gif" width="275px">
|
||||
<a href="https://github.com/squidowl/halloy">
|
||||
<img src="https://iced.rs/showcase/halloy.gif" width="460px">
|
||||
</a>
|
||||
<a href="https://iced.rs/examples/tour.mp4">
|
||||
<img src="https://iced.rs/examples/tour.gif" width="273px">
|
||||
<a href="https://github.com/hecrj/icebreaker">
|
||||
<img src="https://iced.rs/showcase/icebreaker.gif" width="360px">
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
|
@ -34,50 +34,28 @@ Inspired by [Elm].
|
|||
* Custom widget support (create your own!)
|
||||
* [Debug overlay with performance metrics]
|
||||
* First-class support for async actions (use futures!)
|
||||
* [Modular ecosystem] split into reusable parts:
|
||||
* Modular ecosystem split into reusable parts:
|
||||
* A [renderer-agnostic native runtime] enabling integration with existing systems
|
||||
* Two [built-in renderers] leveraging [`wgpu`] and [`tiny-skia`]
|
||||
* Two built-in renderers leveraging [`wgpu`] and [`tiny-skia`]
|
||||
* [`iced_wgpu`] supporting Vulkan, Metal and DX12
|
||||
* [`iced_tiny_skia`] offering a software alternative as a fallback
|
||||
* A [windowing shell]
|
||||
* A [web runtime] leveraging the DOM
|
||||
|
||||
__Iced is currently experimental software.__ [Take a look at the roadmap],
|
||||
[check out the issues], and [feel free to contribute!]
|
||||
__Iced is currently experimental software.__ [Take a look at the roadmap] and
|
||||
[check out the issues].
|
||||
|
||||
[Cross-platform support]: https://raw.githubusercontent.com/iced-rs/iced/master/docs/images/todos_desktop.jpg
|
||||
[text inputs]: https://iced.rs/examples/text_input.mp4
|
||||
[scrollables]: https://iced.rs/examples/scrollable.mp4
|
||||
[Debug overlay with performance metrics]: https://iced.rs/examples/debug.mp4
|
||||
[Modular ecosystem]: ECOSYSTEM.md
|
||||
[renderer-agnostic native runtime]: runtime/
|
||||
[`wgpu`]: https://github.com/gfx-rs/wgpu
|
||||
[`tiny-skia`]: https://github.com/RazrFalcon/tiny-skia
|
||||
[`iced_wgpu`]: wgpu/
|
||||
[`iced_tiny_skia`]: tiny_skia/
|
||||
[built-in renderers]: ECOSYSTEM.md#Renderers
|
||||
[windowing shell]: winit/
|
||||
[`dodrio`]: https://github.com/fitzgen/dodrio
|
||||
[web runtime]: https://github.com/iced-rs/iced_web
|
||||
[Take a look at the roadmap]: ROADMAP.md
|
||||
[check out the issues]: https://github.com/iced-rs/iced/issues
|
||||
[feel free to contribute!]: #contributing--feedback
|
||||
|
||||
## Installation
|
||||
|
||||
Add `iced` as a dependency in your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
iced = "0.12"
|
||||
```
|
||||
|
||||
If your project is using a Rust edition older than 2021, then you will need to
|
||||
set `resolver = "2"` in the `[package]` section as well.
|
||||
|
||||
__Iced moves fast and the `master` branch can contain breaking changes!__ If
|
||||
you want to learn about a specific release, check out [the release list].
|
||||
|
||||
[the release list]: https://github.com/iced-rs/iced/releases
|
||||
|
||||
## Overview
|
||||
|
||||
|
|
@ -180,7 +158,7 @@ Read the [book], the [documentation], and the [examples] to learn more!
|
|||
## Implementation details
|
||||
|
||||
Iced was originally born as an attempt at bringing the simplicity of [Elm] and
|
||||
[The Elm Architecture] into [Coffee], a 2D game engine I am working on.
|
||||
[The Elm Architecture] into [Coffee], a 2D game library I am working on.
|
||||
|
||||
The core of the library was implemented during May 2019 in [this pull request].
|
||||
[The first alpha version] was eventually released as
|
||||
|
|
@ -188,25 +166,17 @@ The core of the library was implemented during May 2019 in [this pull request].
|
|||
implemented the current [tour example] on top of [`ggez`], a game library.
|
||||
|
||||
Since then, the focus has shifted towards providing a batteries-included,
|
||||
end-user-oriented GUI library, while keeping [the ecosystem] modular:
|
||||
|
||||
<p align="center">
|
||||
<a href="ECOSYSTEM.md">
|
||||
<img alt="The Iced Ecosystem" src="docs/graphs/ecosystem.png" width="80%">
|
||||
</a>
|
||||
</p>
|
||||
end-user-oriented GUI library, while keeping the ecosystem modular.
|
||||
|
||||
[this pull request]: https://github.com/hecrj/coffee/pull/35
|
||||
[The first alpha version]: https://github.com/iced-rs/iced/tree/0.1.0-alpha
|
||||
[a renderer-agnostic GUI library]: https://www.reddit.com/r/rust/comments/czzjnv/iced_a_rendereragnostic_gui_library_focused_on/
|
||||
[tour example]: examples/README.md#tour
|
||||
[`ggez`]: https://github.com/ggez/ggez
|
||||
[the ecosystem]: ECOSYSTEM.md
|
||||
|
||||
## Contributing / Feedback
|
||||
|
||||
Contributions are greatly appreciated! If you want to contribute, please
|
||||
read our [contributing guidelines] for more details.
|
||||
If you want to contribute, please read our [contributing guidelines] for more details.
|
||||
|
||||
Feedback is also welcome! You can create a new topic in [our Discourse forum] or
|
||||
come chat to [our Discord server].
|
||||
|
|
@ -217,7 +187,7 @@ The development of Iced is sponsored by the [Cryptowatch] team at [Kraken.com]
|
|||
|
||||
[book]: https://book.iced.rs/
|
||||
[documentation]: https://docs.rs/iced/
|
||||
[examples]: https://github.com/iced-rs/iced/tree/master/examples
|
||||
[examples]: https://github.com/iced-rs/iced/tree/master/examples#examples
|
||||
[Coffee]: https://github.com/hecrj/coffee
|
||||
[Elm]: https://elm-lang.org/
|
||||
[The Elm Architecture]: https://guide.elm-lang.org/architecture/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,2 @@
|
|||
# Roadmap
|
||||
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.
|
||||
|
||||
[the ecosystem overview]: ECOSYSTEM.md
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#![allow(missing_docs)]
|
||||
use criterion::{criterion_group, criterion_main, Bencher, Criterion};
|
||||
use criterion::{Bencher, Criterion, criterion_group, criterion_main};
|
||||
|
||||
use iced::alignment;
|
||||
use iced::mouse;
|
||||
|
|
@ -66,6 +66,7 @@ fn benchmark<'a>(
|
|||
label: None,
|
||||
required_features: wgpu::Features::empty(),
|
||||
required_limits: wgpu::Limits::default(),
|
||||
memory_hints: wgpu::MemoryHints::MemoryUsage,
|
||||
},
|
||||
None,
|
||||
))
|
||||
|
|
|
|||
|
|
@ -21,9 +21,9 @@ advanced = []
|
|||
bitflags.workspace = true
|
||||
bytes.workspace = true
|
||||
glam.workspace = true
|
||||
lilt.workspace = true
|
||||
log.workspace = true
|
||||
num-traits.workspace = true
|
||||
once_cell.workspace = true
|
||||
palette.workspace = true
|
||||
rustc-hash.workspace = true
|
||||
smol_str.workspace = true
|
||||
|
|
|
|||
|
|
@ -13,15 +13,3 @@ This crate is meant to be a starting point for an Iced runtime.
|
|||
</p>
|
||||
|
||||
[documentation]: https://docs.rs/iced_core
|
||||
|
||||
## Installation
|
||||
Add `iced_core` as a dependency in your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
iced_core = "0.9"
|
||||
```
|
||||
|
||||
__Iced moves fast and the `master` branch can contain breaking changes!__ If
|
||||
you want to learn about a specific release, check out [the release list].
|
||||
|
||||
[the release list]: https://github.com/iced-rs/iced/releases
|
||||
|
|
|
|||
|
|
@ -46,6 +46,16 @@ pub enum Horizontal {
|
|||
Right,
|
||||
}
|
||||
|
||||
impl From<Alignment> for Horizontal {
|
||||
fn from(alignment: Alignment) -> Self {
|
||||
match alignment {
|
||||
Alignment::Start => Self::Left,
|
||||
Alignment::Center => Self::Center,
|
||||
Alignment::End => Self::Right,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The vertical [`Alignment`] of some resource.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Vertical {
|
||||
|
|
@ -58,3 +68,13 @@ pub enum Vertical {
|
|||
/// Align bottom
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl From<Alignment> for Vertical {
|
||||
fn from(alignment: Alignment) -> Self {
|
||||
match alignment {
|
||||
Alignment::Start => Self::Top,
|
||||
Alignment::Center => Self::Center,
|
||||
Alignment::End => Self::Bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{Point, Rectangle, Vector};
|
||||
|
||||
use std::f32::consts::{FRAC_PI_2, PI};
|
||||
use std::fmt::Display;
|
||||
use std::ops::{Add, AddAssign, Div, Mul, RangeInclusive, Rem, Sub, SubAssign};
|
||||
|
||||
/// Degrees
|
||||
|
|
@ -237,3 +238,9 @@ impl PartialOrd<f32> for Radians {
|
|||
self.0.partial_cmp(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Radians {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{} rad", self.0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
148
core/src/animation.rs
Normal file
148
core/src/animation.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
//! Animate your applications.
|
||||
use crate::time::{Duration, Instant};
|
||||
|
||||
pub use lilt::{Easing, FloatRepresentable as Float, Interpolable};
|
||||
|
||||
/// The animation of some particular state.
|
||||
///
|
||||
/// It tracks state changes and allows projecting interpolated values
|
||||
/// through time.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Animation<T>
|
||||
where
|
||||
T: Clone + Copy + PartialEq + Float,
|
||||
{
|
||||
raw: lilt::Animated<T, Instant>,
|
||||
duration: Duration, // TODO: Expose duration getter in `lilt`
|
||||
}
|
||||
|
||||
impl<T> Animation<T>
|
||||
where
|
||||
T: Clone + Copy + PartialEq + Float,
|
||||
{
|
||||
/// Creates a new [`Animation`] with the given initial state.
|
||||
pub fn new(state: T) -> Self {
|
||||
Self {
|
||||
raw: lilt::Animated::new(state),
|
||||
duration: Duration::from_millis(100),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the [`Easing`] function of the [`Animation`].
|
||||
///
|
||||
/// See the [Easing Functions Cheat Sheet](https://easings.net) for
|
||||
/// details!
|
||||
pub fn easing(mut self, easing: Easing) -> Self {
|
||||
self.raw = self.raw.easing(easing);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the duration of the [`Animation`] to 100ms.
|
||||
pub fn very_quick(self) -> Self {
|
||||
self.duration(Duration::from_millis(100))
|
||||
}
|
||||
|
||||
/// Sets the duration of the [`Animation`] to 200ms.
|
||||
pub fn quick(self) -> Self {
|
||||
self.duration(Duration::from_millis(200))
|
||||
}
|
||||
|
||||
/// Sets the duration of the [`Animation`] to 400ms.
|
||||
pub fn slow(self) -> Self {
|
||||
self.duration(Duration::from_millis(400))
|
||||
}
|
||||
|
||||
/// Sets the duration of the [`Animation`] to 500ms.
|
||||
pub fn very_slow(self) -> Self {
|
||||
self.duration(Duration::from_millis(500))
|
||||
}
|
||||
|
||||
/// Sets the duration of the [`Animation`] to the given value.
|
||||
pub fn duration(mut self, duration: Duration) -> Self {
|
||||
self.raw = self.raw.duration(duration.as_secs_f32() * 1_000.0);
|
||||
self.duration = duration;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a delay for the [`Animation`].
|
||||
pub fn delay(mut self, duration: Duration) -> Self {
|
||||
self.raw = self.raw.delay(duration.as_secs_f64() as f32 * 1000.0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Makes the [`Animation`] repeat a given amount of times.
|
||||
///
|
||||
/// Providing 1 repetition plays the animation twice in total.
|
||||
pub fn repeat(mut self, repetitions: u32) -> Self {
|
||||
self.raw = self.raw.repeat(repetitions);
|
||||
self
|
||||
}
|
||||
|
||||
/// Makes the [`Animation`] repeat forever.
|
||||
pub fn repeat_forever(mut self) -> Self {
|
||||
self.raw = self.raw.repeat_forever();
|
||||
self
|
||||
}
|
||||
|
||||
/// Makes the [`Animation`] automatically reverse when repeating.
|
||||
pub fn auto_reverse(mut self) -> Self {
|
||||
self.raw = self.raw.auto_reverse();
|
||||
self
|
||||
}
|
||||
|
||||
/// Transitions the [`Animation`] from its current state to the given new state.
|
||||
pub fn go(mut self, new_state: T) -> Self {
|
||||
self.go_mut(new_state);
|
||||
self
|
||||
}
|
||||
|
||||
/// Transitions the [`Animation`] from its current state to the given new state, by reference.
|
||||
pub fn go_mut(&mut self, new_state: T) {
|
||||
self.raw.transition(new_state, Instant::now());
|
||||
}
|
||||
|
||||
/// Returns true if the [`Animation`] is currently in progress.
|
||||
///
|
||||
/// An [`Animation`] is in progress when it is transitioning to a different state.
|
||||
pub fn is_animating(&self, at: Instant) -> bool {
|
||||
self.raw.in_progress(at)
|
||||
}
|
||||
|
||||
/// Projects the [`Animation`] into an interpolated value at the given [`Instant`]; using the
|
||||
/// closure provided to calculate the different keyframes of interpolated values.
|
||||
///
|
||||
/// If the [`Animation`] state is a `bool`, you can use the simpler [`interpolate`] method.
|
||||
///
|
||||
/// [`interpolate`]: Animation::interpolate
|
||||
pub fn interpolate_with<I>(&self, f: impl Fn(T) -> I, at: Instant) -> I
|
||||
where
|
||||
I: Interpolable,
|
||||
{
|
||||
self.raw.animate(f, at)
|
||||
}
|
||||
|
||||
/// Retuns the current state of the [`Animation`].
|
||||
pub fn value(&self) -> T {
|
||||
self.raw.value
|
||||
}
|
||||
}
|
||||
|
||||
impl Animation<bool> {
|
||||
/// Projects the [`Animation`] into an interpolated value at the given [`Instant`]; using the
|
||||
/// `start` and `end` values as the origin and destination keyframes.
|
||||
pub fn interpolate<I>(&self, start: I, end: I, at: Instant) -> I
|
||||
where
|
||||
I: Interpolable + Clone,
|
||||
{
|
||||
self.raw.animate_bool(start, end, at)
|
||||
}
|
||||
|
||||
/// Returns the remaining [`Duration`] of the [`Animation`].
|
||||
pub fn remaining(&self, at: Instant) -> Duration {
|
||||
Duration::from_secs_f32(self.interpolate(
|
||||
self.duration.as_secs_f32(),
|
||||
0.0,
|
||||
at,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::gradient::{self, Gradient};
|
||||
use crate::Color;
|
||||
use crate::gradient::{self, Gradient};
|
||||
|
||||
/// The background of some element.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
|
|
|
|||
|
|
@ -10,40 +10,64 @@ pub struct Border {
|
|||
/// The width of the border.
|
||||
pub width: f32,
|
||||
|
||||
/// The radius of the border.
|
||||
/// The [`Radius`] of the border.
|
||||
pub radius: Radius,
|
||||
}
|
||||
|
||||
impl Border {
|
||||
/// Creates a new default rounded [`Border`] with the given [`Radius`].
|
||||
///
|
||||
/// ```
|
||||
/// # use iced_core::Border;
|
||||
/// #
|
||||
/// assert_eq!(Border::rounded(10), Border::default().with_radius(10));
|
||||
/// ```
|
||||
pub fn rounded(radius: impl Into<Radius>) -> Self {
|
||||
Self::default().with_radius(radius)
|
||||
}
|
||||
/// Creates a new [`Border`] with the given [`Radius`].
|
||||
///
|
||||
/// ```
|
||||
/// # use iced_core::border::{self, Border};
|
||||
/// #
|
||||
/// assert_eq!(border::rounded(10), Border::default().rounded(10));
|
||||
/// ```
|
||||
pub fn rounded(radius: impl Into<Radius>) -> Border {
|
||||
Border::default().rounded(radius)
|
||||
}
|
||||
|
||||
/// Updates the [`Color`] of the [`Border`].
|
||||
pub fn with_color(self, color: impl Into<Color>) -> Self {
|
||||
/// Creates a new [`Border`] with the given [`Color`].
|
||||
///
|
||||
/// ```
|
||||
/// # use iced_core::border::{self, Border};
|
||||
/// # use iced_core::Color;
|
||||
/// #
|
||||
/// assert_eq!(border::color(Color::BLACK), Border::default().color(Color::BLACK));
|
||||
/// ```
|
||||
pub fn color(color: impl Into<Color>) -> Border {
|
||||
Border::default().color(color)
|
||||
}
|
||||
|
||||
/// Creates a new [`Border`] with the given `width`.
|
||||
///
|
||||
/// ```
|
||||
/// # use iced_core::border::{self, Border};
|
||||
/// # use iced_core::Color;
|
||||
/// #
|
||||
/// assert_eq!(border::width(10), Border::default().width(10));
|
||||
/// ```
|
||||
pub fn width(width: impl Into<Pixels>) -> Border {
|
||||
Border::default().width(width)
|
||||
}
|
||||
|
||||
impl Border {
|
||||
/// Sets the [`Color`] of the [`Border`].
|
||||
pub fn color(self, color: impl Into<Color>) -> Self {
|
||||
Self {
|
||||
color: color.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the [`Radius`] of the [`Border`].
|
||||
pub fn with_radius(self, radius: impl Into<Radius>) -> Self {
|
||||
/// Sets the [`Radius`] of the [`Border`].
|
||||
pub fn rounded(self, radius: impl Into<Radius>) -> Self {
|
||||
Self {
|
||||
radius: radius.into(),
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the width of the [`Border`].
|
||||
pub fn with_width(self, width: impl Into<Pixels>) -> Self {
|
||||
/// Sets the width of the [`Border`].
|
||||
pub fn width(self, width: impl Into<Pixels>) -> Self {
|
||||
Self {
|
||||
width: width.into().0,
|
||||
..self
|
||||
|
|
@ -54,11 +78,160 @@ impl Border {
|
|||
/// The border radii for the corners of a graphics primitive in the order:
|
||||
/// top-left, top-right, bottom-right, bottom-left.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct Radius([f32; 4]);
|
||||
pub struct Radius {
|
||||
/// Top left radius
|
||||
pub top_left: f32,
|
||||
/// Top right radius
|
||||
pub top_right: f32,
|
||||
/// Bottom right radius
|
||||
pub bottom_right: f32,
|
||||
/// Bottom left radius
|
||||
pub bottom_left: f32,
|
||||
}
|
||||
|
||||
/// Creates a new [`Radius`] with the same value for each corner.
|
||||
pub fn radius(value: impl Into<Pixels>) -> Radius {
|
||||
Radius::new(value)
|
||||
}
|
||||
|
||||
/// Creates a new [`Radius`] with the given top left value.
|
||||
pub fn top_left(value: impl Into<Pixels>) -> Radius {
|
||||
Radius::default().top_left(value)
|
||||
}
|
||||
|
||||
/// Creates a new [`Radius`] with the given top right value.
|
||||
pub fn top_right(value: impl Into<Pixels>) -> Radius {
|
||||
Radius::default().top_right(value)
|
||||
}
|
||||
|
||||
/// Creates a new [`Radius`] with the given bottom right value.
|
||||
pub fn bottom_right(value: impl Into<Pixels>) -> Radius {
|
||||
Radius::default().bottom_right(value)
|
||||
}
|
||||
|
||||
/// Creates a new [`Radius`] with the given bottom left value.
|
||||
pub fn bottom_left(value: impl Into<Pixels>) -> Radius {
|
||||
Radius::default().bottom_left(value)
|
||||
}
|
||||
|
||||
/// Creates a new [`Radius`] with the given value as top left and top right.
|
||||
pub fn top(value: impl Into<Pixels>) -> Radius {
|
||||
Radius::default().top(value)
|
||||
}
|
||||
|
||||
/// Creates a new [`Radius`] with the given value as bottom left and bottom right.
|
||||
pub fn bottom(value: impl Into<Pixels>) -> Radius {
|
||||
Radius::default().bottom(value)
|
||||
}
|
||||
|
||||
/// Creates a new [`Radius`] with the given value as top left and bottom left.
|
||||
pub fn left(value: impl Into<Pixels>) -> Radius {
|
||||
Radius::default().left(value)
|
||||
}
|
||||
|
||||
/// Creates a new [`Radius`] with the given value as top right and bottom right.
|
||||
pub fn right(value: impl Into<Pixels>) -> Radius {
|
||||
Radius::default().right(value)
|
||||
}
|
||||
|
||||
impl Radius {
|
||||
/// Creates a new [`Radius`] with the same value for each corner.
|
||||
pub fn new(value: impl Into<Pixels>) -> Self {
|
||||
let value = value.into().0;
|
||||
|
||||
Self {
|
||||
top_left: value,
|
||||
top_right: value,
|
||||
bottom_right: value,
|
||||
bottom_left: value,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the top left value of the [`Radius`].
|
||||
pub fn top_left(self, value: impl Into<Pixels>) -> Self {
|
||||
Self {
|
||||
top_left: value.into().0,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the top right value of the [`Radius`].
|
||||
pub fn top_right(self, value: impl Into<Pixels>) -> Self {
|
||||
Self {
|
||||
top_right: value.into().0,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the bottom right value of the [`Radius`].
|
||||
pub fn bottom_right(self, value: impl Into<Pixels>) -> Self {
|
||||
Self {
|
||||
bottom_right: value.into().0,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the bottom left value of the [`Radius`].
|
||||
pub fn bottom_left(self, value: impl Into<Pixels>) -> Self {
|
||||
Self {
|
||||
bottom_left: value.into().0,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the top left and top right values of the [`Radius`].
|
||||
pub fn top(self, value: impl Into<Pixels>) -> Self {
|
||||
let value = value.into().0;
|
||||
|
||||
Self {
|
||||
top_left: value,
|
||||
top_right: value,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the bottom left and bottom right values of the [`Radius`].
|
||||
pub fn bottom(self, value: impl Into<Pixels>) -> Self {
|
||||
let value = value.into().0;
|
||||
|
||||
Self {
|
||||
bottom_left: value,
|
||||
bottom_right: value,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the top left and bottom left values of the [`Radius`].
|
||||
pub fn left(self, value: impl Into<Pixels>) -> Self {
|
||||
let value = value.into().0;
|
||||
|
||||
Self {
|
||||
top_left: value,
|
||||
bottom_left: value,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the top right and bottom right values of the [`Radius`].
|
||||
pub fn right(self, value: impl Into<Pixels>) -> Self {
|
||||
let value = value.into().0;
|
||||
|
||||
Self {
|
||||
top_right: value,
|
||||
bottom_right: value,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f32> for Radius {
|
||||
fn from(w: f32) -> Self {
|
||||
Self([w; 4])
|
||||
fn from(radius: f32) -> Self {
|
||||
Self {
|
||||
top_left: radius,
|
||||
top_right: radius,
|
||||
bottom_right: radius,
|
||||
bottom_left: radius,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,14 +253,13 @@ impl From<i32> for Radius {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<[f32; 4]> for Radius {
|
||||
fn from(radi: [f32; 4]) -> Self {
|
||||
Self(radi)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Radius> for [f32; 4] {
|
||||
fn from(radi: Radius) -> Self {
|
||||
radi.0
|
||||
[
|
||||
radi.top_left,
|
||||
radi.top_right,
|
||||
radi.bottom_right,
|
||||
radi.bottom_left,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
/// The border radii for the corners of a graphics primitive in the order:
|
||||
/// top-left, top-right, bottom-right, bottom-left.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct BorderRadius([f32; 4]);
|
||||
|
||||
impl From<f32> for BorderRadius {
|
||||
fn from(w: f32) -> Self {
|
||||
Self([w; 4])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[f32; 4]> for BorderRadius {
|
||||
fn from(radi: [f32; 4]) -> Self {
|
||||
Self(radi)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BorderRadius> for [f32; 4] {
|
||||
fn from(radi: BorderRadius) -> Self {
|
||||
radi.0
|
||||
}
|
||||
}
|
||||
|
|
@ -43,22 +43,18 @@ impl Color {
|
|||
///
|
||||
/// In debug mode, it will panic if the values are not in the correct
|
||||
/// range: 0.0 - 1.0
|
||||
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Color {
|
||||
const fn new(r: f32, g: f32, b: f32, a: f32) -> Color {
|
||||
debug_assert!(
|
||||
(0.0..=1.0).contains(&r),
|
||||
"Red component must be on [0, 1]"
|
||||
r >= 0.0 && r <= 1.0,
|
||||
"Red component must be in [0, 1] range."
|
||||
);
|
||||
debug_assert!(
|
||||
(0.0..=1.0).contains(&g),
|
||||
"Green component must be on [0, 1]"
|
||||
g >= 0.0 && g <= 1.0,
|
||||
"Green component must be in [0, 1] range."
|
||||
);
|
||||
debug_assert!(
|
||||
(0.0..=1.0).contains(&b),
|
||||
"Blue component must be on [0, 1]"
|
||||
);
|
||||
debug_assert!(
|
||||
(0.0..=1.0).contains(&a),
|
||||
"Alpha component must be on [0, 1]"
|
||||
b >= 0.0 && b <= 1.0,
|
||||
"Blue component must be in [0, 1] range."
|
||||
);
|
||||
|
||||
Color { r, g, b, a }
|
||||
|
|
@ -71,22 +67,17 @@ impl Color {
|
|||
|
||||
/// Creates a [`Color`] from its RGBA components.
|
||||
pub const fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
|
||||
Color { r, g, b, a }
|
||||
Color::new(r, g, b, a)
|
||||
}
|
||||
|
||||
/// Creates a [`Color`] from its RGB8 components.
|
||||
pub fn from_rgb8(r: u8, g: u8, b: u8) -> Color {
|
||||
pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Color {
|
||||
Color::from_rgba8(r, g, b, 1.0)
|
||||
}
|
||||
|
||||
/// Creates a [`Color`] from its RGB8 components and an alpha value.
|
||||
pub fn from_rgba8(r: u8, g: u8, b: u8, a: f32) -> Color {
|
||||
Color {
|
||||
r: f32::from(r) / 255.0,
|
||||
g: f32::from(g) / 255.0,
|
||||
b: f32::from(b) / 255.0,
|
||||
a,
|
||||
}
|
||||
pub const fn from_rgba8(r: u8, g: u8, b: u8, a: f32) -> Color {
|
||||
Color::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a)
|
||||
}
|
||||
|
||||
/// Creates a [`Color`] from its linear RGBA components.
|
||||
|
|
@ -109,6 +100,53 @@ impl Color {
|
|||
}
|
||||
}
|
||||
|
||||
/// Parses a [`Color`] from a hex string.
|
||||
///
|
||||
/// Supported formats are `#rrggbb`, `#rrggbbaa`, `#rgb`, and `#rgba`.
|
||||
/// The starting "#" is optional. Both uppercase and lowercase are supported.
|
||||
///
|
||||
/// If you have a static color string, using the [`color!`] macro should be preferred
|
||||
/// since it leverages hexadecimal literal notation and arithmetic directly.
|
||||
///
|
||||
/// [`color!`]: crate::color!
|
||||
pub fn parse(s: &str) -> Option<Color> {
|
||||
let hex = s.strip_prefix('#').unwrap_or(s);
|
||||
|
||||
let parse_channel = |from: usize, to: usize| {
|
||||
let num =
|
||||
usize::from_str_radix(&hex[from..=to], 16).ok()? as f32 / 255.0;
|
||||
|
||||
// If we only got half a byte (one letter), expand it into a full byte (two letters)
|
||||
Some(if from == to { num + num * 16.0 } else { num })
|
||||
};
|
||||
|
||||
Some(match hex.len() {
|
||||
3 => Color::from_rgb(
|
||||
parse_channel(0, 0)?,
|
||||
parse_channel(1, 1)?,
|
||||
parse_channel(2, 2)?,
|
||||
),
|
||||
4 => Color::from_rgba(
|
||||
parse_channel(0, 0)?,
|
||||
parse_channel(1, 1)?,
|
||||
parse_channel(2, 2)?,
|
||||
parse_channel(3, 3)?,
|
||||
),
|
||||
6 => Color::from_rgb(
|
||||
parse_channel(0, 1)?,
|
||||
parse_channel(2, 3)?,
|
||||
parse_channel(4, 5)?,
|
||||
),
|
||||
8 => Color::from_rgba(
|
||||
parse_channel(0, 1)?,
|
||||
parse_channel(2, 3)?,
|
||||
parse_channel(4, 5)?,
|
||||
parse_channel(6, 7)?,
|
||||
),
|
||||
_ => None?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Converts the [`Color`] into its RGBA8 equivalent.
|
||||
#[must_use]
|
||||
pub fn into_rgba8(self) -> [u8; 4] {
|
||||
|
|
@ -179,34 +217,29 @@ impl From<[f32; 4]> for Color {
|
|||
///
|
||||
/// ```
|
||||
/// # use iced_core::{Color, color};
|
||||
/// assert_eq!(color!(0, 0, 0), Color::from_rgb(0., 0., 0.));
|
||||
/// assert_eq!(color!(0, 0, 0, 0.), Color::from_rgba(0., 0., 0., 0.));
|
||||
/// assert_eq!(color!(0xffffff), Color::from_rgb(1., 1., 1.));
|
||||
/// assert_eq!(color!(0xffffff, 0.), Color::from_rgba(1., 1., 1., 0.));
|
||||
/// assert_eq!(color!(0, 0, 0), Color::BLACK);
|
||||
/// assert_eq!(color!(0, 0, 0, 0.0), Color::TRANSPARENT);
|
||||
/// assert_eq!(color!(0xffffff), Color::from_rgb(1.0, 1.0, 1.0));
|
||||
/// assert_eq!(color!(0xffffff, 0.), Color::from_rgba(1.0, 1.0, 1.0, 0.0));
|
||||
/// assert_eq!(color!(0x0000ff), Color::from_rgba(0.0, 0.0, 1.0, 1.0));
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! color {
|
||||
($r:expr, $g:expr, $b:expr) => {
|
||||
color!($r, $g, $b, 1.0)
|
||||
$crate::Color::from_rgb8($r, $g, $b)
|
||||
};
|
||||
($r:expr, $g:expr, $b:expr, $a:expr) => {
|
||||
$crate::Color {
|
||||
r: $r as f32 / 255.0,
|
||||
g: $g as f32 / 255.0,
|
||||
b: $b as f32 / 255.0,
|
||||
a: $a,
|
||||
}
|
||||
};
|
||||
($hex:expr) => {{
|
||||
color!($hex, 1.0)
|
||||
}};
|
||||
($r:expr, $g:expr, $b:expr, $a:expr) => {{ $crate::Color::from_rgba8($r, $g, $b, $a) }};
|
||||
($hex:expr) => {{ $crate::color!($hex, 1.0) }};
|
||||
($hex:expr, $a:expr) => {{
|
||||
let hex = $hex as u32;
|
||||
|
||||
debug_assert!(hex <= 0xffffff, "color! value must not exceed 0xffffff");
|
||||
|
||||
let r = (hex & 0xff0000) >> 16;
|
||||
let g = (hex & 0xff00) >> 8;
|
||||
let b = (hex & 0xff);
|
||||
|
||||
color!(r, g, b, $a)
|
||||
$crate::color!(r as u8, g as u8, b as u8, $a)
|
||||
}};
|
||||
}
|
||||
|
||||
|
|
@ -274,4 +307,23 @@ mod tests {
|
|||
assert_relative_eq!(result.b, 0.3);
|
||||
assert_relative_eq!(result.a, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
let tests = [
|
||||
("#ff0000", [255, 0, 0, 255]),
|
||||
("00ff0080", [0, 255, 0, 128]),
|
||||
("#F80", [255, 136, 0, 255]),
|
||||
("#00f1", [0, 0, 255, 17]),
|
||||
];
|
||||
|
||||
for (arg, expected) in tests {
|
||||
assert_eq!(
|
||||
Color::parse(arg).expect("color must parse").into_rgba8(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
assert!(Color::parse("invalid").is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::overlay;
|
||||
|
|
@ -6,11 +5,10 @@ use crate::renderer;
|
|||
use crate::widget;
|
||||
use crate::widget::tree::{self, Tree};
|
||||
use crate::{
|
||||
Border, Clipboard, Color, Layout, Length, Rectangle, Shell, Size, Vector,
|
||||
Widget,
|
||||
Border, Clipboard, Color, Event, Layout, Length, Rectangle, Shell, Size,
|
||||
Vector, Widget,
|
||||
};
|
||||
|
||||
use std::any::Any;
|
||||
use std::borrow::Borrow;
|
||||
|
||||
/// A generic [`Widget`].
|
||||
|
|
@ -95,6 +93,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> {
|
|||
///
|
||||
/// ```no_run
|
||||
/// # mod iced {
|
||||
/// # pub use iced_core::Function;
|
||||
/// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, ()>;
|
||||
/// #
|
||||
/// # pub mod widget {
|
||||
|
|
@ -121,7 +120,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> {
|
|||
/// use counter::Counter;
|
||||
///
|
||||
/// use iced::widget::row;
|
||||
/// use iced::Element;
|
||||
/// use iced::{Element, Function};
|
||||
///
|
||||
/// struct ManyCounters {
|
||||
/// counters: Vec<Counter>,
|
||||
|
|
@ -144,7 +143,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> {
|
|||
/// // Here we turn our `Element<counter::Message>` into
|
||||
/// // an `Element<Message>` by combining the `index` and the
|
||||
/// // message of the `element`.
|
||||
/// counter.map(move |message| Message::Counter(index, message))
|
||||
/// counter.map(Message::Counter.with(index))
|
||||
/// }),
|
||||
/// )
|
||||
/// .into()
|
||||
|
|
@ -305,80 +304,26 @@ where
|
|||
tree: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<B>,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
struct MapOperation<'a, B> {
|
||||
operation: &'a mut dyn widget::Operation<B>,
|
||||
self.widget.operate(tree, layout, renderer, operation);
|
||||
}
|
||||
|
||||
impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> {
|
||||
fn container(
|
||||
&mut self,
|
||||
id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
operate_on_children: &mut dyn FnMut(
|
||||
&mut dyn widget::Operation<T>,
|
||||
),
|
||||
) {
|
||||
self.operation.container(id, bounds, &mut |operation| {
|
||||
operate_on_children(&mut MapOperation { operation });
|
||||
});
|
||||
}
|
||||
|
||||
fn focusable(
|
||||
&mut self,
|
||||
state: &mut dyn widget::operation::Focusable,
|
||||
id: Option<&widget::Id>,
|
||||
) {
|
||||
self.operation.focusable(state, id);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn widget::operation::Scrollable,
|
||||
id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
translation: Vector,
|
||||
) {
|
||||
self.operation.scrollable(state, id, bounds, translation);
|
||||
}
|
||||
|
||||
fn text_input(
|
||||
&mut self,
|
||||
state: &mut dyn widget::operation::TextInput,
|
||||
id: Option<&widget::Id>,
|
||||
) {
|
||||
self.operation.text_input(state, id);
|
||||
}
|
||||
|
||||
fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) {
|
||||
self.operation.custom(state, id);
|
||||
}
|
||||
}
|
||||
|
||||
self.widget.operate(
|
||||
tree,
|
||||
layout,
|
||||
renderer,
|
||||
&mut MapOperation { operation },
|
||||
);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, B>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
) {
|
||||
let mut local_messages = Vec::new();
|
||||
let mut local_shell = Shell::new(&mut local_messages);
|
||||
|
||||
let status = self.widget.on_event(
|
||||
self.widget.update(
|
||||
tree,
|
||||
event,
|
||||
layout,
|
||||
|
|
@ -390,8 +335,6 @@ where
|
|||
);
|
||||
|
||||
shell.merge(local_shell, &self.mapper);
|
||||
|
||||
status
|
||||
}
|
||||
|
||||
fn draw(
|
||||
|
|
@ -452,8 +395,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Explain<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Explain<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
|
|
@ -495,27 +438,27 @@ where
|
|||
state: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.element
|
||||
.widget
|
||||
.operate(state, layout, renderer, operation);
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
state: &mut Tree,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
self.element.widget.on_event(
|
||||
) {
|
||||
self.element.widget.update(
|
||||
state, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
fn draw(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
//! Handle events of a user interface.
|
||||
use crate::input_method;
|
||||
use crate::keyboard;
|
||||
use crate::mouse;
|
||||
use crate::touch;
|
||||
|
|
@ -19,31 +20,13 @@ pub enum Event {
|
|||
Mouse(mouse::Event),
|
||||
|
||||
/// A window event
|
||||
Window(window::Id, window::Event),
|
||||
Window(window::Event),
|
||||
|
||||
/// A touch event
|
||||
Touch(touch::Event),
|
||||
|
||||
/// A platform specific event
|
||||
PlatformSpecific(PlatformSpecific),
|
||||
}
|
||||
|
||||
/// A platform specific event
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PlatformSpecific {
|
||||
/// A MacOS specific event
|
||||
MacOS(MacOS),
|
||||
}
|
||||
|
||||
/// Describes an event specific to MacOS
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum MacOS {
|
||||
/// Triggered when the app receives an URL from the system
|
||||
///
|
||||
/// _**Note:** For this event to be triggered, the executable needs to be properly [bundled]!_
|
||||
///
|
||||
/// [bundled]: https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW19
|
||||
ReceivedUrl(String),
|
||||
/// An input method event
|
||||
InputMethod(input_method::Event),
|
||||
}
|
||||
|
||||
/// The status of an [`Event`] after being processed.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,73 @@ use rustc_hash::FxHasher;
|
|||
use std::hash::{Hash, Hasher};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// A raster image that can be drawn.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Image<H = Handle> {
|
||||
/// The handle of the image.
|
||||
pub handle: H,
|
||||
|
||||
/// The filter method of the image.
|
||||
pub filter_method: FilterMethod,
|
||||
|
||||
/// The rotation to be applied to the image; on its center.
|
||||
pub rotation: Radians,
|
||||
|
||||
/// The opacity of the image.
|
||||
///
|
||||
/// 0 means transparent. 1 means opaque.
|
||||
pub opacity: f32,
|
||||
|
||||
/// If set to `true`, the image will be snapped to the pixel grid.
|
||||
///
|
||||
/// This can avoid graphical glitches, specially when using
|
||||
/// [`FilterMethod::Nearest`].
|
||||
pub snap: bool,
|
||||
}
|
||||
|
||||
impl Image<Handle> {
|
||||
/// Creates a new [`Image`] with the given handle.
|
||||
pub fn new(handle: impl Into<Handle>) -> Self {
|
||||
Self {
|
||||
handle: handle.into(),
|
||||
filter_method: FilterMethod::default(),
|
||||
rotation: Radians(0.0),
|
||||
opacity: 1.0,
|
||||
snap: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the filter method of the [`Image`].
|
||||
pub fn filter_method(mut self, filter_method: FilterMethod) -> Self {
|
||||
self.filter_method = filter_method;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the rotation of the [`Image`].
|
||||
pub fn rotation(mut self, rotation: impl Into<Radians>) -> Self {
|
||||
self.rotation = rotation.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the opacity of the [`Image`].
|
||||
pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
|
||||
self.opacity = opacity.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the [`Image`] should be snapped to the pixel grid.
|
||||
pub fn snap(mut self, snap: bool) -> Self {
|
||||
self.snap = snap;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Handle> for Image {
|
||||
fn from(handle: &Handle) -> Self {
|
||||
Image::new(handle.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle of some image data.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum Handle {
|
||||
|
|
@ -101,6 +168,12 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl From<&Handle> for Handle {
|
||||
fn from(value: &Handle) -> Self {
|
||||
value.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Handle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
|
|
@ -166,14 +239,6 @@ pub trait Renderer: crate::Renderer {
|
|||
/// Returns the dimensions of an image for the given [`Handle`].
|
||||
fn measure_image(&self, handle: &Self::Handle) -> Size<u32>;
|
||||
|
||||
/// Draws an image with the given [`Handle`] and inside the provided
|
||||
/// `bounds`.
|
||||
fn draw_image(
|
||||
&mut self,
|
||||
handle: Self::Handle,
|
||||
filter_method: FilterMethod,
|
||||
bounds: Rectangle,
|
||||
rotation: Radians,
|
||||
opacity: f32,
|
||||
);
|
||||
/// Draws an [`Image`] inside the provided `bounds`.
|
||||
fn draw_image(&mut self, image: Image<Self::Handle>, bounds: Rectangle);
|
||||
}
|
||||
|
|
|
|||
221
core/src/input_method.rs
Normal file
221
core/src/input_method.rs
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
//! Listen to input method events.
|
||||
use crate::{Pixels, Point};
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
/// The input method strategy of a widget.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum InputMethod<T = String> {
|
||||
/// Input method is disabled.
|
||||
Disabled,
|
||||
/// Input method is enabled.
|
||||
Enabled {
|
||||
/// The position at which the input method dialog should be placed.
|
||||
position: Point,
|
||||
/// The [`Purpose`] of the input method.
|
||||
purpose: Purpose,
|
||||
/// The preedit to overlay on top of the input method dialog, if needed.
|
||||
///
|
||||
/// Ideally, your widget will show pre-edits on-the-spot; but, since that can
|
||||
/// be tricky, you can instead provide the current pre-edit here and the
|
||||
/// runtime will display it as an overlay (i.e. "Over-the-spot IME").
|
||||
preedit: Option<Preedit<T>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// The pre-edit of an [`InputMethod`].
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
pub struct Preedit<T = String> {
|
||||
/// The current content.
|
||||
pub content: T,
|
||||
/// The selected range of the content.
|
||||
pub selection: Option<Range<usize>>,
|
||||
/// The text size of the content.
|
||||
pub text_size: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl<T> Preedit<T> {
|
||||
/// Creates a new empty [`Preedit`].
|
||||
pub fn new() -> Self
|
||||
where
|
||||
T: Default,
|
||||
{
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Turns a [`Preedit`] into its owned version.
|
||||
pub fn to_owned(&self) -> Preedit
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
Preedit {
|
||||
content: self.content.as_ref().to_owned(),
|
||||
selection: self.selection.clone(),
|
||||
text_size: self.text_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Preedit {
|
||||
/// Borrows the contents of a [`Preedit`].
|
||||
pub fn as_ref(&self) -> Preedit<&str> {
|
||||
Preedit {
|
||||
content: &self.content,
|
||||
selection: self.selection.clone(),
|
||||
text_size: self.text_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The purpose of an [`InputMethod`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum Purpose {
|
||||
/// No special hints for the IME (default).
|
||||
#[default]
|
||||
Normal,
|
||||
/// The IME is used for secure input (e.g. passwords).
|
||||
Secure,
|
||||
/// The IME is used to input into a terminal.
|
||||
///
|
||||
/// For example, that could alter OSK on Wayland to show extra buttons.
|
||||
Terminal,
|
||||
}
|
||||
|
||||
impl InputMethod {
|
||||
/// Merges two [`InputMethod`] strategies, prioritizing the first one when both open:
|
||||
/// ```
|
||||
/// # use iced_core::input_method::{InputMethod, Purpose, Preedit};
|
||||
/// # use iced_core::Point;
|
||||
///
|
||||
/// let open = InputMethod::Enabled {
|
||||
/// position: Point::ORIGIN,
|
||||
/// purpose: Purpose::Normal,
|
||||
/// preedit: Some(Preedit { content: "1".to_owned(), selection: None, text_size: None }),
|
||||
/// };
|
||||
///
|
||||
/// let open_2 = InputMethod::Enabled {
|
||||
/// position: Point::ORIGIN,
|
||||
/// purpose: Purpose::Secure,
|
||||
/// preedit: Some(Preedit { content: "2".to_owned(), selection: None, text_size: None }),
|
||||
/// };
|
||||
///
|
||||
/// let mut ime = InputMethod::Disabled;
|
||||
///
|
||||
/// ime.merge(&open);
|
||||
/// assert_eq!(ime, open);
|
||||
///
|
||||
/// ime.merge(&open_2);
|
||||
/// assert_eq!(ime, open);
|
||||
/// ```
|
||||
pub fn merge<T: AsRef<str>>(&mut self, other: &InputMethod<T>) {
|
||||
if let InputMethod::Enabled { .. } = self {
|
||||
return;
|
||||
}
|
||||
|
||||
*self = other.to_owned();
|
||||
}
|
||||
|
||||
/// Returns true if the [`InputMethod`] is open.
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
matches!(self, Self::Enabled { .. })
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> InputMethod<T> {
|
||||
/// Turns an [`InputMethod`] into its owned version.
|
||||
pub fn to_owned(&self) -> InputMethod
|
||||
where
|
||||
T: AsRef<str>,
|
||||
{
|
||||
match self {
|
||||
Self::Disabled => InputMethod::Disabled,
|
||||
Self::Enabled {
|
||||
position,
|
||||
purpose,
|
||||
preedit,
|
||||
} => InputMethod::Enabled {
|
||||
position: *position,
|
||||
purpose: *purpose,
|
||||
preedit: preedit.as_ref().map(Preedit::to_owned),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes [input method](https://en.wikipedia.org/wiki/Input_method) events.
|
||||
///
|
||||
/// This is also called a "composition event".
|
||||
///
|
||||
/// Most keypresses using a latin-like keyboard layout simply generate a
|
||||
/// [`keyboard::Event::KeyPressed`](crate::keyboard::Event::KeyPressed).
|
||||
/// However, one couldn't possibly have a key for every single
|
||||
/// unicode character that the user might want to type. The solution operating systems employ is
|
||||
/// to allow the user to type these using _a sequence of keypresses_ instead.
|
||||
///
|
||||
/// A prominent example of this is accents—many keyboard layouts allow you to first click the
|
||||
/// "accent key", and then the character you want to apply the accent to. In this case, some
|
||||
/// platforms will generate the following event sequence:
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Press "`" key
|
||||
/// Ime::Preedit("`", Some((0, 0)))
|
||||
/// // Press "E" key
|
||||
/// Ime::Preedit("", None) // Synthetic event generated to clear preedit.
|
||||
/// Ime::Commit("é")
|
||||
/// ```
|
||||
///
|
||||
/// Additionally, certain input devices are configured to display a candidate box that allow the
|
||||
/// user to select the desired character interactively. (To properly position this box, you must use
|
||||
/// [`Shell::request_input_method`](crate::Shell::request_input_method).)
|
||||
///
|
||||
/// An example of a keyboard layout which uses candidate boxes is pinyin. On a latin keyboard the
|
||||
/// following event sequence could be obtained:
|
||||
///
|
||||
/// ```ignore
|
||||
/// // Press "A" key
|
||||
/// Ime::Preedit("a", Some((1, 1)))
|
||||
/// // Press "B" key
|
||||
/// Ime::Preedit("a b", Some((3, 3)))
|
||||
/// // Press left arrow key
|
||||
/// Ime::Preedit("a b", Some((1, 1)))
|
||||
/// // Press space key
|
||||
/// Ime::Preedit("啊b", Some((3, 3)))
|
||||
/// // Press space key
|
||||
/// Ime::Preedit("", None) // Synthetic event generated to clear preedit.
|
||||
/// Ime::Commit("啊不")
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Event {
|
||||
/// Notifies when the IME was opened.
|
||||
///
|
||||
/// After getting this event you could receive [`Preedit`][Self::Preedit] and
|
||||
/// [`Commit`][Self::Commit] events. You should also start performing IME related requests
|
||||
/// like [`Shell::request_input_method`].
|
||||
///
|
||||
/// [`Shell::request_input_method`]: crate::Shell::request_input_method
|
||||
Opened,
|
||||
|
||||
/// Notifies when a new composing text should be set at the cursor position.
|
||||
///
|
||||
/// The value represents a pair of the preedit string and the cursor begin position and end
|
||||
/// position. When it's `None`, the cursor should be hidden. When `String` is an empty string
|
||||
/// this indicates that preedit was cleared.
|
||||
///
|
||||
/// The cursor range is byte-wise indexed.
|
||||
Preedit(String, Option<Range<usize>>),
|
||||
|
||||
/// Notifies when text should be inserted into the editor widget.
|
||||
///
|
||||
/// Right before this event, an empty [`Self::Preedit`] event will be issued.
|
||||
Commit(String),
|
||||
|
||||
/// Notifies when the IME was disabled.
|
||||
///
|
||||
/// After receiving this event you won't get any more [`Preedit`][Self::Preedit] or
|
||||
/// [`Commit`][Self::Commit] events until the next [`Opened`][Self::Opened] event. You should
|
||||
/// also stop issuing IME related requests like [`Shell::request_input_method`] and clear
|
||||
/// pending preedit text.
|
||||
///
|
||||
/// [`Shell::request_input_method`]: crate::Shell::request_input_method
|
||||
Closed,
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::keyboard::{Key, Location, Modifiers};
|
||||
use crate::SmolStr;
|
||||
use crate::keyboard::key;
|
||||
use crate::keyboard::{Key, Location, Modifiers};
|
||||
|
||||
/// A keyboard event.
|
||||
///
|
||||
|
|
@ -14,6 +15,12 @@ pub enum Event {
|
|||
/// The key pressed.
|
||||
key: Key,
|
||||
|
||||
/// The key pressed with all keyboard modifiers applied, except Ctrl.
|
||||
modified_key: Key,
|
||||
|
||||
/// The physical key pressed.
|
||||
physical_key: key::Physical,
|
||||
|
||||
/// The location of the key.
|
||||
location: Location,
|
||||
|
||||
|
|
@ -29,6 +36,12 @@ pub enum Event {
|
|||
/// The key released.
|
||||
key: Key,
|
||||
|
||||
/// The key released with all keyboard modifiers applied, except Ctrl.
|
||||
modified_key: Key,
|
||||
|
||||
/// The physical key released.
|
||||
physical_key: key::Physical,
|
||||
|
||||
/// The location of the key.
|
||||
location: Location,
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ impl Key {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Named> for Key {
|
||||
fn from(named: Named) -> Self {
|
||||
Self::Named(named)
|
||||
}
|
||||
}
|
||||
|
||||
/// A named key.
|
||||
///
|
||||
/// This is mostly the `NamedKey` type found in [`winit`].
|
||||
|
|
@ -203,7 +209,7 @@ pub enum Named {
|
|||
Standby,
|
||||
/// The WakeUp key. (`KEYCODE_WAKEUP`)
|
||||
WakeUp,
|
||||
/// Initate the multi-candidate mode.
|
||||
/// Initiate the multi-candidate mode.
|
||||
AllCandidates,
|
||||
Alphanumeric,
|
||||
/// Initiate the Code Input mode to allow characters to be entered by
|
||||
|
|
@ -742,3 +748,536 @@ pub enum Named {
|
|||
/// General-purpose function key.
|
||||
F35,
|
||||
}
|
||||
|
||||
/// Code representing the location of a physical key.
|
||||
///
|
||||
/// This mostly conforms to the UI Events Specification's [`KeyboardEvent.code`] with a few
|
||||
/// exceptions:
|
||||
/// - The keys that the specification calls "MetaLeft" and "MetaRight" are named "SuperLeft" and
|
||||
/// "SuperRight" here.
|
||||
/// - The key that the specification calls "Super" is reported as `Unidentified` here.
|
||||
///
|
||||
/// [`KeyboardEvent.code`]: https://w3c.github.io/uievents-code/#code-value-tables
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[allow(missing_docs)]
|
||||
#[non_exhaustive]
|
||||
pub enum Code {
|
||||
/// <kbd>`</kbd> on a US keyboard. This is also called a backtick or grave.
|
||||
/// This is the <kbd>半角</kbd>/<kbd>全角</kbd>/<kbd>漢字</kbd>
|
||||
/// (hankaku/zenkaku/kanji) key on Japanese keyboards
|
||||
Backquote,
|
||||
/// Used for both the US <kbd>\\</kbd> (on the 101-key layout) and also for the key
|
||||
/// located between the <kbd>"</kbd> and <kbd>Enter</kbd> keys on row C of the 102-,
|
||||
/// 104- and 106-key layouts.
|
||||
/// Labeled <kbd>#</kbd> on a UK (102) keyboard.
|
||||
Backslash,
|
||||
/// <kbd>[</kbd> on a US keyboard.
|
||||
BracketLeft,
|
||||
/// <kbd>]</kbd> on a US keyboard.
|
||||
BracketRight,
|
||||
/// <kbd>,</kbd> on a US keyboard.
|
||||
Comma,
|
||||
/// <kbd>0</kbd> on a US keyboard.
|
||||
Digit0,
|
||||
/// <kbd>1</kbd> on a US keyboard.
|
||||
Digit1,
|
||||
/// <kbd>2</kbd> on a US keyboard.
|
||||
Digit2,
|
||||
/// <kbd>3</kbd> on a US keyboard.
|
||||
Digit3,
|
||||
/// <kbd>4</kbd> on a US keyboard.
|
||||
Digit4,
|
||||
/// <kbd>5</kbd> on a US keyboard.
|
||||
Digit5,
|
||||
/// <kbd>6</kbd> on a US keyboard.
|
||||
Digit6,
|
||||
/// <kbd>7</kbd> on a US keyboard.
|
||||
Digit7,
|
||||
/// <kbd>8</kbd> on a US keyboard.
|
||||
Digit8,
|
||||
/// <kbd>9</kbd> on a US keyboard.
|
||||
Digit9,
|
||||
/// <kbd>=</kbd> on a US keyboard.
|
||||
Equal,
|
||||
/// Located between the left <kbd>Shift</kbd> and <kbd>Z</kbd> keys.
|
||||
/// Labeled <kbd>\\</kbd> on a UK keyboard.
|
||||
IntlBackslash,
|
||||
/// Located between the <kbd>/</kbd> and right <kbd>Shift</kbd> keys.
|
||||
/// Labeled <kbd>\\</kbd> (ro) on a Japanese keyboard.
|
||||
IntlRo,
|
||||
/// Located between the <kbd>=</kbd> and <kbd>Backspace</kbd> keys.
|
||||
/// Labeled <kbd>¥</kbd> (yen) on a Japanese keyboard. <kbd>\\</kbd> on a
|
||||
/// Russian keyboard.
|
||||
IntlYen,
|
||||
/// <kbd>a</kbd> on a US keyboard.
|
||||
/// Labeled <kbd>q</kbd> on an AZERTY (e.g., French) keyboard.
|
||||
KeyA,
|
||||
/// <kbd>b</kbd> on a US keyboard.
|
||||
KeyB,
|
||||
/// <kbd>c</kbd> on a US keyboard.
|
||||
KeyC,
|
||||
/// <kbd>d</kbd> on a US keyboard.
|
||||
KeyD,
|
||||
/// <kbd>e</kbd> on a US keyboard.
|
||||
KeyE,
|
||||
/// <kbd>f</kbd> on a US keyboard.
|
||||
KeyF,
|
||||
/// <kbd>g</kbd> on a US keyboard.
|
||||
KeyG,
|
||||
/// <kbd>h</kbd> on a US keyboard.
|
||||
KeyH,
|
||||
/// <kbd>i</kbd> on a US keyboard.
|
||||
KeyI,
|
||||
/// <kbd>j</kbd> on a US keyboard.
|
||||
KeyJ,
|
||||
/// <kbd>k</kbd> on a US keyboard.
|
||||
KeyK,
|
||||
/// <kbd>l</kbd> on a US keyboard.
|
||||
KeyL,
|
||||
/// <kbd>m</kbd> on a US keyboard.
|
||||
KeyM,
|
||||
/// <kbd>n</kbd> on a US keyboard.
|
||||
KeyN,
|
||||
/// <kbd>o</kbd> on a US keyboard.
|
||||
KeyO,
|
||||
/// <kbd>p</kbd> on a US keyboard.
|
||||
KeyP,
|
||||
/// <kbd>q</kbd> on a US keyboard.
|
||||
/// Labeled <kbd>a</kbd> on an AZERTY (e.g., French) keyboard.
|
||||
KeyQ,
|
||||
/// <kbd>r</kbd> on a US keyboard.
|
||||
KeyR,
|
||||
/// <kbd>s</kbd> on a US keyboard.
|
||||
KeyS,
|
||||
/// <kbd>t</kbd> on a US keyboard.
|
||||
KeyT,
|
||||
/// <kbd>u</kbd> on a US keyboard.
|
||||
KeyU,
|
||||
/// <kbd>v</kbd> on a US keyboard.
|
||||
KeyV,
|
||||
/// <kbd>w</kbd> on a US keyboard.
|
||||
/// Labeled <kbd>z</kbd> on an AZERTY (e.g., French) keyboard.
|
||||
KeyW,
|
||||
/// <kbd>x</kbd> on a US keyboard.
|
||||
KeyX,
|
||||
/// <kbd>y</kbd> on a US keyboard.
|
||||
/// Labeled <kbd>z</kbd> on a QWERTZ (e.g., German) keyboard.
|
||||
KeyY,
|
||||
/// <kbd>z</kbd> on a US keyboard.
|
||||
/// Labeled <kbd>w</kbd> on an AZERTY (e.g., French) keyboard, and <kbd>y</kbd> on a
|
||||
/// QWERTZ (e.g., German) keyboard.
|
||||
KeyZ,
|
||||
/// <kbd>-</kbd> on a US keyboard.
|
||||
Minus,
|
||||
/// <kbd>.</kbd> on a US keyboard.
|
||||
Period,
|
||||
/// <kbd>'</kbd> on a US keyboard.
|
||||
Quote,
|
||||
/// <kbd>;</kbd> on a US keyboard.
|
||||
Semicolon,
|
||||
/// <kbd>/</kbd> on a US keyboard.
|
||||
Slash,
|
||||
/// <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>.
|
||||
AltLeft,
|
||||
/// <kbd>Alt</kbd>, <kbd>Option</kbd>, or <kbd>⌥</kbd>.
|
||||
/// This is labeled <kbd>AltGr</kbd> on many keyboard layouts.
|
||||
AltRight,
|
||||
/// <kbd>Backspace</kbd> or <kbd>⌫</kbd>.
|
||||
/// Labeled <kbd>Delete</kbd> on Apple keyboards.
|
||||
Backspace,
|
||||
/// <kbd>CapsLock</kbd> or <kbd>⇪</kbd>
|
||||
CapsLock,
|
||||
/// The application context menu key, which is typically found between the right
|
||||
/// <kbd>Super</kbd> key and the right <kbd>Control</kbd> key.
|
||||
ContextMenu,
|
||||
/// <kbd>Control</kbd> or <kbd>⌃</kbd>
|
||||
ControlLeft,
|
||||
/// <kbd>Control</kbd> or <kbd>⌃</kbd>
|
||||
ControlRight,
|
||||
/// <kbd>Enter</kbd> or <kbd>↵</kbd>. Labeled <kbd>Return</kbd> on Apple keyboards.
|
||||
Enter,
|
||||
/// The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key.
|
||||
SuperLeft,
|
||||
/// The Windows, <kbd>⌘</kbd>, <kbd>Command</kbd>, or other OS symbol key.
|
||||
SuperRight,
|
||||
/// <kbd>Shift</kbd> or <kbd>⇧</kbd>
|
||||
ShiftLeft,
|
||||
/// <kbd>Shift</kbd> or <kbd>⇧</kbd>
|
||||
ShiftRight,
|
||||
/// <kbd> </kbd> (space)
|
||||
Space,
|
||||
/// <kbd>Tab</kbd> or <kbd>⇥</kbd>
|
||||
Tab,
|
||||
/// Japanese: <kbd>変</kbd> (henkan)
|
||||
Convert,
|
||||
/// Japanese: <kbd>カタカナ</kbd>/<kbd>ひらがな</kbd>/<kbd>ローマ字</kbd>
|
||||
/// (katakana/hiragana/romaji)
|
||||
KanaMode,
|
||||
/// Korean: HangulMode <kbd>한/영</kbd> (han/yeong)
|
||||
///
|
||||
/// Japanese (Mac keyboard): <kbd>か</kbd> (kana)
|
||||
Lang1,
|
||||
/// Korean: Hanja <kbd>한</kbd> (hanja)
|
||||
///
|
||||
/// Japanese (Mac keyboard): <kbd>英</kbd> (eisu)
|
||||
Lang2,
|
||||
/// Japanese (word-processing keyboard): Katakana
|
||||
Lang3,
|
||||
/// Japanese (word-processing keyboard): Hiragana
|
||||
Lang4,
|
||||
/// Japanese (word-processing keyboard): Zenkaku/Hankaku
|
||||
Lang5,
|
||||
/// Japanese: <kbd>無変換</kbd> (muhenkan)
|
||||
NonConvert,
|
||||
/// <kbd>⌦</kbd>. The forward delete key.
|
||||
/// Note that on Apple keyboards, the key labelled <kbd>Delete</kbd> on the main part of
|
||||
/// the keyboard is encoded as [`Backspace`].
|
||||
///
|
||||
/// [`Backspace`]: Self::Backspace
|
||||
Delete,
|
||||
/// <kbd>Page Down</kbd>, <kbd>End</kbd>, or <kbd>↘</kbd>
|
||||
End,
|
||||
/// <kbd>Help</kbd>. Not present on standard PC keyboards.
|
||||
Help,
|
||||
/// <kbd>Home</kbd> or <kbd>↖</kbd>
|
||||
Home,
|
||||
/// <kbd>Insert</kbd> or <kbd>Ins</kbd>. Not present on Apple keyboards.
|
||||
Insert,
|
||||
/// <kbd>Page Down</kbd>, <kbd>PgDn</kbd>, or <kbd>⇟</kbd>
|
||||
PageDown,
|
||||
/// <kbd>Page Up</kbd>, <kbd>PgUp</kbd>, or <kbd>⇞</kbd>
|
||||
PageUp,
|
||||
/// <kbd>↓</kbd>
|
||||
ArrowDown,
|
||||
/// <kbd>←</kbd>
|
||||
ArrowLeft,
|
||||
/// <kbd>→</kbd>
|
||||
ArrowRight,
|
||||
/// <kbd>↑</kbd>
|
||||
ArrowUp,
|
||||
/// On the Mac, this is used for the numpad <kbd>Clear</kbd> key.
|
||||
NumLock,
|
||||
/// <kbd>0 Ins</kbd> on a keyboard. <kbd>0</kbd> on a phone or remote control
|
||||
Numpad0,
|
||||
/// <kbd>1 End</kbd> on a keyboard. <kbd>1</kbd> or <kbd>1 QZ</kbd> on a phone or remote
|
||||
/// control
|
||||
Numpad1,
|
||||
/// <kbd>2 ↓</kbd> on a keyboard. <kbd>2 ABC</kbd> on a phone or remote control
|
||||
Numpad2,
|
||||
/// <kbd>3 PgDn</kbd> on a keyboard. <kbd>3 DEF</kbd> on a phone or remote control
|
||||
Numpad3,
|
||||
/// <kbd>4 ←</kbd> on a keyboard. <kbd>4 GHI</kbd> on a phone or remote control
|
||||
Numpad4,
|
||||
/// <kbd>5</kbd> on a keyboard. <kbd>5 JKL</kbd> on a phone or remote control
|
||||
Numpad5,
|
||||
/// <kbd>6 →</kbd> on a keyboard. <kbd>6 MNO</kbd> on a phone or remote control
|
||||
Numpad6,
|
||||
/// <kbd>7 Home</kbd> on a keyboard. <kbd>7 PQRS</kbd> or <kbd>7 PRS</kbd> on a phone
|
||||
/// or remote control
|
||||
Numpad7,
|
||||
/// <kbd>8 ↑</kbd> on a keyboard. <kbd>8 TUV</kbd> on a phone or remote control
|
||||
Numpad8,
|
||||
/// <kbd>9 PgUp</kbd> on a keyboard. <kbd>9 WXYZ</kbd> or <kbd>9 WXY</kbd> on a phone
|
||||
/// or remote control
|
||||
Numpad9,
|
||||
/// <kbd>+</kbd>
|
||||
NumpadAdd,
|
||||
/// Found on the Microsoft Natural Keyboard.
|
||||
NumpadBackspace,
|
||||
/// <kbd>C</kbd> or <kbd>A</kbd> (All Clear). Also for use with numpads that have a
|
||||
/// <kbd>Clear</kbd> key that is separate from the <kbd>NumLock</kbd> key. On the Mac, the
|
||||
/// numpad <kbd>Clear</kbd> key is encoded as [`NumLock`].
|
||||
///
|
||||
/// [`NumLock`]: Self::NumLock
|
||||
NumpadClear,
|
||||
/// <kbd>C</kbd> (Clear Entry)
|
||||
NumpadClearEntry,
|
||||
/// <kbd>,</kbd> (thousands separator). For locales where the thousands separator
|
||||
/// is a "." (e.g., Brazil), this key may generate a <kbd>.</kbd>.
|
||||
NumpadComma,
|
||||
/// <kbd>. Del</kbd>. For locales where the decimal separator is "," (e.g.,
|
||||
/// Brazil), this key may generate a <kbd>,</kbd>.
|
||||
NumpadDecimal,
|
||||
/// <kbd>/</kbd>
|
||||
NumpadDivide,
|
||||
NumpadEnter,
|
||||
/// <kbd>=</kbd>
|
||||
NumpadEqual,
|
||||
/// <kbd>#</kbd> on a phone or remote control device. This key is typically found
|
||||
/// below the <kbd>9</kbd> key and to the right of the <kbd>0</kbd> key.
|
||||
NumpadHash,
|
||||
/// <kbd>M</kbd> Add current entry to the value stored in memory.
|
||||
NumpadMemoryAdd,
|
||||
/// <kbd>M</kbd> Clear the value stored in memory.
|
||||
NumpadMemoryClear,
|
||||
/// <kbd>M</kbd> Replace the current entry with the value stored in memory.
|
||||
NumpadMemoryRecall,
|
||||
/// <kbd>M</kbd> Replace the value stored in memory with the current entry.
|
||||
NumpadMemoryStore,
|
||||
/// <kbd>M</kbd> Subtract current entry from the value stored in memory.
|
||||
NumpadMemorySubtract,
|
||||
/// <kbd>*</kbd> on a keyboard. For use with numpads that provide mathematical
|
||||
/// operations (<kbd>+</kbd>, <kbd>-</kbd> <kbd>*</kbd> and <kbd>/</kbd>).
|
||||
///
|
||||
/// Use `NumpadStar` for the <kbd>*</kbd> key on phones and remote controls.
|
||||
NumpadMultiply,
|
||||
/// <kbd>(</kbd> Found on the Microsoft Natural Keyboard.
|
||||
NumpadParenLeft,
|
||||
/// <kbd>)</kbd> Found on the Microsoft Natural Keyboard.
|
||||
NumpadParenRight,
|
||||
/// <kbd>*</kbd> on a phone or remote control device.
|
||||
///
|
||||
/// This key is typically found below the <kbd>7</kbd> key and to the left of
|
||||
/// the <kbd>0</kbd> key.
|
||||
///
|
||||
/// Use <kbd>"NumpadMultiply"</kbd> for the <kbd>*</kbd> key on
|
||||
/// numeric keypads.
|
||||
NumpadStar,
|
||||
/// <kbd>-</kbd>
|
||||
NumpadSubtract,
|
||||
/// <kbd>Esc</kbd> or <kbd>⎋</kbd>
|
||||
Escape,
|
||||
/// <kbd>Fn</kbd> This is typically a hardware key that does not generate a separate code.
|
||||
Fn,
|
||||
/// <kbd>FLock</kbd> or <kbd>FnLock</kbd>. Function Lock key. Found on the Microsoft
|
||||
/// Natural Keyboard.
|
||||
FnLock,
|
||||
/// <kbd>PrtScr SysRq</kbd> or <kbd>Print Screen</kbd>
|
||||
PrintScreen,
|
||||
/// <kbd>Scroll Lock</kbd>
|
||||
ScrollLock,
|
||||
/// <kbd>Pause Break</kbd>
|
||||
Pause,
|
||||
/// Some laptops place this key to the left of the <kbd>↑</kbd> key.
|
||||
///
|
||||
/// This also the "back" button (triangle) on Android.
|
||||
BrowserBack,
|
||||
BrowserFavorites,
|
||||
/// Some laptops place this key to the right of the <kbd>↑</kbd> key.
|
||||
BrowserForward,
|
||||
/// The "home" button on Android.
|
||||
BrowserHome,
|
||||
BrowserRefresh,
|
||||
BrowserSearch,
|
||||
BrowserStop,
|
||||
/// <kbd>Eject</kbd> or <kbd>⏏</kbd>. This key is placed in the function section on some Apple
|
||||
/// keyboards.
|
||||
Eject,
|
||||
/// Sometimes labelled <kbd>My Computer</kbd> on the keyboard
|
||||
LaunchApp1,
|
||||
/// Sometimes labelled <kbd>Calculator</kbd> on the keyboard
|
||||
LaunchApp2,
|
||||
LaunchMail,
|
||||
MediaPlayPause,
|
||||
MediaSelect,
|
||||
MediaStop,
|
||||
MediaTrackNext,
|
||||
MediaTrackPrevious,
|
||||
/// This key is placed in the function section on some Apple keyboards, replacing the
|
||||
/// <kbd>Eject</kbd> key.
|
||||
Power,
|
||||
Sleep,
|
||||
AudioVolumeDown,
|
||||
AudioVolumeMute,
|
||||
AudioVolumeUp,
|
||||
WakeUp,
|
||||
// Legacy modifier key. Also called "Super" in certain places.
|
||||
Meta,
|
||||
// Legacy modifier key.
|
||||
Hyper,
|
||||
Turbo,
|
||||
Abort,
|
||||
Resume,
|
||||
Suspend,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Again,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Copy,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Cut,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Find,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Open,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Paste,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Props,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Select,
|
||||
/// Found on Sun’s USB keyboard.
|
||||
Undo,
|
||||
/// Use for dedicated <kbd>ひらがな</kbd> key found on some Japanese word processing keyboards.
|
||||
Hiragana,
|
||||
/// Use for dedicated <kbd>カタカナ</kbd> key found on some Japanese word processing keyboards.
|
||||
Katakana,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F1,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F2,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F3,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F4,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F5,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F6,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F7,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F8,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F9,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F10,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F11,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F12,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F13,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F14,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F15,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F16,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F17,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F18,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F19,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F20,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F21,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F22,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F23,
|
||||
/// General-purpose function key.
|
||||
/// Usually found at the top of the keyboard.
|
||||
F24,
|
||||
/// General-purpose function key.
|
||||
F25,
|
||||
/// General-purpose function key.
|
||||
F26,
|
||||
/// General-purpose function key.
|
||||
F27,
|
||||
/// General-purpose function key.
|
||||
F28,
|
||||
/// General-purpose function key.
|
||||
F29,
|
||||
/// General-purpose function key.
|
||||
F30,
|
||||
/// General-purpose function key.
|
||||
F31,
|
||||
/// General-purpose function key.
|
||||
F32,
|
||||
/// General-purpose function key.
|
||||
F33,
|
||||
/// General-purpose function key.
|
||||
F34,
|
||||
/// General-purpose function key.
|
||||
F35,
|
||||
}
|
||||
|
||||
/// Contains the platform-native physical key identifier.
|
||||
///
|
||||
/// The exact values vary from platform to platform (which is part of why this is a per-platform
|
||||
/// enum), but the values are primarily tied to the key's physical location on the keyboard.
|
||||
///
|
||||
/// This enum is primarily used to store raw keycodes when Winit doesn't map a given native
|
||||
/// physical key identifier to a meaningful [`Code`] variant. In the presence of identifiers we
|
||||
/// haven't mapped for you yet, this lets you use use [`Code`] to:
|
||||
///
|
||||
/// - Correctly match key press and release events.
|
||||
/// - On non-web platforms, support assigning keybinds to virtually any key through a UI.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum NativeCode {
|
||||
/// An unidentified code.
|
||||
Unidentified,
|
||||
/// An Android "scancode".
|
||||
Android(u32),
|
||||
/// A macOS "scancode".
|
||||
MacOS(u16),
|
||||
/// A Windows "scancode".
|
||||
Windows(u16),
|
||||
/// An XKB "keycode".
|
||||
Xkb(u32),
|
||||
}
|
||||
|
||||
/// Represents the location of a physical key.
|
||||
///
|
||||
/// This type is a superset of [`Code`], including an [`Unidentified`][Self::Unidentified]
|
||||
/// variant.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Physical {
|
||||
/// A known key code
|
||||
Code(Code),
|
||||
/// This variant is used when the key cannot be translated to a [`Code`]
|
||||
///
|
||||
/// The native keycode is provided (if available) so you're able to more reliably match
|
||||
/// key-press and key-release events by hashing the [`Physical`] key. It is also possible to use
|
||||
/// this for keybinds for non-standard keys, but such keybinds are tied to a given platform.
|
||||
Unidentified(NativeCode),
|
||||
}
|
||||
|
||||
impl PartialEq<Code> for Physical {
|
||||
#[inline]
|
||||
fn eq(&self, rhs: &Code) -> bool {
|
||||
match self {
|
||||
Physical::Code(code) => code == rhs,
|
||||
Physical::Unidentified(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Physical> for Code {
|
||||
#[inline]
|
||||
fn eq(&self, rhs: &Physical) -> bool {
|
||||
rhs == self
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<NativeCode> for Physical {
|
||||
#[inline]
|
||||
fn eq(&self, rhs: &NativeCode) -> bool {
|
||||
match self {
|
||||
Physical::Unidentified(code) => code == rhs,
|
||||
Physical::Code(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Physical> for NativeCode {
|
||||
#[inline]
|
||||
fn eq(&self, rhs: &Physical) -> bool {
|
||||
rhs == self
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ impl Modifiers {
|
|||
/// This is normally the main modifier to be used for hotkeys.
|
||||
///
|
||||
/// On macOS, this is equivalent to `Self::LOGO`.
|
||||
/// Ohterwise, this is equivalent to `Self::CTRL`.
|
||||
/// Otherwise, this is equivalent to `Self::CTRL`.
|
||||
pub const COMMAND: Self = if cfg!(target_os = "macos") {
|
||||
Self::LOGO
|
||||
} else {
|
||||
|
|
@ -84,4 +84,28 @@ impl Modifiers {
|
|||
|
||||
is_pressed
|
||||
}
|
||||
|
||||
/// Returns true if the "jump key" is pressed in the [`Modifiers`].
|
||||
///
|
||||
/// The "jump key" is the modifier key used to widen text motions. It is the `Alt`
|
||||
/// key in macOS and the `Ctrl` key in other platforms.
|
||||
pub fn jump(self) -> bool {
|
||||
if cfg!(target_os = "macos") {
|
||||
self.alt()
|
||||
} else {
|
||||
self.control()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the "command key" is pressed on a macOS device.
|
||||
///
|
||||
/// This is relevant for macOS-specific actions (e.g. `⌘ + ArrowLeft` moves the cursor
|
||||
/// to the beginning of the line).
|
||||
pub fn macos_command(self) -> bool {
|
||||
if cfg!(target_os = "macos") {
|
||||
self.logo()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,10 +79,11 @@ where
|
|||
let max_cross = axis.cross(limits.max());
|
||||
|
||||
let mut fill_main_sum = 0;
|
||||
let mut cross = match axis {
|
||||
Axis::Vertical if width == Length::Shrink => 0.0,
|
||||
Axis::Horizontal if height == Length::Shrink => 0.0,
|
||||
_ => max_cross,
|
||||
let mut some_fill_cross = false;
|
||||
let (mut cross, cross_compress) = match axis {
|
||||
Axis::Vertical if width == Length::Shrink => (0.0, true),
|
||||
Axis::Horizontal if height == Length::Shrink => (0.0, true),
|
||||
_ => (max_cross, false),
|
||||
};
|
||||
|
||||
let mut available = axis.main(limits.max()) - total_spacing;
|
||||
|
|
@ -90,6 +91,10 @@ where
|
|||
let mut nodes: Vec<Node> = Vec::with_capacity(items.len());
|
||||
nodes.resize(items.len(), Node::default());
|
||||
|
||||
// FIRST PASS
|
||||
// We lay out non-fluid elements in the main axis.
|
||||
// If we need to compress the cross axis, then we skip any of these elements
|
||||
// that are also fluid in the cross axis.
|
||||
for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() {
|
||||
let (fill_main_factor, fill_cross_factor) = {
|
||||
let size = child.as_widget().size();
|
||||
|
|
@ -97,7 +102,8 @@ where
|
|||
axis.pack(size.width.fill_factor(), size.height.fill_factor())
|
||||
};
|
||||
|
||||
if fill_main_factor == 0 {
|
||||
if fill_main_factor == 0 && (!cross_compress || fill_cross_factor == 0)
|
||||
{
|
||||
let (max_width, max_height) = axis.pack(
|
||||
available,
|
||||
if fill_cross_factor == 0 {
|
||||
|
|
@ -120,6 +126,41 @@ where
|
|||
nodes[i] = layout;
|
||||
} else {
|
||||
fill_main_sum += fill_main_factor;
|
||||
some_fill_cross = some_fill_cross || fill_cross_factor != 0;
|
||||
}
|
||||
}
|
||||
|
||||
// SECOND PASS (conditional)
|
||||
// If we must compress the cross axis and there are fluid elements in the
|
||||
// cross axis, we lay out any of these elements that are also non-fluid in
|
||||
// the main axis (i.e. the ones we deliberately skipped in the first pass).
|
||||
//
|
||||
// We use the maximum cross length obtained in the first pass as the maximum
|
||||
// cross limit.
|
||||
if cross_compress && some_fill_cross {
|
||||
for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate()
|
||||
{
|
||||
let (fill_main_factor, fill_cross_factor) = {
|
||||
let size = child.as_widget().size();
|
||||
|
||||
axis.pack(size.width.fill_factor(), size.height.fill_factor())
|
||||
};
|
||||
|
||||
if fill_main_factor == 0 && fill_cross_factor != 0 {
|
||||
let (max_width, max_height) = axis.pack(available, cross);
|
||||
|
||||
let child_limits =
|
||||
Limits::new(Size::ZERO, Size::new(max_width, max_height));
|
||||
|
||||
let layout =
|
||||
child.as_widget().layout(tree, renderer, &child_limits);
|
||||
let size = layout.size();
|
||||
|
||||
available -= axis.main(size);
|
||||
cross = cross.max(axis.cross(size));
|
||||
|
||||
nodes[i] = layout;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -134,6 +175,9 @@ where
|
|||
},
|
||||
};
|
||||
|
||||
// THIRD PASS
|
||||
// We only have the elements that are fluid in the main axis left.
|
||||
// We use the remaining space to evenly allocate space based on fill factors.
|
||||
for (i, (child, tree)) in items.iter().zip(trees).enumerate() {
|
||||
let (fill_main_factor, fill_cross_factor) = {
|
||||
let size = child.as_widget().size();
|
||||
|
|
@ -145,6 +189,12 @@ where
|
|||
let max_main =
|
||||
remaining * fill_main_factor as f32 / fill_main_sum as f32;
|
||||
|
||||
let max_main = if max_main.is_nan() {
|
||||
f32::INFINITY
|
||||
} else {
|
||||
max_main
|
||||
};
|
||||
|
||||
let min_main = if max_main.is_infinite() {
|
||||
0.0
|
||||
} else {
|
||||
|
|
@ -177,6 +227,8 @@ where
|
|||
let pad = axis.pack(padding.left, padding.top);
|
||||
let mut main = pad.0;
|
||||
|
||||
// FOURTH PASS
|
||||
// We align all the laid out nodes in the cross axis, if needed.
|
||||
for (i, node) in nodes.iter_mut().enumerate() {
|
||||
if i > 0 {
|
||||
main += spacing;
|
||||
|
|
|
|||
|
|
@ -103,12 +103,13 @@ impl Node {
|
|||
}
|
||||
|
||||
/// Translates the [`Node`] by the given translation.
|
||||
pub fn translate(self, translation: impl Into<Vector>) -> Self {
|
||||
let translation = translation.into();
|
||||
|
||||
Self {
|
||||
bounds: self.bounds + translation,
|
||||
..self
|
||||
pub fn translate(mut self, translation: impl Into<Vector>) -> Self {
|
||||
self.translate_mut(translation);
|
||||
self
|
||||
}
|
||||
|
||||
/// Translates the [`Node`] by the given translation.
|
||||
pub fn translate_mut(&mut self, translation: impl Into<Vector>) {
|
||||
self.bounds = self.bounds + translation.into();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,8 +77,8 @@ impl From<f32> for Length {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Length {
|
||||
fn from(units: u16) -> Self {
|
||||
Length::Fixed(f32::from(units))
|
||||
impl From<u32> for Length {
|
||||
fn from(units: u32) -> Self {
|
||||
Length::Fixed(units as f32)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,16 +10,19 @@
|
|||
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
|
||||
)]
|
||||
pub mod alignment;
|
||||
pub mod animation;
|
||||
pub mod border;
|
||||
pub mod clipboard;
|
||||
pub mod event;
|
||||
pub mod font;
|
||||
pub mod gradient;
|
||||
pub mod image;
|
||||
pub mod input_method;
|
||||
pub mod keyboard;
|
||||
pub mod layout;
|
||||
pub mod mouse;
|
||||
pub mod overlay;
|
||||
pub mod padding;
|
||||
pub mod renderer;
|
||||
pub mod svg;
|
||||
pub mod text;
|
||||
|
|
@ -35,11 +38,11 @@ mod color;
|
|||
mod content_fit;
|
||||
mod element;
|
||||
mod length;
|
||||
mod padding;
|
||||
mod pixels;
|
||||
mod point;
|
||||
mod rectangle;
|
||||
mod rotation;
|
||||
mod settings;
|
||||
mod shadow;
|
||||
mod shell;
|
||||
mod size;
|
||||
|
|
@ -48,6 +51,7 @@ mod vector;
|
|||
|
||||
pub use alignment::Alignment;
|
||||
pub use angle::{Degrees, Radians};
|
||||
pub use animation::Animation;
|
||||
pub use background::Background;
|
||||
pub use border::Border;
|
||||
pub use clipboard::Clipboard;
|
||||
|
|
@ -57,6 +61,8 @@ pub use element::Element;
|
|||
pub use event::Event;
|
||||
pub use font::Font;
|
||||
pub use gradient::Gradient;
|
||||
pub use image::Image;
|
||||
pub use input_method::InputMethod;
|
||||
pub use layout::Layout;
|
||||
pub use length::Length;
|
||||
pub use overlay::Overlay;
|
||||
|
|
@ -66,9 +72,11 @@ pub use point::Point;
|
|||
pub use rectangle::Rectangle;
|
||||
pub use renderer::Renderer;
|
||||
pub use rotation::Rotation;
|
||||
pub use settings::Settings;
|
||||
pub use shadow::Shadow;
|
||||
pub use shell::Shell;
|
||||
pub use size::Size;
|
||||
pub use svg::Svg;
|
||||
pub use text::Text;
|
||||
pub use theme::Theme;
|
||||
pub use transformation::Transformation;
|
||||
|
|
@ -76,3 +84,69 @@ pub use vector::Vector;
|
|||
pub use widget::Widget;
|
||||
|
||||
pub use smol_str::SmolStr;
|
||||
|
||||
/// A function that can _never_ be called.
|
||||
///
|
||||
/// This is useful to turn generic types into anything
|
||||
/// you want by coercing them into a type with no possible
|
||||
/// values.
|
||||
pub fn never<T>(never: std::convert::Infallible) -> T {
|
||||
match never {}
|
||||
}
|
||||
|
||||
/// A trait extension for binary functions (`Fn(A, B) -> O`).
|
||||
///
|
||||
/// It enables you to use a bunch of nifty functional programming paradigms
|
||||
/// that work well with iced.
|
||||
pub trait Function<A, B, O> {
|
||||
/// Applies the given first argument to a binary function and returns
|
||||
/// a new function that takes the other argument.
|
||||
///
|
||||
/// This lets you partially "apply" a function—equivalent to currying,
|
||||
/// but it only works with binary functions. If you want to apply an
|
||||
/// arbitrary number of arguments, create a little struct for them.
|
||||
///
|
||||
/// # When is this useful?
|
||||
/// Sometimes you will want to identify the source or target
|
||||
/// of some message in your user interface. This can be achieved through
|
||||
/// normal means by defining a closure and moving the identifier
|
||||
/// inside:
|
||||
///
|
||||
/// ```rust
|
||||
/// # let element: Option<()> = Some(());
|
||||
/// # enum Message { ButtonPressed(u32, ()) }
|
||||
/// let id = 123;
|
||||
///
|
||||
/// # let _ = {
|
||||
/// element.map(move |result| Message::ButtonPressed(id, result))
|
||||
/// # };
|
||||
/// ```
|
||||
///
|
||||
/// That's quite a mouthful. [`with`](Self::with) lets you write:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use iced_core::Function;
|
||||
/// # let element: Option<()> = Some(());
|
||||
/// # enum Message { ButtonPressed(u32, ()) }
|
||||
/// let id = 123;
|
||||
///
|
||||
/// # let _ = {
|
||||
/// element.map(Message::ButtonPressed.with(id))
|
||||
/// # };
|
||||
/// ```
|
||||
///
|
||||
/// Effectively creating the same closure that partially applies
|
||||
/// the `id` to the message—but much more concise!
|
||||
fn with(self, prefix: A) -> impl Fn(B) -> O;
|
||||
}
|
||||
|
||||
impl<F, A, B, O> Function<A, B, O> for F
|
||||
where
|
||||
F: Fn(A, B) -> O,
|
||||
Self: Sized,
|
||||
A: Clone,
|
||||
{
|
||||
fn with(self, prefix: A) -> impl Fn(B) -> O {
|
||||
move |result| self(prefix.clone(), result)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
//! Track mouse clicks.
|
||||
use crate::mouse::Button;
|
||||
use crate::time::Instant;
|
||||
use crate::Point;
|
||||
use crate::{Point, Transformation};
|
||||
|
||||
use std::ops::Mul;
|
||||
|
||||
/// A mouse click.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Click {
|
||||
kind: Kind,
|
||||
button: Button,
|
||||
position: Point,
|
||||
time: Instant,
|
||||
}
|
||||
|
||||
/// The kind of mouse click.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Kind {
|
||||
/// A single click
|
||||
Single,
|
||||
|
|
@ -36,11 +40,17 @@ impl Kind {
|
|||
impl Click {
|
||||
/// Creates a new [`Click`] with the given position and previous last
|
||||
/// [`Click`].
|
||||
pub fn new(position: Point, previous: Option<Click>) -> Click {
|
||||
pub fn new(
|
||||
position: Point,
|
||||
button: Button,
|
||||
previous: Option<Click>,
|
||||
) -> Click {
|
||||
let time = Instant::now();
|
||||
|
||||
let kind = if let Some(previous) = previous {
|
||||
if previous.is_consecutive(position, time) {
|
||||
if previous.is_consecutive(position, time)
|
||||
&& button == previous.button
|
||||
{
|
||||
previous.kind.next()
|
||||
} else {
|
||||
Kind::Single
|
||||
|
|
@ -51,6 +61,7 @@ impl Click {
|
|||
|
||||
Click {
|
||||
kind,
|
||||
button,
|
||||
position,
|
||||
time,
|
||||
}
|
||||
|
|
@ -73,9 +84,22 @@ impl Click {
|
|||
None
|
||||
};
|
||||
|
||||
self.position == new_position
|
||||
self.position.distance(new_position) < 6.0
|
||||
&& duration
|
||||
.map(|duration| duration.as_millis() <= 300)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Transformation> for Click {
|
||||
type Output = Click;
|
||||
|
||||
fn mul(self, transformation: Transformation) -> Click {
|
||||
Click {
|
||||
kind: self.kind,
|
||||
button: self.button,
|
||||
position: self.position * transformation,
|
||||
time: self.time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{Point, Rectangle, Vector};
|
||||
use crate::{Point, Rectangle, Transformation, Vector};
|
||||
|
||||
/// The mouse cursor state.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
|
|
@ -6,6 +6,9 @@ pub enum Cursor {
|
|||
/// The cursor has a defined position.
|
||||
Available(Point),
|
||||
|
||||
/// The cursor has a defined position, but it's levitating over a layer above.
|
||||
Levitating(Point),
|
||||
|
||||
/// The cursor is currently unavailable (i.e. out of bounds or busy).
|
||||
#[default]
|
||||
Unavailable,
|
||||
|
|
@ -16,7 +19,7 @@ impl Cursor {
|
|||
pub fn position(self) -> Option<Point> {
|
||||
match self {
|
||||
Cursor::Available(position) => Some(position),
|
||||
Cursor::Unavailable => None,
|
||||
Cursor::Levitating(_) | Cursor::Unavailable => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -49,4 +52,41 @@ impl Cursor {
|
|||
pub fn is_over(self, bounds: Rectangle) -> bool {
|
||||
self.position_over(bounds).is_some()
|
||||
}
|
||||
|
||||
/// Returns true if the [`Cursor`] is levitating over a layer above.
|
||||
pub fn is_levitating(self) -> bool {
|
||||
matches!(self, Self::Levitating(_))
|
||||
}
|
||||
|
||||
/// Makes the [`Cursor`] levitate over a layer above.
|
||||
pub fn levitate(self) -> Self {
|
||||
match self {
|
||||
Self::Available(position) => Self::Levitating(position),
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
|
||||
/// Brings the [`Cursor`] back to the current layer.
|
||||
pub fn land(self) -> Self {
|
||||
match self {
|
||||
Cursor::Levitating(position) => Cursor::Available(position),
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Transformation> for Cursor {
|
||||
type Output = Self;
|
||||
|
||||
fn mul(self, transformation: Transformation) -> Self {
|
||||
match self {
|
||||
Self::Available(position) => {
|
||||
Self::Available(position * transformation)
|
||||
}
|
||||
Self::Levitating(position) => {
|
||||
Self::Levitating(position * transformation)
|
||||
}
|
||||
Self::Unavailable => Self::Unavailable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,13 @@ pub enum Interaction {
|
|||
Grabbing,
|
||||
ResizingHorizontally,
|
||||
ResizingVertically,
|
||||
ResizingDiagonallyUp,
|
||||
ResizingDiagonallyDown,
|
||||
NotAllowed,
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
Cell,
|
||||
Move,
|
||||
Copy,
|
||||
Help,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,12 @@ mod group;
|
|||
pub use element::Element;
|
||||
pub use group::Group;
|
||||
|
||||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::renderer;
|
||||
use crate::widget;
|
||||
use crate::widget::Tree;
|
||||
use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector};
|
||||
use crate::{Clipboard, Event, Layout, Point, Rectangle, Shell, Size, Vector};
|
||||
|
||||
/// An interactive component that can be displayed on top of other widgets.
|
||||
pub trait Overlay<Message, Theme, Renderer>
|
||||
|
|
@ -41,7 +40,7 @@ where
|
|||
&mut self,
|
||||
_layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
_operation: &mut dyn widget::Operation<Message>,
|
||||
_operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -57,16 +56,15 @@ where
|
|||
/// * a [`Clipboard`], if available
|
||||
///
|
||||
/// By default, it does nothing.
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
_event: Event,
|
||||
_event: &Event,
|
||||
_layout: Layout<'_>,
|
||||
_cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
_shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
event::Status::Ignored
|
||||
) {
|
||||
}
|
||||
|
||||
/// Returns the current [`mouse::Interaction`] of the [`Overlay`].
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
pub use crate::Overlay;
|
||||
|
||||
use crate::event::{self, Event};
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::renderer;
|
||||
use crate::widget;
|
||||
use crate::{Clipboard, Layout, Point, Rectangle, Shell, Size, Vector};
|
||||
|
||||
use std::any::Any;
|
||||
use crate::{Clipboard, Event, Layout, Point, Rectangle, Shell, Size};
|
||||
|
||||
/// A generic [`Overlay`].
|
||||
#[allow(missing_debug_implementations)]
|
||||
|
|
@ -52,17 +49,17 @@ where
|
|||
}
|
||||
|
||||
/// Processes a runtime [`Event`].
|
||||
pub fn on_event(
|
||||
pub fn update(
|
||||
&mut self,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
) {
|
||||
self.overlay
|
||||
.on_event(event, layout, cursor, renderer, clipboard, shell)
|
||||
.update(event, layout, cursor, renderer, clipboard, shell);
|
||||
}
|
||||
|
||||
/// Returns the current [`mouse::Interaction`] of the [`Element`].
|
||||
|
|
@ -94,7 +91,7 @@ where
|
|||
&mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.overlay.operate(layout, renderer, operation);
|
||||
}
|
||||
|
|
@ -133,8 +130,8 @@ impl<'a, A, B, Theme, Renderer> Map<'a, A, B, Theme, Renderer> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, A, B, Theme, Renderer> Overlay<B, Theme, Renderer>
|
||||
for Map<'a, A, B, Theme, Renderer>
|
||||
impl<A, B, Theme, Renderer> Overlay<B, Theme, Renderer>
|
||||
for Map<'_, A, B, Theme, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
|
|
@ -146,74 +143,24 @@ where
|
|||
&mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<B>,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
struct MapOperation<'a, B> {
|
||||
operation: &'a mut dyn widget::Operation<B>,
|
||||
self.content.operate(layout, renderer, operation);
|
||||
}
|
||||
|
||||
impl<'a, T, B> widget::Operation<T> for MapOperation<'a, B> {
|
||||
fn container(
|
||||
fn update(
|
||||
&mut self,
|
||||
id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
operate_on_children: &mut dyn FnMut(
|
||||
&mut dyn widget::Operation<T>,
|
||||
),
|
||||
) {
|
||||
self.operation.container(id, bounds, &mut |operation| {
|
||||
operate_on_children(&mut MapOperation { operation });
|
||||
});
|
||||
}
|
||||
|
||||
fn focusable(
|
||||
&mut self,
|
||||
state: &mut dyn widget::operation::Focusable,
|
||||
id: Option<&widget::Id>,
|
||||
) {
|
||||
self.operation.focusable(state, id);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn widget::operation::Scrollable,
|
||||
id: Option<&widget::Id>,
|
||||
bounds: Rectangle,
|
||||
translation: Vector,
|
||||
) {
|
||||
self.operation.scrollable(state, id, bounds, translation);
|
||||
}
|
||||
|
||||
fn text_input(
|
||||
&mut self,
|
||||
state: &mut dyn widget::operation::TextInput,
|
||||
id: Option<&widget::Id>,
|
||||
) {
|
||||
self.operation.text_input(state, id);
|
||||
}
|
||||
|
||||
fn custom(&mut self, state: &mut dyn Any, id: Option<&widget::Id>) {
|
||||
self.operation.custom(state, id);
|
||||
}
|
||||
}
|
||||
|
||||
self.content
|
||||
.operate(layout, renderer, &mut MapOperation { operation });
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, B>,
|
||||
) -> event::Status {
|
||||
) {
|
||||
let mut local_messages = Vec::new();
|
||||
let mut local_shell = Shell::new(&mut local_messages);
|
||||
|
||||
let event_status = self.content.on_event(
|
||||
self.content.update(
|
||||
event,
|
||||
layout,
|
||||
cursor,
|
||||
|
|
@ -223,8 +170,6 @@ where
|
|||
);
|
||||
|
||||
shell.merge(local_shell, self.mapper);
|
||||
|
||||
event_status
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
|
|
@ -258,11 +203,11 @@ where
|
|||
self.content.is_over(layout, renderer, cursor_position)
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
fn overlay<'a>(
|
||||
&'a mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<Element<'b, B, Theme, Renderer>> {
|
||||
) -> Option<Element<'a, B, Theme, Renderer>> {
|
||||
self.content
|
||||
.overlay(layout, renderer)
|
||||
.map(|overlay| overlay.map(self.mapper))
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use crate::event;
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::overlay;
|
||||
|
|
@ -58,8 +57,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Overlay<Message, Theme, Renderer>
|
||||
for Group<'a, Message, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Overlay<Message, Theme, Renderer>
|
||||
for Group<'_, Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
{
|
||||
|
|
@ -73,29 +72,18 @@ where
|
|||
)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
renderer: &Renderer,
|
||||
clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
) -> event::Status {
|
||||
self.children
|
||||
.iter_mut()
|
||||
.zip(layout.children())
|
||||
.map(|(child, layout)| {
|
||||
child.on_event(
|
||||
event.clone(),
|
||||
layout,
|
||||
cursor,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
)
|
||||
})
|
||||
.fold(event::Status::Ignored, event::Status::merge)
|
||||
) {
|
||||
for (child, layout) in self.children.iter_mut().zip(layout.children()) {
|
||||
child.update(event, layout, cursor, renderer, clipboard, shell);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
|
|
@ -132,7 +120,7 @@ where
|
|||
&mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
operation: &mut dyn widget::Operation<Message>,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
operation.container(None, layout.bounds(), &mut |operation| {
|
||||
self.children.iter_mut().zip(layout.children()).for_each(
|
||||
|
|
@ -157,11 +145,11 @@ where
|
|||
})
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
fn overlay<'a>(
|
||||
&'a mut self,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
) -> Option<overlay::Element<'a, Message, Theme, Renderer>> {
|
||||
let children = self
|
||||
.children
|
||||
.iter_mut()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use crate::Size;
|
||||
//! Space stuff around the perimeter.
|
||||
use crate::{Pixels, Size};
|
||||
|
||||
/// An amount of space to pad for each side of a box
|
||||
///
|
||||
|
|
@ -9,7 +10,6 @@ use crate::Size;
|
|||
/// #
|
||||
/// let padding = Padding::from(20); // 20px on all sides
|
||||
/// let padding = Padding::from([10, 20]); // top/bottom, left/right
|
||||
/// let padding = Padding::from([5, 10, 15, 20]); // top, right, bottom, left
|
||||
/// ```
|
||||
///
|
||||
/// Normally, the `padding` method of a widget will ask for an `Into<Padding>`,
|
||||
|
|
@ -31,9 +31,8 @@ use crate::Size;
|
|||
///
|
||||
/// let widget = Widget::new().padding(20); // 20px on all sides
|
||||
/// let widget = Widget::new().padding([10, 20]); // top/bottom, left/right
|
||||
/// let widget = Widget::new().padding([5, 10, 15, 20]); // top, right, bottom, left
|
||||
/// ```
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Default)]
|
||||
pub struct Padding {
|
||||
/// Top padding
|
||||
pub top: f32,
|
||||
|
|
@ -45,6 +44,31 @@ pub struct Padding {
|
|||
pub left: f32,
|
||||
}
|
||||
|
||||
/// Create a [`Padding`] that is equal on all sides.
|
||||
pub fn all(padding: impl Into<Pixels>) -> Padding {
|
||||
Padding::new(padding.into().0)
|
||||
}
|
||||
|
||||
/// Create some top [`Padding`].
|
||||
pub fn top(padding: impl Into<Pixels>) -> Padding {
|
||||
Padding::default().top(padding)
|
||||
}
|
||||
|
||||
/// Create some bottom [`Padding`].
|
||||
pub fn bottom(padding: impl Into<Pixels>) -> Padding {
|
||||
Padding::default().bottom(padding)
|
||||
}
|
||||
|
||||
/// Create some left [`Padding`].
|
||||
pub fn left(padding: impl Into<Pixels>) -> Padding {
|
||||
Padding::default().left(padding)
|
||||
}
|
||||
|
||||
/// Create some right [`Padding`].
|
||||
pub fn right(padding: impl Into<Pixels>) -> Padding {
|
||||
Padding::default().right(padding)
|
||||
}
|
||||
|
||||
impl Padding {
|
||||
/// Padding of zero
|
||||
pub const ZERO: Padding = Padding {
|
||||
|
|
@ -54,7 +78,7 @@ impl Padding {
|
|||
left: 0.0,
|
||||
};
|
||||
|
||||
/// Create a Padding that is equal on all sides
|
||||
/// Create a [`Padding`] that is equal on all sides.
|
||||
pub const fn new(padding: f32) -> Padding {
|
||||
Padding {
|
||||
top: padding,
|
||||
|
|
@ -64,6 +88,46 @@ impl Padding {
|
|||
}
|
||||
}
|
||||
|
||||
/// Sets the [`top`] of the [`Padding`].
|
||||
///
|
||||
/// [`top`]: Self::top
|
||||
pub fn top(self, top: impl Into<Pixels>) -> Self {
|
||||
Self {
|
||||
top: top.into().0,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the [`bottom`] of the [`Padding`].
|
||||
///
|
||||
/// [`bottom`]: Self::bottom
|
||||
pub fn bottom(self, bottom: impl Into<Pixels>) -> Self {
|
||||
Self {
|
||||
bottom: bottom.into().0,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the [`left`] of the [`Padding`].
|
||||
///
|
||||
/// [`left`]: Self::left
|
||||
pub fn left(self, left: impl Into<Pixels>) -> Self {
|
||||
Self {
|
||||
left: left.into().0,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the [`right`] of the [`Padding`].
|
||||
///
|
||||
/// [`right`]: Self::right
|
||||
pub fn right(self, right: impl Into<Pixels>) -> Self {
|
||||
Self {
|
||||
right: right.into().0,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the total amount of vertical [`Padding`].
|
||||
pub fn vertical(self) -> f32 {
|
||||
self.top + self.bottom
|
||||
|
|
@ -111,17 +175,6 @@ impl From<[u16; 2]> for Padding {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<[u16; 4]> for Padding {
|
||||
fn from(p: [u16; 4]) -> Self {
|
||||
Padding {
|
||||
top: f32::from(p[0]),
|
||||
right: f32::from(p[1]),
|
||||
bottom: f32::from(p[2]),
|
||||
left: f32::from(p[3]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f32> for Padding {
|
||||
fn from(p: f32) -> Self {
|
||||
Padding {
|
||||
|
|
@ -144,19 +197,14 @@ impl From<[f32; 2]> for Padding {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<[f32; 4]> for Padding {
|
||||
fn from(p: [f32; 4]) -> Self {
|
||||
Padding {
|
||||
top: p[0],
|
||||
right: p[1],
|
||||
bottom: p[2],
|
||||
left: p[3],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Padding> for Size {
|
||||
fn from(padding: Padding) -> Self {
|
||||
Self::new(padding.horizontal(), padding.vertical())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pixels> for Padding {
|
||||
fn from(pixels: Pixels) -> Self {
|
||||
Self::from(pixels.0)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,18 +6,23 @@
|
|||
/// (e.g. `impl Into<Pixels>`) and, since `Pixels` implements `From` both for
|
||||
/// `f32` and `u16`, you should be able to provide both integers and float
|
||||
/// literals as needed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Default)]
|
||||
pub struct Pixels(pub f32);
|
||||
|
||||
impl Pixels {
|
||||
/// Zero pixels.
|
||||
pub const ZERO: Self = Self(0.0);
|
||||
}
|
||||
|
||||
impl From<f32> for Pixels {
|
||||
fn from(amount: f32) -> Self {
|
||||
Self(amount)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Pixels {
|
||||
fn from(amount: u16) -> Self {
|
||||
Self(f32::from(amount))
|
||||
impl From<u32> for Pixels {
|
||||
fn from(amount: u32) -> Self {
|
||||
Self(amount as f32)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -27,6 +32,30 @@ impl From<Pixels> for f32 {
|
|||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for Pixels {
|
||||
type Output = Pixels;
|
||||
|
||||
fn add(self, rhs: Self) -> Self {
|
||||
Pixels(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add<f32> for Pixels {
|
||||
type Output = Pixels;
|
||||
|
||||
fn add(self, rhs: f32) -> Self {
|
||||
Pixels(self.0 + rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul for Pixels {
|
||||
type Output = Pixels;
|
||||
|
||||
fn mul(self, rhs: Self) -> Self {
|
||||
Pixels(self.0 * rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<f32> for Pixels {
|
||||
type Output = Pixels;
|
||||
|
||||
|
|
@ -34,3 +63,27 @@ impl std::ops::Mul<f32> for Pixels {
|
|||
Pixels(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Div for Pixels {
|
||||
type Output = Pixels;
|
||||
|
||||
fn div(self, rhs: Self) -> Self {
|
||||
Pixels(self.0 / rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Div<f32> for Pixels {
|
||||
type Output = Pixels;
|
||||
|
||||
fn div(self, rhs: f32) -> Self {
|
||||
Pixels(self.0 / rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Div<u32> for Pixels {
|
||||
type Output = Pixels;
|
||||
|
||||
fn div(self, rhs: u32) -> Self {
|
||||
Pixels(self.0 / rhs as f32)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{Point, Radians, Size, Vector};
|
||||
use crate::{Padding, Point, Radians, Size, Vector};
|
||||
|
||||
/// An axis-aligned rectangle.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
|
|
@ -47,6 +47,62 @@ impl Rectangle<f32> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Creates a new square [`Rectangle`] with the center at the origin and
|
||||
/// with the given radius.
|
||||
pub fn with_radius(radius: f32) -> Self {
|
||||
Self {
|
||||
x: -radius,
|
||||
y: -radius,
|
||||
width: radius * 2.0,
|
||||
height: radius * 2.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new axis-aligned [`Rectangle`] from the given vertices; returning the
|
||||
/// rotation in [`Radians`] that must be applied to the axis-aligned [`Rectangle`]
|
||||
/// to obtain the desired result.
|
||||
pub fn with_vertices(
|
||||
top_left: Point,
|
||||
top_right: Point,
|
||||
bottom_left: Point,
|
||||
) -> (Rectangle, Radians) {
|
||||
let width = (top_right.x - top_left.x).hypot(top_right.y - top_left.y);
|
||||
|
||||
let height =
|
||||
(bottom_left.x - top_left.x).hypot(bottom_left.y - top_left.y);
|
||||
|
||||
let rotation =
|
||||
(top_right.y - top_left.y).atan2(top_right.x - top_left.x);
|
||||
|
||||
let rotation = if rotation < 0.0 {
|
||||
2.0 * std::f32::consts::PI + rotation
|
||||
} else {
|
||||
rotation
|
||||
};
|
||||
|
||||
let position = {
|
||||
let center = Point::new(
|
||||
(top_right.x + bottom_left.x) / 2.0,
|
||||
(top_right.y + bottom_left.y) / 2.0,
|
||||
);
|
||||
|
||||
let rotation = -rotation - std::f32::consts::PI * 2.0;
|
||||
|
||||
Point::new(
|
||||
center.x + (top_left.x - center.x) * rotation.cos()
|
||||
- (top_left.y - center.y) * rotation.sin(),
|
||||
center.y
|
||||
+ (top_left.x - center.x) * rotation.sin()
|
||||
+ (top_left.y - center.y) * rotation.cos(),
|
||||
)
|
||||
};
|
||||
|
||||
(
|
||||
Rectangle::new(position, Size::new(width, height)),
|
||||
Radians(rotation),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the [`Point`] at the center of the [`Rectangle`].
|
||||
pub fn center(&self) -> Point {
|
||||
Point::new(self.center_x(), self.center_y())
|
||||
|
|
@ -87,6 +143,20 @@ impl Rectangle<f32> {
|
|||
&& point.y < self.y + self.height
|
||||
}
|
||||
|
||||
/// Returns the minimum distance from the given [`Point`] to any of the edges
|
||||
/// of the [`Rectangle`].
|
||||
pub fn distance(&self, point: Point) -> f32 {
|
||||
let center = self.center();
|
||||
|
||||
let distance_x =
|
||||
((point.x - center.x).abs() - self.width / 2.0).max(0.0);
|
||||
|
||||
let distance_y =
|
||||
((point.y - center.y).abs() - self.height / 2.0).max(0.0);
|
||||
|
||||
distance_x.hypot(distance_y)
|
||||
}
|
||||
|
||||
/// Returns true if the current [`Rectangle`] is completely within the given
|
||||
/// `container`.
|
||||
pub fn is_within(&self, container: &Rectangle) -> bool {
|
||||
|
|
@ -164,12 +234,26 @@ impl Rectangle<f32> {
|
|||
}
|
||||
|
||||
/// Expands the [`Rectangle`] a given amount.
|
||||
pub fn expand(self, amount: f32) -> Self {
|
||||
pub fn expand(self, padding: impl Into<Padding>) -> Self {
|
||||
let padding = padding.into();
|
||||
|
||||
Self {
|
||||
x: self.x - amount,
|
||||
y: self.y - amount,
|
||||
width: self.width + amount * 2.0,
|
||||
height: self.height + amount * 2.0,
|
||||
x: self.x - padding.left,
|
||||
y: self.y - padding.top,
|
||||
width: self.width + padding.horizontal(),
|
||||
height: self.height + padding.vertical(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shrinks the [`Rectangle`] a given amount.
|
||||
pub fn shrink(self, padding: impl Into<Padding>) -> Self {
|
||||
let padding = padding.into();
|
||||
|
||||
Self {
|
||||
x: self.x + padding.left,
|
||||
y: self.y + padding.top,
|
||||
width: self.width - padding.horizontal(),
|
||||
height: self.height - padding.vertical(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
mod null;
|
||||
|
||||
use crate::{
|
||||
Background, Border, Color, Rectangle, Shadow, Size, Transformation, Vector,
|
||||
Background, Border, Color, Font, Pixels, Rectangle, Shadow, Size,
|
||||
Transformation, Vector,
|
||||
};
|
||||
|
||||
/// A component that can be used by widgets to draw themselves on a screen.
|
||||
|
|
@ -69,7 +70,7 @@ pub struct Quad {
|
|||
/// The bounds of the [`Quad`].
|
||||
pub bounds: Rectangle,
|
||||
|
||||
/// The [`Border`] of the [`Quad`].
|
||||
/// The [`Border`] of the [`Quad`]. The border is drawn on the inside of the [`Quad`].
|
||||
pub border: Border,
|
||||
|
||||
/// The [`Shadow`] of the [`Quad`].
|
||||
|
|
@ -100,3 +101,19 @@ impl Default for Style {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A headless renderer is a renderer that can render offscreen without
|
||||
/// a window nor a compositor.
|
||||
pub trait Headless {
|
||||
/// Creates a new [`Headless`] renderer;
|
||||
fn new(default_font: Font, default_text_size: Pixels) -> Self;
|
||||
|
||||
/// Draws offscreen into a screenshot, returning a collection of
|
||||
/// bytes representing the rendered pixels in RGBA order.
|
||||
fn screenshot(
|
||||
&mut self,
|
||||
size: Size<u32>,
|
||||
scale_factor: f32,
|
||||
background_color: Color,
|
||||
) -> Vec<u8>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
use crate::alignment;
|
||||
use crate::image;
|
||||
use crate::image::{self, Image};
|
||||
use crate::renderer::{self, Renderer};
|
||||
use crate::svg;
|
||||
use crate::text::{self, Text};
|
||||
use crate::{
|
||||
Background, Color, Font, Pixels, Point, Radians, Rectangle, Size,
|
||||
Transformation,
|
||||
Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation,
|
||||
};
|
||||
|
||||
impl Renderer for () {
|
||||
|
|
@ -77,9 +76,14 @@ impl text::Paragraph for () {
|
|||
|
||||
fn with_text(_text: Text<&str>) -> Self {}
|
||||
|
||||
fn with_spans<Link>(
|
||||
_text: Text<&[text::Span<'_, Link, Self::Font>], Self::Font>,
|
||||
) -> Self {
|
||||
}
|
||||
|
||||
fn resize(&mut self, _new_bounds: Size) {}
|
||||
|
||||
fn compare(&self, _text: Text<&str>) -> text::Difference {
|
||||
fn compare(&self, _text: Text<()>) -> text::Difference {
|
||||
text::Difference::None
|
||||
}
|
||||
|
||||
|
|
@ -102,6 +106,14 @@ impl text::Paragraph for () {
|
|||
fn hit_test(&self, _point: Point) -> Option<text::Hit> {
|
||||
None
|
||||
}
|
||||
|
||||
fn hit_span(&self, _point: Point) -> Option<usize> {
|
||||
None
|
||||
}
|
||||
|
||||
fn span_bounds(&self, _index: usize) -> Vec<Rectangle> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl text::Editor for () {
|
||||
|
|
@ -109,6 +121,10 @@ impl text::Editor for () {
|
|||
|
||||
fn with_text(_text: &str) -> Self {}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn cursor(&self) -> text::editor::Cursor {
|
||||
text::editor::Cursor::Caret(Point::ORIGIN)
|
||||
}
|
||||
|
|
@ -121,7 +137,7 @@ impl text::Editor for () {
|
|||
None
|
||||
}
|
||||
|
||||
fn line(&self, _index: usize) -> Option<&str> {
|
||||
fn line(&self, _index: usize) -> Option<text::editor::Line<'_>> {
|
||||
None
|
||||
}
|
||||
|
||||
|
|
@ -145,6 +161,7 @@ impl text::Editor for () {
|
|||
_new_font: Self::Font,
|
||||
_new_size: Pixels,
|
||||
_new_line_height: text::LineHeight,
|
||||
_new_wrapping: text::Wrapping,
|
||||
_new_highlighter: &mut impl text::Highlighter,
|
||||
) {
|
||||
}
|
||||
|
|
@ -161,21 +178,13 @@ impl text::Editor for () {
|
|||
}
|
||||
|
||||
impl image::Renderer for () {
|
||||
type Handle = ();
|
||||
type Handle = image::Handle;
|
||||
|
||||
fn measure_image(&self, _handle: &Self::Handle) -> Size<u32> {
|
||||
Size::default()
|
||||
}
|
||||
|
||||
fn draw_image(
|
||||
&mut self,
|
||||
_handle: Self::Handle,
|
||||
_filter_method: image::FilterMethod,
|
||||
_bounds: Rectangle,
|
||||
_rotation: Radians,
|
||||
_opacity: f32,
|
||||
) {
|
||||
}
|
||||
fn draw_image(&mut self, _image: Image, _bounds: Rectangle) {}
|
||||
}
|
||||
|
||||
impl svg::Renderer for () {
|
||||
|
|
@ -183,13 +192,5 @@ impl svg::Renderer for () {
|
|||
Size::default()
|
||||
}
|
||||
|
||||
fn draw_svg(
|
||||
&mut self,
|
||||
_handle: svg::Handle,
|
||||
_color: Option<Color>,
|
||||
_bounds: Rectangle,
|
||||
_rotation: Radians,
|
||||
_opacity: f32,
|
||||
) {
|
||||
}
|
||||
fn draw_svg(&mut self, _svg: svg::Svg, _bounds: Rectangle) {}
|
||||
}
|
||||
|
|
|
|||
48
core/src/settings.rs
Normal file
48
core/src/settings.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
//! Configure your application.
|
||||
use crate::{Font, Pixels};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// The settings of an iced program.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Settings {
|
||||
/// The identifier of the application.
|
||||
///
|
||||
/// If provided, this identifier may be used to identify the application or
|
||||
/// communicate with it through the windowing system.
|
||||
pub id: Option<String>,
|
||||
|
||||
/// The fonts to load on boot.
|
||||
pub fonts: Vec<Cow<'static, [u8]>>,
|
||||
|
||||
/// The default [`Font`] to be used.
|
||||
///
|
||||
/// By default, it uses [`Family::SansSerif`](crate::font::Family::SansSerif).
|
||||
pub default_font: Font,
|
||||
|
||||
/// The text size that will be used by default.
|
||||
///
|
||||
/// The default value is `16.0`.
|
||||
pub default_text_size: Pixels,
|
||||
|
||||
/// If set to true, the renderer will try to perform antialiasing for some
|
||||
/// primitives.
|
||||
///
|
||||
/// Enabling it can produce a smoother result in some widgets, like the
|
||||
/// `canvas` widget, at a performance cost.
|
||||
///
|
||||
/// By default, it is disabled.
|
||||
pub antialiasing: bool,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
id: None,
|
||||
fonts: Vec::new(),
|
||||
default_font: Font::default(),
|
||||
default_text_size: Pixels(16.0),
|
||||
antialiasing: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
use crate::InputMethod;
|
||||
use crate::event;
|
||||
use crate::window;
|
||||
|
||||
/// A connection to the state of a shell.
|
||||
|
|
@ -9,7 +11,9 @@ use crate::window;
|
|||
#[derive(Debug)]
|
||||
pub struct Shell<'a, Message> {
|
||||
messages: &'a mut Vec<Message>,
|
||||
redraw_request: Option<window::RedrawRequest>,
|
||||
event_status: event::Status,
|
||||
redraw_request: window::RedrawRequest,
|
||||
input_method: InputMethod,
|
||||
is_layout_invalid: bool,
|
||||
are_widgets_invalid: bool,
|
||||
}
|
||||
|
|
@ -19,9 +23,11 @@ impl<'a, Message> Shell<'a, Message> {
|
|||
pub fn new(messages: &'a mut Vec<Message>) -> Self {
|
||||
Self {
|
||||
messages,
|
||||
redraw_request: None,
|
||||
event_status: event::Status::Ignored,
|
||||
redraw_request: window::RedrawRequest::Wait,
|
||||
is_layout_invalid: false,
|
||||
are_widgets_invalid: false,
|
||||
input_method: InputMethod::Disabled,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -35,24 +41,75 @@ impl<'a, Message> Shell<'a, Message> {
|
|||
self.messages.push(message);
|
||||
}
|
||||
|
||||
/// Requests a new frame to be drawn.
|
||||
pub fn request_redraw(&mut self, request: window::RedrawRequest) {
|
||||
match self.redraw_request {
|
||||
None => {
|
||||
self.redraw_request = Some(request);
|
||||
/// Marks the current event as captured. Prevents "event bubbling".
|
||||
///
|
||||
/// A widget should capture an event when no ancestor should
|
||||
/// handle it.
|
||||
pub fn capture_event(&mut self) {
|
||||
self.event_status = event::Status::Captured;
|
||||
}
|
||||
Some(current) if request < current => {
|
||||
self.redraw_request = Some(request);
|
||||
|
||||
/// Returns the current [`event::Status`] of the [`Shell`].
|
||||
pub fn event_status(&self) -> event::Status {
|
||||
self.event_status
|
||||
}
|
||||
_ => {}
|
||||
|
||||
/// Returns whether the current event has been captured.
|
||||
pub fn is_event_captured(&self) -> bool {
|
||||
self.event_status == event::Status::Captured
|
||||
}
|
||||
|
||||
/// Requests a new frame to be drawn as soon as possible.
|
||||
pub fn request_redraw(&mut self) {
|
||||
self.redraw_request = window::RedrawRequest::NextFrame;
|
||||
}
|
||||
|
||||
/// Requests a new frame to be drawn at the given [`window::RedrawRequest`].
|
||||
pub fn request_redraw_at(
|
||||
&mut self,
|
||||
redraw_request: impl Into<window::RedrawRequest>,
|
||||
) {
|
||||
self.redraw_request = self.redraw_request.min(redraw_request.into());
|
||||
}
|
||||
|
||||
/// Returns the request a redraw should happen, if any.
|
||||
pub fn redraw_request(&self) -> Option<window::RedrawRequest> {
|
||||
pub fn redraw_request(&self) -> window::RedrawRequest {
|
||||
self.redraw_request
|
||||
}
|
||||
|
||||
/// Replaces the redraw request of the [`Shell`]; without conflict resolution.
|
||||
///
|
||||
/// This is useful if you want to overwrite the redraw request to a previous value.
|
||||
/// Since it's a fairly advanced use case and should rarely be used, it is a static
|
||||
/// method.
|
||||
pub fn replace_redraw_request(
|
||||
shell: &mut Self,
|
||||
redraw_request: window::RedrawRequest,
|
||||
) {
|
||||
shell.redraw_request = redraw_request;
|
||||
}
|
||||
|
||||
/// Requests the current [`InputMethod`] strategy.
|
||||
///
|
||||
/// __Important__: This request will only be honored by the
|
||||
/// [`Shell`] only during a [`window::Event::RedrawRequested`].
|
||||
pub fn request_input_method<T: AsRef<str>>(
|
||||
&mut self,
|
||||
ime: &InputMethod<T>,
|
||||
) {
|
||||
self.input_method.merge(ime);
|
||||
}
|
||||
|
||||
/// Returns the current [`InputMethod`] strategy.
|
||||
pub fn input_method(&self) -> &InputMethod {
|
||||
&self.input_method
|
||||
}
|
||||
|
||||
/// Returns the current [`InputMethod`] strategy.
|
||||
pub fn input_method_mut(&mut self) -> &mut InputMethod {
|
||||
&mut self.input_method
|
||||
}
|
||||
|
||||
/// Returns whether the current layout is invalid or not.
|
||||
pub fn is_layout_invalid(&self) -> bool {
|
||||
self.is_layout_invalid
|
||||
|
|
@ -95,14 +152,14 @@ impl<'a, Message> Shell<'a, Message> {
|
|||
pub fn merge<B>(&mut self, other: Shell<'_, B>, f: impl Fn(B) -> Message) {
|
||||
self.messages.extend(other.messages.drain(..).map(f));
|
||||
|
||||
if let Some(at) = other.redraw_request {
|
||||
self.request_redraw(at);
|
||||
}
|
||||
|
||||
self.is_layout_invalid =
|
||||
self.is_layout_invalid || other.is_layout_invalid;
|
||||
|
||||
self.are_widgets_invalid =
|
||||
self.are_widgets_invalid || other.are_widgets_invalid;
|
||||
|
||||
self.redraw_request = self.redraw_request.min(other.redraw_request);
|
||||
self.event_status = self.event_status.merge(other.event_status);
|
||||
self.input_method.merge(&other.input_method);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,20 @@ impl<T> From<Size<T>> for Vector<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Add for Size<T>
|
||||
where
|
||||
T: std::ops::Add<Output = T>,
|
||||
{
|
||||
type Output = Size<T>;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Size {
|
||||
width: self.width + rhs.width,
|
||||
height: self.height + rhs.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Sub for Size<T>
|
||||
where
|
||||
T: std::ops::Sub<Output = T>,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,66 @@ use std::hash::{Hash, Hasher as _};
|
|||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A raster image that can be drawn.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Svg<H = Handle> {
|
||||
/// The handle of the [`Svg`].
|
||||
pub handle: H,
|
||||
|
||||
/// The [`Color`] filter to be applied to the [`Svg`].
|
||||
///
|
||||
/// If some [`Color`] is set, the whole [`Svg`] will be
|
||||
/// painted with it—ignoring any intrinsic colors.
|
||||
///
|
||||
/// This can be useful for coloring icons programmatically
|
||||
/// (e.g. with a theme).
|
||||
pub color: Option<Color>,
|
||||
|
||||
/// The rotation to be applied to the image; on its center.
|
||||
pub rotation: Radians,
|
||||
|
||||
/// The opacity of the [`Svg`].
|
||||
///
|
||||
/// 0 means transparent. 1 means opaque.
|
||||
pub opacity: f32,
|
||||
}
|
||||
|
||||
impl Svg<Handle> {
|
||||
/// Creates a new [`Svg`] with the given handle.
|
||||
pub fn new(handle: impl Into<Handle>) -> Self {
|
||||
Self {
|
||||
handle: handle.into(),
|
||||
color: None,
|
||||
rotation: Radians(0.0),
|
||||
opacity: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the [`Color`] filter of the [`Svg`].
|
||||
pub fn color(mut self, color: impl Into<Color>) -> Self {
|
||||
self.color = Some(color.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the rotation of the [`Svg`].
|
||||
pub fn rotation(mut self, rotation: impl Into<Radians>) -> Self {
|
||||
self.rotation = rotation.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the opacity of the [`Svg`].
|
||||
pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
|
||||
self.opacity = opacity.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Handle> for Svg {
|
||||
fn from(handle: &Handle) -> Self {
|
||||
Svg::new(handle.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle of Svg data.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Handle {
|
||||
|
|
@ -95,12 +155,5 @@ pub trait Renderer: crate::Renderer {
|
|||
fn measure_svg(&self, handle: &Handle) -> Size<u32>;
|
||||
|
||||
/// Draws an SVG with the given [`Handle`], an optional [`Color`] filter, and inside the provided `bounds`.
|
||||
fn draw_svg(
|
||||
&mut self,
|
||||
handle: Handle,
|
||||
color: Option<Color>,
|
||||
bounds: Rectangle,
|
||||
rotation: Radians,
|
||||
opacity: f32,
|
||||
);
|
||||
fn draw_svg(&mut self, svg: Svg, bounds: Rectangle);
|
||||
}
|
||||
|
|
|
|||
327
core/src/text.rs
327
core/src/text.rs
|
|
@ -1,16 +1,18 @@
|
|||
//! Draw and interact with text.
|
||||
mod paragraph;
|
||||
|
||||
pub mod editor;
|
||||
pub mod highlighter;
|
||||
pub mod paragraph;
|
||||
|
||||
pub use editor::Editor;
|
||||
pub use highlighter::Highlighter;
|
||||
pub use paragraph::Paragraph;
|
||||
|
||||
use crate::alignment;
|
||||
use crate::{Color, Pixels, Point, Rectangle, Size};
|
||||
use crate::{
|
||||
Background, Border, Color, Padding, Pixels, Point, Rectangle, Size,
|
||||
};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
/// A paragraph.
|
||||
|
|
@ -39,6 +41,9 @@ pub struct Text<Content = String, Font = crate::Font> {
|
|||
|
||||
/// The [`Shaping`] strategy of the [`Text`].
|
||||
pub shaping: Shaping,
|
||||
|
||||
/// The [`Wrapping`] strategy of the [`Text`].
|
||||
pub wrapping: Wrapping,
|
||||
}
|
||||
|
||||
/// The shaping strategy of some text.
|
||||
|
|
@ -65,6 +70,22 @@ pub enum Shaping {
|
|||
Advanced,
|
||||
}
|
||||
|
||||
/// The wrapping strategy of some text.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub enum Wrapping {
|
||||
/// No wrapping.
|
||||
None,
|
||||
/// Wraps at the word level.
|
||||
///
|
||||
/// This is the default.
|
||||
#[default]
|
||||
Word,
|
||||
/// Wraps at the glyph level.
|
||||
Glyph,
|
||||
/// Wraps at the word level, or fallback to glyph level if a word can't fit on a line by itself.
|
||||
WordOrGlyph,
|
||||
}
|
||||
|
||||
/// The height of a line of text in a paragraph.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum LineHeight {
|
||||
|
|
@ -221,3 +242,303 @@ pub trait Renderer: crate::Renderer {
|
|||
clip_bounds: Rectangle,
|
||||
);
|
||||
}
|
||||
|
||||
/// A span of text.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Span<'a, Link = (), Font = crate::Font> {
|
||||
/// The [`Fragment`] of text.
|
||||
pub text: Fragment<'a>,
|
||||
/// The size of the [`Span`] in [`Pixels`].
|
||||
pub size: Option<Pixels>,
|
||||
/// The [`LineHeight`] of the [`Span`].
|
||||
pub line_height: Option<LineHeight>,
|
||||
/// The font of the [`Span`].
|
||||
pub font: Option<Font>,
|
||||
/// The [`Color`] of the [`Span`].
|
||||
pub color: Option<Color>,
|
||||
/// The link of the [`Span`].
|
||||
pub link: Option<Link>,
|
||||
/// The [`Highlight`] of the [`Span`].
|
||||
pub highlight: Option<Highlight>,
|
||||
/// The [`Padding`] of the [`Span`].
|
||||
///
|
||||
/// Currently, it only affects the bounds of the [`Highlight`].
|
||||
pub padding: Padding,
|
||||
/// Whether the [`Span`] should be underlined or not.
|
||||
pub underline: bool,
|
||||
/// Whether the [`Span`] should be struck through or not.
|
||||
pub strikethrough: bool,
|
||||
}
|
||||
|
||||
/// A text highlight.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Highlight {
|
||||
/// The [`Background`] of the highlight.
|
||||
pub background: Background,
|
||||
/// The [`Border`] of the highlight.
|
||||
pub border: Border,
|
||||
}
|
||||
|
||||
impl<'a, Link, Font> Span<'a, Link, Font> {
|
||||
/// Creates a new [`Span`] of text with the given text fragment.
|
||||
pub fn new(fragment: impl IntoFragment<'a>) -> Self {
|
||||
Self {
|
||||
text: fragment.into_fragment(),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the size of the [`Span`].
|
||||
pub fn size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.size = Some(size.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`LineHeight`] of the [`Span`].
|
||||
pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
|
||||
self.line_height = Some(line_height.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the font of the [`Span`].
|
||||
pub fn font(mut self, font: impl Into<Font>) -> Self {
|
||||
self.font = Some(font.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the font of the [`Span`], if any.
|
||||
pub fn font_maybe(mut self, font: Option<impl Into<Font>>) -> Self {
|
||||
self.font = font.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Color`] of the [`Span`].
|
||||
pub fn color(mut self, color: impl Into<Color>) -> Self {
|
||||
self.color = Some(color.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Color`] of the [`Span`], if any.
|
||||
pub fn color_maybe(mut self, color: Option<impl Into<Color>>) -> Self {
|
||||
self.color = color.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the link of the [`Span`].
|
||||
pub fn link(mut self, link: impl Into<Link>) -> Self {
|
||||
self.link = Some(link.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the link of the [`Span`], if any.
|
||||
pub fn link_maybe(mut self, link: Option<impl Into<Link>>) -> Self {
|
||||
self.link = link.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Background`] of the [`Span`].
|
||||
pub fn background(self, background: impl Into<Background>) -> Self {
|
||||
self.background_maybe(Some(background))
|
||||
}
|
||||
|
||||
/// Sets the [`Background`] of the [`Span`], if any.
|
||||
pub fn background_maybe(
|
||||
mut self,
|
||||
background: Option<impl Into<Background>>,
|
||||
) -> Self {
|
||||
let Some(background) = background else {
|
||||
return self;
|
||||
};
|
||||
|
||||
match &mut self.highlight {
|
||||
Some(highlight) => {
|
||||
highlight.background = background.into();
|
||||
}
|
||||
None => {
|
||||
self.highlight = Some(Highlight {
|
||||
background: background.into(),
|
||||
border: Border::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Border`] of the [`Span`].
|
||||
pub fn border(self, border: impl Into<Border>) -> Self {
|
||||
self.border_maybe(Some(border))
|
||||
}
|
||||
|
||||
/// Sets the [`Border`] of the [`Span`], if any.
|
||||
pub fn border_maybe(mut self, border: Option<impl Into<Border>>) -> Self {
|
||||
let Some(border) = border else {
|
||||
return self;
|
||||
};
|
||||
|
||||
match &mut self.highlight {
|
||||
Some(highlight) => {
|
||||
highlight.border = border.into();
|
||||
}
|
||||
None => {
|
||||
self.highlight = Some(Highlight {
|
||||
border: border.into(),
|
||||
background: Background::Color(Color::TRANSPARENT),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`Span`].
|
||||
///
|
||||
/// It only affects the [`background`] and [`border`] of the
|
||||
/// [`Span`], currently.
|
||||
///
|
||||
/// [`background`]: Self::background
|
||||
/// [`border`]: Self::border
|
||||
pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the [`Span`] should be underlined or not.
|
||||
pub fn underline(mut self, underline: bool) -> Self {
|
||||
self.underline = underline;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the [`Span`] should be struck through or not.
|
||||
pub fn strikethrough(mut self, strikethrough: bool) -> Self {
|
||||
self.strikethrough = strikethrough;
|
||||
self
|
||||
}
|
||||
|
||||
/// Turns the [`Span`] into a static one.
|
||||
pub fn to_static(self) -> Span<'static, Link, Font> {
|
||||
Span {
|
||||
text: Cow::Owned(self.text.into_owned()),
|
||||
size: self.size,
|
||||
line_height: self.line_height,
|
||||
font: self.font,
|
||||
color: self.color,
|
||||
link: self.link,
|
||||
highlight: self.highlight,
|
||||
padding: self.padding,
|
||||
underline: self.underline,
|
||||
strikethrough: self.strikethrough,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Link, Font> Default for Span<'_, Link, Font> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
text: Cow::default(),
|
||||
size: None,
|
||||
line_height: None,
|
||||
font: None,
|
||||
color: None,
|
||||
link: None,
|
||||
highlight: None,
|
||||
padding: Padding::default(),
|
||||
underline: false,
|
||||
strikethrough: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Link, Font> From<&'a str> for Span<'a, Link, Font> {
|
||||
fn from(value: &'a str) -> Self {
|
||||
Span::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Link, Font: PartialEq> PartialEq for Span<'_, Link, Font> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.text == other.text
|
||||
&& self.size == other.size
|
||||
&& self.line_height == other.line_height
|
||||
&& self.font == other.font
|
||||
&& self.color == other.color
|
||||
}
|
||||
}
|
||||
|
||||
/// A fragment of [`Text`].
|
||||
///
|
||||
/// This is just an alias to a string that may be either
|
||||
/// borrowed or owned.
|
||||
pub type Fragment<'a> = Cow<'a, str>;
|
||||
|
||||
/// A trait for converting a value to some text [`Fragment`].
|
||||
pub trait IntoFragment<'a> {
|
||||
/// Converts the value to some text [`Fragment`].
|
||||
fn into_fragment(self) -> Fragment<'a>;
|
||||
}
|
||||
|
||||
impl<'a> IntoFragment<'a> for Fragment<'a> {
|
||||
fn into_fragment(self) -> Fragment<'a> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoFragment<'a> for &'a Fragment<'_> {
|
||||
fn into_fragment(self) -> Fragment<'a> {
|
||||
Fragment::Borrowed(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoFragment<'a> for &'a str {
|
||||
fn into_fragment(self) -> Fragment<'a> {
|
||||
Fragment::Borrowed(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoFragment<'a> for &'a String {
|
||||
fn into_fragment(self) -> Fragment<'a> {
|
||||
Fragment::Borrowed(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoFragment<'a> for String {
|
||||
fn into_fragment(self) -> Fragment<'a> {
|
||||
Fragment::Owned(self)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! into_fragment {
|
||||
($type:ty) => {
|
||||
impl<'a> IntoFragment<'a> for $type {
|
||||
fn into_fragment(self) -> Fragment<'a> {
|
||||
Fragment::Owned(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoFragment<'a> for &$type {
|
||||
fn into_fragment(self) -> Fragment<'a> {
|
||||
Fragment::Owned(self.to_string())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
into_fragment!(char);
|
||||
into_fragment!(bool);
|
||||
|
||||
into_fragment!(u8);
|
||||
into_fragment!(u16);
|
||||
into_fragment!(u32);
|
||||
into_fragment!(u64);
|
||||
into_fragment!(u128);
|
||||
into_fragment!(usize);
|
||||
|
||||
into_fragment!(i8);
|
||||
into_fragment!(i16);
|
||||
into_fragment!(i32);
|
||||
into_fragment!(i64);
|
||||
into_fragment!(i128);
|
||||
into_fragment!(isize);
|
||||
|
||||
into_fragment!(f32);
|
||||
into_fragment!(f64);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
//! Edit text.
|
||||
use crate::text::highlighter::{self, Highlighter};
|
||||
use crate::text::LineHeight;
|
||||
use crate::text::{LineHeight, Wrapping};
|
||||
use crate::{Pixels, Point, Rectangle, Size};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A component that can be used by widgets to edit multi-line text.
|
||||
|
|
@ -13,6 +14,9 @@ pub trait Editor: Sized + Default {
|
|||
/// Creates a new [`Editor`] laid out with the given text.
|
||||
fn with_text(text: &str) -> Self;
|
||||
|
||||
/// Returns true if the [`Editor`] has no contents.
|
||||
fn is_empty(&self) -> bool;
|
||||
|
||||
/// Returns the current [`Cursor`] of the [`Editor`].
|
||||
fn cursor(&self) -> Cursor;
|
||||
|
||||
|
|
@ -25,7 +29,7 @@ pub trait Editor: Sized + Default {
|
|||
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>;
|
||||
fn line(&self, index: usize) -> Option<Line<'_>>;
|
||||
|
||||
/// Returns the amount of lines in the [`Editor`].
|
||||
fn line_count(&self) -> usize;
|
||||
|
|
@ -47,6 +51,7 @@ pub trait Editor: Sized + Default {
|
|||
new_font: Self::Font,
|
||||
new_size: Pixels,
|
||||
new_line_height: LineHeight,
|
||||
new_wrapping: Wrapping,
|
||||
new_highlighter: &mut impl Highlighter,
|
||||
);
|
||||
|
||||
|
|
@ -70,6 +75,8 @@ pub enum Action {
|
|||
SelectWord,
|
||||
/// Select the line at the current cursor.
|
||||
SelectLine,
|
||||
/// Select the entire buffer.
|
||||
SelectAll,
|
||||
/// Perform an [`Edit`].
|
||||
Edit(Edit),
|
||||
/// Click the [`Editor`] at the given [`Point`].
|
||||
|
|
@ -183,3 +190,41 @@ pub enum Cursor {
|
|||
/// Cursor selecting a range of text
|
||||
Selection(Vec<Rectangle>),
|
||||
}
|
||||
|
||||
/// A line of an [`Editor`].
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct Line<'a> {
|
||||
/// The raw text of the [`Line`].
|
||||
pub text: Cow<'a, str>,
|
||||
/// The line ending of the [`Line`].
|
||||
pub ending: LineEnding,
|
||||
}
|
||||
|
||||
/// The line ending of a [`Line`].
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub enum LineEnding {
|
||||
/// Use `\n` for line ending (POSIX-style)
|
||||
#[default]
|
||||
Lf,
|
||||
/// Use `\r\n` for line ending (Windows-style)
|
||||
CrLf,
|
||||
/// Use `\r` for line ending (many legacy systems)
|
||||
Cr,
|
||||
/// Use `\n\r` for line ending (some legacy systems)
|
||||
LfCr,
|
||||
/// No line ending
|
||||
None,
|
||||
}
|
||||
|
||||
impl LineEnding {
|
||||
/// Gets the string representation of the [`LineEnding`].
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Lf => "\n",
|
||||
Self::CrLf => "\r\n",
|
||||
Self::Cr => "\r",
|
||||
Self::LfCr => "\n\r",
|
||||
Self::None => "",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
//! Draw paragraphs.
|
||||
use crate::alignment;
|
||||
use crate::text::{Difference, Hit, Text};
|
||||
use crate::{Point, Size};
|
||||
use crate::text::{Difference, Hit, Span, Text};
|
||||
use crate::{Point, Rectangle, Size};
|
||||
|
||||
/// A text paragraph.
|
||||
pub trait Paragraph: Sized + Default {
|
||||
|
|
@ -10,12 +11,17 @@ pub trait Paragraph: Sized + Default {
|
|||
/// Creates a new [`Paragraph`] laid out with the given [`Text`].
|
||||
fn with_text(text: Text<&str, Self::Font>) -> Self;
|
||||
|
||||
/// Creates a new [`Paragraph`] laid out with the given [`Text`].
|
||||
fn with_spans<Link>(
|
||||
text: Text<&[Span<'_, Link, Self::Font>], 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<&str, Self::Font>) -> Difference;
|
||||
fn compare(&self, text: Text<(), Self::Font>) -> Difference;
|
||||
|
||||
/// Returns the horizontal alignment of the [`Paragraph`].
|
||||
fn horizontal_alignment(&self) -> alignment::Horizontal;
|
||||
|
|
@ -31,22 +37,18 @@ pub trait Paragraph: Sized + Default {
|
|||
/// [`Paragraph`], returning information about the nearest character.
|
||||
fn hit_test(&self, point: Point) -> Option<Hit>;
|
||||
|
||||
/// Tests whether the provided point is within the boundaries of a
|
||||
/// [`Span`] in the [`Paragraph`], returning the index of the [`Span`]
|
||||
/// that was hit.
|
||||
fn hit_span(&self, point: Point) -> Option<usize>;
|
||||
|
||||
/// Returns all bounds for the provided [`Span`] index of the [`Paragraph`].
|
||||
/// A [`Span`] can have multiple bounds for each line it's on.
|
||||
fn span_bounds(&self, index: usize) -> Vec<Rectangle>;
|
||||
|
||||
/// 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<&str, 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
|
||||
|
|
@ -57,3 +59,84 @@ pub trait Paragraph: Sized + Default {
|
|||
self.min_bounds().height
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Paragraph`] of plain text.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Plain<P: Paragraph> {
|
||||
raw: P,
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl<P: Paragraph> Plain<P> {
|
||||
/// Creates a new [`Plain`] paragraph.
|
||||
pub fn new(text: Text<&str, P::Font>) -> Self {
|
||||
let content = text.content.to_owned();
|
||||
|
||||
Self {
|
||||
raw: P::with_text(text),
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the plain [`Paragraph`] to match the given [`Text`], if needed.
|
||||
pub fn update(&mut self, text: Text<&str, P::Font>) {
|
||||
if self.content != text.content {
|
||||
text.content.clone_into(&mut self.content);
|
||||
self.raw = P::with_text(text);
|
||||
return;
|
||||
}
|
||||
|
||||
match self.raw.compare(Text {
|
||||
content: (),
|
||||
bounds: text.bounds,
|
||||
size: text.size,
|
||||
line_height: text.line_height,
|
||||
font: text.font,
|
||||
horizontal_alignment: text.horizontal_alignment,
|
||||
vertical_alignment: text.vertical_alignment,
|
||||
shaping: text.shaping,
|
||||
wrapping: text.wrapping,
|
||||
}) {
|
||||
Difference::None => {}
|
||||
Difference::Bounds => {
|
||||
self.raw.resize(text.bounds);
|
||||
}
|
||||
Difference::Shape => {
|
||||
self.raw = P::with_text(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the horizontal alignment of the [`Paragraph`].
|
||||
pub fn horizontal_alignment(&self) -> alignment::Horizontal {
|
||||
self.raw.horizontal_alignment()
|
||||
}
|
||||
|
||||
/// Returns the vertical alignment of the [`Paragraph`].
|
||||
pub fn vertical_alignment(&self) -> alignment::Vertical {
|
||||
self.raw.vertical_alignment()
|
||||
}
|
||||
|
||||
/// Returns the minimum boundaries that can fit the contents of the
|
||||
/// [`Paragraph`].
|
||||
pub fn min_bounds(&self) -> Size {
|
||||
self.raw.min_bounds()
|
||||
}
|
||||
|
||||
/// Returns the minimum width that can fit the contents of the
|
||||
/// [`Paragraph`].
|
||||
pub fn min_width(&self) -> f32 {
|
||||
self.raw.min_width()
|
||||
}
|
||||
|
||||
/// Returns the minimum height that can fit the contents of the
|
||||
/// [`Paragraph`].
|
||||
pub fn min_height(&self) -> f32 {
|
||||
self.raw.min_height()
|
||||
}
|
||||
|
||||
/// Returns the cached [`Paragraph`].
|
||||
pub fn raw(&self) -> &P {
|
||||
&self.raw
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ pub mod palette;
|
|||
|
||||
pub use palette::Palette;
|
||||
|
||||
use crate::Color;
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
@ -164,14 +166,17 @@ impl Default for Theme {
|
|||
fn default() -> Self {
|
||||
#[cfg(feature = "auto-detect-theme")]
|
||||
{
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
static DEFAULT: Lazy<Theme> =
|
||||
Lazy::new(|| match dark_light::detect() {
|
||||
static DEFAULT: LazyLock<Theme> = LazyLock::new(|| {
|
||||
match dark_light::detect()
|
||||
.unwrap_or(dark_light::Mode::Unspecified)
|
||||
{
|
||||
dark_light::Mode::Dark => Theme::Dark,
|
||||
dark_light::Mode::Light | dark_light::Mode::Default => {
|
||||
dark_light::Mode::Light | dark_light::Mode::Unspecified => {
|
||||
Theme::Light
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
DEFAULT.clone()
|
||||
|
|
@ -246,3 +251,35 @@ impl fmt::Display for Custom {
|
|||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
/// The base style of a [`Theme`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Style {
|
||||
/// The background [`Color`] of the application.
|
||||
pub background_color: Color,
|
||||
|
||||
/// The default text [`Color`] of the application.
|
||||
pub text_color: Color,
|
||||
}
|
||||
|
||||
/// The default blank style of a [`Theme`].
|
||||
pub trait Base {
|
||||
/// Returns the default base [`Style`] of a [`Theme`].
|
||||
fn base(&self) -> Style;
|
||||
}
|
||||
|
||||
impl Base for Theme {
|
||||
fn base(&self) -> Style {
|
||||
default(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The default [`Style`] of a built-in [`Theme`].
|
||||
pub fn default(theme: &Theme) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
Style {
|
||||
background_color: palette.background.base.color,
|
||||
text_color: palette.background.base.text,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
//! Define the colors of a theme.
|
||||
use crate::{color, Color};
|
||||
use crate::{Color, color};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use palette::color_difference::Wcag21RelativeContrast;
|
||||
use palette::rgb::Rgb;
|
||||
use palette::{FromColor, Hsl, Mix};
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// A color palette.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
|
|
@ -18,6 +19,8 @@ pub struct Palette {
|
|||
pub primary: Color,
|
||||
/// The success [`Color`] of the [`Palette`].
|
||||
pub success: Color,
|
||||
/// The warning [`Color`] of the [`Palette`].
|
||||
pub warning: Color,
|
||||
/// The danger [`Color`] of the [`Palette`].
|
||||
pub danger: Color,
|
||||
}
|
||||
|
|
@ -27,46 +30,20 @@ impl Palette {
|
|||
pub const LIGHT: Self = Self {
|
||||
background: Color::WHITE,
|
||||
text: Color::BLACK,
|
||||
primary: Color::from_rgb(
|
||||
0x5E as f32 / 255.0,
|
||||
0x7C as f32 / 255.0,
|
||||
0xE2 as f32 / 255.0,
|
||||
),
|
||||
success: Color::from_rgb(
|
||||
0x12 as f32 / 255.0,
|
||||
0x66 as f32 / 255.0,
|
||||
0x4F as f32 / 255.0,
|
||||
),
|
||||
danger: Color::from_rgb(
|
||||
0xC3 as f32 / 255.0,
|
||||
0x42 as f32 / 255.0,
|
||||
0x3F as f32 / 255.0,
|
||||
),
|
||||
primary: color!(0x5865F2),
|
||||
success: color!(0x12664f),
|
||||
warning: color!(0xffc14e),
|
||||
danger: color!(0xc3423f),
|
||||
};
|
||||
|
||||
/// The built-in dark variant of a [`Palette`].
|
||||
pub const DARK: Self = Self {
|
||||
background: Color::from_rgb(
|
||||
0x20 as f32 / 255.0,
|
||||
0x22 as f32 / 255.0,
|
||||
0x25 as f32 / 255.0,
|
||||
),
|
||||
background: color!(0x2B2D31),
|
||||
text: Color::from_rgb(0.90, 0.90, 0.90),
|
||||
primary: Color::from_rgb(
|
||||
0x5E as f32 / 255.0,
|
||||
0x7C as f32 / 255.0,
|
||||
0xE2 as f32 / 255.0,
|
||||
),
|
||||
success: Color::from_rgb(
|
||||
0x12 as f32 / 255.0,
|
||||
0x66 as f32 / 255.0,
|
||||
0x4F as f32 / 255.0,
|
||||
),
|
||||
danger: Color::from_rgb(
|
||||
0xC3 as f32 / 255.0,
|
||||
0x42 as f32 / 255.0,
|
||||
0x3F as f32 / 255.0,
|
||||
),
|
||||
primary: color!(0x5865F2),
|
||||
success: color!(0x12664f),
|
||||
warning: color!(0xffc14e),
|
||||
danger: color!(0xc3423f),
|
||||
};
|
||||
|
||||
/// The built-in [Dracula] variant of a [`Palette`].
|
||||
|
|
@ -77,6 +54,7 @@ impl Palette {
|
|||
text: color!(0xf8f8f2), // FOREGROUND
|
||||
primary: color!(0xbd93f9), // PURPLE
|
||||
success: color!(0x50fa7b), // GREEN
|
||||
warning: color!(0xf1fa8c), // YELLOW
|
||||
danger: color!(0xff5555), // RED
|
||||
};
|
||||
|
||||
|
|
@ -88,6 +66,7 @@ impl Palette {
|
|||
text: color!(0xeceff4), // nord6
|
||||
primary: color!(0x8fbcbb), // nord7
|
||||
success: color!(0xa3be8c), // nord14
|
||||
warning: color!(0xebcb8b), // nord13
|
||||
danger: color!(0xbf616a), // nord11
|
||||
};
|
||||
|
||||
|
|
@ -99,6 +78,7 @@ impl Palette {
|
|||
text: color!(0x657b83), // base00
|
||||
primary: color!(0x2aa198), // cyan
|
||||
success: color!(0x859900), // green
|
||||
warning: color!(0xb58900), // yellow
|
||||
danger: color!(0xdc322f), // red
|
||||
};
|
||||
|
||||
|
|
@ -110,6 +90,7 @@ impl Palette {
|
|||
text: color!(0x839496), // base0
|
||||
primary: color!(0x2aa198), // cyan
|
||||
success: color!(0x859900), // green
|
||||
warning: color!(0xb58900), // yellow
|
||||
danger: color!(0xdc322f), // red
|
||||
};
|
||||
|
||||
|
|
@ -121,6 +102,7 @@ impl Palette {
|
|||
text: color!(0x282828), // light FG0_29
|
||||
primary: color!(0x458588), // light BLUE_4
|
||||
success: color!(0x98971a), // light GREEN_2
|
||||
warning: color!(0xd79921), // light YELLOW_3
|
||||
danger: color!(0xcc241d), // light RED_1
|
||||
};
|
||||
|
||||
|
|
@ -132,6 +114,7 @@ impl Palette {
|
|||
text: color!(0xfbf1c7), // dark FG0_29
|
||||
primary: color!(0x458588), // dark BLUE_4
|
||||
success: color!(0x98971a), // dark GREEN_2
|
||||
warning: color!(0xd79921), // dark YELLOW_3
|
||||
danger: color!(0xcc241d), // dark RED_1
|
||||
};
|
||||
|
||||
|
|
@ -143,6 +126,7 @@ impl Palette {
|
|||
text: color!(0x4c4f69), // Text
|
||||
primary: color!(0x1e66f5), // Blue
|
||||
success: color!(0x40a02b), // Green
|
||||
warning: color!(0xdf8e1d), // Yellow
|
||||
danger: color!(0xd20f39), // Red
|
||||
};
|
||||
|
||||
|
|
@ -154,6 +138,7 @@ impl Palette {
|
|||
text: color!(0xc6d0f5), // Text
|
||||
primary: color!(0x8caaee), // Blue
|
||||
success: color!(0xa6d189), // Green
|
||||
warning: color!(0xe5c890), // Yellow
|
||||
danger: color!(0xe78284), // Red
|
||||
};
|
||||
|
||||
|
|
@ -165,6 +150,7 @@ impl Palette {
|
|||
text: color!(0xcad3f5), // Text
|
||||
primary: color!(0x8aadf4), // Blue
|
||||
success: color!(0xa6da95), // Green
|
||||
warning: color!(0xeed49f), // Yellow
|
||||
danger: color!(0xed8796), // Red
|
||||
};
|
||||
|
||||
|
|
@ -176,6 +162,7 @@ impl Palette {
|
|||
text: color!(0xcdd6f4), // Text
|
||||
primary: color!(0x89b4fa), // Blue
|
||||
success: color!(0xa6e3a1), // Green
|
||||
warning: color!(0xf9e2af), // Yellow
|
||||
danger: color!(0xf38ba8), // Red
|
||||
};
|
||||
|
||||
|
|
@ -187,6 +174,7 @@ impl Palette {
|
|||
text: color!(0x9aa5ce), // Text
|
||||
primary: color!(0x2ac3de), // Blue
|
||||
success: color!(0x9ece6a), // Green
|
||||
warning: color!(0xe0af68), // Yellow
|
||||
danger: color!(0xf7768e), // Red
|
||||
};
|
||||
|
||||
|
|
@ -198,6 +186,7 @@ impl Palette {
|
|||
text: color!(0x9aa5ce), // Text
|
||||
primary: color!(0x2ac3de), // Blue
|
||||
success: color!(0x9ece6a), // Green
|
||||
warning: color!(0xe0af68), // Yellow
|
||||
danger: color!(0xf7768e), // Red
|
||||
};
|
||||
|
||||
|
|
@ -209,6 +198,7 @@ impl Palette {
|
|||
text: color!(0x565a6e), // Text
|
||||
primary: color!(0x166775), // Blue
|
||||
success: color!(0x485e30), // Green
|
||||
warning: color!(0x8f5e15), // Yellow
|
||||
danger: color!(0x8c4351), // Red
|
||||
};
|
||||
|
||||
|
|
@ -216,10 +206,11 @@ impl Palette {
|
|||
///
|
||||
/// [Kanagawa]: https://github.com/rebelot/kanagawa.nvim
|
||||
pub const KANAGAWA_WAVE: Self = Self {
|
||||
background: color!(0x363646), // Sumi Ink 3
|
||||
text: color!(0xCD7BA), // Fuji White
|
||||
primary: color!(0x2D4F67), // Wave Blue 2
|
||||
background: color!(0x1f1f28), // Sumi Ink 3
|
||||
text: color!(0xDCD7BA), // Fuji White
|
||||
primary: color!(0x7FB4CA), // Wave Blue
|
||||
success: color!(0x76946A), // Autumn Green
|
||||
warning: color!(0xff9e3b), // Ronin Yellow
|
||||
danger: color!(0xC34043), // Autumn Red
|
||||
};
|
||||
|
||||
|
|
@ -231,6 +222,7 @@ impl Palette {
|
|||
text: color!(0xc5c9c5), // Dragon White
|
||||
primary: color!(0x223249), // Wave Blue 1
|
||||
success: color!(0x8a9a7b), // Dragon Green 2
|
||||
warning: color!(0xff9e3b), // Ronin Yellow
|
||||
danger: color!(0xc4746e), // Dragon Red
|
||||
};
|
||||
|
||||
|
|
@ -240,8 +232,9 @@ impl Palette {
|
|||
pub const KANAGAWA_LOTUS: Self = Self {
|
||||
background: color!(0xf2ecbc), // Lotus White 3
|
||||
text: color!(0x545464), // Lotus Ink 1
|
||||
primary: color!(0xc9cbd1), // Lotus Violet 3
|
||||
primary: color!(0x4d699b), // Lotus Blue
|
||||
success: color!(0x6f894e), // Lotus Green
|
||||
warning: color!(0xe98a00), // Lotus Orange 2
|
||||
danger: color!(0xc84053), // Lotus Red
|
||||
};
|
||||
|
||||
|
|
@ -253,6 +246,7 @@ impl Palette {
|
|||
text: color!(0xbdbdbd), // Foreground
|
||||
primary: color!(0x80a0ff), // Blue (normal)
|
||||
success: color!(0x8cc85f), // Green (normal)
|
||||
warning: color!(0xe3c78a), // Yellow (normal)
|
||||
danger: color!(0xff5454), // Red (normal)
|
||||
};
|
||||
|
||||
|
|
@ -264,6 +258,7 @@ impl Palette {
|
|||
text: color!(0xbdc1c6), // Foreground
|
||||
primary: color!(0x82aaff), // Blue (normal)
|
||||
success: color!(0xa1cd5e), // Green (normal)
|
||||
warning: color!(0xe3d18a), // Yellow (normal)
|
||||
danger: color!(0xfc514e), // Red (normal)
|
||||
};
|
||||
|
||||
|
|
@ -275,6 +270,7 @@ impl Palette {
|
|||
text: color!(0xd0d0d0),
|
||||
primary: color!(0x00b4ff),
|
||||
success: color!(0x00c15a),
|
||||
warning: color!(0xbe95ff), // Base 14
|
||||
danger: color!(0xf62d0f),
|
||||
};
|
||||
|
||||
|
|
@ -286,6 +282,7 @@ impl Palette {
|
|||
text: color!(0xfecdb2),
|
||||
primary: color!(0xd1d1e0),
|
||||
success: color!(0xb1b695),
|
||||
warning: color!(0xf5d76e), // Honey
|
||||
danger: color!(0xe06b75),
|
||||
};
|
||||
}
|
||||
|
|
@ -301,6 +298,8 @@ pub struct Extended {
|
|||
pub secondary: Secondary,
|
||||
/// The set of success colors.
|
||||
pub success: Success,
|
||||
/// The set of warning colors.
|
||||
pub warning: Warning,
|
||||
/// The set of danger colors.
|
||||
pub danger: Danger,
|
||||
/// Whether the palette is dark or not.
|
||||
|
|
@ -308,92 +307,92 @@ pub struct Extended {
|
|||
}
|
||||
|
||||
/// The built-in light variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_LIGHT: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::LIGHT));
|
||||
pub static EXTENDED_LIGHT: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::LIGHT));
|
||||
|
||||
/// The built-in dark variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_DARK: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::DARK));
|
||||
pub static EXTENDED_DARK: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::DARK));
|
||||
|
||||
/// The built-in Dracula variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_DRACULA: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::DRACULA));
|
||||
pub static EXTENDED_DRACULA: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::DRACULA));
|
||||
|
||||
/// The built-in Nord variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_NORD: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::NORD));
|
||||
pub static EXTENDED_NORD: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::NORD));
|
||||
|
||||
/// The built-in Solarized Light variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_SOLARIZED_LIGHT: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::SOLARIZED_LIGHT));
|
||||
pub static EXTENDED_SOLARIZED_LIGHT: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::SOLARIZED_LIGHT));
|
||||
|
||||
/// The built-in Solarized Dark variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_SOLARIZED_DARK: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::SOLARIZED_DARK));
|
||||
pub static EXTENDED_SOLARIZED_DARK: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::SOLARIZED_DARK));
|
||||
|
||||
/// The built-in Gruvbox Light variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_GRUVBOX_LIGHT: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::GRUVBOX_LIGHT));
|
||||
pub static EXTENDED_GRUVBOX_LIGHT: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::GRUVBOX_LIGHT));
|
||||
|
||||
/// The built-in Gruvbox Dark variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_GRUVBOX_DARK: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::GRUVBOX_DARK));
|
||||
pub static EXTENDED_GRUVBOX_DARK: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::GRUVBOX_DARK));
|
||||
|
||||
/// The built-in Catppuccin Latte variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_CATPPUCCIN_LATTE: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::CATPPUCCIN_LATTE));
|
||||
pub static EXTENDED_CATPPUCCIN_LATTE: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::CATPPUCCIN_LATTE));
|
||||
|
||||
/// The built-in Catppuccin Frappé variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_CATPPUCCIN_FRAPPE: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::CATPPUCCIN_FRAPPE));
|
||||
pub static EXTENDED_CATPPUCCIN_FRAPPE: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::CATPPUCCIN_FRAPPE));
|
||||
|
||||
/// The built-in Catppuccin Macchiato variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_CATPPUCCIN_MACCHIATO: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::CATPPUCCIN_MACCHIATO));
|
||||
pub static EXTENDED_CATPPUCCIN_MACCHIATO: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::CATPPUCCIN_MACCHIATO));
|
||||
|
||||
/// The built-in Catppuccin Mocha variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_CATPPUCCIN_MOCHA: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::CATPPUCCIN_MOCHA));
|
||||
pub static EXTENDED_CATPPUCCIN_MOCHA: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::CATPPUCCIN_MOCHA));
|
||||
|
||||
/// The built-in Tokyo Night variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_TOKYO_NIGHT: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::TOKYO_NIGHT));
|
||||
pub static EXTENDED_TOKYO_NIGHT: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::TOKYO_NIGHT));
|
||||
|
||||
/// The built-in Tokyo Night Storm variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_TOKYO_NIGHT_STORM: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::TOKYO_NIGHT_STORM));
|
||||
pub static EXTENDED_TOKYO_NIGHT_STORM: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::TOKYO_NIGHT_STORM));
|
||||
|
||||
/// The built-in Tokyo Night variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_TOKYO_NIGHT_LIGHT: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::TOKYO_NIGHT_LIGHT));
|
||||
pub static EXTENDED_TOKYO_NIGHT_LIGHT: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::TOKYO_NIGHT_LIGHT));
|
||||
|
||||
/// The built-in Kanagawa Wave variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_KANAGAWA_WAVE: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::KANAGAWA_WAVE));
|
||||
pub static EXTENDED_KANAGAWA_WAVE: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::KANAGAWA_WAVE));
|
||||
|
||||
/// The built-in Kanagawa Dragon variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_KANAGAWA_DRAGON: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::KANAGAWA_DRAGON));
|
||||
pub static EXTENDED_KANAGAWA_DRAGON: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::KANAGAWA_DRAGON));
|
||||
|
||||
/// The built-in Kanagawa Lotus variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_KANAGAWA_LOTUS: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::KANAGAWA_LOTUS));
|
||||
pub static EXTENDED_KANAGAWA_LOTUS: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::KANAGAWA_LOTUS));
|
||||
|
||||
/// The built-in Moonfly variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_MOONFLY: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::MOONFLY));
|
||||
pub static EXTENDED_MOONFLY: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::MOONFLY));
|
||||
|
||||
/// The built-in Nightfly variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_NIGHTFLY: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::NIGHTFLY));
|
||||
pub static EXTENDED_NIGHTFLY: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::NIGHTFLY));
|
||||
|
||||
/// The built-in Oxocarbon variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_OXOCARBON: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::OXOCARBON));
|
||||
pub static EXTENDED_OXOCARBON: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::OXOCARBON));
|
||||
|
||||
/// The built-in Ferra variant of an [`Extended`] palette.
|
||||
pub static EXTENDED_FERRA: Lazy<Extended> =
|
||||
Lazy::new(|| Extended::generate(Palette::FERRA));
|
||||
pub static EXTENDED_FERRA: LazyLock<Extended> =
|
||||
LazyLock::new(|| Extended::generate(Palette::FERRA));
|
||||
|
||||
impl Extended {
|
||||
/// Generates an [`Extended`] palette from a simple [`Palette`].
|
||||
|
|
@ -411,6 +410,11 @@ impl Extended {
|
|||
palette.background,
|
||||
palette.text,
|
||||
),
|
||||
warning: Warning::generate(
|
||||
palette.warning,
|
||||
palette.background,
|
||||
palette.text,
|
||||
),
|
||||
danger: Danger::generate(
|
||||
palette.danger,
|
||||
palette.background,
|
||||
|
|
@ -450,22 +454,30 @@ impl Pair {
|
|||
pub struct Background {
|
||||
/// The base background color.
|
||||
pub base: Pair,
|
||||
/// The weakest version of the base background color.
|
||||
pub weakest: Pair,
|
||||
/// A weaker version of the base background color.
|
||||
pub weak: Pair,
|
||||
/// A stronger version of the base background color.
|
||||
pub strong: Pair,
|
||||
/// The strongest version of the base background color.
|
||||
pub strongest: Pair,
|
||||
}
|
||||
|
||||
impl Background {
|
||||
/// Generates a set of [`Background`] colors from the base and text colors.
|
||||
pub fn new(base: Color, text: Color) -> Self {
|
||||
let weak = mix(base, text, 0.15);
|
||||
let strong = mix(base, text, 0.40);
|
||||
let weakest = deviate(base, 0.03);
|
||||
let weak = muted(deviate(base, 0.1));
|
||||
let strong = muted(deviate(base, 0.2));
|
||||
let strongest = muted(deviate(base, 0.3));
|
||||
|
||||
Self {
|
||||
base: Pair::new(base, text),
|
||||
weakest: Pair::new(weakest, text),
|
||||
weak: Pair::new(weak, text),
|
||||
strong: Pair::new(strong, text),
|
||||
strongest: Pair::new(strongest, text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -546,6 +558,31 @@ impl Success {
|
|||
}
|
||||
}
|
||||
|
||||
/// A set of warning colors.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Warning {
|
||||
/// The base warning color.
|
||||
pub base: Pair,
|
||||
/// A weaker version of the base warning color.
|
||||
pub weak: Pair,
|
||||
/// A stronger version of the base warning color.
|
||||
pub strong: Pair,
|
||||
}
|
||||
|
||||
impl Warning {
|
||||
/// Generates a set of [`Warning`] colors from the base, background, and text colors.
|
||||
pub fn generate(base: Color, background: Color, text: Color) -> Self {
|
||||
let weak = mix(base, background, 0.4);
|
||||
let strong = deviate(base, 0.1);
|
||||
|
||||
Self {
|
||||
base: Pair::new(base, text),
|
||||
weak: Pair::new(weak, text),
|
||||
strong: Pair::new(strong, text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of danger colors.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Danger {
|
||||
|
|
@ -599,10 +636,18 @@ fn deviate(color: Color, amount: f32) -> Color {
|
|||
if is_dark(color) {
|
||||
lighten(color, amount)
|
||||
} else {
|
||||
darken(color, amount)
|
||||
darken(color, amount * 0.8)
|
||||
}
|
||||
}
|
||||
|
||||
fn muted(color: Color) -> Color {
|
||||
let mut hsl = to_hsl(color);
|
||||
|
||||
hsl.saturation = hsl.saturation.min(0.5);
|
||||
|
||||
from_hsl(hsl)
|
||||
}
|
||||
|
||||
fn mix(a: Color, b: Color, factor: f32) -> Color {
|
||||
let a_lin = Rgb::from(a).into_linear();
|
||||
let b_lin = Rgb::from(b).into_linear();
|
||||
|
|
@ -613,16 +658,25 @@ fn mix(a: Color, b: Color, factor: f32) -> Color {
|
|||
|
||||
fn readable(background: Color, text: Color) -> Color {
|
||||
if is_readable(background, text) {
|
||||
text
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
|
||||
let improve = if is_dark(background) { lighten } else { darken };
|
||||
|
||||
// TODO: Compute factor from relative contrast value
|
||||
let candidate = improve(text, 0.1);
|
||||
|
||||
if is_readable(background, candidate) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
let white_contrast = relative_contrast(background, Color::WHITE);
|
||||
let black_contrast = relative_contrast(background, Color::BLACK);
|
||||
|
||||
if white_contrast >= black_contrast {
|
||||
Color::WHITE
|
||||
mix(Color::WHITE, background, 0.05)
|
||||
} else {
|
||||
Color::BLACK
|
||||
}
|
||||
mix(Color::BLACK, background, 0.05)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,3 +3,28 @@
|
|||
pub use web_time::Duration;
|
||||
pub use web_time::Instant;
|
||||
pub use web_time::SystemTime;
|
||||
|
||||
/// Creates a [`Duration`] representing the given amount of milliseconds.
|
||||
pub fn milliseconds(milliseconds: u64) -> Duration {
|
||||
Duration::from_millis(milliseconds)
|
||||
}
|
||||
|
||||
/// Creates a [`Duration`] representing the given amount of seconds.
|
||||
pub fn seconds(seconds: u64) -> Duration {
|
||||
Duration::from_secs(seconds)
|
||||
}
|
||||
|
||||
/// Creates a [`Duration`] representing the given amount of minutes.
|
||||
pub fn minutes(minutes: u64) -> Duration {
|
||||
seconds(minutes * 60)
|
||||
}
|
||||
|
||||
/// Creates a [`Duration`] representing the given amount of hours.
|
||||
pub fn hours(hours: u64) -> Duration {
|
||||
minutes(hours * 60)
|
||||
}
|
||||
|
||||
/// Creates a [`Duration`] representing the given amount of days.
|
||||
pub fn days(days: u64) -> Duration {
|
||||
hours(days * 24)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,17 @@ impl<T> Vector<T> {
|
|||
impl Vector {
|
||||
/// The zero [`Vector`].
|
||||
pub const ZERO: Self = Self::new(0.0, 0.0);
|
||||
}
|
||||
|
||||
/// The unit [`Vector`].
|
||||
pub const UNIT: Self = Self::new(0.0, 0.0);
|
||||
impl<T> std::ops::Neg for Vector<T>
|
||||
where
|
||||
T: std::ops::Neg<Output = T>,
|
||||
{
|
||||
type Output = Self;
|
||||
|
||||
fn neg(self) -> Self::Output {
|
||||
Self::new(-self.x, -self.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Add for Vector<T>
|
||||
|
|
|
|||
|
|
@ -10,12 +10,11 @@ pub use operation::Operation;
|
|||
pub use text::Text;
|
||||
pub use tree::Tree;
|
||||
|
||||
use crate::event::{self, Event};
|
||||
use crate::layout::{self, Layout};
|
||||
use crate::mouse;
|
||||
use crate::overlay;
|
||||
use crate::renderer;
|
||||
use crate::{Clipboard, Length, Rectangle, Shell, Size, Vector};
|
||||
use crate::{Clipboard, Event, Length, Rectangle, Shell, Size, Vector};
|
||||
|
||||
/// A component that displays information and allows interaction.
|
||||
///
|
||||
|
|
@ -33,12 +32,12 @@ use crate::{Clipboard, Length, Rectangle, Shell, Size, Vector};
|
|||
/// - [`geometry`], a custom widget showcasing how to draw geometry with the
|
||||
/// `Mesh2D` primitive in [`iced_wgpu`].
|
||||
///
|
||||
/// [examples]: https://github.com/iced-rs/iced/tree/0.12/examples
|
||||
/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.12/examples/bezier_tool
|
||||
/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.12/examples/custom_widget
|
||||
/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.12/examples/geometry
|
||||
/// [examples]: https://github.com/iced-rs/iced/tree/0.13/examples
|
||||
/// [`bezier_tool`]: https://github.com/iced-rs/iced/tree/0.13/examples/bezier_tool
|
||||
/// [`custom_widget`]: https://github.com/iced-rs/iced/tree/0.13/examples/custom_widget
|
||||
/// [`geometry`]: https://github.com/iced-rs/iced/tree/0.13/examples/geometry
|
||||
/// [`lyon`]: https://github.com/nical/lyon
|
||||
/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.12/wgpu
|
||||
/// [`iced_wgpu`]: https://github.com/iced-rs/iced/tree/0.13/wgpu
|
||||
pub trait Widget<Message, Theme, Renderer>
|
||||
where
|
||||
Renderer: crate::Renderer,
|
||||
|
|
@ -96,7 +95,7 @@ where
|
|||
Vec::new()
|
||||
}
|
||||
|
||||
/// Reconciliates the [`Widget`] with the provided [`Tree`].
|
||||
/// Reconciles the [`Widget`] with the provided [`Tree`].
|
||||
fn diff(&self, _tree: &mut Tree) {}
|
||||
|
||||
/// Applies an [`Operation`] to the [`Widget`].
|
||||
|
|
@ -105,25 +104,24 @@ where
|
|||
_state: &mut Tree,
|
||||
_layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
_operation: &mut dyn Operation<Message>,
|
||||
_operation: &mut dyn Operation,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Processes a runtime [`Event`].
|
||||
///
|
||||
/// By default, it does nothing.
|
||||
fn on_event(
|
||||
fn update(
|
||||
&mut self,
|
||||
_state: &mut Tree,
|
||||
_event: Event,
|
||||
_event: &Event,
|
||||
_layout: Layout<'_>,
|
||||
_cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
_shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
event::Status::Ignored
|
||||
) {
|
||||
}
|
||||
|
||||
/// Returns the current [`mouse::Interaction`] of the [`Widget`].
|
||||
|
|
|
|||
|
|
@ -12,11 +12,12 @@ use crate::{Rectangle, Vector};
|
|||
|
||||
use std::any::Any;
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A piece of logic that can traverse the widget tree of an application in
|
||||
/// order to query or update some widget state.
|
||||
pub trait Operation<T> {
|
||||
pub trait Operation<T = ()>: Send {
|
||||
/// Operates on a widget that contains other widgets.
|
||||
///
|
||||
/// The `operate_on_children` function can be called to return control to
|
||||
|
|
@ -29,23 +30,45 @@ pub trait Operation<T> {
|
|||
);
|
||||
|
||||
/// Operates on a widget that can be focused.
|
||||
fn focusable(&mut self, _state: &mut dyn Focusable, _id: Option<&Id>) {}
|
||||
fn focusable(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_state: &mut dyn Focusable,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Operates on a widget that can be scrolled.
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
_state: &mut dyn Scrollable,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_content_bounds: Rectangle,
|
||||
_translation: Vector,
|
||||
_state: &mut dyn Scrollable,
|
||||
) {
|
||||
}
|
||||
|
||||
/// 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,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_state: &mut dyn TextInput,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Operates on a widget that contains some text.
|
||||
fn text(&mut self, _id: Option<&Id>, _bounds: Rectangle, _text: &str) {}
|
||||
|
||||
/// Operates on a custom widget with some state.
|
||||
fn custom(&mut self, _state: &mut dyn Any, _id: Option<&Id>) {}
|
||||
fn custom(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_state: &mut dyn Any,
|
||||
) {
|
||||
}
|
||||
|
||||
/// Finishes the [`Operation`] and returns its [`Outcome`].
|
||||
fn finish(&self) -> Outcome<T> {
|
||||
|
|
@ -53,6 +76,72 @@ pub trait Operation<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T, O> Operation<O> for Box<T>
|
||||
where
|
||||
T: Operation<O> + ?Sized,
|
||||
{
|
||||
fn container(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<O>),
|
||||
) {
|
||||
self.as_mut().container(id, bounds, operate_on_children);
|
||||
}
|
||||
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
self.as_mut().focusable(id, bounds, state);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
self.as_mut().scrollable(
|
||||
id,
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
self.as_mut().text_input(id, bounds, state);
|
||||
}
|
||||
|
||||
fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) {
|
||||
self.as_mut().text(id, bounds, text);
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Any,
|
||||
) {
|
||||
self.as_mut().custom(id, bounds, state);
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<O> {
|
||||
self.as_ref().finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of an [`Operation`].
|
||||
pub enum Outcome<T> {
|
||||
/// The [`Operation`] produced no result.
|
||||
|
|
@ -78,23 +167,103 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// Wraps the [`Operation`] in a black box, erasing its returning type.
|
||||
pub fn black_box<'a, T, O>(
|
||||
operation: &'a mut dyn Operation<T>,
|
||||
) -> impl Operation<O> + 'a
|
||||
where
|
||||
T: 'a,
|
||||
{
|
||||
struct BlackBox<'a, T> {
|
||||
operation: &'a mut dyn Operation<T>,
|
||||
}
|
||||
|
||||
impl<T, O> Operation<O> for BlackBox<'_, T> {
|
||||
fn container(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<O>),
|
||||
) {
|
||||
self.operation.container(id, bounds, &mut |operation| {
|
||||
operate_on_children(&mut BlackBox { operation });
|
||||
});
|
||||
}
|
||||
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
self.operation.focusable(id, bounds, state);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
self.operation.scrollable(
|
||||
id,
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
self.operation.text_input(id, bounds, state);
|
||||
}
|
||||
|
||||
fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) {
|
||||
self.operation.text(id, bounds, text);
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Any,
|
||||
) {
|
||||
self.operation.custom(id, bounds, state);
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<O> {
|
||||
Outcome::None
|
||||
}
|
||||
}
|
||||
|
||||
BlackBox { operation }
|
||||
}
|
||||
|
||||
/// Maps the output of an [`Operation`] using the given function.
|
||||
pub fn map<A, B>(
|
||||
operation: Box<dyn Operation<A>>,
|
||||
f: impl Fn(A) -> B + 'static,
|
||||
operation: impl Operation<A>,
|
||||
f: impl Fn(A) -> B + Send + Sync + 'static,
|
||||
) -> impl Operation<B>
|
||||
where
|
||||
A: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
#[allow(missing_debug_implementations)]
|
||||
struct Map<A, B> {
|
||||
operation: Box<dyn Operation<A>>,
|
||||
f: Rc<dyn Fn(A) -> B>,
|
||||
struct Map<O, A, B> {
|
||||
operation: O,
|
||||
f: Arc<dyn Fn(A) -> B + Send + Sync>,
|
||||
}
|
||||
|
||||
impl<A, B> Operation<B> for Map<A, B>
|
||||
impl<O, A, B> Operation<B> for Map<O, A, B>
|
||||
where
|
||||
O: Operation<A>,
|
||||
A: 'static,
|
||||
B: 'static,
|
||||
{
|
||||
|
|
@ -108,7 +277,7 @@ where
|
|||
operation: &'a mut dyn Operation<A>,
|
||||
}
|
||||
|
||||
impl<'a, A, B> Operation<B> for MapRef<'a, A> {
|
||||
impl<A, B> Operation<B> for MapRef<'_, A> {
|
||||
fn container(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
|
|
@ -124,63 +293,109 @@ where
|
|||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
self.operation.scrollable(state, id, bounds, translation);
|
||||
self.operation.scrollable(
|
||||
id,
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn focusable(
|
||||
&mut self,
|
||||
state: &mut dyn Focusable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
self.operation.focusable(state, id);
|
||||
self.operation.focusable(id, bounds, state);
|
||||
}
|
||||
|
||||
fn text_input(
|
||||
&mut self,
|
||||
state: &mut dyn TextInput,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
self.operation.text_input(state, id);
|
||||
self.operation.text_input(id, bounds, state);
|
||||
}
|
||||
|
||||
fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) {
|
||||
self.operation.custom(state, id);
|
||||
fn text(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
text: &str,
|
||||
) {
|
||||
self.operation.text(id, bounds, text);
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Any,
|
||||
) {
|
||||
self.operation.custom(id, bounds, state);
|
||||
}
|
||||
}
|
||||
|
||||
let Self { operation, .. } = self;
|
||||
|
||||
MapRef {
|
||||
operation: operation.as_mut(),
|
||||
}
|
||||
.container(id, bounds, operate_on_children);
|
||||
MapRef { operation }.container(id, bounds, operate_on_children);
|
||||
}
|
||||
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
self.operation.focusable(state, id);
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
self.operation.focusable(id, bounds, state);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
self.operation.scrollable(state, id, bounds, translation);
|
||||
self.operation.scrollable(
|
||||
id,
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
self.operation.text_input(state, id);
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
self.operation.text_input(id, bounds, state);
|
||||
}
|
||||
|
||||
fn custom(&mut self, state: &mut dyn Any, id: Option<&Id>) {
|
||||
self.operation.custom(state, id);
|
||||
fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) {
|
||||
self.operation.text(id, bounds, text);
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Any,
|
||||
) {
|
||||
self.operation.custom(id, bounds, state);
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<B> {
|
||||
|
|
@ -197,7 +412,114 @@ where
|
|||
|
||||
Map {
|
||||
operation,
|
||||
f: Rc::new(f),
|
||||
f: Arc::new(f),
|
||||
}
|
||||
}
|
||||
|
||||
/// Chains the output of an [`Operation`] with the provided function to
|
||||
/// build a new [`Operation`].
|
||||
pub fn then<A, B, O>(
|
||||
operation: impl Operation<A> + 'static,
|
||||
f: fn(A) -> O,
|
||||
) -> impl Operation<B>
|
||||
where
|
||||
A: 'static,
|
||||
B: Send + 'static,
|
||||
O: Operation<B> + 'static,
|
||||
{
|
||||
struct Chain<T, O, A, B>
|
||||
where
|
||||
T: Operation<A>,
|
||||
O: Operation<B>,
|
||||
{
|
||||
operation: T,
|
||||
next: fn(A) -> O,
|
||||
_result: PhantomData<B>,
|
||||
}
|
||||
|
||||
impl<T, O, A, B> Operation<B> for Chain<T, O, A, B>
|
||||
where
|
||||
T: Operation<A> + 'static,
|
||||
O: Operation<B> + 'static,
|
||||
A: 'static,
|
||||
B: Send + 'static,
|
||||
{
|
||||
fn container(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<B>),
|
||||
) {
|
||||
self.operation.container(id, bounds, &mut |operation| {
|
||||
operate_on_children(&mut black_box(operation));
|
||||
});
|
||||
}
|
||||
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
self.operation.focusable(id, bounds, state);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
translation: crate::Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
self.operation.scrollable(
|
||||
id,
|
||||
bounds,
|
||||
content_bounds,
|
||||
translation,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
self.operation.text_input(id, bounds, state);
|
||||
}
|
||||
|
||||
fn text(&mut self, id: Option<&Id>, bounds: Rectangle, text: &str) {
|
||||
self.operation.text(id, bounds, text);
|
||||
}
|
||||
|
||||
fn custom(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
state: &mut dyn Any,
|
||||
) {
|
||||
self.operation.custom(id, bounds, state);
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<B> {
|
||||
match self.operation.finish() {
|
||||
Outcome::None => Outcome::None,
|
||||
Outcome::Some(value) => {
|
||||
Outcome::Chain(Box::new((self.next)(value)))
|
||||
}
|
||||
Outcome::Chain(operation) => {
|
||||
Outcome::Chain(Box::new(then(operation, self.next)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Chain {
|
||||
operation,
|
||||
next: f,
|
||||
_result: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Operate on widgets that can be focused.
|
||||
use crate::widget::operation::{Operation, Outcome};
|
||||
use crate::widget::Id;
|
||||
use crate::Rectangle;
|
||||
use crate::widget::Id;
|
||||
use crate::widget::operation::{self, Operation, Outcome};
|
||||
|
||||
/// The internal state of a widget that can be focused.
|
||||
pub trait Focusable {
|
||||
|
|
@ -32,7 +32,12 @@ pub fn focus<T>(target: Id) -> impl Operation<T> {
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for Focus {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.focus();
|
||||
|
|
@ -56,22 +61,47 @@ pub fn focus<T>(target: Id) -> impl Operation<T> {
|
|||
Focus { target }
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that generates a [`Count`] and chains it with the
|
||||
/// provided function to build a new [`Operation`].
|
||||
pub fn count<T, O>(f: fn(Count) -> O) -> impl Operation<T>
|
||||
where
|
||||
O: Operation<T> + 'static,
|
||||
{
|
||||
struct CountFocusable<O> {
|
||||
count: Count,
|
||||
next: fn(Count) -> O,
|
||||
/// Produces an [`Operation`] that unfocuses the focused widget.
|
||||
pub fn unfocus<T>() -> impl Operation<T> {
|
||||
struct Unfocus;
|
||||
|
||||
impl<T> Operation<T> for Unfocus {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
state.unfocus();
|
||||
}
|
||||
|
||||
impl<T, O> Operation<T> for CountFocusable<O>
|
||||
where
|
||||
O: Operation<T> + 'static,
|
||||
{
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
|
||||
fn container(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
||||
) {
|
||||
operate_on_children(self);
|
||||
}
|
||||
}
|
||||
|
||||
Unfocus
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that generates a [`Count`] and chains it with the
|
||||
/// provided function to build a new [`Operation`].
|
||||
pub fn count() -> impl Operation<Count> {
|
||||
struct CountFocusable {
|
||||
count: Count,
|
||||
}
|
||||
|
||||
impl Operation<Count> for CountFocusable {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
if state.is_focused() {
|
||||
self.count.focused = Some(self.count.total);
|
||||
}
|
||||
|
|
@ -83,33 +113,40 @@ where
|
|||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<Count>),
|
||||
) {
|
||||
operate_on_children(self);
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<T> {
|
||||
Outcome::Chain(Box::new((self.next)(self.count)))
|
||||
fn finish(&self) -> Outcome<Count> {
|
||||
Outcome::Some(self.count)
|
||||
}
|
||||
}
|
||||
|
||||
CountFocusable {
|
||||
count: Count::default(),
|
||||
next: f,
|
||||
}
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that searches for the current focused widget, and
|
||||
/// - if found, focuses the previous focusable widget.
|
||||
/// - if not found, focuses the last focusable widget.
|
||||
pub fn focus_previous<T>() -> impl Operation<T> {
|
||||
pub fn focus_previous<T>() -> impl Operation<T>
|
||||
where
|
||||
T: Send + 'static,
|
||||
{
|
||||
struct FocusPrevious {
|
||||
count: Count,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
impl<T> Operation<T> for FocusPrevious {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
if self.count.total == 0 {
|
||||
return;
|
||||
}
|
||||
|
|
@ -136,20 +173,28 @@ pub fn focus_previous<T>() -> impl Operation<T> {
|
|||
}
|
||||
}
|
||||
|
||||
count(|count| FocusPrevious { count, current: 0 })
|
||||
operation::then(count(), |count| FocusPrevious { count, current: 0 })
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that searches for the current focused widget, and
|
||||
/// - if found, focuses the next focusable widget.
|
||||
/// - if not found, focuses the first focusable widget.
|
||||
pub fn focus_next<T>() -> impl Operation<T> {
|
||||
pub fn focus_next<T>() -> impl Operation<T>
|
||||
where
|
||||
T: Send + 'static,
|
||||
{
|
||||
struct FocusNext {
|
||||
count: Count,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
impl<T> Operation<T> for FocusNext {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
match self.count.focused {
|
||||
None if self.current == 0 => state.focus(),
|
||||
Some(focused) if focused == self.current => state.unfocus(),
|
||||
|
|
@ -170,7 +215,7 @@ pub fn focus_next<T>() -> impl Operation<T> {
|
|||
}
|
||||
}
|
||||
|
||||
count(|count| FocusNext { count, current: 0 })
|
||||
operation::then(count(), |count| FocusNext { count, current: 0 })
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that searches for the current focused widget
|
||||
|
|
@ -181,7 +226,12 @@ pub fn find_focused() -> impl Operation<Id> {
|
|||
}
|
||||
|
||||
impl Operation<Id> for FindFocused {
|
||||
fn focusable(&mut self, state: &mut dyn Focusable, id: Option<&Id>) {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
if state.is_focused() && id.is_some() {
|
||||
self.focused = id.cloned();
|
||||
}
|
||||
|
|
@ -207,3 +257,48 @@ pub fn find_focused() -> impl Operation<Id> {
|
|||
|
||||
FindFocused { focused: None }
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that searches for the focusable widget
|
||||
/// and stores whether it is focused or not. This ignores widgets that
|
||||
/// do not have an ID.
|
||||
pub fn is_focused(target: Id) -> impl Operation<bool> {
|
||||
struct IsFocused {
|
||||
target: Id,
|
||||
is_focused: Option<bool>,
|
||||
}
|
||||
|
||||
impl Operation<bool> for IsFocused {
|
||||
fn focusable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn Focusable,
|
||||
) {
|
||||
if id.is_some_and(|id| *id == self.target) {
|
||||
self.is_focused = Some(state.is_focused());
|
||||
}
|
||||
}
|
||||
|
||||
fn container(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<bool>),
|
||||
) {
|
||||
if self.is_focused.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
operate_on_children(self);
|
||||
}
|
||||
|
||||
fn finish(&self) -> Outcome<bool> {
|
||||
self.is_focused.map_or(Outcome::None, Outcome::Some)
|
||||
}
|
||||
}
|
||||
|
||||
IsFocused {
|
||||
target,
|
||||
is_focused: None,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,14 @@ pub trait Scrollable {
|
|||
|
||||
/// Scroll the widget to the given [`AbsoluteOffset`] along the horizontal & vertical axis.
|
||||
fn scroll_to(&mut self, offset: AbsoluteOffset);
|
||||
|
||||
/// Scroll the widget by the given [`AbsoluteOffset`] along the horizontal & vertical axis.
|
||||
fn scroll_by(
|
||||
&mut self,
|
||||
offset: AbsoluteOffset,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
);
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that snaps the widget with the given [`Id`] to
|
||||
|
|
@ -31,10 +39,11 @@ pub fn snap_to<T>(target: Id, offset: RelativeOffset) -> impl Operation<T> {
|
|||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_content_bounds: Rectangle,
|
||||
_translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
if Some(&self.target) == id {
|
||||
state.snap_to(self.offset);
|
||||
|
|
@ -65,10 +74,11 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
|
|||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
state: &mut dyn Scrollable,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
_content_bounds: Rectangle,
|
||||
_translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
if Some(&self.target) == id {
|
||||
state.scroll_to(self.offset);
|
||||
|
|
@ -79,6 +89,41 @@ pub fn scroll_to<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
|
|||
ScrollTo { target, offset }
|
||||
}
|
||||
|
||||
/// Produces an [`Operation`] that scrolls the widget with the given [`Id`] by
|
||||
/// the provided [`AbsoluteOffset`].
|
||||
pub fn scroll_by<T>(target: Id, offset: AbsoluteOffset) -> impl Operation<T> {
|
||||
struct ScrollBy {
|
||||
target: Id,
|
||||
offset: AbsoluteOffset,
|
||||
}
|
||||
|
||||
impl<T> Operation<T> for ScrollBy {
|
||||
fn container(
|
||||
&mut self,
|
||||
_id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
operate_on_children: &mut dyn FnMut(&mut dyn Operation<T>),
|
||||
) {
|
||||
operate_on_children(self);
|
||||
}
|
||||
|
||||
fn scrollable(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
bounds: Rectangle,
|
||||
content_bounds: Rectangle,
|
||||
_translation: Vector,
|
||||
state: &mut dyn Scrollable,
|
||||
) {
|
||||
if Some(&self.target) == id {
|
||||
state.scroll_by(self.offset, bounds, content_bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBy { target, offset }
|
||||
}
|
||||
|
||||
/// The amount of absolute offset in each direction of a [`Scrollable`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct AbsoluteOffset {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! Operate on widgets that have text input.
|
||||
use crate::widget::operation::Operation;
|
||||
use crate::widget::Id;
|
||||
use crate::Rectangle;
|
||||
use crate::widget::Id;
|
||||
use crate::widget::operation::Operation;
|
||||
|
||||
/// The internal state of a widget that has text input.
|
||||
pub trait TextInput {
|
||||
|
|
@ -23,7 +23,12 @@ pub fn move_cursor_to_front<T>(target: Id) -> impl Operation<T> {
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for MoveCursor {
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.move_cursor_to_front();
|
||||
|
|
@ -53,7 +58,12 @@ pub fn move_cursor_to_end<T>(target: Id) -> impl Operation<T> {
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for MoveCursor {
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.move_cursor_to_end();
|
||||
|
|
@ -84,7 +94,12 @@ pub fn move_cursor_to<T>(target: Id, position: usize) -> impl Operation<T> {
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for MoveCursor {
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.move_cursor_to(self.position);
|
||||
|
|
@ -113,7 +128,12 @@ pub fn select_all<T>(target: Id) -> impl Operation<T> {
|
|||
}
|
||||
|
||||
impl<T> Operation<T> for MoveCursor {
|
||||
fn text_input(&mut self, state: &mut dyn TextInput, id: Option<&Id>) {
|
||||
fn text_input(
|
||||
&mut self,
|
||||
id: Option<&Id>,
|
||||
_bounds: Rectangle,
|
||||
state: &mut dyn TextInput,
|
||||
) {
|
||||
match id {
|
||||
Some(id) if id == &self.target => {
|
||||
state.select_all();
|
||||
|
|
|
|||
|
|
@ -1,27 +1,68 @@
|
|||
//! Write some text for your users to read.
|
||||
//! Text widgets display information through writing.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub fn text<T>(t: T) -> iced_core::widget::Text<'static, iced_core::Theme, ()> { unimplemented!() } }
|
||||
//! # pub use iced_core::color; }
|
||||
//! # pub type State = ();
|
||||
//! # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, ()>;
|
||||
//! use iced::widget::text;
|
||||
//! use iced::color;
|
||||
//!
|
||||
//! enum Message {
|
||||
//! // ...
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! text("Hello, this is iced!")
|
||||
//! .size(20)
|
||||
//! .color(color!(0x0000ff))
|
||||
//! .into()
|
||||
//! }
|
||||
//! ```
|
||||
use crate::alignment;
|
||||
use crate::layout;
|
||||
use crate::mouse;
|
||||
use crate::renderer;
|
||||
use crate::text::{self, Paragraph};
|
||||
use crate::text;
|
||||
use crate::text::paragraph::{self, Paragraph};
|
||||
use crate::widget::tree::{self, Tree};
|
||||
use crate::{
|
||||
Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Theme,
|
||||
Widget,
|
||||
};
|
||||
|
||||
use std::borrow::Cow;
|
||||
pub use text::{LineHeight, Shaping, Wrapping};
|
||||
|
||||
pub use text::{LineHeight, Shaping};
|
||||
|
||||
/// A paragraph of text.
|
||||
/// A bunch of text.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub fn text<T>(t: T) -> iced_core::widget::Text<'static, iced_core::Theme, ()> { unimplemented!() } }
|
||||
/// # pub use iced_core::color; }
|
||||
/// # pub type State = ();
|
||||
/// # pub type Element<'a, Message> = iced_core::Element<'a, Message, iced_core::Theme, ()>;
|
||||
/// use iced::widget::text;
|
||||
/// use iced::color;
|
||||
///
|
||||
/// enum Message {
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// text("Hello, this is iced!")
|
||||
/// .size(20)
|
||||
/// .color(color!(0x0000ff))
|
||||
/// .into()
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct Text<'a, Theme, Renderer>
|
||||
where
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
fragment: Fragment<'a>,
|
||||
fragment: text::Fragment<'a>,
|
||||
size: Option<Pixels>,
|
||||
line_height: LineHeight,
|
||||
width: Length,
|
||||
|
|
@ -30,6 +71,7 @@ where
|
|||
vertical_alignment: alignment::Vertical,
|
||||
font: Option<Renderer::Font>,
|
||||
shaping: Shaping,
|
||||
wrapping: Wrapping,
|
||||
class: Theme::Class<'a>,
|
||||
}
|
||||
|
||||
|
|
@ -39,7 +81,7 @@ where
|
|||
Renderer: text::Renderer,
|
||||
{
|
||||
/// Create a new fragment of [`Text`] with the given contents.
|
||||
pub fn new(fragment: impl IntoFragment<'a>) -> Self {
|
||||
pub fn new(fragment: impl text::IntoFragment<'a>) -> Self {
|
||||
Text {
|
||||
fragment: fragment.into_fragment(),
|
||||
size: None,
|
||||
|
|
@ -49,7 +91,8 @@ where
|
|||
height: Length::Shrink,
|
||||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Top,
|
||||
shaping: Shaping::Basic,
|
||||
shaping: Shaping::default(),
|
||||
wrapping: Wrapping::default(),
|
||||
class: Theme::default(),
|
||||
}
|
||||
}
|
||||
|
|
@ -86,21 +129,27 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Centers the [`Text`], both horizontally and vertically.
|
||||
pub fn center(self) -> Self {
|
||||
self.align_x(alignment::Horizontal::Center)
|
||||
.align_y(alignment::Vertical::Center)
|
||||
}
|
||||
|
||||
/// Sets the [`alignment::Horizontal`] of the [`Text`].
|
||||
pub fn horizontal_alignment(
|
||||
pub fn align_x(
|
||||
mut self,
|
||||
alignment: alignment::Horizontal,
|
||||
alignment: impl Into<alignment::Horizontal>,
|
||||
) -> Self {
|
||||
self.horizontal_alignment = alignment;
|
||||
self.horizontal_alignment = alignment.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`alignment::Vertical`] of the [`Text`].
|
||||
pub fn vertical_alignment(
|
||||
pub fn align_y(
|
||||
mut self,
|
||||
alignment: alignment::Vertical,
|
||||
alignment: impl Into<alignment::Vertical>,
|
||||
) -> Self {
|
||||
self.vertical_alignment = alignment;
|
||||
self.vertical_alignment = alignment.into();
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -110,6 +159,12 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Wrapping`] strategy of the [`Text`].
|
||||
pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
|
||||
self.wrapping = wrapping;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Text`].
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
|
||||
|
|
@ -149,10 +204,10 @@ where
|
|||
|
||||
/// The internal state of a [`Text`] widget.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct State<P: Paragraph>(P);
|
||||
pub struct State<P: Paragraph>(pub paragraph::Plain<P>);
|
||||
|
||||
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Text<'a, Theme, Renderer>
|
||||
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for Text<'_, Theme, Renderer>
|
||||
where
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
|
|
@ -162,7 +217,9 @@ where
|
|||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State(Renderer::Paragraph::default()))
|
||||
tree::State::new(State::<Renderer::Paragraph>(
|
||||
paragraph::Plain::default(),
|
||||
))
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
|
|
@ -191,6 +248,7 @@ where
|
|||
self.horizontal_alignment,
|
||||
self.vertical_alignment,
|
||||
self.shaping,
|
||||
self.wrapping,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -207,7 +265,17 @@ where
|
|||
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
|
||||
let style = theme.style(&self.class);
|
||||
|
||||
draw(renderer, defaults, layout, state, style, viewport);
|
||||
draw(renderer, defaults, layout, state.0.raw(), style, viewport);
|
||||
}
|
||||
|
||||
fn operate(
|
||||
&self,
|
||||
_state: &mut Tree,
|
||||
layout: Layout<'_>,
|
||||
_renderer: &Renderer,
|
||||
operation: &mut dyn super::Operation,
|
||||
) {
|
||||
operation.text(None, layout.bounds(), &self.fragment);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -225,6 +293,7 @@ pub fn layout<Renderer>(
|
|||
horizontal_alignment: alignment::Horizontal,
|
||||
vertical_alignment: alignment::Vertical,
|
||||
shaping: Shaping,
|
||||
wrapping: Wrapping,
|
||||
) -> layout::Node
|
||||
where
|
||||
Renderer: text::Renderer,
|
||||
|
|
@ -235,7 +304,7 @@ where
|
|||
let size = size.unwrap_or_else(|| renderer.default_size());
|
||||
let font = font.unwrap_or_else(|| renderer.default_font());
|
||||
|
||||
let State(ref mut paragraph) = state;
|
||||
let State(paragraph) = state;
|
||||
|
||||
paragraph.update(text::Text {
|
||||
content,
|
||||
|
|
@ -246,6 +315,7 @@ where
|
|||
horizontal_alignment,
|
||||
vertical_alignment,
|
||||
shaping,
|
||||
wrapping,
|
||||
});
|
||||
|
||||
paragraph.min_bounds()
|
||||
|
|
@ -266,13 +336,12 @@ pub fn draw<Renderer>(
|
|||
renderer: &mut Renderer,
|
||||
style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
state: &State<Renderer::Paragraph>,
|
||||
paragraph: &Renderer::Paragraph,
|
||||
appearance: Style,
|
||||
viewport: &Rectangle,
|
||||
) where
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
let State(ref paragraph) = state;
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let x = match paragraph.horizontal_alignment() {
|
||||
|
|
@ -330,7 +399,7 @@ where
|
|||
}
|
||||
|
||||
/// The appearance of some text.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
pub struct Style {
|
||||
/// The [`Color`] of the text.
|
||||
///
|
||||
|
|
@ -367,80 +436,42 @@ impl Catalog for Theme {
|
|||
}
|
||||
}
|
||||
|
||||
/// A fragment of [`Text`].
|
||||
///
|
||||
/// This is just an alias to a string that may be either
|
||||
/// borrowed or owned.
|
||||
pub type Fragment<'a> = Cow<'a, str>;
|
||||
|
||||
/// A trait for converting a value to some text [`Fragment`].
|
||||
pub trait IntoFragment<'a> {
|
||||
/// Converts the value to some text [`Fragment`].
|
||||
fn into_fragment(self) -> Fragment<'a>;
|
||||
/// The default text styling; color is inherited.
|
||||
pub fn default(_theme: &Theme) -> Style {
|
||||
Style { color: None }
|
||||
}
|
||||
|
||||
impl<'a> IntoFragment<'a> for Fragment<'a> {
|
||||
fn into_fragment(self) -> Fragment<'a> {
|
||||
self
|
||||
/// Text with the default base color.
|
||||
pub fn base(theme: &Theme) -> Style {
|
||||
Style {
|
||||
color: Some(theme.palette().text),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> IntoFragment<'a> for &'a Fragment<'b> {
|
||||
fn into_fragment(self) -> Fragment<'a> {
|
||||
Fragment::Borrowed(self)
|
||||
/// Text conveying some important information, like an action.
|
||||
pub fn primary(theme: &Theme) -> Style {
|
||||
Style {
|
||||
color: Some(theme.palette().primary),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoFragment<'a> for &'a str {
|
||||
fn into_fragment(self) -> Fragment<'a> {
|
||||
Fragment::Borrowed(self)
|
||||
/// Text conveying some secondary information, like a footnote.
|
||||
pub fn secondary(theme: &Theme) -> Style {
|
||||
Style {
|
||||
color: Some(theme.extended_palette().secondary.strong.color),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoFragment<'a> for &'a String {
|
||||
fn into_fragment(self) -> Fragment<'a> {
|
||||
Fragment::Borrowed(self.as_str())
|
||||
/// Text conveying some positive information, like a successful event.
|
||||
pub fn success(theme: &Theme) -> Style {
|
||||
Style {
|
||||
color: Some(theme.palette().success),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoFragment<'a> for String {
|
||||
fn into_fragment(self) -> Fragment<'a> {
|
||||
Fragment::Owned(self)
|
||||
/// Text conveying some negative information, like an error.
|
||||
pub fn danger(theme: &Theme) -> Style {
|
||||
Style {
|
||||
color: Some(theme.palette().danger),
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! into_fragment {
|
||||
($type:ty) => {
|
||||
impl<'a> IntoFragment<'a> for $type {
|
||||
fn into_fragment(self) -> Fragment<'a> {
|
||||
Fragment::Owned(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoFragment<'a> for &$type {
|
||||
fn into_fragment(self) -> Fragment<'a> {
|
||||
Fragment::Owned(self.to_string())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
into_fragment!(char);
|
||||
into_fragment!(bool);
|
||||
|
||||
into_fragment!(u8);
|
||||
into_fragment!(u16);
|
||||
into_fragment!(u32);
|
||||
into_fragment!(u64);
|
||||
into_fragment!(u128);
|
||||
into_fragment!(usize);
|
||||
|
||||
into_fragment!(i8);
|
||||
into_fragment!(i16);
|
||||
into_fragment!(i32);
|
||||
into_fragment!(i64);
|
||||
into_fragment!(i128);
|
||||
into_fragment!(isize);
|
||||
|
||||
into_fragment!(f32);
|
||||
into_fragment!(f64);
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ impl Tree {
|
|||
}
|
||||
}
|
||||
|
||||
/// Reconciliates the current tree with the provided [`Widget`].
|
||||
/// Reconciles the current tree with the provided [`Widget`].
|
||||
///
|
||||
/// If the tag of the [`Widget`] matches the tag of the [`Tree`], then the
|
||||
/// [`Widget`] proceeds with the reconciliation (i.e. [`Widget::diff`] is called).
|
||||
|
|
@ -81,7 +81,7 @@ impl Tree {
|
|||
);
|
||||
}
|
||||
|
||||
/// Reconciliates the children of the tree with the provided list of widgets using custom
|
||||
/// Reconciles the children of the tree with the provided list of widgets using custom
|
||||
/// logic both for diffing and creating new widget state.
|
||||
pub fn diff_children_custom<T>(
|
||||
&mut self,
|
||||
|
|
@ -107,7 +107,7 @@ impl Tree {
|
|||
}
|
||||
}
|
||||
|
||||
/// Reconciliates the `current_children` with the provided list of widgets using
|
||||
/// Reconciles 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
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
//! Build window-based GUI applications.
|
||||
pub mod icon;
|
||||
pub mod screenshot;
|
||||
pub mod settings;
|
||||
|
||||
mod direction;
|
||||
mod event;
|
||||
mod id;
|
||||
mod level;
|
||||
|
|
@ -10,6 +12,7 @@ mod position;
|
|||
mod redraw_request;
|
||||
mod user_attention;
|
||||
|
||||
pub use direction::Direction;
|
||||
pub use event::Event;
|
||||
pub use icon::Icon;
|
||||
pub use id::Id;
|
||||
|
|
@ -17,5 +20,6 @@ pub use level::Level;
|
|||
pub use mode::Mode;
|
||||
pub use position::Position;
|
||||
pub use redraw_request::RedrawRequest;
|
||||
pub use screenshot::Screenshot;
|
||||
pub use settings::Settings;
|
||||
pub use user_attention::UserAttention;
|
||||
|
|
|
|||
27
core/src/window/direction.rs
Normal file
27
core/src/window/direction.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/// The cardinal directions relative to the center of a window.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Direction {
|
||||
/// Points to the top edge of a window.
|
||||
North,
|
||||
|
||||
/// Points to the bottom edge of a window.
|
||||
South,
|
||||
|
||||
/// Points to the right edge of a window.
|
||||
East,
|
||||
|
||||
/// Points to the left edge of a window.
|
||||
West,
|
||||
|
||||
/// Points to the top-right corner of a window.
|
||||
NorthEast,
|
||||
|
||||
/// Points to the top-left corner of a window.
|
||||
NorthWest,
|
||||
|
||||
/// Points to the bottom-right corner of a window.
|
||||
SouthEast,
|
||||
|
||||
/// Points to the bottom-left corner of a window.
|
||||
SouthWest,
|
||||
}
|
||||
|
|
@ -9,8 +9,8 @@ pub enum Event {
|
|||
/// A window was opened.
|
||||
Opened {
|
||||
/// The position of the opened window. This is relative to the top-left corner of the desktop
|
||||
/// the window is on, including virtual desktops. Refers to window's "inner" position,
|
||||
/// or the client area, in logical pixels.
|
||||
/// the window is on, including virtual desktops. Refers to window's "outer" position,
|
||||
/// or the window area, in logical pixels.
|
||||
///
|
||||
/// **Note**: Not available in Wayland.
|
||||
position: Option<Point>,
|
||||
|
|
@ -23,20 +23,10 @@ pub enum Event {
|
|||
Closed,
|
||||
|
||||
/// A window was moved.
|
||||
Moved {
|
||||
/// The new logical x location of the window
|
||||
x: i32,
|
||||
/// The new logical y location of the window
|
||||
y: i32,
|
||||
},
|
||||
Moved(Point),
|
||||
|
||||
/// A window was resized.
|
||||
Resized {
|
||||
/// The new logical width of the window
|
||||
width: u32,
|
||||
/// The new logical height of the window
|
||||
height: u32,
|
||||
},
|
||||
Resized(Size),
|
||||
|
||||
/// A window redraw was requested.
|
||||
///
|
||||
|
|
@ -56,17 +46,29 @@ pub enum Event {
|
|||
///
|
||||
/// When the user hovers multiple files at once, this event will be emitted
|
||||
/// for each file separately.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Wayland:** Not implemented.
|
||||
FileHovered(PathBuf),
|
||||
|
||||
/// A file has been dropped into the window.
|
||||
///
|
||||
/// When the user drops multiple files at once, this event will be emitted
|
||||
/// for each file separately.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Wayland:** Not implemented.
|
||||
FileDropped(PathBuf),
|
||||
|
||||
/// A file was hovered, but has exited the window.
|
||||
///
|
||||
/// There will be a single `FilesHoveredLeft` event triggered even if
|
||||
/// multiple files were hovered.
|
||||
///
|
||||
/// ## Platform-specific
|
||||
///
|
||||
/// - **Wayland:** Not implemented.
|
||||
FilesHoveredLeft,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
use std::fmt;
|
||||
use std::hash::Hash;
|
||||
|
||||
use std::sync::atomic::{self, AtomicU64};
|
||||
|
||||
/// The id of the window.
|
||||
///
|
||||
/// Internally Iced reserves `window::Id::MAIN` for the first window spawned.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Id(u64);
|
||||
|
|
@ -12,11 +10,14 @@ pub struct Id(u64);
|
|||
static COUNT: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
impl Id {
|
||||
/// The reserved window [`Id`] for the first window in an Iced application.
|
||||
pub const MAIN: Self = Id(0);
|
||||
|
||||
/// Creates a new unique window [`Id`].
|
||||
pub fn unique() -> Id {
|
||||
Id(COUNT.fetch_add(1, atomic::Ordering::Relaxed))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Id {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,15 @@ pub enum RedrawRequest {
|
|||
|
||||
/// Redraw at the given time.
|
||||
At(Instant),
|
||||
|
||||
/// No redraw is needed.
|
||||
Wait,
|
||||
}
|
||||
|
||||
impl From<Instant> for RedrawRequest {
|
||||
fn from(time: Instant) -> Self {
|
||||
Self::At(time)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -34,5 +43,8 @@ mod tests {
|
|||
assert!(RedrawRequest::At(now) <= RedrawRequest::At(now));
|
||||
assert!(RedrawRequest::At(now) <= RedrawRequest::At(later));
|
||||
assert!(RedrawRequest::At(later) >= RedrawRequest::At(now));
|
||||
|
||||
assert!(RedrawRequest::Wait > RedrawRequest::NextFrame);
|
||||
assert!(RedrawRequest::Wait > RedrawRequest::At(later));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! Take screenshots of a window.
|
||||
use crate::core::{Rectangle, Size};
|
||||
use crate::{Rectangle, Size};
|
||||
|
||||
use bytes::Bytes;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
|
|
@ -11,16 +11,20 @@ use std::fmt::{Debug, Formatter};
|
|||
pub struct Screenshot {
|
||||
/// The bytes of the [`Screenshot`].
|
||||
pub bytes: Bytes,
|
||||
/// The size of the [`Screenshot`].
|
||||
/// The size of the [`Screenshot`] in physical pixels.
|
||||
pub size: Size<u32>,
|
||||
/// The scale factor of the [`Screenshot`]. This can be useful when converting between widget
|
||||
/// bounds (which are in logical pixels) to crop screenshots.
|
||||
pub scale_factor: f64,
|
||||
}
|
||||
|
||||
impl Debug for Screenshot {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Screenshot: {{ \n bytes: {}\n size: {:?} }}",
|
||||
"Screenshot: {{ \n bytes: {}\n scale: {}\n size: {:?} }}",
|
||||
self.bytes.len(),
|
||||
self.scale_factor,
|
||||
self.size
|
||||
)
|
||||
}
|
||||
|
|
@ -28,10 +32,15 @@ impl Debug for Screenshot {
|
|||
|
||||
impl Screenshot {
|
||||
/// Creates a new [`Screenshot`].
|
||||
pub fn new(bytes: impl Into<Bytes>, size: Size<u32>) -> Self {
|
||||
pub fn new(
|
||||
bytes: impl Into<Bytes>,
|
||||
size: Size<u32>,
|
||||
scale_factor: f64,
|
||||
) -> Self {
|
||||
Self {
|
||||
bytes: bytes.into(),
|
||||
size,
|
||||
scale_factor,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +79,7 @@ impl Screenshot {
|
|||
Ok(Self {
|
||||
bytes: Bytes::from(chopped),
|
||||
size: Size::new(region.width, region.height),
|
||||
scale_factor: self.scale_factor,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -24,16 +24,23 @@ mod platform;
|
|||
#[path = "settings/other.rs"]
|
||||
mod platform;
|
||||
|
||||
use crate::window::{Icon, Level, Position};
|
||||
use crate::Size;
|
||||
use crate::window::{Icon, Level, Position};
|
||||
|
||||
pub use platform::PlatformSpecific;
|
||||
|
||||
/// The window settings of an application.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Settings {
|
||||
/// The initial logical dimensions of the window.
|
||||
pub size: Size,
|
||||
|
||||
/// Whether the window should start maximized.
|
||||
pub maximized: bool,
|
||||
|
||||
/// Whether the window should start fullscreen.
|
||||
pub fullscreen: bool,
|
||||
|
||||
/// The initial position of the window.
|
||||
pub position: Position,
|
||||
|
||||
|
|
@ -79,6 +86,8 @@ impl Default for Settings {
|
|||
fn default() -> Self {
|
||||
Self {
|
||||
size: Size::new(1024.0, 768.0),
|
||||
maximized: false,
|
||||
fullscreen: false,
|
||||
position: Position::default(),
|
||||
min_size: None,
|
||||
max_size: None,
|
||||
|
|
|
|||
|
|
@ -8,4 +8,10 @@ pub struct PlatformSpecific {
|
|||
/// As a best practice, it is suggested to select an application id that match
|
||||
/// the basename of the application’s .desktop file.
|
||||
pub application_id: String,
|
||||
|
||||
/// Whether bypass the window manager mapping for x11 windows
|
||||
///
|
||||
/// This flag is particularly useful for creating UI elements that need precise
|
||||
/// positioning and immediate display without window manager interference.
|
||||
pub override_redirect: bool,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,27 @@
|
|||
//! Platform specific settings for Windows.
|
||||
use raw_window_handle::RawWindowHandle;
|
||||
|
||||
/// The platform specific window settings of an application.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PlatformSpecific {
|
||||
/// Parent window
|
||||
pub parent: Option<RawWindowHandle>,
|
||||
|
||||
/// Drag and drop support
|
||||
pub drag_and_drop: bool,
|
||||
|
||||
/// Whether show or hide the window icon in the taskbar.
|
||||
pub skip_taskbar: bool,
|
||||
|
||||
/// Shows or hides the background drop shadow for undecorated windows.
|
||||
///
|
||||
/// The shadow is hidden by default.
|
||||
/// Enabling the shadow causes a thin 1px line to appear on the top of the window.
|
||||
pub undecorated_shadow: bool,
|
||||
}
|
||||
|
||||
impl Default for PlatformSpecific {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
parent: None,
|
||||
drag_and_drop: true,
|
||||
skip_taskbar: false,
|
||||
undecorated_shadow: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,13 +11,10 @@ categories.workspace = true
|
|||
keywords.workspace = true
|
||||
|
||||
[features]
|
||||
enable = ["dep:iced_beacon", "dep:once_cell"]
|
||||
enable = ["dep:iced_beacon"]
|
||||
|
||||
[dependencies]
|
||||
iced_core.workspace = true
|
||||
|
||||
iced_beacon.workspace = true
|
||||
iced_beacon.optional = true
|
||||
|
||||
once_cell.workspace = true
|
||||
once_cell.optional = true
|
||||
|
|
|
|||
|
|
@ -72,10 +72,9 @@ mod internal {
|
|||
use beacon::client::{self, Client};
|
||||
use beacon::span;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use std::process;
|
||||
use std::sync::atomic::{self, AtomicBool};
|
||||
use std::sync::RwLock;
|
||||
use std::sync::{LazyLock, RwLock};
|
||||
|
||||
pub fn init(name: &str) {
|
||||
let name = name.split("::").next().unwrap_or(name);
|
||||
|
|
@ -196,7 +195,7 @@ mod internal {
|
|||
}
|
||||
}
|
||||
|
||||
static BEACON: Lazy<Client> = Lazy::new(|| {
|
||||
static BEACON: LazyLock<Client> = LazyLock::new(|| {
|
||||
client::connect(NAME.read().expect("Read application name").to_owned())
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# Examples
|
||||
__Iced moves fast and the `master` branch can contain breaking changes!__ If
|
||||
you want to learn about a specific release, check out [the release list].
|
||||
|
||||
[the release list]: https://github.com/iced-rs/iced/releases
|
||||
__Iced moves fast and the `master` branch can contain breaking changes!__ If you want to browse examples that are compatible with the latest release,
|
||||
then [switch to the `latest` branch](https://github.com/iced-rs/iced/tree/latest/examples#examples).
|
||||
|
||||
## [Tour](tour)
|
||||
A simple UI tour that can run both on native platforms and the web! It showcases different widgets that can be built using Iced.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name = "arc"
|
||||
version = "0.1.0"
|
||||
authors = ["ThatsNoMoon <git@thatsnomoon.dev>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ use std::{f32::consts::PI, time::Instant};
|
|||
|
||||
use iced::mouse;
|
||||
use iced::widget::canvas::{
|
||||
self, stroke, Cache, Canvas, Geometry, Path, Stroke,
|
||||
self, Cache, Canvas, Geometry, Path, Stroke, stroke,
|
||||
};
|
||||
use iced::{Element, Length, Point, Rectangle, Renderer, Subscription, Theme};
|
||||
use iced::window;
|
||||
use iced::{Element, Fill, Point, Rectangle, Renderer, Subscription, Theme};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
iced::program("Arc - Iced", Arc::update, Arc::view)
|
||||
iced::application("Arc - Iced", Arc::update, Arc::view)
|
||||
.subscription(Arc::subscription)
|
||||
.theme(|_| Theme::Dark)
|
||||
.antialiasing(true)
|
||||
|
|
@ -30,15 +31,11 @@ impl Arc {
|
|||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
Canvas::new(self)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
Canvas::new(self).width(Fill).height(Fill).into()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
iced::time::every(std::time::Duration::from_millis(10))
|
||||
.map(|_| Message::Tick)
|
||||
window::frames().map(|_| Message::Tick)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name = "bezier_tool"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
//! This example showcases an interactive `Canvas` for drawing Bézier curves.
|
||||
use iced::alignment;
|
||||
use iced::widget::{button, container, horizontal_space, hover};
|
||||
use iced::{Element, Length, Theme};
|
||||
use iced::widget::{button, container, horizontal_space, hover, right};
|
||||
use iced::{Element, Theme};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
iced::program("Bezier Tool - Iced", Example::update, Example::view)
|
||||
iced::application("Bezier Tool - Iced", Example::update, Example::view)
|
||||
.theme(|_| Theme::CatppuccinMocha)
|
||||
.antialiasing(true)
|
||||
.run()
|
||||
|
|
@ -42,14 +41,12 @@ impl Example {
|
|||
if self.curves.is_empty() {
|
||||
container(horizontal_space())
|
||||
} else {
|
||||
container(
|
||||
right(
|
||||
button("Clear")
|
||||
.style(button::danger)
|
||||
.on_press(Message::Clear),
|
||||
)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.align_x(alignment::Horizontal::Right)
|
||||
},
|
||||
))
|
||||
.padding(20)
|
||||
|
|
@ -59,9 +56,10 @@ impl Example {
|
|||
|
||||
mod bezier {
|
||||
use iced::mouse;
|
||||
use iced::widget::canvas::event::{self, Event};
|
||||
use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke};
|
||||
use iced::{Element, Length, Point, Rectangle, Renderer, Theme};
|
||||
use iced::widget::canvas::{
|
||||
self, Canvas, Event, Frame, Geometry, Path, Stroke,
|
||||
};
|
||||
use iced::{Element, Fill, Point, Rectangle, Renderer, Theme};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct State {
|
||||
|
|
@ -74,8 +72,8 @@ mod bezier {
|
|||
state: self,
|
||||
curves,
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
|
|
@ -89,31 +87,29 @@ mod bezier {
|
|||
curves: &'a [Curve],
|
||||
}
|
||||
|
||||
impl<'a> canvas::Program<Curve> for Bezier<'a> {
|
||||
impl canvas::Program<Curve> for Bezier<'_> {
|
||||
type State = Option<Pending>;
|
||||
|
||||
fn update(
|
||||
&self,
|
||||
state: &mut Self::State,
|
||||
event: Event,
|
||||
event: &Event,
|
||||
bounds: Rectangle,
|
||||
cursor: mouse::Cursor,
|
||||
) -> (event::Status, Option<Curve>) {
|
||||
let Some(cursor_position) = cursor.position_in(bounds) else {
|
||||
return (event::Status::Ignored, None);
|
||||
};
|
||||
) -> Option<canvas::Action<Curve>> {
|
||||
let cursor_position = cursor.position_in(bounds)?;
|
||||
|
||||
match event {
|
||||
Event::Mouse(mouse_event) => {
|
||||
let message = match mouse_event {
|
||||
mouse::Event::ButtonPressed(mouse::Button::Left) => {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(
|
||||
mouse::Button::Left,
|
||||
)) => Some(
|
||||
match *state {
|
||||
None => {
|
||||
*state = Some(Pending::One {
|
||||
from: cursor_position,
|
||||
});
|
||||
|
||||
None
|
||||
canvas::Action::request_redraw()
|
||||
}
|
||||
Some(Pending::One { from }) => {
|
||||
*state = Some(Pending::Two {
|
||||
|
|
@ -121,25 +117,26 @@ mod bezier {
|
|||
to: cursor_position,
|
||||
});
|
||||
|
||||
None
|
||||
canvas::Action::request_redraw()
|
||||
}
|
||||
Some(Pending::Two { from, to }) => {
|
||||
*state = None;
|
||||
|
||||
Some(Curve {
|
||||
canvas::Action::publish(Curve {
|
||||
from,
|
||||
to,
|
||||
control: cursor_position,
|
||||
})
|
||||
}
|
||||
}
|
||||
.and_capture(),
|
||||
),
|
||||
Event::Mouse(mouse::Event::CursorMoved { .. })
|
||||
if state.is_some() =>
|
||||
{
|
||||
Some(canvas::Action::request_redraw())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
(event::Status::Captured, message)
|
||||
}
|
||||
_ => (event::Status::Ignored, None),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
26
examples/changelog/Cargo.toml
Normal file
26
examples/changelog/Cargo.toml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
name = "changelog"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[lints.clippy]
|
||||
large_enum_variant = "allow"
|
||||
|
||||
[dependencies]
|
||||
iced.workspace = true
|
||||
iced.features = ["tokio", "markdown", "highlighter", "debug"]
|
||||
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio.features = ["fs", "process"]
|
||||
tokio.workspace = true
|
||||
|
||||
serde = "1"
|
||||
webbrowser = "1"
|
||||
tracing-subscriber = "0.3"
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.12"
|
||||
features = ["json"]
|
||||
386
examples/changelog/src/changelog.rs
Normal file
386
examples/changelog/src/changelog.rs
Normal file
|
|
@ -0,0 +1,386 @@
|
|||
use serde::Deserialize;
|
||||
use tokio::fs;
|
||||
use tokio::process;
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Changelog {
|
||||
ids: Vec<u64>,
|
||||
added: Vec<String>,
|
||||
changed: Vec<String>,
|
||||
fixed: Vec<String>,
|
||||
removed: Vec<String>,
|
||||
authors: Vec<String>,
|
||||
}
|
||||
|
||||
impl Changelog {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ids: Vec::new(),
|
||||
added: Vec::new(),
|
||||
changed: Vec::new(),
|
||||
fixed: Vec::new(),
|
||||
removed: Vec::new(),
|
||||
authors: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list() -> Result<(Self, Vec<Contribution>), Error> {
|
||||
let mut changelog = Self::new();
|
||||
|
||||
{
|
||||
let markdown = fs::read_to_string("CHANGELOG.md").await?;
|
||||
|
||||
if let Some(unreleased) = markdown.split("\n## ").nth(1) {
|
||||
let sections = unreleased.split("\n\n");
|
||||
|
||||
for section in sections {
|
||||
if section.starts_with("Many thanks to...") {
|
||||
for author in section.lines().skip(1) {
|
||||
let author = author.trim_start_matches("- @");
|
||||
|
||||
if author.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
changelog.authors.push(author.to_owned());
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some((_, rest)) = section.split_once("### ") else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some((name, rest)) = rest.split_once("\n") else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let category = match name {
|
||||
"Added" => Category::Added,
|
||||
"Fixed" => Category::Fixed,
|
||||
"Changed" => Category::Changed,
|
||||
"Removed" => Category::Removed,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
for entry in rest.lines() {
|
||||
let Some((_, id)) = entry.split_once("[#") else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some((id, _)) = id.split_once(']') else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(id): Result<u64, _> = id.parse() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
changelog.ids.push(id);
|
||||
|
||||
let target = match category {
|
||||
Category::Added => &mut changelog.added,
|
||||
Category::Changed => &mut changelog.changed,
|
||||
Category::Fixed => &mut changelog.fixed,
|
||||
Category::Removed => &mut changelog.removed,
|
||||
};
|
||||
|
||||
target.push(entry.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut candidates = Contribution::list().await?;
|
||||
|
||||
for reviewed_entry in changelog.entries() {
|
||||
candidates.retain(|candidate| candidate.id != reviewed_entry);
|
||||
}
|
||||
|
||||
Ok((changelog, candidates))
|
||||
}
|
||||
|
||||
pub async fn save(self) -> Result<(), Error> {
|
||||
let markdown = fs::read_to_string("CHANGELOG.md").await?;
|
||||
|
||||
let Some((header, rest)) = markdown.split_once("\n## ") else {
|
||||
return Err(Error::InvalidFormat);
|
||||
};
|
||||
|
||||
let Some((_unreleased, rest)) = rest.split_once("\n## ") else {
|
||||
return Err(Error::InvalidFormat);
|
||||
};
|
||||
|
||||
let unreleased = format!("\n## [Unreleased]\n{self}");
|
||||
|
||||
let rest = format!("\n## {rest}");
|
||||
|
||||
let changelog = [header, &unreleased, &rest].concat();
|
||||
fs::write("CHANGELOG.md", changelog).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.ids.len()
|
||||
}
|
||||
|
||||
pub fn entries(&self) -> impl Iterator<Item = u64> + '_ {
|
||||
self.ids.iter().copied()
|
||||
}
|
||||
|
||||
pub fn push(&mut self, entry: Entry) {
|
||||
self.ids.push(entry.id);
|
||||
|
||||
let item = format!(
|
||||
"- {title}. [#{id}](https://github.com/iced-rs/iced/pull/{id})",
|
||||
title = entry.title,
|
||||
id = entry.id
|
||||
);
|
||||
|
||||
let target = match entry.category {
|
||||
Category::Added => &mut self.added,
|
||||
Category::Changed => &mut self.changed,
|
||||
Category::Fixed => &mut self.fixed,
|
||||
Category::Removed => &mut self.removed,
|
||||
};
|
||||
|
||||
target.push(item);
|
||||
|
||||
if entry.author != "hecrj" && !self.authors.contains(&entry.author) {
|
||||
self.authors.push(entry.author);
|
||||
self.authors.sort_by_key(|author| author.to_lowercase());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Changelog {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fn section(category: Category, entries: &[String]) -> String {
|
||||
if entries.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
format!("### {category}\n{list}\n", list = entries.join("\n"))
|
||||
}
|
||||
|
||||
fn thank_you<'a>(authors: impl IntoIterator<Item = &'a str>) -> String {
|
||||
let mut list = String::new();
|
||||
|
||||
for author in authors {
|
||||
list.push_str(&format!("- @{author}\n"));
|
||||
}
|
||||
|
||||
format!("Many thanks to...\n{list}")
|
||||
}
|
||||
|
||||
let changelog = [
|
||||
section(Category::Added, &self.added),
|
||||
section(Category::Changed, &self.changed),
|
||||
section(Category::Fixed, &self.fixed),
|
||||
section(Category::Removed, &self.removed),
|
||||
thank_you(self.authors.iter().map(String::as_str)),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|section| !section.is_empty())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
|
||||
f.write_str(&changelog)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Entry {
|
||||
pub id: u64,
|
||||
pub title: String,
|
||||
pub category: Category,
|
||||
pub author: String,
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
pub fn new(
|
||||
title: &str,
|
||||
category: Category,
|
||||
pull_request: &PullRequest,
|
||||
) -> Option<Self> {
|
||||
let title = title.strip_suffix(".").unwrap_or(title);
|
||||
|
||||
if title.is_empty() {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
id: pull_request.id,
|
||||
title: title.to_owned(),
|
||||
category,
|
||||
author: pull_request.author.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Category {
|
||||
Added,
|
||||
Changed,
|
||||
Fixed,
|
||||
Removed,
|
||||
}
|
||||
|
||||
impl Category {
|
||||
pub const ALL: &'static [Self] =
|
||||
&[Self::Added, Self::Changed, Self::Fixed, Self::Removed];
|
||||
|
||||
pub fn guess(label: &str) -> Option<Self> {
|
||||
Some(match label {
|
||||
"feature" | "addition" => Self::Added,
|
||||
"change" => Self::Changed,
|
||||
"bug" | "fix" => Self::Fixed,
|
||||
_ => None?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Category {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
Category::Added => "Added",
|
||||
Category::Changed => "Changed",
|
||||
Category::Fixed => "Fixed",
|
||||
Category::Removed => "Removed",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Contribution {
|
||||
pub id: u64,
|
||||
}
|
||||
|
||||
impl Contribution {
|
||||
pub async fn list() -> Result<Vec<Contribution>, Error> {
|
||||
let output = process::Command::new("git")
|
||||
.args([
|
||||
"log",
|
||||
"--oneline",
|
||||
"--grep",
|
||||
"#[0-9]*",
|
||||
"origin/latest..HEAD",
|
||||
])
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
let log = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
let mut contributions: Vec<_> = log
|
||||
.lines()
|
||||
.filter(|title| !title.is_empty())
|
||||
.filter_map(|title| {
|
||||
let (_, pull_request) = title.split_once("#")?;
|
||||
let (pull_request, _) = pull_request.split_once([')', ' '])?;
|
||||
|
||||
Some(Contribution {
|
||||
id: pull_request.parse().ok()?,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut unique = BTreeSet::from_iter(contributions.clone());
|
||||
contributions.retain_mut(|contribution| unique.remove(contribution));
|
||||
|
||||
Ok(contributions)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PullRequest {
|
||||
pub id: u64,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub labels: Vec<String>,
|
||||
pub author: String,
|
||||
}
|
||||
|
||||
impl PullRequest {
|
||||
pub async fn fetch(contribution: Contribution) -> Result<Self, Error> {
|
||||
let request = reqwest::Client::new()
|
||||
.request(
|
||||
reqwest::Method::GET,
|
||||
format!(
|
||||
"https://api.github.com/repos/iced-rs/iced/pulls/{}",
|
||||
contribution.id
|
||||
),
|
||||
)
|
||||
.header("User-Agent", "iced changelog generator")
|
||||
.header(
|
||||
"Authorization",
|
||||
format!(
|
||||
"Bearer {}",
|
||||
env::var("GITHUB_TOKEN")
|
||||
.map_err(|_| Error::GitHubTokenNotFound)?
|
||||
),
|
||||
);
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Schema {
|
||||
title: String,
|
||||
body: Option<String>,
|
||||
user: User,
|
||||
labels: Vec<Label>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct User {
|
||||
login: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Label {
|
||||
name: String,
|
||||
}
|
||||
|
||||
let schema: Schema = request.send().await?.json().await?;
|
||||
|
||||
Ok(Self {
|
||||
id: contribution.id,
|
||||
title: schema.title,
|
||||
description: schema.body,
|
||||
labels: schema.labels.into_iter().map(|label| label.name).collect(),
|
||||
author: schema.user.login,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("io operation failed: {0}")]
|
||||
IOFailed(Arc<io::Error>),
|
||||
|
||||
#[error("http request failed: {0}")]
|
||||
RequestFailed(Arc<reqwest::Error>),
|
||||
|
||||
#[error("no GITHUB_TOKEN variable was set")]
|
||||
GitHubTokenNotFound,
|
||||
|
||||
#[error("the changelog format is not valid")]
|
||||
InvalidFormat,
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(error: io::Error) -> Self {
|
||||
Error::IOFailed(Arc::new(error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(error: reqwest::Error) -> Self {
|
||||
Error::RequestFailed(Arc::new(error))
|
||||
}
|
||||
}
|
||||
10
examples/changelog/src/icon.rs
Normal file
10
examples/changelog/src/icon.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
use iced::widget::{text, Text};
|
||||
use iced::Font;
|
||||
|
||||
pub const FONT_BYTES: &[u8] = include_bytes!("../fonts/changelog-icons.ttf");
|
||||
|
||||
const FONT: Font = Font::with_name("changelog-icons");
|
||||
|
||||
pub fn copy() -> Text<'static> {
|
||||
text('\u{e800}').font(FONT)
|
||||
}
|
||||
375
examples/changelog/src/main.rs
Normal file
375
examples/changelog/src/main.rs
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
mod changelog;
|
||||
|
||||
use crate::changelog::Changelog;
|
||||
|
||||
use iced::font;
|
||||
use iced::widget::{
|
||||
button, center, column, container, markdown, pick_list, progress_bar,
|
||||
rich_text, row, scrollable, span, stack, text, text_input,
|
||||
};
|
||||
use iced::{Center, Element, Fill, FillPortion, Font, Task, Theme};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
iced::application("Changelog Generator", Generator::update, Generator::view)
|
||||
.theme(Generator::theme)
|
||||
.run_with(Generator::new)
|
||||
}
|
||||
|
||||
enum Generator {
|
||||
Loading,
|
||||
Reviewing {
|
||||
changelog: Changelog,
|
||||
pending: Vec<changelog::Contribution>,
|
||||
state: State,
|
||||
preview: Vec<markdown::Item>,
|
||||
},
|
||||
Done,
|
||||
}
|
||||
|
||||
enum State {
|
||||
Loading(changelog::Contribution),
|
||||
Loaded {
|
||||
pull_request: changelog::PullRequest,
|
||||
description: Vec<markdown::Item>,
|
||||
title: String,
|
||||
category: changelog::Category,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
ChangelogListed(
|
||||
Result<(Changelog, Vec<changelog::Contribution>), changelog::Error>,
|
||||
),
|
||||
PullRequestFetched(Result<changelog::PullRequest, changelog::Error>),
|
||||
UrlClicked(markdown::Url),
|
||||
TitleChanged(String),
|
||||
CategorySelected(changelog::Category),
|
||||
Next,
|
||||
OpenPullRequest(u64),
|
||||
ChangelogSaved(Result<(), changelog::Error>),
|
||||
Quit,
|
||||
}
|
||||
|
||||
impl Generator {
|
||||
fn new() -> (Self, Task<Message>) {
|
||||
(
|
||||
Self::Loading,
|
||||
Task::perform(Changelog::list(), Message::ChangelogListed),
|
||||
)
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::ChangelogListed(Ok((changelog, mut pending))) => {
|
||||
if let Some(contribution) = pending.pop() {
|
||||
let preview =
|
||||
markdown::parse(&changelog.to_string()).collect();
|
||||
|
||||
*self = Self::Reviewing {
|
||||
changelog,
|
||||
pending,
|
||||
state: State::Loading(contribution.clone()),
|
||||
preview,
|
||||
};
|
||||
|
||||
Task::perform(
|
||||
changelog::PullRequest::fetch(contribution),
|
||||
Message::PullRequestFetched,
|
||||
)
|
||||
} else {
|
||||
*self = Self::Done;
|
||||
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
Message::PullRequestFetched(Ok(pull_request)) => {
|
||||
let Self::Reviewing { state, .. } = self else {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
let description = markdown::parse(
|
||||
pull_request
|
||||
.description
|
||||
.as_deref()
|
||||
.unwrap_or("*No description provided*"),
|
||||
)
|
||||
.collect();
|
||||
|
||||
*state = State::Loaded {
|
||||
title: pull_request.title.clone(),
|
||||
category: pull_request
|
||||
.labels
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.filter_map(changelog::Category::guess)
|
||||
.next()
|
||||
.unwrap_or(changelog::Category::Added),
|
||||
pull_request,
|
||||
description,
|
||||
};
|
||||
|
||||
Task::none()
|
||||
}
|
||||
Message::UrlClicked(url) => {
|
||||
let _ = webbrowser::open(url.as_str());
|
||||
|
||||
Task::none()
|
||||
}
|
||||
Message::TitleChanged(new_title) => {
|
||||
let Self::Reviewing { state, .. } = self else {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
let State::Loaded { title, .. } = state else {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
*title = new_title;
|
||||
|
||||
Task::none()
|
||||
}
|
||||
Message::CategorySelected(new_category) => {
|
||||
let Self::Reviewing { state, .. } = self else {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
let State::Loaded { category, .. } = state else {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
*category = new_category;
|
||||
|
||||
Task::none()
|
||||
}
|
||||
Message::Next => {
|
||||
let Self::Reviewing {
|
||||
changelog,
|
||||
pending,
|
||||
state,
|
||||
preview,
|
||||
..
|
||||
} = self
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
let State::Loaded {
|
||||
title,
|
||||
category,
|
||||
pull_request,
|
||||
..
|
||||
} = state
|
||||
else {
|
||||
return Task::none();
|
||||
};
|
||||
|
||||
if let Some(entry) =
|
||||
changelog::Entry::new(title, *category, pull_request)
|
||||
{
|
||||
changelog.push(entry);
|
||||
|
||||
let save = Task::perform(
|
||||
changelog.clone().save(),
|
||||
Message::ChangelogSaved,
|
||||
);
|
||||
|
||||
*preview =
|
||||
markdown::parse(&changelog.to_string()).collect();
|
||||
|
||||
if let Some(contribution) = pending.pop() {
|
||||
*state = State::Loading(contribution.clone());
|
||||
|
||||
Task::batch([
|
||||
save,
|
||||
Task::perform(
|
||||
changelog::PullRequest::fetch(contribution),
|
||||
Message::PullRequestFetched,
|
||||
),
|
||||
])
|
||||
} else {
|
||||
*self = Self::Done;
|
||||
save
|
||||
}
|
||||
} else {
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
Message::OpenPullRequest(id) => {
|
||||
let _ = webbrowser::open(&format!(
|
||||
"https://github.com/iced-rs/iced/pull/{id}"
|
||||
));
|
||||
|
||||
Task::none()
|
||||
}
|
||||
Message::ChangelogSaved(Ok(())) => Task::none(),
|
||||
|
||||
Message::ChangelogListed(Err(error))
|
||||
| Message::PullRequestFetched(Err(error))
|
||||
| Message::ChangelogSaved(Err(error)) => {
|
||||
log::error!("{error}");
|
||||
|
||||
Task::none()
|
||||
}
|
||||
Message::Quit => iced::exit(),
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
match self {
|
||||
Self::Loading => center("Loading...").into(),
|
||||
Self::Done => center(
|
||||
column![
|
||||
text("Changelog is up-to-date! 🎉")
|
||||
.shaping(text::Shaping::Advanced),
|
||||
button("Quit").on_press(Message::Quit),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_x(Center),
|
||||
)
|
||||
.into(),
|
||||
Self::Reviewing {
|
||||
changelog,
|
||||
pending,
|
||||
state,
|
||||
preview,
|
||||
} => {
|
||||
let progress = {
|
||||
let total = pending.len() + changelog.len();
|
||||
|
||||
let bar = progress_bar(
|
||||
0.0..=1.0,
|
||||
changelog.len() as f32 / total as f32,
|
||||
)
|
||||
.style(progress_bar::secondary);
|
||||
|
||||
let label = text!(
|
||||
"{amount_reviewed} / {total}",
|
||||
amount_reviewed = changelog.len()
|
||||
)
|
||||
.font(Font::MONOSPACE)
|
||||
.size(12);
|
||||
|
||||
stack![bar, center(label)]
|
||||
};
|
||||
|
||||
let form: Element<_> = match state {
|
||||
State::Loading(contribution) => {
|
||||
text!("Loading #{}...", contribution.id).into()
|
||||
}
|
||||
State::Loaded {
|
||||
pull_request,
|
||||
description,
|
||||
title,
|
||||
category,
|
||||
} => {
|
||||
let details = {
|
||||
let title = rich_text![
|
||||
span(&pull_request.title)
|
||||
.size(24)
|
||||
.link(pull_request.id),
|
||||
span(format!(" by {}", pull_request.author))
|
||||
.font(Font {
|
||||
style: font::Style::Italic,
|
||||
..Font::default()
|
||||
}),
|
||||
]
|
||||
.on_link_click(Message::OpenPullRequest)
|
||||
.font(Font::MONOSPACE);
|
||||
|
||||
let description =
|
||||
markdown(description, self.theme())
|
||||
.map(Message::UrlClicked);
|
||||
|
||||
let labels =
|
||||
row(pull_request.labels.iter().map(|label| {
|
||||
container(
|
||||
text(label)
|
||||
.size(10)
|
||||
.font(Font::MONOSPACE),
|
||||
)
|
||||
.padding(5)
|
||||
.style(container::rounded_box)
|
||||
.into()
|
||||
}))
|
||||
.spacing(10)
|
||||
.wrap();
|
||||
|
||||
column![
|
||||
title,
|
||||
labels,
|
||||
scrollable(description)
|
||||
.spacing(10)
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
]
|
||||
.spacing(10)
|
||||
};
|
||||
|
||||
let title = text_input(
|
||||
"Type a changelog entry title...",
|
||||
title,
|
||||
)
|
||||
.on_input(Message::TitleChanged);
|
||||
|
||||
let category = pick_list(
|
||||
changelog::Category::ALL,
|
||||
Some(category),
|
||||
Message::CategorySelected,
|
||||
);
|
||||
|
||||
let next = button("Next →")
|
||||
.on_press(Message::Next)
|
||||
.style(button::success);
|
||||
|
||||
column![
|
||||
details,
|
||||
row![title, category, next].spacing(10)
|
||||
]
|
||||
.spacing(10)
|
||||
.into()
|
||||
}
|
||||
};
|
||||
|
||||
let preview = if preview.is_empty() {
|
||||
center(
|
||||
container(
|
||||
text("The changelog is empty... so far!").size(12),
|
||||
)
|
||||
.padding(10)
|
||||
.style(container::rounded_box),
|
||||
)
|
||||
} else {
|
||||
container(
|
||||
scrollable(
|
||||
markdown(
|
||||
preview,
|
||||
markdown::Settings::with_text_size(
|
||||
12,
|
||||
self.theme(),
|
||||
),
|
||||
)
|
||||
.map(Message::UrlClicked),
|
||||
)
|
||||
.spacing(10),
|
||||
)
|
||||
.width(Fill)
|
||||
.padding(10)
|
||||
.style(container::rounded_box)
|
||||
};
|
||||
|
||||
let review = column![container(form).height(Fill), progress]
|
||||
.spacing(10)
|
||||
.width(FillPortion(2));
|
||||
|
||||
row![review, preview].spacing(10).padding(10).into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
Theme::TokyoNightStorm
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
name = "checkbox"
|
||||
version = "0.1.0"
|
||||
authors = ["Casper Rogild Storm<casper@rogildstorm.com>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use iced::{Element, Font};
|
|||
const ICON_FONT: Font = Font::with_name("icons");
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
iced::program("Checkbox - Iced", Example::update, Example::view)
|
||||
iced::application("Checkbox - Iced", Example::update, Example::view)
|
||||
.font(include_bytes!("../fonts/icons.ttf").as_slice())
|
||||
.run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,11 @@
|
|||
name = "clock"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced.workspace = true
|
||||
iced.features = ["canvas", "tokio", "debug"]
|
||||
|
||||
time = { version = "0.3", features = ["local-offset"] }
|
||||
chrono = "0.4"
|
||||
tracing-subscriber = "0.3"
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
use iced::alignment;
|
||||
use iced::mouse;
|
||||
use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke};
|
||||
use iced::time::{self, milliseconds};
|
||||
use iced::widget::canvas::{Cache, Geometry, LineCap, Path, Stroke, stroke};
|
||||
use iced::widget::{canvas, container};
|
||||
use iced::{
|
||||
Degrees, Element, Font, Length, Point, Rectangle, Renderer, Subscription,
|
||||
Theme, Vector,
|
||||
Degrees, Element, Fill, Font, Point, Radians, Rectangle, Renderer, Size,
|
||||
Subscription, Theme, Vector,
|
||||
};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
iced::program("Clock - Iced", Clock::update, Clock::view)
|
||||
iced::application("Clock - Iced", Clock::update, Clock::view)
|
||||
.subscription(Clock::subscription)
|
||||
.theme(Clock::theme)
|
||||
.antialiasing(true)
|
||||
|
|
@ -18,13 +19,13 @@ pub fn main() -> iced::Result {
|
|||
}
|
||||
|
||||
struct Clock {
|
||||
now: time::OffsetDateTime,
|
||||
now: chrono::DateTime<chrono::Local>,
|
||||
clock: Cache,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Message {
|
||||
Tick(time::OffsetDateTime),
|
||||
Tick(chrono::DateTime<chrono::Local>),
|
||||
}
|
||||
|
||||
impl Clock {
|
||||
|
|
@ -42,28 +43,18 @@ impl Clock {
|
|||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
let canvas = canvas(self as &Self)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
let canvas = canvas(self as &Self).width(Fill).height(Fill);
|
||||
|
||||
container(canvas)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.padding(20)
|
||||
.into()
|
||||
container(canvas).padding(20).into()
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
iced::time::every(std::time::Duration::from_millis(500)).map(|_| {
|
||||
Message::Tick(
|
||||
time::OffsetDateTime::now_local()
|
||||
.unwrap_or_else(|_| time::OffsetDateTime::now_utc()),
|
||||
)
|
||||
})
|
||||
time::every(milliseconds(500))
|
||||
.map(|_| Message::Tick(chrono::offset::Local::now()))
|
||||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
Theme::ALL[(self.now.unix_timestamp() as usize / 10) % Theme::ALL.len()]
|
||||
Theme::ALL[(self.now.timestamp() as usize / 10) % Theme::ALL.len()]
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
|
@ -71,8 +62,7 @@ impl Clock {
|
|||
impl Default for Clock {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
now: time::OffsetDateTime::now_local()
|
||||
.unwrap_or_else(|_| time::OffsetDateTime::now_utc()),
|
||||
now: chrono::offset::Local::now(),
|
||||
clock: Cache::default(),
|
||||
}
|
||||
}
|
||||
|
|
@ -89,6 +79,8 @@ impl<Message> canvas::Program<Message> for Clock {
|
|||
bounds: Rectangle,
|
||||
_cursor: mouse::Cursor,
|
||||
) -> Vec<Geometry> {
|
||||
use chrono::Timelike;
|
||||
|
||||
let clock = self.clock.draw(renderer, bounds.size(), |frame| {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
|
|
@ -125,9 +117,14 @@ impl<Message> canvas::Program<Message> for Clock {
|
|||
};
|
||||
|
||||
frame.translate(Vector::new(center.x, center.y));
|
||||
let minutes_portion =
|
||||
Radians::from(hand_rotation(self.now.minute(), 60)) / 12.0;
|
||||
let hour_hand_angle =
|
||||
Radians::from(hand_rotation(self.now.hour(), 12))
|
||||
+ minutes_portion;
|
||||
|
||||
frame.with_save(|frame| {
|
||||
frame.rotate(hand_rotation(self.now.hour(), 12));
|
||||
frame.rotate(hour_hand_angle);
|
||||
frame.stroke(&short_hand, wide_stroke());
|
||||
});
|
||||
|
||||
|
|
@ -163,13 +160,49 @@ impl<Message> canvas::Program<Message> for Clock {
|
|||
..canvas::Text::default()
|
||||
});
|
||||
});
|
||||
|
||||
// Draw clock numbers
|
||||
for hour in 1..=12 {
|
||||
let angle = Radians::from(hand_rotation(hour, 12))
|
||||
- Radians::from(Degrees(90.0));
|
||||
let x = radius * angle.0.cos();
|
||||
let y = radius * angle.0.sin();
|
||||
|
||||
frame.fill_text(canvas::Text {
|
||||
content: format!("{}", hour),
|
||||
size: (radius / 5.0).into(),
|
||||
position: Point::new(x * 0.82, y * 0.82),
|
||||
color: palette.secondary.strong.text,
|
||||
horizontal_alignment: alignment::Horizontal::Center,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
font: Font::MONOSPACE,
|
||||
..canvas::Text::default()
|
||||
});
|
||||
}
|
||||
|
||||
// Draw ticks
|
||||
for tick in 0..60 {
|
||||
let angle = hand_rotation(tick, 60);
|
||||
let width = if tick % 5 == 0 { 3.0 } else { 1.0 };
|
||||
|
||||
frame.with_save(|frame| {
|
||||
frame.rotate(angle);
|
||||
frame.fill(
|
||||
&Path::rectangle(
|
||||
Point::new(0.0, radius - 15.0),
|
||||
Size::new(width, 7.0),
|
||||
),
|
||||
palette.secondary.strong.text,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
vec![clock]
|
||||
}
|
||||
}
|
||||
|
||||
fn hand_rotation(n: u8, total: u8) -> Degrees {
|
||||
fn hand_rotation(n: u32, total: u32) -> Degrees {
|
||||
let turns = n as f32 / total as f32;
|
||||
|
||||
Degrees(360.0 * turns)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name = "color_palette"
|
||||
version = "0.1.0"
|
||||
authors = ["Clark Moody <clark@clarkmoody.com>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
use iced::alignment::{self, Alignment};
|
||||
use iced::alignment;
|
||||
use iced::mouse;
|
||||
use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path};
|
||||
use iced::widget::{column, row, text, Slider};
|
||||
use iced::widget::{Slider, column, row, text};
|
||||
use iced::{
|
||||
Color, Element, Font, Length, Pixels, Point, Rectangle, Renderer, Size,
|
||||
Vector,
|
||||
Center, Color, Element, Fill, Font, Pixels, Point, Rectangle, Renderer,
|
||||
Size, Vector,
|
||||
};
|
||||
use palette::{convert::FromColor, rgb::Rgb, Darken, Hsl, Lighten, ShiftHue};
|
||||
use palette::{Darken, Hsl, Lighten, ShiftHue, convert::FromColor, rgb::Rgb};
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
iced::program(
|
||||
iced::application(
|
||||
"Color Palette - Iced",
|
||||
ColorPalette::update,
|
||||
ColorPalette::view,
|
||||
|
|
@ -89,6 +89,7 @@ impl ColorPalette {
|
|||
primary: *self.theme.lower.first().unwrap(),
|
||||
text: *self.theme.higher.last().unwrap(),
|
||||
success: *self.theme.lower.last().unwrap(),
|
||||
warning: *self.theme.higher.last().unwrap(),
|
||||
danger: *self.theme.higher.last().unwrap(),
|
||||
},
|
||||
)
|
||||
|
|
@ -150,10 +151,7 @@ impl Theme {
|
|||
}
|
||||
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
Canvas::new(self)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
Canvas::new(self).width(Fill).height(Fill).into()
|
||||
}
|
||||
|
||||
fn draw(&self, frame: &mut Frame, text_color: Color) {
|
||||
|
|
@ -320,7 +318,7 @@ impl<C: ColorSpace + Copy> ColorPicker<C> {
|
|||
text(color.to_string()).width(185).size(12),
|
||||
]
|
||||
.spacing(10)
|
||||
.align_items(Alignment::Center)
|
||||
.align_y(Center)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name = "combo_box"
|
||||
version = "0.1.0"
|
||||
authors = ["Joao Freitas <jhff.15@gmail.com>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use iced::widget::{
|
||||
center, column, combo_box, scrollable, text, vertical_space,
|
||||
};
|
||||
use iced::{Alignment, Element, Length};
|
||||
use iced::{Center, Element, Fill};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
iced::run("Combo Box - Iced", Example::update, Example::view)
|
||||
|
|
@ -64,8 +64,8 @@ impl Example {
|
|||
combo_box,
|
||||
vertical_space().height(150),
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.align_items(Alignment::Center)
|
||||
.width(Fill)
|
||||
.align_x(Center)
|
||||
.spacing(10);
|
||||
|
||||
center(scrollable(content)).into()
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
[package]
|
||||
name = "component"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
iced.workspace = true
|
||||
iced.features = ["debug", "lazy"]
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
use iced::widget::center;
|
||||
use iced::Element;
|
||||
|
||||
use numeric_input::numeric_input;
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
iced::run("Component - Iced", Component::update, Component::view)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Component {
|
||||
value: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Message {
|
||||
NumericInputChanged(Option<u32>),
|
||||
}
|
||||
|
||||
impl Component {
|
||||
fn update(&mut self, message: Message) {
|
||||
match message {
|
||||
Message::NumericInputChanged(value) => {
|
||||
self.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
center(numeric_input(self.value, Message::NumericInputChanged))
|
||||
.padding(20)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
mod numeric_input {
|
||||
use iced::alignment::{self, Alignment};
|
||||
use iced::widget::{button, component, row, text, text_input, Component};
|
||||
use iced::{Element, Length, Size};
|
||||
|
||||
pub struct NumericInput<Message> {
|
||||
value: Option<u32>,
|
||||
on_change: Box<dyn Fn(Option<u32>) -> Message>,
|
||||
}
|
||||
|
||||
pub fn numeric_input<Message>(
|
||||
value: Option<u32>,
|
||||
on_change: impl Fn(Option<u32>) -> Message + 'static,
|
||||
) -> NumericInput<Message> {
|
||||
NumericInput::new(value, on_change)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event {
|
||||
InputChanged(String),
|
||||
IncrementPressed,
|
||||
DecrementPressed,
|
||||
}
|
||||
|
||||
impl<Message> NumericInput<Message> {
|
||||
pub fn new(
|
||||
value: Option<u32>,
|
||||
on_change: impl Fn(Option<u32>) -> Message + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
value,
|
||||
on_change: Box::new(on_change),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Message, Theme> Component<Message, Theme> for NumericInput<Message>
|
||||
where
|
||||
Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static,
|
||||
{
|
||||
type State = ();
|
||||
type Event = Event;
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
_state: &mut Self::State,
|
||||
event: Event,
|
||||
) -> Option<Message> {
|
||||
match event {
|
||||
Event::IncrementPressed => Some((self.on_change)(Some(
|
||||
self.value.unwrap_or_default().saturating_add(1),
|
||||
))),
|
||||
Event::DecrementPressed => Some((self.on_change)(Some(
|
||||
self.value.unwrap_or_default().saturating_sub(1),
|
||||
))),
|
||||
Event::InputChanged(value) => {
|
||||
if value.is_empty() {
|
||||
Some((self.on_change)(None))
|
||||
} else {
|
||||
value
|
||||
.parse()
|
||||
.ok()
|
||||
.map(Some)
|
||||
.map(self.on_change.as_ref())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, _state: &Self::State) -> Element<'_, Event, Theme> {
|
||||
let button = |label, on_press| {
|
||||
button(
|
||||
text(label)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.horizontal_alignment(alignment::Horizontal::Center)
|
||||
.vertical_alignment(alignment::Vertical::Center),
|
||||
)
|
||||
.width(40)
|
||||
.height(40)
|
||||
.on_press(on_press)
|
||||
};
|
||||
|
||||
row![
|
||||
button("-", Event::DecrementPressed),
|
||||
text_input(
|
||||
"Type a number",
|
||||
self.value
|
||||
.as_ref()
|
||||
.map(u32::to_string)
|
||||
.as_deref()
|
||||
.unwrap_or(""),
|
||||
)
|
||||
.on_input(Event::InputChanged)
|
||||
.padding(10),
|
||||
button("+", Event::IncrementPressed),
|
||||
]
|
||||
.align_items(Alignment::Center)
|
||||
.spacing(10)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> Size<Length> {
|
||||
Size {
|
||||
width: Length::Fill,
|
||||
height: Length::Shrink,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Message, Theme> From<NumericInput<Message>>
|
||||
for Element<'a, Message, Theme>
|
||||
where
|
||||
Theme: text::Catalog + button::Catalog + text_input::Catalog + 'static,
|
||||
Message: 'a,
|
||||
{
|
||||
fn from(numeric_input: NumericInput<Message>) -> Self {
|
||||
component(numeric_input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
name = "counter"
|
||||
version = "0.1.0"
|
||||
authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
@ -10,4 +10,7 @@ iced.workspace = true
|
|||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
iced.workspace = true
|
||||
iced.features = ["webgl"]
|
||||
iced.features = ["webgl", "fira-sans"]
|
||||
|
||||
[dev-dependencies]
|
||||
iced_test.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use iced::widget::{button, column, text, Column};
|
||||
use iced::Alignment;
|
||||
use iced::Center;
|
||||
use iced::widget::{Column, button, column, text};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
iced::run("A cool counter", Counter::update, Counter::view)
|
||||
|
|
@ -35,6 +35,34 @@ impl Counter {
|
|||
button("Decrement").on_press(Message::Decrement)
|
||||
]
|
||||
.padding(20)
|
||||
.align_items(Alignment::Center)
|
||||
.align_x(Center)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use iced_test::selector::text;
|
||||
use iced_test::{Error, simulator};
|
||||
|
||||
#[test]
|
||||
fn it_counts() -> Result<(), Error> {
|
||||
let mut counter = Counter { value: 0 };
|
||||
let mut ui = simulator(counter.view());
|
||||
|
||||
let _ = ui.click(text("Increment"))?;
|
||||
let _ = ui.click(text("Increment"))?;
|
||||
let _ = ui.click(text("Decrement"))?;
|
||||
|
||||
for message in ui.into_messages() {
|
||||
counter.update(message);
|
||||
}
|
||||
|
||||
assert_eq!(counter.value, 1);
|
||||
|
||||
let mut ui = simulator(counter.view());
|
||||
assert!(ui.find(text("1")).is_ok(), "Counter should display 1!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
name = "custom_quad"
|
||||
version = "0.1.0"
|
||||
authors = ["Robert Krahn"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ mod quad {
|
|||
use iced::advanced::layout::{self, Layout};
|
||||
use iced::advanced::renderer;
|
||||
use iced::advanced::widget::{self, Widget};
|
||||
use iced::border;
|
||||
use iced::mouse;
|
||||
use iced::{Border, Color, Element, Length, Rectangle, Shadow, Size};
|
||||
|
||||
pub struct CustomQuad {
|
||||
size: f32,
|
||||
radius: [f32; 4],
|
||||
radius: border::Radius,
|
||||
border_width: f32,
|
||||
shadow: Shadow,
|
||||
}
|
||||
|
|
@ -16,7 +17,7 @@ mod quad {
|
|||
impl CustomQuad {
|
||||
pub fn new(
|
||||
size: f32,
|
||||
radius: [f32; 4],
|
||||
radius: border::Radius,
|
||||
border_width: f32,
|
||||
shadow: Shadow,
|
||||
) -> Self {
|
||||
|
|
@ -63,7 +64,7 @@ mod quad {
|
|||
renderer::Quad {
|
||||
bounds: layout.bounds(),
|
||||
border: Border {
|
||||
radius: self.radius.into(),
|
||||
radius: self.radius,
|
||||
width: self.border_width,
|
||||
color: Color::from_rgb(1.0, 0.0, 0.0),
|
||||
},
|
||||
|
|
@ -74,22 +75,23 @@ mod quad {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, Message> From<CustomQuad> for Element<'a, Message> {
|
||||
impl<Message> From<CustomQuad> for Element<'_, Message> {
|
||||
fn from(circle: CustomQuad) -> Self {
|
||||
Self::new(circle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
use iced::border;
|
||||
use iced::widget::{center, column, slider, text};
|
||||
use iced::{Alignment, Color, Element, Shadow, Vector};
|
||||
use iced::{Center, Color, Element, Shadow, Vector};
|
||||
|
||||
pub fn main() -> iced::Result {
|
||||
iced::run("Custom Quad - Iced", Example::update, Example::view)
|
||||
}
|
||||
|
||||
struct Example {
|
||||
radius: [f32; 4],
|
||||
radius: border::Radius,
|
||||
border_width: f32,
|
||||
shadow: Shadow,
|
||||
}
|
||||
|
|
@ -110,7 +112,7 @@ enum Message {
|
|||
impl Example {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
radius: [50.0; 4],
|
||||
radius: border::radius(50),
|
||||
border_width: 0.0,
|
||||
shadow: Shadow {
|
||||
color: Color::from_rgba(0.0, 0.0, 0.0, 0.8),
|
||||
|
|
@ -121,19 +123,18 @@ impl Example {
|
|||
}
|
||||
|
||||
fn update(&mut self, message: Message) {
|
||||
let [tl, tr, br, bl] = self.radius;
|
||||
match message {
|
||||
Message::RadiusTopLeftChanged(radius) => {
|
||||
self.radius = [radius, tr, br, bl];
|
||||
self.radius = self.radius.top_left(radius);
|
||||
}
|
||||
Message::RadiusTopRightChanged(radius) => {
|
||||
self.radius = [tl, radius, br, bl];
|
||||
self.radius = self.radius.top_right(radius);
|
||||
}
|
||||
Message::RadiusBottomRightChanged(radius) => {
|
||||
self.radius = [tl, tr, radius, bl];
|
||||
self.radius = self.radius.bottom_right(radius);
|
||||
}
|
||||
Message::RadiusBottomLeftChanged(radius) => {
|
||||
self.radius = [tl, tr, br, radius];
|
||||
self.radius = self.radius.bottom_left(radius);
|
||||
}
|
||||
Message::BorderWidthChanged(width) => {
|
||||
self.border_width = width;
|
||||
|
|
@ -151,7 +152,13 @@ impl Example {
|
|||
}
|
||||
|
||||
fn view(&self) -> Element<Message> {
|
||||
let [tl, tr, br, bl] = self.radius;
|
||||
let border::Radius {
|
||||
top_left,
|
||||
top_right,
|
||||
bottom_right,
|
||||
bottom_left,
|
||||
} = self.radius;
|
||||
|
||||
let Shadow {
|
||||
offset: Vector { x: sx, y: sy },
|
||||
blur_radius: sr,
|
||||
|
|
@ -165,16 +172,16 @@ impl Example {
|
|||
self.border_width,
|
||||
self.shadow
|
||||
),
|
||||
text(format!("Radius: {tl:.2}/{tr:.2}/{br:.2}/{bl:.2}")),
|
||||
slider(1.0..=100.0, tl, Message::RadiusTopLeftChanged).step(0.01),
|
||||
slider(1.0..=100.0, tr, Message::RadiusTopRightChanged).step(0.01),
|
||||
slider(1.0..=100.0, br, Message::RadiusBottomRightChanged)
|
||||
text!("Radius: {top_left:.2}/{top_right:.2}/{bottom_right:.2}/{bottom_left:.2}"),
|
||||
slider(1.0..=100.0, top_left, Message::RadiusTopLeftChanged).step(0.01),
|
||||
slider(1.0..=100.0, top_right, Message::RadiusTopRightChanged).step(0.01),
|
||||
slider(1.0..=100.0, bottom_right, Message::RadiusBottomRightChanged)
|
||||
.step(0.01),
|
||||
slider(1.0..=100.0, bl, Message::RadiusBottomLeftChanged)
|
||||
slider(1.0..=100.0, bottom_left, Message::RadiusBottomLeftChanged)
|
||||
.step(0.01),
|
||||
slider(1.0..=10.0, self.border_width, Message::BorderWidthChanged)
|
||||
.step(0.01),
|
||||
text(format!("Shadow: {sx:.2}x{sy:.2}, {sr:.2}")),
|
||||
text!("Shadow: {sx:.2}x{sy:.2}, {sr:.2}"),
|
||||
slider(-100.0..=100.0, sx, Message::ShadowXOffsetChanged)
|
||||
.step(0.01),
|
||||
slider(-100.0..=100.0, sy, Message::ShadowYOffsetChanged)
|
||||
|
|
@ -185,7 +192,7 @@ impl Example {
|
|||
.padding(20)
|
||||
.spacing(20)
|
||||
.max_width(500)
|
||||
.align_items(Alignment::Center);
|
||||
.align_x(Center);
|
||||
|
||||
center(content).into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@
|
|||
name = "custom_shader"
|
||||
version = "0.1.0"
|
||||
authors = ["Bingus <shankern@protonmail.com>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
iced.workspace = true
|
||||
iced.features = ["debug", "advanced"]
|
||||
iced.features = ["debug", "image", "advanced"]
|
||||
|
||||
image.workspace = true
|
||||
bytemuck.workspace = true
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue