diff --git a/.cargo/config.toml b/.cargo/config.toml index 49ca3252..df979396 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -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" diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml index 20ef2b73..79441958 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -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? diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 5177386c..3e2486d3 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ba1ab003..e7af3b03 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index 827a2ca8..57dc1375 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -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 \ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 47c61f5e..f087f069 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: diff --git a/.gitignore b/.gitignore index f05ec438..9d164436 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ target/ pkg/ **/*.rs.bk -Cargo.lock dist/ traces/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 16a69a7a..fff7d341 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4e7075c6..96302a65 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..9b2fc060 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7654 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom 0.2.15", + "once_cell", + "version_check", + "zerocopy 0.7.35", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.8.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anyhow" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" + +[[package]] +name = "arc" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "ashpd" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c39d707614dbcc6bed00015539f488d8e3fe3e66ed60961efc0c90f4b380b3" +dependencies = [ + "async-fs 2.1.2", + "async-net 2.0.0", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.8.5", + "serde", + "serde_repr", + "url", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener 5.4.0", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.3.0", + "futures-lite 2.6.0", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "blocking", + "futures-lite 1.13.0", +] + +[[package]] +name = "async-fs" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" +dependencies = [ + "async-lock 3.4.0", + "blocking", + "futures-lite 2.6.0", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.3.1", + "async-executor", + "async-io 2.4.0", + "async-lock 3.4.0", + "blocking", + "futures-lite 2.6.0", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.28", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" +dependencies = [ + "async-lock 3.4.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.6.0", + "parking", + "polling 3.7.4", + "rustix 0.38.44", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.4.0", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0434b1ed18ce1cf5769b8ac540e33f01fa9471058b5e89da9e06f3c882a8c12f" +dependencies = [ + "async-io 1.13.0", + "blocking", + "futures-lite 1.13.0", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io 2.4.0", + "blocking", + "futures-lite 2.6.0", +] + +[[package]] +name = "async-process" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +dependencies = [ + "async-io 1.13.0", + "async-lock 2.8.0", + "async-signal", + "blocking", + "cfg-if", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.44", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +dependencies = [ + "async-channel 2.3.1", + "async-io 2.4.0", + "async-lock 3.4.0", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.0", + "futures-lite 2.6.0", + "rustix 0.38.44", + "tracing", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io 2.4.0", + "async-lock 3.4.0", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.44", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-std" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io 2.4.0", + "async-lock 3.4.0", + "async-process 2.3.0", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 2.6.0", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-tungstenite" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cca750b12e02c389c1694d35c16539f88b8bbaa5945934fdc1b41a776688589" +dependencies = [ + "futures-io", + "futures-util", + "log", + "pin-project-lite", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tungstenite", + "webpki-roots", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bezier_tool" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite 2.6.0", + "piper", +] + +[[package]] +name = "blurhash" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79769241dcd44edf79a732545e8b5cec84c247ac060f5252cd51885d093a8fc" + +[[package]] +name = "built" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73848a43c5d63a1251d17adf6c2bf78aa94830e60a335a95eeea45d6ba9e1e4d" + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" + +[[package]] +name = "bytesize" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2c12f985c78475a6b8d629afd0c360260ef34cfef52efccdcfd31972f81c2e" + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.8.0", + "log", + "polling 3.7.4", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "changelog" +version = "0.1.0" +dependencies = [ + "iced", + "log", + "reqwest", + "serde", + "thiserror 1.0.69", + "tokio", + "tracing-subscriber", + "webbrowser", +] + +[[package]] +name = "checkbox" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "4.5.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "clipboard-win" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" +dependencies = [ + "error-code", +] + +[[package]] +name = "clipboard_macos" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "clipboard_wayland" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003f886bc4e2987729d10c1db3424e7f80809f3fc22dbc16c685738887cb37b8" +dependencies = [ + "smithay-clipboard", +] + +[[package]] +name = "clipboard_x11" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4274ea815e013e0f9f04a2633423e14194e408a0576c943ce3d14ca56c50031c" +dependencies = [ + "thiserror 1.0.69", + "x11rb", +] + +[[package]] +name = "clock" +version = "0.1.0" +dependencies = [ + "chrono", + "iced", + "tracing-subscriber", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "color_palette" +version = "0.1.0" +dependencies = [ + "iced", + "palette", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "combo_box" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" +dependencies = [ + "log", + "web-sys", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.8.0", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.8.0", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "cosmic-text" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2" +dependencies = [ + "bitflags 2.8.0", + "fontdb 0.16.2", + "log", + "rangemap", + "rayon", + "rustc-hash 1.1.0", + "rustybuzz", + "self_cell", + "swash", + "sys-locale", + "ttf-parser 0.21.1", + "unicode-bidi", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", +] + +[[package]] +name = "counter" +version = "0.1.0" +dependencies = [ + "iced", + "iced_test", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f791803201ab277ace03903de1594460708d2d54df6053f2d9e82f592b19e3b" + +[[package]] +name = "cursor-icon" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a6ac251f4a2aca6b3f91340350eab87ae57c3f127ffeb585e92bd336717991" + +[[package]] +name = "custom_quad" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "custom_shader" +version = "0.1.0" +dependencies = [ + "bytemuck", + "glam", + "iced", + "image", + "rand 0.8.5", +] + +[[package]] +name = "custom_widget" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "dark-light" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e1a09f280e29a8b00bc7e81eca5ac87dca0575639c9422a5fa25a07bb884b8" +dependencies = [ + "ashpd", + "async-std", + "objc2", + "objc2-foundation", + "web-sys", + "winreg", +] + +[[package]] +name = "data-encoding" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" + +[[package]] +name = "data-url" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.59.0", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "download_progress" +version = "0.1.0" +dependencies = [ + "iced", + "reqwest", +] + +[[package]] +name = "dpi" +version = "0.1.1" +source = "git+https://github.com/iced-rs/winit.git?rev=11414b6aa45699f038114e61b4ddf5102b2d3b4b#11414b6aa45699f038114e61b4ddf5102b2d3b4b" + +[[package]] +name = "drm" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" +dependencies = [ + "bitflags 2.8.0", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53" +dependencies = [ + "drm-sys", + "rustix 0.38.44", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986" +dependencies = [ + "libc", + "linux-raw-sys 0.6.5", +] + +[[package]] +name = "editor" +version = "0.1.0" +dependencies = [ + "iced", + "rfd", + "tokio", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "error-code" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" + +[[package]] +name = "etagere" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "euclid" +version = "0.22.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +dependencies = [ + "event-listener 5.4.0", + "pin-project-lite", +] + +[[package]] +name = "events" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "exit" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "ferris" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "float_next_after" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f" + +[[package]] +name = "font-types" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3971f9a5ca983419cdc386941ba3b9e1feba01a0ab888adf78739feb2798492" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fcfcd44ca6e90c921fee9fa665d530b21ef1327a4c1a6c5250ea44b776ada7" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.20.0", +] + +[[package]] +name = "fontdb" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.21.1", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", + "num_cpus", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand 2.3.0", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "gallery" +version = "0.1.0" +dependencies = [ + "blurhash", + "bytes", + "iced", + "image", + "reqwest", + "serde", + "sipper", + "tokio", +] + +[[package]] +name = "game_of_life" +version = "0.1.0" +dependencies = [ + "iced", + "itertools 0.12.1", + "rustc-hash 2.1.1", + "tokio", + "tracing-subscriber", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "geometry" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glam" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "glow" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "glyphon" +version = "0.5.0" +source = "git+https://github.com/hecrj/glyphon.git?rev=09712a70df7431e9a3b1ac1bbd4fb634096cb3b4#09712a70df7431e9a3b1ac1bbd4fb634096cb3b4" +dependencies = [ + "cosmic-text", + "etagere", + "lru", + "rustc-hash 2.1.1", + "wgpu", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.8.0", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.8.0", +] + +[[package]] +name = "gpu-allocator" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "windows 0.58.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf29e94d6d243368b7a56caa16bc213e4f9f8ed38c4d9557069527b5d5281ca" +dependencies = [ + "bitflags 2.8.0", + "gpu-descriptor-types", + "hashbrown", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.8.0", +] + +[[package]] +name = "gradient" +version = "0.1.0" +dependencies = [ + "iced", + "tracing-subscriber", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "guillotiere" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" +dependencies = [ + "euclid", + "svg_fmt", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.2.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "foldhash", +] + +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http 0.2.12", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http 0.2.12", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.2.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.8", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.7", + "http 1.2.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http 1.2.0", + "hyper 1.6.0", + "hyper-util", + "rustls 0.23.23", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.1", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.2.0", + "http-body 1.0.1", + "hyper 1.6.0", + "pin-project-lite", + "socket2 0.5.8", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "iced" +version = "0.14.0-dev" +dependencies = [ + "criterion", + "iced_core", + "iced_debug", + "iced_futures", + "iced_highlighter", + "iced_renderer", + "iced_wgpu", + "iced_widget", + "iced_winit", + "image", + "thiserror 1.0.69", +] + +[[package]] +name = "iced_beacon" +version = "0.14.0-dev" +dependencies = [ + "bincode", + "futures", + "iced_core", + "log", + "semver", + "serde", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "iced_core" +version = "0.14.0-dev" +dependencies = [ + "approx", + "bitflags 2.8.0", + "bytes", + "dark-light", + "glam", + "lilt", + "log", + "num-traits", + "palette", + "raw-window-handle 0.6.2", + "rustc-hash 2.1.1", + "serde", + "smol_str", + "thiserror 1.0.69", + "web-time", +] + +[[package]] +name = "iced_debug" +version = "0.14.0-dev" +dependencies = [ + "iced_beacon", + "iced_core", +] + +[[package]] +name = "iced_futures" +version = "0.14.0-dev" +dependencies = [ + "async-std", + "futures", + "iced_core", + "log", + "rustc-hash 2.1.1", + "smol", + "tokio", + "wasm-bindgen-futures", + "wasmtimer", +] + +[[package]] +name = "iced_graphics" +version = "0.14.0-dev" +dependencies = [ + "bitflags 2.8.0", + "bytemuck", + "cosmic-text", + "half", + "iced_core", + "iced_futures", + "image", + "kamadak-exif", + "log", + "lyon_path", + "raw-window-handle 0.6.2", + "rustc-hash 2.1.1", + "thiserror 1.0.69", + "unicode-segmentation", +] + +[[package]] +name = "iced_highlighter" +version = "0.14.0-dev" +dependencies = [ + "iced_core", + "syntect", +] + +[[package]] +name = "iced_renderer" +version = "0.14.0-dev" +dependencies = [ + "iced_graphics", + "iced_tiny_skia", + "iced_wgpu", + "log", + "thiserror 1.0.69", +] + +[[package]] +name = "iced_runtime" +version = "0.14.0-dev" +dependencies = [ + "bytes", + "iced_core", + "iced_debug", + "iced_futures", + "raw-window-handle 0.6.2", + "sipper", + "thiserror 1.0.69", +] + +[[package]] +name = "iced_test" +version = "0.14.0-dev" +dependencies = [ + "iced_renderer", + "iced_runtime", + "png", + "sha2", + "thiserror 1.0.69", +] + +[[package]] +name = "iced_tiny_skia" +version = "0.14.0-dev" +dependencies = [ + "bytemuck", + "cosmic-text", + "iced_graphics", + "kurbo 0.10.4", + "log", + "resvg", + "rustc-hash 2.1.1", + "softbuffer", + "tiny-skia", +] + +[[package]] +name = "iced_wgpu" +version = "0.14.0-dev" +dependencies = [ + "bitflags 2.8.0", + "bytemuck", + "futures", + "glam", + "glyphon", + "guillotiere", + "iced_graphics", + "log", + "lyon", + "resvg", + "rustc-hash 2.1.1", + "thiserror 1.0.69", + "wgpu", +] + +[[package]] +name = "iced_widget" +version = "0.14.0-dev" +dependencies = [ + "iced_highlighter", + "iced_renderer", + "iced_runtime", + "log", + "num-traits", + "ouroboros", + "pulldown-cmark", + "qrcode", + "rustc-hash 2.1.1", + "thiserror 1.0.69", + "unicode-segmentation", + "url", +] + +[[package]] +name = "iced_winit" +version = "0.14.0-dev" +dependencies = [ + "iced_debug", + "iced_futures", + "iced_graphics", + "iced_runtime", + "log", + "rustc-hash 2.1.1", + "sysinfo", + "thiserror 1.0.69", + "tracing", + "wasm-bindgen-futures", + "web-sys", + "window_clipboard", + "winit", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" + +[[package]] +name = "imgref" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "integration" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "console_log", + "iced_wgpu", + "iced_widget", + "iced_winit", + "tracing-subscriber", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-terminal" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" +dependencies = [ + "hermit-abi 0.4.0", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kamadak-exif" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" +dependencies = [ + "mutate_once", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kurbo" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440" +dependencies = [ + "arrayvec", + "smallvec", +] + +[[package]] +name = "kurbo" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89234b2cc610a7dd927ebde6b41dd1a5d4214cffaef4cf1fb2195d592f92518f" +dependencies = [ + "arrayvec", + "smallvec", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "layout" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "lazy" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libc" +version = "0.2.169" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libm" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.8.0", + "libc", + "redox_syscall 0.5.8", +] + +[[package]] +name = "lilt" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab94c7e69044511f79ce4b4201a49324b7f5b35410f862264e044690b950a67" +dependencies = [ + "web-time", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "loading_spinners" +version = "0.1.0" +dependencies = [ + "iced", + "lyon_algorithms", +] + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +dependencies = [ + "value-bag", +] + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "loupe" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" + +[[package]] +name = "lyon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7f9cda98b5430809e63ca5197b06c7d191bf7e26dfc467d5a3f0290e2a74f" +dependencies = [ + "lyon_algorithms", + "lyon_tessellation", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f13c9be19d257c7d37e70608ed858e8eab4b2afcea2e3c9a622e892acbf43c08" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_geom" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8af69edc087272df438b3ee436c4bb6d7c04aa8af665cfd398feae627dbd8570" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e0b8aec2f58586f6eef237985b9a9b7cb3a3aff4417c575075cf95bf925252e" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "lyon_tessellation" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579d42360a4b09846eff2feef28f538696c7d6c7439bfa65874ff3cbe0951b2c" +dependencies = [ + "float_next_after", + "lyon_path", + "num-traits", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markdown" +version = "0.1.0" +dependencies = [ + "iced", + "image", + "open", + "reqwest", + "tokio", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "maybe_parallel_iterator" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5069b219d51d2ba2d9388623bd7eead5d4e7974bc8ff3ec9edbe36b09c0ef477" +dependencies = [ + "rayon", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memmap2" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.8.0", + "block", + "core-graphics-types 0.1.3", + "foreign-types 0.5.0", + "log", + "objc", + "paste", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "modal" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 0.2.12", + "httparse", + "log", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "multi_window" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "multitouch" +version = "0.1.0" +dependencies = [ + "iced", + "tracing-subscriber", + "voronator", +] + +[[package]] +name = "mutate_once" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" + +[[package]] +name = "naga" +version = "23.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "364f94bc34f61332abebe8cad6f6cd82a5b65cff22c828d05d0968911462ca4f" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.8.0", + "cfg_aliases 0.1.1", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "rustc-hash 1.1.0", + "spirv", + "termcolor", + "thiserror 1.0.69", + "unicode-xid", +] + +[[package]] +name = "native-tls" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.8.0", + "jni-sys", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle 0.6.2", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.8.0", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.8.0", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.8.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.8.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2", + "objc2", + "objc2-contacts", + "objc2-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.8.0", + "block2", + "dispatch", + "libc", + "objc2", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.8.0", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.8.0", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.8.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.8.0", + "block2", + "objc2", + "objc2-core-location", + "objc2-foundation", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" + +[[package]] +name = "onig" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +dependencies = [ + "bitflags 1.3.2", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "oorandom" +version = "11.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + +[[package]] +name = "open" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" +dependencies = [ + "bitflags 2.8.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orbclient" +version = "0.3.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba0b26cec2e24f08ed8bb31519a9333140a6599b867dac464bb150bdb796fd43" +dependencies = [ + "libredox", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "ouroboros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" +dependencies = [ + "aliasable", + "ouroboros_macro", + "static_assertions", +] + +[[package]] +name = "ouroboros_macro" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owned_ttf_parser" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" +dependencies = [ + "ttf-parser 0.25.1", +] + +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "palette_derive", + "phf", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pane_grid" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.8", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pick_list" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand 2.3.0", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "plist" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +dependencies = [ + "base64 0.22.1", + "indexmap", + "quick-xml 0.32.0", + "serde", + "time", +] + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "pokedex" +version = "0.1.0" +dependencies = [ + "getrandom 0.2.15", + "iced", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix 0.38.44", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy 0.7.35", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "progress_bar" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "pulldown-cmark" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" +dependencies = [ + "bitflags 2.8.0", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "qr_code" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "qrcode" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "166f136dfdb199f98186f3649cf7a0536534a61417a1a30221b492b4fb60ce3f" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.0", + "zerocopy 0.8.18", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.0", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +dependencies = [ + "getrandom 0.3.1", + "zerocopy 0.8.18", +] + +[[package]] +name = "range-alloc" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" + +[[package]] +name = "rangemap" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684" + +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.12.1", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha 0.3.1", + "simd_helpers", + "system-deps", + "thiserror 1.0.69", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.22.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69aacb76b5c29acfb7f90155d39759a29496aebb49395830e928a9703d2eec2f" +dependencies = [ + "bytemuck", + "font-types", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.8.0", +] + +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom 0.2.15", + "libredox", + "thiserror 2.0.11", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "reqwest" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.7", + "http 1.2.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry", +] + +[[package]] +name = "resvg" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "944d052815156ac8fa77eaac055220e95ba0b01fa8887108ca710c03805d9051" +dependencies = [ + "gif", + "jpeg-decoder", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", +] + +[[package]] +name = "rfd" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0d8ab342bcc5436e04d3a4c1e09e17d74958bfaddf8d5fad6f85607df0f994f" +dependencies = [ + "block", + "dispatch", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle 0.5.2", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "0.37.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.8.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + +[[package]] +name = "rustybuzz" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" +dependencies = [ + "bitflags 2.8.0", + "bytemuck", + "libm", + "smallvec", + "ttf-parser 0.21.1", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "screenshot" +version = "0.1.0" +dependencies = [ + "iced", + "image", + "tokio", + "tracing-subscriber", +] + +[[package]] +name = "scrollable" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.8.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.217" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.138" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "sierpinski_triangle" +version = "0.1.0" +dependencies = [ + "iced", + "rand 0.8.5", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "sipper" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bccb4192828b3d9a08e0b5a73f17795080dfb278b50190216e3ae2132cf4f95" +dependencies = [ + "futures", + "pin-project-lite", +] + +[[package]] +name = "skrifa" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1c44ad1f6c5bdd4eefed8326711b7dbda9ea45dfd36068c427d332aa382cbe" +dependencies = [ + "bytemuck", + "read-fonts", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slider" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.8.0", + "calloop", + "calloop-wayland-source", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc8216eec463674a0e90f29e0ae41a4db573ec5b56b1c6c1c71615d249b6d846" +dependencies = [ + "libc", + "smithay-client-toolkit", + "wayland-backend", +] + +[[package]] +name = "smol" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1" +dependencies = [ + "async-channel 1.9.0", + "async-executor", + "async-fs 1.6.0", + "async-io 1.13.0", + "async-lock 2.8.0", + "async-net 1.8.0", + "async-process 1.8.1", + "blocking", + "futures-lite 1.13.0", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "cfg_aliases 0.2.1", + "core-graphics 0.24.0", + "drm", + "fastrand 2.3.0", + "foreign-types 0.5.0", + "js-sys", + "log", + "memmap2", + "objc2", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle 0.6.2", + "redox_syscall 0.5.8", + "rustix 0.38.44", + "tiny-xlib", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.59.0", + "x11rb", +] + +[[package]] +name = "solar_system" +version = "0.1.0" +dependencies = [ + "iced", + "rand 0.8.5", + "tracing-subscriber", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.8.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stopwatch" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "styling" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "svg" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "svg_fmt" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d813d71d82c4cbc1742135004e4a79fd870214c155443451c139c9470a0aa" + +[[package]] +name = "svgtypes" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" +dependencies = [ + "kurbo 0.11.1", + "siphasher", +] + +[[package]] +name = "swash" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd59f3f359ddd2c95af4758c18270eddd9c730dde98598023cdabff472c2ca2" +dependencies = [ + "skrifa", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "2.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syntect" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" +dependencies = [ + "bincode", + "bitflags 1.3.2", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows 0.57.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.8.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "system_information" +version = "0.1.0" +dependencies = [ + "bytesize", + "iced", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tempfile" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +dependencies = [ + "cfg-if", + "fastrand 2.3.0", + "getrandom 0.3.1", + "once_cell", + "rustix 0.38.44", + "windows-sys 0.59.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "the_matrix" +version = "0.1.0" +dependencies = [ + "iced", + "rand 0.8.5", + "tracing-subscriber", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" +dependencies = [ + "as-raw-xcb-connection", + "ctor-lite", + "libloading", + "pkg-config", + "tracing", +] + +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toast" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "todos" +version = "0.1.0" +dependencies = [ + "async-std", + "directories", + "iced", + "iced_test", + "serde", + "serde_json", + "tracing-subscriber", + "uuid", + "wasmtimer", + "web-sys", +] + +[[package]] +name = "tokio" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.8", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls 0.23.23", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tooltip" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "tour" +version = "0.1.0" +dependencies = [ + "console_error_panic_hook", + "console_log", + "iced", + "tracing-subscriber", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" + +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.2.0", + "httparse", + "log", + "rand 0.8.5", + "rustls 0.22.4", + "rustls-pki-types", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cb788ffebc92c5948d0e997106233eeb1d8b9512f93f41651f52b6c5f5af86" + +[[package]] +name = "unicode-ccc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" + +[[package]] +name = "unicode-ident" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-script" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "url_handler" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "usvg" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84ea542ae85c715f07b082438a4231c3760539d902e11d093847a0b22963032" +dependencies = [ + "base64 0.22.1", + "data-url", + "flate2", + "fontdb 0.18.0", + "imagesize", + "kurbo 0.11.1", + "log", + "pico-args", + "roxmltree", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" +dependencies = [ + "getrandom 0.3.1", + "js-sys", + "rand 0.9.0", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "value-bag" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vectorial_text" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "visible_bounds" +version = "0.1.0" +dependencies = [ + "iced", +] + +[[package]] +name = "voronator" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07d8e6fe4b9b3b443f2e4ada327cf4b08ab5cdd27a42cd5c9f38dd29092c10ee" +dependencies = [ + "maybe_parallel_iterator", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "warp" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "headers", + "http 0.2.12", + "hyper 0.14.32", + "log", + "mime", + "mime_guess", + "multer", + "percent-encoding", + "pin-project", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-tungstenite", + "tokio-util", + "tower-service", + "tracing", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmtimer" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0048ad49a55b9deb3953841fa1fc5858f0efbcb7a18868c899a360269fac1b23" +dependencies = [ + "futures", + "js-sys", + "parking_lot", + "pin-utils", + "slab", + "wasm-bindgen", +] + +[[package]] +name = "wayland-backend" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" +dependencies = [ + "cc", + "downcast-rs", + "rustix 0.38.44", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" +dependencies = [ + "bitflags 2.8.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.8.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d" +dependencies = [ + "rustix 0.38.44", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" +dependencies = [ + "bitflags 2.8.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ccaacc76703fefd6763022ac565b590fcade92202492381c95b2edfdf7d46b3" +dependencies = [ + "bitflags 2.8.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" +dependencies = [ + "bitflags 2.8.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.2", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9fe1ebb156110ff855242c1101df158b822487e4957b0556d9ffce9db0f535" +dependencies = [ + "block2", + "core-foundation 0.10.0", + "home", + "jni", + "log", + "ndk-context", + "objc2", + "objc2-foundation", + "url", + "web-sys", +] + +[[package]] +name = "webpki-roots" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "websocket" +version = "1.0.0" +dependencies = [ + "async-tungstenite", + "iced", + "tokio", + "warp", +] + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[package]] +name = "wgpu" +version = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80f70000db37c469ea9d67defdc13024ddf9a5f1b89cb2941b812ad7cde1735a" +dependencies = [ + "arrayvec", + "cfg_aliases 0.1.1", + "document-features", + "js-sys", + "log", + "naga", + "parking_lot", + "profiling", + "raw-window-handle 0.6.2", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d63c3c478de8e7e01786479919c8769f62a22eec16788d8c2ac77ce2c132778a" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.8.0", + "cfg_aliases 0.1.1", + "document-features", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle 0.6.2", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "23.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89364b8a0b211adc7b16aeaf1bd5ad4a919c1154b44c9ce27838213ba05fd821" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.8.0", + "block", + "bytemuck", + "cfg_aliases 0.1.1", + "core-graphics-types 0.1.3", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "parking_lot", + "profiling", + "range-alloc", + "raw-window-handle 0.6.2", + "renderdoc-sys", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows 0.58.0", + "windows-core 0.58.0", +] + +[[package]] +name = "wgpu-types" +version = "23.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610f6ff27778148c31093f3b03abc4840f9636d58d597ca2f5977433acfe0068" +dependencies = [ + "bitflags 2.8.0", + "js-sys", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window_clipboard" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d692d46038c433f9daee7ad8757e002a4248c20b0a3fbc991d99521d3bcb6d" +dependencies = [ + "clipboard-win", + "clipboard_macos", + "clipboard_wayland", + "clipboard_x11", + "raw-window-handle 0.6.2", + "thiserror 1.0.69", +] + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winit" +version = "0.30.8" +source = "git+https://github.com/iced-rs/winit.git?rev=11414b6aa45699f038114e61b4ddf5102b2d3b4b#11414b6aa45699f038114e61b4ddf5102b2d3b4b" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.8.0", + "block2", + "bytemuck", + "calloop", + "cfg_aliases 0.2.1", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle 0.6.2", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 0.38.44", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" + +[[package]] +name = "xcursor" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61" + +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.8.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yazi" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c333f648ea1b647bc95dc1d34807c8e25ed7a6feff3394034dc4776054b236" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs 2.1.2", + "async-io 2.4.0", + "async-lock 3.4.0", + "async-process 2.3.0", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener 5.4.0", + "futures-core", + "futures-lite 2.6.0", + "hex", + "nix", + "ordered-stream", + "serde", + "serde_repr", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f325ad10eb0d0a3eb060203494c3b7ec3162a01a59db75d2deee100339709fc0" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow", + "zvariant", +] + +[[package]] +name = "zeno" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79386d31a42a4996e3336b0919ddb90f81112af416270cff95b5f5af22b839c2" +dependencies = [ + "zerocopy-derive 0.8.18", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76331675d372f91bf8d17e13afbd5fe639200b73d01f0fc748bb059f9cca2db7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2df9ee044893fcffbdc25de30546edef3e32341466811ca18421e3cd6c5a3ac" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74170caa85b8b84cc4935f2d56a57c7a15ea6185ccdd7eadb57e6edd90f94b2f" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml index 046e92ff..ef059701 100644 --- a/Cargo.toml +++ b/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 "] -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" diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index 5d738d85..6ae9995e 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -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; + }; + } + ); +} +``` diff --git a/ECOSYSTEM.md b/ECOSYSTEM.md deleted file mode 100644 index da3066d8..00000000 --- a/ECOSYSTEM.md +++ /dev/null @@ -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. - -

- The Iced Ecosystem -

- -## 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. - -

- The foundations -

- -## The native target -The native side of the ecosystem is split into two different groups: __renderers__ and __shells__. - -

- The native target -

- -### 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`]. - -

- Iced -

- -[`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/ diff --git a/README.md b/README.md index 0db09ded..9cfa03de 100644 --- a/README.md +++ b/README.md @@ -15,11 +15,11 @@ A cross-platform GUI library for Rust focused on simplicity and type-safety. Inspired by [Elm]. - - + + - - + + @@ -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: - -

- - The Iced Ecosystem - -

+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/ diff --git a/ROADMAP.md b/ROADMAP.md index afcece7c..a7f3b677 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 diff --git a/benches/wgpu.rs b/benches/wgpu.rs index cc90bb38..29a84b00 100644 --- a/benches/wgpu.rs +++ b/benches/wgpu.rs @@ -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, )) diff --git a/core/Cargo.toml b/core/Cargo.toml index 4ea6b330..184055e8 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -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 diff --git a/core/README.md b/core/README.md index 519e0608..de11acad 100644 --- a/core/README.md +++ b/core/README.md @@ -13,15 +13,3 @@ This crate is meant to be a starting point for an Iced runtime.

[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 diff --git a/core/src/alignment.rs b/core/src/alignment.rs index 51b7fca9..8f01ef71 100644 --- a/core/src/alignment.rs +++ b/core/src/alignment.rs @@ -46,6 +46,16 @@ pub enum Horizontal { Right, } +impl From 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 for Vertical { + fn from(alignment: Alignment) -> Self { + match alignment { + Alignment::Start => Self::Top, + Alignment::Center => Self::Center, + Alignment::End => Self::Bottom, + } + } +} diff --git a/core/src/angle.rs b/core/src/angle.rs index 9c8a9b24..0882ae80 100644 --- a/core/src/angle.rs +++ b/core/src/angle.rs @@ -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 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) + } +} diff --git a/core/src/animation.rs b/core/src/animation.rs new file mode 100644 index 00000000..14cbb5c3 --- /dev/null +++ b/core/src/animation.rs @@ -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 +where + T: Clone + Copy + PartialEq + Float, +{ + raw: lilt::Animated, + duration: Duration, // TODO: Expose duration getter in `lilt` +} + +impl Animation +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(&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 { + /// 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(&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, + )) + } +} diff --git a/core/src/background.rs b/core/src/background.rs index c8b7cbea..1f665ef4 100644 --- a/core/src/background.rs +++ b/core/src/background.rs @@ -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)] diff --git a/core/src/border.rs b/core/src/border.rs index 2df24988..da0aaa28 100644 --- a/core/src/border.rs +++ b/core/src/border.rs @@ -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) -> 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) -> Border { + Border::default().rounded(radius) +} - /// Updates the [`Color`] of the [`Border`]. - pub fn with_color(self, color: impl Into) -> 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) -> 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) -> Border { + Border::default().width(width) +} + +impl Border { + /// Sets the [`Color`] of the [`Border`]. + pub fn color(self, color: impl Into) -> Self { Self { color: color.into(), ..self } } - /// Updates the [`Radius`] of the [`Border`]. - pub fn with_radius(self, radius: impl Into) -> Self { + /// Sets the [`Radius`] of the [`Border`]. + pub fn rounded(self, radius: impl Into) -> Self { Self { radius: radius.into(), ..self } } - /// Updates the width of the [`Border`]. - pub fn with_width(self, width: impl Into) -> Self { + /// Sets the width of the [`Border`]. + pub fn width(self, width: impl Into) -> 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) -> Radius { + Radius::new(value) +} + +/// Creates a new [`Radius`] with the given top left value. +pub fn top_left(value: impl Into) -> Radius { + Radius::default().top_left(value) +} + +/// Creates a new [`Radius`] with the given top right value. +pub fn top_right(value: impl Into) -> Radius { + Radius::default().top_right(value) +} + +/// Creates a new [`Radius`] with the given bottom right value. +pub fn bottom_right(value: impl Into) -> Radius { + Radius::default().bottom_right(value) +} + +/// Creates a new [`Radius`] with the given bottom left value. +pub fn bottom_left(value: impl Into) -> 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) -> 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) -> 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) -> 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) -> Radius { + Radius::default().right(value) +} + +impl Radius { + /// Creates a new [`Radius`] with the same value for each corner. + pub fn new(value: impl Into) -> 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) -> Self { + Self { + top_left: value.into().0, + ..self + } + } + + /// Sets the top right value of the [`Radius`]. + pub fn top_right(self, value: impl Into) -> Self { + Self { + top_right: value.into().0, + ..self + } + } + + /// Sets the bottom right value of the [`Radius`]. + pub fn bottom_right(self, value: impl Into) -> Self { + Self { + bottom_right: value.into().0, + ..self + } + } + + /// Sets the bottom left value of the [`Radius`]. + pub fn bottom_left(self, value: impl Into) -> 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) -> 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) -> 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) -> 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) -> Self { + let value = value.into().0; + + Self { + top_right: value, + bottom_right: value, + ..self + } + } +} impl From 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 for Radius { } } -impl From<[f32; 4]> for Radius { - fn from(radi: [f32; 4]) -> Self { - Self(radi) - } -} - impl From for [f32; 4] { fn from(radi: Radius) -> Self { - radi.0 + [ + radi.top_left, + radi.top_right, + radi.bottom_right, + radi.bottom_left, + ] } } diff --git a/core/src/border_radius.rs b/core/src/border_radius.rs deleted file mode 100644 index a444dd74..00000000 --- a/core/src/border_radius.rs +++ /dev/null @@ -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 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 for [f32; 4] { - fn from(radi: BorderRadius) -> Self { - radi.0 - } -} diff --git a/core/src/color.rs b/core/src/color.rs index 60fd9a3d..d381d6d3 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -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 { + 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()); + } } diff --git a/core/src/element.rs b/core/src/element.rs index 7d918a2e..b7d51aeb 100644 --- a/core/src/element.rs +++ b/core/src/element.rs @@ -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, @@ -144,7 +143,7 @@ impl<'a, Message, Theme, Renderer> Element<'a, Message, Theme, Renderer> { /// // Here we turn our `Element` into /// // an `Element` 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, + operation: &mut dyn widget::Operation, ) { - struct MapOperation<'a, B> { - operation: &'a mut dyn widget::Operation, - } - - impl<'a, T, B> widget::Operation for MapOperation<'a, B> { - fn container( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation, - ), - ) { - 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 }, - ); + self.widget.operate(tree, layout, renderer, 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 - for Explain<'a, Message, Theme, Renderer> +impl Widget + 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, + 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( diff --git a/core/src/event.rs b/core/src/event.rs index 870b3074..7f0ab914 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -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. diff --git a/core/src/image.rs b/core/src/image.rs index 82ecdd0f..f985636a 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -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 { + /// 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 { + /// Creates a new [`Image`] with the given handle. + pub fn new(handle: impl Into) -> 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) -> Self { + self.rotation = rotation.into(); + self + } + + /// Sets the opacity of the [`Image`]. + pub fn opacity(mut self, opacity: impl Into) -> 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; - /// 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, bounds: Rectangle); } diff --git a/core/src/input_method.rs b/core/src/input_method.rs new file mode 100644 index 00000000..cd8d459d --- /dev/null +++ b/core/src/input_method.rs @@ -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 { + /// 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>, + }, +} + +/// The pre-edit of an [`InputMethod`]. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Preedit { + /// The current content. + pub content: T, + /// The selected range of the content. + pub selection: Option>, + /// The text size of the content. + pub text_size: Option, +} + +impl Preedit { + /// 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, + { + 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>(&mut self, other: &InputMethod) { + 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 InputMethod { + /// Turns an [`InputMethod`] into its owned version. + pub fn to_owned(&self) -> InputMethod + where + T: AsRef, + { + 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>), + + /// 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, +} diff --git a/core/src/keyboard/event.rs b/core/src/keyboard/event.rs index 1eb42334..88c57b21 100644 --- a/core/src/keyboard/event.rs +++ b/core/src/keyboard/event.rs @@ -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, diff --git a/core/src/keyboard/key.rs b/core/src/keyboard/key.rs index dbde5196..8edb280c 100644 --- a/core/src/keyboard/key.rs +++ b/core/src/keyboard/key.rs @@ -32,6 +32,12 @@ impl Key { } } +impl From 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 { + /// ` on a US keyboard. This is also called a backtick or grave. + /// This is the 半角/全角/漢字 + /// (hankaku/zenkaku/kanji) key on Japanese keyboards + Backquote, + /// Used for both the US \\ (on the 101-key layout) and also for the key + /// located between the " and Enter keys on row C of the 102-, + /// 104- and 106-key layouts. + /// Labeled # on a UK (102) keyboard. + Backslash, + /// [ on a US keyboard. + BracketLeft, + /// ] on a US keyboard. + BracketRight, + /// , on a US keyboard. + Comma, + /// 0 on a US keyboard. + Digit0, + /// 1 on a US keyboard. + Digit1, + /// 2 on a US keyboard. + Digit2, + /// 3 on a US keyboard. + Digit3, + /// 4 on a US keyboard. + Digit4, + /// 5 on a US keyboard. + Digit5, + /// 6 on a US keyboard. + Digit6, + /// 7 on a US keyboard. + Digit7, + /// 8 on a US keyboard. + Digit8, + /// 9 on a US keyboard. + Digit9, + /// = on a US keyboard. + Equal, + /// Located between the left Shift and Z keys. + /// Labeled \\ on a UK keyboard. + IntlBackslash, + /// Located between the / and right Shift keys. + /// Labeled \\ (ro) on a Japanese keyboard. + IntlRo, + /// Located between the = and Backspace keys. + /// Labeled ¥ (yen) on a Japanese keyboard. \\ on a + /// Russian keyboard. + IntlYen, + /// a on a US keyboard. + /// Labeled q on an AZERTY (e.g., French) keyboard. + KeyA, + /// b on a US keyboard. + KeyB, + /// c on a US keyboard. + KeyC, + /// d on a US keyboard. + KeyD, + /// e on a US keyboard. + KeyE, + /// f on a US keyboard. + KeyF, + /// g on a US keyboard. + KeyG, + /// h on a US keyboard. + KeyH, + /// i on a US keyboard. + KeyI, + /// j on a US keyboard. + KeyJ, + /// k on a US keyboard. + KeyK, + /// l on a US keyboard. + KeyL, + /// m on a US keyboard. + KeyM, + /// n on a US keyboard. + KeyN, + /// o on a US keyboard. + KeyO, + /// p on a US keyboard. + KeyP, + /// q on a US keyboard. + /// Labeled a on an AZERTY (e.g., French) keyboard. + KeyQ, + /// r on a US keyboard. + KeyR, + /// s on a US keyboard. + KeyS, + /// t on a US keyboard. + KeyT, + /// u on a US keyboard. + KeyU, + /// v on a US keyboard. + KeyV, + /// w on a US keyboard. + /// Labeled z on an AZERTY (e.g., French) keyboard. + KeyW, + /// x on a US keyboard. + KeyX, + /// y on a US keyboard. + /// Labeled z on a QWERTZ (e.g., German) keyboard. + KeyY, + /// z on a US keyboard. + /// Labeled w on an AZERTY (e.g., French) keyboard, and y on a + /// QWERTZ (e.g., German) keyboard. + KeyZ, + /// - on a US keyboard. + Minus, + /// . on a US keyboard. + Period, + /// ' on a US keyboard. + Quote, + /// ; on a US keyboard. + Semicolon, + /// / on a US keyboard. + Slash, + /// Alt, Option, or . + AltLeft, + /// Alt, Option, or . + /// This is labeled AltGr on many keyboard layouts. + AltRight, + /// Backspace or . + /// Labeled Delete on Apple keyboards. + Backspace, + /// CapsLock or + CapsLock, + /// The application context menu key, which is typically found between the right + /// Super key and the right Control key. + ContextMenu, + /// Control or + ControlLeft, + /// Control or + ControlRight, + /// Enter or . Labeled Return on Apple keyboards. + Enter, + /// The Windows, , Command, or other OS symbol key. + SuperLeft, + /// The Windows, , Command, or other OS symbol key. + SuperRight, + /// Shift or + ShiftLeft, + /// Shift or + ShiftRight, + /// (space) + Space, + /// Tab or + Tab, + /// Japanese: (henkan) + Convert, + /// Japanese: カタカナ/ひらがな/ローマ字 + /// (katakana/hiragana/romaji) + KanaMode, + /// Korean: HangulMode 한/영 (han/yeong) + /// + /// Japanese (Mac keyboard): (kana) + Lang1, + /// Korean: Hanja (hanja) + /// + /// Japanese (Mac keyboard): (eisu) + Lang2, + /// Japanese (word-processing keyboard): Katakana + Lang3, + /// Japanese (word-processing keyboard): Hiragana + Lang4, + /// Japanese (word-processing keyboard): Zenkaku/Hankaku + Lang5, + /// Japanese: 無変換 (muhenkan) + NonConvert, + /// . The forward delete key. + /// Note that on Apple keyboards, the key labelled Delete on the main part of + /// the keyboard is encoded as [`Backspace`]. + /// + /// [`Backspace`]: Self::Backspace + Delete, + /// Page Down, End, or + End, + /// Help. Not present on standard PC keyboards. + Help, + /// Home or + Home, + /// Insert or Ins. Not present on Apple keyboards. + Insert, + /// Page Down, PgDn, or + PageDown, + /// Page Up, PgUp, or + PageUp, + /// + ArrowDown, + /// + ArrowLeft, + /// + ArrowRight, + /// + ArrowUp, + /// On the Mac, this is used for the numpad Clear key. + NumLock, + /// 0 Ins on a keyboard. 0 on a phone or remote control + Numpad0, + /// 1 End on a keyboard. 1 or 1 QZ on a phone or remote + /// control + Numpad1, + /// 2 ↓ on a keyboard. 2 ABC on a phone or remote control + Numpad2, + /// 3 PgDn on a keyboard. 3 DEF on a phone or remote control + Numpad3, + /// 4 ← on a keyboard. 4 GHI on a phone or remote control + Numpad4, + /// 5 on a keyboard. 5 JKL on a phone or remote control + Numpad5, + /// 6 → on a keyboard. 6 MNO on a phone or remote control + Numpad6, + /// 7 Home on a keyboard. 7 PQRS or 7 PRS on a phone + /// or remote control + Numpad7, + /// 8 ↑ on a keyboard. 8 TUV on a phone or remote control + Numpad8, + /// 9 PgUp on a keyboard. 9 WXYZ or 9 WXY on a phone + /// or remote control + Numpad9, + /// + + NumpadAdd, + /// Found on the Microsoft Natural Keyboard. + NumpadBackspace, + /// C or A (All Clear). Also for use with numpads that have a + /// Clear key that is separate from the NumLock key. On the Mac, the + /// numpad Clear key is encoded as [`NumLock`]. + /// + /// [`NumLock`]: Self::NumLock + NumpadClear, + /// C (Clear Entry) + NumpadClearEntry, + /// , (thousands separator). For locales where the thousands separator + /// is a "." (e.g., Brazil), this key may generate a .. + NumpadComma, + /// . Del. For locales where the decimal separator is "," (e.g., + /// Brazil), this key may generate a ,. + NumpadDecimal, + /// / + NumpadDivide, + NumpadEnter, + /// = + NumpadEqual, + /// # on a phone or remote control device. This key is typically found + /// below the 9 key and to the right of the 0 key. + NumpadHash, + /// M Add current entry to the value stored in memory. + NumpadMemoryAdd, + /// M Clear the value stored in memory. + NumpadMemoryClear, + /// M Replace the current entry with the value stored in memory. + NumpadMemoryRecall, + /// M Replace the value stored in memory with the current entry. + NumpadMemoryStore, + /// M Subtract current entry from the value stored in memory. + NumpadMemorySubtract, + /// * on a keyboard. For use with numpads that provide mathematical + /// operations (+, - * and /). + /// + /// Use `NumpadStar` for the * key on phones and remote controls. + NumpadMultiply, + /// ( Found on the Microsoft Natural Keyboard. + NumpadParenLeft, + /// ) Found on the Microsoft Natural Keyboard. + NumpadParenRight, + /// * on a phone or remote control device. + /// + /// This key is typically found below the 7 key and to the left of + /// the 0 key. + /// + /// Use "NumpadMultiply" for the * key on + /// numeric keypads. + NumpadStar, + /// - + NumpadSubtract, + /// Esc or + Escape, + /// Fn This is typically a hardware key that does not generate a separate code. + Fn, + /// FLock or FnLock. Function Lock key. Found on the Microsoft + /// Natural Keyboard. + FnLock, + /// PrtScr SysRq or Print Screen + PrintScreen, + /// Scroll Lock + ScrollLock, + /// Pause Break + Pause, + /// Some laptops place this key to the left of the key. + /// + /// This also the "back" button (triangle) on Android. + BrowserBack, + BrowserFavorites, + /// Some laptops place this key to the right of the key. + BrowserForward, + /// The "home" button on Android. + BrowserHome, + BrowserRefresh, + BrowserSearch, + BrowserStop, + /// Eject or . This key is placed in the function section on some Apple + /// keyboards. + Eject, + /// Sometimes labelled My Computer on the keyboard + LaunchApp1, + /// Sometimes labelled Calculator on the keyboard + LaunchApp2, + LaunchMail, + MediaPlayPause, + MediaSelect, + MediaStop, + MediaTrackNext, + MediaTrackPrevious, + /// This key is placed in the function section on some Apple keyboards, replacing the + /// Eject 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 ひらがな key found on some Japanese word processing keyboards. + Hiragana, + /// Use for dedicated カタカナ 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 for Physical { + #[inline] + fn eq(&self, rhs: &Code) -> bool { + match self { + Physical::Code(code) => code == rhs, + Physical::Unidentified(_) => false, + } + } +} + +impl PartialEq for Code { + #[inline] + fn eq(&self, rhs: &Physical) -> bool { + rhs == self + } +} + +impl PartialEq for Physical { + #[inline] + fn eq(&self, rhs: &NativeCode) -> bool { + match self { + Physical::Unidentified(code) => code == rhs, + Physical::Code(_) => false, + } + } +} + +impl PartialEq for NativeCode { + #[inline] + fn eq(&self, rhs: &Physical) -> bool { + rhs == self + } +} diff --git a/core/src/keyboard/modifiers.rs b/core/src/keyboard/modifiers.rs index e531510f..00b31882 100644 --- a/core/src/keyboard/modifiers.rs +++ b/core/src/keyboard/modifiers.rs @@ -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 + } + } } diff --git a/core/src/layout/flex.rs b/core/src/layout/flex.rs index dcb4d8de..2cff5bfd 100644 --- a/core/src/layout/flex.rs +++ b/core/src/layout/flex.rs @@ -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 = 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; diff --git a/core/src/layout/node.rs b/core/src/layout/node.rs index 5743a9bd..0c0f90fb 100644 --- a/core/src/layout/node.rs +++ b/core/src/layout/node.rs @@ -103,12 +103,13 @@ impl Node { } /// Translates the [`Node`] by the given translation. - pub fn translate(self, translation: impl Into) -> Self { - let translation = translation.into(); + pub fn translate(mut self, translation: impl Into) -> Self { + self.translate_mut(translation); + self + } - Self { - bounds: self.bounds + translation, - ..self - } + /// Translates the [`Node`] by the given translation. + pub fn translate_mut(&mut self, translation: impl Into) { + self.bounds = self.bounds + translation.into(); } } diff --git a/core/src/length.rs b/core/src/length.rs index 5f24169f..363833c4 100644 --- a/core/src/length.rs +++ b/core/src/length.rs @@ -77,8 +77,8 @@ impl From for Length { } } -impl From for Length { - fn from(units: u16) -> Self { - Length::Fixed(f32::from(units)) +impl From for Length { + fn from(units: u32) -> Self { + Length::Fixed(units as f32) } } diff --git a/core/src/lib.rs b/core/src/lib.rs index 32156441..e75ef2a7 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -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(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 { + /// 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 Function 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) + } +} diff --git a/core/src/mouse/click.rs b/core/src/mouse/click.rs index 6f3844be..12039d79 100644 --- a/core/src/mouse/click.rs +++ b/core/src/mouse/click.rs @@ -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 { + pub fn new( + position: Point, + button: Button, + previous: Option, + ) -> 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 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, + } + } +} diff --git a/core/src/mouse/cursor.rs b/core/src/mouse/cursor.rs index 203526e9..9388a578 100644 --- a/core/src/mouse/cursor.rs +++ b/core/src/mouse/cursor.rs @@ -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 { 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 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, + } + } } diff --git a/core/src/mouse/interaction.rs b/core/src/mouse/interaction.rs index 065eb8e7..aad6a3ea 100644 --- a/core/src/mouse/interaction.rs +++ b/core/src/mouse/interaction.rs @@ -13,6 +13,13 @@ pub enum Interaction { Grabbing, ResizingHorizontally, ResizingVertically, + ResizingDiagonallyUp, + ResizingDiagonallyDown, NotAllowed, ZoomIn, + ZoomOut, + Cell, + Move, + Copy, + Help, } diff --git a/core/src/overlay.rs b/core/src/overlay.rs index 3a57fe16..94239152 100644 --- a/core/src/overlay.rs +++ b/core/src/overlay.rs @@ -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 @@ -41,7 +40,7 @@ where &mut self, _layout: Layout<'_>, _renderer: &Renderer, - _operation: &mut dyn widget::Operation, + _operation: &mut dyn widget::Operation, ) { } @@ -52,21 +51,20 @@ where /// * the computed [`Layout`] of the [`Overlay`] /// * the current cursor position /// * a mutable `Message` list, allowing the [`Overlay`] to produce - /// new messages based on user interaction. + /// new messages based on user interaction. /// * the `Renderer` /// * 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`]. diff --git a/core/src/overlay/element.rs b/core/src/overlay/element.rs index 695b88b3..de6e73fd 100644 --- a/core/src/overlay/element.rs +++ b/core/src/overlay/element.rs @@ -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, + 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 - for Map<'a, A, B, Theme, Renderer> +impl Overlay + 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, + operation: &mut dyn widget::Operation, ) { - struct MapOperation<'a, B> { - operation: &'a mut dyn widget::Operation, - } - - impl<'a, T, B> widget::Operation for MapOperation<'a, B> { - fn container( - &mut self, - id: Option<&widget::Id>, - bounds: Rectangle, - operate_on_children: &mut dyn FnMut( - &mut dyn widget::Operation, - ), - ) { - 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 }); + self.content.operate(layout, renderer, operation); } - 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<'_, 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> { + ) -> Option> { self.content .overlay(layout, renderer) .map(|overlay| overlay.map(self.mapper)) diff --git a/core/src/overlay/group.rs b/core/src/overlay/group.rs index 7e4bebd0..970c1b0e 100644 --- a/core/src/overlay/group.rs +++ b/core/src/overlay/group.rs @@ -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 - for Group<'a, Message, Theme, Renderer> +impl Overlay + 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, + 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> { + ) -> Option> { let children = self .children .iter_mut() diff --git a/core/src/padding.rs b/core/src/padding.rs index a63f6e29..9ec02e6d 100644 --- a/core/src/padding.rs +++ b/core/src/padding.rs @@ -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`, @@ -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) -> Padding { + Padding::new(padding.into().0) +} + +/// Create some top [`Padding`]. +pub fn top(padding: impl Into) -> Padding { + Padding::default().top(padding) +} + +/// Create some bottom [`Padding`]. +pub fn bottom(padding: impl Into) -> Padding { + Padding::default().bottom(padding) +} + +/// Create some left [`Padding`]. +pub fn left(padding: impl Into) -> Padding { + Padding::default().left(padding) +} + +/// Create some right [`Padding`]. +pub fn right(padding: impl Into) -> 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) -> Self { + Self { + top: top.into().0, + ..self + } + } + + /// Sets the [`bottom`] of the [`Padding`]. + /// + /// [`bottom`]: Self::bottom + pub fn bottom(self, bottom: impl Into) -> Self { + Self { + bottom: bottom.into().0, + ..self + } + } + + /// Sets the [`left`] of the [`Padding`]. + /// + /// [`left`]: Self::left + pub fn left(self, left: impl Into) -> Self { + Self { + left: left.into().0, + ..self + } + } + + /// Sets the [`right`] of the [`Padding`]. + /// + /// [`right`]: Self::right + pub fn right(self, right: impl Into) -> 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 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 for Size { fn from(padding: Padding) -> Self { Self::new(padding.horizontal(), padding.vertical()) } } + +impl From for Padding { + fn from(pixels: Pixels) -> Self { + Self::from(pixels.0) + } +} diff --git a/core/src/pixels.rs b/core/src/pixels.rs index 425c0028..c87e2b31 100644 --- a/core/src/pixels.rs +++ b/core/src/pixels.rs @@ -6,18 +6,23 @@ /// (e.g. `impl Into`) 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 for Pixels { fn from(amount: f32) -> Self { Self(amount) } } -impl From for Pixels { - fn from(amount: u16) -> Self { - Self(f32::from(amount)) +impl From for Pixels { + fn from(amount: u32) -> Self { + Self(amount as f32) } } @@ -27,6 +32,30 @@ impl From 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 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 for Pixels { type Output = Pixels; @@ -34,3 +63,27 @@ impl std::ops::Mul 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 for Pixels { + type Output = Pixels; + + fn div(self, rhs: f32) -> Self { + Pixels(self.0 / rhs) + } +} + +impl std::ops::Div for Pixels { + type Output = Pixels; + + fn div(self, rhs: u32) -> Self { + Pixels(self.0 / rhs as f32) + } +} diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 1556e072..14d2a2e8 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -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 { } } + /// 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 { && 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 { } /// Expands the [`Rectangle`] a given amount. - pub fn expand(self, amount: f32) -> Self { + pub fn expand(self, padding: impl Into) -> 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) -> 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(), } } diff --git a/core/src/renderer.rs b/core/src/renderer.rs index a2785ae8..68e070e8 100644 --- a/core/src/renderer.rs +++ b/core/src/renderer.rs @@ -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, + scale_factor: f32, + background_color: Color, + ) -> Vec; +} diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index e8709dbc..5732c41b 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -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( + _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 { None } + + fn hit_span(&self, _point: Point) -> Option { + None + } + + fn span_bounds(&self, _index: usize) -> Vec { + 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> { 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 { 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, - _bounds: Rectangle, - _rotation: Radians, - _opacity: f32, - ) { - } + fn draw_svg(&mut self, _svg: svg::Svg, _bounds: Rectangle) {} } diff --git a/core/src/settings.rs b/core/src/settings.rs new file mode 100644 index 00000000..3189c8d1 --- /dev/null +++ b/core/src/settings.rs @@ -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, + + /// The fonts to load on boot. + pub fonts: Vec>, + + /// 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, + } + } +} diff --git a/core/src/shell.rs b/core/src/shell.rs index 2952ceff..e3fcdf89 100644 --- a/core/src/shell.rs +++ b/core/src/shell.rs @@ -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, - redraw_request: Option, + 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) -> 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); - } - Some(current) if request < current => { - 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; + } + + /// 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, + ) { + 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 { + 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>( + &mut self, + ime: &InputMethod, + ) { + 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(&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); } } diff --git a/core/src/size.rs b/core/src/size.rs index d7459355..95089236 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -99,6 +99,20 @@ impl From> for Vector { } } +impl std::ops::Add for Size +where + T: std::ops::Add, +{ + type Output = Size; + + fn add(self, rhs: Self) -> Self::Output { + Size { + width: self.width + rhs.width, + height: self.height + rhs.height, + } + } +} + impl std::ops::Sub for Size where T: std::ops::Sub, diff --git a/core/src/svg.rs b/core/src/svg.rs index 946b8156..ac19b223 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -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 { + /// 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, + + /// 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 { + /// Creates a new [`Svg`] with the given handle. + pub fn new(handle: impl Into) -> 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) -> Self { + self.color = Some(color.into()); + self + } + + /// Sets the rotation of the [`Svg`]. + pub fn rotation(mut self, rotation: impl Into) -> Self { + self.rotation = rotation.into(); + self + } + + /// Sets the opacity of the [`Svg`]. + pub fn opacity(mut self, opacity: impl Into) -> 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; /// 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, - bounds: Rectangle, - rotation: Radians, - opacity: f32, - ); + fn draw_svg(&mut self, svg: Svg, bounds: Rectangle); } diff --git a/core/src/text.rs b/core/src/text.rs index b30feae0..a7e1f281 100644 --- a/core/src/text.rs +++ b/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 { /// 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, + /// The [`LineHeight`] of the [`Span`]. + pub line_height: Option, + /// The font of the [`Span`]. + pub font: Option, + /// The [`Color`] of the [`Span`]. + pub color: Option, + /// The link of the [`Span`]. + pub link: Option, + /// The [`Highlight`] of the [`Span`]. + pub highlight: Option, + /// 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) -> Self { + self.size = Some(size.into()); + self + } + + /// Sets the [`LineHeight`] of the [`Span`]. + pub fn line_height(mut self, line_height: impl Into) -> Self { + self.line_height = Some(line_height.into()); + self + } + + /// Sets the font of the [`Span`]. + pub fn font(mut self, font: impl Into) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the font of the [`Span`], if any. + pub fn font_maybe(mut self, font: Option>) -> Self { + self.font = font.map(Into::into); + self + } + + /// Sets the [`Color`] of the [`Span`]. + pub fn color(mut self, color: impl Into) -> Self { + self.color = Some(color.into()); + self + } + + /// Sets the [`Color`] of the [`Span`], if any. + pub fn color_maybe(mut self, color: Option>) -> Self { + self.color = color.map(Into::into); + self + } + + /// Sets the link of the [`Span`]. + pub fn link(mut self, link: impl Into) -> Self { + self.link = Some(link.into()); + self + } + + /// Sets the link of the [`Span`], if any. + pub fn link_maybe(mut self, link: Option>) -> Self { + self.link = link.map(Into::into); + self + } + + /// Sets the [`Background`] of the [`Span`]. + pub fn background(self, background: impl Into) -> Self { + self.background_maybe(Some(background)) + } + + /// Sets the [`Background`] of the [`Span`], if any. + pub fn background_maybe( + mut self, + background: Option>, + ) -> 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) -> Self { + self.border_maybe(Some(border)) + } + + /// Sets the [`Border`] of the [`Span`], if any. + pub fn border_maybe(mut self, border: Option>) -> 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) -> 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 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 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); diff --git a/core/src/text/editor.rs b/core/src/text/editor.rs index fbf60696..6921c61c 100644 --- a/core/src/text/editor.rs +++ b/core/src/text/editor.rs @@ -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; /// 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>; /// 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), } + +/// 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 => "", + } + } +} diff --git a/core/src/text/paragraph.rs b/core/src/text/paragraph.rs index 8ff04015..700c2c75 100644 --- a/core/src/text/paragraph.rs +++ b/core/src/text/paragraph.rs @@ -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( + 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; + /// 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; + + /// 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; + /// Returns the distance to the given grapheme index in the [`Paragraph`]. fn grapheme_position(&self, line: usize, index: usize) -> Option; - /// 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 { + raw: P, + content: String, +} + +impl Plain

{ + /// 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 + } +} diff --git a/core/src/theme.rs b/core/src/theme.rs index 6b2c04da..cc5b77df 100644 --- a/core/src/theme.rs +++ b/core/src/theme.rs @@ -3,6 +3,8 @@ pub mod palette; pub use palette::Palette; +use crate::Color; + use std::fmt; use std::sync::Arc; @@ -164,15 +166,18 @@ impl Default for Theme { fn default() -> Self { #[cfg(feature = "auto-detect-theme")] { - use once_cell::sync::Lazy; + use std::sync::LazyLock; - static DEFAULT: Lazy = - Lazy::new(|| match dark_light::detect() { + static DEFAULT: LazyLock = 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, + } +} diff --git a/core/src/theme/palette.rs b/core/src/theme/palette.rs index ec54fb9c..08fba257 100644 --- a/core/src/theme/palette.rs +++ b/core/src/theme/palette.rs @@ -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 = - Lazy::new(|| Extended::generate(Palette::LIGHT)); +pub static EXTENDED_LIGHT: LazyLock = + LazyLock::new(|| Extended::generate(Palette::LIGHT)); /// The built-in dark variant of an [`Extended`] palette. -pub static EXTENDED_DARK: Lazy = - Lazy::new(|| Extended::generate(Palette::DARK)); +pub static EXTENDED_DARK: LazyLock = + LazyLock::new(|| Extended::generate(Palette::DARK)); /// The built-in Dracula variant of an [`Extended`] palette. -pub static EXTENDED_DRACULA: Lazy = - Lazy::new(|| Extended::generate(Palette::DRACULA)); +pub static EXTENDED_DRACULA: LazyLock = + LazyLock::new(|| Extended::generate(Palette::DRACULA)); /// The built-in Nord variant of an [`Extended`] palette. -pub static EXTENDED_NORD: Lazy = - Lazy::new(|| Extended::generate(Palette::NORD)); +pub static EXTENDED_NORD: LazyLock = + LazyLock::new(|| Extended::generate(Palette::NORD)); /// The built-in Solarized Light variant of an [`Extended`] palette. -pub static EXTENDED_SOLARIZED_LIGHT: Lazy = - Lazy::new(|| Extended::generate(Palette::SOLARIZED_LIGHT)); +pub static EXTENDED_SOLARIZED_LIGHT: LazyLock = + LazyLock::new(|| Extended::generate(Palette::SOLARIZED_LIGHT)); /// The built-in Solarized Dark variant of an [`Extended`] palette. -pub static EXTENDED_SOLARIZED_DARK: Lazy = - Lazy::new(|| Extended::generate(Palette::SOLARIZED_DARK)); +pub static EXTENDED_SOLARIZED_DARK: LazyLock = + LazyLock::new(|| Extended::generate(Palette::SOLARIZED_DARK)); /// The built-in Gruvbox Light variant of an [`Extended`] palette. -pub static EXTENDED_GRUVBOX_LIGHT: Lazy = - Lazy::new(|| Extended::generate(Palette::GRUVBOX_LIGHT)); +pub static EXTENDED_GRUVBOX_LIGHT: LazyLock = + LazyLock::new(|| Extended::generate(Palette::GRUVBOX_LIGHT)); /// The built-in Gruvbox Dark variant of an [`Extended`] palette. -pub static EXTENDED_GRUVBOX_DARK: Lazy = - Lazy::new(|| Extended::generate(Palette::GRUVBOX_DARK)); +pub static EXTENDED_GRUVBOX_DARK: LazyLock = + LazyLock::new(|| Extended::generate(Palette::GRUVBOX_DARK)); /// The built-in Catppuccin Latte variant of an [`Extended`] palette. -pub static EXTENDED_CATPPUCCIN_LATTE: Lazy = - Lazy::new(|| Extended::generate(Palette::CATPPUCCIN_LATTE)); +pub static EXTENDED_CATPPUCCIN_LATTE: LazyLock = + LazyLock::new(|| Extended::generate(Palette::CATPPUCCIN_LATTE)); /// The built-in Catppuccin Frappé variant of an [`Extended`] palette. -pub static EXTENDED_CATPPUCCIN_FRAPPE: Lazy = - Lazy::new(|| Extended::generate(Palette::CATPPUCCIN_FRAPPE)); +pub static EXTENDED_CATPPUCCIN_FRAPPE: LazyLock = + LazyLock::new(|| Extended::generate(Palette::CATPPUCCIN_FRAPPE)); /// The built-in Catppuccin Macchiato variant of an [`Extended`] palette. -pub static EXTENDED_CATPPUCCIN_MACCHIATO: Lazy = - Lazy::new(|| Extended::generate(Palette::CATPPUCCIN_MACCHIATO)); +pub static EXTENDED_CATPPUCCIN_MACCHIATO: LazyLock = + LazyLock::new(|| Extended::generate(Palette::CATPPUCCIN_MACCHIATO)); /// The built-in Catppuccin Mocha variant of an [`Extended`] palette. -pub static EXTENDED_CATPPUCCIN_MOCHA: Lazy = - Lazy::new(|| Extended::generate(Palette::CATPPUCCIN_MOCHA)); +pub static EXTENDED_CATPPUCCIN_MOCHA: LazyLock = + LazyLock::new(|| Extended::generate(Palette::CATPPUCCIN_MOCHA)); /// The built-in Tokyo Night variant of an [`Extended`] palette. -pub static EXTENDED_TOKYO_NIGHT: Lazy = - Lazy::new(|| Extended::generate(Palette::TOKYO_NIGHT)); +pub static EXTENDED_TOKYO_NIGHT: LazyLock = + 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 = - Lazy::new(|| Extended::generate(Palette::TOKYO_NIGHT_STORM)); +pub static EXTENDED_TOKYO_NIGHT_STORM: LazyLock = + 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 = - Lazy::new(|| Extended::generate(Palette::TOKYO_NIGHT_LIGHT)); +pub static EXTENDED_TOKYO_NIGHT_LIGHT: LazyLock = + LazyLock::new(|| Extended::generate(Palette::TOKYO_NIGHT_LIGHT)); /// The built-in Kanagawa Wave variant of an [`Extended`] palette. -pub static EXTENDED_KANAGAWA_WAVE: Lazy = - Lazy::new(|| Extended::generate(Palette::KANAGAWA_WAVE)); +pub static EXTENDED_KANAGAWA_WAVE: LazyLock = + LazyLock::new(|| Extended::generate(Palette::KANAGAWA_WAVE)); /// The built-in Kanagawa Dragon variant of an [`Extended`] palette. -pub static EXTENDED_KANAGAWA_DRAGON: Lazy = - Lazy::new(|| Extended::generate(Palette::KANAGAWA_DRAGON)); +pub static EXTENDED_KANAGAWA_DRAGON: LazyLock = + LazyLock::new(|| Extended::generate(Palette::KANAGAWA_DRAGON)); /// The built-in Kanagawa Lotus variant of an [`Extended`] palette. -pub static EXTENDED_KANAGAWA_LOTUS: Lazy = - Lazy::new(|| Extended::generate(Palette::KANAGAWA_LOTUS)); +pub static EXTENDED_KANAGAWA_LOTUS: LazyLock = + LazyLock::new(|| Extended::generate(Palette::KANAGAWA_LOTUS)); /// The built-in Moonfly variant of an [`Extended`] palette. -pub static EXTENDED_MOONFLY: Lazy = - Lazy::new(|| Extended::generate(Palette::MOONFLY)); +pub static EXTENDED_MOONFLY: LazyLock = + LazyLock::new(|| Extended::generate(Palette::MOONFLY)); /// The built-in Nightfly variant of an [`Extended`] palette. -pub static EXTENDED_NIGHTFLY: Lazy = - Lazy::new(|| Extended::generate(Palette::NIGHTFLY)); +pub static EXTENDED_NIGHTFLY: LazyLock = + LazyLock::new(|| Extended::generate(Palette::NIGHTFLY)); /// The built-in Oxocarbon variant of an [`Extended`] palette. -pub static EXTENDED_OXOCARBON: Lazy = - Lazy::new(|| Extended::generate(Palette::OXOCARBON)); +pub static EXTENDED_OXOCARBON: LazyLock = + LazyLock::new(|| Extended::generate(Palette::OXOCARBON)); /// The built-in Ferra variant of an [`Extended`] palette. -pub static EXTENDED_FERRA: Lazy = - Lazy::new(|| Extended::generate(Palette::FERRA)); +pub static EXTENDED_FERRA: LazyLock = + 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 { - let white_contrast = relative_contrast(background, Color::WHITE); - let black_contrast = relative_contrast(background, Color::BLACK); + return text; + } - if white_contrast >= black_contrast { - Color::WHITE - } else { - Color::BLACK - } + 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 { + mix(Color::WHITE, background, 0.05) + } else { + mix(Color::BLACK, background, 0.05) } } diff --git a/core/src/time.rs b/core/src/time.rs index a57075b7..664045fa 100644 --- a/core/src/time.rs +++ b/core/src/time.rs @@ -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) +} diff --git a/core/src/vector.rs b/core/src/vector.rs index 049e648f..ff848c4f 100644 --- a/core/src/vector.rs +++ b/core/src/vector.rs @@ -18,9 +18,17 @@ impl Vector { 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 std::ops::Neg for Vector +where + T: std::ops::Neg, +{ + type Output = Self; + + fn neg(self) -> Self::Output { + Self::new(-self.x, -self.y) + } } impl std::ops::Add for Vector diff --git a/core/src/widget.rs b/core/src/widget.rs index b02e3a4f..3c9c50ab 100644 --- a/core/src/widget.rs +++ b/core/src/widget.rs @@ -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. /// @@ -27,18 +26,18 @@ use crate::{Clipboard, Length, Rectangle, Shell, Size, Vector}; /// widget: /// /// - [`bezier_tool`], a Paint-like tool for drawing Bézier curves using -/// [`lyon`]. +/// [`lyon`]. /// - [`custom_widget`], a demonstration of how to build a custom widget that -/// draws a circle. +/// draws a circle. /// - [`geometry`], a custom widget showcasing how to draw geometry with the -/// `Mesh2D` primitive in [`iced_wgpu`]. +/// `Mesh2D` primitive in [`iced_wgpu`]. /// -/// [examples]: https://github.com/iced-rs/iced/tree/0.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 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, + _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`]. diff --git a/core/src/widget/operation.rs b/core/src/widget/operation.rs index b91cf9ac..8fc627bf 100644 --- a/core/src/widget/operation.rs +++ b/core/src/widget/operation.rs @@ -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 { +pub trait Operation: 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 { ); /// 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 { @@ -53,6 +76,72 @@ pub trait Operation { } } +impl Operation for Box +where + T: Operation + ?Sized, +{ + fn container( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + 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 { + self.as_ref().finish() + } +} + /// The result of an [`Operation`]. pub enum Outcome { /// 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, +) -> impl Operation + 'a +where + T: 'a, +{ + struct BlackBox<'a, T> { + operation: &'a mut dyn Operation, + } + + impl Operation for BlackBox<'_, T> { + fn container( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + 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 { + Outcome::None + } + } + + BlackBox { operation } +} + /// Maps the output of an [`Operation`] using the given function. pub fn map( - operation: Box>, - f: impl Fn(A) -> B + 'static, + operation: impl Operation, + f: impl Fn(A) -> B + Send + Sync + 'static, ) -> impl Operation where A: 'static, B: 'static, { #[allow(missing_debug_implementations)] - struct Map { - operation: Box>, - f: Rc B>, + struct Map { + operation: O, + f: Arc B + Send + Sync>, } - impl Operation for Map + impl Operation for Map where + O: Operation, A: 'static, B: 'static, { @@ -108,7 +277,7 @@ where operation: &'a mut dyn Operation, } - impl<'a, A, B> Operation for MapRef<'a, A> { + impl Operation 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 { @@ -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( + operation: impl Operation + 'static, + f: fn(A) -> O, +) -> impl Operation +where + A: 'static, + B: Send + 'static, + O: Operation + 'static, +{ + struct Chain + where + T: Operation, + O: Operation, + { + operation: T, + next: fn(A) -> O, + _result: PhantomData, + } + + impl Operation for Chain + where + T: Operation + 'static, + O: Operation + 'static, + A: 'static, + B: Send + 'static, + { + fn container( + &mut self, + id: Option<&Id>, + bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + 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 { + 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, } } diff --git a/core/src/widget/operation/focusable.rs b/core/src/widget/operation/focusable.rs index 68c22faa..44c9d647 100644 --- a/core/src/widget/operation/focusable.rs +++ b/core/src/widget/operation/focusable.rs @@ -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(target: Id) -> impl Operation { } impl Operation 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(target: Id) -> impl Operation { Focus { target } } -/// Produces an [`Operation`] that generates a [`Count`] and chains it with the -/// provided function to build a new [`Operation`]. -pub fn count(f: fn(Count) -> O) -> impl Operation -where - O: Operation + 'static, -{ - struct CountFocusable { - count: Count, - next: fn(Count) -> O, +/// Produces an [`Operation`] that unfocuses the focused widget. +pub fn unfocus() -> impl Operation { + struct Unfocus; + + impl Operation for Unfocus { + fn focusable( + &mut self, + _id: Option<&Id>, + _bounds: Rectangle, + state: &mut dyn Focusable, + ) { + state.unfocus(); + } + + fn container( + &mut self, + _id: Option<&Id>, + _bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self); + } } - impl Operation for CountFocusable - where - O: Operation + 'static, - { - fn focusable(&mut self, state: &mut dyn Focusable, _id: Option<&Id>) { + 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 { + struct CountFocusable { + count: Count, + } + + impl Operation 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), + operate_on_children: &mut dyn FnMut(&mut dyn Operation), ) { operate_on_children(self); } - fn finish(&self) -> Outcome { - Outcome::Chain(Box::new((self.next)(self.count))) + fn finish(&self) -> Outcome { + 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() -> impl Operation { +pub fn focus_previous() -> impl Operation +where + T: Send + 'static, +{ struct FocusPrevious { count: Count, current: usize, } impl Operation 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() -> impl Operation { } } - 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() -> impl Operation { +pub fn focus_next() -> impl Operation +where + T: Send + 'static, +{ struct FocusNext { count: Count, current: usize, } impl Operation 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() -> impl Operation { } } - 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 { } impl Operation 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 { 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 { + struct IsFocused { + target: Id, + is_focused: Option, + } + + impl Operation 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), + ) { + if self.is_focused.is_some() { + return; + } + + operate_on_children(self); + } + + fn finish(&self) -> Outcome { + self.is_focused.map_or(Outcome::None, Outcome::Some) + } + } + + IsFocused { + target, + is_focused: None, + } +} diff --git a/core/src/widget/operation/scrollable.rs b/core/src/widget/operation/scrollable.rs index 12161255..7c78c087 100644 --- a/core/src/widget/operation/scrollable.rs +++ b/core/src/widget/operation/scrollable.rs @@ -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(target: Id, offset: RelativeOffset) -> impl Operation { 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(target: Id, offset: AbsoluteOffset) -> impl Operation { 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(target: Id, offset: AbsoluteOffset) -> impl Operation { ScrollTo { target, offset } } +/// Produces an [`Operation`] that scrolls the widget with the given [`Id`] by +/// the provided [`AbsoluteOffset`]. +pub fn scroll_by(target: Id, offset: AbsoluteOffset) -> impl Operation { + struct ScrollBy { + target: Id, + offset: AbsoluteOffset, + } + + impl Operation for ScrollBy { + fn container( + &mut self, + _id: Option<&Id>, + _bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + 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 { diff --git a/core/src/widget/operation/text_input.rs b/core/src/widget/operation/text_input.rs index 41731d4c..efb2a4d3 100644 --- a/core/src/widget/operation/text_input.rs +++ b/core/src/widget/operation/text_input.rs @@ -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(target: Id) -> impl Operation { } impl Operation 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(target: Id) -> impl Operation { } impl Operation 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(target: Id, position: usize) -> impl Operation { } impl Operation 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(target: Id) -> impl Operation { } impl Operation 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(); diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index f1f0b345..9a00fcdb 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -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) -> 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) -> 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, line_height: LineHeight, width: Length, @@ -30,6 +71,7 @@ where vertical_alignment: alignment::Vertical, font: Option, 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, ) -> 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, ) -> 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); +pub struct State(pub paragraph::Plain

); -impl<'a, Message, Theme, Renderer> Widget - for Text<'a, Theme, Renderer> +impl Widget + 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::( + paragraph::Plain::default(), + )) } fn size(&self) -> Size { @@ -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::>(); 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( 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: &mut Renderer, style: &renderer::Style, layout: Layout<'_>, - state: &State, + 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); diff --git a/core/src/widget/tree.rs b/core/src/widget/tree.rs index 6b1a1309..2600cfc6 100644 --- a/core/src/widget/tree.rs +++ b/core/src/widget/tree.rs @@ -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( &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 diff --git a/core/src/window.rs b/core/src/window.rs index 448ffc45..d0e741d8 100644 --- a/core/src/window.rs +++ b/core/src/window.rs @@ -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; diff --git a/core/src/window/direction.rs b/core/src/window/direction.rs new file mode 100644 index 00000000..b757961e --- /dev/null +++ b/core/src/window/direction.rs @@ -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, +} diff --git a/core/src/window/event.rs b/core/src/window/event.rs index a14d127f..45d29179 100644 --- a/core/src/window/event.rs +++ b/core/src/window/event.rs @@ -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, @@ -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, } diff --git a/core/src/window/id.rs b/core/src/window/id.rs index 80ab8e98..ee0a4c59 100644 --- a/core/src/window/id.rs +++ b/core/src/window/id.rs @@ -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) + } +} diff --git a/core/src/window/redraw_request.rs b/core/src/window/redraw_request.rs index b0c000d6..0ae4face 100644 --- a/core/src/window/redraw_request.rs +++ b/core/src/window/redraw_request.rs @@ -8,6 +8,15 @@ pub enum RedrawRequest { /// Redraw at the given time. At(Instant), + + /// No redraw is needed. + Wait, +} + +impl From 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)); } } diff --git a/runtime/src/window/screenshot.rs b/core/src/window/screenshot.rs similarity index 81% rename from runtime/src/window/screenshot.rs rename to core/src/window/screenshot.rs index fb318110..424168bb 100644 --- a/runtime/src/window/screenshot.rs +++ b/core/src/window/screenshot.rs @@ -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, + /// 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, size: Size) -> Self { + pub fn new( + bytes: impl Into, + size: Size, + 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, }) } } diff --git a/core/src/window/settings.rs b/core/src/window/settings.rs index fbbf86ab..94bcfd78 100644 --- a/core/src/window/settings.rs +++ b/core/src/window/settings.rs @@ -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, diff --git a/core/src/window/settings/linux.rs b/core/src/window/settings/linux.rs index 009b9d9e..0a1e11cd 100644 --- a/core/src/window/settings/linux.rs +++ b/core/src/window/settings/linux.rs @@ -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, } diff --git a/core/src/window/settings/windows.rs b/core/src/window/settings/windows.rs index d3bda259..a47582a6 100644 --- a/core/src/window/settings/windows.rs +++ b/core/src/window/settings/windows.rs @@ -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, - /// 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, } } } diff --git a/debug/Cargo.toml b/debug/Cargo.toml index 99ee1ea1..0cf8d7af 100644 --- a/debug/Cargo.toml +++ b/debug/Cargo.toml @@ -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 diff --git a/debug/src/lib.rs b/debug/src/lib.rs index 4aaae46b..3adf251f 100644 --- a/debug/src/lib.rs +++ b/debug/src/lib.rs @@ -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 = Lazy::new(|| { + static BEACON: LazyLock = LazyLock::new(|| { client::connect(NAME.read().expect("Read application name").to_owned()) }); diff --git a/examples/README.md b/examples/README.md index 71dad13e..232b6042 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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. diff --git a/examples/arc/Cargo.toml b/examples/arc/Cargo.toml index 5012ff82..f62b98ea 100644 --- a/examples/arc/Cargo.toml +++ b/examples/arc/Cargo.toml @@ -2,7 +2,7 @@ name = "arc" version = "0.1.0" authors = ["ThatsNoMoon "] -edition = "2021" +edition = "2024" publish = false [dependencies] diff --git a/examples/arc/src/main.rs b/examples/arc/src/main.rs index 4576404f..f63b82d0 100644 --- a/examples/arc/src/main.rs +++ b/examples/arc/src/main.rs @@ -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 { - Canvas::new(self) - .width(Length::Fill) - .height(Length::Fill) - .into() + Canvas::new(self).width(Fill).height(Fill).into() } fn subscription(&self) -> Subscription { - iced::time::every(std::time::Duration::from_millis(10)) - .map(|_| Message::Tick) + window::frames().map(|_| Message::Tick) } } diff --git a/examples/bezier_tool/Cargo.toml b/examples/bezier_tool/Cargo.toml index e5624097..91ceb467 100644 --- a/examples/bezier_tool/Cargo.toml +++ b/examples/bezier_tool/Cargo.toml @@ -2,7 +2,7 @@ name = "bezier_tool" version = "0.1.0" authors = ["Héctor Ramón Jiménez "] -edition = "2021" +edition = "2024" publish = false [dependencies] diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 29df3eeb..95ad299d 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -1,10 +1,9 @@ //! This example showcases an interactive `Canvas` for drawing Bézier curves. -use iced::alignment; -use iced::widget::{button, container, horizontal_space, hover}; -use iced::{Element, Length, Theme}; +use iced::widget::{button, container, horizontal_space, hover, right}; +use iced::{Element, Theme}; pub fn main() -> iced::Result { - iced::program("Bezier Tool - Iced", Example::update, Example::view) + iced::application("Bezier Tool - Iced", Example::update, Example::view) .theme(|_| Theme::CatppuccinMocha) .antialiasing(true) .run() @@ -42,14 +41,12 @@ impl Example { if self.curves.is_empty() { container(horizontal_space()) } else { - container( + right( button("Clear") .style(button::danger) .on_press(Message::Clear), ) .padding(10) - .width(Length::Fill) - .align_x(alignment::Horizontal::Right) }, )) .padding(20) @@ -59,9 +56,10 @@ impl Example { mod bezier { use iced::mouse; - use iced::widget::canvas::event::{self, Event}; - use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path, Stroke}; - use iced::{Element, Length, Point, Rectangle, Renderer, Theme}; + use iced::widget::canvas::{ + self, Canvas, Event, Frame, Geometry, Path, Stroke, + }; + use iced::{Element, Fill, Point, Rectangle, Renderer, Theme}; #[derive(Default)] pub struct State { @@ -74,8 +72,8 @@ mod bezier { state: self, curves, }) - .width(Length::Fill) - .height(Length::Fill) + .width(Fill) + .height(Fill) .into() } @@ -89,57 +87,56 @@ mod bezier { curves: &'a [Curve], } - impl<'a> canvas::Program for Bezier<'a> { + impl canvas::Program for Bezier<'_> { type State = Option; fn update( &self, state: &mut Self::State, - event: Event, + event: &Event, bounds: Rectangle, cursor: mouse::Cursor, - ) -> (event::Status, Option) { - let Some(cursor_position) = cursor.position_in(bounds) else { - return (event::Status::Ignored, None); - }; + ) -> Option> { + let cursor_position = cursor.position_in(bounds)?; match event { - Event::Mouse(mouse_event) => { - let message = match mouse_event { - mouse::Event::ButtonPressed(mouse::Button::Left) => { - match *state { - None => { - *state = Some(Pending::One { - from: cursor_position, - }); + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) => Some( + match *state { + None => { + *state = Some(Pending::One { + from: cursor_position, + }); - None - } - Some(Pending::One { from }) => { - *state = Some(Pending::Two { - from, - to: cursor_position, - }); - - None - } - Some(Pending::Two { from, to }) => { - *state = None; - - Some(Curve { - from, - to, - control: cursor_position, - }) - } - } + canvas::Action::request_redraw() } - _ => None, - }; + Some(Pending::One { from }) => { + *state = Some(Pending::Two { + from, + to: cursor_position, + }); - (event::Status::Captured, message) + canvas::Action::request_redraw() + } + Some(Pending::Two { from, to }) => { + *state = None; + + canvas::Action::publish(Curve { + from, + to, + control: cursor_position, + }) + } + } + .and_capture(), + ), + Event::Mouse(mouse::Event::CursorMoved { .. }) + if state.is_some() => + { + Some(canvas::Action::request_redraw()) } - _ => (event::Status::Ignored, None), + _ => None, } } diff --git a/examples/changelog/Cargo.toml b/examples/changelog/Cargo.toml new file mode 100644 index 00000000..9bc56280 --- /dev/null +++ b/examples/changelog/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "changelog" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez "] +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"] diff --git a/examples/changelog/src/changelog.rs b/examples/changelog/src/changelog.rs new file mode 100644 index 00000000..354f0bc8 --- /dev/null +++ b/examples/changelog/src/changelog.rs @@ -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, + added: Vec, + changed: Vec, + fixed: Vec, + removed: Vec, + authors: Vec, +} + +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), 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 = 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 + '_ { + 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) -> 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::>() + .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 { + 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 { + 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, 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, + pub labels: Vec, + pub author: String, +} + +impl PullRequest { + pub async fn fetch(contribution: Contribution) -> Result { + 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, + user: User, + labels: Vec(mut self, value: A) -> Subscription<(A, T)> where - Message: 'static, - T: std::hash::Hash + Clone + Send + Sync + 'static, + T: 'static, + A: std::hash::Hash + Clone + Send + Sync + 'static, { Subscription { recipes: self @@ -84,7 +251,7 @@ impl Subscription { .drain(..) .map(|recipe| { Box::new(With::new(recipe, value.clone())) - as Box> + as Box> }) .collect(), } @@ -97,8 +264,8 @@ impl Subscription { /// will panic in debug mode otherwise. pub fn map(mut self, f: F) -> Subscription where - Message: 'static, - F: Fn(Message) -> A + MaybeSend + Clone + 'static, + T: 'static, + F: Fn(T) -> A + MaybeSend + Clone + 'static, A: 'static, { debug_assert!( @@ -120,7 +287,23 @@ impl Subscription { } } -impl std::fmt::Debug for Subscription { +/// Creates a [`Subscription`] from a [`Recipe`] describing it. +pub fn from_recipe( + recipe: impl Recipe + 'static, +) -> Subscription { + Subscription { + recipes: vec![Box::new(recipe)], + } +} + +/// Returns the different recipes of the [`Subscription`]. +pub fn into_recipes( + subscription: Subscription, +) -> Vec>> { + subscription.recipes +} + +impl std::fmt::Debug for Subscription { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Subscription").finish() } @@ -136,13 +319,13 @@ impl std::fmt::Debug for Subscription { /// The repository has a couple of [examples] that use a custom [`Recipe`]: /// /// - [`download_progress`], a basic application that asynchronously downloads -/// a dummy file of 100 MB and tracks the download progress. +/// a dummy file of 100 MB and tracks the download progress. /// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how -/// to listen to time. +/// to listen to time. /// -/// [examples]: https://github.com/iced-rs/iced/tree/0.12/examples -/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.12/examples/download_progress -/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.12/examples/stopwatch +/// [examples]: https://github.com/iced-rs/iced/tree/0.13/examples +/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.13/examples/download_progress +/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.13/examples/stopwatch pub trait Recipe { /// The events that will be produced by a [`Subscription`] with this /// [`Recipe`]. @@ -234,200 +417,46 @@ where } } -/// Returns a [`Subscription`] that will call the given function to create and -/// asynchronously run the given [`Stream`]. -pub fn run(builder: fn() -> S) -> Subscription -where - S: Stream + MaybeSend + 'static, - Message: 'static, -{ - Subscription::from_recipe(Runner { - id: builder, - spawn: move |_| builder(), - }) -} - -/// Returns a [`Subscription`] that will create and asynchronously run the -/// given [`Stream`]. -/// -/// The `id` will be used to uniquely identify the [`Subscription`]. -pub fn run_with_id(id: I, stream: S) -> Subscription +pub(crate) fn filter_map(id: I, f: F) -> Subscription where I: Hash + 'static, - S: Stream + MaybeSend + 'static, - Message: 'static, + F: Fn(Event) -> Option + MaybeSend + 'static, + T: 'static + MaybeSend, { - Subscription::from_recipe(Runner { - id, - spawn: move |_| stream, - }) -} - -/// Returns a [`Subscription`] that will create and asynchronously run a -/// [`Stream`] that will call the provided closure to produce every `Message`. -/// -/// The `id` will be used to uniquely identify the [`Subscription`]. -pub fn unfold( - id: I, - initial: T, - mut f: impl FnMut(T) -> Fut + MaybeSend + Sync + 'static, -) -> Subscription -where - I: Hash + 'static, - T: MaybeSend + 'static, - Fut: Future + MaybeSend + 'static, - Message: 'static + MaybeSend, -{ - use futures::future::FutureExt; - - run_with_id( - id, - futures::stream::unfold(initial, move |state| f(state).map(Some)), - ) -} - -pub(crate) fn filter_map(id: I, f: F) -> Subscription -where - I: Hash + 'static, - F: Fn(Event, event::Status) -> Option + MaybeSend + 'static, - Message: 'static + MaybeSend, -{ - Subscription::from_recipe(Runner { - id, - spawn: |events| { + from_recipe(Runner { + data: id, + spawn: |_, events| { use futures::future; use futures::stream::StreamExt; - events.filter_map(move |(event, status)| { - future::ready(f(event, status)) - }) + events.filter_map(move |event| future::ready(f(event))) }, }) } -/// Creates a [`Subscription`] that publishes the events sent from a [`Future`] -/// to an [`mpsc::Sender`] with the given bounds. -/// -/// # Creating an asynchronous worker with bidirectional communication -/// You can leverage this helper to create a [`Subscription`] that spawns -/// an asynchronous worker in the background and establish a channel of -/// communication with an `iced` application. -/// -/// You can achieve this by creating an `mpsc` channel inside the closure -/// and returning the `Sender` as a `Message` for the `Application`: -/// -/// ``` -/// use iced_futures::subscription::{self, Subscription}; -/// use iced_futures::futures::channel::mpsc; -/// use iced_futures::futures::sink::SinkExt; -/// -/// pub enum Event { -/// Ready(mpsc::Sender), -/// WorkFinished, -/// // ... -/// } -/// -/// enum Input { -/// DoSomeWork, -/// // ... -/// } -/// -/// enum State { -/// Starting, -/// Ready(mpsc::Receiver), -/// } -/// -/// fn some_worker() -> Subscription { -/// struct SomeWorker; -/// -/// subscription::channel(std::any::TypeId::of::(), 100, |mut output| async move { -/// let mut state = State::Starting; -/// -/// loop { -/// match &mut state { -/// State::Starting => { -/// // Create channel -/// let (sender, receiver) = mpsc::channel(100); -/// -/// // Send the sender back to the application -/// output.send(Event::Ready(sender)).await; -/// -/// // We are ready to receive messages -/// state = State::Ready(receiver); -/// } -/// State::Ready(receiver) => { -/// use iced_futures::futures::StreamExt; -/// -/// // Read next input sent from `Application` -/// let input = receiver.select_next_some().await; -/// -/// match input { -/// Input::DoSomeWork => { -/// // Do some async work... -/// -/// // Finally, we can optionally produce a message to tell the -/// // `Application` the work is done -/// output.send(Event::WorkFinished).await; -/// } -/// } -/// } -/// } -/// } -/// }) -/// } -/// ``` -/// -/// Check out the [`websocket`] example, which showcases this pattern to maintain a `WebSocket` -/// connection open. -/// -/// [`websocket`]: https://github.com/iced-rs/iced/tree/0.12/examples/websocket -pub fn channel( - id: I, - size: usize, - f: impl FnOnce(mpsc::Sender) -> Fut + MaybeSend + 'static, -) -> Subscription +struct Runner where - I: Hash + 'static, - Fut: Future + MaybeSend + 'static, - Message: 'static + MaybeSend, + F: FnOnce(&I, EventStream) -> S, + S: Stream, { - use futures::stream::{self, StreamExt}; - - Subscription::from_recipe(Runner { - id, - spawn: move |_| { - let (sender, receiver) = mpsc::channel(size); - - let runner = stream::once(f(sender)).map(|_| unreachable!()); - - stream::select(receiver, runner) - }, - }) -} - -struct Runner -where - F: FnOnce(EventStream) -> S, - S: Stream, -{ - id: I, + data: I, spawn: F, } -impl Recipe for Runner +impl Recipe for Runner where I: Hash + 'static, - F: FnOnce(EventStream) -> S, - S: Stream + MaybeSend + 'static, + F: FnOnce(&I, EventStream) -> S, + S: Stream + MaybeSend + 'static, { - type Output = Message; + type Output = T; fn hash(&self, state: &mut Hasher) { std::any::TypeId::of::().hash(state); - self.id.hash(state); + self.data.hash(state); } fn stream(self: Box, input: EventStream) -> BoxStream { - crate::boxed_stream((self.spawn)(input)) + crate::boxed_stream((self.spawn)(&self.data, input)) } } diff --git a/futures/src/subscription/tracker.rs b/futures/src/subscription/tracker.rs index 277a446b..6daead24 100644 --- a/futures/src/subscription/tracker.rs +++ b/futures/src/subscription/tracker.rs @@ -1,5 +1,4 @@ -use crate::core::event::{self, Event}; -use crate::subscription::{Hasher, Recipe}; +use crate::subscription::{Event, Hasher, Recipe}; use crate::{BoxFuture, MaybeSend}; use futures::channel::mpsc; @@ -23,7 +22,7 @@ pub struct Tracker { #[derive(Debug)] pub struct Execution { _cancel: futures::channel::oneshot::Sender<()>, - listener: Option>, + listener: Option>, } impl Tracker { @@ -43,10 +42,10 @@ impl Tracker { /// method: /// /// - If the provided [`Subscription`] contains a new [`Recipe`] that is - /// currently not being run, it will spawn a new stream and keep it alive. + /// currently not being run, it will spawn a new stream and keep it alive. /// - On the other hand, if a [`Recipe`] is currently in execution and the - /// provided [`Subscription`] does not contain it anymore, then the - /// [`Tracker`] will close and drop the relevant stream. + /// provided [`Subscription`] does not contain it anymore, then the + /// [`Tracker`] will close and drop the relevant stream. /// /// It returns a list of futures that need to be spawned to materialize /// the [`Tracker`] changes. @@ -139,12 +138,12 @@ impl Tracker { /// currently open. /// /// [`Recipe::stream`]: crate::subscription::Recipe::stream - pub fn broadcast(&mut self, event: Event, status: event::Status) { + pub fn broadcast(&mut self, event: Event) { self.subscriptions .values_mut() .filter_map(|connection| connection.listener.as_mut()) .for_each(|listener| { - if let Err(error) = listener.try_send((event.clone(), status)) { + if let Err(error) = listener.try_send(event.clone()) { log::warn!( "Error sending event to subscription: {error:?}" ); diff --git a/graphics/Cargo.toml b/graphics/Cargo.toml index e8d27d07..43191a59 100644 --- a/graphics/Cargo.toml +++ b/graphics/Cargo.toml @@ -20,6 +20,7 @@ all-features = true [features] geometry = ["lyon_path"] image = ["dep:image", "kamadak-exif"] +svg = [] web-colors = [] fira-sans = [] @@ -32,7 +33,6 @@ bytemuck.workspace = true cosmic-text.workspace = true half.workspace = true log.workspace = true -once_cell.workspace = true raw-window-handle.workspace = true rustc-hash.workspace = true thiserror.workspace = true diff --git a/graphics/src/cache.rs b/graphics/src/cache.rs index bbba79eb..7db80a01 100644 --- a/graphics/src/cache.rs +++ b/graphics/src/cache.rs @@ -1,6 +1,7 @@ //! Cache computations and efficiently reuse them. use std::cell::RefCell; use std::fmt; +use std::mem; use std::sync::atomic::{self, AtomicU64}; /// A simple cache that stores generated values to avoid recomputation. @@ -58,18 +59,18 @@ impl Cache { } /// Clears the [`Cache`]. - pub fn clear(&self) - where - T: Clone, - { - use std::ops::Deref; + pub fn clear(&self) { + let mut state = self.state.borrow_mut(); - let previous = match self.state.borrow().deref() { - State::Empty { previous } => previous.clone(), - State::Filled { current } => Some(current.clone()), + let previous = + mem::replace(&mut *state, State::Empty { previous: None }); + + let previous = match previous { + State::Empty { previous } => previous, + State::Filled { current } => Some(current), }; - *self.state.borrow_mut() = State::Empty { previous }; + *state = State::Empty { previous }; } } diff --git a/graphics/src/compositor.rs b/graphics/src/compositor.rs index e2bfcbed..b3e50061 100644 --- a/graphics/src/compositor.rs +++ b/graphics/src/compositor.rs @@ -8,7 +8,6 @@ use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use thiserror::Error; use std::borrow::Cow; -use std::future::Future; /// A graphics compositor that can draw to windows. pub trait Compositor: Sized { @@ -89,7 +88,6 @@ pub trait Compositor: Sized { fn screenshot( &mut self, renderer: &mut Self::Renderer, - surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, ) -> Vec; @@ -119,9 +117,7 @@ pub trait Default { #[derive(Clone, PartialEq, Eq, Debug, Error)] pub enum SurfaceError { /// A timeout was encountered while trying to acquire the next frame. - #[error( - "A timeout was encountered while trying to acquire the next frame" - )] + #[error("A timeout was encountered while trying to acquire the next frame")] Timeout, /// The underlying surface has changed, and therefore the surface must be updated. #[error( @@ -153,7 +149,7 @@ impl Compositor for () { async fn with_backend( _settings: Settings, _compatible_window: W, - _preffered_backend: Option<&str>, + _preferred_backend: Option<&str>, ) -> Result { Ok(()) } @@ -198,7 +194,6 @@ impl Compositor for () { fn screenshot( &mut self, _renderer: &mut Self::Renderer, - _surface: &mut Self::Surface, _viewport: &Viewport, _background_color: Color, ) -> Vec { diff --git a/graphics/src/damage.rs b/graphics/src/damage.rs index 17d60451..8aa42798 100644 --- a/graphics/src/damage.rs +++ b/graphics/src/damage.rs @@ -45,15 +45,12 @@ pub fn list( /// Groups the given damage regions that are close together inside the given /// bounds. pub fn group(mut damage: Vec, bounds: Rectangle) -> Vec { - use std::cmp::Ordering; - const AREA_THRESHOLD: f32 = 20_000.0; damage.sort_by(|a, b| { a.center() .distance(Point::ORIGIN) - .partial_cmp(&b.center().distance(Point::ORIGIN)) - .unwrap_or(Ordering::Equal) + .total_cmp(&b.center().distance(Point::ORIGIN)) }); let mut output = Vec::new(); diff --git a/graphics/src/geometry.rs b/graphics/src/geometry.rs index ab4a7a36..2b4b45a6 100644 --- a/graphics/src/geometry.rs +++ b/graphics/src/geometry.rs @@ -16,6 +16,7 @@ pub use stroke::{LineCap, LineDash, LineJoin, Stroke}; pub use style::Style; pub use text::Text; +pub use crate::core::{Image, Svg}; pub use crate::gradient::{self, Gradient}; use crate::cache::Cached; diff --git a/graphics/src/geometry/fill.rs b/graphics/src/geometry/fill.rs index 670fbc12..b79a2582 100644 --- a/graphics/src/geometry/fill.rs +++ b/graphics/src/geometry/fill.rs @@ -7,7 +7,7 @@ use crate::core::Color; use crate::gradient::{self, Gradient}; /// The style used to fill geometry. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct Fill { /// The color or gradient of the fill. /// diff --git a/graphics/src/geometry/frame.rs b/graphics/src/geometry/frame.rs index 377589d7..3dee7e75 100644 --- a/graphics/src/geometry/frame.rs +++ b/graphics/src/geometry/frame.rs @@ -1,6 +1,6 @@ //! Draw and generate geometry. use crate::core::{Point, Radians, Rectangle, Size, Vector}; -use crate::geometry::{self, Fill, Path, Stroke, Text}; +use crate::geometry::{self, Fill, Image, Path, Stroke, Svg, Text}; /// The region of a surface that can be used to draw geometry. #[allow(missing_debug_implementations)] @@ -65,6 +65,17 @@ where self.raw.stroke(path, stroke); } + /// Draws the stroke of an axis-aligned rectangle with the provided style + /// given its top-left corner coordinate and its `Size` on the [`Frame`] . + pub fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into>, + ) { + self.raw.stroke_rectangle(top_left, size, stroke); + } + /// Draws the characters of the given [`Text`] on the [`Frame`], filling /// them with the given color. /// @@ -75,6 +86,18 @@ where self.raw.fill_text(text); } + /// Draws the given [`Image`] on the [`Frame`] inside the given bounds. + #[cfg(feature = "image")] + pub fn draw_image(&mut self, bounds: Rectangle, image: impl Into) { + self.raw.draw_image(bounds, image); + } + + /// Draws the given [`Svg`] on the [`Frame`] inside the given bounds. + #[cfg(feature = "svg")] + pub fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into) { + self.raw.draw_svg(bounds, svg); + } + /// Stores the current transform of the [`Frame`] and executes the given /// drawing operations, restoring the transform afterwards. /// @@ -116,8 +139,7 @@ where let mut frame = self.draft(region); let result = f(&mut frame); - - self.paste(frame, Point::new(region.x, region.y)); + self.paste(frame); result } @@ -134,8 +156,8 @@ where } /// Draws the contents of the given [`Frame`] with origin at the given [`Point`]. - fn paste(&mut self, frame: Self, at: Point) { - self.raw.paste(frame.raw, at); + fn paste(&mut self, frame: Self) { + self.raw.paste(frame.raw); } /// Applies a translation to the current transform of the [`Frame`]. @@ -186,9 +208,15 @@ pub trait Backend: Sized { fn scale_nonuniform(&mut self, scale: impl Into); fn draft(&mut self, clip_bounds: Rectangle) -> Self; - fn paste(&mut self, frame: Self, at: Point); + fn paste(&mut self, frame: Self); fn stroke<'a>(&mut self, path: &Path, stroke: impl Into>); + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into>, + ); fn fill(&mut self, path: &Path, fill: impl Into); fn fill_text(&mut self, text: impl Into); @@ -199,6 +227,9 @@ pub trait Backend: Sized { fill: impl Into, ); + fn draw_image(&mut self, bounds: Rectangle, image: impl Into); + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into); + fn into_geometry(self) -> Self::Geometry; } @@ -231,9 +262,16 @@ impl Backend for () { fn scale_nonuniform(&mut self, _scale: impl Into) {} fn draft(&mut self, _clip_bounds: Rectangle) -> Self {} - fn paste(&mut self, _frame: Self, _at: Point) {} + fn paste(&mut self, _frame: Self) {} fn stroke<'a>(&mut self, _path: &Path, _stroke: impl Into>) {} + fn stroke_rectangle<'a>( + &mut self, + _top_left: Point, + _size: Size, + _stroke: impl Into>, + ) { + } fn fill(&mut self, _path: &Path, _fill: impl Into) {} fn fill_text(&mut self, _text: impl Into) {} @@ -245,5 +283,8 @@ impl Backend for () { ) { } + fn draw_image(&mut self, _bounds: Rectangle, _image: impl Into) {} + fn draw_svg(&mut self, _bounds: Rectangle, _svg: impl Into) {} + fn into_geometry(self) -> Self::Geometry {} } diff --git a/graphics/src/geometry/path.rs b/graphics/src/geometry/path.rs index 3d8fc6fa..c4f51593 100644 --- a/graphics/src/geometry/path.rs +++ b/graphics/src/geometry/path.rs @@ -9,7 +9,8 @@ pub use builder::Builder; pub use lyon_path; -use iced_core::{Point, Size}; +use crate::core::border; +use crate::core::{Point, Size}; /// An immutable set of points that may or may not be connected. /// @@ -47,6 +48,16 @@ impl Path { Self::new(|p| p.rectangle(top_left, size)) } + /// Creates a new [`Path`] representing a rounded rectangle given its top-left + /// corner coordinate, its [`Size`] and [`border::Radius`]. + pub fn rounded_rectangle( + top_left: Point, + size: Size, + radius: border::Radius, + ) -> Self { + Self::new(|p| p.rounded_rectangle(top_left, size, radius)) + } + /// Creates a new [`Path`] representing a circle given its center /// coordinate and its radius. pub fn circle(center: Point, radius: f32) -> Self { diff --git a/graphics/src/geometry/path/builder.rs b/graphics/src/geometry/path/builder.rs index 1ccd83f2..e814a3a7 100644 --- a/graphics/src/geometry/path/builder.rs +++ b/graphics/src/geometry/path/builder.rs @@ -1,6 +1,7 @@ -use crate::geometry::path::{arc, Arc, Path}; +use crate::geometry::path::{Arc, Path, arc}; -use iced_core::{Point, Radians, Size}; +use crate::core::border; +use crate::core::{Point, Radians, Size}; use lyon_path::builder::{self, SvgPathBuilder}; use lyon_path::geom; @@ -160,6 +161,75 @@ impl Builder { self.close(); } + /// Adds a rounded rectangle to the [`Path`] given its top-left + /// corner coordinate its [`Size`] and [`border::Radius`]. + #[inline] + pub fn rounded_rectangle( + &mut self, + top_left: Point, + size: Size, + radius: border::Radius, + ) { + let min_size = (size.height / 2.0).min(size.width / 2.0); + let [ + top_left_corner, + top_right_corner, + bottom_right_corner, + bottom_left_corner, + ] = radius.into(); + + self.move_to(Point::new( + top_left.x + min_size.min(top_left_corner), + top_left.y, + )); + self.line_to(Point::new( + top_left.x + size.width - min_size.min(top_right_corner), + top_left.y, + )); + self.arc_to( + Point::new(top_left.x + size.width, top_left.y), + Point::new( + top_left.x + size.width, + top_left.y + min_size.min(top_right_corner), + ), + min_size.min(top_right_corner), + ); + self.line_to(Point::new( + top_left.x + size.width, + top_left.y + size.height - min_size.min(bottom_right_corner), + )); + self.arc_to( + Point::new(top_left.x + size.width, top_left.y + size.height), + Point::new( + top_left.x + size.width - min_size.min(bottom_right_corner), + top_left.y + size.height, + ), + min_size.min(bottom_right_corner), + ); + self.line_to(Point::new( + top_left.x + min_size.min(bottom_left_corner), + top_left.y + size.height, + )); + self.arc_to( + Point::new(top_left.x, top_left.y + size.height), + Point::new( + top_left.x, + top_left.y + size.height - min_size.min(bottom_left_corner), + ), + min_size.min(bottom_left_corner), + ); + self.line_to(Point::new( + top_left.x, + top_left.y + min_size.min(top_left_corner), + )); + self.arc_to( + Point::new(top_left.x, top_left.y), + Point::new(top_left.x + min_size.min(top_left_corner), top_left.y), + min_size.min(top_left_corner), + ); + self.close(); + } + /// Adds a circle to the [`Path`] given its center coordinate and its /// radius. #[inline] diff --git a/graphics/src/geometry/stroke.rs b/graphics/src/geometry/stroke.rs index aff49ab3..88a5fd7b 100644 --- a/graphics/src/geometry/stroke.rs +++ b/graphics/src/geometry/stroke.rs @@ -6,7 +6,7 @@ pub use crate::geometry::Style; use iced_core::Color; /// The style of a stroke. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct Stroke<'a> { /// The color or gradient of the stroke. /// @@ -23,7 +23,7 @@ pub struct Stroke<'a> { pub line_dash: LineDash<'a>, } -impl<'a> Stroke<'a> { +impl Stroke<'_> { /// Sets the color of the [`Stroke`]. pub fn with_color(self, color: Color) -> Self { Stroke { @@ -48,7 +48,7 @@ impl<'a> Stroke<'a> { } } -impl<'a> Default for Stroke<'a> { +impl Default for Stroke<'_> { fn default() -> Self { Stroke { style: Style::Solid(Color::BLACK), diff --git a/graphics/src/geometry/style.rs b/graphics/src/geometry/style.rs index a0f4b08a..de77eccc 100644 --- a/graphics/src/geometry/style.rs +++ b/graphics/src/geometry/style.rs @@ -2,7 +2,7 @@ use crate::core::Color; use crate::geometry::Gradient; /// The coloring style of some drawing. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Style { /// A solid [`Color`]. Solid(Color), diff --git a/graphics/src/geometry/text.rs b/graphics/src/geometry/text.rs index d314e85e..90147f87 100644 --- a/graphics/src/geometry/text.rs +++ b/graphics/src/geometry/text.rs @@ -43,6 +43,7 @@ impl Text { let mut buffer = cosmic_text::BufferLine::new( &self.content, + cosmic_text::LineEnding::default(), cosmic_text::AttrsList::new(text::to_attributes(self.font)), text::to_shaping(self.shaping), ); @@ -50,8 +51,10 @@ impl Text { let layout = buffer.layout( font_system.raw(), self.size.0, - f32::MAX, + None, cosmic_text::Wrap::None, + None, + 4, ); let translation_x = match self.horizontal_alignment { diff --git a/graphics/src/gradient.rs b/graphics/src/gradient.rs index 603f1b4a..54261721 100644 --- a/graphics/src/gradient.rs +++ b/graphics/src/gradient.rs @@ -9,7 +9,7 @@ use bytemuck::{Pod, Zeroable}; use half::f16; use std::cmp::Ordering; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] /// A fill which linearly interpolates colors along a direction. /// /// For a gradient which can be used as a fill for a background of a widget, see [`crate::core::Gradient`]. diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 318592be..171edd80 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -2,57 +2,26 @@ #[cfg(feature = "image")] pub use ::image as image_rs; -use crate::core::{image, svg, Color, Radians, Rectangle}; +use crate::core::Rectangle; +use crate::core::image; +use crate::core::svg; /// A raster or vector image. #[derive(Debug, Clone, PartialEq)] pub enum Image { /// A raster image. - Raster { - /// The handle of a raster image. - handle: image::Handle, + Raster(image::Image, Rectangle), - /// The filter method of a raster image. - filter_method: image::FilterMethod, - - /// The bounds of the image. - bounds: Rectangle, - - /// The rotation of the image. - rotation: Radians, - - /// The opacity of the image. - opacity: f32, - }, /// A vector image. - Vector { - /// The handle of a vector image. - handle: svg::Handle, - - /// The [`Color`] filter - color: Option, - - /// The bounds of the image. - bounds: Rectangle, - - /// The rotation of the image. - rotation: Radians, - - /// The opacity of the image. - opacity: f32, - }, + Vector(svg::Svg, Rectangle), } impl Image { /// Returns the bounds of the [`Image`]. pub fn bounds(&self) -> Rectangle { match self { - Image::Raster { - bounds, rotation, .. - } - | Image::Vector { - bounds, rotation, .. - } => bounds.rotate(*rotation), + Image::Raster(image, bounds) => bounds.rotate(image.rotation), + Image::Vector(svg, bounds) => bounds.rotate(svg.rotation), } } } diff --git a/graphics/src/settings.rs b/graphics/src/settings.rs index 2e8275c6..118ed73b 100644 --- a/graphics/src/settings.rs +++ b/graphics/src/settings.rs @@ -1,5 +1,5 @@ -use crate::core::{Font, Pixels}; use crate::Antialiasing; +use crate::core::{Font, Pixels}; /// The settings of a renderer. #[derive(Debug, Clone, Copy, PartialEq)] diff --git a/graphics/src/text.rs b/graphics/src/text.rs index 30269e69..7694ff1f 100644 --- a/graphics/src/text.rs +++ b/graphics/src/text.rs @@ -11,12 +11,12 @@ pub use cosmic_text; use crate::core::alignment; use crate::core::font::{self, Font}; -use crate::core::text::Shaping; +use crate::core::text::{Shaping, Wrapping}; use crate::core::{Color, Pixels, Point, Rectangle, Size, Transformation}; -use once_cell::sync::OnceCell; use std::borrow::Cow; -use std::sync::{Arc, RwLock, Weak}; +use std::collections::HashSet; +use std::sync::{Arc, OnceLock, RwLock, Weak}; /// A text primitive. #[derive(Debug, Clone, PartialEq)] @@ -146,16 +146,17 @@ impl Text { /// The regular variant of the [Fira Sans] font. /// -/// It is loaded as part of the default fonts in Wasm builds. +/// It is loaded as part of the default fonts when the `fira-sans` +/// feature is enabled. /// /// [Fira Sans]: https://mozilla.github.io/Fira/ -#[cfg(all(target_arch = "wasm32", feature = "fira-sans"))] -pub const FIRA_SANS_REGULAR: &'static [u8] = +#[cfg(feature = "fira-sans")] +pub const FIRA_SANS_REGULAR: &[u8] = include_bytes!("../fonts/FiraSans-Regular.ttf").as_slice(); /// Returns the global [`FontSystem`]. pub fn font_system() -> &'static RwLock { - static FONT_SYSTEM: OnceCell> = OnceCell::new(); + static FONT_SYSTEM: OnceLock> = OnceLock::new(); FONT_SYSTEM.get_or_init(|| { RwLock::new(FontSystem { @@ -163,11 +164,12 @@ pub fn font_system() -> &'static RwLock { cosmic_text::fontdb::Source::Binary(Arc::new( include_bytes!("../fonts/Iced-Icons.ttf").as_slice(), )), - #[cfg(all(target_arch = "wasm32", feature = "fira-sans"))] + #[cfg(feature = "fira-sans")] cosmic_text::fontdb::Source::Binary(Arc::new( include_bytes!("../fonts/FiraSans-Regular.ttf").as_slice(), )), ]), + loaded_fonts: HashSet::new(), version: Version::default(), }) }) @@ -177,6 +179,7 @@ pub fn font_system() -> &'static RwLock { #[allow(missing_debug_implementations)] pub struct FontSystem { raw: cosmic_text::FontSystem, + loaded_fonts: HashSet, version: Version, } @@ -188,6 +191,14 @@ impl FontSystem { /// Loads a font from its bytes. pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) { + if let Cow::Borrowed(bytes) = bytes { + let address = bytes.as_ptr() as usize; + + if !self.loaded_fonts.insert(address) { + return; + } + } + let _ = self.raw.db_mut().load_font_source( cosmic_text::fontdb::Source::Binary(Arc::new(bytes.into_owned())), ); @@ -232,13 +243,14 @@ impl PartialEq for Raw { /// Measures the dimensions of the given [`cosmic_text::Buffer`]. pub fn measure(buffer: &cosmic_text::Buffer) -> Size { - let (width, total_lines) = buffer - .layout_runs() - .fold((0.0, 0usize), |(width, total_lines), run| { - (run.line_w.max(width), total_lines + 1) - }); + let (width, height) = + buffer + .layout_runs() + .fold((0.0, 0.0), |(width, height), run| { + (run.line_w.max(width), height + run.line_height) + }); - Size::new(width, total_lines as f32 * buffer.metrics().line_height) + Size::new(width, height) } /// Returns the attributes of the given [`Font`]. @@ -305,6 +317,16 @@ pub fn to_shaping(shaping: Shaping) -> cosmic_text::Shaping { } } +/// Converts some [`Wrapping`] strategy to a [`cosmic_text::Wrap`] strategy. +pub fn to_wrap(wrapping: Wrapping) -> cosmic_text::Wrap { + match wrapping { + Wrapping::None => cosmic_text::Wrap::None, + Wrapping::Word => cosmic_text::Wrap::Word, + Wrapping::Glyph => cosmic_text::Wrap::Glyph, + Wrapping::WordOrGlyph => cosmic_text::Wrap::WordOrGlyph, + } +} + /// Converts some [`Color`] to a [`cosmic_text::Color`]. pub fn to_color(color: Color) -> cosmic_text::Color { let [r, g, b, a] = color.into_rgba8(); diff --git a/graphics/src/text/cache.rs b/graphics/src/text/cache.rs index 822b61c4..e64d93f1 100644 --- a/graphics/src/text/cache.rs +++ b/graphics/src/text/cache.rs @@ -48,8 +48,8 @@ impl Cache { buffer.set_size( font_system, - key.bounds.width, - key.bounds.height.max(key.line_height), + Some(key.bounds.width), + Some(key.bounds.height.max(key.line_height)), ); buffer.set_text( font_system, diff --git a/graphics/src/text/editor.rs b/graphics/src/text/editor.rs index 4b8f0f2a..765de07e 100644 --- a/graphics/src/text/editor.rs +++ b/graphics/src/text/editor.rs @@ -3,21 +3,23 @@ use crate::core::text::editor::{ self, Action, Cursor, Direction, Edit, Motion, }; use crate::core::text::highlighter::{self, Highlighter}; -use crate::core::text::LineHeight; +use crate::core::text::{LineHeight, Wrapping}; use crate::core::{Font, Pixels, Point, Rectangle, Size}; use crate::text; use cosmic_text::Edit as _; +use std::borrow::Cow; use std::fmt; -use std::sync::{self, Arc}; +use std::sync::{self, Arc, RwLock}; /// A multi-line text editor. #[derive(Debug, PartialEq)] pub struct Editor(Option>); struct Internal { - editor: cosmic_text::Editor, + editor: cosmic_text::Editor<'static>, + cursor: RwLock>, font: Font, bounds: Size, topmost_line_changed: Option, @@ -32,7 +34,7 @@ impl Editor { /// Returns the buffer of the [`Editor`]. pub fn buffer(&self) -> &cosmic_text::Buffer { - self.internal().editor.buffer() + buffer_from_editor(&self.internal().editor) } /// Creates a [`Weak`] reference to the [`Editor`]. @@ -82,11 +84,24 @@ impl editor::Editor for Editor { }))) } - fn line(&self, index: usize) -> Option<&str> { - self.buffer() - .lines - .get(index) - .map(cosmic_text::BufferLine::text) + fn is_empty(&self) -> bool { + let buffer = self.buffer(); + + buffer.lines.is_empty() + || (buffer.lines.len() == 1 && buffer.lines[0].text().is_empty()) + } + + fn line(&self, index: usize) -> Option> { + self.buffer().lines.get(index).map(|line| editor::Line { + text: Cow::Borrowed(line.text()), + ending: match line.ending() { + cosmic_text::LineEnding::Lf => editor::LineEnding::Lf, + cosmic_text::LineEnding::CrLf => editor::LineEnding::CrLf, + cosmic_text::LineEnding::Cr => editor::LineEnding::Cr, + cosmic_text::LineEnding::LfCr => editor::LineEnding::LfCr, + cosmic_text::LineEnding::None => editor::LineEnding::None, + }, + }) } fn line_count(&self) -> usize { @@ -100,17 +115,15 @@ impl editor::Editor for Editor { fn cursor(&self) -> editor::Cursor { let internal = self.internal(); + if let Ok(Some(cursor)) = internal.cursor.read().as_deref() { + return cursor.clone(); + } + let cursor = internal.editor.cursor(); - let buffer = internal.editor.buffer(); - - match internal.editor.select_opt() { - Some(selection) => { - let (start, end) = if cursor < selection { - (cursor, selection) - } else { - (selection, cursor) - }; + let buffer = buffer_from_editor(&internal.editor); + let cursor = match internal.editor.selection_bounds() { + Some((start, end)) => { let line_height = buffer.metrics().line_height; let selected_lines = end.line - start.line + 1; @@ -142,7 +155,8 @@ impl editor::Editor for Editor { width, y: (visual_line as i32 + visual_lines_offset) as f32 - * line_height, + * line_height + - buffer.scroll().vertical, height: line_height, }) } else { @@ -224,10 +238,16 @@ impl editor::Editor for Editor { Cursor::Caret(Point::new( offset, (visual_lines_offset + visual_line as i32) as f32 - * line_height, + * line_height + - buffer.scroll().vertical, )) } - } + }; + + *internal.cursor.write().expect("Write to cursor cache") = + Some(cursor.clone()); + + cursor } fn cursor_position(&self) -> (usize, usize) { @@ -249,19 +269,18 @@ impl editor::Editor for Editor { let editor = &mut internal.editor; + // Clear cursor cache + let _ = internal + .cursor + .write() + .expect("Write to cursor cache") + .take(); + match action { // Motion events Action::Move(motion) => { - if let Some(selection) = editor.select_opt() { - let cursor = editor.cursor(); - - let (left, right) = if cursor < selection { - (cursor, selection) - } else { - (selection, cursor) - }; - - editor.set_select_opt(None); + if let Some((start, end)) = editor.selection_bounds() { + editor.set_selection(cosmic_text::Selection::None); match motion { // These motions are performed as-is even when a selection @@ -272,17 +291,20 @@ impl editor::Editor for Editor { | Motion::DocumentEnd => { editor.action( font_system.raw(), - motion_to_action(motion), + cosmic_text::Action::Motion(to_motion(motion)), ); } // Other motions simply move the cursor to one end of the selection _ => editor.set_cursor(match motion.direction() { - Direction::Left => left, - Direction::Right => right, + Direction::Left => start, + Direction::Right => end, }), } } else { - editor.action(font_system.raw(), motion_to_action(motion)); + editor.action( + font_system.raw(), + cosmic_text::Action::Motion(to_motion(motion)), + ); } } @@ -290,99 +312,58 @@ impl editor::Editor for Editor { Action::Select(motion) => { let cursor = editor.cursor(); - if editor.select_opt().is_none() { - editor.set_select_opt(Some(cursor)); + if editor.selection_bounds().is_none() { + editor + .set_selection(cosmic_text::Selection::Normal(cursor)); } - editor.action(font_system.raw(), motion_to_action(motion)); + editor.action( + font_system.raw(), + cosmic_text::Action::Motion(to_motion(motion)), + ); // Deselect if selection matches cursor position - if let Some(selection) = editor.select_opt() { - let cursor = editor.cursor(); - - if cursor.line == selection.line - && cursor.index == selection.index - { - editor.set_select_opt(None); + if let Some((start, end)) = editor.selection_bounds() { + if start.line == end.line && start.index == end.index { + editor.set_selection(cosmic_text::Selection::None); } } } Action::SelectWord => { - use unicode_segmentation::UnicodeSegmentation; - let cursor = editor.cursor(); - if let Some(line) = editor.buffer().lines.get(cursor.line) { - let (start, end) = - UnicodeSegmentation::unicode_word_indices(line.text()) - // Split words with dots - .flat_map(|(i, word)| { - word.split('.').scan(i, |current, word| { - let start = *current; - *current += word.len() + 1; - - Some((start, word)) - }) - }) - // Turn words into ranges - .map(|(i, word)| (i, i + word.len())) - // Find the word at cursor - .find(|&(start, end)| { - start <= cursor.index && cursor.index < end - }) - // Cursor is not in a word. Let's select its punctuation cluster. - .unwrap_or_else(|| { - let start = line.text()[..cursor.index] - .char_indices() - .rev() - .take_while(|(_, c)| { - c.is_ascii_punctuation() - }) - .map(|(i, _)| i) - .last() - .unwrap_or(cursor.index); - - let end = line.text()[cursor.index..] - .char_indices() - .skip_while(|(_, c)| { - c.is_ascii_punctuation() - }) - .map(|(i, _)| i + cursor.index) - .next() - .unwrap_or(cursor.index); - - (start, end) - }); - - if start != end { - editor.set_cursor(cosmic_text::Cursor { - index: start, - ..cursor - }); - - editor.set_select_opt(Some(cosmic_text::Cursor { - index: end, - ..cursor - })); - } - } + editor.set_selection(cosmic_text::Selection::Word(cursor)); } Action::SelectLine => { let cursor = editor.cursor(); - if let Some(line_length) = editor - .buffer() - .lines - .get(cursor.line) - .map(|line| line.text().len()) - { - editor - .set_cursor(cosmic_text::Cursor { index: 0, ..cursor }); + editor.set_selection(cosmic_text::Selection::Line(cursor)); + } + Action::SelectAll => { + let buffer = buffer_from_editor(editor); - editor.set_select_opt(Some(cosmic_text::Cursor { - index: line_length, - ..cursor - })); + if buffer.lines.len() > 1 + || buffer + .lines + .first() + .is_some_and(|line| !line.text().is_empty()) + { + let cursor = editor.cursor(); + + editor.set_selection(cosmic_text::Selection::Normal( + cosmic_text::Cursor { + line: 0, + index: 0, + ..cursor + }, + )); + + editor.action( + font_system.raw(), + cosmic_text::Action::Motion( + cosmic_text::Motion::BufferEnd, + ), + ); } } @@ -419,10 +400,12 @@ impl editor::Editor for Editor { } let cursor = editor.cursor(); - let selection = editor.select_opt().unwrap_or(cursor); + let selection_start = editor + .selection_bounds() + .map(|(start, _)| start) + .unwrap_or(cursor); - internal.topmost_line_changed = - Some(cursor.min(selection).line); + internal.topmost_line_changed = Some(selection_start.line); } // Mouse events @@ -445,25 +428,17 @@ impl editor::Editor for Editor { ); // Deselect if selection matches cursor position - if let Some(selection) = editor.select_opt() { - let cursor = editor.cursor(); - - if cursor.line == selection.line - && cursor.index == selection.index - { - editor.set_select_opt(None); + if let Some((start, end)) = editor.selection_bounds() { + if start.line == end.line && start.index == end.index { + editor.set_selection(cosmic_text::Selection::None); } } } Action::Scroll { lines } => { - let (_, height) = editor.buffer().size(); - - if height < i32::MAX as f32 { - editor.action( - font_system.raw(), - cosmic_text::Action::Scroll { lines }, - ); - } + editor.action( + font_system.raw(), + cosmic_text::Action::Scroll { lines }, + ); } } @@ -477,7 +452,7 @@ impl editor::Editor for Editor { fn min_bounds(&self) -> Size { let internal = self.internal(); - text::measure(internal.editor.buffer()) + text::measure(buffer_from_editor(&internal.editor)) } fn update( @@ -486,6 +461,7 @@ impl editor::Editor for Editor { new_font: Font, new_size: Pixels, new_line_height: LineHeight, + new_wrapping: Wrapping, new_highlighter: &mut impl Highlighter, ) { let editor = @@ -497,10 +473,12 @@ impl editor::Editor for Editor { let mut font_system = text::font_system().write().expect("Write font system"); + let buffer = buffer_mut_from_editor(&mut internal.editor); + if font_system.version() != internal.version { log::trace!("Updating `FontSystem` of `Editor`..."); - for line in internal.editor.buffer_mut().lines.iter_mut() { + for line in buffer.lines.iter_mut() { line.reset(); } @@ -511,7 +489,7 @@ impl editor::Editor for Editor { if new_font != internal.font { log::trace!("Updating font of `Editor`..."); - for line in internal.editor.buffer_mut().lines.iter_mut() { + for line in buffer.lines.iter_mut() { let _ = line.set_attrs_list(cosmic_text::AttrsList::new( text::to_attributes(new_font), )); @@ -521,7 +499,7 @@ impl editor::Editor for Editor { internal.topmost_line_changed = Some(0); } - let metrics = internal.editor.buffer().metrics(); + let metrics = buffer.metrics(); let new_line_height = new_line_height.to_absolute(new_size); if new_size.0 != metrics.font_size @@ -529,19 +507,27 @@ impl editor::Editor for Editor { { log::trace!("Updating `Metrics` of `Editor`..."); - internal.editor.buffer_mut().set_metrics( + buffer.set_metrics( font_system.raw(), cosmic_text::Metrics::new(new_size.0, new_line_height.0), ); } + let new_wrap = text::to_wrap(new_wrapping); + + if new_wrap != buffer.wrap() { + log::trace!("Updating `Wrap` strategy of `Editor`..."); + + buffer.set_wrap(font_system.raw(), new_wrap); + } + if new_bounds != internal.bounds { log::trace!("Updating size of `Editor`..."); - internal.editor.buffer_mut().set_size( + buffer.set_size( font_system.raw(), - new_bounds.width, - new_bounds.height, + Some(new_bounds.width), + Some(new_bounds.height), ); internal.bounds = new_bounds; @@ -556,7 +542,14 @@ impl editor::Editor for Editor { new_highlighter.change_line(topmost_line_changed); } - internal.editor.shape_as_needed(font_system.raw()); + internal.editor.shape_as_needed(font_system.raw(), false); + + // Clear cursor cache + let _ = internal + .cursor + .write() + .expect("Write to cursor cache") + .take(); self.0 = Some(Arc::new(internal)); } @@ -568,12 +561,13 @@ impl editor::Editor for Editor { format_highlight: impl Fn(&H::Highlight) -> highlighter::Format, ) { let internal = self.internal(); - let buffer = internal.editor.buffer(); + let buffer = buffer_from_editor(&internal.editor); - let mut window = buffer.scroll() + buffer.visible_lines(); + let scroll = buffer.scroll(); + let mut window = (internal.bounds.height / buffer.metrics().line_height) + .ceil() as i32; - let last_visible_line = buffer - .lines + let last_visible_line = buffer.lines[scroll.line..] .iter() .enumerate() .find_map(|(i, line)| { @@ -587,7 +581,7 @@ impl editor::Editor for Editor { window -= visible_lines; None } else { - Some(i) + Some(scroll.line + i) } }) .unwrap_or(buffer.lines.len().saturating_sub(1)); @@ -609,7 +603,7 @@ impl editor::Editor for Editor { let attributes = text::to_attributes(font); - for line in &mut internal.editor.buffer_mut().lines + for line in &mut buffer_mut_from_editor(&mut internal.editor).lines [current_line..=last_visible_line] { let mut list = cosmic_text::AttrsList::new(attributes); @@ -635,7 +629,7 @@ impl editor::Editor for Editor { let _ = line.set_attrs_list(list); } - internal.editor.shape_as_needed(font_system.raw()); + internal.editor.shape_as_needed(font_system.raw(), false); self.0 = Some(Arc::new(internal)); } @@ -651,7 +645,8 @@ impl PartialEq for Internal { fn eq(&self, other: &Self) -> bool { self.font == other.font && self.bounds == other.bounds - && self.editor.buffer().metrics() == other.editor.buffer().metrics() + && buffer_from_editor(&self.editor).metrics() + == buffer_from_editor(&other.editor).metrics() } } @@ -664,6 +659,7 @@ impl Default for Internal { line_height: 1.0, }, )), + cursor: RwLock::new(None), font: Font::default(), bounds: Size::ZERO, topmost_line_changed: None, @@ -713,7 +709,8 @@ fn highlight_line( let layout = line .layout_opt() .as_ref() - .expect("Line layout should be cached"); + .map(Vec::as_slice) + .unwrap_or_default(); layout.iter().map(move |visual_line| { let start = visual_line @@ -756,34 +753,61 @@ fn highlight_line( } fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 { - let visual_lines_before_start: usize = buffer - .lines + let scroll = buffer.scroll(); + + let start = scroll.line.min(line); + let end = scroll.line.max(line); + + let visual_lines_offset: usize = buffer.lines[start..] .iter() - .take(line) + .take(end - start) .map(|line| { - line.layout_opt() - .as_ref() - .expect("Line layout should be cached") - .len() + line.layout_opt().as_ref().map(Vec::len).unwrap_or_default() }) .sum(); - visual_lines_before_start as i32 - buffer.scroll() + visual_lines_offset as i32 * if scroll.line < line { 1 } else { -1 } } -fn motion_to_action(motion: Motion) -> cosmic_text::Action { +fn to_motion(motion: Motion) -> cosmic_text::Motion { match motion { - Motion::Left => cosmic_text::Action::Left, - Motion::Right => cosmic_text::Action::Right, - Motion::Up => cosmic_text::Action::Up, - Motion::Down => cosmic_text::Action::Down, - Motion::WordLeft => cosmic_text::Action::LeftWord, - Motion::WordRight => cosmic_text::Action::RightWord, - Motion::Home => cosmic_text::Action::Home, - Motion::End => cosmic_text::Action::End, - Motion::PageUp => cosmic_text::Action::PageUp, - Motion::PageDown => cosmic_text::Action::PageDown, - Motion::DocumentStart => cosmic_text::Action::BufferStart, - Motion::DocumentEnd => cosmic_text::Action::BufferEnd, + Motion::Left => cosmic_text::Motion::Left, + Motion::Right => cosmic_text::Motion::Right, + Motion::Up => cosmic_text::Motion::Up, + Motion::Down => cosmic_text::Motion::Down, + Motion::WordLeft => cosmic_text::Motion::LeftWord, + Motion::WordRight => cosmic_text::Motion::RightWord, + Motion::Home => cosmic_text::Motion::Home, + Motion::End => cosmic_text::Motion::End, + Motion::PageUp => cosmic_text::Motion::PageUp, + Motion::PageDown => cosmic_text::Motion::PageDown, + Motion::DocumentStart => cosmic_text::Motion::BufferStart, + Motion::DocumentEnd => cosmic_text::Motion::BufferEnd, + } +} + +fn buffer_from_editor<'a, 'b>( + editor: &'a impl cosmic_text::Edit<'b>, +) -> &'a cosmic_text::Buffer +where + 'b: 'a, +{ + match editor.buffer_ref() { + cosmic_text::BufferRef::Owned(buffer) => buffer, + cosmic_text::BufferRef::Borrowed(buffer) => buffer, + cosmic_text::BufferRef::Arc(buffer) => buffer, + } +} + +fn buffer_mut_from_editor<'a, 'b>( + editor: &'a mut impl cosmic_text::Edit<'b>, +) -> &'a mut cosmic_text::Buffer +where + 'b: 'a, +{ + match editor.buffer_ref_mut() { + cosmic_text::BufferRef::Owned(buffer) => buffer, + cosmic_text::BufferRef::Borrowed(buffer) => buffer, + cosmic_text::BufferRef::Arc(_buffer) => unreachable!(), } } diff --git a/graphics/src/text/paragraph.rs b/graphics/src/text/paragraph.rs index 31a323ac..48c8e9e6 100644 --- a/graphics/src/text/paragraph.rs +++ b/graphics/src/text/paragraph.rs @@ -1,8 +1,8 @@ //! Draw paragraphs. use crate::core; use crate::core::alignment; -use crate::core::text::{Hit, LineHeight, Shaping, Text}; -use crate::core::{Font, Pixels, Point, Size}; +use crate::core::text::{Hit, Shaping, Span, Text, Wrapping}; +use crate::core::{Font, Point, Rectangle, Size}; use crate::text; use std::fmt; @@ -10,13 +10,14 @@ use std::sync::{self, Arc}; /// A bunch of text. #[derive(Clone, PartialEq)] -pub struct Paragraph(Option>); +pub struct Paragraph(Arc); +#[derive(Clone)] struct Internal { buffer: cosmic_text::Buffer, - content: String, // TODO: Reuse from `buffer` (?) font: Font, shaping: Shaping, + wrapping: Wrapping, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, bounds: Size, @@ -52,9 +53,7 @@ impl Paragraph { } fn internal(&self) -> &Arc { - self.0 - .as_ref() - .expect("paragraph should always be initialized") + &self.0 } } @@ -62,7 +61,7 @@ impl core::text::Paragraph for Paragraph { type Font = Font; fn with_text(text: Text<&str>) -> Self { - log::trace!("Allocating paragraph: {}", text.content); + log::trace!("Allocating plain paragraph: {}", text.content); let mut font_system = text::font_system().write().expect("Write font system"); @@ -77,10 +76,12 @@ impl core::text::Paragraph for Paragraph { buffer.set_size( font_system.raw(), - text.bounds.width, - text.bounds.height, + Some(text.bounds.width), + Some(text.bounds.height), ); + buffer.set_wrap(font_system.raw(), text::to_wrap(text.wrapping)); + buffer.set_text( font_system.raw(), text.content, @@ -90,73 +91,115 @@ impl core::text::Paragraph for Paragraph { let min_bounds = text::measure(&buffer); - Self(Some(Arc::new(Internal { + Self(Arc::new(Internal { buffer, - content: text.content.to_owned(), font: text.font, horizontal_alignment: text.horizontal_alignment, vertical_alignment: text.vertical_alignment, shaping: text.shaping, + wrapping: text.wrapping, bounds: text.bounds, min_bounds, version: font_system.version(), - }))) + })) + } + + fn with_spans(text: Text<&[Span<'_, Link>]>) -> Self { + log::trace!("Allocating rich paragraph: {} spans", text.content.len()); + + let mut font_system = + text::font_system().write().expect("Write font system"); + + let mut buffer = cosmic_text::Buffer::new( + font_system.raw(), + cosmic_text::Metrics::new( + text.size.into(), + text.line_height.to_absolute(text.size).into(), + ), + ); + + buffer.set_size( + font_system.raw(), + Some(text.bounds.width), + Some(text.bounds.height), + ); + + buffer.set_wrap(font_system.raw(), text::to_wrap(text.wrapping)); + + buffer.set_rich_text( + font_system.raw(), + text.content.iter().enumerate().map(|(i, span)| { + let attrs = text::to_attributes(span.font.unwrap_or(text.font)); + + let attrs = match (span.size, span.line_height) { + (None, None) => attrs, + _ => { + let size = span.size.unwrap_or(text.size); + + attrs.metrics(cosmic_text::Metrics::new( + size.into(), + span.line_height + .unwrap_or(text.line_height) + .to_absolute(size) + .into(), + )) + } + }; + + let attrs = if let Some(color) = span.color { + attrs.color(text::to_color(color)) + } else { + attrs + }; + + (span.text.as_ref(), attrs.metadata(i)) + }), + text::to_attributes(text.font), + text::to_shaping(text.shaping), + ); + + let min_bounds = text::measure(&buffer); + + Self(Arc::new(Internal { + buffer, + font: text.font, + horizontal_alignment: text.horizontal_alignment, + vertical_alignment: text.vertical_alignment, + shaping: text.shaping, + wrapping: text.wrapping, + bounds: text.bounds, + min_bounds, + version: font_system.version(), + })) } fn resize(&mut self, new_bounds: Size) { - let paragraph = self - .0 - .take() - .expect("paragraph should always be initialized"); + let paragraph = Arc::make_mut(&mut self.0); - match Arc::try_unwrap(paragraph) { - Ok(mut internal) => { - let mut font_system = - text::font_system().write().expect("Write font system"); + let mut font_system = + text::font_system().write().expect("Write font system"); - internal.buffer.set_size( - font_system.raw(), - new_bounds.width, - new_bounds.height, - ); + paragraph.buffer.set_size( + font_system.raw(), + Some(new_bounds.width), + Some(new_bounds.height), + ); - internal.bounds = new_bounds; - internal.min_bounds = text::measure(&internal.buffer); - - self.0 = Some(Arc::new(internal)); - } - Err(internal) => { - let metrics = internal.buffer.metrics(); - - // If there is a strong reference somewhere, we recompute the - // buffer from scratch - *self = Self::with_text(Text { - content: &internal.content, - bounds: internal.bounds, - size: Pixels(metrics.font_size), - line_height: LineHeight::Absolute(Pixels( - metrics.line_height, - )), - font: internal.font, - horizontal_alignment: internal.horizontal_alignment, - vertical_alignment: internal.vertical_alignment, - shaping: internal.shaping, - }); - } - } + paragraph.bounds = new_bounds; + paragraph.min_bounds = text::measure(¶graph.buffer); } - fn compare(&self, text: Text<&str>) -> core::text::Difference { + fn compare(&self, text: Text<()>) -> core::text::Difference { let font_system = text::font_system().read().expect("Read font system"); let paragraph = self.internal(); let metrics = paragraph.buffer.metrics(); if paragraph.version != font_system.version - || paragraph.content != text.content || metrics.font_size != text.size.0 || metrics.line_height != text.line_height.to_absolute(text.size).0 || paragraph.font != text.font || paragraph.shaping != text.shaping + || paragraph.wrapping != text.wrapping || paragraph.horizontal_alignment != text.horizontal_alignment || paragraph.vertical_alignment != text.vertical_alignment { @@ -186,6 +229,87 @@ impl core::text::Paragraph for Paragraph { Some(Hit::CharOffset(cursor.index)) } + fn hit_span(&self, point: Point) -> Option { + let internal = self.internal(); + + let cursor = internal.buffer.hit(point.x, point.y)?; + let line = internal.buffer.lines.get(cursor.line)?; + + let mut last_glyph = None; + let mut glyphs = line + .layout_opt() + .as_ref()? + .iter() + .flat_map(|line| line.glyphs.iter()) + .peekable(); + + while let Some(glyph) = glyphs.peek() { + if glyph.start <= cursor.index && cursor.index < glyph.end { + break; + } + + last_glyph = glyphs.next(); + } + + let glyph = match cursor.affinity { + cosmic_text::Affinity::Before => last_glyph, + cosmic_text::Affinity::After => glyphs.next(), + }?; + + Some(glyph.metadata) + } + + fn span_bounds(&self, index: usize) -> Vec { + let internal = self.internal(); + + let mut bounds = Vec::new(); + let mut current_bounds = None; + + let glyphs = internal + .buffer + .layout_runs() + .flat_map(|run| { + let line_top = run.line_top; + let line_height = run.line_height; + + run.glyphs + .iter() + .map(move |glyph| (line_top, line_height, glyph)) + }) + .skip_while(|(_, _, glyph)| glyph.metadata != index) + .take_while(|(_, _, glyph)| glyph.metadata == index); + + for (line_top, line_height, glyph) in glyphs { + let y = line_top + glyph.y; + + let new_bounds = || { + Rectangle::new( + Point::new(glyph.x, y), + Size::new( + glyph.w, + glyph.line_height_opt.unwrap_or(line_height), + ), + ) + }; + + match current_bounds.as_mut() { + None => { + current_bounds = Some(new_bounds()); + } + Some(current_bounds) if y != current_bounds.y => { + bounds.push(*current_bounds); + *current_bounds = new_bounds(); + } + Some(current_bounds) => { + current_bounds.width += glyph.w; + } + } + } + + bounds.extend(current_bounds); + bounds + } + fn grapheme_position(&self, line: usize, index: usize) -> Option { use unicode_segmentation::UnicodeSegmentation; @@ -231,7 +355,7 @@ impl core::text::Paragraph for Paragraph { impl Default for Paragraph { fn default() -> Self { - Self(Some(Arc::new(Internal::default()))) + Self(Arc::new(Internal::default())) } } @@ -240,7 +364,6 @@ impl fmt::Debug for Paragraph { let paragraph = self.internal(); f.debug_struct("Paragraph") - .field("content", ¶graph.content) .field("font", ¶graph.font) .field("shaping", ¶graph.shaping) .field("horizontal_alignment", ¶graph.horizontal_alignment) @@ -253,8 +376,7 @@ impl fmt::Debug for Paragraph { impl PartialEq for Internal { fn eq(&self, other: &Self) -> bool { - self.content == other.content - && self.font == other.font + self.font == other.font && self.shaping == other.shaping && self.horizontal_alignment == other.horizontal_alignment && self.vertical_alignment == other.vertical_alignment @@ -271,9 +393,9 @@ impl Default for Internal { font_size: 1.0, line_height: 1.0, }), - content: String::new(), font: Font::default(), shaping: Shaping::default(), + wrapping: Wrapping::default(), horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, bounds: Size::ZERO, @@ -298,7 +420,7 @@ pub struct Weak { impl Weak { /// Tries to update the reference into a [`Paragraph`]. pub fn upgrade(&self) -> Option { - self.raw.upgrade().map(Some).map(Paragraph) + self.raw.upgrade().map(Paragraph) } } diff --git a/highlighter/Cargo.toml b/highlighter/Cargo.toml index 7962b89d..4c20a678 100644 --- a/highlighter/Cargo.toml +++ b/highlighter/Cargo.toml @@ -16,5 +16,4 @@ workspace = true [dependencies] iced_core.workspace = true -once_cell.workspace = true syntect.workspace = true diff --git a/highlighter/src/lib.rs b/highlighter/src/lib.rs index 7636a712..982f1279 100644 --- a/highlighter/src/lib.rs +++ b/highlighter/src/lib.rs @@ -1,19 +1,21 @@ //! A syntax highlighter for iced. use iced_core as core; +use crate::core::Color; +use crate::core::font::{self, Font}; use crate::core::text::highlighter::{self, Format}; -use crate::core::{Color, Font}; -use once_cell::sync::Lazy; use std::ops::Range; +use std::sync::LazyLock; + use syntect::highlighting; use syntect::parsing; -static SYNTAXES: Lazy = - Lazy::new(parsing::SyntaxSet::load_defaults_nonewlines); +static SYNTAXES: LazyLock = + LazyLock::new(parsing::SyntaxSet::load_defaults_nonewlines); -static THEMES: Lazy = - Lazy::new(highlighting::ThemeSet::load_defaults); +static THEMES: LazyLock = + LazyLock::new(highlighting::ThemeSet::load_defaults); const LINES_PER_SNAPSHOT: usize = 50; @@ -35,7 +37,7 @@ impl highlighter::Highlighter for Highlighter { fn new(settings: &Self::Settings) -> Self { let syntax = SYNTAXES - .find_syntax_by_token(&settings.extension) + .find_syntax_by_token(&settings.token) .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()); let highlighter = highlighting::Highlighter::new( @@ -55,7 +57,7 @@ impl highlighter::Highlighter for Highlighter { fn update(&mut self, new_settings: &Self::Settings) { self.syntax = SYNTAXES - .find_syntax_by_token(&new_settings.extension) + .find_syntax_by_token(&new_settings.token) .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()); self.highlighter = highlighting::Highlighter::new( @@ -103,30 +105,7 @@ impl highlighter::Highlighter for Highlighter { let ops = parser.parse_line(line, &SYNTAXES).unwrap_or_default(); - let highlighter = &self.highlighter; - - Box::new( - ScopeRangeIterator { - ops, - line_length: line.len(), - index: 0, - last_str_index: 0, - } - .filter_map(move |(range, scope)| { - let _ = stack.apply(&scope); - - if range.is_empty() { - None - } else { - Some(( - range, - Highlight( - highlighter.style_mod_for_stack(&stack.scopes), - ), - )) - } - }), - ) + Box::new(scope_iterator(ops, line, stack, &self.highlighter)) } fn current_line(&self) -> usize { @@ -134,6 +113,92 @@ impl highlighter::Highlighter for Highlighter { } } +fn scope_iterator<'a>( + ops: Vec<(usize, parsing::ScopeStackOp)>, + line: &str, + stack: &'a mut parsing::ScopeStack, + highlighter: &'a highlighting::Highlighter<'static>, +) -> impl Iterator, Highlight)> + 'a { + ScopeRangeIterator { + ops, + line_length: line.len(), + index: 0, + last_str_index: 0, + } + .filter_map(move |(range, scope)| { + let _ = stack.apply(&scope); + + if range.is_empty() { + None + } else { + Some(( + range, + Highlight(highlighter.style_mod_for_stack(&stack.scopes)), + )) + } + }) +} + +/// A streaming syntax highlighter. +/// +/// It can efficiently highlight an immutable stream of tokens. +#[derive(Debug)] +pub struct Stream { + syntax: &'static parsing::SyntaxReference, + highlighter: highlighting::Highlighter<'static>, + commit: (parsing::ParseState, parsing::ScopeStack), + state: parsing::ParseState, + stack: parsing::ScopeStack, +} + +impl Stream { + /// Creates a new [`Stream`] highlighter. + pub fn new(settings: &Settings) -> Self { + let syntax = SYNTAXES + .find_syntax_by_token(&settings.token) + .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text()); + + let highlighter = highlighting::Highlighter::new( + &THEMES.themes[settings.theme.key()], + ); + + let state = parsing::ParseState::new(syntax); + let stack = parsing::ScopeStack::new(); + + Self { + syntax, + highlighter, + commit: (state.clone(), stack.clone()), + state, + stack, + } + } + + /// Highlights the given line from the last commit. + pub fn highlight_line( + &mut self, + line: &str, + ) -> impl Iterator, Highlight)> + '_ { + self.state = self.commit.0.clone(); + self.stack = self.commit.1.clone(); + + let ops = self.state.parse_line(line, &SYNTAXES).unwrap_or_default(); + scope_iterator(ops, line, &mut self.stack, &self.highlighter) + } + + /// Commits the last highlighted line. + pub fn commit(&mut self) { + self.commit = (self.state.clone(), self.stack.clone()); + } + + /// Resets the [`Stream`] highlighter. + pub fn reset(&mut self) { + self.state = parsing::ParseState::new(self.syntax); + self.stack = parsing::ScopeStack::new(); + self.commit = (self.state.clone(), self.stack.clone()); + } +} + /// The settings of a [`Highlighter`]. #[derive(Debug, Clone, PartialEq)] pub struct Settings { @@ -141,11 +206,11 @@ pub struct Settings { /// /// It dictates the color scheme that will be used for highlighting. pub theme: Theme, - /// The extension of the file to highlight. + /// The extension of the file or the name of the language to highlight. /// - /// The [`Highlighter`] will use the extension to automatically determine + /// The [`Highlighter`] will use the token to automatically determine /// the grammar to use for highlighting. - pub extension: String, + pub token: String, } /// A highlight produced by a [`Highlighter`]. @@ -166,7 +231,28 @@ impl Highlight { /// /// If `None`, the original font should be unchanged. pub fn font(&self) -> Option { - None + self.0.font_style.and_then(|style| { + let bold = style.contains(highlighting::FontStyle::BOLD); + let italic = style.contains(highlighting::FontStyle::ITALIC); + + if bold || italic { + Some(Font { + weight: if bold { + font::Weight::Bold + } else { + font::Weight::Normal + }, + style: if italic { + font::Style::Italic + } else { + font::Style::Normal + }, + ..Font::MONOSPACE + }) + } else { + None + } + }) } /// Returns the [`Format`] of the [`Highlight`]. diff --git a/renderer/Cargo.toml b/renderer/Cargo.toml index 458681dd..ac223f16 100644 --- a/renderer/Cargo.toml +++ b/renderer/Cargo.toml @@ -22,6 +22,7 @@ geometry = ["iced_graphics/geometry", "iced_tiny_skia?/geometry", "iced_wgpu?/ge web-colors = ["iced_wgpu?/web-colors"] webgl = ["iced_wgpu?/webgl"] fira-sans = ["iced_graphics/fira-sans"] +strict-assertions = ["iced_wgpu?/strict-assertions"] [dependencies] iced_graphics.workspace = true diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index a0f4f40e..c6fbcff6 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -3,7 +3,7 @@ use crate::core::image; use crate::core::renderer; use crate::core::svg; use crate::core::{ - self, Background, Color, Point, Radians, Rectangle, Size, Transformation, + self, Background, Color, Image, Point, Rectangle, Size, Svg, Transformation, }; use crate::graphics; use crate::graphics::compositor; @@ -74,10 +74,10 @@ impl core::text::Renderer for Renderer where A: core::text::Renderer, B: core::text::Renderer< - Font = A::Font, - Paragraph = A::Paragraph, - Editor = A::Editor, - >, + Font = A::Font, + Paragraph = A::Paragraph, + Editor = A::Editor, + >, { type Font = A::Font; type Paragraph = A::Paragraph; @@ -149,25 +149,8 @@ where delegate!(self, renderer, renderer.measure_image(handle)) } - fn draw_image( - &mut self, - handle: Self::Handle, - filter_method: image::FilterMethod, - bounds: Rectangle, - rotation: Radians, - opacity: f32, - ) { - delegate!( - self, - renderer, - renderer.draw_image( - handle, - filter_method, - bounds, - rotation, - opacity - ) - ); + fn draw_image(&mut self, image: Image, bounds: Rectangle) { + delegate!(self, renderer, renderer.draw_image(image, bounds)); } } @@ -180,19 +163,8 @@ where delegate!(self, renderer, renderer.measure_svg(handle)) } - fn draw_svg( - &mut self, - handle: svg::Handle, - color: Option, - bounds: Rectangle, - rotation: Radians, - opacity: f32, - ) { - delegate!( - self, - renderer, - renderer.draw_svg(handle, color, bounds, rotation, opacity) - ); + fn draw_svg(&mut self, svg: Svg, bounds: Rectangle) { + delegate!(self, renderer, renderer.draw_svg(svg, bounds)); } } @@ -378,31 +350,16 @@ where fn screenshot( &mut self, renderer: &mut Self::Renderer, - surface: &mut Self::Surface, viewport: &graphics::Viewport, background_color: Color, ) -> Vec { - match (self, renderer, surface) { - ( - Self::Primary(compositor), - Renderer::Primary(renderer), - Surface::Primary(surface), - ) => compositor.screenshot( - renderer, - surface, - viewport, - background_color, - ), - ( - Self::Secondary(compositor), - Renderer::Secondary(renderer), - Surface::Secondary(surface), - ) => compositor.screenshot( - renderer, - surface, - viewport, - background_color, - ), + match (self, renderer) { + (Self::Primary(compositor), Renderer::Primary(renderer)) => { + compositor.screenshot(renderer, viewport, background_color) + } + (Self::Secondary(compositor), Renderer::Secondary(renderer)) => { + compositor.screenshot(renderer, viewport, background_color) + } _ => unreachable!(), } } @@ -435,9 +392,9 @@ where #[cfg(feature = "geometry")] mod geometry { use super::Renderer; - use crate::core::{Point, Radians, Rectangle, Size, Vector}; + use crate::core::{Point, Radians, Rectangle, Size, Svg, Vector}; use crate::graphics::cache::{self, Cached}; - use crate::graphics::geometry::{self, Fill, Path, Stroke, Text}; + use crate::graphics::geometry::{self, Fill, Image, Path, Stroke, Text}; impl geometry::Renderer for Renderer where @@ -562,10 +519,31 @@ mod geometry { delegate!(self, frame, frame.stroke(path, stroke)); } + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into>, + ) { + delegate!( + self, + frame, + frame.stroke_rectangle(top_left, size, stroke) + ); + } + fn fill_text(&mut self, text: impl Into) { delegate!(self, frame, frame.fill_text(text)); } + fn draw_image(&mut self, bounds: Rectangle, image: impl Into) { + delegate!(self, frame, frame.draw_image(bounds, image)); + } + + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into) { + delegate!(self, frame, frame.draw_svg(bounds, svg)); + } + fn push_transform(&mut self) { delegate!(self, frame, frame.push_transform()); } @@ -581,13 +559,13 @@ mod geometry { } } - fn paste(&mut self, frame: Self, at: Point) { + fn paste(&mut self, frame: Self) { match (self, frame) { (Self::Primary(target), Self::Primary(source)) => { - target.paste(source, at); + target.paste(source); } (Self::Secondary(target), Self::Secondary(source)) => { - target.paste(source, at); + target.paste(source); } _ => unreachable!(), } diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 056da5ed..ee20a458 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -23,6 +23,9 @@ pub type Compositor = renderer::Compositor; #[cfg(all(feature = "wgpu", feature = "tiny-skia"))] mod renderer { + use crate::core::renderer; + use crate::core::{Color, Font, Pixels, Size}; + pub type Renderer = crate::fallback::Renderer< iced_wgpu::Renderer, iced_tiny_skia::Renderer, @@ -32,6 +35,31 @@ mod renderer { iced_wgpu::window::Compositor, iced_tiny_skia::window::Compositor, >; + + impl renderer::Headless for Renderer { + fn new(default_font: Font, default_text_size: Pixels) -> Self { + Self::Secondary(iced_tiny_skia::Renderer::new( + default_font, + default_text_size, + )) + } + + fn screenshot( + &mut self, + size: Size, + scale_factor: f32, + background_color: Color, + ) -> Vec { + match self { + crate::fallback::Renderer::Primary(_) => unreachable!( + "iced_wgpu does not support headless mode yet!" + ), + crate::fallback::Renderer::Secondary(renderer) => { + renderer.screenshot(size, scale_factor, background_color) + } + } + } + } } #[cfg(all(feature = "wgpu", not(feature = "tiny-skia")))] @@ -48,6 +76,13 @@ mod renderer { #[cfg(not(any(feature = "wgpu", feature = "tiny-skia")))] mod renderer { + #[cfg(not(debug_assertions))] + compile_error!( + "Cannot compile `iced_renderer` in release mode \ + without a renderer feature enabled. \ + Enable either the `wgpu` or `tiny-skia` feature, or both." + ); + pub type Renderer = (); pub type Compositor = (); } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index dfe3a1b0..1aff0a10 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -13,9 +13,6 @@ keywords.workspace = true [lints] workspace = true -[features] -multi-window = [] - [dependencies] bytes.workspace = true iced_core.workspace = true @@ -24,5 +21,6 @@ iced_debug.workspace = true iced_futures.workspace = true iced_futures.features = ["thread-pool"] -thiserror.workspace = true raw-window-handle.workspace = true +sipper.workspace = true +thiserror.workspace = true diff --git a/runtime/src/clipboard.rs b/runtime/src/clipboard.rs index dd47c47d..a02cc011 100644 --- a/runtime/src/clipboard.rs +++ b/runtime/src/clipboard.rs @@ -1,80 +1,62 @@ //! Access the clipboard. -use crate::command::{self, Command}; use crate::core::clipboard::Kind; -use crate::futures::MaybeSend; +use crate::futures::futures::channel::oneshot; +use crate::task::{self, Task}; -use std::fmt; - -/// A clipboard action to be performed by some [`Command`]. +/// A clipboard action to be performed by some [`Task`]. /// -/// [`Command`]: crate::Command -pub enum Action { +/// [`Task`]: crate::Task +#[derive(Debug)] +pub enum Action { /// Read the clipboard and produce `T` with the result. - Read(Box) -> T>, Kind), + Read { + /// The clipboard target. + target: Kind, + /// The channel to send the read contents. + channel: oneshot::Sender>, + }, /// Write the given contents to the clipboard. - Write(String, Kind), -} - -impl Action { - /// Maps the output of a clipboard [`Action`] using the provided closure. - pub fn map( - self, - f: impl Fn(T) -> A + 'static + MaybeSend + Sync, - ) -> Action - where - T: 'static, - { - match self { - Self::Read(o, target) => { - Action::Read(Box::new(move |s| f(o(s))), target) - } - Self::Write(content, target) => Action::Write(content, target), - } - } -} - -impl fmt::Debug for Action { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Read(_, target) => write!(f, "Action::Read{target:?}"), - Self::Write(_, target) => write!(f, "Action::Write({target:?})"), - } - } + Write { + /// The clipboard target. + target: Kind, + /// The contents to be written. + contents: String, + }, } /// Read the current contents of the clipboard. -pub fn read( - f: impl Fn(Option) -> Message + 'static, -) -> Command { - Command::single(command::Action::Clipboard(Action::Read( - Box::new(f), - Kind::Standard, - ))) +pub fn read() -> Task> { + task::oneshot(|channel| { + crate::Action::Clipboard(Action::Read { + target: Kind::Standard, + channel, + }) + }) } /// Read the current contents of the primary clipboard. -pub fn read_primary( - f: impl Fn(Option) -> Message + 'static, -) -> Command { - Command::single(command::Action::Clipboard(Action::Read( - Box::new(f), - Kind::Primary, - ))) +pub fn read_primary() -> Task> { + task::oneshot(|channel| { + crate::Action::Clipboard(Action::Read { + target: Kind::Primary, + channel, + }) + }) } /// Write the given contents to the clipboard. -pub fn write(contents: String) -> Command { - Command::single(command::Action::Clipboard(Action::Write( +pub fn write(contents: String) -> Task { + task::effect(crate::Action::Clipboard(Action::Write { + target: Kind::Standard, contents, - Kind::Standard, - ))) + })) } /// Write the given contents to the primary clipboard. -pub fn write_primary(contents: String) -> Command { - Command::single(command::Action::Clipboard(Action::Write( +pub fn write_primary(contents: String) -> Task { + task::effect(crate::Action::Clipboard(Action::Write { + target: Kind::Primary, contents, - Kind::Primary, - ))) + })) } diff --git a/runtime/src/command.rs b/runtime/src/command.rs deleted file mode 100644 index f7a746fe..00000000 --- a/runtime/src/command.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! Run asynchronous actions. -mod action; - -pub use action::Action; - -use crate::core::widget; -use crate::futures::futures; -use crate::futures::MaybeSend; - -use futures::channel::mpsc; -use futures::Stream; -use std::fmt; -use std::future::Future; - -/// A set of asynchronous actions to be performed by some runtime. -#[must_use = "`Command` must be returned to runtime to take effect"] -pub struct Command(Internal>); - -#[derive(Debug)] -enum Internal { - None, - Single(T), - Batch(Vec), -} - -impl Command { - /// Creates an empty [`Command`]. - /// - /// In other words, a [`Command`] that does nothing. - pub const fn none() -> Self { - Self(Internal::None) - } - - /// Creates a [`Command`] that performs a single [`Action`]. - pub const fn single(action: Action) -> Self { - Self(Internal::Single(action)) - } - - /// Creates a [`Command`] that performs a [`widget::Operation`]. - pub fn widget(operation: impl widget::Operation + 'static) -> Self { - Self::single(Action::Widget(Box::new(operation))) - } - - /// Creates a [`Command`] that performs the action of the given future. - pub fn perform( - future: impl Future + 'static + MaybeSend, - f: impl FnOnce(A) -> T + 'static + MaybeSend, - ) -> Command { - use futures::FutureExt; - - Command::single(Action::Future(Box::pin(future.map(f)))) - } - - /// Creates a [`Command`] that runs the given stream to completion. - pub fn run( - stream: impl Stream + 'static + MaybeSend, - f: impl Fn(A) -> T + 'static + MaybeSend, - ) -> Command { - use futures::StreamExt; - - Command::single(Action::Stream(Box::pin(stream.map(f)))) - } - - /// Creates a [`Command`] that performs the actions of all the given - /// commands. - /// - /// Once this command is run, all the commands will be executed at once. - pub fn batch(commands: impl IntoIterator>) -> Self { - let mut batch = Vec::new(); - - for Command(command) in commands { - match command { - Internal::None => {} - Internal::Single(command) => batch.push(command), - Internal::Batch(commands) => batch.extend(commands), - } - } - - Self(Internal::Batch(batch)) - } - - /// Applies a transformation to the result of a [`Command`]. - pub fn map( - self, - f: impl Fn(T) -> A + 'static + MaybeSend + Sync + Clone, - ) -> Command - where - T: 'static, - A: 'static, - { - match self.0 { - Internal::None => Command::none(), - Internal::Single(action) => Command::single(action.map(f)), - Internal::Batch(batch) => Command(Internal::Batch( - batch - .into_iter() - .map(|action| action.map(f.clone())) - .collect(), - )), - } - } - - /// Returns all of the actions of the [`Command`]. - pub fn actions(self) -> Vec> { - let Command(command) = self; - - match command { - Internal::None => Vec::new(), - Internal::Single(action) => vec![action], - Internal::Batch(batch) => batch, - } - } -} - -impl From<()> for Command { - fn from(_value: ()) -> Self { - Self::none() - } -} - -impl fmt::Debug for Command { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let Command(command) = self; - - command.fmt(f) - } -} - -/// Creates a [`Command`] that produces the `Message`s published from a [`Future`] -/// to an [`mpsc::Sender`] with the given bounds. -pub fn channel( - size: usize, - f: impl FnOnce(mpsc::Sender) -> Fut + MaybeSend + 'static, -) -> Command -where - Fut: Future + MaybeSend + 'static, - Message: 'static + MaybeSend, -{ - use futures::future; - use futures::stream::{self, StreamExt}; - - let (sender, receiver) = mpsc::channel(size); - - let runner = stream::once(f(sender)).filter_map(|_| future::ready(None)); - - Command::single(Action::Stream(Box::pin(stream::select(receiver, runner)))) -} diff --git a/runtime/src/command/action.rs b/runtime/src/command/action.rs deleted file mode 100644 index c9ffe801..00000000 --- a/runtime/src/command/action.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::clipboard; -use crate::core::widget; -use crate::font; -use crate::futures::MaybeSend; -use crate::system; -use crate::window; - -use std::any::Any; -use std::borrow::Cow; -use std::fmt; - -/// An action that a [`Command`] can perform. -/// -/// [`Command`]: crate::Command -pub enum Action { - /// Run a [`Future`] to completion. - /// - /// [`Future`]: iced_futures::BoxFuture - Future(iced_futures::BoxFuture), - - /// Run a [`Stream`] to completion. - /// - /// [`Stream`]: iced_futures::BoxStream - Stream(iced_futures::BoxStream), - - /// Run a clipboard action. - Clipboard(clipboard::Action), - - /// Run a window action. - Window(window::Action), - - /// Run a system action. - System(system::Action), - - /// Run a widget action. - Widget(Box>), - - /// Load a font from its bytes. - LoadFont { - /// The bytes of the font to load. - bytes: Cow<'static, [u8]>, - - /// The message to produce when the font has been loaded. - tagger: Box) -> T>, - }, - - /// A custom action supported by a specific runtime. - Custom(Box), -} - -impl Action { - /// Applies a transformation to the result of a [`Command`]. - /// - /// [`Command`]: crate::Command - pub fn map( - self, - f: impl Fn(T) -> A + 'static + MaybeSend + Sync, - ) -> Action - where - A: 'static, - T: 'static, - { - use iced_futures::futures::{FutureExt, StreamExt}; - - match self { - Self::Future(future) => Action::Future(Box::pin(future.map(f))), - Self::Stream(stream) => Action::Stream(Box::pin(stream.map(f))), - Self::Clipboard(action) => Action::Clipboard(action.map(f)), - Self::Window(window) => Action::Window(window.map(f)), - Self::System(system) => Action::System(system.map(f)), - Self::Widget(operation) => { - Action::Widget(Box::new(widget::operation::map(operation, f))) - } - Self::LoadFont { bytes, tagger } => Action::LoadFont { - bytes, - tagger: Box::new(move |result| f(tagger(result))), - }, - Self::Custom(custom) => Action::Custom(custom), - } - } -} - -impl fmt::Debug for Action { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Future(_) => write!(f, "Action::Future"), - Self::Stream(_) => write!(f, "Action::Stream"), - Self::Clipboard(action) => { - write!(f, "Action::Clipboard({action:?})") - } - Self::Window(action) => { - write!(f, "Action::Window({action:?})") - } - Self::System(action) => write!(f, "Action::System({action:?})"), - Self::Widget(_action) => write!(f, "Action::Widget"), - Self::LoadFont { .. } => write!(f, "Action::LoadFont"), - Self::Custom(_) => write!(f, "Action::Custom"), - } - } -} diff --git a/runtime/src/font.rs b/runtime/src/font.rs index 15359694..2d73566d 100644 --- a/runtime/src/font.rs +++ b/runtime/src/font.rs @@ -1,7 +1,6 @@ //! Load and use fonts. -pub use iced_core::font::*; - -use crate::command::{self, Command}; +use crate::Action; +use crate::task::{self, Task}; use std::borrow::Cow; /// An error while loading a font. @@ -9,11 +8,9 @@ use std::borrow::Cow; pub enum Error {} /// Load a font from its bytes. -pub fn load( - bytes: impl Into>, -) -> Command> { - Command::single(command::Action::LoadFont { +pub fn load(bytes: impl Into>) -> Task> { + task::oneshot(|channel| Action::LoadFont { bytes: bytes.into(), - tagger: Box::new(std::convert::identity), + channel, }) } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index bea8e9a0..47bd92d9 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -4,29 +4,113 @@ //! //! `iced_runtime` takes [`iced_core`] and builds a native runtime on top of it. //! -//! [`iced_core`]: https://github.com/iced-rs/iced/tree/0.12/core +//! [`iced_core`]: https://github.com/iced-rs/iced/tree/0.13/core #![doc( html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod clipboard; -pub mod command; pub mod font; pub mod keyboard; pub mod overlay; -pub mod program; pub mod system; +pub mod task; pub mod user_interface; pub mod window; -#[cfg(feature = "multi-window")] -pub mod multi_window; - pub use iced_core as core; pub use iced_debug as debug; pub use iced_futures as futures; -pub use command::Command; -pub use font::Font; -pub use program::Program; +pub use task::Task; pub use user_interface::UserInterface; + +use crate::core::widget; +use crate::futures::futures::channel::oneshot; + +use std::borrow::Cow; +use std::fmt; + +/// An action that the iced runtime can perform. +pub enum Action { + /// Output some value. + Output(T), + + /// Load a font from its bytes. + LoadFont { + /// The bytes of the font to load. + bytes: Cow<'static, [u8]>, + /// The channel to send back the load result. + channel: oneshot::Sender>, + }, + + /// Run a widget operation. + Widget(Box), + + /// Run a clipboard action. + Clipboard(clipboard::Action), + + /// Run a window action. + Window(window::Action), + + /// Run a system action. + System(system::Action), + + /// Exits the runtime. + /// + /// This will normally close any application windows and + /// terminate the runtime loop. + Exit, +} + +impl Action { + /// Creates a new [`Action::Widget`] with the given [`widget::Operation`]. + pub fn widget(operation: impl widget::Operation + 'static) -> Self { + Self::Widget(Box::new(operation)) + } + + fn output(self) -> Result> { + match self { + Action::Output(output) => Ok(output), + Action::LoadFont { bytes, channel } => { + Err(Action::LoadFont { bytes, channel }) + } + Action::Widget(operation) => Err(Action::Widget(operation)), + Action::Clipboard(action) => Err(Action::Clipboard(action)), + Action::Window(action) => Err(Action::Window(action)), + Action::System(action) => Err(Action::System(action)), + Action::Exit => Err(Action::Exit), + } + } +} + +impl fmt::Debug for Action +where + T: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Action::Output(output) => write!(f, "Action::Output({output:?})"), + Action::LoadFont { .. } => { + write!(f, "Action::LoadFont") + } + Action::Widget { .. } => { + write!(f, "Action::Widget") + } + Action::Clipboard(action) => { + write!(f, "Action::Clipboard({action:?})") + } + Action::Window(_) => write!(f, "Action::Window"), + Action::System(action) => write!(f, "Action::System({action:?})"), + Action::Exit => write!(f, "Action::Exit"), + } + } +} + +/// Creates a [`Task`] that exits the iced runtime. +/// +/// This will normally close any application windows and +/// terminate the runtime loop. +pub fn exit() -> Task { + task::effect(Action::Exit) +} diff --git a/runtime/src/multi_window.rs b/runtime/src/multi_window.rs deleted file mode 100644 index 34a2c9f4..00000000 --- a/runtime/src/multi_window.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! A multi-window application. -use crate::core::text; -use crate::core::window; -use crate::core::{Element, Renderer}; -use crate::Command; - -/// The core of a user interface for a multi-window application following The Elm Architecture. -pub trait Program: Sized { - /// The graphics backend to use to draw the [`Program`]. - type Renderer: Renderer + text::Renderer; - - /// The type of __messages__ your [`Program`] will produce. - type Message: std::fmt::Debug + Send; - - /// The theme used to draw the [`Program`]. - type Theme; - - /// Handles a __message__ and updates the state of the [`Program`]. - /// - /// This is where you define your __update logic__. All the __messages__, - /// produced by either user interactions or commands, will be handled by - /// this method. - /// - /// Any [`Command`] returned will be executed immediately in the - /// background by shells. - fn update(&mut self, message: Self::Message) -> Command; - - /// Returns the widgets to display in the [`Program`] for the `window`. - /// - /// These widgets can produce __messages__ based on user interaction. - fn view( - &self, - window: window::Id, - ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; -} diff --git a/runtime/src/multi_window/program.rs b/runtime/src/multi_window/program.rs deleted file mode 100644 index 963a09d7..00000000 --- a/runtime/src/multi_window/program.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Build interactive programs using The Elm Architecture. -use crate::core::text; -use crate::core::window; -use crate::core::{Element, Renderer}; -use crate::Command; - -/// The core of a user interface for a multi-window application following The Elm Architecture. -pub trait Program: Sized { - /// The graphics backend to use to draw the [`Program`]. - type Renderer: Renderer + text::Renderer; - - /// The type of __messages__ your [`Program`] will produce. - type Message: std::fmt::Debug + Send; - - /// The theme used to draw the [`Program`]. - type Theme; - - /// Handles a __message__ and updates the state of the [`Program`]. - /// - /// This is where you define your __update logic__. All the __messages__, - /// produced by either user interactions or commands, will be handled by - /// this method. - /// - /// Any [`Command`] returned will be executed immediately in the - /// background by shells. - fn update(&mut self, message: Self::Message) -> Command; - - /// Returns the widgets to display in the [`Program`] for the `window`. - /// - /// These widgets can produce __messages__ based on user interaction. - fn view( - &self, - window: window::Id, - ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; -} diff --git a/runtime/src/multi_window/state.rs b/runtime/src/multi_window/state.rs deleted file mode 100644 index 21a4d9a9..00000000 --- a/runtime/src/multi_window/state.rs +++ /dev/null @@ -1,264 +0,0 @@ -//! The internal state of a multi-window [`Program`]. -use crate::core::event::{self, Event}; -use crate::core::mouse; -use crate::core::renderer; -use crate::core::widget::operation::{self, Operation}; -use crate::core::{Clipboard, Size}; -use crate::debug; -use crate::user_interface::{self, UserInterface}; -use crate::{Command, Program}; - -/// The execution state of a multi-window [`Program`]. It leverages caching, event -/// processing, and rendering primitive storage. -#[allow(missing_debug_implementations)] -pub struct State

-where - P: Program + 'static, -{ - program: P, - caches: Option>, - queued_events: Vec, - queued_messages: Vec, - mouse_interaction: mouse::Interaction, -} - -impl

State

-where - P: Program + 'static, -{ - /// Creates a new [`State`] with the provided [`Program`], initializing its - /// primitive with the given logical bounds and renderer. - pub fn new(program: P, bounds: Size, renderer: &mut P::Renderer) -> Self { - let user_interface = build_user_interface( - &program, - user_interface::Cache::default(), - renderer, - bounds, - ); - - let caches = Some(vec![user_interface.into_cache()]); - - State { - program, - caches, - queued_events: Vec::new(), - queued_messages: Vec::new(), - mouse_interaction: mouse::Interaction::None, - } - } - - /// Returns a reference to the [`Program`] of the [`State`]. - pub fn program(&self) -> &P { - &self.program - } - - /// Queues an event in the [`State`] for processing during an [`update`]. - /// - /// [`update`]: Self::update - pub fn queue_event(&mut self, event: Event) { - self.queued_events.push(event); - } - - /// Queues a message in the [`State`] for processing during an [`update`]. - /// - /// [`update`]: Self::update - pub fn queue_message(&mut self, message: P::Message) { - self.queued_messages.push(message); - } - - /// Returns whether the event queue of the [`State`] is empty or not. - pub fn is_queue_empty(&self) -> bool { - self.queued_events.is_empty() && self.queued_messages.is_empty() - } - - /// Returns the current [`mouse::Interaction`] of the [`State`]. - pub fn mouse_interaction(&self) -> mouse::Interaction { - self.mouse_interaction - } - - /// Processes all the queued events and messages, rebuilding and redrawing - /// the widgets of the linked [`Program`] if necessary. - /// - /// Returns a list containing the instances of [`Event`] that were not - /// captured by any widget, and the [`Command`] obtained from [`Program`] - /// after updating it, only if an update was necessary. - pub fn update( - &mut self, - bounds: Size, - cursor: mouse::Cursor, - renderer: &mut P::Renderer, - theme: &P::Theme, - style: &renderer::Style, - clipboard: &mut dyn Clipboard, - ) -> (Vec, Option>) { - let mut user_interfaces = build_user_interfaces( - &self.program, - self.caches.take().unwrap(), - renderer, - bounds, - ); - - let mut messages = Vec::new(); - - let uncaptured_events = user_interfaces.iter_mut().fold( - vec![], - |mut uncaptured_events, ui| { - let interact_timer = debug::interact_time(); - let (_, event_statuses) = ui.update( - &self.queued_events, - cursor, - renderer, - clipboard, - &mut messages, - ); - interact_timer.finish(); - - uncaptured_events.extend( - self.queued_events - .iter() - .zip(event_statuses) - .filter_map(|(event, status)| { - matches!(status, event::Status::Ignored) - .then_some(event) - }) - .cloned(), - ); - uncaptured_events - }, - ); - - self.queued_events.clear(); - messages.append(&mut self.queued_messages); - - let commands = if messages.is_empty() { - let draw_timer = debug::draw_time(); - for ui in &mut user_interfaces { - self.mouse_interaction = - ui.draw(renderer, theme, style, cursor); - } - drop(draw_timer); - - self.caches = Some( - user_interfaces - .drain(..) - .map(UserInterface::into_cache) - .collect(), - ); - - None - } else { - let temp_caches = user_interfaces - .drain(..) - .map(UserInterface::into_cache) - .collect(); - - drop(user_interfaces); - - let commands = Command::batch(messages.into_iter().map(|msg| { - debug::log_message(&msg); - - let update_timer = debug::update_time(); - let command = self.program.update(msg); - drop(update_timer); - - command - })); - - let mut user_interfaces = build_user_interfaces( - &self.program, - temp_caches, - renderer, - bounds, - ); - - let draw_timer = debug::draw_time(); - for ui in &mut user_interfaces { - self.mouse_interaction = - ui.draw(renderer, theme, style, cursor); - } - drop(draw_timer); - - self.caches = Some( - user_interfaces - .drain(..) - .map(UserInterface::into_cache) - .collect(), - ); - - Some(commands) - }; - - (uncaptured_events, commands) - } - - /// Applies widget [`Operation`]s to the [`State`]. - pub fn operate( - &mut self, - renderer: &mut P::Renderer, - operations: impl Iterator>>, - bounds: Size, - ) { - let mut user_interfaces = build_user_interfaces( - &self.program, - self.caches.take().unwrap(), - renderer, - bounds, - ); - - for operation in operations { - let mut current_operation = Some(operation); - - while let Some(mut operation) = current_operation.take() { - for ui in &mut user_interfaces { - ui.operate(renderer, operation.as_mut()); - } - - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(message) => { - self.queued_messages.push(message); - } - operation::Outcome::Chain(next) => { - current_operation = Some(next); - } - }; - } - } - - self.caches = Some( - user_interfaces - .drain(..) - .map(UserInterface::into_cache) - .collect(), - ); - } -} - -fn build_user_interfaces<'a, P: Program>( - program: &'a P, - mut caches: Vec, - renderer: &mut P::Renderer, - size: Size, -) -> Vec> { - caches - .drain(..) - .map(|cache| build_user_interface(program, cache, renderer, size)) - .collect() -} - -fn build_user_interface<'a, P: Program>( - program: &'a P, - cache: user_interface::Cache, - renderer: &mut P::Renderer, - size: Size, -) -> UserInterface<'a, P::Message, P::Theme, P::Renderer> { - let view_timer = debug::view_time(); - let view = program.view(); - drop(view_timer); - - let layout_timer = debug::layout_time(); - let user_interface = UserInterface::build(view, size, cache, renderer); - drop(layout_timer); - - user_interface -} diff --git a/runtime/src/overlay/nested.rs b/runtime/src/overlay/nested.rs index ddb9532b..38054d7b 100644 --- a/runtime/src/overlay/nested.rs +++ b/runtime/src/overlay/nested.rs @@ -131,13 +131,13 @@ where &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation, ) { fn recurse( element: &mut overlay::Element<'_, Message, Theme, Renderer>, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation, ) where Renderer: renderer::Renderer, { @@ -158,48 +158,47 @@ 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 { + ) { fn recurse( element: &mut overlay::Element<'_, Message, Theme, Renderer>, layout: Layout<'_>, - event: Event, + event: &Event, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> (event::Status, bool) + ) -> bool where Renderer: renderer::Renderer, { let mut layouts = layout.children(); if let Some(layout) = layouts.next() { - let (nested_status, nested_is_over) = - if let Some((mut nested, nested_layout)) = - element.overlay(layout, renderer).zip(layouts.next()) - { - recurse( - &mut nested, - nested_layout, - event.clone(), - cursor, - renderer, - clipboard, - shell, - ) - } else { - (event::Status::Ignored, false) - }; + let nested_is_over = if let Some((mut nested, nested_layout)) = + element.overlay(layout, renderer).zip(layouts.next()) + { + recurse( + &mut nested, + nested_layout, + event, + cursor, + renderer, + clipboard, + shell, + ) + } else { + false + }; - if matches!(nested_status, event::Status::Ignored) { + if shell.event_status() == event::Status::Ignored { let is_over = nested_is_over || cursor .position() @@ -212,30 +211,29 @@ where }) .unwrap_or_default(); - ( - element.on_event( - event, - layout, - if nested_is_over { - mouse::Cursor::Unavailable - } else { - cursor - }, - renderer, - clipboard, - shell, - ), - is_over, - ) + element.update( + event, + layout, + if nested_is_over { + mouse::Cursor::Unavailable + } else { + cursor + }, + renderer, + clipboard, + shell, + ); + + is_over } else { - (nested_status, nested_is_over) + nested_is_over } } else { - (event::Status::Ignored, false) + false } } - let (status, _) = recurse( + let _ = recurse( &mut self.overlay, layout, event, @@ -244,8 +242,6 @@ where clipboard, shell, ); - - status } /// Returns the current [`mouse::Interaction`] of the [`Nested`] overlay. diff --git a/runtime/src/program.rs b/runtime/src/program.rs deleted file mode 100644 index 0ea94d3b..00000000 --- a/runtime/src/program.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Build interactive programs using The Elm Architecture. -use crate::Command; - -use iced_core::text; -use iced_core::Element; - -mod state; - -pub use state::State; - -/// The core of a user interface application following The Elm Architecture. -pub trait Program: Sized { - /// The graphics backend to use to draw the [`Program`]. - type Renderer: text::Renderer; - - /// The theme used to draw the [`Program`]. - type Theme; - - /// The type of __messages__ your [`Program`] will produce. - type Message: std::fmt::Debug + Send; - - /// Handles a __message__ and updates the state of the [`Program`]. - /// - /// This is where you define your __update logic__. All the __messages__, - /// produced by either user interactions or commands, will be handled by - /// this method. - /// - /// Any [`Command`] returned will be executed immediately in the - /// background by shells. - fn update(&mut self, message: Self::Message) -> Command; - - /// Returns the widgets to display in the [`Program`]. - /// - /// These widgets can produce __messages__ based on user interaction. - fn view(&self) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; -} diff --git a/runtime/src/program/state.rs b/runtime/src/program/state.rs deleted file mode 100644 index 033fe018..00000000 --- a/runtime/src/program/state.rs +++ /dev/null @@ -1,224 +0,0 @@ -use crate::core::event::{self, Event}; -use crate::core::mouse; -use crate::core::renderer; -use crate::core::widget::operation::{self, Operation}; -use crate::core::window; -use crate::core::{Clipboard, Size}; -use crate::debug; -use crate::user_interface::{self, UserInterface}; -use crate::{Command, Program}; - -/// The execution state of a [`Program`]. It leverages caching, event -/// processing, and rendering primitive storage. -#[allow(missing_debug_implementations)] -pub struct State

-where - P: Program + 'static, -{ - program: P, - cache: Option, - queued_events: Vec, - queued_messages: Vec, - mouse_interaction: mouse::Interaction, -} - -impl

State

-where - P: Program + 'static, -{ - /// Creates a new [`State`] with the provided [`Program`], initializing its - /// primitive with the given logical bounds and renderer. - pub fn new( - mut program: P, - bounds: Size, - renderer: &mut P::Renderer, - ) -> Self { - let user_interface = build_user_interface( - &mut program, - user_interface::Cache::default(), - renderer, - bounds, - ); - - let cache = Some(user_interface.into_cache()); - - State { - program, - cache, - queued_events: Vec::new(), - queued_messages: Vec::new(), - mouse_interaction: mouse::Interaction::None, - } - } - - /// Returns a reference to the [`Program`] of the [`State`]. - pub fn program(&self) -> &P { - &self.program - } - - /// Queues an event in the [`State`] for processing during an [`update`]. - /// - /// [`update`]: Self::update - pub fn queue_event(&mut self, event: Event) { - self.queued_events.push(event); - } - - /// Queues a message in the [`State`] for processing during an [`update`]. - /// - /// [`update`]: Self::update - pub fn queue_message(&mut self, message: P::Message) { - self.queued_messages.push(message); - } - - /// Returns whether the event queue of the [`State`] is empty or not. - pub fn is_queue_empty(&self) -> bool { - self.queued_events.is_empty() && self.queued_messages.is_empty() - } - - /// Returns the current [`mouse::Interaction`] of the [`State`]. - pub fn mouse_interaction(&self) -> mouse::Interaction { - self.mouse_interaction - } - - /// Processes all the queued events and messages, rebuilding and redrawing - /// the widgets of the linked [`Program`] if necessary. - /// - /// Returns a list containing the instances of [`Event`] that were not - /// captured by any widget, and the [`Command`] obtained from [`Program`] - /// after updating it, only if an update was necessary. - pub fn update( - &mut self, - bounds: Size, - cursor: mouse::Cursor, - renderer: &mut P::Renderer, - theme: &P::Theme, - style: &renderer::Style, - clipboard: &mut dyn Clipboard, - ) -> (Vec, Option>) { - let mut user_interface = build_user_interface( - &mut self.program, - self.cache.take().unwrap(), - renderer, - bounds, - ); - - let interact_span = debug::interact(window::Id::MAIN); - let mut messages = Vec::new(); - - let (_, event_statuses) = user_interface.update( - &self.queued_events, - cursor, - renderer, - clipboard, - &mut messages, - ); - - let uncaptured_events = self - .queued_events - .iter() - .zip(event_statuses) - .filter_map(|(event, status)| { - matches!(status, event::Status::Ignored).then_some(event) - }) - .cloned() - .collect(); - - self.queued_events.clear(); - messages.append(&mut self.queued_messages); - interact_span.finish(); - - let command = if messages.is_empty() { - let draw_span = debug::draw(window::Id::MAIN); - self.mouse_interaction = - user_interface.draw(renderer, theme, style, cursor); - draw_span.finish(); - - self.cache = Some(user_interface.into_cache()); - - None - } else { - // When there are messages, we are forced to rebuild twice - // for now :^) - let temp_cache = user_interface.into_cache(); - - let commands = - Command::batch(messages.into_iter().map(|message| { - let update_span = debug::update(&message); - let command = self.program.update(message); - update_span.finish(); - - command - })); - - let mut user_interface = build_user_interface( - &mut self.program, - temp_cache, - renderer, - bounds, - ); - - let draw_spawn = debug::draw(window::Id::MAIN); - self.mouse_interaction = - user_interface.draw(renderer, theme, style, cursor); - draw_spawn.finish(); - - self.cache = Some(user_interface.into_cache()); - - Some(commands) - }; - - (uncaptured_events, command) - } - - /// Applies [`Operation`]s to the [`State`] - pub fn operate( - &mut self, - renderer: &mut P::Renderer, - operations: impl Iterator>>, - bounds: Size, - ) { - let mut user_interface = build_user_interface( - &mut self.program, - self.cache.take().unwrap(), - renderer, - bounds, - ); - - for operation in operations { - let mut current_operation = Some(operation); - - while let Some(mut operation) = current_operation.take() { - user_interface.operate(renderer, operation.as_mut()); - - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(message) => { - self.queued_messages.push(message); - } - operation::Outcome::Chain(next) => { - current_operation = Some(next); - } - }; - } - } - - self.cache = Some(user_interface.into_cache()); - } -} - -fn build_user_interface<'a, P: Program>( - program: &'a mut P, - cache: user_interface::Cache, - renderer: &mut P::Renderer, - size: Size, -) -> UserInterface<'a, P::Message, P::Theme, P::Renderer> { - let view_span = debug::view(window::Id::MAIN); - let view = program.view(); - view_span.finish(); - - let layout_span = debug::layout(window::Id::MAIN); - let user_interface = UserInterface::build(view, size, cache, renderer); - layout_span.finish(); - - user_interface -} diff --git a/runtime/src/system.rs b/runtime/src/system.rs index 61c8ff29..8b0ec2d8 100644 --- a/runtime/src/system.rs +++ b/runtime/src/system.rs @@ -1,6 +1,39 @@ //! Access the native system. -mod action; -mod information; +use crate::futures::futures::channel::oneshot; -pub use action::Action; -pub use information::Information; +/// An operation to be performed on the system. +#[derive(Debug)] +pub enum Action { + /// Query system information and produce `T` with the result. + QueryInformation(oneshot::Sender), +} + +/// Contains information about the system (e.g. system name, processor, memory, graphics adapter). +#[derive(Clone, Debug)] +pub struct Information { + /// The operating system name + pub system_name: Option, + /// Operating system kernel version + pub system_kernel: Option, + /// Long operating system version + /// + /// Examples: + /// - MacOS 10.15 Catalina + /// - Windows 10 Pro + /// - Ubuntu 20.04 LTS (Focal Fossa) + pub system_version: Option, + /// Short operating system version number + pub system_short_version: Option, + /// Detailed processor model information + pub cpu_brand: String, + /// The number of physical cores on the processor + pub cpu_cores: Option, + /// Total RAM size, in bytes + pub memory_total: u64, + /// Memory used by this process, in bytes + pub memory_used: Option, + /// Underlying graphics backend for rendering + pub graphics_backend: String, + /// Model information for the active graphics adapter + pub graphics_adapter: String, +} diff --git a/runtime/src/system/action.rs b/runtime/src/system/action.rs deleted file mode 100644 index dea9536f..00000000 --- a/runtime/src/system/action.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::system; - -use iced_futures::MaybeSend; -use std::fmt; - -/// An operation to be performed on the system. -pub enum Action { - /// Query system information and produce `T` with the result. - QueryInformation(Box>), -} - -pub trait Closure: Fn(system::Information) -> T + MaybeSend {} - -impl Closure for T where T: Fn(system::Information) -> O + MaybeSend {} - -impl Action { - /// Maps the output of a system [`Action`] using the provided closure. - pub fn map( - self, - f: impl Fn(T) -> A + 'static + MaybeSend + Sync, - ) -> Action - where - T: 'static, - { - match self { - Self::QueryInformation(o) => { - Action::QueryInformation(Box::new(move |s| f(o(s)))) - } - } - } -} - -impl fmt::Debug for Action { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::QueryInformation(_) => write!(f, "Action::QueryInformation"), - } - } -} diff --git a/runtime/src/system/information.rs b/runtime/src/system/information.rs deleted file mode 100644 index 0f78f5e9..00000000 --- a/runtime/src/system/information.rs +++ /dev/null @@ -1,29 +0,0 @@ -/// Contains informations about the system (e.g. system name, processor, memory, graphics adapter). -#[derive(Clone, Debug)] -pub struct Information { - /// The operating system name - pub system_name: Option, - /// Operating system kernel version - pub system_kernel: Option, - /// Long operating system version - /// - /// Examples: - /// - MacOS 10.15 Catalina - /// - Windows 10 Pro - /// - Ubuntu 20.04 LTS (Focal Fossa) - pub system_version: Option, - /// Short operating system version number - pub system_short_version: Option, - /// Detailed processor model information - pub cpu_brand: String, - /// The number of physical cores on the processor - pub cpu_cores: Option, - /// Total RAM size, in bytes - pub memory_total: u64, - /// Memory used by this process, in bytes - pub memory_used: Option, - /// Underlying graphics backend for rendering - pub graphics_backend: String, - /// Model information for the active graphics adapter - pub graphics_adapter: String, -} diff --git a/runtime/src/task.rs b/runtime/src/task.rs new file mode 100644 index 00000000..710be5d9 --- /dev/null +++ b/runtime/src/task.rs @@ -0,0 +1,405 @@ +//! Create runtime tasks. +use crate::Action; +use crate::core::widget; +use crate::futures::futures::channel::mpsc; +use crate::futures::futures::channel::oneshot; +use crate::futures::futures::future::{self, FutureExt}; +use crate::futures::futures::stream::{self, Stream, StreamExt}; +use crate::futures::{BoxStream, MaybeSend, boxed_stream}; + +use std::sync::Arc; + +#[doc(no_inline)] +pub use sipper::{Never, Sender, Sipper, Straw, sipper, stream}; + +/// A set of concurrent actions to be performed by the iced runtime. +/// +/// A [`Task`] _may_ produce a bunch of values of type `T`. +#[allow(missing_debug_implementations)] +#[must_use = "`Task` must be returned to the runtime to take effect; normally in your `update` or `new` functions."] +pub struct Task(Option>>); + +impl Task { + /// Creates a [`Task`] that does nothing. + pub fn none() -> Self { + Self(None) + } + + /// Creates a new [`Task`] that instantly produces the given value. + pub fn done(value: T) -> Self + where + T: MaybeSend + 'static, + { + Self::future(future::ready(value)) + } + + /// Creates a [`Task`] that runs the given [`Future`] to completion and maps its + /// output with the given closure. + pub fn perform( + future: impl Future + MaybeSend + 'static, + f: impl Fn(A) -> T + MaybeSend + 'static, + ) -> Self + where + T: MaybeSend + 'static, + A: MaybeSend + 'static, + { + Self::future(future.map(f)) + } + + /// Creates a [`Task`] that runs the given [`Stream`] to completion and maps each + /// item with the given closure. + pub fn run( + stream: impl Stream + MaybeSend + 'static, + f: impl Fn(A) -> T + MaybeSend + 'static, + ) -> Self + where + T: 'static, + { + Self::stream(stream.map(f)) + } + + /// Creates a [`Task`] that runs the given [`Sipper`] to completion, mapping + /// progress with the first closure and the output with the second one. + pub fn sip( + sipper: S, + on_progress: impl FnMut(S::Progress) -> T + MaybeSend + 'static, + on_output: impl FnOnce(::Output) -> T + MaybeSend + 'static, + ) -> Self + where + S: sipper::Core + MaybeSend + 'static, + T: MaybeSend + 'static, + { + Self::stream(stream(sipper::sipper(move |sender| async move { + on_output(sipper.with(on_progress).run(sender).await) + }))) + } + + /// Combines the given tasks and produces a single [`Task`] that will run all of them + /// in parallel. + pub fn batch(tasks: impl IntoIterator) -> Self + where + T: 'static, + { + Self(Some(boxed_stream(stream::select_all( + tasks.into_iter().filter_map(|task| task.0), + )))) + } + + /// Maps the output of a [`Task`] with the given closure. + pub fn map( + self, + mut f: impl FnMut(T) -> O + MaybeSend + 'static, + ) -> Task + where + T: MaybeSend + 'static, + O: MaybeSend + 'static, + { + self.then(move |output| Task::done(f(output))) + } + + /// Performs a new [`Task`] for every output of the current [`Task`] using the + /// given closure. + /// + /// This is the monadic interface of [`Task`]—analogous to [`Future`] and + /// [`Stream`]. + pub fn then( + self, + mut f: impl FnMut(T) -> Task + MaybeSend + 'static, + ) -> Task + where + T: MaybeSend + 'static, + O: MaybeSend + 'static, + { + Task(match self.0 { + None => None, + Some(stream) => { + Some(boxed_stream(stream.flat_map(move |action| { + match action.output() { + Ok(output) => f(output) + .0 + .unwrap_or_else(|| boxed_stream(stream::empty())), + Err(action) => { + boxed_stream(stream::once(async move { action })) + } + } + }))) + } + }) + } + + /// Chains a new [`Task`] to be performed once the current one finishes completely. + pub fn chain(self, task: Self) -> Self + where + T: 'static, + { + match self.0 { + None => task, + Some(first) => match task.0 { + None => Task(Some(first)), + Some(second) => Task(Some(boxed_stream(first.chain(second)))), + }, + } + } + + /// Creates a new [`Task`] that collects all the output of the current one into a [`Vec`]. + pub fn collect(self) -> Task> + where + T: MaybeSend + 'static, + { + match self.0 { + None => Task::done(Vec::new()), + Some(stream) => Task(Some(boxed_stream( + stream::unfold( + (stream, Some(Vec::new())), + move |(mut stream, outputs)| async move { + let mut outputs = outputs?; + + let Some(action) = stream.next().await else { + return Some(( + Some(Action::Output(outputs)), + (stream, None), + )); + }; + + match action.output() { + Ok(output) => { + outputs.push(output); + + Some((None, (stream, Some(outputs)))) + } + Err(action) => { + Some((Some(action), (stream, Some(outputs)))) + } + } + }, + ) + .filter_map(future::ready), + ))), + } + } + + /// Creates a new [`Task`] that discards the result of the current one. + /// + /// Useful if you only care about the side effects of a [`Task`]. + pub fn discard(self) -> Task + where + T: MaybeSend + 'static, + O: MaybeSend + 'static, + { + self.then(|_| Task::none()) + } + + /// Creates a new [`Task`] that can be aborted with the returned [`Handle`]. + pub fn abortable(self) -> (Self, Handle) + where + T: 'static, + { + match self.0 { + Some(stream) => { + let (stream, handle) = stream::abortable(stream); + + ( + Self(Some(boxed_stream(stream))), + Handle { + internal: InternalHandle::Manual(handle), + }, + ) + } + None => ( + Self(None), + Handle { + internal: InternalHandle::Manual( + stream::AbortHandle::new_pair().0, + ), + }, + ), + } + } + + /// Creates a new [`Task`] that runs the given [`Future`] and produces + /// its output. + pub fn future(future: impl Future + MaybeSend + 'static) -> Self + where + T: 'static, + { + Self::stream(stream::once(future)) + } + + /// Creates a new [`Task`] that runs the given [`Stream`] and produces + /// each of its items. + pub fn stream(stream: impl Stream + MaybeSend + 'static) -> Self + where + T: 'static, + { + Self(Some(boxed_stream(stream.map(Action::Output)))) + } +} + +/// A handle to a [`Task`] that can be used for aborting it. +#[derive(Debug, Clone)] +pub struct Handle { + internal: InternalHandle, +} + +#[derive(Debug, Clone)] +enum InternalHandle { + Manual(stream::AbortHandle), + AbortOnDrop(Arc), +} + +impl InternalHandle { + pub fn as_ref(&self) -> &stream::AbortHandle { + match self { + InternalHandle::Manual(handle) => handle, + InternalHandle::AbortOnDrop(handle) => handle.as_ref(), + } + } +} + +impl Handle { + /// Aborts the [`Task`] of this [`Handle`]. + pub fn abort(&self) { + self.internal.as_ref().abort(); + } + + /// Returns a new [`Handle`] that will call [`Handle::abort`] whenever + /// all of its instances are dropped. + /// + /// If a [`Handle`] is cloned, [`Handle::abort`] will only be called + /// once all of the clones are dropped. + /// + /// This can be really useful if you do not want to worry about calling + /// [`Handle::abort`] yourself. + pub fn abort_on_drop(self) -> Self { + match &self.internal { + InternalHandle::Manual(handle) => Self { + internal: InternalHandle::AbortOnDrop(Arc::new(handle.clone())), + }, + InternalHandle::AbortOnDrop(_) => self, + } + } + + /// Returns `true` if the [`Task`] of this [`Handle`] has been aborted. + pub fn is_aborted(&self) -> bool { + self.internal.as_ref().is_aborted() + } +} + +impl Drop for Handle { + fn drop(&mut self) { + if let InternalHandle::AbortOnDrop(handle) = &mut self.internal { + let handle = std::mem::replace( + handle, + Arc::new(stream::AbortHandle::new_pair().0), + ); + + if let Some(handle) = Arc::into_inner(handle) { + handle.abort(); + } + } + } +} + +impl Task> { + /// Executes a new [`Task`] after this one, only when it produces `Some` value. + /// + /// The value is provided to the closure to create the subsequent [`Task`]. + pub fn and_then( + self, + f: impl Fn(T) -> Task + MaybeSend + 'static, + ) -> Task + where + T: MaybeSend + 'static, + A: MaybeSend + 'static, + { + self.then(move |option| option.map_or_else(Task::none, &f)) + } +} + +impl Task> { + /// Executes a new [`Task`] after this one, only when it succeeds with an `Ok` value. + /// + /// The success value is provided to the closure to create the subsequent [`Task`]. + pub fn and_then( + self, + f: impl Fn(T) -> Task + MaybeSend + 'static, + ) -> Task + where + T: MaybeSend + 'static, + E: MaybeSend + 'static, + A: MaybeSend + 'static, + { + self.then(move |option| option.map_or_else(|_| Task::none(), &f)) + } +} + +impl From<()> for Task { + fn from(_value: ()) -> Self { + Self::none() + } +} + +/// Creates a new [`Task`] that runs the given [`widget::Operation`] and produces +/// its output. +pub fn widget(operation: impl widget::Operation + 'static) -> Task +where + T: Send + 'static, +{ + channel(move |sender| { + let operation = + widget::operation::map(Box::new(operation), move |value| { + let _ = sender.clone().try_send(value); + }); + + Action::Widget(Box::new(operation)) + }) +} + +/// Creates a new [`Task`] that executes the [`Action`] returned by the closure and +/// produces the value fed to the [`oneshot::Sender`]. +pub fn oneshot(f: impl FnOnce(oneshot::Sender) -> Action) -> Task +where + T: MaybeSend + 'static, +{ + let (sender, receiver) = oneshot::channel(); + + let action = f(sender); + + Task(Some(boxed_stream( + stream::once(async move { action }).chain( + receiver.into_stream().filter_map(|result| async move { + Some(Action::Output(result.ok()?)) + }), + ), + ))) +} + +/// Creates a new [`Task`] that executes the [`Action`] returned by the closure and +/// produces the values fed to the [`mpsc::Sender`]. +pub fn channel(f: impl FnOnce(mpsc::Sender) -> Action) -> Task +where + T: MaybeSend + 'static, +{ + let (sender, receiver) = mpsc::channel(1); + + let action = f(sender); + + Task(Some(boxed_stream( + stream::once(async move { action }) + .chain(receiver.map(|result| Action::Output(result))), + ))) +} + +/// Creates a new [`Task`] that executes the given [`Action`] and produces no output. +pub fn effect(action: impl Into>) -> Task { + let action = action.into(); + + Task(Some(boxed_stream(stream::once(async move { + action.output().expect_err("no output") + })))) +} + +/// Returns the underlying [`Stream`] of the [`Task`]. +pub fn into_stream(task: Task) -> Option>> { + task.0 +} diff --git a/runtime/src/user_interface.rs b/runtime/src/user_interface.rs index 006225ed..9b396c69 100644 --- a/runtime/src/user_interface.rs +++ b/runtime/src/user_interface.rs @@ -5,7 +5,9 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget; use crate::core::window; -use crate::core::{Clipboard, Element, Layout, Rectangle, Shell, Size, Vector}; +use crate::core::{ + Clipboard, Element, InputMethod, Layout, Rectangle, Shell, Size, Vector, +}; use crate::overlay; /// A set of interactive graphical elements with a specific [`Layout`]. @@ -19,7 +21,7 @@ use crate::overlay; /// The [`integration`] example uses a [`UserInterface`] to integrate Iced in an /// existing graphical application. /// -/// [`integration`]: https://github.com/iced-rs/iced/tree/0.12/examples/integration +/// [`integration`]: https://github.com/iced-rs/iced/tree/0.13/examples/integration #[allow(missing_debug_implementations)] pub struct UserInterface<'a, Message, Theme, Renderer> { root: Element<'a, Message, Theme, Renderer>, @@ -186,7 +188,8 @@ where use std::mem::ManuallyDrop; let mut outdated = false; - let mut redraw_request = None; + let mut redraw_request = window::RedrawRequest::Wait; + let mut input_method = InputMethod::Disabled; let mut manual_overlay = ManuallyDrop::new( self.root @@ -207,10 +210,10 @@ where let mut layout = overlay.layout(renderer, bounds); let mut event_statuses = Vec::new(); - for event in events.iter().cloned() { + for event in events { let mut shell = Shell::new(messages); - let event_status = overlay.on_event( + overlay.update( event, Layout::new(&layout), cursor, @@ -219,17 +222,9 @@ where &mut shell, ); - event_statuses.push(event_status); - - match (redraw_request, shell.redraw_request()) { - (None, Some(at)) => { - redraw_request = Some(at); - } - (Some(current), Some(new)) if new < current => { - redraw_request = Some(new); - } - _ => {} - } + event_statuses.push(shell.event_status()); + redraw_request = redraw_request.min(shell.redraw_request()); + input_method.merge(shell.input_method()); if shell.is_layout_invalid() { let _ = ManuallyDrop::into_inner(manual_overlay); @@ -299,7 +294,6 @@ where let event_statuses = events .iter() - .cloned() .zip(overlay_statuses) .map(|(event, overlay_status)| { if matches!(overlay_status, event::Status::Captured) { @@ -308,7 +302,7 @@ where let mut shell = Shell::new(messages); - let event_status = self.root.as_widget_mut().on_event( + self.root.as_widget_mut().update( &mut self.state, event, Layout::new(&self.base), @@ -319,19 +313,12 @@ where &viewport, ); - if matches!(event_status, event::Status::Captured) { + if shell.event_status() == event::Status::Captured { self.overlay = None; } - match (redraw_request, shell.redraw_request()) { - (None, Some(at)) => { - redraw_request = Some(at); - } - (Some(current), Some(new)) if new < current => { - redraw_request = Some(new); - } - _ => {} - } + redraw_request = redraw_request.min(shell.redraw_request()); + input_method.merge(shell.input_method()); shell.revalidate_layout(|| { self.base = self.root.as_widget().layout( @@ -347,7 +334,7 @@ where outdated = true; } - event_status.merge(overlay_status) + shell.event_status().merge(overlay_status) }) .collect(); @@ -355,7 +342,10 @@ where if outdated { State::Outdated } else { - State::Updated { redraw_request } + State::Updated { + redraw_request, + input_method, + } }, event_statuses, ) @@ -566,7 +556,7 @@ where pub fn operate( &mut self, renderer: &Renderer, - operation: &mut dyn widget::Operation, + operation: &mut dyn widget::Operation, ) { self.root.as_widget().operate( &mut self.state, @@ -636,7 +626,7 @@ impl Default for Cache { } /// The resulting state after updating a [`UserInterface`]. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub enum State { /// The [`UserInterface`] is outdated and needs to be rebuilt. Outdated, @@ -644,7 +634,9 @@ pub enum State { /// The [`UserInterface`] is up-to-date and can be reused without /// rebuilding. Updated { - /// The [`window::RedrawRequest`] when a redraw should be performed. - redraw_request: Option, + /// The [`window::RedrawRequest`] describing when a redraw should be performed. + redraw_request: window::RedrawRequest, + /// The current [`InputMethod`] strategy of the user interface. + input_method: InputMethod, }, } diff --git a/runtime/src/window.rs b/runtime/src/window.rs index e32465d3..ccd8721b 100644 --- a/runtime/src/window.rs +++ b/runtime/src/window.rs @@ -1,24 +1,182 @@ //! Build window-based GUI applications. -mod action; - -pub mod screenshot; - -pub use action::Action; -pub use screenshot::Screenshot; - -use crate::command::{self, Command}; use crate::core::time::Instant; use crate::core::window::{ - Event, Icon, Id, Level, Mode, Settings, UserAttention, + Direction, Event, Icon, Id, Level, Mode, Screenshot, Settings, + UserAttention, }; use crate::core::{Point, Size}; -use crate::futures::event; use crate::futures::Subscription; +use crate::futures::event; +use crate::futures::futures::channel::oneshot; +use crate::task::{self, Task}; pub use raw_window_handle; use raw_window_handle::WindowHandle; +/// An operation to be performed on some window. +#[allow(missing_debug_implementations)] +pub enum Action { + /// Opens a new window with some [`Settings`]. + Open(Id, Settings, oneshot::Sender), + + /// Close the window and exits the application. + Close(Id), + + /// Gets the [`Id`] of the oldest window. + GetOldest(oneshot::Sender>), + + /// Gets the [`Id`] of the latest window. + GetLatest(oneshot::Sender>), + + /// Move the window with the left mouse button until the button is + /// released. + /// + /// There's no guarantee that this will work unless the left mouse + /// button was pressed immediately before this function is called. + Drag(Id), + + /// Resize the window with the left mouse button until the button is + /// released. + /// + /// There's no guarantee that this will work unless the left mouse + /// button was pressed immediately before this function is called. + DragResize(Id, Direction), + + /// Resize the window to the given logical dimensions. + Resize(Id, Size), + + /// Get the current logical dimensions of the window. + GetSize(Id, oneshot::Sender), + + /// Get if the current window is maximized or not. + GetMaximized(Id, oneshot::Sender), + + /// Set the window to maximized or back + Maximize(Id, bool), + + /// Get if the current window is minimized or not. + /// + /// ## Platform-specific + /// - **Wayland:** Always `None`. + GetMinimized(Id, oneshot::Sender>), + + /// Set the window to minimized or back + Minimize(Id, bool), + + /// Get the current logical coordinates of the window. + GetPosition(Id, oneshot::Sender>), + + /// Get the current scale factor (DPI) of the window. + GetScaleFactor(Id, oneshot::Sender), + + /// Move the window to the given logical coordinates. + /// + /// Unsupported on Wayland. + Move(Id, Point), + + /// Change the [`Mode`] of the window. + SetMode(Id, Mode), + + /// Get the current [`Mode`] of the window. + GetMode(Id, oneshot::Sender), + + /// Toggle the window to maximized or back + ToggleMaximize(Id), + + /// Toggle whether window has decorations. + /// + /// ## Platform-specific + /// - **X11:** Not implemented. + /// - **Web:** Unsupported. + ToggleDecorations(Id), + + /// Request user attention to the window, this has no effect if the application + /// is already focused. How requesting for user attention manifests is platform dependent, + /// see [`UserAttention`] for details. + /// + /// Providing `None` will unset the request for user attention. Unsetting the request for + /// user attention might not be done automatically by the WM when the window receives input. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** Unsupported. + /// - **macOS:** `None` has no effect. + /// - **X11:** Requests for user attention must be manually cleared. + /// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect. + RequestUserAttention(Id, Option), + + /// Bring the window to the front and sets input focus. Has no effect if the window is + /// already in focus, minimized, or not visible. + /// + /// This method steals input focus from other applications. Do not use this method unless + /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive + /// user experience. + /// + /// ## Platform-specific + /// + /// - **Web / Wayland:** Unsupported. + GainFocus(Id), + + /// Change the window [`Level`]. + SetLevel(Id, Level), + + /// Show the system menu at cursor position. + /// + /// ## Platform-specific + /// Android / iOS / macOS / Orbital / Web / X11: Unsupported. + ShowSystemMenu(Id), + + /// Get the raw identifier unique to the window. + GetRawId(Id, oneshot::Sender), + + /// Change the window [`Icon`]. + /// + /// On Windows and X11, this is typically the small icon in the top-left + /// corner of the titlebar. + /// + /// ## Platform-specific + /// + /// - **Web / Wayland / macOS:** Unsupported. + /// + /// - **Windows:** Sets `ICON_SMALL`. The base size for a window icon is 16x16, but it's + /// recommended to account for screen scaling and pick a multiple of that, i.e. 32x32. + /// + /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That + /// said, it's usually in the same ballpark as on Windows. + SetIcon(Id, Icon), + + /// Runs the closure with the native window handle of the window with the given [`Id`]. + RunWithHandle(Id, Box) + Send>), + + /// Screenshot the viewport of the window. + Screenshot(Id, oneshot::Sender), + + /// Enables mouse passthrough for the given window. + /// + /// This disables mouse events for the window and passes mouse events + /// through to whatever window is underneath. + EnableMousePassthrough(Id), + + /// Disable mouse passthrough for the given window. + /// + /// This enables mouse events for the window and stops mouse events + /// from being passed to whatever is underneath. + DisableMousePassthrough(Id), + + /// Set the minimum inner window size. + SetMinSize(Id, Option), + + /// Set the maximum inner window size. + SetMaxSize(Id, Option), + + /// Set the window to be resizable or not. + SetResizable(Id, bool), + + /// Set the window size increment. + SetResizeIncrements(Id, Option), +} + /// Subscribes to the frames of the window of the running application. /// /// The resulting [`Subscription`] will produce items at a rate equal to the @@ -28,116 +186,204 @@ use raw_window_handle::WindowHandle; /// In any case, this [`Subscription`] is useful to smoothly draw application-driven /// animations without missing any frames. pub fn frames() -> Subscription { - event::listen_raw(|event, _status| match event { - crate::core::Event::Window(_, Event::RedrawRequested(at)) => Some(at), + event::listen_raw(|event, _status, _window| match event { + crate::core::Event::Window(Event::RedrawRequested(at)) => Some(at), _ => None, }) } -/// Spawns a new window with the given `settings`. -/// -/// Returns the new window [`Id`] alongside the [`Command`]. -pub fn spawn(settings: Settings) -> (Id, Command) { +/// Subscribes to all window events of the running application. +pub fn events() -> Subscription<(Id, Event)> { + event::listen_with(|event, _status, id| { + if let crate::core::Event::Window(event) = event { + Some((id, event)) + } else { + None + } + }) +} + +/// Subscribes to all [`Event::Opened`] occurrences in the running application. +pub fn open_events() -> Subscription { + event::listen_with(|event, _status, id| { + if let crate::core::Event::Window(Event::Opened { .. }) = event { + Some(id) + } else { + None + } + }) +} + +/// Subscribes to all [`Event::Closed`] occurrences in the running application. +pub fn close_events() -> Subscription { + event::listen_with(|event, _status, id| { + if let crate::core::Event::Window(Event::Closed) = event { + Some(id) + } else { + None + } + }) +} + +/// Subscribes to all [`Event::Resized`] occurrences in the running application. +pub fn resize_events() -> Subscription<(Id, Size)> { + event::listen_with(|event, _status, id| { + if let crate::core::Event::Window(Event::Resized(size)) = event { + Some((id, size)) + } else { + None + } + }) +} + +/// Subscribes to all [`Event::CloseRequested`] occurrences in the running application. +pub fn close_requests() -> Subscription { + event::listen_with(|event, _status, id| { + if let crate::core::Event::Window(Event::CloseRequested) = event { + Some(id) + } else { + None + } + }) +} + +/// Opens a new window with the given [`Settings`]; producing the [`Id`] +/// of the new window on completion. +pub fn open(settings: Settings) -> (Id, Task) { let id = Id::unique(); ( id, - Command::single(command::Action::Window(Action::Spawn(id, settings))), + task::oneshot(|channel| { + crate::Action::Window(Action::Open(id, settings, channel)) + }), ) } /// Closes the window with `id`. -pub fn close(id: Id) -> Command { - Command::single(command::Action::Window(Action::Close(id))) +pub fn close(id: Id) -> Task { + task::effect(crate::Action::Window(Action::Close(id))) +} + +/// Gets the window [`Id`] of the oldest window. +pub fn get_oldest() -> Task> { + task::oneshot(|channel| crate::Action::Window(Action::GetOldest(channel))) +} + +/// Gets the window [`Id`] of the latest window. +pub fn get_latest() -> Task> { + task::oneshot(|channel| crate::Action::Window(Action::GetLatest(channel))) } /// Begins dragging the window while the left mouse button is held. -pub fn drag(id: Id) -> Command { - Command::single(command::Action::Window(Action::Drag(id))) +pub fn drag(id: Id) -> Task { + task::effect(crate::Action::Window(Action::Drag(id))) +} + +/// Begins resizing the window while the left mouse button is held. +pub fn drag_resize(id: Id, direction: Direction) -> Task { + task::effect(crate::Action::Window(Action::DragResize(id, direction))) } /// Resizes the window to the given logical dimensions. -pub fn resize(id: Id, new_size: Size) -> Command { - Command::single(command::Action::Window(Action::Resize(id, new_size))) +pub fn resize(id: Id, new_size: Size) -> Task { + task::effect(crate::Action::Window(Action::Resize(id, new_size))) } -/// Fetches the window's size in logical dimensions. -pub fn fetch_size( - id: Id, - f: impl FnOnce(Size) -> Message + 'static, -) -> Command { - Command::single(command::Action::Window(Action::FetchSize(id, Box::new(f)))) +/// Set the window to be resizable or not. +pub fn set_resizable(id: Id, resizable: bool) -> Task { + task::effect(crate::Action::Window(Action::SetResizable(id, resizable))) } -/// Fetches if the window is maximized. -pub fn fetch_maximized( - id: Id, - f: impl FnOnce(bool) -> Message + 'static, -) -> Command { - Command::single(command::Action::Window(Action::FetchMaximized( - id, - Box::new(f), +/// Set the inner maximum size of the window. +pub fn set_max_size(id: Id, size: Option) -> Task { + task::effect(crate::Action::Window(Action::SetMaxSize(id, size))) +} + +/// Set the inner minimum size of the window. +pub fn set_min_size(id: Id, size: Option) -> Task { + task::effect(crate::Action::Window(Action::SetMinSize(id, size))) +} + +/// Set the window size increment. +/// +/// This is usually used by apps such as terminal emulators that need "blocky" resizing. +pub fn set_resize_increments(id: Id, increments: Option) -> Task { + task::effect(crate::Action::Window(Action::SetResizeIncrements( + id, increments, ))) } +/// Get the window's size in logical dimensions. +pub fn get_size(id: Id) -> Task { + task::oneshot(move |channel| { + crate::Action::Window(Action::GetSize(id, channel)) + }) +} + +/// Gets the maximized state of the window with the given [`Id`]. +pub fn get_maximized(id: Id) -> Task { + task::oneshot(move |channel| { + crate::Action::Window(Action::GetMaximized(id, channel)) + }) +} + /// Maximizes the window. -pub fn maximize(id: Id, maximized: bool) -> Command { - Command::single(command::Action::Window(Action::Maximize(id, maximized))) +pub fn maximize(id: Id, maximized: bool) -> Task { + task::effect(crate::Action::Window(Action::Maximize(id, maximized))) } -/// Fetches if the window is minimized. -pub fn fetch_minimized( - id: Id, - f: impl FnOnce(Option) -> Message + 'static, -) -> Command { - Command::single(command::Action::Window(Action::FetchMinimized( - id, - Box::new(f), - ))) +/// Gets the minimized state of the window with the given [`Id`]. +pub fn get_minimized(id: Id) -> Task> { + task::oneshot(move |channel| { + crate::Action::Window(Action::GetMinimized(id, channel)) + }) } /// Minimizes the window. -pub fn minimize(id: Id, minimized: bool) -> Command { - Command::single(command::Action::Window(Action::Minimize(id, minimized))) +pub fn minimize(id: Id, minimized: bool) -> Task { + task::effect(crate::Action::Window(Action::Minimize(id, minimized))) } -/// Fetches the current window position in logical coordinates. -pub fn fetch_position( - id: Id, - f: impl FnOnce(Option) -> Message + 'static, -) -> Command { - Command::single(command::Action::Window(Action::FetchPosition( - id, - Box::new(f), - ))) +/// Gets the position in logical coordinates of the window with the given [`Id`]. +pub fn get_position(id: Id) -> Task> { + task::oneshot(move |channel| { + crate::Action::Window(Action::GetPosition(id, channel)) + }) +} + +/// Gets the scale factor of the window with the given [`Id`]. +pub fn get_scale_factor(id: Id) -> Task { + task::oneshot(move |channel| { + crate::Action::Window(Action::GetScaleFactor(id, channel)) + }) } /// Moves the window to the given logical coordinates. -pub fn move_to(id: Id, position: Point) -> Command { - Command::single(command::Action::Window(Action::Move(id, position))) +pub fn move_to(id: Id, position: Point) -> Task { + task::effect(crate::Action::Window(Action::Move(id, position))) +} + +/// Gets the current [`Mode`] of the window. +pub fn get_mode(id: Id) -> Task { + task::oneshot(move |channel| { + crate::Action::Window(Action::GetMode(id, channel)) + }) } /// Changes the [`Mode`] of the window. -pub fn change_mode(id: Id, mode: Mode) -> Command { - Command::single(command::Action::Window(Action::ChangeMode(id, mode))) -} - -/// Fetches the current [`Mode`] of the window. -pub fn fetch_mode( - id: Id, - f: impl FnOnce(Mode) -> Message + 'static, -) -> Command { - Command::single(command::Action::Window(Action::FetchMode(id, Box::new(f)))) +pub fn set_mode(id: Id, mode: Mode) -> Task { + task::effect(crate::Action::Window(Action::SetMode(id, mode))) } /// Toggles the window to maximized or back. -pub fn toggle_maximize(id: Id) -> Command { - Command::single(command::Action::Window(Action::ToggleMaximize(id))) +pub fn toggle_maximize(id: Id) -> Task { + task::effect(crate::Action::Window(Action::ToggleMaximize(id))) } /// Toggles the window decorations. -pub fn toggle_decorations(id: Id) -> Command { - Command::single(command::Action::Window(Action::ToggleDecorations(id))) +pub fn toggle_decorations(id: Id) -> Task { + task::effect(crate::Action::Window(Action::ToggleDecorations(id))) } /// Request user attention to the window. This has no effect if the application @@ -146,11 +392,11 @@ pub fn toggle_decorations(id: Id) -> Command { /// /// Providing `None` will unset the request for user attention. Unsetting the request for /// user attention might not be done automatically by the WM when the window receives input. -pub fn request_user_attention( +pub fn request_user_attention( id: Id, user_attention: Option, -) -> Command { - Command::single(command::Action::Window(Action::RequestUserAttention( +) -> Task { + task::effect(crate::Action::Window(Action::RequestUserAttention( id, user_attention, ))) @@ -159,59 +405,77 @@ pub fn request_user_attention( /// Brings the window to the front and sets input focus. Has no effect if the window is /// already in focus, minimized, or not visible. /// -/// This [`Command`] steals input focus from other applications. Do not use this method unless +/// This [`Task`] steals input focus from other applications. Do not use this method unless /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive /// user experience. -pub fn gain_focus(id: Id) -> Command { - Command::single(command::Action::Window(Action::GainFocus(id))) +pub fn gain_focus(id: Id) -> Task { + task::effect(crate::Action::Window(Action::GainFocus(id))) } /// Changes the window [`Level`]. -pub fn change_level(id: Id, level: Level) -> Command { - Command::single(command::Action::Window(Action::ChangeLevel(id, level))) +pub fn set_level(id: Id, level: Level) -> Task { + task::effect(crate::Action::Window(Action::SetLevel(id, level))) } /// Show the [system menu] at cursor position. /// /// [system menu]: https://en.wikipedia.org/wiki/Common_menus_in_Microsoft_Windows#System_menu -pub fn show_system_menu(id: Id) -> Command { - Command::single(command::Action::Window(Action::ShowSystemMenu(id))) +pub fn show_system_menu(id: Id) -> Task { + task::effect(crate::Action::Window(Action::ShowSystemMenu(id))) } -/// Fetches an identifier unique to the window, provided by the underlying windowing system. This is +/// Gets an identifier unique to the window, provided by the underlying windowing system. This is /// not to be confused with [`Id`]. -pub fn fetch_id( - id: Id, - f: impl FnOnce(u64) -> Message + 'static, -) -> Command { - Command::single(command::Action::Window(Action::FetchId(id, Box::new(f)))) +pub fn get_raw_id(id: Id) -> Task { + task::oneshot(|channel| { + crate::Action::Window(Action::GetRawId(id, channel)) + }) } /// Changes the [`Icon`] of the window. -pub fn change_icon(id: Id, icon: Icon) -> Command { - Command::single(command::Action::Window(Action::ChangeIcon(id, icon))) +pub fn set_icon(id: Id, icon: Icon) -> Task { + task::effect(crate::Action::Window(Action::SetIcon(id, icon))) } /// Runs the given callback with the native window handle for the window with the given id. /// /// Note that if the window closes before this call is processed the callback will not be run. -pub fn run_with_handle( +pub fn run_with_handle( id: Id, - f: impl FnOnce(WindowHandle<'_>) -> Message + 'static, -) -> Command { - Command::single(command::Action::Window(Action::RunWithHandle( - id, - Box::new(f), - ))) + f: impl FnOnce(WindowHandle<'_>) -> T + Send + 'static, +) -> Task +where + T: Send + 'static, +{ + task::oneshot(move |channel| { + crate::Action::Window(Action::RunWithHandle( + id, + Box::new(move |handle| { + let _ = channel.send(f(handle)); + }), + )) + }) } /// Captures a [`Screenshot`] from the window. -pub fn screenshot( - id: Id, - f: impl FnOnce(Screenshot) -> Message + Send + 'static, -) -> Command { - Command::single(command::Action::Window(Action::Screenshot( - id, - Box::new(f), - ))) +pub fn screenshot(id: Id) -> Task { + task::oneshot(move |channel| { + crate::Action::Window(Action::Screenshot(id, channel)) + }) +} + +/// Enables mouse passthrough for the given window. +/// +/// This disables mouse events for the window and passes mouse events +/// through to whatever window is underneath. +pub fn enable_mouse_passthrough(id: Id) -> Task { + task::effect(crate::Action::Window(Action::EnableMousePassthrough(id))) +} + +/// Disable mouse passthrough for the given window. +/// +/// This enables mouse events for the window and stops mouse events +/// from being passed to whatever is underneath. +pub fn disable_mouse_passthrough(id: Id) -> Task { + task::effect(crate::Action::Window(Action::DisableMousePassthrough(id))) } diff --git a/runtime/src/window/action.rs b/runtime/src/window/action.rs deleted file mode 100644 index 07e77872..00000000 --- a/runtime/src/window/action.rs +++ /dev/null @@ -1,230 +0,0 @@ -use crate::core::window::{Icon, Id, Level, Mode, Settings, UserAttention}; -use crate::core::{Point, Size}; -use crate::futures::MaybeSend; -use crate::window::Screenshot; - -use raw_window_handle::WindowHandle; - -use std::fmt; - -/// An operation to be performed on some window. -pub enum Action { - /// Spawns a new window with some [`Settings`]. - Spawn(Id, Settings), - /// Close the window and exits the application. - Close(Id), - /// Move the window with the left mouse button until the button is - /// released. - /// - /// There’s no guarantee that this will work unless the left mouse - /// button was pressed immediately before this function is called. - Drag(Id), - /// Resize the window to the given logical dimensions. - Resize(Id, Size), - /// Fetch the current logical dimensions of the window. - FetchSize(Id, Box T + 'static>), - /// Fetch if the current window is maximized or not. - /// - /// ## Platform-specific - /// - **iOS / Android / Web:** Unsupported. - FetchMaximized(Id, Box T + 'static>), - /// Set the window to maximized or back - Maximize(Id, bool), - /// Fetch if the current window is minimized or not. - /// - /// ## Platform-specific - /// - **Wayland:** Always `None`. - /// - **iOS / Android / Web:** Unsupported. - FetchMinimized(Id, Box) -> T + 'static>), - /// Set the window to minimized or back - Minimize(Id, bool), - /// Fetch the current logical coordinates of the window. - FetchPosition(Id, Box) -> T + 'static>), - /// Move the window to the given logical coordinates. - /// - /// Unsupported on Wayland. - Move(Id, Point), - /// Change the [`Mode`] of the window. - ChangeMode(Id, Mode), - /// Fetch the current [`Mode`] of the window. - FetchMode(Id, Box T + 'static>), - /// Toggle the window to maximized or back - ToggleMaximize(Id), - /// Toggle whether window has decorations. - /// - /// ## Platform-specific - /// - **X11:** Not implemented. - /// - **Web:** Unsupported. - ToggleDecorations(Id), - /// Request user attention to the window, this has no effect if the application - /// is already focused. How requesting for user attention manifests is platform dependent, - /// see [`UserAttention`] for details. - /// - /// Providing `None` will unset the request for user attention. Unsetting the request for - /// user attention might not be done automatically by the WM when the window receives input. - /// - /// ## Platform-specific - /// - /// - **iOS / Android / Web:** Unsupported. - /// - **macOS:** `None` has no effect. - /// - **X11:** Requests for user attention must be manually cleared. - /// - **Wayland:** Requires `xdg_activation_v1` protocol, `None` has no effect. - RequestUserAttention(Id, Option), - /// Bring the window to the front and sets input focus. Has no effect if the window is - /// already in focus, minimized, or not visible. - /// - /// This method steals input focus from other applications. Do not use this method unless - /// you are certain that's what the user wants. Focus stealing can cause an extremely disruptive - /// user experience. - /// - /// ## Platform-specific - /// - /// - **Web / Wayland:** Unsupported. - GainFocus(Id), - /// Change the window [`Level`]. - ChangeLevel(Id, Level), - /// Show the system menu at cursor position. - /// - /// ## Platform-specific - /// Android / iOS / macOS / Orbital / Web / X11: Unsupported. - ShowSystemMenu(Id), - /// Fetch the raw identifier unique to the window. - FetchId(Id, Box T + 'static>), - /// Change the window [`Icon`]. - /// - /// On Windows and X11, this is typically the small icon in the top-left - /// corner of the titlebar. - /// - /// ## Platform-specific - /// - /// - **Web / Wayland / macOS:** Unsupported. - /// - /// - **Windows:** Sets `ICON_SMALL`. The base size for a window icon is 16x16, but it's - /// recommended to account for screen scaling and pick a multiple of that, i.e. 32x32. - /// - /// - **X11:** Has no universal guidelines for icon sizes, so you're at the whims of the WM. That - /// said, it's usually in the same ballpark as on Windows. - ChangeIcon(Id, Icon), - /// Runs the closure with the native window handle of the window with the given [`Id`]. - RunWithHandle(Id, Box) -> T + 'static>), - /// Screenshot the viewport of the window. - Screenshot(Id, Box T + 'static>), -} - -impl Action { - /// Maps the output of a window [`Action`] using the provided closure. - pub fn map( - self, - f: impl Fn(T) -> A + 'static + MaybeSend + Sync, - ) -> Action - where - T: 'static, - { - match self { - Self::Spawn(id, settings) => Action::Spawn(id, settings), - Self::Close(id) => Action::Close(id), - Self::Drag(id) => Action::Drag(id), - Self::Resize(id, size) => Action::Resize(id, size), - Self::FetchSize(id, o) => { - Action::FetchSize(id, Box::new(move |s| f(o(s)))) - } - Self::FetchMaximized(id, o) => { - Action::FetchMaximized(id, Box::new(move |s| f(o(s)))) - } - Self::Maximize(id, maximized) => Action::Maximize(id, maximized), - Self::FetchMinimized(id, o) => { - Action::FetchMinimized(id, Box::new(move |s| f(o(s)))) - } - Self::Minimize(id, minimized) => Action::Minimize(id, minimized), - Self::FetchPosition(id, o) => { - Action::FetchPosition(id, Box::new(move |s| f(o(s)))) - } - Self::Move(id, position) => Action::Move(id, position), - Self::ChangeMode(id, mode) => Action::ChangeMode(id, mode), - Self::FetchMode(id, o) => { - Action::FetchMode(id, Box::new(move |s| f(o(s)))) - } - Self::ToggleMaximize(id) => Action::ToggleMaximize(id), - Self::ToggleDecorations(id) => Action::ToggleDecorations(id), - Self::RequestUserAttention(id, attention_type) => { - Action::RequestUserAttention(id, attention_type) - } - Self::GainFocus(id) => Action::GainFocus(id), - Self::ChangeLevel(id, level) => Action::ChangeLevel(id, level), - Self::ShowSystemMenu(id) => Action::ShowSystemMenu(id), - Self::FetchId(id, o) => { - Action::FetchId(id, Box::new(move |s| f(o(s)))) - } - Self::ChangeIcon(id, icon) => Action::ChangeIcon(id, icon), - Self::RunWithHandle(id, o) => { - Action::RunWithHandle(id, Box::new(move |s| f(o(s)))) - } - Self::Screenshot(id, tag) => Action::Screenshot( - id, - Box::new(move |screenshot| f(tag(screenshot))), - ), - } - } -} - -impl fmt::Debug for Action { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Spawn(id, settings) => { - write!(f, "Action::Spawn({id:?}, {settings:?})") - } - Self::Close(id) => write!(f, "Action::Close({id:?})"), - Self::Drag(id) => write!(f, "Action::Drag({id:?})"), - Self::Resize(id, size) => { - write!(f, "Action::Resize({id:?}, {size:?})") - } - Self::FetchSize(id, _) => write!(f, "Action::FetchSize({id:?})"), - Self::FetchMaximized(id, _) => { - write!(f, "Action::FetchMaximized({id:?})") - } - Self::Maximize(id, maximized) => { - write!(f, "Action::Maximize({id:?}, {maximized})") - } - Self::FetchMinimized(id, _) => { - write!(f, "Action::FetchMinimized({id:?})") - } - Self::Minimize(id, minimized) => { - write!(f, "Action::Minimize({id:?}, {minimized}") - } - Self::FetchPosition(id, _) => { - write!(f, "Action::FetchPosition({id:?})") - } - Self::Move(id, position) => { - write!(f, "Action::Move({id:?}, {position})") - } - Self::ChangeMode(id, mode) => { - write!(f, "Action::SetMode({id:?}, {mode:?})") - } - Self::FetchMode(id, _) => write!(f, "Action::FetchMode({id:?})"), - Self::ToggleMaximize(id) => { - write!(f, "Action::ToggleMaximize({id:?})") - } - Self::ToggleDecorations(id) => { - write!(f, "Action::ToggleDecorations({id:?})") - } - Self::RequestUserAttention(id, _) => { - write!(f, "Action::RequestUserAttention({id:?})") - } - Self::GainFocus(id) => write!(f, "Action::GainFocus({id:?})"), - Self::ChangeLevel(id, level) => { - write!(f, "Action::ChangeLevel({id:?}, {level:?})") - } - Self::ShowSystemMenu(id) => { - write!(f, "Action::ShowSystemMenu({id:?})") - } - Self::FetchId(id, _) => write!(f, "Action::FetchId({id:?})"), - Self::ChangeIcon(id, _icon) => { - write!(f, "Action::ChangeIcon({id:?})") - } - Self::RunWithHandle(id, _) => { - write!(f, "Action::RunWithHandle({id:?})") - } - Self::Screenshot(id, _) => write!(f, "Action::Screenshot({id:?})"), - } - } -} diff --git a/rustfmt.toml b/rustfmt.toml index 501828b4..dccc2a06 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,2 +1,2 @@ max_width=80 -edition="2021" +edition="2024" diff --git a/src/advanced.rs b/src/advanced.rs index ed768b51..5a2ac990 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -1,5 +1,19 @@ //! Leverage advanced concepts like custom widgets. -pub use crate::application::Application; +pub mod subscription { + //! Write your own subscriptions. + pub use crate::runtime::futures::subscription::{ + Event, EventStream, Hasher, MacOS, PlatformSpecific, Recipe, + from_recipe, into_recipes, + }; +} + +pub mod widget { + //! Create custom widgets and operate on them. + pub use crate::core::widget::*; + pub use crate::runtime::task::widget as operate; +} + +pub use crate::core::Shell; pub use crate::core::clipboard::{self, Clipboard}; pub use crate::core::image; pub use crate::core::layout::{self, Layout}; @@ -8,14 +22,8 @@ pub use crate::core::overlay::{self, Overlay}; pub use crate::core::renderer::{self, Renderer}; pub use crate::core::svg; pub use crate::core::text::{self, Text}; -pub use crate::core::widget::{self, Widget}; -pub use crate::core::Shell; pub use crate::renderer::graphics; + pub use iced_debug as debug; -pub mod subscription { - //! Write your own subscriptions. - pub use crate::runtime::futures::subscription::{ - EventStream, Hasher, Recipe, - }; -} +pub use widget::Widget; diff --git a/src/application.rs b/src/application.rs index 84aacc07..c79ed62b 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,287 +1,485 @@ -//! Build interactive cross-platform applications. -use crate::core::text; -use crate::graphics::compositor; -use crate::shell::application; -use crate::{Command, Element, Executor, Settings, Subscription}; +//! Create and run iced applications step by step. +//! +//! # Example +//! ```no_run +//! use iced::widget::{button, column, text, Column}; +//! use iced::Theme; +//! +//! pub fn main() -> iced::Result { +//! iced::application("A counter", update, view) +//! .theme(|_| Theme::Dark) +//! .centered() +//! .run() +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! Increment, +//! } +//! +//! fn update(value: &mut u64, message: Message) { +//! match message { +//! Message::Increment => *value += 1, +//! } +//! } +//! +//! fn view(value: &u64) -> Column { +//! column![ +//! text(value), +//! button("+").on_press(Message::Increment), +//! ] +//! } +//! ``` +use crate::program::{self, Program}; +use crate::theme; +use crate::window; +use crate::{ + Element, Executor, Font, Result, Settings, Size, Subscription, Task, +}; -pub use application::{Appearance, DefaultStyle}; +use std::borrow::Cow; -/// An interactive cross-platform application. -/// -/// This trait is the main entrypoint of Iced. Once implemented, you can run -/// your GUI application by simply calling [`run`](#method.run). -/// -/// - On native platforms, it will run in its own window. -/// - On the web, it will take control of the `` and the `<body>` of the -/// document. -/// -/// An [`Application`] can execute asynchronous actions by returning a -/// [`Command`] in some of its methods. -/// -/// When using an [`Application`] with the `debug` feature enabled, a debug view -/// can be toggled by pressing `F12`. -/// -/// # Examples -/// [The repository has a bunch of examples] that use the [`Application`] trait: -/// -/// - [`clock`], an application that uses the [`Canvas`] widget to draw a clock -/// and its hands to display the current time. -/// - [`download_progress`], a basic application that asynchronously downloads -/// a dummy file of 100 MB and tracks the download progress. -/// - [`events`], a log of native events displayed using a conditional -/// [`Subscription`]. -/// - [`game_of_life`], an interactive version of the [Game of Life], invented -/// by [John Horton Conway]. -/// - [`pokedex`], an application that displays a random Pokédex entry (sprite -/// included!) by using the [PokéAPI]. -/// - [`solar_system`], an animated solar system drawn using the [`Canvas`] widget -/// and showcasing how to compose different transforms. -/// - [`stopwatch`], a watch with start/stop and reset buttons showcasing how -/// to listen to time. -/// - [`todos`], a todos tracker inspired by [TodoMVC]. -/// -/// [The repository has a bunch of examples]: https://github.com/iced-rs/iced/tree/0.12/examples -/// [`clock`]: https://github.com/iced-rs/iced/tree/0.12/examples/clock -/// [`download_progress`]: https://github.com/iced-rs/iced/tree/0.12/examples/download_progress -/// [`events`]: https://github.com/iced-rs/iced/tree/0.12/examples/events -/// [`game_of_life`]: https://github.com/iced-rs/iced/tree/0.12/examples/game_of_life -/// [`pokedex`]: https://github.com/iced-rs/iced/tree/0.12/examples/pokedex -/// [`solar_system`]: https://github.com/iced-rs/iced/tree/0.12/examples/solar_system -/// [`stopwatch`]: https://github.com/iced-rs/iced/tree/0.12/examples/stopwatch -/// [`todos`]: https://github.com/iced-rs/iced/tree/0.12/examples/todos -/// [`Sandbox`]: crate::Sandbox -/// [`Canvas`]: crate::widget::Canvas -/// [PokéAPI]: https://pokeapi.co/ -/// [TodoMVC]: http://todomvc.com/ -/// -/// ## A simple "Hello, world!" -/// -/// If you just want to get started, here is a simple [`Application`] that -/// says "Hello, world!": +/// Creates an iced [`Application`] given its title, update, and view logic. /// +/// # Example /// ```no_run -/// use iced::advanced::Application; -/// use iced::executor; -/// use iced::{Command, Element, Settings, Theme, Renderer}; +/// use iced::widget::{button, column, text, Column}; /// /// pub fn main() -> iced::Result { -/// Hello::run(Settings::default()) +/// iced::application("A counter", update, view).run() /// } /// -/// struct Hello; +/// #[derive(Debug, Clone)] +/// enum Message { +/// Increment, +/// } /// -/// impl Application for Hello { -/// type Executor = executor::Default; -/// type Flags = (); -/// type Message = (); -/// type Theme = Theme; -/// type Renderer = Renderer; -/// -/// fn new(_flags: ()) -> (Hello, Command<Self::Message>) { -/// (Hello, Command::none()) +/// fn update(value: &mut u64, message: Message) { +/// match message { +/// Message::Increment => *value += 1, /// } +/// } /// -/// fn title(&self) -> String { -/// String::from("A cool application") -/// } -/// -/// fn update(&mut self, _message: Self::Message) -> Command<Self::Message> { -/// Command::none() -/// } -/// -/// fn view(&self) -> Element<Self::Message> { -/// "Hello, world!".into() -/// } +/// fn view(value: &u64) -> Column<Message> { +/// column![ +/// text(value), +/// button("+").on_press(Message::Increment), +/// ] /// } /// ``` -pub trait Application: Sized +pub fn application<State, Message, Theme, Renderer>( + title: impl Title<State>, + update: impl Update<State, Message>, + view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, +) -> Application<impl Program<State = State, Message = Message, Theme = Theme>> where - Self::Theme: DefaultStyle, + State: 'static, + Message: Send + std::fmt::Debug + 'static, + Theme: Default + theme::Base, + Renderer: program::Renderer, { - /// The [`Executor`] that will run commands and subscriptions. - /// - /// The [default executor] can be a good starting point! - /// - /// [`Executor`]: Self::Executor - /// [default executor]: crate::executor::Default - type Executor: Executor; + use std::marker::PhantomData; - /// The type of __messages__ your [`Application`] will produce. - type Message: std::fmt::Debug + Send; - - /// The theme of your [`Application`]. - type Theme: Default; - - /// The renderer of your [`Application`]. - type Renderer: text::Renderer + compositor::Default; - - /// The data needed to initialize your [`Application`]. - type Flags; - - /// Returns the unique name of the [`Application`]. - fn name() -> &'static str { - std::any::type_name::<Self>() + struct Instance<State, Message, Theme, Renderer, Update, View> { + update: Update, + view: View, + _state: PhantomData<State>, + _message: PhantomData<Message>, + _theme: PhantomData<Theme>, + _renderer: PhantomData<Renderer>, } - /// Initializes the [`Application`] with the flags provided to - /// [`run`] as part of the [`Settings`]. - /// - /// Here is where you should return the initial state of your app. - /// - /// Additionally, you can return a [`Command`] if you need to perform some - /// async action in the background on startup. This is useful if you want to - /// load state from a file, perform an initial HTTP request, etc. - /// - /// [`run`]: Self::run - fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); + impl<State, Message, Theme, Renderer, Update, View> Program + for Instance<State, Message, Theme, Renderer, Update, View> + where + Message: Send + std::fmt::Debug + 'static, + Theme: Default + theme::Base, + Renderer: program::Renderer, + Update: self::Update<State, Message>, + View: for<'a> self::View<'a, State, Message, Theme, Renderer>, + { + type State = State; + type Message = Message; + type Theme = Theme; + type Renderer = Renderer; + type Executor = iced_futures::backend::default::Executor; - /// Returns the current title of the [`Application`]. - /// - /// This title can be dynamic! The runtime will automatically update the - /// title of your application when necessary. - fn title(&self) -> String; + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task<Self::Message> { + self.update.update(state, message).into() + } - /// Handles a __message__ and updates the state of the [`Application`]. - /// - /// This is where you define your __update logic__. All the __messages__, - /// produced by either user interactions or commands, will be handled by - /// this method. - /// - /// Any [`Command`] returned will be executed immediately in the background. - fn update(&mut self, message: Self::Message) -> Command<Self::Message>; - - /// Returns the widgets to display in the [`Application`]. - /// - /// These widgets can produce __messages__ based on user interaction. - fn view(&self) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; - - /// Returns the current [`Theme`] of the [`Application`]. - /// - /// [`Theme`]: Self::Theme - fn theme(&self) -> Self::Theme { - Self::Theme::default() + fn view<'a>( + &self, + state: &'a Self::State, + _window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.view.view(state).into() + } } - /// Returns the current [`Appearance`] of the [`Application`]. - fn style(&self, theme: &Self::Theme) -> Appearance { - theme.default_style() + Application { + raw: Instance { + update, + view, + _state: PhantomData, + _message: PhantomData, + _theme: PhantomData, + _renderer: PhantomData, + }, + settings: Settings::default(), + window: window::Settings::default(), } + .title(title) +} - /// Returns the event [`Subscription`] for the current state of the - /// application. - /// - /// A [`Subscription`] will be kept alive as long as you keep returning it, - /// and the __messages__ produced will be handled by - /// [`update`](#tymethod.update). - /// - /// By default, this method returns an empty [`Subscription`]. - fn subscription(&self) -> Subscription<Self::Message> { - Subscription::none() - } - - /// Returns the scale factor of the [`Application`]. - /// - /// It can be used to dynamically control the size of the UI at runtime - /// (i.e. zooming). - /// - /// For instance, a scale factor of `2.0` will make widgets twice as big, - /// while a scale factor of `0.5` will shrink them to half their size. - /// - /// By default, it returns `1.0`. - fn scale_factor(&self) -> f64 { - 1.0 - } +/// The underlying definition and configuration of an iced application. +/// +/// You can use this API to create and run iced applications +/// step by step—without coupling your logic to a trait +/// or a specific type. +/// +/// You can create an [`Application`] with the [`application`] helper. +#[derive(Debug)] +pub struct Application<P: Program> { + raw: P, + settings: Settings, + window: window::Settings, +} +impl<P: Program> Application<P> { /// Runs the [`Application`]. /// - /// On native platforms, this method will take control of the current thread - /// until the [`Application`] exits. + /// The state of the [`Application`] must implement [`Default`]. + /// If your state does not implement [`Default`], use [`run_with`] + /// instead. /// - /// On the web platform, this method __will NOT return__ unless there is an - /// [`Error`] during startup. - /// - /// [`Error`]: crate::Error - fn run(settings: Settings<Self::Flags>) -> crate::Result + /// [`run_with`]: Self::run_with + pub fn run(self) -> Result where Self: 'static, + P::State: Default, { - #[allow(clippy::needless_update)] - let renderer_settings = crate::graphics::Settings { - default_font: settings.default_font, - default_text_size: settings.default_text_size, - antialiasing: if settings.antialiasing { - Some(crate::graphics::Antialiasing::MSAAx4) - } else { - None + self.raw.run(self.settings, Some(self.window)) + } + + /// Runs the [`Application`] with a closure that creates the initial state. + pub fn run_with<I>(self, initialize: I) -> Result + where + Self: 'static, + I: FnOnce() -> (P::State, Task<P::Message>) + 'static, + { + self.raw + .run_with(self.settings, Some(self.window), initialize) + } + + /// Sets the [`Settings`] that will be used to run the [`Application`]. + pub fn settings(self, settings: Settings) -> Self { + Self { settings, ..self } + } + + /// Sets the [`Settings::antialiasing`] of the [`Application`]. + pub fn antialiasing(self, antialiasing: bool) -> Self { + Self { + settings: Settings { + antialiasing, + ..self.settings }, - ..crate::graphics::Settings::default() - }; + ..self + } + } - Ok(crate::shell::application::run::< - Instance<Self>, - Self::Executor, - <Self::Renderer as compositor::Default>::Compositor, - >(settings.into(), renderer_settings)?) + /// Sets the default [`Font`] of the [`Application`]. + pub fn default_font(self, default_font: Font) -> Self { + Self { + settings: Settings { + default_font, + ..self.settings + }, + ..self + } + } + + /// Adds a font to the list of fonts that will be loaded at the start of the [`Application`]. + pub fn font(mut self, font: impl Into<Cow<'static, [u8]>>) -> Self { + self.settings.fonts.push(font.into()); + self + } + + /// Sets the [`window::Settings`] of the [`Application`]. + /// + /// Overwrites any previous [`window::Settings`]. + pub fn window(self, window: window::Settings) -> Self { + Self { window, ..self } + } + + /// Sets the [`window::Settings::position`] to [`window::Position::Centered`] in the [`Application`]. + pub fn centered(self) -> Self { + Self { + window: window::Settings { + position: window::Position::Centered, + ..self.window + }, + ..self + } + } + + /// Sets the [`window::Settings::exit_on_close_request`] of the [`Application`]. + pub fn exit_on_close_request(self, exit_on_close_request: bool) -> Self { + Self { + window: window::Settings { + exit_on_close_request, + ..self.window + }, + ..self + } + } + + /// Sets the [`window::Settings::size`] of the [`Application`]. + pub fn window_size(self, size: impl Into<Size>) -> Self { + Self { + window: window::Settings { + size: size.into(), + ..self.window + }, + ..self + } + } + + /// Sets the [`window::Settings::transparent`] of the [`Application`]. + pub fn transparent(self, transparent: bool) -> Self { + Self { + window: window::Settings { + transparent, + ..self.window + }, + ..self + } + } + + /// Sets the [`window::Settings::resizable`] of the [`Application`]. + pub fn resizable(self, resizable: bool) -> Self { + Self { + window: window::Settings { + resizable, + ..self.window + }, + ..self + } + } + + /// Sets the [`window::Settings::decorations`] of the [`Application`]. + pub fn decorations(self, decorations: bool) -> Self { + Self { + window: window::Settings { + decorations, + ..self.window + }, + ..self + } + } + + /// Sets the [`window::Settings::position`] of the [`Application`]. + pub fn position(self, position: window::Position) -> Self { + Self { + window: window::Settings { + position, + ..self.window + }, + ..self + } + } + + /// Sets the [`window::Settings::level`] of the [`Application`]. + pub fn level(self, level: window::Level) -> Self { + Self { + window: window::Settings { + level, + ..self.window + }, + ..self + } + } + + /// Sets the [`Title`] of the [`Application`]. + pub(crate) fn title( + self, + title: impl Title<P::State>, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_title(self.raw, move |state, _window| { + title.title(state) + }), + settings: self.settings, + window: self.window, + } + } + + /// Sets the subscription logic of the [`Application`]. + pub fn subscription( + self, + f: impl Fn(&P::State) -> Subscription<P::Message>, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_subscription(self.raw, f), + settings: self.settings, + window: self.window, + } + } + + /// Sets the theme logic of the [`Application`]. + pub fn theme( + self, + f: impl Fn(&P::State) -> P::Theme, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_theme(self.raw, move |state, _window| f(state)), + settings: self.settings, + window: self.window, + } + } + + /// Sets the style logic of the [`Application`]. + pub fn style( + self, + f: impl Fn(&P::State, &P::Theme) -> theme::Style, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_style(self.raw, f), + settings: self.settings, + window: self.window, + } + } + + /// Sets the scale factor of the [`Application`]. + pub fn scale_factor( + self, + f: impl Fn(&P::State) -> f64, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Application { + raw: program::with_scale_factor(self.raw, move |state, _window| { + f(state) + }), + settings: self.settings, + window: self.window, + } + } + + /// Sets the executor of the [`Application`]. + pub fn executor<E>( + self, + ) -> Application< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > + where + E: Executor, + { + Application { + raw: program::with_executor::<P, E>(self.raw), + settings: self.settings, + window: self.window, + } } } -struct Instance<A>(A) -where - A: Application, - A::Theme: DefaultStyle; +/// The title logic of some [`Application`]. +/// +/// This trait is implemented both for `&static str` and +/// any closure `Fn(&State) -> String`. +/// +/// This trait allows the [`application`] builder to take any of them. +pub trait Title<State> { + /// Produces the title of the [`Application`]. + fn title(&self, state: &State) -> String; +} -impl<A> crate::runtime::Program for Instance<A> +impl<State> Title<State> for &'static str { + fn title(&self, _state: &State) -> String { + self.to_string() + } +} + +impl<T, State> Title<State> for T where - A: Application, - A::Theme: DefaultStyle, + T: Fn(&State) -> String, { - type Message = A::Message; - type Theme = A::Theme; - type Renderer = A::Renderer; - - fn update(&mut self, message: Self::Message) -> Command<Self::Message> { - self.0.update(message) - } - - fn view(&self) -> Element<'_, Self::Message, Self::Theme, Self::Renderer> { - self.0.view() + fn title(&self, state: &State) -> String { + self(state) } } -impl<A> application::Application for Instance<A> +/// The update logic of some [`Application`]. +/// +/// This trait allows the [`application`] builder to take any closure that +/// returns any `Into<Task<Message>>`. +pub trait Update<State, Message> { + /// Processes the message and updates the state of the [`Application`]. + fn update( + &self, + state: &mut State, + message: Message, + ) -> impl Into<Task<Message>>; +} + +impl<State, Message> Update<State, Message> for () { + fn update( + &self, + _state: &mut State, + _message: Message, + ) -> impl Into<Task<Message>> { + } +} + +impl<T, State, Message, C> Update<State, Message> for T where - A: Application, - A::Theme: DefaultStyle, + T: Fn(&mut State, Message) -> C, + C: Into<Task<Message>>, { - type Flags = A::Flags; - - fn name() -> &'static str { - A::name() - } - - fn new(flags: Self::Flags) -> (Self, Command<A::Message>) { - let (app, command) = A::new(flags); - - (Instance(app), command) - } - - fn title(&self) -> String { - self.0.title() - } - - fn theme(&self) -> A::Theme { - self.0.theme() - } - - fn style(&self, theme: &A::Theme) -> Appearance { - self.0.style(theme) - } - - fn subscription(&self) -> Subscription<Self::Message> { - self.0.subscription() - } - - fn scale_factor(&self) -> f64 { - self.0.scale_factor() + fn update( + &self, + state: &mut State, + message: Message, + ) -> impl Into<Task<Message>> { + self(state, message) + } +} + +/// The view logic of some [`Application`]. +/// +/// This trait allows the [`application`] builder to take any closure that +/// returns any `Into<Element<'_, Message>>`. +pub trait View<'a, State, Message, Theme, Renderer> { + /// Produces the widget of the [`Application`]. + fn view( + &self, + state: &'a State, + ) -> impl Into<Element<'a, Message, Theme, Renderer>>; +} + +impl<'a, T, State, Message, Theme, Renderer, Widget> + View<'a, State, Message, Theme, Renderer> for T +where + T: Fn(&'a State) -> Widget, + State: 'static, + Widget: Into<Element<'a, Message, Theme, Renderer>>, +{ + fn view( + &self, + state: &'a State, + ) -> impl Into<Element<'a, Message, Theme, Renderer>> { + self(state) } } diff --git a/src/daemon.rs b/src/daemon.rs new file mode 100644 index 00000000..fd6d0278 --- /dev/null +++ b/src/daemon.rs @@ -0,0 +1,295 @@ +//! Create and run daemons that run in the background. +use crate::application; +use crate::program::{self, Program}; +use crate::theme; +use crate::window; +use crate::{Element, Executor, Font, Result, Settings, Subscription, Task}; + +use std::borrow::Cow; + +/// Creates an iced [`Daemon`] given its title, update, and view logic. +/// +/// A [`Daemon`] will not open a window by default, but will run silently +/// instead until a [`Task`] from [`window::open`] is returned by its update logic. +/// +/// Furthermore, a [`Daemon`] will not stop running when all its windows are closed. +/// In order to completely terminate a [`Daemon`], its process must be interrupted or +/// its update logic must produce a [`Task`] from [`exit`]. +/// +/// [`exit`]: crate::exit +pub fn daemon<State, Message, Theme, Renderer>( + title: impl Title<State>, + update: impl application::Update<State, Message>, + view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, +) -> Daemon<impl Program<State = State, Message = Message, Theme = Theme>> +where + State: 'static, + Message: Send + std::fmt::Debug + 'static, + Theme: Default + theme::Base, + Renderer: program::Renderer, +{ + use std::marker::PhantomData; + + struct Instance<State, Message, Theme, Renderer, Update, View> { + update: Update, + view: View, + _state: PhantomData<State>, + _message: PhantomData<Message>, + _theme: PhantomData<Theme>, + _renderer: PhantomData<Renderer>, + } + + impl<State, Message, Theme, Renderer, Update, View> Program + for Instance<State, Message, Theme, Renderer, Update, View> + where + Message: Send + std::fmt::Debug + 'static, + Theme: Default + theme::Base, + Renderer: program::Renderer, + Update: application::Update<State, Message>, + View: for<'a> self::View<'a, State, Message, Theme, Renderer>, + { + type State = State; + type Message = Message; + type Theme = Theme; + type Renderer = Renderer; + type Executor = iced_futures::backend::default::Executor; + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task<Self::Message> { + self.update.update(state, message).into() + } + + fn view<'a>( + &self, + state: &'a Self::State, + window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.view.view(state, window).into() + } + } + + Daemon { + raw: Instance { + update, + view, + _state: PhantomData, + _message: PhantomData, + _theme: PhantomData, + _renderer: PhantomData, + }, + settings: Settings::default(), + } + .title(title) +} + +/// The underlying definition and configuration of an iced daemon. +/// +/// You can use this API to create and run iced applications +/// step by step—without coupling your logic to a trait +/// or a specific type. +/// +/// You can create a [`Daemon`] with the [`daemon`] helper. +#[derive(Debug)] +pub struct Daemon<P: Program> { + raw: P, + settings: Settings, +} + +impl<P: Program> Daemon<P> { + /// Runs the [`Daemon`]. + /// + /// The state of the [`Daemon`] must implement [`Default`]. + /// If your state does not implement [`Default`], use [`run_with`] + /// instead. + /// + /// [`run_with`]: Self::run_with + pub fn run(self) -> Result + where + Self: 'static, + P::State: Default, + { + self.raw.run(self.settings, None) + } + + /// Runs the [`Daemon`] with a closure that creates the initial state. + pub fn run_with<I>(self, initialize: I) -> Result + where + Self: 'static, + I: FnOnce() -> (P::State, Task<P::Message>) + 'static, + { + self.raw.run_with(self.settings, None, initialize) + } + + /// Sets the [`Settings`] that will be used to run the [`Daemon`]. + pub fn settings(self, settings: Settings) -> Self { + Self { settings, ..self } + } + + /// Sets the [`Settings::antialiasing`] of the [`Daemon`]. + pub fn antialiasing(self, antialiasing: bool) -> Self { + Self { + settings: Settings { + antialiasing, + ..self.settings + }, + ..self + } + } + + /// Sets the default [`Font`] of the [`Daemon`]. + pub fn default_font(self, default_font: Font) -> Self { + Self { + settings: Settings { + default_font, + ..self.settings + }, + ..self + } + } + + /// Adds a font to the list of fonts that will be loaded at the start of the [`Daemon`]. + pub fn font(mut self, font: impl Into<Cow<'static, [u8]>>) -> Self { + self.settings.fonts.push(font.into()); + self + } + + /// Sets the [`Title`] of the [`Daemon`]. + pub(crate) fn title( + self, + title: impl Title<P::State>, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_title(self.raw, move |state, window| { + title.title(state, window) + }), + settings: self.settings, + } + } + + /// Sets the subscription logic of the [`Daemon`]. + pub fn subscription( + self, + f: impl Fn(&P::State) -> Subscription<P::Message>, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_subscription(self.raw, f), + settings: self.settings, + } + } + + /// Sets the theme logic of the [`Daemon`]. + pub fn theme( + self, + f: impl Fn(&P::State, window::Id) -> P::Theme, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_theme(self.raw, f), + settings: self.settings, + } + } + + /// Sets the style logic of the [`Daemon`]. + pub fn style( + self, + f: impl Fn(&P::State, &P::Theme) -> theme::Style, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_style(self.raw, f), + settings: self.settings, + } + } + + /// Sets the scale factor of the [`Daemon`]. + pub fn scale_factor( + self, + f: impl Fn(&P::State, window::Id) -> f64, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > { + Daemon { + raw: program::with_scale_factor(self.raw, f), + settings: self.settings, + } + } + + /// Sets the executor of the [`Daemon`]. + pub fn executor<E>( + self, + ) -> Daemon< + impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, + > + where + E: Executor, + { + Daemon { + raw: program::with_executor::<P, E>(self.raw), + settings: self.settings, + } + } +} + +/// The title logic of some [`Daemon`]. +/// +/// This trait is implemented both for `&static str` and +/// any closure `Fn(&State, window::Id) -> String`. +/// +/// This trait allows the [`daemon`] builder to take any of them. +pub trait Title<State> { + /// Produces the title of the [`Daemon`]. + fn title(&self, state: &State, window: window::Id) -> String; +} + +impl<State> Title<State> for &'static str { + fn title(&self, _state: &State, _window: window::Id) -> String { + self.to_string() + } +} + +impl<T, State> Title<State> for T +where + T: Fn(&State, window::Id) -> String, +{ + fn title(&self, state: &State, window: window::Id) -> String { + self(state, window) + } +} + +/// The view logic of some [`Daemon`]. +/// +/// This trait allows the [`daemon`] builder to take any closure that +/// returns any `Into<Element<'_, Message>>`. +pub trait View<'a, State, Message, Theme, Renderer> { + /// Produces the widget of the [`Daemon`]. + fn view( + &self, + state: &'a State, + window: window::Id, + ) -> impl Into<Element<'a, Message, Theme, Renderer>>; +} + +impl<'a, T, State, Message, Theme, Renderer, Widget> + View<'a, State, Message, Theme, Renderer> for T +where + T: Fn(&'a State, window::Id) -> Widget, + State: 'static, + Widget: Into<Element<'a, Message, Theme, Renderer>>, +{ + fn view( + &self, + state: &'a State, + window: window::Id, + ) -> impl Into<Element<'a, Message, Theme, Renderer>> { + self(state, window) + } +} diff --git a/src/lib.rs b/src/lib.rs index 50ee7ecc..95820ed7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,170 +1,473 @@ -//! Iced is a cross-platform GUI library focused on simplicity and type-safety. +//! iced is a cross-platform GUI library focused on simplicity and type-safety. //! Inspired by [Elm]. //! -//! # Features -//! * Simple, easy-to-use, batteries-included API -//! * Type-safe, reactive programming model -//! * [Cross-platform support] (Windows, macOS, Linux, and the Web) -//! * Responsive layout -//! * Built-in widgets (including [text inputs], [scrollables], and more!) -//! * 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: -//! * A [renderer-agnostic native runtime] enabling integration with existing -//! systems -//! * A [built-in renderer] supporting Vulkan, Metal, DX11, and DX12 -//! * A [windowing shell] -//! * A [web runtime] leveraging the DOM +//! [Elm]: https://elm-lang.org/ //! -//! Check out the [repository] and the [examples] for more details! +//! # Disclaimer +//! iced is __experimental__ software. If you expect the documentation to hold your hand +//! as you learn the ropes, you are in for a frustrating experience. //! -//! [Cross-platform support]: https://github.com/iced-rs/iced/blob/master/docs/images/todos_desktop.jpg?raw=true -//! [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]: https://github.com/iced-rs/iced/blob/master/ECOSYSTEM.md -//! [renderer-agnostic native runtime]: https://github.com/iced-rs/iced/tree/0.12/runtime -//! [`wgpu`]: https://github.com/gfx-rs/wgpu-rs -//! [built-in renderer]: https://github.com/iced-rs/iced/tree/0.12/wgpu -//! [windowing shell]: https://github.com/iced-rs/iced/tree/0.12/winit -//! [`dodrio`]: https://github.com/fitzgen/dodrio -//! [web runtime]: https://github.com/iced-rs/iced_web -//! [examples]: https://github.com/iced-rs/iced/tree/0.12/examples -//! [repository]: https://github.com/iced-rs/iced +//! The library leverages Rust to its full extent: ownership, borrowing, lifetimes, futures, +//! streams, first-class functions, trait bounds, closures, and more. This documentation +//! is not meant to teach you any of these. Far from it, it will assume you have __mastered__ +//! all of them. //! -//! # Overview -//! Inspired by [The Elm Architecture], Iced expects you to split user -//! interfaces into four different concepts: +//! Furthermore—just like Rust—iced is very unforgiving. It will not let you easily cut corners. +//! The type signatures alone can be used to learn how to use most of the library. +//! Everything is connected. //! -//! * __State__ — the state of your application -//! * __Messages__ — user interactions or meaningful events that you care -//! about -//! * __View logic__ — a way to display your __state__ as widgets that -//! may produce __messages__ on user interaction -//! * __Update logic__ — a way to react to __messages__ and update your -//! __state__ +//! Therefore, iced is easy to learn for __advanced__ Rust programmers; but plenty of patient +//! beginners have learned it and had a good time with it. Since it leverages a lot of what +//! Rust has to offer in a type-safe way, it can be a great way to discover Rust itself. //! -//! We can build something to see how this works! Let's say we want a simple -//! counter that can be incremented and decremented using two buttons. +//! If you don't like the sound of that, you expect to be spoonfed, or you feel frustrated +//! and struggle to use the library; then I recommend you to wait patiently until [the book] +//! is finished. //! -//! We start by modelling the __state__ of our application: +//! [the book]: https://book.iced.rs //! -//! ``` -//! #[derive(Default)] -//! struct Counter { -//! // The counter value -//! value: i32, +//! # The Pocket Guide +//! Start by calling [`run`]: +//! +//! ```rust,no_run +//! pub fn main() -> iced::Result { +//! iced::run("A cool counter", update, view) //! } +//! # fn update(state: &mut (), message: ()) {} +//! # fn view(state: &()) -> iced::Element<()> { iced::widget::text("").into() } //! ``` //! -//! Next, we need to define the possible user interactions of our counter: -//! the button presses. These interactions are our __messages__: +//! Define an `update` function to __change__ your state: //! -//! ``` -//! #[derive(Debug, Clone, Copy)] -//! pub enum Message { -//! Increment, -//! Decrement, -//! } -//! ``` -//! -//! Now, let's show the actual counter by putting it all together in our -//! __view logic__: -//! -//! ``` -//! # struct Counter { -//! # // The counter value -//! # value: i32, -//! # } -//! # -//! # #[derive(Debug, Clone, Copy)] -//! # pub enum Message { -//! # Increment, -//! # Decrement, -//! # } -//! # -//! use iced::widget::{button, column, text, Column}; -//! -//! impl Counter { -//! pub fn view(&self) -> Column<Message> { -//! // We use a column: a simple vertical layout -//! column![ -//! // The increment button. We tell it to produce an -//! // `Increment` message when pressed -//! button("+").on_press(Message::Increment), -//! -//! // We show the value of the counter here -//! text(self.value).size(50), -//! -//! // The decrement button. We tell it to produce a -//! // `Decrement` message when pressed -//! button("-").on_press(Message::Decrement), -//! ] +//! ```rust +//! fn update(counter: &mut u64, message: Message) { +//! match message { +//! Message::Increment => *counter += 1, //! } //! } +//! # #[derive(Clone)] +//! # enum Message { Increment } //! ``` //! -//! Finally, we need to be able to react to any produced __messages__ and change -//! our __state__ accordingly in our __update logic__: +//! Define a `view` function to __display__ your state: //! +//! ```rust +//! use iced::widget::{button, text}; +//! use iced::Element; +//! +//! fn view(counter: &u64) -> Element<Message> { +//! button(text(counter)).on_press(Message::Increment).into() +//! } +//! # #[derive(Clone)] +//! # enum Message { Increment } //! ``` -//! # struct Counter { -//! # // The counter value -//! # value: i32, -//! # } -//! # -//! # #[derive(Debug, Clone, Copy)] -//! # pub enum Message { -//! # Increment, -//! # Decrement, -//! # } -//! impl Counter { -//! // ... //! -//! pub fn update(&mut self, message: Message) { -//! match message { -//! Message::Increment => { -//! self.value += 1; +//! And create a `Message` enum to __connect__ `view` and `update` together: +//! +//! ```rust +//! #[derive(Debug, Clone)] +//! enum Message { +//! Increment, +//! } +//! ``` +//! +//! ## Custom State +//! You can define your own struct for your state: +//! +//! ```rust +//! #[derive(Default)] +//! struct Counter { +//! value: u64, +//! } +//! ``` +//! +//! But you have to change `update` and `view` accordingly: +//! +//! ```rust +//! # struct Counter { value: u64 } +//! # #[derive(Clone)] +//! # enum Message { Increment } +//! # use iced::widget::{button, text}; +//! # use iced::Element; +//! fn update(counter: &mut Counter, message: Message) { +//! match message { +//! Message::Increment => counter.value += 1, +//! } +//! } +//! +//! fn view(counter: &Counter) -> Element<Message> { +//! button(text(counter.value)).on_press(Message::Increment).into() +//! } +//! ``` +//! +//! ## Widgets and Elements +//! The `view` function must return an [`Element`]. An [`Element`] is just a generic [`widget`]. +//! +//! The [`widget`] module contains a bunch of functions to help you build +//! and use widgets. +//! +//! Widgets are configured using the builder pattern: +//! +//! ```rust +//! # struct Counter { value: u64 } +//! # #[derive(Clone)] +//! # enum Message { Increment } +//! use iced::widget::{button, column, text}; +//! use iced::Element; +//! +//! fn view(counter: &Counter) -> Element<Message> { +//! column![ +//! text(counter.value).size(20), +//! button("Increment").on_press(Message::Increment), +//! ] +//! .spacing(10) +//! .into() +//! } +//! ``` +//! +//! A widget can be turned into an [`Element`] by calling `into`. +//! +//! Widgets and elements are generic over the message type they produce. The +//! [`Element`] returned by `view` must have the same `Message` type as +//! your `update`. +//! +//! ## Layout +//! There is no unified layout system in iced. Instead, each widget implements +//! its own layout strategy. +//! +//! Building your layout will often consist in using a combination of +//! [rows], [columns], and [containers]: +//! +//! ```rust +//! # struct State; +//! # enum Message {} +//! use iced::widget::{column, container, row}; +//! use iced::{Fill, Element}; +//! +//! fn view(state: &State) -> Element<Message> { +//! container( +//! column![ +//! "Top", +//! row!["Left", "Right"].spacing(10), +//! "Bottom" +//! ] +//! .spacing(10) +//! ) +//! .padding(10) +//! .center_x(Fill) +//! .center_y(Fill) +//! .into() +//! } +//! ``` +//! +//! Rows and columns lay out their children horizontally and vertically, +//! respectively. [Spacing] can be easily added between elements. +//! +//! Containers position or align a single widget inside their bounds. +//! +//! [rows]: widget::Row +//! [columns]: widget::Column +//! [containers]: widget::Container +//! [Spacing]: widget::Column::spacing +//! +//! ## Sizing +//! The width and height of widgets can generally be defined using a [`Length`]. +//! +//! - [`Fill`] will make the widget take all the available space in a given axis. +//! - [`Shrink`] will make the widget use its intrinsic size. +//! +//! Most widgets use a [`Shrink`] sizing strategy by default, but will inherit +//! a [`Fill`] strategy from their children. +//! +//! A fixed numeric [`Length`] in [`Pixels`] can also be used: +//! +//! ```rust +//! # struct State; +//! # enum Message {} +//! use iced::widget::container; +//! use iced::Element; +//! +//! fn view(state: &State) -> Element<Message> { +//! container("I am 300px tall!").height(300).into() +//! } +//! ``` +//! +//! ## Theming +//! The default [`Theme`] of an application can be changed by defining a `theme` +//! function and leveraging the [`Application`] builder, instead of directly +//! calling [`run`]: +//! +//! ```rust,no_run +//! # #[derive(Default)] +//! # struct State; +//! use iced::Theme; +//! +//! pub fn main() -> iced::Result { +//! iced::application("A cool application", update, view) +//! .theme(theme) +//! .run() +//! } +//! +//! fn theme(state: &State) -> Theme { +//! Theme::TokyoNight +//! } +//! # fn update(state: &mut State, message: ()) {} +//! # fn view(state: &State) -> iced::Element<()> { iced::widget::text("").into() } +//! ``` +//! +//! The `theme` function takes the current state of the application, allowing the +//! returned [`Theme`] to be completely dynamic—just like `view`. +//! +//! There are a bunch of built-in [`Theme`] variants at your disposal, but you can +//! also [create your own](Theme::custom). +//! +//! ## Styling +//! As with layout, iced does not have a unified styling system. However, all +//! of the built-in widgets follow the same styling approach. +//! +//! The appearance of a widget can be changed by calling its `style` method: +//! +//! ```rust +//! # struct State; +//! # enum Message {} +//! use iced::widget::container; +//! use iced::Element; +//! +//! fn view(state: &State) -> Element<Message> { +//! container("I am a rounded box!").style(container::rounded_box).into() +//! } +//! ``` +//! +//! The `style` method of a widget takes a closure that, given the current active +//! [`Theme`], returns the widget style: +//! +//! ```rust +//! # struct State; +//! # #[derive(Clone)] +//! # enum Message {} +//! use iced::widget::button; +//! use iced::{Element, Theme}; +//! +//! fn view(state: &State) -> Element<Message> { +//! button("I am a styled button!").style(|theme: &Theme, status| { +//! let palette = theme.extended_palette(); +//! +//! match status { +//! button::Status::Active => { +//! button::Style::default() +//! .with_background(palette.success.strong.color) //! } -//! Message::Decrement => { -//! self.value -= 1; +//! _ => button::primary(theme, status), +//! } +//! }) +//! .into() +//! } +//! ``` +//! +//! Widgets that can be in multiple different states will also provide the closure +//! with some [`Status`], allowing you to use a different style for each state. +//! +//! You can extract the [`Palette`] colors of a [`Theme`] with the [`palette`] or +//! [`extended_palette`] methods. +//! +//! Most widgets provide styling functions for your convenience in their respective modules; +//! like [`container::rounded_box`], [`button::primary`], or [`text::danger`]. +//! +//! [`Status`]: widget::button::Status +//! [`palette`]: Theme::palette +//! [`extended_palette`]: Theme::extended_palette +//! [`container::rounded_box`]: widget::container::rounded_box +//! [`button::primary`]: widget::button::primary +//! [`text::danger`]: widget::text::danger +//! +//! ## Concurrent Tasks +//! The `update` function can _optionally_ return a [`Task`]. +//! +//! A [`Task`] can be leveraged to perform asynchronous work, like running a +//! future or a stream: +//! +//! ```rust +//! # #[derive(Clone)] +//! # struct Weather; +//! use iced::Task; +//! +//! struct State { +//! weather: Option<Weather>, +//! } +//! +//! enum Message { +//! FetchWeather, +//! WeatherFetched(Weather), +//! } +//! +//! fn update(state: &mut State, message: Message) -> Task<Message> { +//! match message { +//! Message::FetchWeather => Task::perform( +//! fetch_weather(), +//! Message::WeatherFetched, +//! ), +//! Message::WeatherFetched(weather) => { +//! state.weather = Some(weather); +//! +//! Task::none() +//! } +//! } +//! } +//! +//! async fn fetch_weather() -> Weather { +//! // ... +//! # unimplemented!() +//! } +//! ``` +//! +//! Tasks can also be used to interact with the iced runtime. Some modules +//! expose functions that create tasks for different purposes—like [changing +//! window settings](window#functions), [focusing a widget](widget::focus_next), or +//! [querying its visible bounds](widget::container::visible_bounds). +//! +//! Like futures and streams, tasks expose [a monadic interface](Task::then)—but they can also be +//! [mapped](Task::map), [chained](Task::chain), [batched](Task::batch), [canceled](Task::abortable), +//! and more. +//! +//! ## Passive Subscriptions +//! Applications can subscribe to passive sources of data—like time ticks or runtime events. +//! +//! You will need to define a `subscription` function and use the [`Application`] builder: +//! +//! ```rust,no_run +//! # #[derive(Default)] +//! # struct State; +//! use iced::window; +//! use iced::{Size, Subscription}; +//! +//! #[derive(Debug)] +//! enum Message { +//! WindowResized(Size), +//! } +//! +//! pub fn main() -> iced::Result { +//! iced::application("A cool application", update, view) +//! .subscription(subscription) +//! .run() +//! } +//! +//! fn subscription(state: &State) -> Subscription<Message> { +//! window::resize_events().map(|(_id, size)| Message::WindowResized(size)) +//! } +//! # fn update(state: &mut State, message: Message) {} +//! # fn view(state: &State) -> iced::Element<Message> { iced::widget::text("").into() } +//! ``` +//! +//! A [`Subscription`] is [a _declarative_ builder of streams](Subscription#the-lifetime-of-a-subscription) +//! that are not allowed to end on their own. Only the `subscription` function +//! dictates the active subscriptions—just like `view` fully dictates the +//! visible widgets of your user interface, at every moment. +//! +//! As with tasks, some modules expose convenient functions that build a [`Subscription`] for you—like +//! [`time::every`] which can be used to listen to time, or [`keyboard::on_key_press`] which will notify you +//! of any key presses. But you can also create your own with [`Subscription::run`] and [`run_with`]. +//! +//! [`run_with`]: Subscription::run_with +//! +//! ## Scaling Applications +//! The `update`, `view`, and `Message` triplet composes very nicely. +//! +//! A common pattern is to leverage this composability to split an +//! application into different screens: +//! +//! ```rust +//! # mod contacts { +//! # use iced::{Element, Task}; +//! # pub struct Contacts; +//! # impl Contacts { +//! # pub fn update(&mut self, message: Message) -> Action { unimplemented!() } +//! # pub fn view(&self) -> Element<Message> { unimplemented!() } +//! # } +//! # #[derive(Debug)] +//! # pub enum Message {} +//! # pub enum Action { None, Run(Task<Message>), Chat(()) } +//! # } +//! # mod conversation { +//! # use iced::{Element, Task}; +//! # pub struct Conversation; +//! # impl Conversation { +//! # pub fn new(contact: ()) -> (Self, Task<Message>) { unimplemented!() } +//! # pub fn update(&mut self, message: Message) -> Task<Message> { unimplemented!() } +//! # pub fn view(&self) -> Element<Message> { unimplemented!() } +//! # } +//! # #[derive(Debug)] +//! # pub enum Message {} +//! # } +//! use contacts::Contacts; +//! use conversation::Conversation; +//! +//! use iced::{Element, Task}; +//! +//! struct State { +//! screen: Screen, +//! } +//! +//! enum Screen { +//! Contacts(Contacts), +//! Conversation(Conversation), +//! } +//! +//! enum Message { +//! Contacts(contacts::Message), +//! Conversation(conversation::Message) +//! } +//! +//! fn update(state: &mut State, message: Message) -> Task<Message> { +//! match message { +//! Message::Contacts(message) => { +//! if let Screen::Contacts(contacts) = &mut state.screen { +//! let action = contacts.update(message); +//! +//! match action { +//! contacts::Action::None => Task::none(), +//! contacts::Action::Run(task) => task.map(Message::Contacts), +//! contacts::Action::Chat(contact) => { +//! let (conversation, task) = Conversation::new(contact); +//! +//! state.screen = Screen::Conversation(conversation); +//! +//! task.map(Message::Conversation) +//! } +//! } +//! } else { +//! Task::none() +//! } +//! } +//! Message::Conversation(message) => { +//! if let Screen::Conversation(conversation) = &mut state.screen { +//! conversation.update(message).map(Message::Conversation) +//! } else { +//! Task::none() //! } //! } //! } //! } -//! ``` //! -//! And that's everything! We just wrote a whole user interface. Let's run it: -//! -//! ```no_run -//! # #[derive(Default)] -//! # struct Counter; -//! # impl Counter { -//! # fn update(&mut self, _message: ()) {} -//! # fn view(&self) -> iced::Element<()> { unimplemented!() } -//! # } -//! # -//! fn main() -> iced::Result { -//! iced::run("A cool counter", Counter::update, Counter::view) +//! fn view(state: &State) -> Element<Message> { +//! match &state.screen { +//! Screen::Contacts(contacts) => contacts.view().map(Message::Contacts), +//! Screen::Conversation(conversation) => conversation.view().map(Message::Conversation), +//! } //! } //! ``` //! -//! Iced will automatically: +//! The `update` method of a screen can return an `Action` enum that can be leveraged by the parent to +//! execute a task or transition to a completely different screen altogether. The variants of `Action` can +//! have associated data. For instance, in the example above, the `Conversation` screen is created when +//! `Contacts::update` returns an `Action::Chat` with the selected contact. //! -//! 1. Take the result of our __view logic__ and layout its widgets. -//! 1. Process events from our system and produce __messages__ for our -//! __update logic__. -//! 1. Draw the resulting user interface. +//! Effectively, this approach lets you "tell a story" to connect different screens together in a type safe +//! way. //! -//! # Usage -//! Use [`run`] or the [`program`] builder. -//! -//! [Elm]: https://elm-lang.org/ -//! [The Elm Architecture]: https://guide.elm-lang.org/architecture/ -//! [`program`]: program() +//! Furthermore, functor methods like [`Task::map`], [`Element::map`], and [`Subscription::map`] make composition +//! seamless. #![doc( - html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg" + html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/bdf0430880f5c29443f5f0a0ae4895866dfef4c6/docs/logo.svg" )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))] @@ -175,34 +478,51 @@ use iced_winit::core; use iced_winit::runtime; pub use iced_futures::futures; +pub use iced_futures::stream; #[cfg(feature = "highlighter")] pub use iced_highlighter as highlighter; -mod application; -mod error; +#[cfg(feature = "wgpu")] +pub use iced_renderer::wgpu::wgpu; -pub mod program; -pub mod settings; +mod error; +mod program; + +pub mod application; +pub mod daemon; pub mod time; pub mod window; #[cfg(feature = "advanced")] pub mod advanced; -#[cfg(feature = "multi-window")] -pub mod multi_window; - pub use crate::core::alignment; +pub use crate::core::animation; pub use crate::core::border; pub use crate::core::color; pub use crate::core::gradient; +pub use crate::core::padding; pub use crate::core::theme; pub use crate::core::{ - Alignment, Background, Border, Color, ContentFit, Degrees, Gradient, - Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size, - Theme, Transformation, Vector, + Alignment, Animation, Background, Border, Color, ContentFit, Degrees, + Function, Gradient, Length, Padding, Pixels, Point, Radians, Rectangle, + Rotation, Settings, Shadow, Size, Theme, Transformation, Vector, never, }; +pub use crate::runtime::exit; +pub use iced_futures::Subscription; + +pub use Alignment::Center; +pub use Length::{Fill, FillPortion, Shrink}; +pub use alignment::Horizontal::{Left, Right}; +pub use alignment::Vertical::{Bottom, Top}; + +pub mod task { + //! Create runtime tasks. + pub use crate::runtime::task::{ + Handle, Never, Sipper, Straw, Task, sipper, stream, + }; +} pub mod clipboard { //! Access the clipboard. @@ -236,8 +556,10 @@ pub mod font { pub mod event { //! Handle events of a user interface. - pub use crate::core::event::{Event, MacOS, PlatformSpecific, Status}; - pub use iced_futures::event::{listen, listen_raw, listen_with}; + pub use crate::core::event::{Event, Status}; + pub use iced_futures::event::{ + listen, listen_raw, listen_url, listen_with, + }; } pub mod keyboard { @@ -254,18 +576,6 @@ pub mod mouse { }; } -pub mod command { - //! Run asynchronous actions. - pub use crate::runtime::command::{channel, Command}; -} - -pub mod subscription { - //! Listen to external events in your application. - pub use iced_futures::subscription::{ - channel, run, run_with_id, unfold, Subscription, - }; -} - #[cfg(feature = "system")] pub mod system { //! Retrieve system information. @@ -310,15 +620,20 @@ pub mod widget { mod runtime {} } -pub use command::Command; +pub use application::Application; +pub use daemon::Daemon; pub use error::Error; pub use event::Event; pub use executor::Executor; pub use font::Font; pub use program::Program; pub use renderer::Renderer; -pub use settings::Settings; -pub use subscription::Subscription; +pub use task::Task; + +#[doc(inline)] +pub use application::application; +#[doc(inline)] +pub use daemon::daemon; /// A generic widget. /// @@ -330,15 +645,13 @@ pub type Element< Renderer = crate::Renderer, > = crate::core::Element<'a, Message, Theme, Renderer>; -/// The result of running a [`Program`]. +/// The result of running an iced program. pub type Result = std::result::Result<(), Error>; /// Runs a basic iced application with default [`Settings`] given its title, /// update, and view logic. /// -/// This is equivalent to chaining [`program`] with [`Program::run`]. -/// -/// [`program`]: program() +/// This is equivalent to chaining [`application()`] with [`Application::run`]. /// /// # Example /// ```no_run @@ -367,18 +680,16 @@ pub type Result = std::result::Result<(), Error>; /// } /// ``` pub fn run<State, Message, Theme, Renderer>( - title: impl program::Title<State> + 'static, - update: impl program::Update<State, Message> + 'static, - view: impl for<'a> program::View<'a, State, Message, Theme, Renderer> + 'static, + title: impl application::Title<State> + 'static, + update: impl application::Update<State, Message> + 'static, + view: impl for<'a> application::View<'a, State, Message, Theme, Renderer> + + 'static, ) -> Result where State: Default + 'static, Message: std::fmt::Debug + Send + 'static, - Theme: Default + program::DefaultStyle + 'static, + Theme: Default + theme::Base + 'static, Renderer: program::Renderer + 'static, { - program(title, update, view).run() + application(title, update, view).run() } - -#[doc(inline)] -pub use program::program; diff --git a/src/multi_window.rs b/src/multi_window.rs deleted file mode 100644 index 2320911e..00000000 --- a/src/multi_window.rs +++ /dev/null @@ -1,263 +0,0 @@ -//! Leverage multi-window support in your application. -use crate::window; -use crate::{Command, Element, Executor, Settings, Subscription}; - -pub use crate::application::{Appearance, DefaultStyle}; - -/// An interactive cross-platform multi-window application. -/// -/// This trait is the main entrypoint of Iced. Once implemented, you can run -/// your GUI application by simply calling [`run`](#method.run). -/// -/// - On native platforms, it will run in its own windows. -/// - On the web, it will take control of the `<title>` and the `<body>` of the -/// document and display only the contents of the `window::Id::MAIN` window. -/// -/// An [`Application`] can execute asynchronous actions by returning a -/// [`Command`] in some of its methods. -/// -/// When using an [`Application`] with the `debug` feature enabled, a debug view -/// can be toggled by pressing `F12`. -/// -/// # Examples -/// See the `examples/multi-window` example to see this multi-window `Application` trait in action. -/// -/// ## A simple "Hello, world!" -/// -/// If you just want to get started, here is a simple [`Application`] that -/// says "Hello, world!": -/// -/// ```no_run -/// use iced::{executor, window}; -/// use iced::{Command, Element, Settings, Theme}; -/// use iced::multi_window::{self, Application}; -/// -/// pub fn main() -> iced::Result { -/// Hello::run(Settings::default()) -/// } -/// -/// struct Hello; -/// -/// impl multi_window::Application for Hello { -/// type Executor = executor::Default; -/// type Flags = (); -/// type Message = (); -/// type Theme = Theme; -/// -/// fn new(_flags: ()) -> (Hello, Command<Self::Message>) { -/// (Hello, Command::none()) -/// } -/// -/// fn title(&self, _window: window::Id) -> String { -/// String::from("A cool application") -/// } -/// -/// fn update(&mut self, _message: Self::Message) -> Command<Self::Message> { -/// Command::none() -/// } -/// -/// fn view(&self, _window: window::Id) -> Element<Self::Message> { -/// "Hello, world!".into() -/// } -/// } -/// ``` -/// -/// [`Sandbox`]: crate::Sandbox -pub trait Application: Sized -where - Self::Theme: DefaultStyle, -{ - /// The [`Executor`] that will run commands and subscriptions. - /// - /// The [default executor] can be a good starting point! - /// - /// [`Executor`]: Self::Executor - /// [default executor]: crate::executor::Default - type Executor: Executor; - - /// The type of __messages__ your [`Application`] will produce. - type Message: std::fmt::Debug + Send; - - /// The theme of your [`Application`]. - type Theme: Default; - - /// The data needed to initialize your [`Application`]. - type Flags; - - /// Returns the unique name of the [`Application`]. - fn name() -> &'static str { - std::any::type_name::<Self>() - } - - /// Initializes the [`Application`] with the flags provided to - /// [`run`] as part of the [`Settings`]. - /// - /// Here is where you should return the initial state of your app. - /// - /// Additionally, you can return a [`Command`] if you need to perform some - /// async action in the background on startup. This is useful if you want to - /// load state from a file, perform an initial HTTP request, etc. - /// - /// [`run`]: Self::run - fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); - - /// Returns the current title of the `window` of the [`Application`]. - /// - /// This title can be dynamic! The runtime will automatically update the - /// title of your window when necessary. - fn title(&self, window: window::Id) -> String; - - /// Handles a __message__ and updates the state of the [`Application`]. - /// - /// This is where you define your __update logic__. All the __messages__, - /// produced by either user interactions or commands, will be handled by - /// this method. - /// - /// Any [`Command`] returned will be executed immediately in the background. - fn update(&mut self, message: Self::Message) -> Command<Self::Message>; - - /// Returns the widgets to display in the `window` of the [`Application`]. - /// - /// These widgets can produce __messages__ based on user interaction. - fn view( - &self, - window: window::Id, - ) -> Element<'_, Self::Message, Self::Theme, crate::Renderer>; - - /// Returns the current [`Theme`] of the `window` of the [`Application`]. - /// - /// [`Theme`]: Self::Theme - #[allow(unused_variables)] - fn theme(&self, window: window::Id) -> Self::Theme { - Self::Theme::default() - } - - /// Returns the current `Style` of the [`Theme`]. - /// - /// [`Theme`]: Self::Theme - fn style(&self, theme: &Self::Theme) -> Appearance { - Self::Theme::default_style(theme) - } - - /// Returns the event [`Subscription`] for the current state of the - /// application. - /// - /// A [`Subscription`] will be kept alive as long as you keep returning it, - /// and the __messages__ produced will be handled by - /// [`update`](#tymethod.update). - /// - /// By default, this method returns an empty [`Subscription`]. - fn subscription(&self) -> Subscription<Self::Message> { - Subscription::none() - } - - /// Returns the scale factor of the `window` of the [`Application`]. - /// - /// It can be used to dynamically control the size of the UI at runtime - /// (i.e. zooming). - /// - /// For instance, a scale factor of `2.0` will make widgets twice as big, - /// while a scale factor of `0.5` will shrink them to half their size. - /// - /// By default, it returns `1.0`. - #[allow(unused_variables)] - fn scale_factor(&self, window: window::Id) -> f64 { - 1.0 - } - - /// Runs the multi-window [`Application`]. - /// - /// On native platforms, this method will take control of the current thread - /// until the [`Application`] exits. - /// - /// On the web platform, this method __will NOT return__ unless there is an - /// [`Error`] during startup. - /// - /// [`Error`]: crate::Error - fn run(settings: Settings<Self::Flags>) -> crate::Result - where - Self: 'static, - { - #[allow(clippy::needless_update)] - let renderer_settings = crate::graphics::Settings { - default_font: settings.default_font, - default_text_size: settings.default_text_size, - antialiasing: if settings.antialiasing { - Some(crate::graphics::Antialiasing::MSAAx4) - } else { - None - }, - ..crate::graphics::Settings::default() - }; - - Ok(crate::shell::multi_window::run::< - Instance<Self>, - Self::Executor, - crate::renderer::Compositor, - >(settings.into(), renderer_settings)?) - } -} - -struct Instance<A>(A) -where - A: Application, - A::Theme: DefaultStyle; - -impl<A> crate::runtime::multi_window::Program for Instance<A> -where - A: Application, - A::Theme: DefaultStyle, -{ - type Message = A::Message; - type Theme = A::Theme; - type Renderer = crate::Renderer; - - fn update(&mut self, message: Self::Message) -> Command<Self::Message> { - self.0.update(message) - } - - fn view( - &self, - window: window::Id, - ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer> { - self.0.view(window) - } -} - -impl<A> crate::shell::multi_window::Application for Instance<A> -where - A: Application, - A::Theme: DefaultStyle, -{ - type Flags = A::Flags; - - fn new(flags: Self::Flags) -> (Self, Command<A::Message>) { - let (app, command) = A::new(flags); - - (Instance(app), command) - } - - fn name() -> &'static str { - A::name() - } - - fn title(&self, window: window::Id) -> String { - self.0.title(window) - } - - fn theme(&self, window: window::Id) -> A::Theme { - self.0.theme(window) - } - - fn style(&self, theme: &Self::Theme) -> Appearance { - self.0.style(theme) - } - - fn subscription(&self) -> Subscription<Self::Message> { - self.0.subscription() - } - - fn scale_factor(&self, window: window::Id) -> f64 { - self.0.scale_factor(window) - } -} diff --git a/src/program.rs b/src/program.rs index 7c7b0d37..ace4da74 100644 --- a/src/program.rs +++ b/src/program.rs @@ -1,437 +1,24 @@ -//! Create and run iced applications step by step. -//! -//! # Example -//! ```no_run -//! use iced::widget::{button, column, text, Column}; -//! use iced::Theme; -//! -//! pub fn main() -> iced::Result { -//! iced::program("A counter", update, view) -//! .theme(|_| Theme::Dark) -//! .centered() -//! .run() -//! } -//! -//! #[derive(Debug, Clone)] -//! enum Message { -//! Increment, -//! } -//! -//! fn update(value: &mut u64, message: Message) { -//! match message { -//! Message::Increment => *value += 1, -//! } -//! } -//! -//! fn view(value: &u64) -> Column<Message> { -//! column![ -//! text(value), -//! button("+").on_press(Message::Increment), -//! ] -//! } -//! ``` -use crate::application::Application; use crate::core::text; -use crate::executor::{self, Executor}; use crate::graphics::compositor; +use crate::shell; +use crate::theme; use crate::window; -use crate::{Command, Element, Font, Result, Settings, Size, Subscription}; - -pub use crate::application::{Appearance, DefaultStyle}; - -use std::borrow::Cow; - -/// Creates an iced [`Program`] given its title, update, and view logic. -/// -/// # Example -/// ```no_run -/// use iced::widget::{button, column, text, Column}; -/// -/// pub fn main() -> iced::Result { -/// iced::program("A counter", update, view).run() -/// } -/// -/// #[derive(Debug, Clone)] -/// enum Message { -/// Increment, -/// } -/// -/// fn update(value: &mut u64, message: Message) { -/// match message { -/// Message::Increment => *value += 1, -/// } -/// } -/// -/// fn view(value: &u64) -> Column<Message> { -/// column![ -/// text(value), -/// button("+").on_press(Message::Increment), -/// ] -/// } -/// ``` -pub fn program<State, Message, Theme, Renderer>( - title: impl Title<State>, - update: impl Update<State, Message>, - view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, -) -> Program<impl Definition<State = State, Message = Message, Theme = Theme>> -where - State: 'static, - Message: Send + std::fmt::Debug, - Theme: Default + DefaultStyle, - Renderer: self::Renderer, -{ - use std::marker::PhantomData; - - struct Application<State, Message, Theme, Renderer, Update, View> { - update: Update, - view: View, - _state: PhantomData<State>, - _message: PhantomData<Message>, - _theme: PhantomData<Theme>, - _renderer: PhantomData<Renderer>, - } - - impl<State, Message, Theme, Renderer, Update, View> Definition - for Application<State, Message, Theme, Renderer, Update, View> - where - Message: Send + std::fmt::Debug, - Theme: Default + DefaultStyle, - Renderer: self::Renderer, - Update: self::Update<State, Message>, - View: for<'a> self::View<'a, State, Message, Theme, Renderer>, - { - type State = State; - type Message = Message; - type Theme = Theme; - type Renderer = Renderer; - type Executor = executor::Default; - - fn name() -> &'static str { - std::any::type_name::<State>() - } - - fn load(&self) -> Command<Self::Message> { - Command::none() - } - - fn update( - &self, - state: &mut Self::State, - message: Self::Message, - ) -> Command<Self::Message> { - self.update.update(state, message).into() - } - - fn view<'a>( - &self, - state: &'a Self::State, - ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.view.view(state).into() - } - } - - Program { - raw: Application { - update, - view, - _state: PhantomData, - _message: PhantomData, - _theme: PhantomData, - _renderer: PhantomData, - }, - settings: Settings::default(), - } - .title(title) -} - -/// The underlying definition and configuration of an iced application. -/// -/// You can use this API to create and run iced applications -/// step by step—without coupling your logic to a trait -/// or a specific type. -/// -/// You can create a [`Program`] with the [`program`] helper. -/// -/// [`run`]: Program::run -#[derive(Debug)] -pub struct Program<P: Definition> { - raw: P, - settings: Settings, -} - -impl<P: Definition> Program<P> { - /// Runs the underlying [`Application`] of the [`Program`]. - /// - /// The state of the [`Program`] must implement [`Default`]. - /// If your state does not implement [`Default`], use [`run_with`] - /// instead. - /// - /// [`run_with`]: Self::run_with - pub fn run(self) -> Result - where - Self: 'static, - P::State: Default, - { - self.run_with(P::State::default) - } - - /// Runs the underlying [`Application`] of the [`Program`] with a - /// closure that creates the initial state. - pub fn run_with( - self, - initialize: impl Fn() -> P::State + Clone + 'static, - ) -> Result - where - Self: 'static, - { - use std::marker::PhantomData; - - struct Instance<P: Definition, I> { - program: P, - state: P::State, - _initialize: PhantomData<I>, - } - - impl<P: Definition, I: Fn() -> P::State> Application for Instance<P, I> { - type Message = P::Message; - type Theme = P::Theme; - type Renderer = P::Renderer; - type Flags = (P, I); - type Executor = P::Executor; - - fn new( - (program, initialize): Self::Flags, - ) -> (Self, Command<Self::Message>) { - let state = initialize(); - let command = program.load(); - - ( - Self { - program, - state, - _initialize: PhantomData, - }, - command, - ) - } - - fn name() -> &'static str { - P::name() - } - - fn title(&self) -> String { - self.program.title(&self.state) - } - - fn update( - &mut self, - message: Self::Message, - ) -> Command<Self::Message> { - self.program.update(&mut self.state, message) - } - - fn view( - &self, - ) -> crate::Element<'_, Self::Message, Self::Theme, Self::Renderer> - { - self.program.view(&self.state) - } - - fn subscription(&self) -> Subscription<Self::Message> { - self.program.subscription(&self.state) - } - - fn theme(&self) -> Self::Theme { - self.program.theme(&self.state) - } - - fn style(&self, theme: &Self::Theme) -> Appearance { - self.program.style(&self.state, theme) - } - } - - let Self { raw, settings } = self; - - Instance::run(Settings { - flags: (raw, initialize), - id: settings.id, - window: settings.window, - fonts: settings.fonts, - default_font: settings.default_font, - default_text_size: settings.default_text_size, - antialiasing: settings.antialiasing, - }) - } - - /// Sets the [`Settings`] that will be used to run the [`Program`]. - pub fn settings(self, settings: Settings) -> Self { - Self { settings, ..self } - } - - /// Sets the [`Settings::antialiasing`] of the [`Program`]. - pub fn antialiasing(self, antialiasing: bool) -> Self { - Self { - settings: Settings { - antialiasing, - ..self.settings - }, - ..self - } - } - - /// Sets the default [`Font`] of the [`Program`]. - pub fn default_font(self, default_font: Font) -> Self { - Self { - settings: Settings { - default_font, - ..self.settings - }, - ..self - } - } - - /// Adds a font to the list of fonts that will be loaded at the start of the [`Program`]. - pub fn font(mut self, font: impl Into<Cow<'static, [u8]>>) -> Self { - self.settings.fonts.push(font.into()); - self - } - - /// Sets the [`window::Settings::position`] to [`window::Position::Centered`] in the [`Program`]. - pub fn centered(self) -> Self { - Self { - settings: Settings { - window: window::Settings { - position: window::Position::Centered, - ..self.settings.window - }, - ..self.settings - }, - ..self - } - } - - /// Sets the [`window::Settings::exit_on_close_request`] of the [`Program`]. - pub fn exit_on_close_request(self, exit_on_close_request: bool) -> Self { - Self { - settings: Settings { - window: window::Settings { - exit_on_close_request, - ..self.settings.window - }, - ..self.settings - }, - ..self - } - } - - /// Sets the [`window::Settings::size`] of the [`Program`]. - pub fn window_size(self, size: impl Into<Size>) -> Self { - Self { - settings: Settings { - window: window::Settings { - size: size.into(), - ..self.settings.window - }, - ..self.settings - }, - ..self - } - } - - /// Sets the [`window::Settings::transparent`] of the [`Program`]. - pub fn transparent(self, transparent: bool) -> Self { - Self { - settings: Settings { - window: window::Settings { - transparent, - ..self.settings.window - }, - ..self.settings - }, - ..self - } - } - - /// Sets the [`Title`] of the [`Program`]. - pub(crate) fn title( - self, - title: impl Title<P::State>, - ) -> Program< - impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Program { - raw: with_title(self.raw, title), - settings: self.settings, - } - } - - /// Runs the [`Command`] produced by the closure at startup. - pub fn load( - self, - f: impl Fn() -> Command<P::Message>, - ) -> Program< - impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Program { - raw: with_load(self.raw, f), - settings: self.settings, - } - } - - /// Sets the subscription logic of the [`Program`]. - pub fn subscription( - self, - f: impl Fn(&P::State) -> Subscription<P::Message>, - ) -> Program< - impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Program { - raw: with_subscription(self.raw, f), - settings: self.settings, - } - } - - /// Sets the theme logic of the [`Program`]. - pub fn theme( - self, - f: impl Fn(&P::State) -> P::Theme, - ) -> Program< - impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Program { - raw: with_theme(self.raw, f), - settings: self.settings, - } - } - - /// Sets the style logic of the [`Program`]. - pub fn style( - self, - f: impl Fn(&P::State, &P::Theme) -> Appearance, - ) -> Program< - impl Definition<State = P::State, Message = P::Message, Theme = P::Theme>, - > { - Program { - raw: with_style(self.raw, f), - settings: self.settings, - } - } -} +use crate::{Element, Executor, Result, Settings, Subscription, Task}; /// The internal definition of a [`Program`]. /// /// You should not need to implement this trait directly. Instead, use the /// methods available in the [`Program`] struct. #[allow(missing_docs)] -pub trait Definition: Sized { +pub trait Program: Sized { /// The state of the program. type State; /// The message of the program. - type Message: Send + std::fmt::Debug; + type Message: Send + std::fmt::Debug + 'static; /// The theme of the program. - type Theme: Default + DefaultStyle; + type Theme: Default + theme::Base; /// The renderer of the program. type Renderer: Renderer; @@ -439,22 +26,19 @@ pub trait Definition: Sized { /// The executor of the program. type Executor: Executor; - fn name() -> &'static str; - - fn load(&self) -> Command<Self::Message>; - fn update( &self, state: &mut Self::State, message: Self::Message, - ) -> Command<Self::Message>; + ) -> Task<Self::Message>; fn view<'a>( &self, state: &'a Self::State, + window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer>; - fn title(&self, _state: &Self::State) -> String { + fn title(&self, _state: &Self::State, _window: window::Id) -> String { String::from("A cool iced application!") } @@ -465,28 +49,162 @@ pub trait Definition: Sized { Subscription::none() } - fn theme(&self, _state: &Self::State) -> Self::Theme { - Self::Theme::default() + fn theme(&self, _state: &Self::State, _window: window::Id) -> Self::Theme { + <Self::Theme as Default>::default() } - fn style(&self, _state: &Self::State, theme: &Self::Theme) -> Appearance { - DefaultStyle::default_style(theme) + fn style(&self, _state: &Self::State, theme: &Self::Theme) -> theme::Style { + theme::Base::base(theme) + } + + fn scale_factor(&self, _state: &Self::State, _window: window::Id) -> f64 { + 1.0 + } + + /// Runs the [`Program`]. + /// + /// The state of the [`Program`] must implement [`Default`]. + /// If your state does not implement [`Default`], use [`run_with`] + /// instead. + /// + /// [`run_with`]: Self::run_with + fn run( + self, + settings: Settings, + window_settings: Option<window::Settings>, + ) -> Result + where + Self: 'static, + Self::State: Default, + { + self.run_with(settings, window_settings, || { + (Self::State::default(), Task::none()) + }) + } + + /// Runs the [`Program`] with the given [`Settings`] and a closure that creates the initial state. + fn run_with<I>( + self, + settings: Settings, + window_settings: Option<window::Settings>, + initialize: I, + ) -> Result + where + Self: 'static, + I: FnOnce() -> (Self::State, Task<Self::Message>) + 'static, + { + use std::marker::PhantomData; + + struct Instance<P: Program, I> { + program: P, + state: P::State, + _initialize: PhantomData<I>, + } + + impl<P: Program, I: FnOnce() -> (P::State, Task<P::Message>)> + shell::Program for Instance<P, I> + { + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Flags = (P, I); + type Executor = P::Executor; + + fn new( + (program, initialize): Self::Flags, + ) -> (Self, Task<Self::Message>) { + let (state, task) = initialize(); + + ( + Self { + program, + state, + _initialize: PhantomData, + }, + task, + ) + } + + fn title(&self, window: window::Id) -> String { + self.program.title(&self.state, window) + } + + fn update( + &mut self, + message: Self::Message, + ) -> Task<Self::Message> { + self.program.update(&mut self.state, message) + } + + fn view( + &self, + window: window::Id, + ) -> crate::Element<'_, Self::Message, Self::Theme, Self::Renderer> + { + self.program.view(&self.state, window) + } + + fn subscription(&self) -> Subscription<Self::Message> { + self.program.subscription(&self.state) + } + + fn theme(&self, window: window::Id) -> Self::Theme { + self.program.theme(&self.state, window) + } + + fn style(&self, theme: &Self::Theme) -> theme::Style { + self.program.style(&self.state, theme) + } + + fn scale_factor(&self, window: window::Id) -> f64 { + self.program.scale_factor(&self.state, window) + } + } + + #[allow(clippy::needless_update)] + let renderer_settings = crate::graphics::Settings { + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: if settings.antialiasing { + Some(crate::graphics::Antialiasing::MSAAx4) + } else { + None + }, + ..crate::graphics::Settings::default() + }; + + Ok(shell::program::run::< + Instance<Self, I>, + <Self::Renderer as compositor::Default>::Compositor, + >( + Settings { + id: settings.id, + fonts: settings.fonts, + default_font: settings.default_font, + default_text_size: settings.default_text_size, + antialiasing: settings.antialiasing, + } + .into(), + renderer_settings, + window_settings, + (self, initialize), + )?) } } -fn with_title<P: Definition>( +pub fn with_title<P: Program>( program: P, - title: impl Title<P::State>, -) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { + title: impl Fn(&P::State, window::Id) -> String, +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { struct WithTitle<P, Title> { program: P, title: Title, } - impl<P, Title> Definition for WithTitle<P, Title> + impl<P, Title> Program for WithTitle<P, Title> where - P: Definition, - Title: self::Title<P::State>, + P: Program, + Title: Fn(&P::State, window::Id) -> String, { type State = P::State; type Message = P::Message; @@ -494,35 +212,32 @@ fn with_title<P: Definition>( type Renderer = P::Renderer; type Executor = P::Executor; - fn title(&self, state: &Self::State) -> String { - self.title.title(state) - } - - fn load(&self) -> Command<Self::Message> { - self.program.load() - } - - fn name() -> &'static str { - P::name() + fn title(&self, state: &Self::State, window: window::Id) -> String { + (self.title)(state, window) } fn update( &self, state: &mut Self::State, message: Self::Message, - ) -> Command<Self::Message> { + ) -> Task<Self::Message> { self.program.update(state, message) } fn view<'a>( &self, state: &'a Self::State, + window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state) + self.program.view(state, window) } - fn theme(&self, state: &Self::State) -> Self::Theme { - self.program.theme(state) + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) } fn subscription( @@ -536,93 +251,28 @@ fn with_title<P: Definition>( &self, state: &Self::State, theme: &Self::Theme, - ) -> Appearance { + ) -> theme::Style { self.program.style(state, theme) } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.program.scale_factor(state, window) + } } WithTitle { program, title } } -fn with_load<P: Definition>( - program: P, - f: impl Fn() -> Command<P::Message>, -) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { - struct WithLoad<P, F> { - program: P, - load: F, - } - - impl<P: Definition, F> Definition for WithLoad<P, F> - where - F: Fn() -> Command<P::Message>, - { - type State = P::State; - type Message = P::Message; - type Theme = P::Theme; - type Renderer = P::Renderer; - type Executor = executor::Default; - - fn load(&self) -> Command<Self::Message> { - Command::batch([self.program.load(), (self.load)()]) - } - - fn name() -> &'static str { - P::name() - } - - fn update( - &self, - state: &mut Self::State, - message: Self::Message, - ) -> Command<Self::Message> { - self.program.update(state, message) - } - - fn view<'a>( - &self, - state: &'a Self::State, - ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state) - } - - fn title(&self, state: &Self::State) -> String { - self.program.title(state) - } - - fn subscription( - &self, - state: &Self::State, - ) -> Subscription<Self::Message> { - self.program.subscription(state) - } - - fn theme(&self, state: &Self::State) -> Self::Theme { - self.program.theme(state) - } - - fn style( - &self, - state: &Self::State, - theme: &Self::Theme, - ) -> Appearance { - self.program.style(state, theme) - } - } - - WithLoad { program, load: f } -} - -fn with_subscription<P: Definition>( +pub fn with_subscription<P: Program>( program: P, f: impl Fn(&P::State) -> Subscription<P::Message>, -) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { struct WithSubscription<P, F> { program: P, subscription: F, } - impl<P: Definition, F> Definition for WithSubscription<P, F> + impl<P: Program, F> Program for WithSubscription<P, F> where F: Fn(&P::State) -> Subscription<P::Message>, { @@ -630,7 +280,7 @@ fn with_subscription<P: Definition>( type Message = P::Message; type Theme = P::Theme; type Renderer = P::Renderer; - type Executor = executor::Default; + type Executor = P::Executor; fn subscription( &self, @@ -639,44 +289,45 @@ fn with_subscription<P: Definition>( (self.subscription)(state) } - fn name() -> &'static str { - P::name() - } - - fn load(&self) -> Command<Self::Message> { - self.program.load() - } - fn update( &self, state: &mut Self::State, message: Self::Message, - ) -> Command<Self::Message> { + ) -> Task<Self::Message> { self.program.update(state, message) } fn view<'a>( &self, state: &'a Self::State, + window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state) + self.program.view(state, window) } - fn title(&self, state: &Self::State) -> String { - self.program.title(state) + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.program.title(state, window) } - fn theme(&self, state: &Self::State) -> Self::Theme { - self.program.theme(state) + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) } fn style( &self, state: &Self::State, theme: &Self::Theme, - ) -> Appearance { + ) -> theme::Style { self.program.style(state, theme) } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.program.scale_factor(state, window) + } } WithSubscription { @@ -685,18 +336,18 @@ fn with_subscription<P: Definition>( } } -fn with_theme<P: Definition>( +pub fn with_theme<P: Program>( program: P, - f: impl Fn(&P::State) -> P::Theme, -) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { + f: impl Fn(&P::State, window::Id) -> P::Theme, +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { struct WithTheme<P, F> { program: P, theme: F, } - impl<P: Definition, F> Definition for WithTheme<P, F> + impl<P: Program, F> Program for WithTheme<P, F> where - F: Fn(&P::State) -> P::Theme, + F: Fn(&P::State, window::Id) -> P::Theme, { type State = P::State; type Message = P::Message; @@ -704,35 +355,32 @@ fn with_theme<P: Definition>( type Renderer = P::Renderer; type Executor = P::Executor; - fn theme(&self, state: &Self::State) -> Self::Theme { - (self.theme)(state) + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + (self.theme)(state, window) } - fn name() -> &'static str { - P::name() - } - - fn load(&self) -> Command<Self::Message> { - self.program.load() - } - - fn title(&self, state: &Self::State) -> String { - self.program.title(state) + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.program.title(state, window) } fn update( &self, state: &mut Self::State, message: Self::Message, - ) -> Command<Self::Message> { + ) -> Task<Self::Message> { self.program.update(state, message) } fn view<'a>( &self, state: &'a Self::State, + window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state) + self.program.view(state, window) } fn subscription( @@ -746,26 +394,30 @@ fn with_theme<P: Definition>( &self, state: &Self::State, theme: &Self::Theme, - ) -> Appearance { + ) -> theme::Style { self.program.style(state, theme) } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.program.scale_factor(state, window) + } } WithTheme { program, theme: f } } -fn with_style<P: Definition>( +pub fn with_style<P: Program>( program: P, - f: impl Fn(&P::State, &P::Theme) -> Appearance, -) -> impl Definition<State = P::State, Message = P::Message, Theme = P::Theme> { + f: impl Fn(&P::State, &P::Theme) -> theme::Style, +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { struct WithStyle<P, F> { program: P, style: F, } - impl<P: Definition, F> Definition for WithStyle<P, F> + impl<P: Program, F> Program for WithStyle<P, F> where - F: Fn(&P::State, &P::Theme) -> Appearance, + F: Fn(&P::State, &P::Theme) -> theme::Style, { type State = P::State; type Message = P::Message; @@ -777,35 +429,28 @@ fn with_style<P: Definition>( &self, state: &Self::State, theme: &Self::Theme, - ) -> Appearance { + ) -> theme::Style { (self.style)(state, theme) } - fn name() -> &'static str { - P::name() - } - - fn load(&self) -> Command<Self::Message> { - self.program.load() - } - - fn title(&self, state: &Self::State) -> String { - self.program.title(state) + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.program.title(state, window) } fn update( &self, state: &mut Self::State, message: Self::Message, - ) -> Command<Self::Message> { + ) -> Task<Self::Message> { self.program.update(state, message) } fn view<'a>( &self, state: &'a Self::State, + window: window::Id, ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.program.view(state) + self.program.view(state, window) } fn subscription( @@ -815,91 +460,166 @@ fn with_style<P: Definition>( self.program.subscription(state) } - fn theme(&self, state: &Self::State) -> Self::Theme { - self.program.theme(state) + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) + } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.program.scale_factor(state, window) } } WithStyle { program, style: f } } -/// The title logic of some [`Program`]. -/// -/// This trait is implemented both for `&static str` and -/// any closure `Fn(&State) -> String`. -/// -/// This trait allows the [`program`] builder to take any of them. -pub trait Title<State> { - /// Produces the title of the [`Program`]. - fn title(&self, state: &State) -> String; -} +pub fn with_scale_factor<P: Program>( + program: P, + f: impl Fn(&P::State, window::Id) -> f64, +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { + struct WithScaleFactor<P, F> { + program: P, + scale_factor: F, + } -impl<State> Title<State> for &'static str { - fn title(&self, _state: &State) -> String { - self.to_string() + impl<P: Program, F> Program for WithScaleFactor<P, F> + where + F: Fn(&P::State, window::Id) -> f64, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Executor = P::Executor; + + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.program.title(state, window) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task<Self::Message> { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.program.view(state, window) + } + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription<Self::Message> { + self.program.subscription(state) + } + + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) + } + + fn style( + &self, + state: &Self::State, + theme: &Self::Theme, + ) -> theme::Style { + self.program.style(state, theme) + } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + (self.scale_factor)(state, window) + } + } + + WithScaleFactor { + program, + scale_factor: f, } } -impl<T, State> Title<State> for T -where - T: Fn(&State) -> String, -{ - fn title(&self, state: &State) -> String { - self(state) +pub fn with_executor<P: Program, E: Executor>( + program: P, +) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { + use std::marker::PhantomData; + + struct WithExecutor<P, E> { + program: P, + executor: PhantomData<E>, } -} -/// The update logic of some [`Program`]. -/// -/// This trait allows the [`program`] builder to take any closure that -/// returns any `Into<Command<Message>>`. -pub trait Update<State, Message> { - /// Processes the message and updates the state of the [`Program`]. - fn update( - &self, - state: &mut State, - message: Message, - ) -> impl Into<Command<Message>>; -} + impl<P: Program, E> Program for WithExecutor<P, E> + where + E: Executor, + { + type State = P::State; + type Message = P::Message; + type Theme = P::Theme; + type Renderer = P::Renderer; + type Executor = E; -impl<T, State, Message, C> Update<State, Message> for T -where - T: Fn(&mut State, Message) -> C, - C: Into<Command<Message>>, -{ - fn update( - &self, - state: &mut State, - message: Message, - ) -> impl Into<Command<Message>> { - self(state, message) + fn title(&self, state: &Self::State, window: window::Id) -> String { + self.program.title(state, window) + } + + fn update( + &self, + state: &mut Self::State, + message: Self::Message, + ) -> Task<Self::Message> { + self.program.update(state, message) + } + + fn view<'a>( + &self, + state: &'a Self::State, + window: window::Id, + ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { + self.program.view(state, window) + } + + fn subscription( + &self, + state: &Self::State, + ) -> Subscription<Self::Message> { + self.program.subscription(state) + } + + fn theme( + &self, + state: &Self::State, + window: window::Id, + ) -> Self::Theme { + self.program.theme(state, window) + } + + fn style( + &self, + state: &Self::State, + theme: &Self::Theme, + ) -> theme::Style { + self.program.style(state, theme) + } + + fn scale_factor(&self, state: &Self::State, window: window::Id) -> f64 { + self.program.scale_factor(state, window) + } } -} -/// The view logic of some [`Program`]. -/// -/// This trait allows the [`program`] builder to take any closure that -/// returns any `Into<Element<'_, Message>>`. -pub trait View<'a, State, Message, Theme, Renderer> { - /// Produces the widget of the [`Program`]. - fn view( - &self, - state: &'a State, - ) -> impl Into<Element<'a, Message, Theme, Renderer>>; -} - -impl<'a, T, State, Message, Theme, Renderer, Widget> - View<'a, State, Message, Theme, Renderer> for T -where - T: Fn(&'a State) -> Widget, - State: 'static, - Widget: Into<Element<'a, Message, Theme, Renderer>>, -{ - fn view( - &self, - state: &'a State, - ) -> impl Into<Element<'a, Message, Theme, Renderer>> { - self(state) + WithExecutor { + program, + executor: PhantomData::<E>, } } diff --git a/src/settings.rs b/src/settings.rs deleted file mode 100644 index f7947841..00000000 --- a/src/settings.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Configure your application. -use crate::window; -use crate::{Font, Pixels}; - -use std::borrow::Cow; - -/// The settings of an iced [`Program`]. -/// -/// [`Program`]: crate::Program -#[derive(Debug, Clone)] -pub struct Settings<Flags = ()> { - /// 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 window settings. - /// - /// They will be ignored on the Web. - pub window: window::Settings, - - /// The data needed to initialize the [`Program`]. - /// - /// [`Program`]: crate::Program - pub flags: Flags, - - /// 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`], at a performance cost. - /// - /// By default, it is disabled. - /// - /// [`Canvas`]: crate::widget::Canvas - pub antialiasing: bool, -} - -impl<Flags> Settings<Flags> { - /// Initialize [`Program`] settings using the given data. - /// - /// [`Program`]: crate::Program - pub fn with_flags(flags: Flags) -> Self { - let default_settings = Settings::<()>::default(); - - Self { - flags, - id: default_settings.id, - window: default_settings.window, - fonts: default_settings.fonts, - default_font: default_settings.default_font, - default_text_size: default_settings.default_text_size, - antialiasing: default_settings.antialiasing, - } - } -} - -impl<Flags> Default for Settings<Flags> -where - Flags: Default, -{ - fn default() -> Self { - Self { - id: None, - window: window::Settings::default(), - flags: Default::default(), - fonts: Vec::new(), - default_font: Font::default(), - default_text_size: Pixels(16.0), - antialiasing: false, - } - } -} - -impl<Flags> From<Settings<Flags>> for iced_winit::Settings<Flags> { - fn from(settings: Settings<Flags>) -> iced_winit::Settings<Flags> { - iced_winit::Settings { - id: settings.id, - window: settings.window, - flags: settings.flags, - fonts: settings.fonts, - } - } -} diff --git a/src/time.rs b/src/time.rs index 55982277..98a800ac 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,5 +1,5 @@ //! Listen and react to time. -pub use crate::core::time::{Duration, Instant, SystemTime}; +pub use crate::core::time::*; #[allow(unused_imports)] #[cfg_attr( diff --git a/src/window/icon.rs b/src/window/icon.rs index 7fe4ca7b..f453d580 100644 --- a/src/window/icon.rs +++ b/src/window/icon.rs @@ -13,7 +13,7 @@ use std::path::Path; /// This will return an error in case the file is missing at run-time. You may prefer [`from_file_data`] instead. #[cfg(feature = "image")] pub fn from_file<P: AsRef<Path>>(icon_path: P) -> Result<Icon, Error> { - let icon = image::io::Reader::open(icon_path)?.decode()?.to_rgba8(); + let icon = image::ImageReader::open(icon_path)?.decode()?.to_rgba8(); Ok(icon::from_rgba(icon.to_vec(), icon.width(), icon.height())?) } @@ -27,7 +27,7 @@ pub fn from_file_data( data: &[u8], explicit_format: Option<image::ImageFormat>, ) -> Result<Icon, Error> { - let mut icon = image::io::Reader::new(std::io::Cursor::new(data)); + let mut icon = image::ImageReader::new(std::io::Cursor::new(data)); let icon_with_format = match explicit_format { Some(format) => { diff --git a/test/Cargo.toml b/test/Cargo.toml new file mode 100644 index 00000000..2dd35e7f --- /dev/null +++ b/test/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "iced_test" +version.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true +categories.workspace = true +keywords.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +iced_runtime.workspace = true + +iced_renderer.workspace = true +iced_renderer.features = ["fira-sans"] + +png.workspace = true +sha2.workspace = true +thiserror.workspace = true diff --git a/test/src/lib.rs b/test/src/lib.rs new file mode 100644 index 00000000..982cc2c1 --- /dev/null +++ b/test/src/lib.rs @@ -0,0 +1,639 @@ +//! Test your `iced` applications in headless mode. +//! +//! # Basic Usage +//! Let's assume we want to test [the classical counter interface]. +//! +//! First, we will want to create a [`Simulator`] of our interface: +//! +//! ```rust,no_run +//! # struct Counter { value: i64 } +//! # impl Counter { +//! # pub fn view(&self) -> iced_runtime::core::Element<(), iced_runtime::core::Theme, iced_renderer::Renderer> { unimplemented!() } +//! # } +//! use iced_test::simulator; +//! +//! let mut counter = Counter { value: 0 }; +//! let mut ui = simulator(counter.view()); +//! ``` +//! +//! Now we can simulate a user interacting with our interface. Let's use [`Simulator::click`] to click +//! the counter buttons: +//! +//! ```rust,no_run +//! # struct Counter { value: i64 } +//! # impl Counter { +//! # pub fn view(&self) -> iced_runtime::core::Element<(), iced_runtime::core::Theme, iced_renderer::Renderer> { unimplemented!() } +//! # } +//! use iced_test::selector::text; +//! # use iced_test::simulator; +//! # +//! # let mut counter = Counter { value: 0 }; +//! # let mut ui = simulator(counter.view()); +//! +//! let _ = ui.click(text("+")); +//! let _ = ui.click(text("+")); +//! let _ = ui.click(text("-")); +//! ``` +//! +//! [`Simulator::click`] takes a [`Selector`]. A [`Selector`] describes a way to query the widgets of an interface. In this case, +//! [`selector::text`] lets us select a widget by the text it contains. +//! +//! We can now process any messages produced by these interactions and then assert that the final value of our counter is +//! indeed `1`! +//! +//! ```rust,no_run +//! # struct Counter { value: i64 } +//! # impl Counter { +//! # pub fn update(&mut self, message: ()) {} +//! # pub fn view(&self) -> iced_runtime::core::Element<(), iced_runtime::core::Theme, iced_renderer::Renderer> { unimplemented!() } +//! # } +//! # use iced_test::selector::text; +//! # use iced_test::simulator; +//! # +//! # let mut counter = Counter { value: 0 }; +//! # let mut ui = simulator(counter.view()); +//! # +//! # let _ = ui.click(text("+")); +//! # let _ = ui.click(text("+")); +//! # let _ = ui.click(text("-")); +//! # +//! for message in ui.into_messages() { +//! counter.update(message); +//! } +//! +//! assert_eq!(counter.value, 1); +//! ``` +//! +//! We can even rebuild the interface to make sure the counter _displays_ the proper value with [`Simulator::find`]: +//! +//! ```rust,no_run +//! # struct Counter { value: i64 } +//! # impl Counter { +//! # pub fn view(&self) -> iced_runtime::core::Element<(), iced_runtime::core::Theme, iced_renderer::Renderer> { unimplemented!() } +//! # } +//! # use iced_test::selector::text; +//! # use iced_test::simulator; +//! # +//! # let mut counter = Counter { value: 0 }; +//! let mut ui = simulator(counter.view()); +//! +//! assert!(ui.find(text("1")).is_ok(), "Counter should display 1!"); +//! ``` +//! +//! And that's it! That's the gist of testing `iced` applications! +//! +//! [`Simulator`] contains additional operations you can use to simulate more interactions—like [`tap_key`](Simulator::tap_key) or +//! [`typewrite`](Simulator::typewrite)—and even perform [_snapshot testing_](Simulator::snapshot)! +//! +//! [the classical counter interface]: https://book.iced.rs/architecture.html#dissecting-an-interface +pub mod selector; + +pub use selector::Selector; + +use iced_renderer as renderer; +use iced_runtime as runtime; +use iced_runtime::core; + +use crate::core::clipboard; +use crate::core::event; +use crate::core::keyboard; +use crate::core::mouse; +use crate::core::theme; +use crate::core::time; +use crate::core::widget; +use crate::core::window; +use crate::core::{ + Element, Event, Font, Point, Rectangle, Settings, Size, SmolStr, +}; +use crate::runtime::UserInterface; +use crate::runtime::user_interface; + +use std::borrow::Cow; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +/// Creates a new [`Simulator`]. +/// +/// This is just a function version of [`Simulator::new`]. +pub fn simulator<'a, Message, Theme, Renderer>( + element: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Simulator<'a, Message, Theme, Renderer> +where + Theme: theme::Base, + Renderer: core::Renderer + core::renderer::Headless, +{ + Simulator::new(element) +} + +/// A user interface that can be interacted with and inspected programmatically. +#[allow(missing_debug_implementations)] +pub struct Simulator< + 'a, + Message, + Theme = core::Theme, + Renderer = renderer::Renderer, +> { + raw: UserInterface<'a, Message, Theme, Renderer>, + renderer: Renderer, + size: Size, + cursor: mouse::Cursor, + messages: Vec<Message>, +} + +/// A specific area of a [`Simulator`], normally containing a widget. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Target { + /// The bounds of the area. + pub bounds: Rectangle, +} + +impl<'a, Message, Theme, Renderer> Simulator<'a, Message, Theme, Renderer> +where + Theme: theme::Base, + Renderer: core::Renderer + core::renderer::Headless, +{ + /// Creates a new [`Simulator`] with default [`Settings`] and a default size (1024x768). + pub fn new( + element: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self::with_settings(Settings::default(), element) + } + + /// Creates a new [`Simulator`] with the given [`Settings`] and a default size (1024x768). + pub fn with_settings( + settings: Settings, + element: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self::with_size(settings, window::Settings::default().size, element) + } + + /// Creates a new [`Simulator`] with the given [`Settings`] and size. + pub fn with_size( + settings: Settings, + size: impl Into<Size>, + element: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + let size = size.into(); + + let default_font = match settings.default_font { + Font::DEFAULT => Font::with_name("Fira Sans"), + _ => settings.default_font, + }; + + for font in settings.fonts { + load_font(font).expect("Font must be valid"); + } + + let mut renderer = + Renderer::new(default_font, settings.default_text_size); + + let raw = UserInterface::build( + element, + size, + user_interface::Cache::default(), + &mut renderer, + ); + + Simulator { + raw, + renderer, + size, + cursor: mouse::Cursor::Unavailable, + messages: Vec::new(), + } + } + + /// Finds the [`Target`] of the given widget [`Selector`] in the [`Simulator`]. + pub fn find( + &mut self, + selector: impl Into<Selector>, + ) -> Result<Target, Error> { + let selector = selector.into(); + + match &selector { + Selector::Id(id) => { + struct FindById<'a> { + id: &'a widget::Id, + target: Option<Target>, + } + + impl widget::Operation for FindById<'_> { + fn container( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation<()>, + ), + ) { + if self.target.is_some() { + return; + } + + if Some(self.id) == id { + self.target = Some(Target { bounds }); + return; + } + + operate_on_children(self); + } + + fn scrollable( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _content_bounds: Rectangle, + _translation: core::Vector, + _state: &mut dyn widget::operation::Scrollable, + ) { + if self.target.is_some() { + return; + } + + if Some(self.id) == id { + self.target = Some(Target { bounds }); + } + } + + fn text_input( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _state: &mut dyn widget::operation::TextInput, + ) { + if self.target.is_some() { + return; + } + + if Some(self.id) == id { + self.target = Some(Target { bounds }); + } + } + + fn text( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _text: &str, + ) { + if self.target.is_some() { + return; + } + + if Some(self.id) == id { + self.target = Some(Target { bounds }); + } + } + + fn custom( + &mut self, + id: Option<&widget::Id>, + bounds: Rectangle, + _state: &mut dyn std::any::Any, + ) { + if self.target.is_some() { + return; + } + + if Some(self.id) == id { + self.target = Some(Target { bounds }); + } + } + } + + let mut find = FindById { id, target: None }; + self.raw.operate(&self.renderer, &mut find); + + find.target.ok_or(Error::NotFound(selector)) + } + Selector::Text(text) => { + struct FindByText<'a> { + text: &'a str, + target: Option<Target>, + } + + impl widget::Operation for FindByText<'_> { + fn container( + &mut self, + _id: Option<&widget::Id>, + _bounds: Rectangle, + operate_on_children: &mut dyn FnMut( + &mut dyn widget::Operation<()>, + ), + ) { + if self.target.is_some() { + return; + } + + operate_on_children(self); + } + + fn text( + &mut self, + _id: Option<&widget::Id>, + bounds: Rectangle, + text: &str, + ) { + if self.target.is_some() { + return; + } + + if self.text == text { + self.target = Some(Target { bounds }); + } + } + } + + let mut find = FindByText { text, target: None }; + self.raw.operate(&self.renderer, &mut find); + + find.target.ok_or(Error::NotFound(selector)) + } + } + } + + /// Points the mouse cursor at the given position in the [`Simulator`]. + /// + /// This does _not_ produce mouse movement events! + pub fn point_at(&mut self, position: impl Into<Point>) { + self.cursor = mouse::Cursor::Available(position.into()); + } + + /// Clicks the [`Target`] found by the given [`Selector`], if any. + /// + /// This consists in: + /// - Pointing the mouse cursor at the center of the [`Target`]. + /// - Simulating a [`click`]. + pub fn click( + &mut self, + selector: impl Into<Selector>, + ) -> Result<Target, Error> { + let target = self.find(selector)?; + self.point_at(target.bounds.center()); + + let _ = self.simulate(click()); + + Ok(target) + } + + /// Simulates a key press, followed by a release, in the [`Simulator`]. + pub fn tap_key(&mut self, key: impl Into<keyboard::Key>) -> event::Status { + self.simulate(tap_key(key, None)) + .first() + .copied() + .unwrap_or(event::Status::Ignored) + } + + /// Simulates a user typing in the keyboard the given text in the [`Simulator`]. + pub fn typewrite(&mut self, text: &str) -> event::Status { + let statuses = self.simulate(typewrite(text)); + + statuses + .into_iter() + .fold(event::Status::Ignored, event::Status::merge) + } + + /// Simulates the given raw sequence of events in the [`Simulator`]. + pub fn simulate( + &mut self, + events: impl IntoIterator<Item = Event>, + ) -> Vec<event::Status> { + let events: Vec<Event> = events.into_iter().collect(); + + let (_state, statuses) = self.raw.update( + &events, + self.cursor, + &mut self.renderer, + &mut clipboard::Null, + &mut self.messages, + ); + + statuses + } + + /// Draws and takes a [`Snapshot`] of the interface in the [`Simulator`]. + pub fn snapshot(&mut self, theme: &Theme) -> Result<Snapshot, Error> { + let base = theme.base(); + + let _ = self.raw.update( + &[Event::Window(window::Event::RedrawRequested( + time::Instant::now(), + ))], + self.cursor, + &mut self.renderer, + &mut clipboard::Null, + &mut self.messages, + ); + + let _ = self.raw.draw( + &mut self.renderer, + theme, + &core::renderer::Style { + text_color: base.text_color, + }, + self.cursor, + ); + + let scale_factor = 2.0; + + let physical_size = Size::new( + (self.size.width * scale_factor).round() as u32, + (self.size.height * scale_factor).round() as u32, + ); + + let rgba = self.renderer.screenshot( + physical_size, + scale_factor, + base.background_color, + ); + + Ok(Snapshot { + screenshot: window::Screenshot::new( + rgba, + physical_size, + f64::from(scale_factor), + ), + }) + } + + /// Turns the [`Simulator`] into the sequence of messages produced by any interactions. + pub fn into_messages( + self, + ) -> impl Iterator<Item = Message> + use<Message, Theme, Renderer> { + self.messages.into_iter() + } +} + +/// A frame of a user interface rendered by a [`Simulator`]. +#[derive(Debug, Clone)] +pub struct Snapshot { + screenshot: window::Screenshot, +} + +impl Snapshot { + /// Compares the [`Snapshot`] with the PNG image found in the given path, returning + /// `true` if they are identical. + /// + /// If the PNG image does not exist, it will be created by the [`Snapshot`] for future + /// testing and `true` will be returned. + pub fn matches_image(&self, path: impl AsRef<Path>) -> Result<bool, Error> { + let path = snapshot_path(path, "png"); + + if path.exists() { + let file = fs::File::open(&path)?; + let decoder = png::Decoder::new(file); + + let mut reader = decoder.read_info()?; + let mut bytes = vec![0; reader.output_buffer_size()]; + let info = reader.next_frame(&mut bytes)?; + + Ok(self.screenshot.bytes == bytes[..info.buffer_size()]) + } else { + if let Some(directory) = path.parent() { + fs::create_dir_all(directory)?; + } + + let file = fs::File::create(path)?; + + let mut encoder = png::Encoder::new( + file, + self.screenshot.size.width, + self.screenshot.size.height, + ); + encoder.set_color(png::ColorType::Rgba); + + let mut writer = encoder.write_header()?; + writer.write_image_data(&self.screenshot.bytes)?; + writer.finish()?; + + Ok(true) + } + } + + /// Compares the [`Snapshot`] with the SHA-256 hash file found in the given path, returning + /// `true` if they are identical. + /// + /// If the hash file does not exist, it will be created by the [`Snapshot`] for future + /// testing and `true` will be returned. + pub fn matches_hash(&self, path: impl AsRef<Path>) -> Result<bool, Error> { + use sha2::{Digest, Sha256}; + + let path = snapshot_path(path, "sha256"); + + let hash = { + let mut hasher = Sha256::new(); + hasher.update(&self.screenshot.bytes); + format!("{:x}", hasher.finalize()) + }; + + if path.exists() { + let saved_hash = fs::read_to_string(&path)?; + + Ok(hash == saved_hash) + } else { + if let Some(directory) = path.parent() { + fs::create_dir_all(directory)?; + } + + fs::write(path, hash)?; + Ok(true) + } + } +} + +/// Returns the sequence of events of a click. +pub fn click() -> impl Iterator<Item = Event> { + [ + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)), + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)), + ] + .into_iter() +} + +/// Returns the sequence of events of a "key tap" (i.e. pressing and releasing a key). +pub fn tap_key( + key: impl Into<keyboard::Key>, + text: Option<SmolStr>, +) -> impl Iterator<Item = Event> { + let key = key.into(); + + [ + Event::Keyboard(keyboard::Event::KeyPressed { + key: key.clone(), + modified_key: key.clone(), + physical_key: keyboard::key::Physical::Unidentified( + keyboard::key::NativeCode::Unidentified, + ), + location: keyboard::Location::Standard, + modifiers: keyboard::Modifiers::default(), + text, + }), + Event::Keyboard(keyboard::Event::KeyReleased { + key: key.clone(), + modified_key: key, + physical_key: keyboard::key::Physical::Unidentified( + keyboard::key::NativeCode::Unidentified, + ), + location: keyboard::Location::Standard, + modifiers: keyboard::Modifiers::default(), + }), + ] + .into_iter() +} + +/// Returns the sequence of events of typewriting the given text in a keyboard. +pub fn typewrite(text: &str) -> impl Iterator<Item = Event> + '_ { + text.chars() + .map(|c| SmolStr::new_inline(&c.to_string())) + .flat_map(|c| tap_key(keyboard::Key::Character(c.clone()), Some(c))) +} + +/// A test error. +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + /// No matching widget was found for the [`Selector`]. + #[error("no matching widget was found for the selector: {0:?}")] + NotFound(Selector), + /// An IO operation failed. + #[error("an IO operation failed: {0}")] + IOFailed(Arc<io::Error>), + /// The decoding of some PNG image failed. + #[error("the decoding of some PNG image failed: {0}")] + PngDecodingFailed(Arc<png::DecodingError>), + /// The encoding of some PNG image failed. + #[error("the encoding of some PNG image failed: {0}")] + PngEncodingFailed(Arc<png::EncodingError>), +} + +impl From<io::Error> for Error { + fn from(error: io::Error) -> Self { + Self::IOFailed(Arc::new(error)) + } +} + +impl From<png::DecodingError> for Error { + fn from(error: png::DecodingError) -> Self { + Self::PngDecodingFailed(Arc::new(error)) + } +} + +impl From<png::EncodingError> for Error { + fn from(error: png::EncodingError) -> Self { + Self::PngEncodingFailed(Arc::new(error)) + } +} + +fn load_font(font: impl Into<Cow<'static, [u8]>>) -> Result<(), Error> { + renderer::graphics::text::font_system() + .write() + .expect("Write to font system") + .load_font(font.into()); + + Ok(()) +} + +fn snapshot_path(path: impl AsRef<Path>, extension: &str) -> PathBuf { + path.as_ref().with_extension(extension) +} diff --git a/test/src/selector.rs b/test/src/selector.rs new file mode 100644 index 00000000..7b8dcb7e --- /dev/null +++ b/test/src/selector.rs @@ -0,0 +1,29 @@ +//! Select widgets of a user interface. +use crate::core::text; +use crate::core::widget; + +/// A selector describes a strategy to find a certain widget in a user interface. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Selector { + /// Find the widget with the given [`widget::Id`]. + Id(widget::Id), + /// Find the widget containing the given [`text::Fragment`]. + Text(text::Fragment<'static>), +} + +impl From<widget::Id> for Selector { + fn from(id: widget::Id) -> Self { + Self::Id(id) + } +} + +impl From<&'static str> for Selector { + fn from(id: &'static str) -> Self { + Self::Id(widget::Id::new(id)) + } +} + +/// Creates [`Selector`] that finds the widget containing the given text fragment. +pub fn text(fragment: impl text::IntoFragment<'static>) -> Selector { + Selector::Text(fragment.into_fragment()) +} diff --git a/tiny_skia/Cargo.toml b/tiny_skia/Cargo.toml index 32ead3e0..323233f0 100644 --- a/tiny_skia/Cargo.toml +++ b/tiny_skia/Cargo.toml @@ -15,7 +15,7 @@ workspace = true [features] image = ["iced_graphics/image"] -svg = ["resvg"] +svg = ["iced_graphics/svg", "resvg"] geometry = ["iced_graphics/geometry"] [dependencies] diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index 028b304f..c1c02376 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -1,10 +1,10 @@ +use crate::Primitive; use crate::core::renderer::Quad; use crate::core::{ Background, Color, Gradient, Rectangle, Size, Transformation, Vector, }; use crate::graphics::{Image, Text}; use crate::text; -use crate::Primitive; #[derive(Debug)] pub struct Engine { @@ -439,9 +439,13 @@ impl Engine { let transformation = transformation * *local_transformation; let (width, height) = buffer.size(); - let physical_bounds = - Rectangle::new(raw.position, Size::new(width, height)) - * transformation; + let physical_bounds = Rectangle::new( + raw.position, + Size::new( + width.unwrap_or(clip_bounds.width), + height.unwrap_or(clip_bounds.height), + ), + ) * transformation; if !clip_bounds.intersects(&physical_bounds) { return; @@ -546,13 +550,7 @@ impl Engine { ) { match image { #[cfg(feature = "image")] - Image::Raster { - handle, - filter_method, - bounds, - rotation, - opacity, - } => { + Image::Raster(raster, bounds) => { let physical_bounds = *bounds * _transformation; if !_clip_bounds.intersects(&physical_bounds) { @@ -563,7 +561,7 @@ impl Engine { .then_some(_clip_mask as &_); let center = physical_bounds.center(); - let radians = f32::from(*rotation); + let radians = f32::from(raster.rotation); let transform = into_transform(_transformation).post_rotate_at( radians.to_degrees(), @@ -572,23 +570,17 @@ impl Engine { ); self.raster_pipeline.draw( - handle, - *filter_method, + &raster.handle, + raster.filter_method, *bounds, - *opacity, + raster.opacity, _pixels, transform, clip_mask, ); } #[cfg(feature = "svg")] - Image::Vector { - handle, - color, - bounds, - rotation, - opacity, - } => { + Image::Vector(svg, bounds) => { let physical_bounds = *bounds * _transformation; if !_clip_bounds.intersects(&physical_bounds) { @@ -599,7 +591,7 @@ impl Engine { .then_some(_clip_mask as &_); let center = physical_bounds.center(); - let radians = f32::from(*rotation); + let radians = f32::from(svg.rotation); let transform = into_transform(_transformation).post_rotate_at( radians.to_degrees(), @@ -608,10 +600,10 @@ impl Engine { ); self.vector_pipeline.draw( - handle, - *color, + &svg.handle, + svg.color, physical_bounds, - *opacity, + svg.opacity, _pixels, transform, clip_mask, diff --git a/tiny_skia/src/geometry.rs b/tiny_skia/src/geometry.rs index 02b6e1b9..dbdff444 100644 --- a/tiny_skia/src/geometry.rs +++ b/tiny_skia/src/geometry.rs @@ -1,11 +1,11 @@ +use crate::Primitive; use crate::core::text::LineHeight; -use crate::core::{Pixels, Point, Radians, Rectangle, Size, Vector}; +use crate::core::{self, Pixels, Point, Radians, Rectangle, Size, Svg, Vector}; use crate::graphics::cache::{self, Cached}; use crate::graphics::geometry::fill::{self, Fill}; use crate::graphics::geometry::stroke::{self, Stroke}; use crate::graphics::geometry::{self, Path, Style}; -use crate::graphics::{Gradient, Text}; -use crate::Primitive; +use crate::graphics::{self, Gradient, Image, Text}; use std::rc::Rc; @@ -13,6 +13,7 @@ use std::rc::Rc; pub enum Geometry { Live { text: Vec<Text>, + images: Vec<graphics::Image>, primitives: Vec<Primitive>, clip_bounds: Rectangle, }, @@ -22,6 +23,7 @@ pub enum Geometry { #[derive(Debug, Clone)] pub struct Cache { pub text: Rc<[Text]>, + pub images: Rc<[graphics::Image]>, pub primitives: Rc<[Primitive]>, pub clip_bounds: Rectangle, } @@ -37,10 +39,12 @@ impl Cached for Geometry { match self { Self::Live { primitives, + images, text, clip_bounds, } => Cache { primitives: Rc::from(primitives), + images: Rc::from(images), text: Rc::from(text), clip_bounds, }, @@ -55,6 +59,7 @@ pub struct Frame { transform: tiny_skia::Transform, stack: Vec<tiny_skia::Transform>, primitives: Vec<Primitive>, + images: Vec<graphics::Image>, text: Vec<Text>, } @@ -68,6 +73,7 @@ impl Frame { clip_bounds, stack: Vec::new(), primitives: Vec::new(), + images: Vec::new(), text: Vec::new(), transform: tiny_skia::Transform::from_translate( clip_bounds.x, @@ -162,6 +168,15 @@ impl geometry::frame::Backend for Frame { }); } + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into<Stroke<'a>>, + ) { + self.stroke(&Path::rectangle(top_left, size), stroke); + } + fn fill_text(&mut self, text: impl Into<geometry::Text>) { let text = text.into(); @@ -238,9 +253,10 @@ impl geometry::frame::Backend for Frame { Self::with_clip(clip_bounds) } - fn paste(&mut self, frame: Self, _at: Point) { + fn paste(&mut self, frame: Self) { self.primitives.extend(frame.primitives); self.text.extend(frame.text); + self.images.extend(frame.images); } fn translate(&mut self, translation: Vector) { @@ -269,10 +285,63 @@ impl geometry::frame::Backend for Frame { fn into_geometry(self) -> Geometry { Geometry::Live { primitives: self.primitives, + images: self.images, text: self.text, clip_bounds: self.clip_bounds, } } + + fn draw_image(&mut self, bounds: Rectangle, image: impl Into<core::Image>) { + let mut image = image.into(); + + let (bounds, external_rotation) = + transform_rectangle(bounds, self.transform); + + image.rotation += external_rotation; + + self.images.push(graphics::Image::Raster(image, bounds)); + } + + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) { + let mut svg = svg.into(); + + let (bounds, external_rotation) = + transform_rectangle(bounds, self.transform); + + svg.rotation += external_rotation; + + self.images.push(Image::Vector(svg, bounds)); + } +} + +fn transform_rectangle( + rectangle: Rectangle, + transform: tiny_skia::Transform, +) -> (Rectangle, Radians) { + let mut top_left = tiny_skia::Point { + x: rectangle.x, + y: rectangle.y, + }; + + let mut top_right = tiny_skia::Point { + x: rectangle.x + rectangle.width, + y: rectangle.y, + }; + + let mut bottom_left = tiny_skia::Point { + x: rectangle.x, + y: rectangle.y + rectangle.height, + }; + + transform.map_point(&mut top_left); + transform.map_point(&mut top_right); + transform.map_point(&mut bottom_left); + + Rectangle::with_vertices( + Point::new(top_left.x, top_left.y), + Point::new(top_right.x, top_right.y), + Point::new(bottom_left.x, bottom_left.y), + ) } fn convert_path(path: &Path) -> Option<tiny_skia::Path> { diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index 48fca1d8..444efb84 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -1,12 +1,12 @@ +use crate::Primitive; +use crate::core::renderer::Quad; use crate::core::{ - image, renderer::Quad, svg, Background, Color, Point, Radians, Rectangle, - Transformation, + self, Background, Color, Point, Rectangle, Svg, Transformation, }; use crate::graphics::damage; use crate::graphics::layer; use crate::graphics::text::{Editor, Paragraph, Text}; use crate::graphics::{self, Image}; -use crate::Primitive; use std::rc::Rc; @@ -72,7 +72,7 @@ impl Layer { pub fn draw_text( &mut self, - text: crate::core::Text, + text: core::Text, position: Point, color: Color, clip_bounds: Rectangle, @@ -115,42 +115,35 @@ impl Layer { .push(Item::Cached(text, clip_bounds, transformation)); } - pub fn draw_image( + pub fn draw_image(&mut self, image: Image, transformation: Transformation) { + match image { + Image::Raster(raster, bounds) => { + self.draw_raster(raster, bounds, transformation); + } + Image::Vector(svg, bounds) => { + self.draw_svg(svg, bounds, transformation); + } + } + } + + pub fn draw_raster( &mut self, - handle: image::Handle, - filter_method: image::FilterMethod, + image: core::Image, bounds: Rectangle, transformation: Transformation, - rotation: Radians, - opacity: f32, ) { - let image = Image::Raster { - handle, - filter_method, - bounds: bounds * transformation, - rotation, - opacity, - }; + let image = Image::Raster(image, bounds * transformation); self.images.push(image); } pub fn draw_svg( &mut self, - handle: svg::Handle, - color: Option<Color>, + svg: Svg, bounds: Rectangle, transformation: Transformation, - rotation: Radians, - opacity: f32, ) { - let svg = Image::Vector { - handle, - color, - bounds: bounds * transformation, - rotation, - opacity, - }; + let svg = Image::Vector(svg, bounds * transformation); self.images.push(svg); } @@ -293,7 +286,7 @@ impl graphics::Layer for Layer { fn flush(&mut self) {} - fn resize(&mut self, bounds: graphics::core::Rectangle) { + fn resize(&mut self, bounds: Rectangle) { self.bounds = bounds; } diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index a7fc2d7c..f34f7e76 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -29,12 +29,12 @@ pub use geometry::Geometry; use crate::core::renderer; use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Transformation, + Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, }; use crate::engine::Engine; +use crate::graphics::Viewport; use crate::graphics::compositor; use crate::graphics::text::{Editor, Paragraph}; -use crate::graphics::Viewport; /// A [`tiny-skia`] graphics renderer for [`iced`]. /// @@ -147,6 +147,16 @@ impl Renderer { engine::adjust_clip_mask(clip_mask, clip_bounds); } + for image in &layer.images { + self.engine.draw_image( + image, + Transformation::scale(scale_factor), + pixels, + clip_mask, + clip_bounds, + ); + } + for group in &layer.text { for text in group.as_slice() { self.engine.draw_text( @@ -159,16 +169,6 @@ impl Renderer { ); } } - - for image in &layer.images { - self.engine.draw_image( - image, - Transformation::scale(scale_factor), - pixels, - clip_mask, - clip_bounds, - ); - } } } @@ -280,6 +280,7 @@ impl graphics::geometry::Renderer for Renderer { match geometry { Geometry::Live { primitives, + images, text, clip_bounds, } => { @@ -289,6 +290,10 @@ impl graphics::geometry::Renderer for Renderer { transformation, ); + for image in images { + layer.draw_image(image, transformation); + } + layer.draw_text_group(text, clip_bounds, transformation); } Geometry::Cache(cache) => { @@ -298,6 +303,10 @@ impl graphics::geometry::Renderer for Renderer { transformation, ); + for image in cache.images.iter() { + layer.draw_image(image.clone(), transformation); + } + layer.draw_text_cache( cache.text, cache.clip_bounds, @@ -322,23 +331,9 @@ impl core::image::Renderer for Renderer { self.engine.raster_pipeline.dimensions(handle) } - fn draw_image( - &mut self, - handle: Self::Handle, - filter_method: core::image::FilterMethod, - bounds: Rectangle, - rotation: core::Radians, - opacity: f32, - ) { + fn draw_image(&mut self, image: core::Image, bounds: Rectangle) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_image( - handle, - filter_method, - bounds, - transformation, - rotation, - opacity, - ); + layer.draw_raster(image, bounds, transformation); } } @@ -351,26 +346,30 @@ impl core::svg::Renderer for Renderer { self.engine.vector_pipeline.viewport_dimensions(handle) } - fn draw_svg( - &mut self, - handle: core::svg::Handle, - color: Option<Color>, - bounds: Rectangle, - rotation: core::Radians, - opacity: f32, - ) { + fn draw_svg(&mut self, svg: core::Svg, bounds: Rectangle) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg( - handle, - color, - bounds, - transformation, - rotation, - opacity, - ); + layer.draw_svg(svg, bounds, transformation); } } impl compositor::Default for Renderer { type Compositor = window::Compositor; } + +impl renderer::Headless for Renderer { + fn new(default_font: Font, default_text_size: Pixels) -> Self { + Self::new(default_font, default_text_size) + } + + fn screenshot( + &mut self, + size: Size<u32>, + scale_factor: f32, + background_color: Color, + ) -> Vec<u8> { + let viewport = + Viewport::with_physical_size(size, f64::from(scale_factor)); + + window::compositor::screenshot(self, &viewport, background_color) + } +} diff --git a/tiny_skia/src/text.rs b/tiny_skia/src/text.rs index c71deb10..0fc3d1f7 100644 --- a/tiny_skia/src/text.rs +++ b/tiny_skia/src/text.rs @@ -169,7 +169,13 @@ impl Pipeline { font_system.raw(), &mut self.glyph_cache, buffer, - Rectangle::new(position, Size::new(width, height)), + Rectangle::new( + position, + Size::new( + width.unwrap_or(pixels.width() as f32), + height.unwrap_or(pixels.height() as f32), + ), + ), color, alignment::Horizontal::Left, alignment::Vertical::Top, diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs index bbe08cb8..ea7de215 100644 --- a/tiny_skia/src/vector.rs +++ b/tiny_skia/src/vector.rs @@ -1,14 +1,14 @@ use crate::core::svg::{Data, Handle}; use crate::core::{Color, Rectangle, Size}; -use crate::graphics::text; -use resvg::usvg::{self, TreeTextToPath}; +use resvg::usvg; use rustc_hash::{FxHashMap, FxHashSet}; use tiny_skia::Transform; use std::cell::RefCell; use std::collections::hash_map; use std::fs; +use std::sync::Arc; #[derive(Debug)] pub struct Pipeline { @@ -69,6 +69,7 @@ struct Cache { tree_hits: FxHashSet<u64>, rasters: FxHashMap<RasterKey, tiny_skia::Pixmap>, raster_hits: FxHashSet<RasterKey>, + fontdb: Option<Arc<usvg::fontdb::Database>>, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -80,35 +81,37 @@ struct RasterKey { impl Cache { fn load(&mut self, handle: &Handle) -> Option<&usvg::Tree> { - use usvg::TreeParsing; - let id = handle.id(); + // TODO: Reuse `cosmic-text` font database + if self.fontdb.is_none() { + let mut fontdb = usvg::fontdb::Database::new(); + fontdb.load_system_fonts(); + + self.fontdb = Some(Arc::new(fontdb)); + } + + let options = usvg::Options { + fontdb: self + .fontdb + .as_ref() + .expect("fontdb must be initialized") + .clone(), + ..usvg::Options::default() + }; + if let hash_map::Entry::Vacant(entry) = self.trees.entry(id) { - let mut svg = match handle.data() { + let svg = match handle.data() { Data::Path(path) => { fs::read_to_string(path).ok().and_then(|contents| { - usvg::Tree::from_str( - &contents, - &usvg::Options::default(), - ) - .ok() + usvg::Tree::from_str(&contents, &options).ok() }) } Data::Bytes(bytes) => { - usvg::Tree::from_data(bytes, &usvg::Options::default()).ok() + usvg::Tree::from_data(bytes, &options).ok() } }; - if let Some(svg) = &mut svg { - if svg.has_text_nodes() { - let mut font_system = - text::font_system().write().expect("Write font system"); - - svg.convert_text(font_system.raw().db_mut()); - } - } - let _ = entry.insert(svg); } @@ -118,11 +121,9 @@ impl Cache { fn viewport_dimensions(&mut self, handle: &Handle) -> Option<Size<u32>> { let tree = self.load(handle)?; + let size = tree.size(); - Some(Size::new( - tree.size.width() as u32, - tree.size.height() as u32, - )) + Some(Size::new(size.width() as u32, size.height() as u32)) } fn draw( @@ -147,7 +148,7 @@ impl Cache { let mut image = tiny_skia::Pixmap::new(size.width, size.height)?; - let tree_size = tree.size.to_int_size(); + let tree_size = tree.size().to_int_size(); let target_size = if size.width > size.height { tree_size.scale_to_width(size.width) @@ -167,7 +168,7 @@ impl Cache { tiny_skia::Transform::default() }; - resvg::Tree::from_usvg(tree).render(transform, &mut image.as_mut()); + resvg::render(tree, transform, &mut image.as_mut()); if let Some([r, g, b, _]) = key.color { // Apply color filter diff --git a/tiny_skia/src/window/compositor.rs b/tiny_skia/src/window/compositor.rs index 2ad90a73..f14ad34e 100644 --- a/tiny_skia/src/window/compositor.rs +++ b/tiny_skia/src/window/compositor.rs @@ -120,11 +120,10 @@ impl crate::graphics::Compositor for Compositor { fn screenshot( &mut self, renderer: &mut Self::Renderer, - surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, ) -> Vec<u8> { - screenshot(renderer, surface, viewport, background_color) + screenshot(renderer, viewport, background_color) } } @@ -208,7 +207,6 @@ pub fn present( pub fn screenshot( renderer: &mut Renderer, - surface: &mut Surface, viewport: &Viewport, background_color: Color, ) -> Vec<u8> { @@ -217,6 +215,9 @@ pub fn screenshot( let mut offscreen_buffer: Vec<u32> = vec![0; size.width as usize * size.height as usize]; + let mut clip_mask = tiny_skia::Mask::new(size.width, size.height) + .expect("Create clip mask"); + renderer.draw( &mut tiny_skia::PixmapMut::from_bytes( bytemuck::cast_slice_mut(&mut offscreen_buffer), @@ -224,7 +225,7 @@ pub fn screenshot( size.height, ) .expect("Create offscreen pixel map"), - &mut surface.clip_mask, + &mut clip_mask, viewport, &[Rectangle::with_size(Size::new( size.width as f32, diff --git a/wgpu/Cargo.toml b/wgpu/Cargo.toml index 30545fa2..4b6b0483 100644 --- a/wgpu/Cargo.toml +++ b/wgpu/Cargo.toml @@ -20,9 +20,10 @@ all-features = true [features] geometry = ["iced_graphics/geometry", "lyon"] image = ["iced_graphics/image"] -svg = ["resvg/text"] +svg = ["iced_graphics/svg", "resvg/text"] web-colors = ["iced_graphics/web-colors"] webgl = ["wgpu/webgl"] +strict-assertions = [] [dependencies] iced_graphics.workspace = true @@ -34,7 +35,6 @@ glam.workspace = true glyphon.workspace = true guillotiere.workspace = true log.workspace = true -once_cell.workspace = true rustc-hash.workspace = true thiserror.workspace = true wgpu.workspace = true diff --git a/wgpu/README.md b/wgpu/README.md index 95d7028a..8e9602ea 100644 --- a/wgpu/README.md +++ b/wgpu/README.md @@ -6,14 +6,7 @@ `iced_wgpu` is a [`wgpu`] renderer for [`iced_runtime`]. For now, it is the default renderer of Iced on [native platforms]. -[`wgpu`] supports most modern graphics backends: Vulkan, Metal, and DX12 (OpenGL and WebGL are still WIP). Additionally, it will support the incoming [WebGPU API]. - -Currently, `iced_wgpu` supports the following primitives: -- Text, which is rendered using [`wgpu_glyph`]. No shaping at all. -- Quads or rectangles, with rounded borders and a solid background color. -- Clip areas, useful to implement scrollables or hide overflowing content. -- Images and SVG, loaded from memory or the file system. -- Meshes of triangles, useful to draw geometry freely. +[`wgpu`] supports most modern graphics backends: Vulkan, Metal, DX12, OpenGL, and WebGPU. <p align="center"> <img alt="The native target" src="../docs/graphs/native.png" width="80%"> @@ -25,29 +18,3 @@ Currently, `iced_wgpu` supports the following primitives: [native platforms]: https://github.com/gfx-rs/wgpu#supported-platforms [WebGPU API]: https://gpuweb.github.io/gpuweb/ [`wgpu_glyph`]: https://github.com/hecrj/wgpu_glyph - -## Installation -Add `iced_wgpu` as a dependency in your `Cargo.toml`: - -```toml -iced_wgpu = "0.10" -``` - -__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 - -## Current limitations - -The current implementation is quite naive; it uses: - -- A different pipeline/shader for each primitive -- A very simplistic layer model: every `Clip` primitive will generate new layers -- _Many_ render passes instead of preparing everything upfront -- A glyph cache that is trimmed incorrectly when there are multiple layers (a [`glyph_brush`] limitation) - -Some of these issues are already being worked on! If you want to help, [get in touch!] - -[get in touch!]: ../CONTRIBUTING.md -[`glyph_brush`]: https://github.com/alexheretic/glyph-brush diff --git a/wgpu/src/color.rs b/wgpu/src/color.rs index 9d593d9c..0f2c202f 100644 --- a/wgpu/src/color.rs +++ b/wgpu/src/color.rs @@ -108,12 +108,14 @@ pub fn convert( layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "vs_main", + entry_point: Some("vs_main"), buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::default( + ), }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "fs_main", + entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format, blend: Some(wgpu::BlendState { @@ -130,6 +132,8 @@ pub fn convert( }), write_mask: wgpu::ColorWrites::ALL, })], + compilation_options: wgpu::PipelineCompilationOptions::default( + ), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, @@ -139,6 +143,7 @@ pub fn convert( depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview: None, + cache: None, }); let texture = device.create_texture(&wgpu::TextureDescriptor { diff --git a/wgpu/src/geometry.rs b/wgpu/src/geometry.rs index f6213e1d..8d161015 100644 --- a/wgpu/src/geometry.rs +++ b/wgpu/src/geometry.rs @@ -1,7 +1,7 @@ //! Build and draw geometry. use crate::core::text::LineHeight; use crate::core::{ - Pixels, Point, Radians, Rectangle, Size, Transformation, Vector, + self, Pixels, Point, Radians, Rectangle, Size, Svg, Transformation, Vector, }; use crate::graphics::cache::{self, Cached}; use crate::graphics::color; @@ -11,7 +11,7 @@ use crate::graphics::geometry::{ }; use crate::graphics::gradient::{self, Gradient}; use crate::graphics::mesh::{self, Mesh}; -use crate::graphics::{self, Text}; +use crate::graphics::{Image, Text}; use crate::text; use crate::triangle; @@ -19,16 +19,22 @@ use lyon::geom::euclid; use lyon::tessellation; use std::borrow::Cow; +use std::sync::Arc; #[derive(Debug)] pub enum Geometry { - Live { meshes: Vec<Mesh>, text: Vec<Text> }, + Live { + meshes: Vec<Mesh>, + images: Vec<Image>, + text: Vec<Text>, + }, Cached(Cache), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Cache { pub meshes: Option<triangle::Cache>, + pub images: Option<Arc<[Image]>>, pub text: Option<text::Cache>, } @@ -45,7 +51,17 @@ impl Cached for Geometry { previous: Option<Self::Cache>, ) -> Self::Cache { match self { - Self::Live { meshes, text } => { + Self::Live { + meshes, + images, + text, + } => { + let images = if images.is_empty() { + None + } else { + Some(Arc::from(images)) + }; + if let Some(mut previous) = previous { if let Some(cache) = &mut previous.meshes { cache.update(meshes); @@ -59,10 +75,13 @@ impl Cached for Geometry { previous.text = text::Cache::new(group, text); } + previous.images = images; + previous } else { Cache { meshes: triangle::Cache::new(meshes), + images, text: text::Cache::new(group, text), } } @@ -78,6 +97,7 @@ pub struct Frame { clip_bounds: Rectangle, buffers: BufferStack, meshes: Vec<Mesh>, + images: Vec<Image>, text: Vec<Text>, transforms: Transforms, fill_tessellator: tessellation::FillTessellator, @@ -96,6 +116,7 @@ impl Frame { clip_bounds: bounds, buffers: BufferStack::new(), meshes: Vec::new(), + images: Vec::new(), text: Vec::new(), transforms: Transforms { previous: Vec::new(), @@ -232,6 +253,44 @@ impl geometry::frame::Backend for Frame { .expect("Stroke path"); } + fn stroke_rectangle<'a>( + &mut self, + top_left: Point, + size: Size, + stroke: impl Into<Stroke<'a>>, + ) { + let stroke = stroke.into(); + + let mut buffer = self + .buffers + .get_stroke(&self.transforms.current.transform_style(stroke.style)); + + let top_left = self + .transforms + .current + .0 + .transform_point(lyon::math::Point::new(top_left.x, top_left.y)); + + let size = + self.transforms.current.0.transform_vector( + lyon::math::Vector::new(size.width, size.height), + ); + + let mut options = tessellation::StrokeOptions::default(); + options.line_width = stroke.width; + options.start_cap = into_line_cap(stroke.line_cap); + options.end_cap = into_line_cap(stroke.line_cap); + options.line_join = into_line_join(stroke.line_join); + + self.stroke_tessellator + .tessellate_rectangle( + &lyon::math::Box2D::new(top_left, top_left + size), + &options, + buffer.as_mut(), + ) + .expect("Stroke rectangle"); + } + fn fill_text(&mut self, text: impl Into<geometry::Text>) { let text = text.into(); @@ -270,7 +329,7 @@ impl geometry::frame::Backend for Frame { height: f32::INFINITY, }; - self.text.push(graphics::Text::Cached { + self.text.push(Text::Cached { content: text.content, bounds, color: text.color, @@ -335,10 +394,12 @@ impl geometry::frame::Backend for Frame { Frame::with_clip(clip_bounds) } - fn paste(&mut self, frame: Frame, _at: Point) { + fn paste(&mut self, frame: Frame) { + self.meshes.extend(frame.meshes); self.meshes .extend(frame.buffers.into_meshes(frame.clip_bounds)); + self.images.extend(frame.images); self.text.extend(frame.text); } @@ -348,9 +409,32 @@ impl geometry::frame::Backend for Frame { Geometry::Live { meshes: self.meshes, + images: self.images, text: self.text, } } + + fn draw_image(&mut self, bounds: Rectangle, image: impl Into<core::Image>) { + let mut image = image.into(); + + let (bounds, external_rotation) = + self.transforms.current.transform_rectangle(bounds); + + image.rotation += external_rotation; + + self.images.push(Image::Raster(image, bounds)); + } + + fn draw_svg(&mut self, bounds: Rectangle, svg: impl Into<Svg>) { + let mut svg = svg.into(); + + let (bounds, external_rotation) = + self.transforms.current.transform_rectangle(bounds); + + svg.rotation += external_rotation; + + self.images.push(Image::Vector(svg, bounds)); + } } enum Buffer { @@ -518,6 +602,21 @@ impl Transform { gradient } + + fn transform_rectangle( + &self, + rectangle: Rectangle, + ) -> (Rectangle, Radians) { + let top_left = self.transform_point(rectangle.position()); + let top_right = self.transform_point( + rectangle.position() + Vector::new(rectangle.width, 0.0), + ); + let bottom_left = self.transform_point( + rectangle.position() + Vector::new(0.0, rectangle.height), + ); + + Rectangle::with_vertices(top_left, top_right, bottom_left) + } } struct GradientVertex2DBuilder { gradient: gradient::Packed, @@ -614,7 +713,7 @@ fn into_fill_rule(rule: fill::Rule) -> lyon::tessellation::FillRule { pub(super) fn dashed(path: &Path, line_dash: LineDash<'_>) -> Path { use lyon::algorithms::walk::{ - walk_along_path, RepeatedPattern, WalkerEvent, + RepeatedPattern, WalkerEvent, walk_along_path, }; use lyon::path::iterator::PathIterator; diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index daa2fe16..6cb18a07 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -9,8 +9,8 @@ mod raster; #[cfg(feature = "svg")] mod vector; -use crate::core::{Rectangle, Size, Transformation}; use crate::Buffer; +use crate::core::{Rectangle, Size, Transformation}; use bytemuck::{Pod, Zeroable}; @@ -128,7 +128,7 @@ impl Pipeline { layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "vs_main", + entry_point: Some("vs_main"), buffers: &[wgpu::VertexBufferLayout { array_stride: mem::size_of::<Instance>() as u64, step_mode: wgpu::VertexStepMode::Instance, @@ -149,12 +149,16 @@ impl Pipeline { 6 => Float32x2, // Layer 7 => Sint32, + // Snap + 8 => Uint32, ), }], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "fs_main", + entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format, blend: Some(wgpu::BlendState { @@ -171,6 +175,8 @@ impl Pipeline { }), write_mask: wgpu::ColorWrites::ALL, })], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, @@ -184,6 +190,7 @@ impl Pipeline { alpha_to_coverage_enabled: false, }, multiview: None, + cache: None, }); Pipeline { @@ -212,31 +219,24 @@ impl Pipeline { transformation: Transformation, scale: f32, ) { - let transformation = transformation * Transformation::scale(scale); - let nearest_instances: &mut Vec<Instance> = &mut Vec::new(); let linear_instances: &mut Vec<Instance> = &mut Vec::new(); for image in images { match &image { #[cfg(feature = "image")] - Image::Raster { - handle, - filter_method, - bounds, - rotation, - opacity, - } => { + Image::Raster(image, bounds) => { if let Some(atlas_entry) = - cache.upload_raster(device, encoder, handle) + cache.upload_raster(device, encoder, &image.handle) { add_instances( [bounds.x, bounds.y], [bounds.width, bounds.height], - f32::from(*rotation), - *opacity, + f32::from(image.rotation), + image.opacity, + image.snap, atlas_entry, - match filter_method { + match image.filter_method { crate::core::image::FilterMethod::Nearest => { nearest_instances } @@ -251,23 +251,23 @@ impl Pipeline { Image::Raster { .. } => {} #[cfg(feature = "svg")] - Image::Vector { - handle, - color, - bounds, - rotation, - opacity, - } => { + Image::Vector(svg, bounds) => { let size = [bounds.width, bounds.height]; if let Some(atlas_entry) = cache.upload_vector( - device, encoder, handle, *color, size, scale, + device, + encoder, + &svg.handle, + svg.color, + size, + scale, ) { add_instances( [bounds.x, bounds.y], size, - f32::from(*rotation), - *opacity, + f32::from(svg.rotation), + svg.opacity, + true, atlas_entry, nearest_instances, ); @@ -300,6 +300,7 @@ impl Pipeline { nearest_instances, linear_instances, transformation, + scale, ); self.prepare_layer += 1; @@ -375,9 +376,12 @@ impl Layer { nearest_instances: &[Instance], linear_instances: &[Instance], transformation: Transformation, + scale_factor: f32, ) { let uniforms = Uniforms { transform: transformation.into(), + scale_factor, + _padding: [0.0; 3], }; let bytes = bytemuck::bytes_of(&uniforms); @@ -492,6 +496,7 @@ struct Instance { _position_in_atlas: [f32; 2], _size_in_atlas: [f32; 2], _layer: u32, + _snap: u32, } impl Instance { @@ -502,6 +507,10 @@ impl Instance { #[derive(Debug, Clone, Copy, Zeroable, Pod)] struct Uniforms { transform: [f32; 16], + scale_factor: f32, + // Uniforms must be aligned to their largest member, + // this uses a mat4x4<f32> which aligns to 16, so align to that + _padding: [f32; 3], } fn add_instances( @@ -509,6 +518,7 @@ fn add_instances( image_size: [f32; 2], rotation: f32, opacity: f32, + snap: bool, entry: &atlas::Entry, instances: &mut Vec<Instance>, ) { @@ -525,6 +535,7 @@ fn add_instances( image_size, rotation, opacity, + snap, allocation, instances, ); @@ -554,8 +565,8 @@ fn add_instances( ]; add_instance( - position, center, size, rotation, opacity, allocation, - instances, + position, center, size, rotation, opacity, snap, + allocation, instances, ); } } @@ -569,6 +580,7 @@ fn add_instance( size: [f32; 2], rotation: f32, opacity: f32, + snap: bool, allocation: &atlas::Allocation, instances: &mut Vec<Instance>, ) { @@ -591,6 +603,7 @@ fn add_instance( (height as f32 - 1.0) / atlas::SIZE as f32, ], _layer: layer as u32, + _snap: snap as u32, }; instances.push(instance); diff --git a/wgpu/src/image/raster.rs b/wgpu/src/image/raster.rs index 4d3c3125..83777807 100644 --- a/wgpu/src/image/raster.rs +++ b/wgpu/src/image/raster.rs @@ -1,5 +1,5 @@ -use crate::core::image; use crate::core::Size; +use crate::core::image; use crate::graphics; use crate::graphics::image::image_rs; use crate::image::atlas::{self, Atlas}; diff --git a/wgpu/src/image/vector.rs b/wgpu/src/image/vector.rs index c6d829af..e55ade38 100644 --- a/wgpu/src/image/vector.rs +++ b/wgpu/src/image/vector.rs @@ -1,12 +1,12 @@ use crate::core::svg; use crate::core::{Color, Size}; -use crate::graphics::text; use crate::image::atlas::{self, Atlas}; use resvg::tiny_skia; -use resvg::usvg::{self, TreeTextToPath}; +use resvg::usvg; use rustc_hash::{FxHashMap, FxHashSet}; use std::fs; +use std::sync::Arc; /// Entry in cache corresponding to an svg handle pub enum Svg { @@ -21,7 +21,7 @@ impl Svg { pub fn viewport_dimensions(&self) -> Size<u32> { match self { Svg::Loaded(tree) => { - let size = tree.size; + let size = tree.size(); Size::new(size.width() as u32, size.height() as u32) } @@ -38,6 +38,7 @@ pub struct Cache { svg_hits: FxHashSet<u64>, rasterized_hits: FxHashSet<(u64, u32, u32, ColorFilter)>, should_trim: bool, + fontdb: Option<Arc<usvg::fontdb::Database>>, } type ColorFilter = Option<[u8; 4]>; @@ -45,38 +46,43 @@ type ColorFilter = Option<[u8; 4]>; impl Cache { /// Load svg pub fn load(&mut self, handle: &svg::Handle) -> &Svg { - use usvg::TreeParsing; - if self.svgs.contains_key(&handle.id()) { return self.svgs.get(&handle.id()).unwrap(); } - let mut svg = match handle.data() { + // TODO: Reuse `cosmic-text` font database + if self.fontdb.is_none() { + let mut fontdb = usvg::fontdb::Database::new(); + fontdb.load_system_fonts(); + + self.fontdb = Some(Arc::new(fontdb)); + } + + let options = usvg::Options { + fontdb: self + .fontdb + .as_ref() + .expect("fontdb must be initialized") + .clone(), + ..usvg::Options::default() + }; + + let svg = match handle.data() { svg::Data::Path(path) => fs::read_to_string(path) .ok() .and_then(|contents| { - usvg::Tree::from_str(&contents, &usvg::Options::default()) - .ok() + usvg::Tree::from_str(&contents, &options).ok() }) .map(Svg::Loaded) .unwrap_or(Svg::NotFound), svg::Data::Bytes(bytes) => { - match usvg::Tree::from_data(bytes, &usvg::Options::default()) { + match usvg::Tree::from_data(bytes, &options) { Ok(tree) => Svg::Loaded(tree), Err(_) => Svg::NotFound, } } }; - if let Svg::Loaded(svg) = &mut svg { - if svg.has_text_nodes() { - let mut font_system = - text::font_system().write().expect("Write font system"); - - svg.convert_text(font_system.raw().db_mut()); - } - } - self.should_trim = true; let _ = self.svgs.insert(handle.id(), svg); @@ -127,7 +133,7 @@ impl Cache { // It would be cool to be able to smooth resize the `svg` example. let mut img = tiny_skia::Pixmap::new(width, height)?; - let tree_size = tree.size.to_int_size(); + let tree_size = tree.size().to_int_size(); let target_size = if width > height { tree_size.scale_to_width(width) @@ -147,8 +153,7 @@ impl Cache { tiny_skia::Transform::default() }; - resvg::Tree::from_usvg(tree) - .render(transform, &mut img.as_mut()); + resvg::render(tree, transform, &mut img.as_mut()); let mut rgba = img.take(); diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 9551311d..2255b3f2 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,11 +1,11 @@ use crate::core::{ - renderer, Background, Color, Point, Radians, Rectangle, Transformation, + self, Background, Color, Point, Rectangle, Svg, Transformation, renderer, }; use crate::graphics; +use crate::graphics::Mesh; use crate::graphics::color; use crate::graphics::layer; use crate::graphics::text::{Editor, Paragraph}; -use crate::graphics::Mesh; use crate::image::{self, Image}; use crate::primitive::{self, Primitive}; use crate::quad::{self, Quad}; @@ -20,8 +20,8 @@ pub struct Layer { pub quads: quad::Batch, pub triangles: triangle::Batch, pub primitives: primitive::Batch, - pub text: text::Batch, pub images: image::Batch, + pub text: text::Batch, pending_meshes: Vec<Mesh>, pending_text: Vec<Text>, } @@ -112,42 +112,35 @@ impl Layer { self.pending_text.push(text); } - pub fn draw_image( + pub fn draw_image(&mut self, image: Image, transformation: Transformation) { + match image { + Image::Raster(image, bounds) => { + self.draw_raster(image, bounds, transformation); + } + Image::Vector(svg, bounds) => { + self.draw_svg(svg, bounds, transformation); + } + } + } + + pub fn draw_raster( &mut self, - handle: crate::core::image::Handle, - filter_method: crate::core::image::FilterMethod, + image: core::Image, bounds: Rectangle, transformation: Transformation, - rotation: Radians, - opacity: f32, ) { - let image = Image::Raster { - handle, - filter_method, - bounds: bounds * transformation, - rotation, - opacity, - }; + let image = Image::Raster(image, bounds * transformation); self.images.push(image); } pub fn draw_svg( &mut self, - handle: crate::core::svg::Handle, - color: Option<Color>, + svg: Svg, bounds: Rectangle, transformation: Transformation, - rotation: Radians, - opacity: f32, ) { - let svg = Image::Vector { - handle, - color, - bounds: bounds * transformation, - rotation, - opacity, - }; + let svg = Image::Vector(svg, bounds * transformation); self.images.push(svg); } diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 9d6c09f5..9e48a0f4 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -63,8 +63,8 @@ pub use geometry::Geometry; use crate::core::{ Background, Color, Font, Pixels, Point, Rectangle, Transformation, }; -use crate::graphics::text::{Editor, Paragraph}; use crate::graphics::Viewport; +use crate::graphics::text::{Editor, Paragraph}; /// A [`wgpu`] graphics renderer for [`iced`]. /// @@ -142,7 +142,19 @@ impl Renderer { self.text_viewport.update(queue, viewport.physical_size()); + let physical_bounds = Rectangle::<f32>::from(Rectangle::with_size( + viewport.physical_size(), + )); + for layer in self.layers.iter_mut() { + if physical_bounds + .intersection(&(layer.bounds * scale_factor)) + .and_then(Rectangle::snap) + .is_none() + { + continue; + } + if !layer.quads.is_empty() { engine.quad_pipeline.prepare( device, @@ -179,19 +191,6 @@ impl Renderer { } } - if !layer.text.is_empty() { - engine.text_pipeline.prepare( - device, - queue, - &self.text_viewport, - encoder, - &mut self.text_storage, - &layer.text, - layer.bounds, - Transformation::scale(scale_factor), - ); - } - #[cfg(any(feature = "svg", feature = "image"))] if !layer.images.is_empty() { engine.image_pipeline.prepare( @@ -204,6 +203,19 @@ impl Renderer { scale_factor, ); } + + if !layer.text.is_empty() { + engine.text_pipeline.prepare( + device, + queue, + &self.text_viewport, + encoder, + &mut self.text_storage, + &layer.text, + layer.bounds, + Transformation::scale(scale_factor), + ); + } } } @@ -266,7 +278,7 @@ impl Renderer { for layer in self.layers.iter() { let Some(physical_bounds) = - physical_bounds.intersection(&(layer.bounds * scale)) + physical_bounds.intersection(&(layer.bounds * scale_factor)) else { continue; }; @@ -356,17 +368,6 @@ impl Renderer { )); } - if !layer.text.is_empty() { - text_layer += engine.text_pipeline.render( - &self.text_viewport, - &self.text_storage, - text_layer, - &layer.text, - scissor_rect, - &mut render_pass, - ); - } - #[cfg(any(feature = "svg", feature = "image"))] if !layer.images.is_empty() { engine.image_pipeline.render( @@ -378,6 +379,17 @@ impl Renderer { image_layer += 1; } + + if !layer.text.is_empty() { + text_layer += engine.text_pipeline.render( + &self.text_viewport, + &self.text_storage, + text_layer, + &layer.text, + scissor_rect, + &mut render_pass, + ); + } } let _ = ManuallyDrop::into_inner(render_pass); @@ -481,23 +493,9 @@ impl core::image::Renderer for Renderer { self.image_cache.borrow_mut().measure_image(handle) } - fn draw_image( - &mut self, - handle: Self::Handle, - filter_method: core::image::FilterMethod, - bounds: Rectangle, - rotation: core::Radians, - opacity: f32, - ) { + fn draw_image(&mut self, image: core::Image, bounds: Rectangle) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_image( - handle, - filter_method, - bounds, - transformation, - rotation, - opacity, - ); + layer.draw_raster(image, bounds, transformation); } } @@ -507,28 +505,24 @@ impl core::svg::Renderer for Renderer { self.image_cache.borrow_mut().measure_svg(handle) } - fn draw_svg( - &mut self, - handle: core::svg::Handle, - color_filter: Option<Color>, - bounds: Rectangle, - rotation: core::Radians, - opacity: f32, - ) { + fn draw_svg(&mut self, svg: core::Svg, bounds: Rectangle) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg( - handle, - color_filter, - bounds, - transformation, - rotation, - opacity, - ); + layer.draw_svg(svg, bounds, transformation); } } impl graphics::mesh::Renderer for Renderer { fn draw_mesh(&mut self, mesh: graphics::Mesh) { + debug_assert!( + !mesh.indices().is_empty(), + "Mesh must not have empty indices" + ); + + debug_assert!( + mesh.indices().len() % 3 == 0, + "Mesh indices length must be a multiple of 3" + ); + let (layer, transformation) = self.layers.current_mut(); layer.draw_mesh(mesh, transformation); } @@ -547,8 +541,17 @@ impl graphics::geometry::Renderer for Renderer { let (layer, transformation) = self.layers.current_mut(); match geometry { - Geometry::Live { meshes, text } => { + Geometry::Live { + meshes, + images, + text, + } => { layer.draw_mesh_group(meshes, transformation); + + for image in images { + layer.draw_image(image, transformation); + } + layer.draw_text_group(text, transformation); } Geometry::Cached(cache) => { @@ -556,6 +559,12 @@ impl graphics::geometry::Renderer for Renderer { layer.draw_mesh_cache(meshes, transformation); } + if let Some(images) = cache.images { + for image in images.iter().cloned() { + layer.draw_image(image, transformation); + } + } + if let Some(text) = cache.text { layer.draw_text_cache(text, transformation); } diff --git a/wgpu/src/quad/gradient.rs b/wgpu/src/quad/gradient.rs index 5b32c52a..68c11157 100644 --- a/wgpu/src/quad/gradient.rs +++ b/wgpu/src/quad/gradient.rs @@ -1,6 +1,6 @@ +use crate::Buffer; use crate::graphics::gradient; use crate::quad::{self, Quad}; -use crate::Buffer; use bytemuck::{Pod, Zeroable}; use std::ops::Range; @@ -124,7 +124,7 @@ impl Pipeline { layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "gradient_vs_main", + entry_point: Some("gradient_vs_main"), buffers: &[wgpu::VertexBufferLayout { array_stride: std::mem::size_of::<Gradient>() as u64, @@ -152,11 +152,15 @@ impl Pipeline { 9 => Float32 ), }], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "gradient_fs_main", + entry_point: Some("gradient_fs_main"), targets: &quad::color_target_state(format), + compilation_options: + wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, @@ -170,6 +174,7 @@ impl Pipeline { alpha_to_coverage_enabled: false, }, multiview: None, + cache: None, }, ); @@ -188,9 +193,6 @@ impl Pipeline { layer: &'a Layer, range: Range<usize>, ) { - #[cfg(feature = "tracing")] - let _ = tracing::info_span!("Wgpu::Quad::Gradient", "DRAW").entered(); - #[cfg(not(target_arch = "wasm32"))] { render_pass.set_pipeline(&self.pipeline); diff --git a/wgpu/src/quad/solid.rs b/wgpu/src/quad/solid.rs index 1cead367..b6d88486 100644 --- a/wgpu/src/quad/solid.rs +++ b/wgpu/src/quad/solid.rs @@ -1,6 +1,6 @@ +use crate::Buffer; use crate::graphics::color; use crate::quad::{self, Quad}; -use crate::Buffer; use bytemuck::{Pod, Zeroable}; use std::ops::Range; @@ -89,7 +89,7 @@ impl Pipeline { layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "solid_vs_main", + entry_point: Some("solid_vs_main"), buffers: &[wgpu::VertexBufferLayout { array_stride: std::mem::size_of::<Solid>() as u64, step_mode: wgpu::VertexStepMode::Instance, @@ -114,11 +114,15 @@ impl Pipeline { 8 => Float32, ), }], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "solid_fs_main", + entry_point: Some("solid_fs_main"), targets: &quad::color_target_state(format), + compilation_options: + wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, @@ -132,6 +136,7 @@ impl Pipeline { alpha_to_coverage_enabled: false, }, multiview: None, + cache: None, }); Self { pipeline } @@ -144,9 +149,6 @@ impl Pipeline { layer: &'a Layer, range: Range<usize>, ) { - #[cfg(feature = "tracing")] - let _ = tracing::info_span!("Wgpu::Quad::Solid", "DRAW").entered(); - render_pass.set_pipeline(&self.pipeline); render_pass.set_bind_group(0, constants, &[]); render_pass.set_vertex_buffer(0, layer.instances.slice(..)); diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl index 0eeb100f..bc922838 100644 --- a/wgpu/src/shader/image.wgsl +++ b/wgpu/src/shader/image.wgsl @@ -1,5 +1,6 @@ struct Globals { transform: mat4x4<f32>, + scale_factor: f32, } @group(0) @binding(0) var<uniform> globals: Globals; @@ -16,6 +17,7 @@ struct VertexInput { @location(5) atlas_pos: vec2<f32>, @location(6) atlas_scale: vec2<f32>, @location(7) layer: i32, + @location(8) snap: u32, } struct VertexOutput { @@ -38,7 +40,7 @@ fn vs_main(input: VertexInput) -> VertexOutput { out.opacity = input.opacity; // Calculate the vertex position and move the center to the origin - v_pos = round(input.pos) + v_pos * input.scale - input.center; + v_pos = input.pos + v_pos * input.scale - input.center; // Apply the rotation around the center of the image let cos_rot = cos(input.rotation); @@ -51,7 +53,13 @@ fn vs_main(input: VertexInput) -> VertexOutput { ); // Calculate the final position of the vertex - out.position = globals.transform * (vec4<f32>(input.center, 0.0, 0.0) + rotate * vec4<f32>(v_pos, 0.0, 1.0)); + out.position = vec4(vec2(globals.scale_factor), 1.0, 1.0) * (vec4<f32>(input.center, 0.0, 0.0) + rotate * vec4<f32>(v_pos, 0.0, 1.0)); + + if bool(input.snap) { + out.position = round(out.position); + } + + out.position = globals.transform * out.position; return out; } diff --git a/wgpu/src/shader/quad.wgsl b/wgpu/src/shader/quad.wgsl index a367d5e6..b213c8cf 100644 --- a/wgpu/src/shader/quad.wgsl +++ b/wgpu/src/shader/quad.wgsl @@ -22,14 +22,14 @@ fn rounded_box_sdf(to_center: vec2<f32>, size: vec2<f32>, radius: f32) -> f32 { return length(max(abs(to_center) - size + vec2<f32>(radius, radius), vec2<f32>(0.0, 0.0))) - radius; } -// Based on the fragment position and the center of the quad, select one of the 4 radi. +// Based on the fragment position and the center of the quad, select one of the 4 radii. // Order matches CSS border radius attribute: -// radi.x = top-left, radi.y = top-right, radi.z = bottom-right, radi.w = bottom-left -fn select_border_radius(radi: vec4<f32>, position: vec2<f32>, center: vec2<f32>) -> f32 { - var rx = radi.x; - var ry = radi.y; - rx = select(radi.x, radi.y, position.x > center.x); - ry = select(radi.w, radi.z, position.x > center.x); +// radii.x = top-left, radii.y = top-right, radii.z = bottom-right, radii.w = bottom-left +fn select_border_radius(radii: vec4<f32>, position: vec2<f32>, center: vec2<f32>) -> f32 { + var rx = radii.x; + var ry = radii.y; + rx = select(radii.x, radii.y, position.x > center.x); + ry = select(radii.w, radii.z, position.x > center.x); rx = select(rx, ry, position.y > center.y); return rx; } diff --git a/wgpu/src/shader/quad/solid.wgsl b/wgpu/src/shader/quad/solid.wgsl index d908afbc..8eee16bb 100644 --- a/wgpu/src/shader/quad/solid.wgsl +++ b/wgpu/src/shader/quad/solid.wgsl @@ -30,6 +30,15 @@ fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput { var pos: vec2<f32> = (input.pos + min(input.shadow_offset, vec2<f32>(0.0, 0.0)) - input.shadow_blur_radius) * globals.scale; var scale: vec2<f32> = (input.scale + vec2<f32>(abs(input.shadow_offset.x), abs(input.shadow_offset.y)) + input.shadow_blur_radius * 2.0) * globals.scale; + var snap: vec2<f32> = vec2<f32>(0.0, 0.0); + + if input.scale.x == 1.0 { + snap.x = round(pos.x) - pos.x; + } + + if input.scale.y == 1.0 { + snap.y = round(pos.y) - pos.y; + } var min_border_radius = min(input.scale.x, input.scale.y) * 0.5; var border_radius: vec4<f32> = vec4<f32>( @@ -43,13 +52,13 @@ fn solid_vs_main(input: SolidVertexInput) -> SolidVertexOutput { vec4<f32>(scale.x + 1.0, 0.0, 0.0, 0.0), vec4<f32>(0.0, scale.y + 1.0, 0.0, 0.0), vec4<f32>(0.0, 0.0, 1.0, 0.0), - vec4<f32>(pos - vec2<f32>(0.5, 0.5), 0.0, 1.0) + vec4<f32>(pos - vec2<f32>(0.5, 0.5) + snap, 0.0, 1.0) ); out.position = globals.transform * transform * vec4<f32>(vertex_position(input.vertex_index), 0.0, 1.0); out.color = input.color; out.border_color = input.border_color; - out.pos = input.pos * globals.scale; + out.pos = input.pos * globals.scale + snap; out.scale = input.scale * globals.scale; out.border_radius = border_radius * globals.scale; out.border_width = input.border_width * globals.scale; diff --git a/wgpu/src/text.rs b/wgpu/src/text.rs index 05db5f80..33fbd4dc 100644 --- a/wgpu/src/text.rs +++ b/wgpu/src/text.rs @@ -3,13 +3,12 @@ use crate::core::{Rectangle, Size, Transformation}; use crate::graphics::cache; use crate::graphics::color; use crate::graphics::text::cache::{self as text_cache, Cache as BufferCache}; -use crate::graphics::text::{font_system, to_color, Editor, Paragraph}; +use crate::graphics::text::{Editor, Paragraph, font_system, to_color}; use rustc_hash::FxHashMap; use std::collections::hash_map; -use std::rc::{self, Rc}; use std::sync::atomic::{self, AtomicU64}; -use std::sync::Arc; +use std::sync::{self, Arc}; pub use crate::graphics::Text; @@ -37,7 +36,7 @@ pub enum Item { pub struct Cache { id: Id, group: cache::Group, - text: Rc<[Text]>, + text: Arc<[Text]>, version: usize, } @@ -55,7 +54,7 @@ impl Cache { Some(Self { id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)), group, - text: Rc::from(text), + text: Arc::from(text), version: 0, }) } @@ -65,7 +64,7 @@ impl Cache { return; } - self.text = Rc::from(text); + self.text = Arc::from(text); self.version += 1; } } @@ -76,8 +75,8 @@ struct Upload { transformation: Transformation, version: usize, group_version: usize, - text: rc::Weak<[Text]>, - _atlas: rc::Weak<()>, + text: sync::Weak<[Text]>, + _atlas: sync::Weak<()>, } #[derive(Default)] @@ -90,7 +89,7 @@ struct Group { atlas: glyphon::TextAtlas, version: usize, should_trim: bool, - handle: Rc<()>, // Keeps track of active uploads + handle: Arc<()>, // Keeps track of active uploads } impl Storage { @@ -136,7 +135,7 @@ impl Storage { ), version: 0, should_trim: false, - handle: Rc::new(()), + handle: Arc::new(()), } }); @@ -167,7 +166,7 @@ impl Storage { group.should_trim = group.should_trim || upload.version != cache.version; - upload.text = Rc::downgrade(&cache.text); + upload.text = Arc::downgrade(&cache.text); upload.version = cache.version; upload.group_version = group.version; upload.transformation = new_transformation; @@ -206,8 +205,8 @@ impl Storage { transformation: new_transformation, version: 0, group_version: group.version, - text: Rc::downgrade(&cache.text), - _atlas: Rc::downgrade(&group.handle), + text: Arc::downgrade(&cache.text), + _atlas: Arc::downgrade(&group.handle), }); group.should_trim = cache.group.is_singleton(); @@ -226,7 +225,7 @@ impl Storage { .retain(|_id, upload| upload.text.strong_count() > 0); self.groups.retain(|id, group| { - let active_uploads = Rc::weak_count(&group.handle); + let active_uploads = Arc::weak_count(&group.handle); if active_uploads == 0 { log::debug!("Dropping text atlas: {id:?}"); @@ -585,7 +584,13 @@ fn prepare( ( buffer.as_ref(), - Rectangle::new(raw.position, Size::new(width, height)), + Rectangle::new( + raw.position, + Size::new( + width.unwrap_or(layer_bounds.width), + height.unwrap_or(layer_bounds.height), + ), + ), alignment::Horizontal::Left, alignment::Vertical::Top, raw.color, diff --git a/wgpu/src/triangle.rs b/wgpu/src/triangle.rs index b0551f55..a2b976ea 100644 --- a/wgpu/src/triangle.rs +++ b/wgpu/src/triangle.rs @@ -1,15 +1,15 @@ //! Draw meshes of triangles. mod msaa; -use crate::core::{Rectangle, Size, Transformation}; -use crate::graphics::mesh::{self, Mesh}; -use crate::graphics::Antialiasing; use crate::Buffer; +use crate::core::{Rectangle, Size, Transformation}; +use crate::graphics::Antialiasing; +use crate::graphics::mesh::{self, Mesh}; use rustc_hash::FxHashMap; use std::collections::hash_map; -use std::rc::{self, Rc}; use std::sync::atomic::{self, AtomicU64}; +use std::sync::{self, Arc}; const INITIAL_INDEX_COUNT: usize = 1_000; const INITIAL_VERTEX_COUNT: usize = 1_000; @@ -31,7 +31,7 @@ pub enum Item { #[derive(Debug, Clone)] pub struct Cache { id: Id, - batch: Rc<[Mesh]>, + batch: Arc<[Mesh]>, version: usize, } @@ -48,13 +48,13 @@ impl Cache { Some(Self { id: Id(NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)), - batch: Rc::from(meshes), + batch: Arc::from(meshes), version: 0, }) } pub fn update(&mut self, meshes: Vec<Mesh>) { - self.batch = Rc::from(meshes); + self.batch = Arc::from(meshes); self.version += 1; } } @@ -64,7 +64,7 @@ struct Upload { layer: Layer, transformation: Transformation, version: usize, - batch: rc::Weak<[Mesh]>, + batch: sync::Weak<[Mesh]>, } #[derive(Debug, Default)] @@ -113,7 +113,7 @@ impl Storage { new_transformation, ); - upload.batch = Rc::downgrade(&cache.batch); + upload.batch = Arc::downgrade(&cache.batch); upload.version = cache.version; upload.transformation = new_transformation; } @@ -135,7 +135,7 @@ impl Storage { layer, transformation: new_transformation, version: 0, - batch: Rc::downgrade(&cache.batch), + batch: Arc::downgrade(&cache.batch), }); log::debug!( @@ -505,6 +505,14 @@ impl Layer { .intersection(&(mesh.clip_bounds() * transformation)) .and_then(Rectangle::snap) else { + match mesh { + Mesh::Solid { .. } => { + num_solids += 1; + } + Mesh::Gradient { .. } => { + num_gradients += 1; + } + } continue; }; @@ -636,10 +644,10 @@ impl Uniforms { } mod solid { - use crate::graphics::mesh; - use crate::graphics::Antialiasing; - use crate::triangle; use crate::Buffer; + use crate::graphics::Antialiasing; + use crate::graphics::mesh; + use crate::triangle; #[derive(Debug)] pub struct Pipeline { @@ -745,7 +753,7 @@ mod solid { layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "solid_vs_main", + entry_point: Some("solid_vs_main"), buffers: &[wgpu::VertexBufferLayout { array_stride: std::mem::size_of::< mesh::SolidVertex2D, @@ -760,16 +768,21 @@ mod solid { 1 => Float32x4, ), }], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "solid_fs_main", + entry_point: Some("solid_fs_main"), targets: &[Some(triangle::fragment_target(format))], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }), primitive: triangle::primitive_state(), depth_stencil: None, multisample: triangle::multisample_state(antialiasing), multiview: None, + cache: None, }, ); @@ -782,11 +795,11 @@ mod solid { } mod gradient { + use crate::Buffer; + use crate::graphics::Antialiasing; use crate::graphics::color; use crate::graphics::mesh; - use crate::graphics::Antialiasing; use crate::triangle; - use crate::Buffer; #[derive(Debug)] pub struct Pipeline { @@ -913,7 +926,7 @@ mod gradient { layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "gradient_vs_main", + entry_point: Some("gradient_vs_main"), buffers: &[wgpu::VertexBufferLayout { array_stride: std::mem::size_of::< mesh::GradientVertex2D, @@ -937,16 +950,21 @@ mod gradient { 6 => Float32x4 ), }], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "gradient_fs_main", + entry_point: Some("gradient_fs_main"), targets: &[Some(triangle::fragment_target(format))], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }), primitive: triangle::primitive_state(), depth_stencil: None, multisample: triangle::multisample_state(antialiasing), multiview: None, + cache: None, }, ); diff --git a/wgpu/src/triangle/msaa.rs b/wgpu/src/triangle/msaa.rs index 71c16925..0a5b134f 100644 --- a/wgpu/src/triangle/msaa.rs +++ b/wgpu/src/triangle/msaa.rs @@ -110,12 +110,14 @@ impl Blit { layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, - entry_point: "vs_main", + entry_point: Some("vs_main"), buffers: &[], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, - entry_point: "fs_main", + entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format, blend: Some( @@ -123,6 +125,8 @@ impl Blit { ), write_mask: wgpu::ColorWrites::ALL, })], + compilation_options: + wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, @@ -136,6 +140,7 @@ impl Blit { alpha_to_coverage_enabled: false, }, multiview: None, + cache: None, }); Blit { diff --git a/wgpu/src/window/compositor.rs b/wgpu/src/window/compositor.rs index b49df899..cd7699d1 100644 --- a/wgpu/src/window/compositor.rs +++ b/wgpu/src/window/compositor.rs @@ -56,6 +56,11 @@ impl Compositor { ) -> Result<Self, Error> { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: settings.backends, + flags: if cfg!(feature = "strict-assertions") { + wgpu::InstanceFlags::debugging() + } else { + wgpu::InstanceFlags::empty() + }, ..Default::default() }); @@ -162,6 +167,7 @@ impl Compositor { ), required_features: wgpu::Features::empty(), required_limits: required_limits.clone(), + memory_hints: wgpu::MemoryHints::MemoryUsage, }, None, ) @@ -361,7 +367,6 @@ impl graphics::Compositor for Compositor { fn screenshot( &mut self, renderer: &mut Self::Renderer, - _surface: &mut Self::Surface, viewport: &Viewport, background_color: Color, ) -> Vec<u8> { diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 3c9f6a54..6d1f054e 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -22,8 +22,10 @@ lazy = ["ouroboros"] image = ["iced_renderer/image"] svg = ["iced_renderer/svg"] canvas = ["iced_renderer/geometry"] -qr_code = ["canvas", "qrcode"] +qr_code = ["canvas", "dep:qrcode"] wgpu = ["iced_renderer/wgpu"] +markdown = ["dep:pulldown-cmark", "dep:url"] +highlighter = ["dep:iced_highlighter"] advanced = [] [dependencies] @@ -31,6 +33,7 @@ iced_renderer.workspace = true iced_runtime.workspace = true num-traits.workspace = true +log.workspace = true rustc-hash.workspace = true thiserror.workspace = true unicode-segmentation.workspace = true @@ -40,3 +43,12 @@ ouroboros.optional = true qrcode.workspace = true qrcode.optional = true + +pulldown-cmark.workspace = true +pulldown-cmark.optional = true + +iced_highlighter.workspace = true +iced_highlighter.optional = true + +url.workspace = true +url.optional = true diff --git a/widget/assets/iced-logo.svg b/widget/assets/iced-logo.svg new file mode 100644 index 00000000..459b7fbb --- /dev/null +++ b/widget/assets/iced-logo.svg @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" width="140" height="140" fill="none" version="1.1" viewBox="35 31 179 171"><rect x="42" y="31.001" width="169.9" height="169.9" rx="49.815" fill="url(#paint1_linear)"/><path d="m182.62 65.747-28.136 28.606-6.13-6.0291 28.136-28.606 6.13 6.0291zm-26.344 0.218-42.204 42.909-6.13-6.029 42.204-42.909 6.13 6.0291zm-61.648 23.913c5.3254-5.3831 10.65-10.765 21.569-21.867l6.13 6.0291c-10.927 11.11-16.258 16.498-21.587 21.885-4.4007 4.4488-8.8009 8.8968-16.359 16.573l31.977 8.358 25.968-26.402 6.13 6.0292-25.968 26.402 8.907 31.908 42.138-42.087 6.076 6.083-49.109 49.05-45.837-12.628-13.394-45.646 1.7714-1.801c10.928-11.111 16.258-16.499 21.588-21.886zm28.419 70.99-8.846-31.689-31.831-8.32 9.1945 31.335 31.482 8.674zm47.734-56.517 7.122-7.1221-6.08-6.0797-7.147 7.1474-30.171 30.674 6.13 6.029 30.146-30.649z" clip-rule="evenodd" fill="url(#paint2_linear)" fill-rule="evenodd"/><defs><filter id="filter0_f" x="55" y="47.001" width="144" height="168" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur" stdDeviation="2"/></filter><linearGradient id="paint0_linear" x1="127" x2="127" y1="51.001" y2="211" gradientUnits="userSpaceOnUse"><stop offset=".052083"/><stop stop-opacity=".08" offset="1"/></linearGradient><linearGradient id="paint1_linear" x1="212" x2="57.5" y1="31.001" y2="189" gradientUnits="userSpaceOnUse"><stop stop-color="#00A3FF" offset="0"/><stop stop-color="#30f" offset="1"/></linearGradient><linearGradient id="paint2_linear" x1="86.098" x2="206.01" y1="158.28" y2="35.327" gradientUnits="userSpaceOnUse"><stop stop-color="#fff" offset="0"/><stop stop-color="#fff" offset="1"/></linearGradient></defs></svg> diff --git a/widget/src/action.rs b/widget/src/action.rs new file mode 100644 index 00000000..cc31e76a --- /dev/null +++ b/widget/src/action.rs @@ -0,0 +1,85 @@ +use crate::core::event; +use crate::core::time::Instant; +use crate::core::window; + +/// A runtime action that can be performed by some widgets. +#[derive(Debug, Clone)] +pub struct Action<Message> { + message_to_publish: Option<Message>, + redraw_request: window::RedrawRequest, + event_status: event::Status, +} + +impl<Message> Action<Message> { + fn new() -> Self { + Self { + message_to_publish: None, + redraw_request: window::RedrawRequest::Wait, + event_status: event::Status::Ignored, + } + } + + /// Creates a new "capturing" [`Action`]. A capturing [`Action`] + /// will make other widgets consider it final and prevent further + /// processing. + /// + /// Prevents "event bubbling". + pub fn capture() -> Self { + Self { + event_status: event::Status::Captured, + ..Self::new() + } + } + + /// Creates a new [`Action`] that publishes the given `Message` for + /// the application to handle. + /// + /// Publishing a `Message` always produces a redraw. + pub fn publish(message: Message) -> Self { + Self { + message_to_publish: Some(message), + ..Self::new() + } + } + + /// Creates a new [`Action`] that requests a redraw to happen as + /// soon as possible; without publishing any `Message`. + pub fn request_redraw() -> Self { + Self { + redraw_request: window::RedrawRequest::NextFrame, + ..Self::new() + } + } + + /// Creates a new [`Action`] that requests a redraw to happen at + /// the given [`Instant`]; without publishing any `Message`. + /// + /// This can be useful to efficiently animate content, like a + /// blinking caret on a text input. + pub fn request_redraw_at(at: Instant) -> Self { + Self { + redraw_request: window::RedrawRequest::At(at), + ..Self::new() + } + } + + /// Marks the [`Action`] as "capturing". See [`Self::capture`]. + pub fn and_capture(mut self) -> Self { + self.event_status = event::Status::Captured; + self + } + + /// Converts the [`Action`] into its internal parts. + /// + /// This method is meant to be used by runtimes, libraries, or internal + /// widget implementations. + pub fn into_inner( + self, + ) -> (Option<Message>, window::RedrawRequest, event::Status) { + ( + self.message_to_publish, + self.redraw_request, + self.event_status, + ) + } +} diff --git a/widget/src/button.rs b/widget/src/button.rs index dc949671..d4500888 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -1,48 +1,71 @@ -//! Allow your users to perform actions by pressing a button. -use crate::core::event::{self, Event}; +//! Buttons allow your users to perform actions by pressing them. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::button; +//! +//! #[derive(Clone)] +//! enum Message { +//! ButtonPressed, +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! button("Press me!").on_press(Message::ButtonPressed).into() +//! } +//! ``` +use crate::core::border::{self, Border}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::theme::palette; use crate::core::touch; -use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; +use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ - Background, Border, Clipboard, Color, Element, Layout, Length, Padding, + Background, Clipboard, Color, Element, Event, Layout, Length, Padding, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, }; /// A generic widget that produces a message when pressed. /// +/// # Example /// ```no_run -/// # type Button<'a, Message> = iced_widget::Button<'a, Message>; -/// # +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::button; +/// /// #[derive(Clone)] /// enum Message { /// ButtonPressed, /// } /// -/// let button = Button::new("Press me!").on_press(Message::ButtonPressed); +/// fn view(state: &State) -> Element<'_, Message> { +/// button("Press me!").on_press(Message::ButtonPressed).into() +/// } /// ``` /// /// If a [`Button::on_press`] handler is not set, the resulting [`Button`] will /// be disabled: /// -/// ``` -/// # type Button<'a, Message> = iced_widget::Button<'a, Message>; -/// # +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::button; +/// /// #[derive(Clone)] /// enum Message { /// ButtonPressed, /// } /// -/// fn disabled_button<'a>() -> Button<'a, Message> { -/// Button::new("I'm disabled!") -/// } -/// -/// fn enabled_button<'a>() -> Button<'a, Message> { -/// disabled_button().on_press(Message::ButtonPressed) +/// fn view(state: &State) -> Element<'_, Message> { +/// button("I am disabled!").into() /// } /// ``` #[allow(missing_debug_implementations)] @@ -52,12 +75,27 @@ where Theme: Catalog, { content: Element<'a, Message, Theme, Renderer>, - on_press: Option<Message>, + on_press: Option<OnPress<'a, Message>>, width: Length, height: Length, padding: Padding, clip: bool, class: Theme::Class<'a>, + status: Option<Status>, +} + +enum OnPress<'a, Message> { + Direct(Message), + Closure(Box<dyn Fn() -> Message + 'a>), +} + +impl<Message: Clone> OnPress<'_, Message> { + fn get(&self) -> Message { + match self { + OnPress::Direct(message) => message.clone(), + OnPress::Closure(f) => f(), + } + } } impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer> @@ -80,6 +118,7 @@ where padding: DEFAULT_PADDING, clip: false, class: Theme::default(), + status: None, } } @@ -105,7 +144,23 @@ where /// /// Unless `on_press` is called, the [`Button`] will be disabled. pub fn on_press(mut self, on_press: Message) -> Self { - self.on_press = Some(on_press); + self.on_press = Some(OnPress::Direct(on_press)); + self + } + + /// Sets the message that will be produced when the [`Button`] is pressed. + /// + /// This is analogous to [`Button::on_press`], but using a closure to produce + /// the message. + /// + /// This closure will only be called when the [`Button`] is actually pressed and, + /// therefore, this method is useful to reduce overhead if creating the resulting + /// message is slow. + pub fn on_press_with( + mut self, + on_press: impl Fn() -> Message + 'a, + ) -> Self { + self.on_press = Some(OnPress::Closure(Box::new(on_press))); self } @@ -114,7 +169,7 @@ where /// /// If `None`, the [`Button`] will be disabled. pub fn on_press_maybe(mut self, on_press: Option<Message>) -> Self { - self.on_press = on_press; + self.on_press = on_press.map(OnPress::Direct); self } @@ -205,7 +260,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.content.as_widget().operate( @@ -217,28 +272,30 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - if let event::Status::Captured = self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().update( &mut tree.children[0], - event.clone(), + event, layout.children().next().unwrap(), cursor, renderer, clipboard, shell, viewport, - ) { - return event::Status::Captured; + ); + + if shell.is_event_captured() { + return; } match event { @@ -252,13 +309,13 @@ where state.is_pressed = true; - return event::Status::Captured; + shell.capture_event(); } } } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) => { - if let Some(on_press) = self.on_press.clone() { + if let Some(on_press) = &self.on_press { let state = tree.state.downcast_mut::<State>(); if state.is_pressed { @@ -267,10 +324,10 @@ where let bounds = layout.bounds(); if cursor.is_over(bounds) { - shell.publish(on_press); + shell.publish(on_press.get()); } - return event::Status::Captured; + shell.capture_event(); } } } @@ -282,7 +339,25 @@ where _ => {} } - event::Status::Ignored + let current_status = if self.on_press.is_none() { + Status::Disabled + } else if cursor.is_over(layout.bounds()) { + let state = tree.state.downcast_ref::<State>(); + + if state.is_pressed { + Status::Pressed + } else { + Status::Hovered + } + } else { + Status::Active + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.status = Some(current_status); + } else if self.status.is_some_and(|status| status != current_status) { + shell.request_redraw(); + } } fn draw( @@ -297,23 +372,8 @@ where ) { let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); - let is_mouse_over = cursor.is_over(bounds); - - let status = if self.on_press.is_none() { - Status::Disabled - } else if is_mouse_over { - let state = tree.state.downcast_ref::<State>(); - - if state.is_pressed { - Status::Pressed - } else { - Status::Hovered - } - } else { - Status::Active - }; - - let style = theme.style(&self.class, status); + let style = + theme.style(&self.class, self.status.unwrap_or(Status::Disabled)); if style.background.is_some() || style.border.width > 0.0 @@ -417,15 +477,18 @@ pub enum Status { } /// The style of a button. +/// +/// If not specified with [`Button::style`] +/// the theme will provide the style. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`Background`] of the button. pub background: Option<Background>, /// The text [`Color`] of the button. pub text_color: Color, - /// The [`Border`] of the buton. + /// The [`Border`] of the button. pub border: Border, - /// The [`Shadow`] of the butoon. + /// The [`Shadow`] of the button. pub shadow: Shadow, } @@ -451,6 +514,54 @@ impl Default for Style { } /// The theme catalog of a [`Button`]. +/// +/// All themes that can be used with [`Button`] +/// must implement this trait. +/// +/// # Example +/// ```no_run +/// # use iced_widget::core::{Color, Background}; +/// # use iced_widget::button::{Catalog, Status, Style}; +/// # struct MyTheme; +/// #[derive(Debug, Default)] +/// pub enum ButtonClass { +/// #[default] +/// Primary, +/// Secondary, +/// Danger +/// } +/// +/// impl Catalog for MyTheme { +/// type Class<'a> = ButtonClass; +/// +/// fn default<'a>() -> Self::Class<'a> { +/// ButtonClass::default() +/// } +/// +/// +/// fn style(&self, class: &Self::Class<'_>, status: Status) -> Style { +/// let mut style = Style::default(); +/// +/// match class { +/// ButtonClass::Primary => { +/// style.background = Some(Background::Color(Color::from_rgb(0.529, 0.808, 0.921))); +/// }, +/// ButtonClass::Secondary => { +/// style.background = Some(Background::Color(Color::WHITE)); +/// }, +/// ButtonClass::Danger => { +/// style.background = Some(Background::Color(Color::from_rgb(0.941, 0.502, 0.502))); +/// }, +/// } +/// +/// style +/// } +/// } +/// ``` +/// +/// Although, in order to use [`Button::style`] +/// with `MyTheme`, [`Catalog::Class`] must implement +/// `From<StyleFn<'_, MyTheme>>`. pub trait Catalog { /// The item class of the [`Catalog`]. type Class<'a>; @@ -480,12 +591,12 @@ impl Catalog for Theme { /// A primary button; denoting a main action. pub fn primary(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); - let base = styled(palette.primary.strong); + let base = styled(palette.primary.base); match status { Status::Active | Status::Pressed => base, Status::Hovered => Style { - background: Some(Background::Color(palette.primary.base.color)), + background: Some(Background::Color(palette.primary.strong.color)), ..base }, Status::Disabled => disabled(base), @@ -522,6 +633,21 @@ pub fn success(theme: &Theme, status: Status) -> Style { } } +/// A warning button; denoting a risky action. +pub fn warning(theme: &Theme, status: Status) -> Style { + let palette = theme.extended_palette(); + let base = styled(palette.warning.base); + + match status { + Status::Active | Status::Pressed => base, + Status::Hovered => Style { + background: Some(Background::Color(palette.warning.strong.color)), + ..base + }, + Status::Disabled => disabled(base), + } +} + /// A danger button; denoting a destructive action. pub fn danger(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); @@ -560,7 +686,7 @@ fn styled(pair: palette::Pair) -> Style { Style { background: Some(Background::Color(pair.color)), text_color: pair.text, - border: Border::rounded(2), + border: border::rounded(2), ..Style::default() } } diff --git a/widget/src/canvas.rs b/widget/src/canvas.rs index be09f163..50acade5 100644 --- a/widget/src/canvas.rs +++ b/widget/src/canvas.rs @@ -1,22 +1,71 @@ -//! Draw 2D graphics for your users. -pub mod event; - +//! Canvases can be leveraged to draw interactive 2D graphics. +//! +//! # Example: Drawing a Simple Circle +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::mouse; +//! use iced::widget::canvas; +//! use iced::{Color, Rectangle, Renderer, Theme}; +//! +//! // First, we define the data we need for drawing +//! #[derive(Debug)] +//! struct Circle { +//! radius: f32, +//! } +//! +//! // Then, we implement the `Program` trait +//! impl<Message> canvas::Program<Message> for Circle { +//! // No internal state +//! type State = (); +//! +//! fn draw( +//! &self, +//! _state: &(), +//! renderer: &Renderer, +//! _theme: &Theme, +//! bounds: Rectangle, +//! _cursor: mouse::Cursor +//! ) -> Vec<canvas::Geometry> { +//! // We prepare a new `Frame` +//! let mut frame = canvas::Frame::new(renderer, bounds.size()); +//! +//! // We create a `Path` representing a simple circle +//! let circle = canvas::Path::circle(frame.center(), self.radius); +//! +//! // And fill it with some color +//! frame.fill(&circle, Color::BLACK); +//! +//! // Then, we produce the geometry +//! vec![frame.into_geometry()] +//! } +//! } +//! +//! // Finally, we simply use our `Circle` to create the `Canvas`! +//! fn view<'a, Message: 'a>(_state: &'a State) -> Element<'a, Message> { +//! canvas(Circle { radius: 50.0 }).into() +//! } +//! ``` mod program; -pub use event::Event; pub use program::Program; +pub use crate::Action; +pub use crate::core::event::Event; pub use crate::graphics::cache::Group; pub use crate::graphics::geometry::{ - fill, gradient, path, stroke, Fill, Gradient, LineCap, LineDash, LineJoin, - Path, Stroke, Style, Text, + Fill, Gradient, Image, LineCap, LineDash, LineJoin, Path, Stroke, Style, + Text, fill, gradient, path, stroke, }; -use crate::core; +use crate::core::event; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ Clipboard, Element, Length, Rectangle, Shell, Size, Vector, Widget, }; @@ -39,15 +88,16 @@ pub type Frame<Renderer = crate::Renderer> = geometry::Frame<Renderer>; /// A widget capable of drawing 2D graphics. /// -/// ## Drawing a simple circle -/// If you want to get a quick overview, here's how we can draw a simple circle: -/// +/// # Example: Drawing a Simple Circle /// ```no_run -/// # use iced_widget::canvas::{self, Canvas, Fill, Frame, Geometry, Path, Program}; -/// # use iced_widget::core::{Color, Rectangle}; -/// # use iced_widget::core::mouse; -/// # use iced_widget::{Renderer, Theme}; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # +/// use iced::mouse; +/// use iced::widget::canvas; +/// use iced::{Color, Rectangle, Renderer, Theme}; +/// /// // First, we define the data we need for drawing /// #[derive(Debug)] /// struct Circle { @@ -55,26 +105,36 @@ pub type Frame<Renderer = crate::Renderer> = geometry::Frame<Renderer>; /// } /// /// // Then, we implement the `Program` trait -/// impl Program<()> for Circle { +/// impl<Message> canvas::Program<Message> for Circle { +/// // No internal state /// type State = (); /// -/// fn draw(&self, _state: &(), renderer: &Renderer, _theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor) -> Vec<Geometry> { +/// fn draw( +/// &self, +/// _state: &(), +/// renderer: &Renderer, +/// _theme: &Theme, +/// bounds: Rectangle, +/// _cursor: mouse::Cursor +/// ) -> Vec<canvas::Geometry> { /// // We prepare a new `Frame` -/// let mut frame = Frame::new(renderer, bounds.size()); +/// let mut frame = canvas::Frame::new(renderer, bounds.size()); /// /// // We create a `Path` representing a simple circle -/// let circle = Path::circle(frame.center(), self.radius); +/// let circle = canvas::Path::circle(frame.center(), self.radius); /// /// // And fill it with some color /// frame.fill(&circle, Color::BLACK); /// -/// // Finally, we produce the geometry +/// // Then, we produce the geometry /// vec![frame.into_geometry()] /// } /// } /// /// // Finally, we simply use our `Circle` to create the `Canvas`! -/// let canvas = Canvas::new(Circle { radius: 50.0 }); +/// fn view<'a, Message: 'a>(_state: &'a State) -> Element<'a, Message> { +/// canvas(Circle { radius: 50.0 }).into() +/// } /// ``` #[derive(Debug)] pub struct Canvas<P, Message, Theme = crate::Theme, Renderer = crate::Renderer> @@ -88,6 +148,7 @@ where message_: PhantomData<Message>, theme_: PhantomData<Theme>, renderer_: PhantomData<Renderer>, + last_mouse_interaction: Option<mouse::Interaction>, } impl<P, Message, Theme, Renderer> Canvas<P, Message, Theme, Renderer> @@ -106,6 +167,7 @@ where message_: PhantomData, theme_: PhantomData, renderer_: PhantomData, + last_mouse_interaction: None, } } @@ -153,42 +215,54 @@ where layout::atomic(limits, self.width, self.height) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: core::Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, - _renderer: &Renderer, + renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - _viewport: &Rectangle, - ) -> event::Status { + viewport: &Rectangle, + ) { let bounds = layout.bounds(); - let canvas_event = match event { - core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)), - core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), - core::Event::Keyboard(keyboard_event) => { - Some(Event::Keyboard(keyboard_event)) - } - _ => None, - }; + let state = tree.state.downcast_mut::<P::State>(); + let is_redraw_request = matches!( + event, + Event::Window(window::Event::RedrawRequested(_now)), + ); - if let Some(canvas_event) = canvas_event { - let state = tree.state.downcast_mut::<P::State>(); + if let Some(action) = self.program.update(state, event, bounds, cursor) + { + let (message, redraw_request, event_status) = action.into_inner(); - let (event_status, message) = - self.program.update(state, canvas_event, bounds, cursor); + shell.request_redraw_at(redraw_request); if let Some(message) = message { shell.publish(message); } - return event_status; + if event_status == event::Status::Captured { + shell.capture_event(); + } } - event::Status::Ignored + if shell.redraw_request() != window::RedrawRequest::NextFrame { + let mouse_interaction = self + .mouse_interaction(tree, layout, cursor, viewport, renderer); + + if is_redraw_request { + self.last_mouse_interaction = Some(mouse_interaction); + } else if self.last_mouse_interaction.is_some_and( + |last_mouse_interaction| { + last_mouse_interaction != mouse_interaction + }, + ) { + shell.request_redraw(); + } + } } fn mouse_interaction( diff --git a/widget/src/canvas/event.rs b/widget/src/canvas/event.rs deleted file mode 100644 index a8eb47f7..00000000 --- a/widget/src/canvas/event.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Handle events of a canvas. -use crate::core::keyboard; -use crate::core::mouse; -use crate::core::touch; - -pub use crate::core::event::Status; - -/// A [`Canvas`] event. -/// -/// [`Canvas`]: crate::Canvas -#[derive(Debug, Clone, PartialEq)] -pub enum Event { - /// A mouse event. - Mouse(mouse::Event), - - /// A touch event. - Touch(touch::Event), - - /// A keyboard event. - Keyboard(keyboard::Event), -} diff --git a/widget/src/canvas/program.rs b/widget/src/canvas/program.rs index a7ded0f4..9644f676 100644 --- a/widget/src/canvas/program.rs +++ b/widget/src/canvas/program.rs @@ -1,6 +1,6 @@ -use crate::canvas::event::{self, Event}; +use crate::Action; use crate::canvas::mouse; -use crate::canvas::Geometry; +use crate::canvas::{Event, Geometry}; use crate::core::Rectangle; use crate::graphics::geometry; @@ -22,8 +22,9 @@ where /// When a [`Program`] is used in a [`Canvas`], the runtime will call this /// method for each [`Event`]. /// - /// This method can optionally return a `Message` to notify an application - /// of any meaningful interactions. + /// This method can optionally return an [`Action`] to either notify an + /// application of any meaningful interactions, capture the event, or + /// request a redraw. /// /// By default, this method does and returns nothing. /// @@ -31,11 +32,11 @@ where fn update( &self, _state: &mut Self::State, - _event: Event, + _event: &Event, _bounds: Rectangle, _cursor: mouse::Cursor, - ) -> (event::Status, Option<Message>) { - (event::Status::Ignored, None) + ) -> Option<Action<Message>> { + None } /// Draws the state of the [`Program`], producing a bunch of [`Geometry`]. @@ -81,10 +82,10 @@ where fn update( &self, state: &mut Self::State, - event: Event, + event: &Event, bounds: Rectangle, cursor: mouse::Cursor, - ) -> (event::Status, Option<Message>) { + ) -> Option<Action<Message>> { T::update(self, state, event, bounds, cursor) } diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 225c316d..3c1ef276 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -1,6 +1,36 @@ -//! Show toggle controls using checkboxes. +//! Checkboxes can be used to let users make binary choices. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::checkbox; +//! +//! struct State { +//! is_checked: bool, +//! } +//! +//! enum Message { +//! CheckboxToggled(bool), +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! checkbox("Toggle me!", state.is_checked) +//! .on_toggle(Message::CheckboxToggled) +//! .into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::CheckboxToggled(is_checked) => { +//! state.is_checked = is_checked; +//! } +//! } +//! } +//! ``` +//! ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true) use crate::core::alignment; -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; @@ -9,27 +39,43 @@ use crate::core::theme::palette; use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ - Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, - Rectangle, Shell, Size, Theme, Widget, + Background, Border, Clipboard, Color, Element, Event, Layout, Length, + Pixels, Rectangle, Shell, Size, Theme, Widget, }; /// A box that can be checked. /// /// # Example -/// /// ```no_run -/// # type Checkbox<'a, Message> = iced_widget::Checkbox<'a, Message>; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # -/// pub enum Message { +/// use iced::widget::checkbox; +/// +/// struct State { +/// is_checked: bool, +/// } +/// +/// enum Message { /// CheckboxToggled(bool), /// } /// -/// let is_checked = true; +/// fn view(state: &State) -> Element<'_, Message> { +/// checkbox("Toggle me!", state.is_checked) +/// .on_toggle(Message::CheckboxToggled) +/// .into() +/// } /// -/// Checkbox::new("Toggle me!", is_checked).on_toggle(Message::CheckboxToggled); +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::CheckboxToggled(is_checked) => { +/// state.is_checked = is_checked; +/// } +/// } +/// } /// ``` -/// /// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true) #[allow(missing_debug_implementations)] pub struct Checkbox< @@ -50,9 +96,11 @@ pub struct Checkbox< text_size: Option<Pixels>, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrapping: text::Wrapping, font: Option<Renderer::Font>, icon: Icon<Renderer::Font>, class: Theme::Class<'a>, + last_status: Option<Status>, } impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer> @@ -81,7 +129,8 @@ where spacing: Self::DEFAULT_SPACING, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::default(), + text_wrapping: text::Wrapping::default(), font: None, icon: Icon { font: Renderer::ICON_FONT, @@ -91,6 +140,7 @@ where shaping: text::Shaping::Basic, }, class: Theme::default(), + last_status: None, } } @@ -158,6 +208,12 @@ where self } + /// Sets the [`text::Wrapping`] strategy of the [`Checkbox`]. + pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self { + self.text_wrapping = wrapping; + self + } + /// Sets the [`Renderer::Font`] of the text of the [`Checkbox`]. /// /// [`Renderer::Font`]: crate::core::text::Renderer @@ -191,8 +247,8 @@ where } } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for Checkbox<'a, Message, Theme, Renderer> +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Checkbox<'_, Message, Theme, Renderer> where Renderer: text::Renderer, Theme: Catalog, @@ -240,22 +296,23 @@ where alignment::Horizontal::Left, alignment::Vertical::Top, self.text_shaping, + self.text_wrapping, ) }, ) } - fn on_event( + fn update( &mut self, _tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -264,14 +321,35 @@ where if mouse_over { if let Some(on_toggle) = &self.on_toggle { shell.publish((on_toggle)(!self.is_checked)); - return event::Status::Captured; + shell.capture_event(); } } } _ => {} } - event::Status::Ignored + let current_status = { + let is_mouse_over = cursor.is_over(layout.bounds()); + let is_disabled = self.on_toggle.is_none(); + let is_checked = self.is_checked; + + if is_disabled { + Status::Disabled { is_checked } + } else if is_mouse_over { + Status::Hovered { is_checked } + } else { + Status::Active { is_checked } + } + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.last_status = Some(current_status); + } else if self + .last_status + .is_some_and(|status| status != current_status) + { + shell.request_redraw(); + } } fn mouse_interaction( @@ -296,24 +374,17 @@ where theme: &Theme, defaults: &renderer::Style, layout: Layout<'_>, - cursor: mouse::Cursor, + _cursor: mouse::Cursor, viewport: &Rectangle, ) { - let is_mouse_over = cursor.is_over(layout.bounds()); - let is_disabled = self.on_toggle.is_none(); - let is_checked = self.is_checked; - let mut children = layout.children(); - let status = if is_disabled { - Status::Disabled { is_checked } - } else if is_mouse_over { - Status::Hovered { is_checked } - } else { - Status::Active { is_checked } - }; - - let style = theme.style(&self.class, status); + let style = theme.style( + &self.class, + self.last_status.unwrap_or(Status::Disabled { + is_checked: self.is_checked, + }), + ); { let layout = children.next().unwrap(); @@ -348,6 +419,7 @@ where horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, shaping: *shaping, + wrapping: text::Wrapping::default(), }, bounds.center(), style.icon_color, @@ -358,12 +430,14 @@ where { let label_layout = children.next().unwrap(); + let state: &widget::text::State<Renderer::Paragraph> = + tree.state.downcast_ref(); crate::text::draw( renderer, defaults, label_layout, - tree.state.downcast_ref(), + state.0.raw(), crate::text::Style { color: style.text_color, }, @@ -371,6 +445,16 @@ where ); } } + + fn operate( + &self, + _state: &mut Tree, + layout: Layout<'_>, + _renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + operation.text(None, layout.bounds(), &self.label); + } } impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>> @@ -423,13 +507,13 @@ pub enum Status { } /// The style of a checkbox. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`Background`] of the checkbox. pub background: Background, /// The icon [`Color`] of the checkbox. pub icon_color: Color, - /// The [`Border`] of hte checkbox. + /// The [`Border`] of the checkbox. pub border: Border, /// The text [`Color`] of the checkbox. pub text_color: Option<Color>, @@ -471,18 +555,21 @@ pub fn primary(theme: &Theme, status: Status) -> Style { match status { Status::Active { is_checked } => styled( palette.primary.strong.text, + palette.background.strongest.color, palette.background.base, - palette.primary.strong, + palette.primary.base, is_checked, ), Status::Hovered { is_checked } => styled( palette.primary.strong.text, + palette.background.strongest.color, palette.background.weak, - palette.primary.base, + palette.primary.strong, is_checked, ), Status::Disabled { is_checked } => styled( palette.primary.strong.text, + palette.background.weak.color, palette.background.weak, palette.background.strong, is_checked, @@ -497,18 +584,21 @@ pub fn secondary(theme: &Theme, status: Status) -> Style { match status { Status::Active { is_checked } => styled( palette.background.base.text, + palette.background.strongest.color, palette.background.base, palette.background.strong, is_checked, ), Status::Hovered { is_checked } => styled( palette.background.base.text, + palette.background.strongest.color, palette.background.weak, palette.background.strong, is_checked, ), Status::Disabled { is_checked } => styled( palette.background.strong.color, + palette.background.weak.color, palette.background.weak, palette.background.weak, is_checked, @@ -523,18 +613,21 @@ pub fn success(theme: &Theme, status: Status) -> Style { match status { Status::Active { is_checked } => styled( palette.success.base.text, + palette.background.weak.color, palette.background.base, palette.success.base, is_checked, ), Status::Hovered { is_checked } => styled( palette.success.base.text, + palette.background.strongest.color, palette.background.weak, - palette.success.base, + palette.success.strong, is_checked, ), Status::Disabled { is_checked } => styled( palette.success.base.text, + palette.background.weak.color, palette.background.weak, palette.success.weak, is_checked, @@ -542,25 +635,28 @@ pub fn success(theme: &Theme, status: Status) -> Style { } } -/// A danger checkbox; denoting a negaive toggle. +/// A danger checkbox; denoting a negative toggle. pub fn danger(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); match status { Status::Active { is_checked } => styled( palette.danger.base.text, + palette.background.strongest.color, palette.background.base, palette.danger.base, is_checked, ), Status::Hovered { is_checked } => styled( palette.danger.base.text, + palette.background.strongest.color, palette.background.weak, - palette.danger.base, + palette.danger.strong, is_checked, ), Status::Disabled { is_checked } => styled( palette.danger.base.text, + palette.background.weak.color, palette.background.weak, palette.danger.weak, is_checked, @@ -570,6 +666,7 @@ pub fn danger(theme: &Theme, status: Status) -> Style { fn styled( icon_color: Color, + border_color: Color, base: palette::Pair, accent: palette::Pair, is_checked: bool, @@ -584,7 +681,11 @@ fn styled( border: Border { radius: 2.0.into(), width: 1.0, - color: accent.color, + color: if is_checked { + accent.color + } else { + border_color + }, }, text_color: None, } diff --git a/widget/src/column.rs b/widget/src/column.rs index df7829b3..7200690b 100644 --- a/widget/src/column.rs +++ b/widget/src/column.rs @@ -1,16 +1,37 @@ //! Distribute content vertically. -use crate::core::event::{self, Event}; +use crate::core::alignment::{self, Alignment}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, + Clipboard, Element, Event, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, }; /// A container that distributes its contents vertically. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{button, column}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// column![ +/// "I am on top!", +/// button("I am in the center!"), +/// "I am below.", +/// ].into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> { @@ -19,7 +40,7 @@ pub struct Column<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> width: Length, height: Length, max_width: f32, - align_items: Alignment, + align: Alignment, clip: bool, children: Vec<Element<'a, Message, Theme, Renderer>>, } @@ -63,7 +84,7 @@ where width: Length::Shrink, height: Length::Shrink, max_width: f32::INFINITY, - align_items: Alignment::Start, + align: Alignment::Start, clip: false, children, } @@ -104,8 +125,8 @@ where } /// Sets the horizontal alignment of the contents of the [`Column`] . - pub fn align_items(mut self, align: Alignment) -> Self { - self.align_items = align; + pub fn align_x(mut self, align: impl Into<alignment::Horizontal>) -> Self { + self.align = Alignment::from(align.into()); self } @@ -152,7 +173,7 @@ where } } -impl<'a, Message, Renderer> Default for Column<'a, Message, Renderer> +impl<Message, Renderer> Default for Column<'_, Message, Renderer> where Renderer: crate::core::Renderer, { @@ -161,8 +182,21 @@ where } } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> +impl<'a, Message, Theme, Renderer: crate::core::Renderer> + FromIterator<Element<'a, Message, Theme, Renderer>> for Column<'a, Message, Theme, Renderer> +{ + fn from_iter< + T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, + >( + iter: T, + ) -> Self { + Self::with_children(iter) + } +} + +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Column<'_, Message, Theme, Renderer> where Renderer: crate::core::Renderer, { @@ -197,7 +231,7 @@ where self.height, self.padding, self.spacing, - self.align_items, + self.align, &self.children, &mut tree.children, ) @@ -208,7 +242,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children @@ -223,34 +257,28 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.children + ) { + for ((child, state), layout) in self + .children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) + { + child.as_widget_mut().update( + state, event, layout, cursor, renderer, clipboard, shell, + viewport, + ); + } } fn mouse_interaction( @@ -285,24 +313,21 @@ where viewport: &Rectangle, ) { if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + let viewport = if self.clip { + &clipped_viewport + } else { + viewport + }; + for ((child, state), layout) in self .children .iter() .zip(&tree.children) .zip(layout.children()) + .filter(|(_, layout)| layout.bounds().intersects(viewport)) { child.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor, - if self.clip { - &clipped_viewport - } else { - viewport - }, + state, renderer, theme, style, layout, cursor, viewport, ); } } diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index 253850df..f71e4a6e 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -1,5 +1,59 @@ -//! Display a dropdown list of searchable and selectable options. -use crate::core::event::{self, Event}; +//! Combo boxes display a dropdown list of searchable and selectable options. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::combo_box; +//! +//! struct State { +//! fruits: combo_box::State<Fruit>, +//! favorite: Option<Fruit>, +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Fruit { +//! Apple, +//! Orange, +//! Strawberry, +//! Tomato, +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! FruitSelected(Fruit), +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! combo_box( +//! &state.fruits, +//! "Select your favorite fruit...", +//! state.favorite.as_ref(), +//! Message::FruitSelected +//! ) +//! .into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::FruitSelected(fruit) => { +//! state.favorite = Some(fruit); +//! } +//! } +//! } +//! +//! impl std::fmt::Display for Fruit { +//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +//! f.write_str(match self { +//! Self::Apple => "Apple", +//! Self::Orange => "Orange", +//! Self::Strawberry => "Strawberry", +//! Self::Tomato => "Tomato", +//! }) +//! } +//! } +//! ``` use crate::core::keyboard; use crate::core::keyboard::key; use crate::core::layout::{self, Layout}; @@ -10,7 +64,8 @@ use crate::core::text; use crate::core::time::Instant; use crate::core::widget::{self, Widget}; use crate::core::{ - Clipboard, Element, Length, Padding, Rectangle, Shell, Size, Theme, Vector, + Clipboard, Element, Event, Length, Padding, Rectangle, Shell, Size, Theme, + Vector, }; use crate::overlay::menu; use crate::text::LineHeight; @@ -21,9 +76,60 @@ use std::fmt::Display; /// A widget for searching and selecting a single value from a list of options. /// -/// This widget is composed by a [`TextInput`] that can be filled with the text -/// to search for corresponding values from the list of options that are displayed -/// as a Menu. +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::combo_box; +/// +/// struct State { +/// fruits: combo_box::State<Fruit>, +/// favorite: Option<Fruit>, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Fruit { +/// Apple, +/// Orange, +/// Strawberry, +/// Tomato, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// FruitSelected(Fruit), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// combo_box( +/// &state.fruits, +/// "Select your favorite fruit...", +/// state.favorite.as_ref(), +/// Message::FruitSelected +/// ) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::FruitSelected(fruit) => { +/// state.favorite = Some(fruit); +/// } +/// } +/// } +/// +/// impl std::fmt::Display for Fruit { +/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// f.write_str(match self { +/// Self::Apple => "Apple", +/// Self::Orange => "Orange", +/// Self::Strawberry => "Strawberry", +/// Self::Tomato => "Tomato", +/// }) +/// } +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct ComboBox< 'a, @@ -41,6 +147,7 @@ pub struct ComboBox< selection: text_input::Value, on_selected: Box<dyn Fn(T) -> Message>, on_option_hovered: Option<Box<dyn Fn(T) -> Message>>, + on_open: Option<Message>, on_close: Option<Message>, on_input: Option<Box<dyn Fn(String) -> Message>>, menu_class: <Theme as menu::Catalog>::Class<'a>, @@ -77,6 +184,7 @@ where on_selected: Box::new(on_selected), on_option_hovered: None, on_input: None, + on_open: None, on_close: None, menu_class: <Theme as Catalog>::default_menu(), padding: text_input::DEFAULT_PADDING, @@ -104,6 +212,13 @@ where self } + /// Sets the message that will be produced when the [`ComboBox`] is + /// opened. + pub fn on_open(mut self, message: Message) -> Self { + self.on_open = Some(message); + self + } + /// Sets the message that will be produced when the outside area /// of the [`ComboBox`] is pressed. pub fn on_close(mut self, message: Message) -> Self { @@ -208,12 +323,14 @@ where /// The local state of a [`ComboBox`]. #[derive(Debug, Clone)] -pub struct State<T>(RefCell<Inner<T>>); +pub struct State<T> { + options: Vec<T>, + inner: RefCell<Inner<T>>, +} #[derive(Debug, Clone)] struct Inner<T> { value: String, - options: Vec<T>, option_matchers: Vec<String>, filtered_options: Filtered<T>, } @@ -247,39 +364,58 @@ where .collect(), ); - Self(RefCell::new(Inner { - value, + Self { options, - option_matchers, - filtered_options, - })) + inner: RefCell::new(Inner { + value, + option_matchers, + filtered_options, + }), + } + } + + /// Returns the options of the [`State`]. + /// + /// These are the options provided when the [`State`] + /// was constructed with [`State::new`]. + pub fn options(&self) -> &[T] { + &self.options } fn value(&self) -> String { - let inner = self.0.borrow(); + let inner = self.inner.borrow(); inner.value.clone() } fn with_inner<O>(&self, f: impl FnOnce(&Inner<T>) -> O) -> O { - let inner = self.0.borrow(); + let inner = self.inner.borrow(); f(&inner) } fn with_inner_mut(&self, f: impl FnOnce(&mut Inner<T>)) { - let mut inner = self.0.borrow_mut(); + let mut inner = self.inner.borrow_mut(); f(&mut inner); } fn sync_filtered_options(&self, options: &mut Filtered<T>) { - let inner = self.0.borrow(); + let inner = self.inner.borrow(); inner.filtered_options.sync(options); } } +impl<T> Default for State<T> +where + T: Display + Clone, +{ + fn default() -> Self { + Self::new(Vec::new()) + } +} + impl<T> Filtered<T> where T: Clone, @@ -322,8 +458,8 @@ enum TextInputEvent { TextChanged(String), } -impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for ComboBox<'a, T, Message, Theme, Renderer> +impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for ComboBox<'_, T, Message, Theme, Renderer> where T: Display + Clone + 'static, Message: Clone, @@ -373,17 +509,17 @@ where vec![widget::Tree::new(&self.text_input as &dyn Widget<_, _, _>)] } - fn on_event( + fn update( &mut self, tree: &mut widget::Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let menu = tree.state.downcast_mut::<Menu<T>>(); let started_focused = { @@ -402,9 +538,9 @@ where let mut local_shell = Shell::new(&mut local_messages); // Provide it to the widget - let mut event_status = self.text_input.on_event( + self.text_input.update( &mut tree.children[0], - event.clone(), + event, layout, cursor, renderer, @@ -413,13 +549,19 @@ where viewport, ); + if local_shell.is_event_captured() { + shell.capture_event(); + } + + shell.request_redraw_at(local_shell.redraw_request()); + shell.request_input_method(local_shell.input_method()); + // Then finally react to them here for message in local_messages { let TextInputEvent::TextChanged(new_value) = message; if let Some(on_input) = &self.on_input { shell.publish((on_input)(new_value.clone())); - published_message_to_shell = true; } // Couple the filtered options with the `ComboBox` @@ -431,7 +573,7 @@ where state.filtered_options.update( search( - &state.options, + &self.state.options, &state.option_matchers, &state.value, ) @@ -440,6 +582,7 @@ where ); }); shell.invalidate_layout(); + shell.request_redraw(); } let is_focused = { @@ -472,8 +615,8 @@ where .. }) = event { - let shift_modifer = modifiers.shift(); - match (named_key, shift_modifer) { + let shift_modifier = modifiers.shift(); + match (named_key, shift_modifier) { (key::Named::Enter, _) => { if let Some(index) = &menu.hovered_option { if let Some(option) = @@ -483,9 +626,9 @@ where } } - event_status = event::Status::Captured; + shell.capture_event(); + shell.request_redraw(); } - (key::Named::ArrowUp, _) | (key::Named::Tab, true) => { if let Some(index) = &mut menu.hovered_option { if *index == 0 { @@ -520,7 +663,8 @@ where } } - event_status = event::Status::Captured; + shell.capture_event(); + shell.request_redraw(); } (key::Named::ArrowDown, _) | (key::Named::Tab, false) @@ -567,7 +711,8 @@ where } } - event_status = event::Status::Captured; + shell.capture_event(); + shell.request_redraw(); } _ => {} } @@ -580,7 +725,7 @@ where if let Some(selection) = menu.new_selection.take() { // Clear the value and reset the options and menu state.value = String::new(); - state.filtered_options.update(state.options.clone()); + state.filtered_options.update(self.state.options.clone()); menu.menu = menu::State::default(); // Notify the selection @@ -588,18 +733,21 @@ where published_message_to_shell = true; // Unfocus the input - let _ = self.text_input.on_event( + let mut local_messages = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); + self.text_input.update( &mut tree.children[0], - Event::Mouse(mouse::Event::ButtonPressed( + &Event::Mouse(mouse::Event::ButtonPressed( mouse::Button::Left, )), layout, mouse::Cursor::Unavailable, renderer, clipboard, - &mut Shell::new(&mut vec![]), + &mut local_shell, viewport, ); + shell.request_input_method(local_shell.input_method()); } }); @@ -611,18 +759,20 @@ where text_input_state.is_focused() }; - if started_focused && !is_focused && !published_message_to_shell { - if let Some(message) = self.on_close.take() { - shell.publish(message); + if started_focused != is_focused { + // Focus changed, invalidate widget tree to force a fresh `view` + shell.invalidate_widgets(); + + if !published_message_to_shell { + if is_focused { + if let Some(on_open) = self.on_open.take() { + shell.publish(on_open); + } + } else if let Some(on_close) = self.on_close.take() { + shell.publish(on_close); + } } } - - // Focus changed, invalidate widget tree to force a fresh `view` - if started_focused != is_focused { - shell.invalidate_widgets(); - } - - event_status } fn mouse_interaction( diff --git a/widget/src/container.rs b/widget/src/container.rs index 51967707..82774186 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -1,23 +1,62 @@ -//! Decorate content and apply alignment. +//! Containers let you align a widget inside their boundaries. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::container; +//! +//! enum Message { +//! // ... +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! container("This text is centered inside a rounded box!") +//! .padding(10) +//! .center(800) +//! .style(container::rounded_box) +//! .into() +//! } +//! ``` use crate::core::alignment::{self, Alignment}; -use crate::core::event::{self, Event}; +use crate::core::border::{self, Border}; use crate::core::gradient::{self, Gradient}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; +use crate::core::theme; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Operation}; use crate::core::{ - self, Background, Border, Clipboard, Color, Element, Layout, Length, + self, Background, Clipboard, Color, Element, Event, Layout, Length, Padding, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, - Widget, + Widget, color, }; -use crate::runtime::Command; +use crate::runtime::task::{self, Task}; -/// An element decorating some content. +/// A widget that aligns its contents inside of its boundaries. /// -/// It is normally used for alignment purposes. +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::container; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// container("This text is centered inside a rounded box!") +/// .padding(10) +/// .center(800) +/// .style(container::rounded_box) +/// .into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Container< 'a, @@ -69,8 +108,8 @@ where } /// Sets the [`Id`] of the [`Container`]. - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); + pub fn id(mut self, id: impl Into<Id>) -> Self { + self.id = Some(id.into()); self } @@ -92,46 +131,6 @@ where self } - /// Sets the [`Container`] to fill the available space in the horizontal axis. - /// - /// This can be useful to quickly position content when chained with - /// alignment functions—like [`center_x`]. - /// - /// Calling this method is equivalent to calling [`width`] with a - /// [`Length::Fill`]. - /// - /// [`center_x`]: Self::center_x - /// [`width`]: Self::width - pub fn fill_x(self) -> Self { - self.width(Length::Fill) - } - - /// Sets the [`Container`] to fill the available space in the vetical axis. - /// - /// This can be useful to quickly position content when chained with - /// alignment functions—like [`center_y`]. - /// - /// Calling this method is equivalent to calling [`height`] with a - /// [`Length::Fill`]. - /// - /// [`center_y`]: Self::center_x - /// [`height`]: Self::height - pub fn fill_y(self) -> Self { - self.height(Length::Fill) - } - - /// Sets the [`Container`] to fill all the available space. - /// - /// Calling this method is equivalent to chaining [`fill_x`] and - /// [`fill_y`]. - /// - /// [`center`]: Self::center - /// [`fill_x`]: Self::fill_x - /// [`fill_y`]: Self::fill_y - pub fn fill(self) -> Self { - self.width(Length::Fill).height(Length::Fill) - } - /// Sets the maximum width of the [`Container`]. pub fn max_width(mut self, max_width: impl Into<Pixels>) -> Self { self.max_width = max_width.into().0; @@ -144,18 +143,6 @@ where self } - /// Sets the content alignment for the horizontal axis of the [`Container`]. - pub fn align_x(mut self, alignment: alignment::Horizontal) -> Self { - self.horizontal_alignment = alignment; - self - } - - /// Sets the content alignment for the vertical axis of the [`Container`]. - pub fn align_y(mut self, alignment: alignment::Vertical) -> Self { - self.vertical_alignment = alignment; - self - } - /// Sets the width of the [`Container`] and centers its contents horizontally. pub fn center_x(self, width: impl Into<Length>) -> Self { self.width(width).align_x(alignment::Horizontal::Center) @@ -179,6 +166,44 @@ where self.center_x(length).center_y(length) } + /// Aligns the contents of the [`Container`] to the left. + pub fn align_left(self, width: impl Into<Length>) -> Self { + self.width(width).align_x(alignment::Horizontal::Left) + } + + /// Aligns the contents of the [`Container`] to the right. + pub fn align_right(self, width: impl Into<Length>) -> Self { + self.width(width).align_x(alignment::Horizontal::Right) + } + + /// Aligns the contents of the [`Container`] to the top. + pub fn align_top(self, height: impl Into<Length>) -> Self { + self.height(height).align_y(alignment::Vertical::Top) + } + + /// Aligns the contents of the [`Container`] to the bottom. + pub fn align_bottom(self, height: impl Into<Length>) -> Self { + self.height(height).align_y(alignment::Vertical::Bottom) + } + + /// Sets the content alignment for the horizontal axis of the [`Container`]. + pub fn align_x( + mut self, + alignment: impl Into<alignment::Horizontal>, + ) -> Self { + self.horizontal_alignment = alignment.into(); + self + } + + /// Sets the content alignment for the vertical axis of the [`Container`]. + pub fn align_y( + mut self, + alignment: impl Into<alignment::Vertical>, + ) -> Self { + self.vertical_alignment = alignment.into(); + self + } + /// Sets whether the contents of the [`Container`] should be clipped on /// overflow. pub fn clip(mut self, clip: bool) -> Self { @@ -197,7 +222,6 @@ where } /// Sets the style class of the [`Container`]. - #[cfg(feature = "advanced")] #[must_use] pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { self.class = class.into(); @@ -205,8 +229,8 @@ where } } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for Container<'a, Message, Theme, Renderer> +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Container<'_, Message, Theme, Renderer> where Theme: Catalog, Renderer: core::Renderer, @@ -258,7 +282,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation, ) { operation.container( self.id.as_ref().map(|id| &id.0), @@ -274,18 +298,18 @@ where ); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().update( tree, event, layout.children().next().unwrap(), @@ -294,7 +318,7 @@ where clipboard, shell, viewport, - ) + ); } fn mouse_interaction( @@ -457,9 +481,17 @@ impl From<Id> for widget::Id { } } -/// Produces a [`Command`] that queries the visible screen bounds of the +impl From<&'static str> for Id { + fn from(value: &'static str) -> Self { + Id::new(value) + } +} + +/// Produces a [`Task`] that queries the visible screen bounds of the /// [`Container`] with the given [`Id`]. -pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> { +pub fn visible_bounds(id: impl Into<Id>) -> Task<Option<Rectangle>> { + let id = id.into(); + struct VisibleBounds { target: widget::Id, depth: usize, @@ -470,10 +502,11 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> { impl Operation<Option<Rectangle>> for VisibleBounds { fn scrollable( &mut self, - _state: &mut dyn widget::operation::Scrollable, _id: Option<&widget::Id>, bounds: Rectangle, + _content_bounds: Rectangle, translation: Vector, + _state: &mut dyn widget::operation::Scrollable, ) { match self.scrollables.last() { Some((last_translation, last_viewport, _depth)) => { @@ -538,7 +571,7 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> { } } - Command::widget(VisibleBounds { + task::widget(VisibleBounds { target: id.into(), depth: 0, scrollables: Vec::new(), @@ -547,7 +580,7 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> { } /// The appearance of a container. -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Default)] pub struct Style { /// The text [`Color`] of the container. pub text_color: Option<Color>, @@ -560,46 +593,54 @@ pub struct Style { } impl Style { - /// Updates the border of the [`Style`] with the given [`Color`] and `width`. - pub fn with_border( - self, - color: impl Into<Color>, - width: impl Into<Pixels>, - ) -> Self { + /// Updates the text color of the [`Style`]. + pub fn color(self, color: impl Into<Color>) -> Self { Self { - border: Border { - color: color.into(), - width: width.into().0, - ..Border::default() - }, + text_color: Some(color.into()), + ..self + } + } + + /// Updates the border of the [`Style`]. + pub fn border(self, border: impl Into<Border>) -> Self { + Self { + border: border.into(), ..self } } /// Updates the background of the [`Style`]. - pub fn with_background(self, background: impl Into<Background>) -> Self { + pub fn background(self, background: impl Into<Background>) -> Self { Self { background: Some(background.into()), ..self } } + + /// Updates the shadow of the [`Style`]. + pub fn shadow(self, shadow: impl Into<Shadow>) -> Self { + Self { + shadow: shadow.into(), + ..self + } + } } impl From<Color> for Style { fn from(color: Color) -> Self { - Self::default().with_background(color) + Self::default().background(color) } } impl From<Gradient> for Style { fn from(gradient: Gradient) -> Self { - Self::default().with_background(gradient) + Self::default().background(gradient) } } impl From<gradient::Linear> for Style { fn from(gradient: gradient::Linear) -> Self { - Self::default().with_background(gradient) + Self::default().background(gradient) } } @@ -618,6 +659,12 @@ pub trait Catalog { /// A styling function for a [`Container`]. pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>; +impl<Theme> From<Style> for StyleFn<'_, Theme> { + fn from(style: Style) -> Self { + Box::new(move |_theme| style) + } +} + impl Catalog for Theme { type Class<'a> = StyleFn<'a, Self>; @@ -635,13 +682,18 @@ pub fn transparent<Theme>(_theme: &Theme) -> Style { Style::default() } +/// A [`Container`] with the given [`Background`]. +pub fn background(background: impl Into<Background>) -> Style { + Style::default().background(background) +} + /// A rounded [`Container`] with a background. pub fn rounded_box(theme: &Theme) -> Style { let palette = theme.extended_palette(); Style { background: Some(palette.background.weak.color.into()), - border: Border::rounded(2), + border: border::rounded(2), ..Style::default() } } @@ -651,12 +703,57 @@ pub fn bordered_box(theme: &Theme) -> Style { let palette = theme.extended_palette(); Style { - background: Some(palette.background.weak.color.into()), + background: Some(palette.background.weakest.color.into()), border: Border { width: 1.0, - radius: 0.0.into(), + radius: 5.0.into(), color: palette.background.strong.color, }, ..Style::default() } } + +/// A [`Container`] with a dark background and white text. +pub fn dark(_theme: &Theme) -> Style { + style(theme::palette::Pair { + color: color!(0x111111), + text: Color::WHITE, + }) +} + +/// A [`Container`] with a primary background color. +pub fn primary(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + style(palette.primary.base) +} + +/// A [`Container`] with a secondary background color. +pub fn secondary(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + style(palette.secondary.base) +} + +/// A [`Container`] with a success background color. +pub fn success(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + style(palette.success.base) +} + +/// A [`Container`] with a danger background color. +pub fn danger(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + style(palette.danger.base) +} + +fn style(pair: theme::palette::Pair) -> Style { + Style { + background: Some(pair.color.into()), + text_color: Some(pair.text), + border: border::rounded(2), + ..Style::default() + } +} diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index 3d96c9ba..ff178e4f 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -4,7 +4,8 @@ use crate::checkbox::{self, Checkbox}; use crate::combo_box::{self, ComboBox}; use crate::container::{self, Container}; use crate::core; -use crate::core::widget::operation; +use crate::core::widget::operation::{self, Operation}; +use crate::core::window; use crate::core::{Element, Length, Pixels, Widget}; use crate::keyed; use crate::overlay; @@ -13,7 +14,8 @@ use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; use crate::radio::{self, Radio}; use crate::rule::{self, Rule}; -use crate::runtime::Command; +use crate::runtime::Action; +use crate::runtime::task::{self, Task}; use crate::scrollable::{self, Scrollable}; use crate::slider::{self, Slider}; use crate::text::{self, Text}; @@ -22,14 +24,35 @@ use crate::text_input::{self, TextInput}; use crate::toggler::{self, Toggler}; use crate::tooltip::{self, Tooltip}; use crate::vertical_slider::{self, VerticalSlider}; -use crate::{Column, MouseArea, Row, Space, Stack, Themer}; +use crate::{Column, MouseArea, Pin, Pop, Row, Space, Stack, Themer}; use std::borrow::Borrow; use std::ops::RangeInclusive; /// Creates a [`Column`] with the given children. /// -/// [`Column`]: crate::Column +/// Columns distribute their children vertically. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{button, column}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// column![ +/// "I am on top!", +/// button("I am in the center!"), +/// "I am below.", +/// ].into() +/// } +/// ``` #[macro_export] macro_rules! column { () => ( @@ -42,7 +65,28 @@ macro_rules! column { /// Creates a [`Row`] with the given children. /// -/// [`Row`]: crate::Row +/// Rows distribute their children horizontally. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{button, row}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// row![ +/// "I am to the left!", +/// button("I am in the middle!"), +/// "I am to the right!", +/// ].into() +/// } +/// ``` #[macro_export] macro_rules! row { () => ( @@ -66,9 +110,114 @@ macro_rules! stack { ); } +/// Creates a new [`Text`] widget with the provided content. +/// +/// [`Text`]: core::widget::Text +/// +/// This macro uses the same syntax as [`format!`], but creates a new [`Text`] widget instead. +/// +/// See [the formatting documentation in `std::fmt`](std::fmt) +/// for details of the macro argument syntax. +/// +/// # Examples +/// +/// ```no_run +/// # mod iced { +/// # pub mod widget { +/// # macro_rules! text { +/// # ($($arg:tt)*) => {unimplemented!()} +/// # } +/// # pub(crate) use text; +/// # } +/// # } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::core::Theme, ()>; +/// use iced::widget::text; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(_state: &State) -> Element<Message> { +/// let simple = text!("Hello, world!"); +/// +/// let keyword = text!("Hello, {}", "world!"); +/// +/// let planet = "Earth"; +/// let local_variable = text!("Hello, {planet}!"); +/// // ... +/// # unimplemented!() +/// } +/// ``` +#[macro_export] +macro_rules! text { + ($($arg:tt)*) => { + $crate::Text::new(format!($($arg)*)) + }; +} + +/// Creates some [`Rich`] text with the given spans. +/// +/// [`Rich`]: text::Rich +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::*; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::font; +/// use iced::widget::{rich_text, span}; +/// use iced::{color, never, Font}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// rich_text![ +/// span("I am red!").color(color!(0xff0000)), +/// span(" "), +/// span("And I am bold!").font(Font { weight: font::Weight::Bold, ..Font::default() }), +/// ] +/// .on_link_click(never) +/// .size(20) +/// .into() +/// } +/// ``` +#[macro_export] +macro_rules! rich_text { + () => ( + $crate::text::Rich::new() + ); + ($($x:expr),+ $(,)?) => ( + $crate::text::Rich::from_iter([$($crate::text::Span::from($x)),+]) + ); +} + /// Creates a new [`Container`] with the provided content. /// -/// [`Container`]: crate::Container +/// Containers let you align a widget inside their boundaries. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::container; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// container("This text is centered inside a rounded box!") +/// .padding(10) +/// .center(800) +/// .style(container::rounded_box) +/// .into() +/// } +/// ``` pub fn container<'a, Message, Theme, Renderer>( content: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Container<'a, Message, Theme, Renderer> @@ -84,10 +233,10 @@ where /// /// This is equivalent to: /// ```rust,no_run -/// # use iced_widget::core::Length; +/// # use iced_widget::core::Length::Fill; /// # use iced_widget::Container; /// # fn container<A>(x: A) -> Container<'static, ()> { unreachable!() } -/// let centered = container("Centered!").center(Length::Fill); +/// let center = container("Center!").center(Fill); /// ``` /// /// [`Container`]: crate::Container @@ -101,7 +250,217 @@ where container(content).center(Length::Fill) } +/// Creates a new [`Container`] that fills all the available space +/// horizontally and centers its contents inside. +/// +/// This is equivalent to: +/// ```rust,no_run +/// # use iced_widget::core::Length::Fill; +/// # use iced_widget::Container; +/// # fn container<A>(x: A) -> Container<'static, ()> { unreachable!() } +/// let center_x = container("Horizontal Center!").center_x(Fill); +/// ``` +/// +/// [`Container`]: crate::Container +pub fn center_x<'a, Message, Theme, Renderer>( + content: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Container<'a, Message, Theme, Renderer> +where + Theme: container::Catalog + 'a, + Renderer: core::Renderer, +{ + container(content).center_x(Length::Fill) +} + +/// Creates a new [`Container`] that fills all the available space +/// vertically and centers its contents inside. +/// +/// This is equivalent to: +/// ```rust,no_run +/// # use iced_widget::core::Length::Fill; +/// # use iced_widget::Container; +/// # fn container<A>(x: A) -> Container<'static, ()> { unreachable!() } +/// let center_y = container("Vertical Center!").center_y(Fill); +/// ``` +/// +/// [`Container`]: crate::Container +pub fn center_y<'a, Message, Theme, Renderer>( + content: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Container<'a, Message, Theme, Renderer> +where + Theme: container::Catalog + 'a, + Renderer: core::Renderer, +{ + container(content).center_y(Length::Fill) +} + +/// Creates a new [`Container`] that fills all the available space +/// horizontally and right-aligns its contents inside. +/// +/// This is equivalent to: +/// ```rust,no_run +/// # use iced_widget::core::Length::Fill; +/// # use iced_widget::Container; +/// # fn container<A>(x: A) -> Container<'static, ()> { unreachable!() } +/// let right = container("Right!").align_right(Fill); +/// ``` +/// +/// [`Container`]: crate::Container +pub fn right<'a, Message, Theme, Renderer>( + content: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Container<'a, Message, Theme, Renderer> +where + Theme: container::Catalog + 'a, + Renderer: core::Renderer, +{ + container(content).align_right(Length::Fill) +} + +/// Creates a new [`Container`] that fills all the available space +/// and aligns its contents inside to the right center. +/// +/// This is equivalent to: +/// ```rust,no_run +/// # use iced_widget::core::Length::Fill; +/// # use iced_widget::Container; +/// # fn container<A>(x: A) -> Container<'static, ()> { unreachable!() } +/// let right_center = container("Bottom Center!").align_right(Fill).center_y(Fill); +/// ``` +/// +/// [`Container`]: crate::Container +pub fn right_center<'a, Message, Theme, Renderer>( + content: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Container<'a, Message, Theme, Renderer> +where + Theme: container::Catalog + 'a, + Renderer: core::Renderer, +{ + container(content) + .align_right(Length::Fill) + .center_y(Length::Fill) +} + +/// Creates a new [`Container`] that fills all the available space +/// vertically and bottom-aligns its contents inside. +/// +/// This is equivalent to: +/// ```rust,no_run +/// # use iced_widget::core::Length::Fill; +/// # use iced_widget::Container; +/// # fn container<A>(x: A) -> Container<'static, ()> { unreachable!() } +/// let bottom = container("Bottom!").align_bottom(Fill); +/// ``` +/// +/// [`Container`]: crate::Container +pub fn bottom<'a, Message, Theme, Renderer>( + content: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Container<'a, Message, Theme, Renderer> +where + Theme: container::Catalog + 'a, + Renderer: core::Renderer, +{ + container(content).align_bottom(Length::Fill) +} + +/// Creates a new [`Container`] that fills all the available space +/// and aligns its contents inside to the bottom center. +/// +/// This is equivalent to: +/// ```rust,no_run +/// # use iced_widget::core::Length::Fill; +/// # use iced_widget::Container; +/// # fn container<A>(x: A) -> Container<'static, ()> { unreachable!() } +/// let bottom_center = container("Bottom Center!").center_x(Fill).align_bottom(Fill); +/// ``` +/// +/// [`Container`]: crate::Container +pub fn bottom_center<'a, Message, Theme, Renderer>( + content: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Container<'a, Message, Theme, Renderer> +where + Theme: container::Catalog + 'a, + Renderer: core::Renderer, +{ + container(content) + .center_x(Length::Fill) + .align_bottom(Length::Fill) +} + +/// Creates a new [`Container`] that fills all the available space +/// and aligns its contents inside to the bottom right corner. +/// +/// This is equivalent to: +/// ```rust,no_run +/// # use iced_widget::core::Length::Fill; +/// # use iced_widget::Container; +/// # fn container<A>(x: A) -> Container<'static, ()> { unreachable!() } +/// let bottom_right = container("Bottom!").align_right(Fill).align_bottom(Fill); +/// ``` +/// +/// [`Container`]: crate::Container +pub fn bottom_right<'a, Message, Theme, Renderer>( + content: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Container<'a, Message, Theme, Renderer> +where + Theme: container::Catalog + 'a, + Renderer: core::Renderer, +{ + container(content) + .align_right(Length::Fill) + .align_bottom(Length::Fill) +} + +/// Creates a new [`Pin`] widget with the given content. +/// +/// A [`Pin`] widget positions its contents at some fixed coordinates inside of its boundaries. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::Length::Fill; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::pin; +/// use iced::Fill; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// pin("This text is displayed at coordinates (50, 50)!") +/// .x(50) +/// .y(50) +/// .into() +/// } +/// ``` +pub fn pin<'a, Message, Theme, Renderer>( + content: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Pin<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + Pin::new(content) +} + /// Creates a new [`Column`] with the given children. +/// +/// Columns distribute their children vertically. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{column, text}; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// column((0..5).map(|i| text!("Item {i}").into())).into() +/// } +/// ``` pub fn column<'a, Message, Theme, Renderer>( children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, ) -> Column<'a, Message, Theme, Renderer> @@ -111,7 +470,27 @@ where Column::with_children(children) } -/// Creates a new [`keyed::Column`] with the given children. +/// Creates a new [`keyed::Column`] from an iterator of elements. +/// +/// Keyed columns distribute content vertically while keeping continuity. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{keyed_column, text}; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// keyed_column((0..=100).map(|i| { +/// (i, text!("Item {i}").into()) +/// })).into() +/// } +/// ``` pub fn keyed_column<'a, Key, Message, Theme, Renderer>( children: impl IntoIterator<Item = (Key, Element<'a, Message, Theme, Renderer>)>, ) -> keyed::Column<'a, Key, Message, Theme, Renderer> @@ -122,9 +501,25 @@ where keyed::Column::with_children(children) } -/// Creates a new [`Row`] with the given children. +/// Creates a new [`Row`] from an iterator. /// -/// [`Row`]: crate::Row +/// Rows distribute their children horizontally. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{row, text}; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// row((0..5).map(|i| text!("Item {i}").into())).into() +/// } +/// ``` pub fn row<'a, Message, Theme, Renderer>( children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, ) -> Row<'a, Message, Theme, Renderer> @@ -161,19 +556,18 @@ where Theme: 'a, Renderer: core::Renderer + 'a, { - use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; - use crate::core::{Rectangle, Shell, Size}; + use crate::core::{Event, Rectangle, Shell, Size}; struct Opaque<'a, Message, Theme, Renderer> { content: Element<'a, Message, Theme, Renderer>, } - impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for Opaque<'a, Message, Theme, Renderer> + impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Opaque<'_, Message, Theme, Renderer> where Renderer: core::Renderer, { @@ -230,42 +624,36 @@ where state: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn operation::Operation<Message>, + operation: &mut dyn operation::Operation, ) { self.content .as_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 core::Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let is_mouse_press = matches!( event, core::Event::Mouse(mouse::Event::ButtonPressed(_)) ); - if let core::event::Status::Captured = - self.content.as_widget_mut().on_event( - state, event, layout, cursor, renderer, clipboard, shell, - viewport, - ) - { - return event::Status::Captured; - } + self.content.as_widget_mut().update( + state, event, layout, cursor, renderer, clipboard, shell, + viewport, + ); if is_mouse_press && cursor.is_over(layout.bounds()) { - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } @@ -328,21 +716,22 @@ where Theme: 'a, Renderer: core::Renderer + 'a, { - use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; - use crate::core::{Rectangle, Shell, Size}; + use crate::core::{Event, Rectangle, Shell, Size}; struct Hover<'a, Message, Theme, Renderer> { base: Element<'a, Message, Theme, Renderer>, top: Element<'a, Message, Theme, Renderer>, + is_top_focused: bool, is_top_overlay_active: bool, + is_hovered: bool, } - impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for Hover<'a, Message, Theme, Renderer> + impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Hover<'_, Message, Theme, Renderer> where Renderer: core::Renderer, { @@ -413,7 +802,9 @@ where viewport, ); - if cursor.is_over(layout.bounds()) || self.is_top_overlay_active + if cursor.is_over(layout.bounds()) + || self.is_top_focused + || self.is_top_overlay_active { let (top_layout, top_tree) = children.next().unwrap(); @@ -432,7 +823,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn operation::Operation<Message>, + operation: &mut dyn operation::Operation, ) { let children = [&self.base, &self.top] .into_iter() @@ -443,58 +834,82 @@ where } } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn core::Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let mut children = layout.children().zip(&mut tree.children); let (base_layout, base_tree) = children.next().unwrap(); + let (top_layout, top_tree) = children.next().unwrap(); - let top_status = if matches!( + let is_hovered = cursor.is_over(layout.bounds()); + + if matches!(event, Event::Window(window::Event::RedrawRequested(_))) + { + let mut count_focused = operation::focusable::count(); + + self.top.as_widget_mut().operate( + top_tree, + top_layout, + renderer, + &mut operation::black_box(&mut count_focused), + ); + + self.is_top_focused = match count_focused.finish() { + operation::Outcome::Some(count) => count.focused.is_some(), + _ => false, + }; + + self.is_hovered = is_hovered; + } else if is_hovered != self.is_hovered { + shell.request_redraw(); + } + + let is_visible = + is_hovered || self.is_top_focused || self.is_top_overlay_active; + + if matches!( event, Event::Mouse( mouse::Event::CursorMoved { .. } | mouse::Event::ButtonReleased(_) ) - ) || cursor.is_over(layout.bounds()) + ) || is_visible { - let (top_layout, top_tree) = children.next().unwrap(); + let redraw_request = shell.redraw_request(); - self.top.as_widget_mut().on_event( - top_tree, - event.clone(), - top_layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) - } else { - event::Status::Ignored + self.top.as_widget_mut().update( + top_tree, event, top_layout, cursor, renderer, clipboard, + shell, viewport, + ); + + // Ignore redraw requests of invisible content + if !is_visible { + Shell::replace_redraw_request(shell, redraw_request); + } }; - if top_status == event::Status::Captured { - return top_status; + if shell.is_event_captured() { + return; } - self.base.as_widget_mut().on_event( + self.base.as_widget_mut().update( base_tree, - event.clone(), + event, base_layout, cursor, renderer, clipboard, shell, viewport, - ) + ); } fn mouse_interaction( @@ -552,13 +967,49 @@ where Element::new(Hover { base: base.into(), top: top.into(), + is_top_focused: false, is_top_overlay_active: false, + is_hovered: false, }) } +/// Creates a new [`Pop`] widget. +/// +/// A [`Pop`] widget can generate messages when it pops in and out of view. +/// It can even notify you with anticipation at a given distance! +pub fn pop<'a, Message, Theme, Renderer>( + content: impl Into<Element<'a, Message, Theme, Renderer>>, +) -> Pop<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, + Message: Clone, +{ + Pop::new(content) +} + /// Creates a new [`Scrollable`] with the provided content. /// -/// [`Scrollable`]: crate::Scrollable +/// Scrollables let users navigate an endless amount of content with a scrollbar. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{column, scrollable, vertical_space}; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// scrollable(column![ +/// "Scroll me!", +/// vertical_space().height(3000), +/// "You did it!", +/// ]).into() +/// } +/// ``` pub fn scrollable<'a, Message, Theme, Renderer>( content: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Scrollable<'a, Message, Theme, Renderer> @@ -571,7 +1022,22 @@ where /// Creates a new [`Button`] with the provided content. /// -/// [`Button`]: crate::Button +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::button; +/// +/// #[derive(Clone)] +/// enum Message { +/// ButtonPressed, +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// button("Press me!").on_press(Message::ButtonPressed).into() +/// } +/// ``` pub fn button<'a, Message, Theme, Renderer>( content: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Button<'a, Message, Theme, Renderer> @@ -585,8 +1051,29 @@ where /// Creates a new [`Tooltip`] for the provided content with the given /// [`Element`] and [`tooltip::Position`]. /// -/// [`Tooltip`]: crate::Tooltip -/// [`tooltip::Position`]: crate::tooltip::Position +/// Tooltips display a hint of information over some element when hovered. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{container, tooltip}; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(_state: &State) -> Element<'_, Message> { +/// tooltip( +/// "Hover me to display the tooltip!", +/// container("This is the tooltip contents!") +/// .padding(10) +/// .style(container::rounded_box), +/// tooltip::Position::Bottom, +/// ).into() +/// } +/// ``` pub fn tooltip<'a, Message, Theme, Renderer>( content: impl Into<Element<'a, Message, Theme, Renderer>>, tooltip: impl Into<Element<'a, Message, Theme, Renderer>>, @@ -601,7 +1088,25 @@ where /// Creates a new [`Text`] widget with the provided content. /// -/// [`Text`]: core::widget::Text +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::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() +/// } +/// ``` pub fn text<'a, Theme, Renderer>( text: impl text::IntoFragment<'a>, ) -> Text<'a, Theme, Renderer> @@ -613,8 +1118,6 @@ where } /// Creates a new [`Text`] widget that displays the provided value. -/// -/// [`Text`]: core::widget::Text pub fn value<'a, Theme, Renderer>( value: impl ToString, ) -> Text<'a, Theme, Renderer> @@ -625,9 +1128,122 @@ where Text::new(value.to_string()) } +/// Creates a new [`Rich`] text widget with the provided spans. +/// +/// [`Rich`]: text::Rich +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::*; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::font; +/// use iced::widget::{rich_text, span}; +/// use iced::{color, never, Font}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// LinkClicked(&'static str), +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// rich_text([ +/// span("I am red!").color(color!(0xff0000)), +/// span(" "), +/// span("And I am bold!").font(Font { weight: font::Weight::Bold, ..Font::default() }), +/// ]) +/// .on_link_click(never) +/// .size(20) +/// .into() +/// } +/// ``` +pub fn rich_text<'a, Link, Message, Theme, Renderer>( + spans: impl AsRef<[text::Span<'a, Link, Renderer::Font>]> + 'a, +) -> text::Rich<'a, Link, Message, Theme, Renderer> +where + Link: Clone + 'static, + Theme: text::Catalog + 'a, + Renderer: core::text::Renderer, + Renderer::Font: 'a, +{ + text::Rich::with_spans(spans) +} + +/// Creates a new [`Span`] of text with the provided content. +/// +/// A [`Span`] is a fragment of some [`Rich`] text. +/// +/// [`Span`]: text::Span +/// [`Rich`]: text::Rich +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::*; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::font; +/// use iced::widget::{rich_text, span}; +/// use iced::{color, never, Font}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// rich_text![ +/// span("I am red!").color(color!(0xff0000)), +/// " ", +/// span("And I am bold!").font(Font { weight: font::Weight::Bold, ..Font::default() }), +/// ] +/// .on_link_click(never) +/// .size(20) +/// .into() +/// } +/// ``` +pub fn span<'a, Link, Font>( + text: impl text::IntoFragment<'a>, +) -> text::Span<'a, Link, Font> { + text::Span::new(text) +} + +#[cfg(feature = "markdown")] +#[doc(inline)] +pub use crate::markdown::view as markdown; + /// Creates a new [`Checkbox`]. /// -/// [`Checkbox`]: crate::Checkbox +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::checkbox; +/// +/// struct State { +/// is_checked: bool, +/// } +/// +/// enum Message { +/// CheckboxToggled(bool), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// checkbox("Toggle me!", state.is_checked) +/// .on_toggle(Message::CheckboxToggled) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::CheckboxToggled(is_checked) => { +/// state.is_checked = is_checked; +/// } +/// } +/// } +/// ``` +/// ![Checkbox drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/checkbox.png?raw=true) pub fn checkbox<'a, Message, Theme, Renderer>( label: impl Into<String>, is_checked: bool, @@ -641,7 +1257,64 @@ where /// Creates a new [`Radio`]. /// -/// [`Radio`]: crate::Radio +/// Radio buttons let users choose a single option from a bunch of options. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::{column, radio}; +/// +/// struct State { +/// selection: Option<Choice>, +/// } +/// +/// #[derive(Debug, Clone, Copy)] +/// enum Message { +/// RadioSelected(Choice), +/// } +/// +/// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// enum Choice { +/// A, +/// B, +/// C, +/// All, +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// let a = radio( +/// "A", +/// Choice::A, +/// state.selection, +/// Message::RadioSelected, +/// ); +/// +/// let b = radio( +/// "B", +/// Choice::B, +/// state.selection, +/// Message::RadioSelected, +/// ); +/// +/// let c = radio( +/// "C", +/// Choice::C, +/// state.selection, +/// Message::RadioSelected, +/// ); +/// +/// let all = radio( +/// "All of the above", +/// Choice::All, +/// state.selection, +/// Message::RadioSelected +/// ); +/// +/// column![a, b, c, all].into() +/// } +/// ``` pub fn radio<'a, Message, Theme, Renderer, V>( label: impl Into<String>, value: V, @@ -659,22 +1332,82 @@ where /// Creates a new [`Toggler`]. /// -/// [`Toggler`]: crate::Toggler +/// Togglers let users make binary choices by toggling a switch. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::toggler; +/// +/// struct State { +/// is_checked: bool, +/// } +/// +/// enum Message { +/// TogglerToggled(bool), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// toggler(state.is_checked) +/// .label("Toggle me!") +/// .on_toggle(Message::TogglerToggled) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::TogglerToggled(is_checked) => { +/// state.is_checked = is_checked; +/// } +/// } +/// } +/// ``` pub fn toggler<'a, Message, Theme, Renderer>( - label: impl Into<Option<String>>, is_checked: bool, - f: impl Fn(bool) -> Message + 'a, ) -> Toggler<'a, Message, Theme, Renderer> where Theme: toggler::Catalog + 'a, Renderer: core::text::Renderer, { - Toggler::new(label, is_checked, f) + Toggler::new(is_checked) } /// Creates a new [`TextInput`]. /// -/// [`TextInput`]: crate::TextInput +/// Text inputs display fields that can be filled with text. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::text_input; +/// +/// struct State { +/// content: String, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// ContentChanged(String) +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// text_input("Type something here...", &state.content) +/// .on_input(Message::ContentChanged) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::ContentChanged(content) => { +/// state.content = content; +/// } +/// } +/// } +/// ``` pub fn text_input<'a, Message, Theme, Renderer>( placeholder: &str, value: &str, @@ -689,7 +1422,39 @@ where /// Creates a new [`TextEditor`]. /// -/// [`TextEditor`]: crate::TextEditor +/// Text editors display a multi-line text input for text editing. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::text_editor; +/// +/// struct State { +/// content: text_editor::Content, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// Edit(text_editor::Action) +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// text_editor(&state.content) +/// .placeholder("Type something here...") +/// .on_action(Message::Edit) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::Edit(action) => { +/// state.content.perform(action); +/// } +/// } +/// } +/// ``` pub fn text_editor<'a, Message, Theme, Renderer>( content: &'a text_editor::Content<Renderer>, ) -> TextEditor<'a, core::text::highlighter::PlainText, Message, Theme, Renderer> @@ -703,7 +1468,36 @@ where /// Creates a new [`Slider`]. /// -/// [`Slider`]: crate::Slider +/// Sliders let users set a value by moving an indicator. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::slider; +/// +/// struct State { +/// value: f32, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// ValueChanged(f32), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// slider(0.0..=100.0, state.value, Message::ValueChanged).into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::ValueChanged(value) => { +/// state.value = value; +/// } +/// } +/// } +/// ``` pub fn slider<'a, T, Message, Theme>( range: std::ops::RangeInclusive<T>, value: T, @@ -719,7 +1513,36 @@ where /// Creates a new [`VerticalSlider`]. /// -/// [`VerticalSlider`]: crate::VerticalSlider +/// Sliders let users set a value by moving an indicator. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::vertical_slider; +/// +/// struct State { +/// value: f32, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// ValueChanged(f32), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// vertical_slider(0.0..=100.0, state.value, Message::ValueChanged).into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::ValueChanged(value) => { +/// state.value = value; +/// } +/// } +/// } +/// ``` pub fn vertical_slider<'a, T, Message, Theme>( range: std::ops::RangeInclusive<T>, value: T, @@ -735,7 +1558,68 @@ where /// Creates a new [`PickList`]. /// -/// [`PickList`]: crate::PickList +/// Pick lists display a dropdown list of selectable options. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::pick_list; +/// +/// struct State { +/// favorite: Option<Fruit>, +/// } +/// +/// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// enum Fruit { +/// Apple, +/// Orange, +/// Strawberry, +/// Tomato, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// FruitSelected(Fruit), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// let fruits = [ +/// Fruit::Apple, +/// Fruit::Orange, +/// Fruit::Strawberry, +/// Fruit::Tomato, +/// ]; +/// +/// pick_list( +/// fruits, +/// state.favorite, +/// Message::FruitSelected, +/// ) +/// .placeholder("Select your favorite fruit...") +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::FruitSelected(fruit) => { +/// state.favorite = Some(fruit); +/// } +/// } +/// } +/// +/// impl std::fmt::Display for Fruit { +/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// f.write_str(match self { +/// Self::Apple => "Apple", +/// Self::Orange => "Orange", +/// Self::Strawberry => "Strawberry", +/// Self::Tomato => "Tomato", +/// }) +/// } +/// } +/// ``` pub fn pick_list<'a, T, L, V, Message, Theme, Renderer>( options: L, selected: Option<V>, @@ -754,7 +1638,62 @@ where /// Creates a new [`ComboBox`]. /// -/// [`ComboBox`]: crate::ComboBox +/// Combo boxes display a dropdown list of searchable and selectable options. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::combo_box; +/// +/// struct State { +/// fruits: combo_box::State<Fruit>, +/// favorite: Option<Fruit>, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Fruit { +/// Apple, +/// Orange, +/// Strawberry, +/// Tomato, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// FruitSelected(Fruit), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// combo_box( +/// &state.fruits, +/// "Select your favorite fruit...", +/// state.favorite.as_ref(), +/// Message::FruitSelected +/// ) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::FruitSelected(fruit) => { +/// state.favorite = Some(fruit); +/// } +/// } +/// } +/// +/// impl std::fmt::Display for Fruit { +/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// f.write_str(match self { +/// Self::Apple => "Apple", +/// Self::Orange => "Orange", +/// Self::Strawberry => "Strawberry", +/// Self::Tomato => "Tomato", +/// }) +/// } +/// } +/// ``` pub fn combo_box<'a, T, Message, Theme, Renderer>( state: &'a combo_box::State<T>, placeholder: &str, @@ -787,7 +1726,22 @@ pub fn vertical_space() -> Space { /// Creates a horizontal [`Rule`] with the given height. /// -/// [`Rule`]: crate::Rule +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::horizontal_rule; +/// +/// #[derive(Clone)] +/// enum Message { +/// // ..., +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// horizontal_rule(2).into() +/// } +/// ``` pub fn horizontal_rule<'a, Theme>(height: impl Into<Pixels>) -> Rule<'a, Theme> where Theme: rule::Catalog + 'a, @@ -797,7 +1751,22 @@ where /// Creates a vertical [`Rule`] with the given width. /// -/// [`Rule`]: crate::Rule +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::vertical_rule; +/// +/// #[derive(Clone)] +/// enum Message { +/// // ..., +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// vertical_rule(2).into() +/// } +/// ``` pub fn vertical_rule<'a, Theme>(width: impl Into<Pixels>) -> Rule<'a, Theme> where Theme: rule::Catalog + 'a, @@ -807,11 +1776,31 @@ where /// Creates a new [`ProgressBar`]. /// +/// Progress bars visualize the progression of an extended computer operation, such as a download, file transfer, or installation. +/// /// It expects: /// * an inclusive range of possible values, and /// * the current value of the [`ProgressBar`]. /// -/// [`ProgressBar`]: crate::ProgressBar +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::progress_bar; +/// +/// struct State { +/// progress: f32, +/// } +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// progress_bar(0.0..=100.0, state.progress).into() +/// } +/// ``` pub fn progress_bar<'a, Theme>( range: RangeInclusive<f32>, value: f32, @@ -824,7 +1813,26 @@ where /// Creates a new [`Image`]. /// +/// Images display raster graphics in different formats (PNG, JPG, etc.). +/// /// [`Image`]: crate::Image +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::image; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// image("ferris.png").into() +/// } +/// ``` +/// <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300"> #[cfg(feature = "image")] pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> { crate::Image::new(handle.into()) @@ -832,8 +1840,26 @@ pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> { /// Creates a new [`Svg`] widget from the given [`Handle`]. /// +/// Svg widgets display vector graphics in your application. +/// /// [`Svg`]: crate::Svg /// [`Handle`]: crate::svg::Handle +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::svg; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// svg("tiger.svg").into() +/// } +/// ``` #[cfg(feature = "svg")] pub fn svg<'a, Theme>( handle: impl Into<core::svg::Handle>, @@ -844,9 +1870,95 @@ where crate::Svg::new(handle) } +/// Creates an [`Element`] that displays the iced logo with the given `text_size`. +/// +/// Useful for showing some love to your favorite GUI library in your "About" screen, +/// for instance. +#[cfg(feature = "svg")] +pub fn iced<'a, Message, Theme, Renderer>( + text_size: impl Into<Pixels>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Renderer: core::Renderer + + core::text::Renderer<Font = core::Font> + + core::svg::Renderer + + 'a, + Theme: text::Catalog + crate::svg::Catalog + 'a, +{ + use crate::core::{Alignment, Font}; + use crate::svg; + use std::sync::LazyLock; + + static LOGO: LazyLock<svg::Handle> = LazyLock::new(|| { + svg::Handle::from_memory(include_bytes!("../assets/iced-logo.svg")) + }); + + let text_size = text_size.into(); + + row![ + svg(LOGO.clone()).width(text_size * 1.3), + text("iced").size(text_size).font(Font::MONOSPACE) + ] + .spacing(text_size.0 / 3.0) + .align_y(Alignment::Center) + .into() +} + /// Creates a new [`Canvas`]. /// +/// Canvases can be leveraged to draw interactive 2D graphics. +/// /// [`Canvas`]: crate::Canvas +/// +/// # Example: Drawing a Simple Circle +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::mouse; +/// use iced::widget::canvas; +/// use iced::{Color, Rectangle, Renderer, Theme}; +/// +/// // First, we define the data we need for drawing +/// #[derive(Debug)] +/// struct Circle { +/// radius: f32, +/// } +/// +/// // Then, we implement the `Program` trait +/// impl<Message> canvas::Program<Message> for Circle { +/// // No internal state +/// type State = (); +/// +/// fn draw( +/// &self, +/// _state: &(), +/// renderer: &Renderer, +/// _theme: &Theme, +/// bounds: Rectangle, +/// _cursor: mouse::Cursor +/// ) -> Vec<canvas::Geometry> { +/// // We prepare a new `Frame` +/// let mut frame = canvas::Frame::new(renderer, bounds.size()); +/// +/// // We create a `Path` representing a simple circle +/// let circle = canvas::Path::circle(frame.center(), self.radius); +/// +/// // And fill it with some color +/// frame.fill(&circle, Color::BLACK); +/// +/// // Then, we produce the geometry +/// vec![frame.into_geometry()] +/// } +/// } +/// +/// // Finally, we simply use our `Circle` to create the `Canvas`! +/// fn view<'a, Message: 'a>(_state: &'a State) -> Element<'a, Message> { +/// canvas(Circle { radius: 50.0 }).into() +/// } +/// ``` #[cfg(feature = "canvas")] pub fn canvas<P, Message, Theme, Renderer>( program: P, @@ -860,8 +1972,31 @@ where /// Creates a new [`QRCode`] widget from the given [`Data`]. /// +/// QR codes display information in a type of two-dimensional matrix barcode. +/// /// [`QRCode`]: crate::QRCode /// [`Data`]: crate::qr_code::Data +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::qr_code; +/// +/// struct State { +/// data: qr_code::Data, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// qr_code(&state.data).into() +/// } +/// ``` #[cfg(feature = "qr_code")] pub fn qr_code<'a, Theme>( data: &'a crate::qr_code::Data, @@ -884,19 +2019,13 @@ where } /// Focuses the previous focusable widget. -pub fn focus_previous<Message>() -> Command<Message> -where - Message: 'static, -{ - Command::widget(operation::focusable::focus_previous()) +pub fn focus_previous<T>() -> Task<T> { + task::effect(Action::widget(operation::focusable::focus_previous())) } /// Focuses the next focusable widget. -pub fn focus_next<Message>() -> Command<Message> -where - Message: 'static, -{ - Command::widget(operation::focusable::focus_next()) +pub fn focus_next<T>() -> Task<T> { + task::effect(Action::widget(operation::focusable::focus_next())) } /// Creates a new [`MouseArea`]. @@ -928,7 +2057,43 @@ where Themer::new(move |_| new_theme.clone(), content) } -/// Creates a new [`PaneGrid`]. +/// Creates a [`PaneGrid`] with the given [`pane_grid::State`] and view function. +/// +/// Pane grids let your users split regions of your application and organize layout dynamically. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::{pane_grid, text}; +/// +/// struct State { +/// panes: pane_grid::State<Pane>, +/// } +/// +/// enum Pane { +/// SomePane, +/// AnotherKindOfPane, +/// } +/// +/// enum Message { +/// PaneDragged(pane_grid::DragEvent), +/// PaneResized(pane_grid::ResizeEvent), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// pane_grid(&state.panes, |pane, state, is_maximized| { +/// pane_grid::Content::new(match state { +/// Pane::SomePane => text("This is some pane"), +/// Pane::AnotherKindOfPane => text("This is another kind of pane"), +/// }) +/// }) +/// .on_drag(Message::PaneDragged) +/// .on_resize(10, Message::PaneResized) +/// .into() +/// } +/// ``` pub fn pane_grid<'a, T, Message, Theme, Renderer>( state: &'a pane_grid::State<T>, view: impl Fn( @@ -938,8 +2103,8 @@ pub fn pane_grid<'a, T, Message, Theme, Renderer>( ) -> pane_grid::Content<'a, Message, Theme, Renderer>, ) -> PaneGrid<'a, Message, Theme, Renderer> where - Renderer: core::Renderer, Theme: pane_grid::Catalog, + Renderer: core::Renderer, { PaneGrid::new(state, view) } diff --git a/widget/src/image.rs b/widget/src/image.rs index 80e17263..6c84ec92 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -1,4 +1,21 @@ -//! Display images in your user interface. +//! Images display raster graphics in different formats (PNG, JPG, etc.). +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::image; +//! +//! enum Message { +//! // ... +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! image("ferris.png").into() +//! } +//! ``` +//! <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300"> pub mod viewer; pub use viewer::Viewer; @@ -22,16 +39,23 @@ pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> { /// A frame that displays an image while keeping aspect ratio. /// /// # Example -/// /// ```no_run -/// # use iced_widget::image::{self, Image}; -/// # -/// let image = Image::<image::Handle>::new("resources/ferris.png"); -/// ``` +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::image; /// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// image("ferris.png").into() +/// } +/// ``` /// <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300"> #[derive(Debug)] -pub struct Image<Handle> { +pub struct Image<Handle = image::Handle> { handle: Handle, width: Length, height: Length, @@ -39,11 +63,12 @@ pub struct Image<Handle> { filter_method: FilterMethod, rotation: Rotation, opacity: f32, + scale: f32, } impl<Handle> Image<Handle> { /// Creates a new [`Image`] with the given path. - pub fn new<T: Into<Handle>>(handle: T) -> Self { + pub fn new(handle: impl Into<Handle>) -> Self { Image { handle: handle.into(), width: Length::Shrink, @@ -52,6 +77,7 @@ impl<Handle> Image<Handle> { filter_method: FilterMethod::default(), rotation: Rotation::default(), opacity: 1.0, + scale: 1.0, } } @@ -95,6 +121,15 @@ impl<Handle> Image<Handle> { self.opacity = opacity.into(); self } + + /// Sets the scale of the [`Image`]. + /// + /// The region of the [`Image`] drawn will be scaled from the center by the given scale factor. + /// This can be useful to create certain effects and animations, like smooth zoom in / out. + pub fn scale(mut self, scale: impl Into<f32>) -> Self { + self.scale = scale.into(); + self + } } /// Computes the layout of an [`Image`]. @@ -143,11 +178,13 @@ where pub fn draw<Renderer, Handle>( renderer: &mut Renderer, layout: Layout<'_>, + viewport: &Rectangle, handle: &Handle, content_fit: ContentFit, filter_method: FilterMethod, rotation: Rotation, opacity: f32, + scale: f32, ) where Renderer: image::Renderer<Handle = Handle>, Handle: Clone, @@ -159,12 +196,12 @@ pub fn draw<Renderer, Handle>( let bounds = layout.bounds(); let adjusted_fit = content_fit.fit(rotated_size, bounds.size()); - let scale = Vector::new( + let fit_scale = Vector::new( adjusted_fit.width / rotated_size.width, adjusted_fit.height / rotated_size.height, ); - let final_size = image_size * scale; + let final_size = image_size * fit_scale * scale; let position = match content_fit { ContentFit::None => Point::new( @@ -181,17 +218,22 @@ pub fn draw<Renderer, Handle>( let render = |renderer: &mut Renderer| { renderer.draw_image( - handle.clone(), - filter_method, + image::Image { + handle: handle.clone(), + filter_method, + rotation: rotation.radians(), + opacity, + snap: true, + }, drawing_bounds, - rotation.radians(), - opacity, ); }; if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height { - renderer.with_layer(bounds, render); + if let Some(bounds) = bounds.intersection(viewport) { + renderer.with_layer(bounds, render); + } } else { render(renderer); } @@ -235,16 +277,18 @@ where _style: &renderer::Style, layout: Layout<'_>, _cursor: mouse::Cursor, - _viewport: &Rectangle, + viewport: &Rectangle, ) { draw( renderer, layout, + viewport, &self.handle, self.content_fit, self.filter_method, self.rotation, self.opacity, + self.scale, ); } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 8fe6f021..811241a9 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -1,13 +1,12 @@ //! Zoom and pan on an image. -use crate::core::event::{self, Event}; -use crate::core::image; +use crate::core::image::{self, FilterMethod}; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Pixels, Point, Radians, Rectangle, - Shell, Size, Vector, Widget, + Clipboard, ContentFit, Element, Event, Image, Layout, Length, Pixels, + Point, Radians, Rectangle, Shell, Size, Vector, Widget, }; /// A frame that displays an image with the ability to zoom in/out and pan. @@ -20,30 +19,38 @@ pub struct Viewer<Handle> { max_scale: f32, scale_step: f32, handle: Handle, - filter_method: image::FilterMethod, + filter_method: FilterMethod, + content_fit: ContentFit, } impl<Handle> Viewer<Handle> { /// Creates a new [`Viewer`] with the given [`State`]. - pub fn new(handle: Handle) -> Self { + pub fn new<T: Into<Handle>>(handle: T) -> Self { Viewer { - handle, + handle: handle.into(), padding: 0.0, width: Length::Shrink, height: Length::Shrink, min_scale: 0.25, max_scale: 10.0, scale_step: 0.10, - filter_method: image::FilterMethod::default(), + filter_method: FilterMethod::default(), + content_fit: ContentFit::default(), } } - /// Sets the [`image::FilterMethod`] of the [`Viewer`]. + /// Sets the [`FilterMethod`] of the [`Viewer`]. pub fn filter_method(mut self, filter_method: image::FilterMethod) -> Self { self.filter_method = filter_method; self } + /// Sets the [`ContentFit`] of the [`Viewer`]. + pub fn content_fit(mut self, content_fit: ContentFit) -> Self { + self.content_fit = content_fit; + self + } + /// Sets the padding of the [`Viewer`]. pub fn padding(mut self, padding: impl Into<Pixels>) -> Self { self.padding = padding.into().0; @@ -115,58 +122,52 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let Size { width, height } = renderer.measure_image(&self.handle); + // The raw w/h of the underlying image + let image_size = renderer.measure_image(&self.handle); + let image_size = + Size::new(image_size.width as f32, image_size.height as f32); - let mut size = limits.resolve( - self.width, - self.height, - Size::new(width as f32, height as f32), - ); + // The size to be available to the widget prior to `Shrink`ing + let raw_size = limits.resolve(self.width, self.height, image_size); - let expansion_size = if height > width { - self.width - } else { - self.height + // The uncropped size of the image when fit to the bounds above + let full_size = self.content_fit.fit(image_size, raw_size); + + // Shrink the widget to fit the resized image, if requested + let final_size = Size { + width: match self.width { + Length::Shrink => f32::min(raw_size.width, full_size.width), + _ => raw_size.width, + }, + height: match self.height { + Length::Shrink => f32::min(raw_size.height, full_size.height), + _ => raw_size.height, + }, }; - // Only calculate viewport sizes if the images are constrained to a limited space. - // If they are Fill|Portion let them expand within their allotted space. - match expansion_size { - Length::Shrink | Length::Fixed(_) => { - let aspect_ratio = width as f32 / height as f32; - let viewport_aspect_ratio = size.width / size.height; - if viewport_aspect_ratio > aspect_ratio { - size.width = width as f32 * size.height / height as f32; - } else { - size.height = height as f32 * size.width / width as f32; - } - } - Length::Fill | Length::FillPortion(_) => {} - } - - layout::Node::new(size) + layout::Node::new(final_size) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, _clipboard: &mut dyn Clipboard, - _shell: &mut Shell<'_, Message>, + shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let bounds = layout.bounds(); match event { Event::Mouse(mouse::Event::WheelScrolled { delta }) => { let Some(cursor_position) = cursor.position_over(bounds) else { - return event::Status::Ignored; + return; }; - match delta { + match *delta { mouse::ScrollDelta::Lines { y, .. } | mouse::ScrollDelta::Pixels { y, .. } => { let state = tree.state.downcast_mut::<State>(); @@ -182,11 +183,12 @@ where }) .clamp(self.min_scale, self.max_scale); - let image_size = image_size( + let scaled_size = scaled_image_size( renderer, &self.handle, state, bounds.size(), + self.content_fit, ); let factor = state.scale / previous_scale - 1.0; @@ -198,12 +200,12 @@ where + state.current_offset * factor; state.current_offset = Vector::new( - if image_size.width > bounds.width { + if scaled_size.width > bounds.width { state.current_offset.x + adjustment.x } else { 0.0 }, - if image_size.height > bounds.height { + if scaled_size.height > bounds.height { state.current_offset.y + adjustment.y } else { 0.0 @@ -213,11 +215,12 @@ where } } - event::Status::Captured + shell.request_redraw(); + shell.capture_event(); } Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { let Some(cursor_position) = cursor.position_over(bounds) else { - return event::Status::Ignored; + return; }; let state = tree.state.downcast_mut::<State>(); @@ -225,49 +228,48 @@ where state.cursor_grabbed_at = Some(cursor_position); state.starting_offset = state.current_offset; - event::Status::Captured + shell.request_redraw(); + shell.capture_event(); } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { let state = tree.state.downcast_mut::<State>(); if state.cursor_grabbed_at.is_some() { state.cursor_grabbed_at = None; - - event::Status::Captured - } else { - event::Status::Ignored + shell.request_redraw(); + shell.capture_event(); } } Event::Mouse(mouse::Event::CursorMoved { position }) => { let state = tree.state.downcast_mut::<State>(); if let Some(origin) = state.cursor_grabbed_at { - let image_size = image_size( + let scaled_size = scaled_image_size( renderer, &self.handle, state, bounds.size(), + self.content_fit, ); - - let hidden_width = (image_size.width - bounds.width / 2.0) + let hidden_width = (scaled_size.width - bounds.width / 2.0) .max(0.0) .round(); - let hidden_height = (image_size.height + let hidden_height = (scaled_size.height - bounds.height / 2.0) .max(0.0) .round(); - let delta = position - origin; + let delta = *position - origin; - let x = if bounds.width < image_size.width { + let x = if bounds.width < scaled_size.width { (state.starting_offset.x - delta.x) .clamp(-hidden_width, hidden_width) } else { 0.0 }; - let y = if bounds.height < image_size.height { + let y = if bounds.height < scaled_size.height { (state.starting_offset.y - delta.y) .clamp(-hidden_height, hidden_height) } else { @@ -275,13 +277,11 @@ where }; state.current_offset = Vector::new(x, y); - - event::Status::Captured - } else { - event::Status::Ignored + shell.request_redraw(); + shell.capture_event(); } } - _ => event::Status::Ignored, + _ => {} } } @@ -319,33 +319,46 @@ where let state = tree.state.downcast_ref::<State>(); let bounds = layout.bounds(); - let image_size = - image_size(renderer, &self.handle, state, bounds.size()); + let final_size = scaled_image_size( + renderer, + &self.handle, + state, + bounds.size(), + self.content_fit, + ); let translation = { - let image_top_left = Vector::new( - bounds.width / 2.0 - image_size.width / 2.0, - bounds.height / 2.0 - image_size.height / 2.0, - ); + let diff_w = bounds.width - final_size.width; + let diff_h = bounds.height - final_size.height; - image_top_left - state.offset(bounds, image_size) + let image_top_left = match self.content_fit { + ContentFit::None => { + Vector::new(diff_w.max(0.0) / 2.0, diff_h.max(0.0) / 2.0) + } + _ => Vector::new(diff_w / 2.0, diff_h / 2.0), + }; + + image_top_left - state.offset(bounds, final_size) }; - renderer.with_layer(bounds, |renderer| { + let drawing_bounds = Rectangle::new(bounds.position(), final_size); + + let render = |renderer: &mut Renderer| { renderer.with_translation(translation, |renderer| { renderer.draw_image( - self.handle.clone(), - self.filter_method, - Rectangle { - x: bounds.x, - y: bounds.y, - ..Rectangle::with_size(image_size) + Image { + handle: self.handle.clone(), + filter_method: self.filter_method, + rotation: Radians(0.0), + opacity: 1.0, + snap: true, }, - Radians(0.0), - 1.0, + drawing_bounds, ); }); - }); + }; + + renderer.with_layer(bounds, render); } } @@ -411,32 +424,23 @@ where /// Returns the bounds of the underlying image, given the bounds of /// the [`Viewer`]. Scaling will be applied and original aspect ratio /// will be respected. -pub fn image_size<Renderer>( +pub fn scaled_image_size<Renderer>( renderer: &Renderer, handle: &<Renderer as image::Renderer>::Handle, state: &State, bounds: Size, + content_fit: ContentFit, ) -> Size where Renderer: image::Renderer, { let Size { width, height } = renderer.measure_image(handle); + let image_size = Size::new(width as f32, height as f32); - let (width, height) = { - let dimensions = (width as f32, height as f32); + let adjusted_fit = content_fit.fit(image_size, bounds); - let width_ratio = bounds.width / dimensions.0; - let height_ratio = bounds.height / dimensions.1; - - let ratio = width_ratio.min(height_ratio); - let scale = state.scale; - - if ratio < 1.0 { - (dimensions.0 * ratio * scale, dimensions.1 * ratio * scale) - } else { - (dimensions.0 * scale, dimensions.1 * scale) - } - }; - - Size::new(width, height) + Size::new( + adjusted_fit.width * state.scale, + adjusted_fit.height * state.scale, + ) } diff --git a/widget/src/keyed.rs b/widget/src/keyed.rs index ad531e66..923cb118 100644 --- a/widget/src/keyed.rs +++ b/widget/src/keyed.rs @@ -1,4 +1,4 @@ -//! Use widgets that can provide hints to ensure continuity. +//! Keyed widgets can provide hints to ensure continuity. //! //! # What is continuity? //! Continuity is the feeling of persistence of state. @@ -41,13 +41,35 @@ pub mod column; pub use column::Column; -/// Creates a [`Column`] with the given children. +/// Creates a keyed [`Column`] with the given children. +/// +/// Keyed columns distribute content vertically while keeping continuity. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::keyed_column; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// keyed_column![ +/// (0, "Item 0"), +/// (1, "Item 1"), +/// (2, "Item 2"), +/// ].into() +/// } +/// ``` #[macro_export] macro_rules! keyed_column { () => ( - $crate::Column::new() + $crate::keyed::Column::new() ); - ($($x:expr),+ $(,)?) => ( - $crate::keyed::Column::with_children(vec![$($crate::core::Element::from($x)),+]) + ($(($key:expr, $x:expr)),+ $(,)?) => ( + $crate::keyed::Column::with_children(vec![$(($key, $crate::core::Element::from($x))),+]) ); } diff --git a/widget/src/keyed/column.rs b/widget/src/keyed/column.rs index fdaadefa..3064a8c4 100644 --- a/widget/src/keyed/column.rs +++ b/widget/src/keyed/column.rs @@ -1,17 +1,34 @@ -//! Distribute content vertically. -use crate::core::event::{self, Event}; +//! Keyed columns distribute content vertically while keeping continuity. use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; -use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; +use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Alignment, Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, - Shell, Size, Vector, Widget, + Alignment, Clipboard, Element, Event, Layout, Length, Padding, Pixels, + Rectangle, Shell, Size, Vector, Widget, }; -/// A container that distributes its contents vertically. +/// A container that distributes its contents vertically while keeping continuity. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{keyed_column, text}; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// keyed_column((0..=100).map(|i| { +/// (i, text!("Item {i}").into()) +/// })).into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Column< 'a, @@ -168,7 +185,7 @@ where } } -impl<'a, Key, Message, Renderer> Default for Column<'a, Key, Message, Renderer> +impl<Key, Message, Renderer> Default for Column<'_, Key, Message, Renderer> where Key: Copy + PartialEq, Renderer: crate::core::Renderer, @@ -185,8 +202,8 @@ where keys: Vec<Key>, } -impl<'a, Key, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for Column<'a, Key, Message, Theme, Renderer> +impl<Key, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Column<'_, Key, Message, Theme, Renderer> where Renderer: crate::core::Renderer, Key: Copy + PartialEq + 'static, @@ -265,7 +282,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children @@ -280,34 +297,28 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.children + ) { + for ((child, state), layout) in self + .children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) + { + child.as_widget_mut().update( + state, event, layout, cursor, renderer, clipboard, shell, + viewport, + ); + } } fn mouse_interaction( diff --git a/widget/src/lazy.rs b/widget/src/lazy.rs index 04783dbe..8b7b38ce 100644 --- a/widget/src/lazy.rs +++ b/widget/src/lazy.rs @@ -4,21 +4,21 @@ pub(crate) mod helpers; pub mod component; pub mod responsive; +#[allow(deprecated)] pub use component::Component; pub use responsive::Responsive; mod cache; -use crate::core::event::{self, Event}; +use crate::core::Element; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Widget}; -use crate::core::Element; use crate::core::{ - self, Clipboard, Length, Point, Rectangle, Shell, Size, Vector, + self, Clipboard, Event, Length, Point, Rectangle, Shell, Size, Vector, }; use crate::runtime::overlay::Nested; @@ -29,6 +29,7 @@ use std::hash::{Hash, Hasher as H}; use std::rc::Rc; /// A widget that only rebuilds its contents when necessary. +#[cfg(feature = "lazy")] #[allow(missing_debug_implementations)] pub struct Lazy<'a, Message, Theme, Renderer, Dependency, View> { dependency: Dependency, @@ -182,7 +183,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation, ) { self.with_element(|element| { element.as_widget().operate( @@ -194,19 +195,19 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { self.with_element_mut(|element| { - element.as_widget_mut().on_event( + element.as_widget_mut().update( &mut tree.children[0], event, layout, @@ -215,8 +216,8 @@ where clipboard, shell, viewport, - ) - }) + ); + }); } fn mouse_interaction( @@ -267,30 +268,41 @@ where layout: Layout<'_>, renderer: &Renderer, translation: Vector, - ) -> Option<overlay::Element<'_, Message, Theme, Renderer>> { - let overlay = Overlay(Some( - InnerBuilder { - cell: self.element.borrow().as_ref().unwrap().clone(), - element: self - .element - .borrow() - .as_ref() - .unwrap() - .borrow_mut() - .take() - .unwrap(), - tree: &mut tree.children[0], - overlay_builder: |element, tree| { - element - .as_widget_mut() - .overlay(tree, layout, renderer, translation) - .map(|overlay| RefCell::new(Nested::new(overlay))) - }, - } - .build(), - )); + ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { + let overlay = InnerBuilder { + cell: self.element.borrow().as_ref().unwrap().clone(), + element: self + .element + .borrow() + .as_ref() + .unwrap() + .borrow_mut() + .take() + .unwrap(), + tree: &mut tree.children[0], + overlay_builder: |element, tree| { + element + .as_widget_mut() + .overlay(tree, layout, renderer, translation) + .map(|overlay| RefCell::new(Nested::new(overlay))) + }, + } + .build(); - Some(overlay::Element::new(Box::new(overlay))) + #[allow(clippy::redundant_closure_for_method_calls)] + if overlay.with_overlay(|overlay| overlay.is_some()) { + Some(overlay::Element::new(Box::new(Overlay(Some(overlay))))) + } else { + let heads = overlay.into_heads(); + + // - You may not like it, but this is what peak performance looks like + // - TODO: Get rid of ouroboros, for good + // - What?! + *self.element.borrow().as_ref().unwrap().borrow_mut() = + Some(heads.element); + + None + } } } @@ -309,16 +321,14 @@ struct Overlay<'a, Message, Theme, Renderer>( Option<Inner<'a, Message, Theme, Renderer>>, ); -impl<'a, Message, Theme, Renderer> Drop - for Overlay<'a, Message, Theme, Renderer> -{ +impl<Message, Theme, Renderer> Drop for Overlay<'_, Message, Theme, Renderer> { fn drop(&mut self) { let heads = self.0.take().unwrap().into_heads(); (*heads.cell.borrow_mut()) = Some(heads.element); } } -impl<'a, Message, Theme, Renderer> Overlay<'a, Message, Theme, Renderer> { +impl<Message, Theme, Renderer> Overlay<'_, Message, Theme, Renderer> { fn with_overlay_maybe<T>( &self, f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T, @@ -338,8 +348,8 @@ impl<'a, Message, Theme, Renderer> Overlay<'a, Message, Theme, Renderer> { } } -impl<'a, Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer> - for Overlay<'a, Message, Theme, Renderer> +impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer> + for Overlay<'_, Message, Theme, Renderer> where Renderer: core::Renderer, { @@ -374,19 +384,18 @@ where .unwrap_or_default() } - 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.with_overlay_mut_maybe(|overlay| { - overlay.on_event(event, layout, cursor, renderer, clipboard, shell) - }) - .unwrap_or(event::Status::Ignored) + ) { + let _ = self.with_overlay_mut_maybe(|overlay| { + overlay.update(event, layout, cursor, renderer, clipboard, shell); + }); } fn is_over( diff --git a/widget/src/lazy/cache.rs b/widget/src/lazy/cache.rs index f922fd19..367eefda 100644 --- a/widget/src/lazy/cache.rs +++ b/widget/src/lazy/cache.rs @@ -1,5 +1,6 @@ -use crate::core::overlay; +#![allow(dead_code)] use crate::core::Element; +use crate::core::overlay; use ouroboros::self_referencing; diff --git a/widget/src/lazy/component.rs b/widget/src/lazy/component.rs index 7ba71a02..0cfcc953 100644 --- a/widget/src/lazy/component.rs +++ b/widget/src/lazy/component.rs @@ -1,5 +1,5 @@ //! Build and reuse custom widgets using The Elm Architecture. -use crate::core::event; +#![allow(deprecated)] use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; @@ -30,6 +30,25 @@ use std::rc::Rc; /// /// Additionally, a [`Component`] is capable of producing a `Message` to notify /// the parent application of any relevant interactions. +/// +/// # State +/// A component can store its state in one of two ways: either as data within the +/// implementor of the trait, or in a type [`State`][Component::State] that is managed +/// by the runtime and provided to the trait methods. These two approaches are not +/// mutually exclusive and have opposite pros and cons. +/// +/// For instance, if a piece of state is needed by multiple components that reside +/// in different branches of the tree, then it's more convenient to let a common +/// ancestor store it and pass it down. +/// +/// On the other hand, if a piece of state is only needed by the component itself, +/// you can store it as part of its internal [`State`][Component::State]. +#[cfg(feature = "lazy")] +#[deprecated( + since = "0.13.0", + note = "components introduce encapsulated state and hamper the use of a single source of truth. \ + Instead, leverage the Elm Architecture directly, or implement a custom widget" +)] pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> { /// The internal state of this [`Component`]. type State: Default; @@ -58,8 +77,9 @@ pub trait Component<Message, Theme = crate::Theme, Renderer = crate::Renderer> { /// By default, it does nothing. fn operate( &self, + _bounds: Rectangle, _state: &mut Self::State, - _operation: &mut dyn widget::Operation<Message>, + _operation: &mut dyn widget::Operation, ) { } @@ -121,8 +141,8 @@ struct State<'a, Message: 'a, Theme: 'a, Renderer: 'a, Event: 'a, S: 'a> { element: Option<Element<'this, Event, Theme, Renderer>>, } -impl<'a, Message, Theme, Renderer, Event, S> - Instance<'a, Message, Theme, Renderer, Event, S> +impl<Message, Theme, Renderer, Event, S> + Instance<'_, Message, Theme, Renderer, Event, S> where S: Default + 'static, Renderer: renderer::Renderer, @@ -172,11 +192,13 @@ where fn rebuild_element_with_operation( &self, - operation: &mut dyn widget::Operation<Message>, + layout: Layout<'_>, + operation: &mut dyn widget::Operation, ) { let heads = self.state.borrow_mut().take().unwrap().into_heads(); heads.component.operate( + layout.bounds(), self.tree .borrow_mut() .borrow_mut() @@ -231,8 +253,8 @@ where } } -impl<'a, Message, Theme, Renderer, Event, S> Widget<Message, Theme, Renderer> - for Instance<'a, Message, Theme, Renderer, Event, S> +impl<Message, Theme, Renderer, Event, S> Widget<Message, Theme, Renderer> + for Instance<'_, Message, Theme, Renderer, Event, S> where S: 'static + Default, Renderer: core::Renderer, @@ -247,7 +269,10 @@ where state: tree::State::new(S::default()), children: vec![Tree::empty()], }))); + *self.tree.borrow_mut() = state.clone(); + self.diff_self(); + tree::State::new(state) } @@ -291,23 +316,23 @@ where }) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: core::Event, + event: &core::Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let mut local_messages = Vec::new(); let mut local_shell = Shell::new(&mut local_messages); let t = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>(); - let event_status = self.with_element_mut(|element| { - element.as_widget_mut().on_event( + self.with_element_mut(|element| { + element.as_widget_mut().update( &mut t.borrow_mut().as_mut().unwrap().children[0], event, layout, @@ -316,15 +341,17 @@ where clipboard, &mut local_shell, viewport, - ) + ); }); - local_shell.revalidate_layout(|| shell.invalidate_layout()); - - if let Some(redraw_request) = local_shell.redraw_request() { - shell.request_redraw(redraw_request); + if local_shell.is_event_captured() { + shell.capture_event(); } + local_shell.revalidate_layout(|| shell.invalidate_layout()); + shell.request_redraw_at(local_shell.redraw_request()); + shell.request_input_method(local_shell.input_method()); + if !local_messages.is_empty() { let mut heads = self.state.take().unwrap().into_heads(); @@ -349,8 +376,6 @@ where shell.invalidate_layout(); } - - event_status } fn operate( @@ -358,62 +383,9 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation, ) { - self.rebuild_element_with_operation(operation); - - struct MapOperation<'a, B> { - operation: &'a mut dyn widget::Operation<B>, - } - - 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 text_input( - &mut self, - state: &mut dyn widget::operation::TextInput, - id: Option<&widget::Id>, - ) { - self.operation.text_input(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 custom( - &mut self, - state: &mut dyn std::any::Any, - id: Option<&widget::Id>, - ) { - self.operation.custom(state, id); - } - } + self.rebuild_element_with_operation(layout, operation); let tree = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>(); self.with_element(|element| { @@ -421,7 +393,7 @@ where &mut tree.borrow_mut().as_mut().unwrap().children[0], layout, renderer, - &mut MapOperation { operation }, + operation, ); }); } @@ -479,44 +451,48 @@ where ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { self.rebuild_element_if_necessary(); - let tree = tree - .state - .downcast_mut::<Rc<RefCell<Option<Tree>>>>() - .borrow_mut() - .take() - .unwrap(); + let state = tree.state.downcast_mut::<Rc<RefCell<Option<Tree>>>>(); + let tree = state.borrow_mut().take().unwrap(); - let overlay = Overlay(Some( - InnerBuilder { - instance: self, - tree, - types: PhantomData, - overlay_builder: |instance, tree| { - instance.state.get_mut().as_mut().unwrap().with_element_mut( - move |element| { - element - .as_mut() - .unwrap() - .as_widget_mut() - .overlay( - &mut tree.children[0], - layout, - renderer, - translation, - ) - .map(|overlay| { - RefCell::new(Nested::new(overlay)) - }) - }, - ) - }, - } - .build(), - )); + let overlay = InnerBuilder { + instance: self, + tree, + types: PhantomData, + overlay_builder: |instance, tree| { + instance.state.get_mut().as_mut().unwrap().with_element_mut( + move |element| { + element + .as_mut() + .unwrap() + .as_widget_mut() + .overlay( + &mut tree.children[0], + layout, + renderer, + translation, + ) + .map(|overlay| RefCell::new(Nested::new(overlay))) + }, + ) + }, + } + .build(); - Some(overlay::Element::new(Box::new(OverlayInstance { - overlay: Some(overlay), - }))) + #[allow(clippy::redundant_closure_for_method_calls)] + if overlay.with_overlay(|overlay| overlay.is_some()) { + Some(overlay::Element::new(Box::new(OverlayInstance { + overlay: Some(Overlay(Some(overlay))), // Beautiful, I know + }))) + } else { + let heads = overlay.into_heads(); + + // - You may not like it, but this is what peak performance looks like + // - TODO: Get rid of ouroboros, for good + // - What?! + *state.borrow_mut() = Some(heads.tree); + + None + } } } @@ -524,8 +500,8 @@ struct Overlay<'a, 'b, Message, Theme, Renderer, Event, S>( Option<Inner<'a, 'b, Message, Theme, Renderer, Event, S>>, ); -impl<'a, 'b, Message, Theme, Renderer, Event, S> Drop - for Overlay<'a, 'b, Message, Theme, Renderer, Event, S> +impl<Message, Theme, Renderer, Event, S> Drop + for Overlay<'_, '_, Message, Theme, Renderer, Event, S> { fn drop(&mut self) { if let Some(heads) = self.0.take().map(Inner::into_heads) { @@ -549,8 +525,8 @@ struct OverlayInstance<'a, 'b, Message, Theme, Renderer, Event, S> { overlay: Option<Overlay<'a, 'b, Message, Theme, Renderer, Event, S>>, } -impl<'a, 'b, Message, Theme, Renderer, Event, S> - OverlayInstance<'a, 'b, Message, Theme, Renderer, Event, S> +impl<Message, Theme, Renderer, Event, S> + OverlayInstance<'_, '_, Message, Theme, Renderer, Event, S> { fn with_overlay_maybe<T>( &self, @@ -583,9 +559,9 @@ impl<'a, 'b, Message, Theme, Renderer, Event, S> } } -impl<'a, 'b, Message, Theme, Renderer, Event, S> +impl<Message, Theme, Renderer, Event, S> overlay::Overlay<Message, Theme, Renderer> - for OverlayInstance<'a, 'b, Message, Theme, Renderer, Event, S> + for OverlayInstance<'_, '_, Message, Theme, Renderer, Event, S> where Renderer: core::Renderer, S: 'static + Default, @@ -621,36 +597,36 @@ where .unwrap_or_default() } - fn on_event( + fn update( &mut self, - event: core::Event, + event: &core::Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) { let mut local_messages = Vec::new(); let mut local_shell = Shell::new(&mut local_messages); - let event_status = self - .with_overlay_mut_maybe(|overlay| { - overlay.on_event( - event, - layout, - cursor, - renderer, - clipboard, - &mut local_shell, - ) - }) - .unwrap_or(event::Status::Ignored); + let _ = self.with_overlay_mut_maybe(|overlay| { + overlay.update( + event, + layout, + cursor, + renderer, + clipboard, + &mut local_shell, + ); + }); + + if local_shell.is_event_captured() { + shell.capture_event(); + } local_shell.revalidate_layout(|| shell.invalidate_layout()); - - if let Some(redraw_request) = local_shell.redraw_request() { - shell.request_redraw(redraw_request); - } + shell.request_redraw_at(local_shell.redraw_request()); + shell.request_input_method(local_shell.input_method()); if !local_messages.is_empty() { let mut inner = @@ -687,8 +663,6 @@ where shell.invalidate_layout(); } - - event_status } fn is_over( diff --git a/widget/src/lazy/helpers.rs b/widget/src/lazy/helpers.rs index 4d0776ca..52e690ff 100644 --- a/widget/src/lazy/helpers.rs +++ b/widget/src/lazy/helpers.rs @@ -1,9 +1,11 @@ use crate::core::{self, Element, Size}; -use crate::lazy::component::{self, Component}; -use crate::lazy::{Lazy, Responsive}; +use crate::lazy::component; use std::hash::Hash; +#[allow(deprecated)] +pub use crate::lazy::{Component, Lazy, Responsive}; + /// Creates a new [`Lazy`] widget with the given data `Dependency` and a /// closure that can turn this data into a widget tree. #[cfg(feature = "lazy")] @@ -21,6 +23,12 @@ where /// Turns an implementor of [`Component`] into an [`Element`] that can be /// embedded in any application. #[cfg(feature = "lazy")] +#[deprecated( + since = "0.13.0", + note = "components introduce encapsulated state and hamper the use of a single source of truth. \ + Instead, leverage the Elm Architecture directly, or implement a custom widget" +)] +#[allow(deprecated)] pub fn component<'a, C, Message, Theme, Renderer>( component: C, ) -> Element<'a, Message, Theme, Renderer> diff --git a/widget/src/lazy/responsive.rs b/widget/src/lazy/responsive.rs index f612102e..e7c937af 100644 --- a/widget/src/lazy/responsive.rs +++ b/widget/src/lazy/responsive.rs @@ -1,4 +1,3 @@ -use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; @@ -6,8 +5,8 @@ use crate::core::renderer; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - self, Clipboard, Element, Length, Point, Rectangle, Shell, Size, Vector, - Widget, + self, Clipboard, Element, Event, Length, Point, Rectangle, Shell, Size, + Vector, Widget, }; use crate::horizontal_space; use crate::runtime::overlay::Nested; @@ -21,6 +20,7 @@ use std::ops::Deref; /// /// A [`Responsive`] widget will always try to fill all the available space of /// its parent. +#[cfg(feature = "lazy")] #[allow(missing_debug_implementations)] pub struct Responsive< 'a, @@ -82,15 +82,21 @@ where new_size: Size, view: &dyn Fn(Size) -> Element<'a, Message, Theme, Renderer>, ) { - if self.size == new_size { - return; + if self.size != new_size { + self.element = view(new_size); + self.size = new_size; + self.layout = None; + + tree.diff(&self.element); + } else { + let is_tree_empty = + tree.tag == tree::Tag::stateless() && tree.children.is_empty(); + + if is_tree_empty { + self.layout = None; + tree.diff(&self.element); + } } - - self.element = view(new_size); - self.size = new_size; - self.layout = None; - - tree.diff(&self.element); } fn resolve<R, T>( @@ -125,8 +131,8 @@ struct State { tree: RefCell<Tree>, } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for Responsive<'a, Message, Theme, Renderer> +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Responsive<'_, Message, Theme, Renderer> where Renderer: core::Renderer, { @@ -161,7 +167,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation, ) { let state = tree.state.downcast_mut::<State>(); let mut content = self.content.borrow_mut(); @@ -179,30 +185,30 @@ where ); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let state = tree.state.downcast_mut::<State>(); let mut content = self.content.borrow_mut(); let mut local_messages = vec![]; let mut local_shell = Shell::new(&mut local_messages); - let status = content.resolve( + content.resolve( &mut state.tree.borrow_mut(), renderer, layout, &self.view, |tree, renderer, layout, element| { - element.as_widget_mut().on_event( + element.as_widget_mut().update( tree, event, layout, @@ -211,7 +217,7 @@ where clipboard, &mut local_shell, viewport, - ) + ); }, ); @@ -220,8 +226,6 @@ where } shell.merge(local_shell, std::convert::identity); - - status } fn draw( @@ -319,7 +323,11 @@ where } .build(); - Some(overlay::Element::new(Box::new(overlay))) + if overlay.with_overlay(|(overlay, _layout)| overlay.is_some()) { + Some(overlay::Element::new(Box::new(overlay))) + } else { + None + } } } @@ -350,9 +358,7 @@ struct Overlay<'a, 'b, Message, Theme, Renderer> { ), } -impl<'a, 'b, Message, Theme, Renderer> - Overlay<'a, 'b, Message, Theme, Renderer> -{ +impl<Message, Theme, Renderer> Overlay<'_, '_, Message, Theme, Renderer> { fn with_overlay_maybe<T>( &self, f: impl FnOnce(&mut Nested<'_, Message, Theme, Renderer>) -> T, @@ -372,9 +378,8 @@ impl<'a, 'b, Message, Theme, Renderer> } } -impl<'a, 'b, Message, Theme, Renderer> - overlay::Overlay<Message, Theme, Renderer> - for Overlay<'a, 'b, Message, Theme, Renderer> +impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer> + for Overlay<'_, '_, Message, Theme, Renderer> where Renderer: core::Renderer, { @@ -409,36 +414,28 @@ where .unwrap_or_default() } - 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 { + ) { let mut is_layout_invalid = false; - let event_status = self - .with_overlay_mut_maybe(|overlay| { - let event_status = overlay.on_event( - event, layout, cursor, renderer, clipboard, shell, - ); + let _ = self.with_overlay_mut_maybe(|overlay| { + overlay.update(event, layout, cursor, renderer, clipboard, shell); - is_layout_invalid = shell.is_layout_invalid(); - - event_status - }) - .unwrap_or(event::Status::Ignored); + is_layout_invalid = shell.is_layout_invalid(); + }); if is_layout_invalid { self.with_overlay_mut(|(_overlay, layout)| { **layout = None; }); } - - event_status } fn is_over( @@ -452,4 +449,15 @@ where }) .unwrap_or_default() } + + fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + let _ = self.with_overlay_mut_maybe(|overlay| { + overlay.operate(layout, renderer, operation); + }); + } } diff --git a/widget/src/lib.rs b/widget/src/lib.rs index 00e9aaa4..31dcc205 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -8,9 +8,10 @@ pub use iced_renderer::graphics; pub use iced_runtime as runtime; pub use iced_runtime::core; +mod action; mod column; mod mouse_area; -mod row; +mod pin; mod space; mod stack; mod themer; @@ -23,8 +24,10 @@ pub mod keyed; pub mod overlay; pub mod pane_grid; pub mod pick_list; +pub mod pop; pub mod progress_bar; pub mod radio; +pub mod row; pub mod rule; pub mod scrollable; pub mod slider; @@ -42,9 +45,6 @@ pub use helpers::*; #[cfg(feature = "lazy")] mod lazy; -#[cfg(feature = "lazy")] -pub use crate::lazy::{Component, Lazy, Responsive}; - #[cfg(feature = "lazy")] pub use crate::lazy::helpers::*; @@ -65,6 +65,10 @@ pub use pane_grid::PaneGrid; #[doc(no_inline)] pub use pick_list::PickList; #[doc(no_inline)] +pub use pin::Pin; +#[doc(no_inline)] +pub use pop::Pop; +#[doc(no_inline)] pub use progress_bar::ProgressBar; #[doc(no_inline)] pub use radio::Radio; @@ -130,5 +134,9 @@ pub mod qr_code; #[doc(no_inline)] pub use qr_code::QRCode; +#[cfg(feature = "markdown")] +pub mod markdown; + pub use crate::core::theme::{self, Theme}; +pub use action::Action; pub use renderer::Renderer; diff --git a/widget/src/markdown.rs b/widget/src/markdown.rs new file mode 100644 index 00000000..d4de2a8c --- /dev/null +++ b/widget/src/markdown.rs @@ -0,0 +1,1337 @@ +//! Markdown widgets can parse and display Markdown. +//! +//! You can enable the `highlighter` feature for syntax highlighting +//! in code blocks. +//! +//! Only the variants of [`Item`] are currently supported. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::markdown; +//! use iced::Theme; +//! +//! struct State { +//! markdown: Vec<markdown::Item>, +//! } +//! +//! enum Message { +//! LinkClicked(markdown::Url), +//! } +//! +//! impl State { +//! pub fn new() -> Self { +//! Self { +//! markdown: markdown::parse("This is some **Markdown**!").collect(), +//! } +//! } +//! +//! fn view(&self) -> Element<'_, Message> { +//! markdown::view(&self.markdown, Theme::TokyoNight) +//! .map(Message::LinkClicked) +//! .into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::LinkClicked(url) => { +//! println!("The following url was clicked: {url}"); +//! } +//! } +//! } +//! } +//! ``` +use crate::core::border; +use crate::core::font::{self, Font}; +use crate::core::padding; +use crate::core::theme; +use crate::core::{ + self, Color, Element, Length, Padding, Pixels, Theme, color, +}; +use crate::{column, container, rich_text, row, scrollable, span, text}; + +use std::borrow::BorrowMut; +use std::cell::{Cell, RefCell}; +use std::collections::{HashMap, HashSet}; +use std::mem; +use std::ops::Range; +use std::rc::Rc; +use std::sync::Arc; + +pub use core::text::Highlight; +pub use pulldown_cmark::HeadingLevel; +pub use url::Url; + +/// A bunch of Markdown that has been parsed. +#[derive(Debug, Default)] +pub struct Content { + items: Vec<Item>, + incomplete: HashMap<usize, Section>, + state: State, +} + +#[derive(Debug)] +struct Section { + content: String, + broken_links: HashSet<String>, +} + +impl Content { + /// Creates a new empty [`Content`]. + pub fn new() -> Self { + Self::default() + } + + /// Creates some new [`Content`] by parsing the given Markdown. + pub fn parse(markdown: &str) -> Self { + let mut content = Self::new(); + content.push_str(markdown); + content + } + + /// Pushes more Markdown into the [`Content`]; parsing incrementally! + /// + /// This is specially useful when you have long streams of Markdown; like + /// big files or potentially long replies. + pub fn push_str(&mut self, markdown: &str) { + if markdown.is_empty() { + return; + } + + // Append to last leftover text + let mut leftover = std::mem::take(&mut self.state.leftover); + leftover.push_str(markdown); + + // Pop the last item + let _ = self.items.pop(); + + // Re-parse last item and new text + for (item, source, broken_links) in + parse_with(&mut self.state, &leftover) + { + if !broken_links.is_empty() { + let _ = self.incomplete.insert( + self.items.len(), + Section { + content: source.to_owned(), + broken_links, + }, + ); + } + + self.items.push(item); + } + + // Re-parse incomplete sections if new references are available + if !self.incomplete.is_empty() { + self.incomplete.retain(|index, section| { + if self.items.len() <= *index { + return false; + } + + let broken_links_before = section.broken_links.len(); + + section + .broken_links + .retain(|link| !self.state.references.contains_key(link)); + + if broken_links_before != section.broken_links.len() { + let mut state = State { + leftover: String::new(), + references: self.state.references.clone(), + images: HashSet::new(), + #[cfg(feature = "highlighter")] + highlighter: None, + }; + + if let Some((item, _source, _broken_links)) = + parse_with(&mut state, §ion.content).next() + { + self.items[*index] = item; + } + + self.state.images.extend(state.images.drain()); + drop(state); + } + + !section.broken_links.is_empty() + }); + } + } + + /// Returns the Markdown items, ready to be rendered. + /// + /// You can use [`view`] to turn them into an [`Element`]. + pub fn items(&self) -> &[Item] { + &self.items + } + + /// Returns the URLs of the Markdown images present in the [`Content`]. + pub fn images(&self) -> &HashSet<Url> { + &self.state.images + } +} + +/// A Markdown item. +#[derive(Debug, Clone)] +pub enum Item { + /// A heading. + Heading(pulldown_cmark::HeadingLevel, Text), + /// A paragraph. + Paragraph(Text), + /// A code block. + /// + /// You can enable the `highlighter` feature for syntax highlighting. + CodeBlock { + /// The language of the code block, if any. + language: Option<String>, + /// The raw code of the code block. + code: String, + /// The styled lines of text in the code block. + lines: Vec<Text>, + }, + /// A list. + List { + /// The first number of the list, if it is ordered. + start: Option<u64>, + /// The items of the list. + items: Vec<Vec<Item>>, + }, + /// An image. + Image { + /// The destination URL of the image. + url: Url, + /// The title of the image. + title: String, + /// The alternative text of the image. + alt: Text, + }, +} + +/// A bunch of parsed Markdown text. +#[derive(Debug, Clone)] +pub struct Text { + spans: Vec<Span>, + last_style: Cell<Option<Style>>, + last_styled_spans: RefCell<Arc<[text::Span<'static, Url>]>>, +} + +impl Text { + fn new(spans: Vec<Span>) -> Self { + Self { + spans, + last_style: Cell::default(), + last_styled_spans: RefCell::default(), + } + } + + /// Returns the [`rich_text()`] spans ready to be used for the given style. + /// + /// This method performs caching for you. It will only reallocate if the [`Style`] + /// provided changes. + pub fn spans(&self, style: Style) -> Arc<[text::Span<'static, Url>]> { + if Some(style) != self.last_style.get() { + *self.last_styled_spans.borrow_mut() = + self.spans.iter().map(|span| span.view(&style)).collect(); + + self.last_style.set(Some(style)); + } + + self.last_styled_spans.borrow().clone() + } +} + +#[derive(Debug, Clone)] +enum Span { + Standard { + text: String, + strikethrough: bool, + link: Option<Url>, + strong: bool, + emphasis: bool, + code: bool, + }, + #[cfg(feature = "highlighter")] + Highlight { + text: String, + color: Option<Color>, + font: Option<Font>, + }, +} + +impl Span { + fn view(&self, style: &Style) -> text::Span<'static, Url> { + match self { + Span::Standard { + text, + strikethrough, + link, + strong, + emphasis, + code, + } => { + let span = span(text.clone()).strikethrough(*strikethrough); + + let span = if *code { + span.font(Font::MONOSPACE) + .color(style.inline_code_color) + .background(style.inline_code_highlight.background) + .border(style.inline_code_highlight.border) + .padding(style.inline_code_padding) + } else if *strong || *emphasis { + span.font(Font { + weight: if *strong { + font::Weight::Bold + } else { + font::Weight::Normal + }, + style: if *emphasis { + font::Style::Italic + } else { + font::Style::Normal + }, + ..Font::default() + }) + } else { + span + }; + + let span = if let Some(link) = link.as_ref() { + span.color(style.link_color).link(link.clone()) + } else { + span + }; + + span + } + #[cfg(feature = "highlighter")] + Span::Highlight { text, color, font } => { + span(text.clone()).color_maybe(*color).font_maybe(*font) + } + } + } +} + +/// Parse the given Markdown content. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::markdown; +/// use iced::Theme; +/// +/// struct State { +/// markdown: Vec<markdown::Item>, +/// } +/// +/// enum Message { +/// LinkClicked(markdown::Url), +/// } +/// +/// impl State { +/// pub fn new() -> Self { +/// Self { +/// markdown: markdown::parse("This is some **Markdown**!").collect(), +/// } +/// } +/// +/// fn view(&self) -> Element<'_, Message> { +/// markdown::view(&self.markdown, Theme::TokyoNight) +/// .map(Message::LinkClicked) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::LinkClicked(url) => { +/// println!("The following url was clicked: {url}"); +/// } +/// } +/// } +/// } +/// ``` +pub fn parse(markdown: &str) -> impl Iterator<Item = Item> + '_ { + parse_with(State::default(), markdown) + .map(|(item, _source, _broken_links)| item) +} + +#[derive(Debug, Default)] +struct State { + leftover: String, + references: HashMap<String, String>, + images: HashSet<Url>, + #[cfg(feature = "highlighter")] + highlighter: Option<Highlighter>, +} + +#[cfg(feature = "highlighter")] +#[derive(Debug)] +struct Highlighter { + lines: Vec<(String, Vec<Span>)>, + language: String, + parser: iced_highlighter::Stream, + current: usize, +} + +#[cfg(feature = "highlighter")] +impl Highlighter { + pub fn new(language: &str) -> Self { + Self { + lines: Vec::new(), + parser: iced_highlighter::Stream::new( + &iced_highlighter::Settings { + theme: iced_highlighter::Theme::Base16Ocean, + token: language.to_owned(), + }, + ), + language: language.to_owned(), + current: 0, + } + } + + pub fn prepare(&mut self) { + self.current = 0; + } + + pub fn highlight_line(&mut self, text: &str) -> &[Span] { + match self.lines.get(self.current) { + Some(line) if line.0 == text => {} + _ => { + if self.current + 1 < self.lines.len() { + log::debug!("Resetting highlighter..."); + self.parser.reset(); + self.lines.truncate(self.current); + + for line in &self.lines { + log::debug!( + "Refeeding {n} lines", + n = self.lines.len() + ); + + let _ = self.parser.highlight_line(&line.0); + } + } + + log::trace!("Parsing: {text}", text = text.trim_end()); + + if self.current + 1 < self.lines.len() { + self.parser.commit(); + } + + let mut spans = Vec::new(); + + for (range, highlight) in self.parser.highlight_line(text) { + spans.push(Span::Highlight { + text: text[range].to_owned(), + color: highlight.color(), + font: highlight.font(), + }); + } + + if self.current + 1 == self.lines.len() { + let _ = self.lines.pop(); + } + + self.lines.push((text.to_owned(), spans)); + } + } + + self.current += 1; + + &self + .lines + .get(self.current - 1) + .expect("Line must be parsed") + .1 + } +} + +fn parse_with<'a>( + mut state: impl BorrowMut<State> + 'a, + markdown: &'a str, +) -> impl Iterator<Item = (Item, &'a str, HashSet<String>)> + 'a { + enum Scope { + List(List), + } + + struct List { + start: Option<u64>, + items: Vec<Vec<Item>>, + } + + let broken_links = Rc::new(RefCell::new(HashSet::new())); + + let mut spans = Vec::new(); + let mut code = String::new(); + let mut code_language = None; + let mut code_lines = Vec::new(); + let mut strong = false; + let mut emphasis = false; + let mut strikethrough = false; + let mut metadata = false; + let mut table = false; + let mut link = None; + let mut image = None; + let mut stack = Vec::new(); + + #[cfg(feature = "highlighter")] + let mut highlighter = None; + + let parser = pulldown_cmark::Parser::new_with_broken_link_callback( + markdown, + pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS + | pulldown_cmark::Options::ENABLE_PLUSES_DELIMITED_METADATA_BLOCKS + | pulldown_cmark::Options::ENABLE_TABLES + | pulldown_cmark::Options::ENABLE_STRIKETHROUGH, + { + let references = state.borrow().references.clone(); + let broken_links = broken_links.clone(); + + Some(move |broken_link: pulldown_cmark::BrokenLink<'_>| { + if let Some(reference) = + references.get(broken_link.reference.as_ref()) + { + Some(( + pulldown_cmark::CowStr::from(reference.to_owned()), + broken_link.reference.into_static(), + )) + } else { + let _ = RefCell::borrow_mut(&broken_links) + .insert(broken_link.reference.into_string()); + + None + } + }) + }, + ); + + let references = &mut state.borrow_mut().references; + + for reference in parser.reference_definitions().iter() { + let _ = references + .insert(reference.0.to_owned(), reference.1.dest.to_string()); + } + + let produce = move |state: &mut State, + stack: &mut Vec<Scope>, + item, + source: Range<usize>| { + if let Some(scope) = stack.last_mut() { + match scope { + Scope::List(list) => { + list.items.last_mut().expect("item context").push(item); + } + } + + None + } else { + state.leftover = markdown[source.start..].to_owned(); + + Some(( + item, + &markdown[source.start..source.end], + broken_links.take(), + )) + } + }; + + let parser = parser.into_offset_iter(); + + // We want to keep the `spans` capacity + #[allow(clippy::drain_collect)] + parser.filter_map(move |(event, source)| match event { + pulldown_cmark::Event::Start(tag) => match tag { + pulldown_cmark::Tag::Strong if !metadata && !table => { + strong = true; + None + } + pulldown_cmark::Tag::Emphasis if !metadata && !table => { + emphasis = true; + None + } + pulldown_cmark::Tag::Strikethrough if !metadata && !table => { + strikethrough = true; + None + } + pulldown_cmark::Tag::Link { dest_url, .. } + if !metadata && !table => + { + match Url::parse(&dest_url) { + Ok(url) + if url.scheme() == "http" + || url.scheme() == "https" => + { + link = Some(url); + } + _ => {} + } + + None + } + pulldown_cmark::Tag::Image { + dest_url, title, .. + } if !metadata && !table => { + image = Url::parse(&dest_url) + .ok() + .map(|url| (url, title.into_string())); + None + } + pulldown_cmark::Tag::List(first_item) if !metadata && !table => { + let prev = if spans.is_empty() { + None + } else { + produce( + state.borrow_mut(), + &mut stack, + Item::Paragraph(Text::new(spans.drain(..).collect())), + source, + ) + }; + + stack.push(Scope::List(List { + start: first_item, + items: Vec::new(), + })); + + prev + } + pulldown_cmark::Tag::Item => { + if let Some(Scope::List(list)) = stack.last_mut() { + list.items.push(Vec::new()); + } + + None + } + pulldown_cmark::Tag::CodeBlock( + pulldown_cmark::CodeBlockKind::Fenced(language), + ) if !metadata && !table => { + #[cfg(feature = "highlighter")] + { + highlighter = Some({ + let mut highlighter = state + .borrow_mut() + .highlighter + .take() + .filter(|highlighter| { + highlighter.language == language.as_ref() + }) + .unwrap_or_else(|| Highlighter::new(&language)); + + highlighter.prepare(); + + highlighter + }); + } + + code_language = + (!language.is_empty()).then(|| language.into_string()); + + let prev = if spans.is_empty() { + None + } else { + produce( + state.borrow_mut(), + &mut stack, + Item::Paragraph(Text::new(spans.drain(..).collect())), + source, + ) + }; + + prev + } + pulldown_cmark::Tag::MetadataBlock(_) => { + metadata = true; + None + } + pulldown_cmark::Tag::Table(_) => { + table = true; + None + } + _ => None, + }, + pulldown_cmark::Event::End(tag) => match tag { + pulldown_cmark::TagEnd::Heading(level) if !metadata && !table => { + produce( + state.borrow_mut(), + &mut stack, + Item::Heading(level, Text::new(spans.drain(..).collect())), + source, + ) + } + pulldown_cmark::TagEnd::Strong if !metadata && !table => { + strong = false; + None + } + pulldown_cmark::TagEnd::Emphasis if !metadata && !table => { + emphasis = false; + None + } + pulldown_cmark::TagEnd::Strikethrough if !metadata && !table => { + strikethrough = false; + None + } + pulldown_cmark::TagEnd::Link if !metadata && !table => { + link = None; + None + } + pulldown_cmark::TagEnd::Paragraph if !metadata && !table => { + if spans.is_empty() { + None + } else { + produce( + state.borrow_mut(), + &mut stack, + Item::Paragraph(Text::new(spans.drain(..).collect())), + source, + ) + } + } + pulldown_cmark::TagEnd::Item if !metadata && !table => { + if spans.is_empty() { + None + } else { + produce( + state.borrow_mut(), + &mut stack, + Item::Paragraph(Text::new(spans.drain(..).collect())), + source, + ) + } + } + pulldown_cmark::TagEnd::List(_) if !metadata && !table => { + let scope = stack.pop()?; + + let Scope::List(list) = scope; + + produce( + state.borrow_mut(), + &mut stack, + Item::List { + start: list.start, + items: list.items, + }, + source, + ) + } + pulldown_cmark::TagEnd::Image if !metadata && !table => { + let (url, title) = image.take()?; + let alt = Text::new(spans.drain(..).collect()); + + let state = state.borrow_mut(); + let _ = state.images.insert(url.clone()); + + produce( + state, + &mut stack, + Item::Image { url, title, alt }, + source, + ) + } + pulldown_cmark::TagEnd::CodeBlock if !metadata && !table => { + #[cfg(feature = "highlighter")] + { + state.borrow_mut().highlighter = highlighter.take(); + } + + produce( + state.borrow_mut(), + &mut stack, + Item::CodeBlock { + language: code_language.take(), + code: mem::take(&mut code), + lines: code_lines.drain(..).collect(), + }, + source, + ) + } + pulldown_cmark::TagEnd::MetadataBlock(_) => { + metadata = false; + None + } + pulldown_cmark::TagEnd::Table => { + table = false; + None + } + _ => None, + }, + pulldown_cmark::Event::Text(text) if !metadata && !table => { + #[cfg(feature = "highlighter")] + if let Some(highlighter) = &mut highlighter { + code.push_str(&text); + + for line in text.lines() { + code_lines.push(Text::new( + highlighter.highlight_line(line).to_vec(), + )); + } + + return None; + } + + let span = Span::Standard { + text: text.into_string(), + strong, + emphasis, + strikethrough, + link: link.clone(), + code: false, + }; + + spans.push(span); + + None + } + pulldown_cmark::Event::Code(code) if !metadata && !table => { + let span = Span::Standard { + text: code.into_string(), + strong, + emphasis, + strikethrough, + link: link.clone(), + code: true, + }; + + spans.push(span); + None + } + pulldown_cmark::Event::SoftBreak if !metadata && !table => { + spans.push(Span::Standard { + text: String::from(" "), + strikethrough, + strong, + emphasis, + link: link.clone(), + code: false, + }); + None + } + pulldown_cmark::Event::HardBreak if !metadata && !table => { + spans.push(Span::Standard { + text: String::from("\n"), + strikethrough, + strong, + emphasis, + link: link.clone(), + code: false, + }); + None + } + _ => None, + }) +} + +/// Configuration controlling Markdown rendering in [`view`]. +#[derive(Debug, Clone, Copy)] +pub struct Settings { + /// The base text size. + pub text_size: Pixels, + /// The text size of level 1 heading. + pub h1_size: Pixels, + /// The text size of level 2 heading. + pub h2_size: Pixels, + /// The text size of level 3 heading. + pub h3_size: Pixels, + /// The text size of level 4 heading. + pub h4_size: Pixels, + /// The text size of level 5 heading. + pub h5_size: Pixels, + /// The text size of level 6 heading. + pub h6_size: Pixels, + /// The text size used in code blocks. + pub code_size: Pixels, + /// The spacing to be used between elements. + pub spacing: Pixels, + /// The styling of the Markdown. + pub style: Style, +} + +impl Settings { + /// Creates new [`Settings`] with default text size and the given [`Style`]. + pub fn with_style(style: impl Into<Style>) -> Self { + Self::with_text_size(16, style) + } + + /// Creates new [`Settings`] with the given base text size in [`Pixels`]. + /// + /// Heading levels will be adjusted automatically. Specifically, + /// the first level will be twice the base size, and then every level + /// after that will be 25% smaller. + pub fn with_text_size( + text_size: impl Into<Pixels>, + style: impl Into<Style>, + ) -> Self { + let text_size = text_size.into(); + + Self { + text_size, + h1_size: text_size * 2.0, + h2_size: text_size * 1.75, + h3_size: text_size * 1.5, + h4_size: text_size * 1.25, + h5_size: text_size, + h6_size: text_size, + code_size: text_size * 0.75, + spacing: text_size * 0.875, + style: style.into(), + } + } +} + +impl From<&Theme> for Settings { + fn from(theme: &Theme) -> Self { + Self::with_style(Style::from(theme)) + } +} + +impl From<Theme> for Settings { + fn from(theme: Theme) -> Self { + Self::with_style(Style::from(theme)) + } +} + +/// The text styling of some Markdown rendering in [`view`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Style { + /// The [`Highlight`] to be applied to the background of inline code. + pub inline_code_highlight: Highlight, + /// The [`Padding`] to be applied to the background of inline code. + pub inline_code_padding: Padding, + /// The [`Color`] to be applied to inline code. + pub inline_code_color: Color, + /// The [`Color`] to be applied to links. + pub link_color: Color, +} + +impl Style { + /// Creates a new [`Style`] from the given [`theme::Palette`]. + pub fn from_palette(palette: theme::Palette) -> Self { + Self { + inline_code_padding: padding::left(1).right(1), + inline_code_highlight: Highlight { + background: color!(0x111).into(), + border: border::rounded(2), + }, + inline_code_color: Color::WHITE, + link_color: palette.primary, + } + } +} + +impl From<theme::Palette> for Style { + fn from(palette: theme::Palette) -> Self { + Self::from_palette(palette) + } +} + +impl From<&Theme> for Style { + fn from(theme: &Theme) -> Self { + Self::from_palette(theme.palette()) + } +} + +impl From<Theme> for Style { + fn from(theme: Theme) -> Self { + Self::from_palette(theme.palette()) + } +} + +/// Display a bunch of Markdown items. +/// +/// You can obtain the items with [`parse`]. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::markdown; +/// use iced::Theme; +/// +/// struct State { +/// markdown: Vec<markdown::Item>, +/// } +/// +/// enum Message { +/// LinkClicked(markdown::Url), +/// } +/// +/// impl State { +/// pub fn new() -> Self { +/// Self { +/// markdown: markdown::parse("This is some **Markdown**!").collect(), +/// } +/// } +/// +/// fn view(&self) -> Element<'_, Message> { +/// markdown::view(&self.markdown, Theme::TokyoNight) +/// .map(Message::LinkClicked) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::LinkClicked(url) => { +/// println!("The following url was clicked: {url}"); +/// } +/// } +/// } +/// } +/// ``` +pub fn view<'a, Theme, Renderer>( + items: impl IntoIterator<Item = &'a Item>, + settings: impl Into<Settings>, +) -> Element<'a, Url, Theme, Renderer> +where + Theme: Catalog + 'a, + Renderer: core::text::Renderer<Font = Font> + 'a, +{ + view_with(items, settings, &DefaultViewer) +} + +/// Runs [`view`] but with a custom [`Viewer`] to turn an [`Item`] into +/// an [`Element`]. +/// +/// This is useful if you want to customize the look of certain Markdown +/// elements. +pub fn view_with<'a, Message, Theme, Renderer>( + items: impl IntoIterator<Item = &'a Item>, + settings: impl Into<Settings>, + viewer: &impl Viewer<'a, Message, Theme, Renderer>, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: Catalog + 'a, + Renderer: core::text::Renderer<Font = Font> + 'a, +{ + let settings = settings.into(); + + let blocks = items + .into_iter() + .enumerate() + .map(|(i, item_)| item(viewer, settings, item_, i)); + + Element::new(column(blocks).spacing(settings.spacing)) +} + +/// Displays an [`Item`] using the given [`Viewer`]. +pub fn item<'a, Message, Theme, Renderer>( + viewer: &impl Viewer<'a, Message, Theme, Renderer>, + settings: Settings, + item: &'a Item, + index: usize, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: Catalog + 'a, + Renderer: core::text::Renderer<Font = Font> + 'a, +{ + match item { + Item::Image { url, title, alt } => { + viewer.image(settings, url, title, alt) + } + Item::Heading(level, text) => { + viewer.heading(settings, level, text, index) + } + Item::Paragraph(text) => viewer.paragraph(settings, text), + Item::CodeBlock { + language, + code, + lines, + } => viewer.code_block(settings, language.as_deref(), code, lines), + Item::List { start: None, items } => { + viewer.unordered_list(settings, items) + } + Item::List { + start: Some(start), + items, + } => viewer.ordered_list(settings, *start, items), + } +} + +/// Displays a heading using the default look. +pub fn heading<'a, Message, Theme, Renderer>( + settings: Settings, + level: &'a HeadingLevel, + text: &'a Text, + index: usize, + on_link_click: impl Fn(Url) -> Message + 'a, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: Catalog + 'a, + Renderer: core::text::Renderer<Font = Font> + 'a, +{ + let Settings { + h1_size, + h2_size, + h3_size, + h4_size, + h5_size, + h6_size, + text_size, + .. + } = settings; + + container( + rich_text(text.spans(settings.style)) + .on_link_click(on_link_click) + .size(match level { + pulldown_cmark::HeadingLevel::H1 => h1_size, + pulldown_cmark::HeadingLevel::H2 => h2_size, + pulldown_cmark::HeadingLevel::H3 => h3_size, + pulldown_cmark::HeadingLevel::H4 => h4_size, + pulldown_cmark::HeadingLevel::H5 => h5_size, + pulldown_cmark::HeadingLevel::H6 => h6_size, + }), + ) + .padding(padding::top(if index > 0 { + text_size / 2.0 + } else { + Pixels::ZERO + })) + .into() +} + +/// Displays a paragraph using the default look. +pub fn paragraph<'a, Message, Theme, Renderer>( + settings: Settings, + text: &Text, + on_link_click: impl Fn(Url) -> Message + 'a, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: Catalog + 'a, + Renderer: core::text::Renderer<Font = Font> + 'a, +{ + rich_text(text.spans(settings.style)) + .size(settings.text_size) + .on_link_click(on_link_click) + .into() +} + +/// Displays an unordered list using the default look and +/// calling the [`Viewer`] for each bullet point item. +pub fn unordered_list<'a, Message, Theme, Renderer>( + viewer: &impl Viewer<'a, Message, Theme, Renderer>, + settings: Settings, + items: &'a [Vec<Item>], +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: Catalog + 'a, + Renderer: core::text::Renderer<Font = Font> + 'a, +{ + column(items.iter().map(|items| { + row![ + text("•").size(settings.text_size), + view_with( + items, + Settings { + spacing: settings.spacing * 0.6, + ..settings + }, + viewer, + ) + ] + .spacing(settings.spacing) + .into() + })) + .spacing(settings.spacing * 0.75) + .padding([0.0, settings.spacing.0]) + .into() +} + +/// Displays an ordered list using the default look and +/// calling the [`Viewer`] for each numbered item. +pub fn ordered_list<'a, Message, Theme, Renderer>( + viewer: &impl Viewer<'a, Message, Theme, Renderer>, + settings: Settings, + start: u64, + items: &'a [Vec<Item>], +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: Catalog + 'a, + Renderer: core::text::Renderer<Font = Font> + 'a, +{ + column(items.iter().enumerate().map(|(i, items)| { + row![ + text!("{}.", i as u64 + start).size(settings.text_size), + view_with( + items, + Settings { + spacing: settings.spacing * 0.6, + ..settings + }, + viewer, + ) + ] + .spacing(settings.spacing) + .into() + })) + .spacing(settings.spacing * 0.75) + .padding([0.0, settings.spacing.0]) + .into() +} + +/// Displays a code block using the default look. +pub fn code_block<'a, Message, Theme, Renderer>( + settings: Settings, + lines: &'a [Text], + on_link_click: impl Fn(Url) -> Message + Clone + 'a, +) -> Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: Catalog + 'a, + Renderer: core::text::Renderer<Font = Font> + 'a, +{ + container( + scrollable( + container(column(lines.iter().map(|line| { + rich_text(line.spans(settings.style)) + .on_link_click(on_link_click.clone()) + .font(Font::MONOSPACE) + .size(settings.code_size) + .into() + }))) + .padding(settings.code_size), + ) + .direction(scrollable::Direction::Horizontal( + scrollable::Scrollbar::default() + .width(settings.code_size / 2) + .scroller_width(settings.code_size / 2), + )), + ) + .width(Length::Fill) + .padding(settings.code_size / 4) + .class(Theme::code_block()) + .into() +} + +/// A view strategy to display a Markdown [`Item`].j +pub trait Viewer<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> +where + Self: Sized + 'a, + Message: 'a, + Theme: Catalog + 'a, + Renderer: core::text::Renderer<Font = Font> + 'a, +{ + /// Produces a message when a link is clicked with the given [`Url`]. + fn on_link_click(url: Url) -> Message; + + /// Displays an image. + /// + /// By default, it will show a container with the image title. + fn image( + &self, + settings: Settings, + url: &'a Url, + title: &'a str, + alt: &Text, + ) -> Element<'a, Message, Theme, Renderer> { + let _url = url; + let _title = title; + + container( + rich_text(alt.spans(settings.style)) + .on_link_click(Self::on_link_click), + ) + .padding(settings.spacing.0) + .class(Theme::code_block()) + .into() + } + + /// Displays a heading. + /// + /// By default, it calls [`heading`]. + fn heading( + &self, + settings: Settings, + level: &'a HeadingLevel, + text: &'a Text, + index: usize, + ) -> Element<'a, Message, Theme, Renderer> { + heading(settings, level, text, index, Self::on_link_click) + } + + /// Displays a paragraph. + /// + /// By default, it calls [`paragraph`]. + fn paragraph( + &self, + settings: Settings, + text: &Text, + ) -> Element<'a, Message, Theme, Renderer> { + paragraph(settings, text, Self::on_link_click) + } + + /// Displays a code block. + /// + /// By default, it calls [`code_block`]. + fn code_block( + &self, + settings: Settings, + language: Option<&'a str>, + code: &'a str, + lines: &'a [Text], + ) -> Element<'a, Message, Theme, Renderer> { + let _language = language; + let _code = code; + + code_block(settings, lines, Self::on_link_click) + } + + /// Displays an unordered list. + /// + /// By default, it calls [`unordered_list`]. + fn unordered_list( + &self, + settings: Settings, + items: &'a [Vec<Item>], + ) -> Element<'a, Message, Theme, Renderer> { + unordered_list(self, settings, items) + } + + /// Displays an ordered list. + /// + /// By default, it calls [`ordered_list`]. + fn ordered_list( + &self, + settings: Settings, + start: u64, + items: &'a [Vec<Item>], + ) -> Element<'a, Message, Theme, Renderer> { + ordered_list(self, settings, start, items) + } +} + +#[derive(Debug, Clone, Copy)] +struct DefaultViewer; + +impl<'a, Theme, Renderer> Viewer<'a, Url, Theme, Renderer> for DefaultViewer +where + Theme: Catalog + 'a, + Renderer: core::text::Renderer<Font = Font> + 'a, +{ + fn on_link_click(url: Url) -> Url { + url + } +} + +/// The theme catalog of Markdown items. +pub trait Catalog: + container::Catalog + scrollable::Catalog + text::Catalog +{ + /// The styling class of a Markdown code block. + fn code_block<'a>() -> <Self as container::Catalog>::Class<'a>; +} + +impl Catalog for Theme { + fn code_block<'a>() -> <Self as container::Catalog>::Class<'a> { + Box::new(container::dark) + } +} diff --git a/widget/src/mouse_area.rs b/widget/src/mouse_area.rs index d7235cf6..10976c76 100644 --- a/widget/src/mouse_area.rs +++ b/widget/src/mouse_area.rs @@ -1,16 +1,13 @@ //! A container for capturing mouse events. - -use iced_renderer::core::Point; - -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::touch; -use crate::core::widget::{tree, Operation, Tree}; +use crate::core::widget::{Operation, Tree, tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget, + Clipboard, Element, Event, Layout, Length, Point, Rectangle, Shell, Size, + Vector, Widget, }; /// Emit messages on mouse events. @@ -24,12 +21,14 @@ pub struct MouseArea< content: Element<'a, Message, Theme, Renderer>, on_press: Option<Message>, on_release: Option<Message>, + on_double_click: Option<Message>, on_right_press: Option<Message>, on_right_release: Option<Message>, on_middle_press: Option<Message>, on_middle_release: Option<Message>, + on_scroll: Option<Box<dyn Fn(mouse::ScrollDelta) -> Message + 'a>>, on_enter: Option<Message>, - on_move: Option<Box<dyn Fn(Point) -> Message>>, + on_move: Option<Box<dyn Fn(Point) -> Message + 'a>>, on_exit: Option<Message>, interaction: Option<mouse::Interaction>, } @@ -49,6 +48,22 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { self } + /// The message to emit on a double click. + /// + /// If you use this with [`on_press`]/[`on_release`], those + /// event will be emit as normal. + /// + /// The events stream will be: on_press -> on_release -> on_press + /// -> on_double_click -> on_release -> on_press ... + /// + /// [`on_press`]: Self::on_press + /// [`on_release`]: Self::on_release + #[must_use] + pub fn on_double_click(mut self, message: Message) -> Self { + self.on_double_click = Some(message); + self + } + /// The message to emit on a right button press. #[must_use] pub fn on_right_press(mut self, message: Message) -> Self { @@ -77,6 +92,16 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { self } + /// The message to emit when scroll wheel is used + #[must_use] + pub fn on_scroll( + mut self, + on_scroll: impl Fn(mouse::ScrollDelta) -> Message + 'a, + ) -> Self { + self.on_scroll = Some(Box::new(on_scroll)); + self + } + /// The message to emit when the mouse enters the area. #[must_use] pub fn on_enter(mut self, message: Message) -> Self { @@ -86,11 +111,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { /// The message to emit when the mouse moves in the area. #[must_use] - pub fn on_move<F>(mut self, build_message: F) -> Self - where - F: Fn(Point) -> Message + 'static, - { - self.on_move = Some(Box::new(build_message)); + pub fn on_move(mut self, on_move: impl Fn(Point) -> Message + 'a) -> Self { + self.on_move = Some(Box::new(on_move)); self } @@ -113,6 +135,9 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { #[derive(Default)] struct State { is_hovered: bool, + bounds: Rectangle, + cursor_position: Option<Point>, + previous_click: Option<mouse::Click>, } impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { @@ -124,10 +149,12 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { content: content.into(), on_press: None, on_release: None, + on_double_click: None, on_right_press: None, on_right_release: None, on_middle_press: None, on_middle_release: None, + on_scroll: None, on_enter: None, on_move: None, on_exit: None, @@ -136,8 +163,8 @@ impl<'a, Message, Theme, Renderer> MouseArea<'a, Message, Theme, Renderer> { } } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for MouseArea<'a, Message, Theme, Renderer> +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for MouseArea<'_, Message, Theme, Renderer> where Renderer: renderer::Renderer, Message: Clone, @@ -178,7 +205,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation, ) { self.content.as_widget().operate( &mut tree.children[0], @@ -188,31 +215,33 @@ where ); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - if let event::Status::Captured = self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().update( &mut tree.children[0], - event.clone(), + event, layout, cursor, renderer, clipboard, shell, viewport, - ) { - return event::Status::Captured; + ); + + if shell.is_event_captured() { + return; } - update(self, tree, event, layout, cursor, shell) + update(self, tree, event, layout, cursor, shell); } fn mouse_interaction( @@ -297,18 +326,22 @@ where fn update<Message: Clone, Theme, Renderer>( widget: &mut MouseArea<'_, Message, Theme, Renderer>, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, -) -> event::Status { - if let Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) = event - { - let state: &mut State = tree.state.downcast_mut(); +) { + let state: &mut State = tree.state.downcast_mut(); + let cursor_position = cursor.position(); + let bounds = layout.bounds(); + + if state.cursor_position != cursor_position || state.bounds != bounds { let was_hovered = state.is_hovered; + state.is_hovered = cursor.is_over(layout.bounds()); + state.cursor_position = cursor_position; + state.bounds = bounds; match ( widget.on_enter.as_ref(), @@ -331,71 +364,71 @@ fn update<Message: Clone, Theme, Renderer>( } if !cursor.is_over(layout.bounds()) { - return event::Status::Ignored; + return; } - if let Some(message) = widget.on_press.as_ref() { - if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) = event - { - shell.publish(message.clone()); + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(message) = widget.on_press.as_ref() { + shell.publish(message.clone()); + shell.capture_event(); + } - return event::Status::Captured; + if let Some(position) = cursor_position { + if let Some(message) = widget.on_double_click.as_ref() { + let new_click = mouse::Click::new( + position, + mouse::Button::Left, + state.previous_click, + ); + + if new_click.kind() == mouse::click::Kind::Double { + shell.publish(message.clone()); + } + + state.previous_click = Some(new_click); + + // Even if this is not a double click, but the press is nevertheless + // processed by us and should not be popup to parent widgets. + shell.capture_event(); + } + } } - } - - if let Some(message) = widget.on_release.as_ref() { - if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) = event - { - shell.publish(message.clone()); - - return event::Status::Captured; + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { + if let Some(message) = widget.on_release.as_ref() { + shell.publish(message.clone()); + } } - } - - if let Some(message) = widget.on_right_press.as_ref() { - if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) = - event - { - shell.publish(message.clone()); - - return event::Status::Captured; + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => { + if let Some(message) = widget.on_right_press.as_ref() { + shell.publish(message.clone()); + shell.capture_event(); + } } - } - - if let Some(message) = widget.on_right_release.as_ref() { - if let Event::Mouse(mouse::Event::ButtonReleased( - mouse::Button::Right, - )) = event - { - shell.publish(message.clone()); - - return event::Status::Captured; + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) => { + if let Some(message) = widget.on_right_release.as_ref() { + shell.publish(message.clone()); + } } - } - - if let Some(message) = widget.on_middle_press.as_ref() { - if let Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Middle, - )) = event - { - shell.publish(message.clone()); - - return event::Status::Captured; + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) => { + if let Some(message) = widget.on_middle_press.as_ref() { + shell.publish(message.clone()); + shell.capture_event(); + } } - } - - if let Some(message) = widget.on_middle_release.as_ref() { - if let Event::Mouse(mouse::Event::ButtonReleased( - mouse::Button::Middle, - )) = event - { - shell.publish(message.clone()); - - return event::Status::Captured; + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Middle)) => { + if let Some(message) = widget.on_middle_release.as_ref() { + shell.publish(message.clone()); + } } + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + if let Some(on_scroll) = widget.on_scroll.as_ref() { + shell.publish(on_scroll(*delta)); + shell.capture_event(); + } + } + _ => {} } - - event::Status::Ignored } diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 98efe305..9d0539ff 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -1,15 +1,16 @@ //! Build and show dropdown menus. use crate::core::alignment; -use crate::core::event::{self, Event}; +use crate::core::border::{self, Border}; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::text::{self, Text}; use crate::core::touch; -use crate::core::widget::Tree; +use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ - Background, Border, Clipboard, Color, Length, Padding, Pixels, Point, + Background, Clipboard, Color, Event, Length, Padding, Pixels, Point, Rectangle, Size, Theme, Vector, }; use crate::core::{Element, Shell, Widget}; @@ -200,21 +201,18 @@ where class, } = menu; - let list = Scrollable::with_direction( - List { - options, - hovered_option, - on_selected, - on_option_hovered, - font, - text_size, - text_line_height, - text_shaping, - padding, - class, - }, - scrollable::Direction::default(), - ); + let list = Scrollable::new(List { + options, + hovered_option, + on_selected, + on_option_hovered, + font, + text_size, + text_line_height, + text_shaping, + padding, + class, + }); state.tree.diff(&list as &dyn Widget<_, _, _>); @@ -229,9 +227,8 @@ where } } -impl<'a, 'b, Message, Theme, Renderer> - crate::core::Overlay<Message, Theme, Renderer> - for Overlay<'a, 'b, Message, Theme, Renderer> +impl<Message, Theme, Renderer> crate::core::Overlay<Message, Theme, Renderer> + for Overlay<'_, '_, Message, Theme, Renderer> where Theme: Catalog, Renderer: text::Renderer, @@ -264,21 +261,21 @@ 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 { + ) { let bounds = layout.bounds(); - self.list.on_event( + self.list.update( self.state, event, layout, cursor, renderer, clipboard, shell, &bounds, - ) + ); } fn mouse_interaction( @@ -336,13 +333,25 @@ where class: &'a <Theme as Catalog>::Class<'b>, } -impl<'a, 'b, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for List<'a, 'b, T, Message, Theme, Renderer> +struct ListState { + is_hovered: Option<bool>, +} + +impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for List<'_, '_, T, Message, Theme, Renderer> where T: Clone + ToString, Theme: Catalog, Renderer: text::Renderer, { + fn tag(&self) -> tree::Tag { + tree::Tag::of::<Option<bool>>() + } + + fn state(&self) -> tree::State { + tree::State::new(ListState { is_hovered: None }) + } + fn size(&self) -> Size<Length> { Size { width: Length::Fill, @@ -376,24 +385,24 @@ where layout::Node::new(size) } - fn on_event( + fn update( &mut self, - _state: &mut Tree, - event: Event, + tree: &mut Tree, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { if cursor.is_over(layout.bounds()) { if let Some(index) = *self.hovered_option { if let Some(option) = self.options.get(index) { shell.publish((self.on_selected)(option.clone())); - return event::Status::Captured; + shell.capture_event(); } } } @@ -413,14 +422,18 @@ where let new_hovered_option = (cursor_position.y / option_height) as usize; - if let Some(on_option_hovered) = self.on_option_hovered { - if *self.hovered_option != Some(new_hovered_option) { - if let Some(option) = - self.options.get(new_hovered_option) + if *self.hovered_option != Some(new_hovered_option) { + if let Some(option) = + self.options.get(new_hovered_option) + { + if let Some(on_option_hovered) = + self.on_option_hovered { shell .publish(on_option_hovered(option.clone())); } + + shell.request_redraw(); } } @@ -445,7 +458,7 @@ where if let Some(index) = *self.hovered_option { if let Some(option) = self.options.get(index) { shell.publish((self.on_selected)(option.clone())); - return event::Status::Captured; + shell.capture_event(); } } } @@ -453,7 +466,15 @@ where _ => {} } - event::Status::Ignored + let state = tree.state.downcast_mut::<ListState>(); + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + state.is_hovered = Some(cursor.is_over(layout.bounds())); + } else if state.is_hovered.is_some_and(|is_hovered| { + is_hovered != cursor.is_over(layout.bounds()) + }) { + shell.request_redraw(); + } } fn mouse_interaction( @@ -517,7 +538,7 @@ where width: bounds.width - style.border.width * 2.0, ..bounds }, - border: Border::rounded(style.border.radius), + border: border::rounded(style.border.radius), ..renderer::Quad::default() }, style.selected_background, @@ -534,6 +555,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: self.text_shaping, + wrapping: text::Wrapping::default(), }, Point::new(bounds.x + self.padding.left, bounds.center_y()), if is_selected { @@ -563,7 +585,7 @@ where } /// The appearance of a [`Menu`]. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`Background`] of the menu. pub background: Background, diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index acfa9d44..db93c724 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -1,15 +1,62 @@ -//! Let your users split regions of your application and organize layout dynamically. +//! Pane grids let your users split regions of your application and organize layout dynamically. //! //! ![Pane grid - Iced](https://iced.rs/examples/pane_grid.gif) //! +//! This distribution of space is common in tiling window managers (like +//! [`awesome`](https://awesomewm.org/), [`i3`](https://i3wm.org/), or even +//! [`tmux`](https://github.com/tmux/tmux)). +//! +//! A [`PaneGrid`] supports: +//! +//! * Vertical and horizontal splits +//! * Tracking of the last active pane +//! * Mouse-based resizing +//! * Drag and drop to reorganize panes +//! * Hotkey support +//! * Configurable modifier keys +//! * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.) +//! //! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::{pane_grid, text}; +//! +//! struct State { +//! panes: pane_grid::State<Pane>, +//! } +//! +//! enum Pane { +//! SomePane, +//! AnotherKindOfPane, +//! } +//! +//! enum Message { +//! PaneDragged(pane_grid::DragEvent), +//! PaneResized(pane_grid::ResizeEvent), +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! pane_grid(&state.panes, |pane, state, is_maximized| { +//! pane_grid::Content::new(match state { +//! Pane::SomePane => text("This is some pane"), +//! Pane::AnotherKindOfPane => text("This is another kind of pane"), +//! }) +//! }) +//! .on_drag(Message::PaneDragged) +//! .on_resize(10, Message::PaneResized) +//! .into() +//! } +//! ``` //! The [`pane_grid` example] showcases how to use a [`PaneGrid`] with resizing, //! drag and drop, and hotkey support. //! -//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.12/examples/pane_grid +//! [`pane_grid` example]: https://github.com/iced-rs/iced/tree/0.13/examples/pane_grid mod axis; mod configuration; mod content; +mod controls; mod direction; mod draggable; mod node; @@ -22,6 +69,7 @@ pub mod state; pub use axis::Axis; pub use configuration::Configuration; pub use content::Content; +pub use controls::Controls; pub use direction::Direction; pub use draggable::Draggable; pub use node::Node; @@ -31,7 +79,6 @@ pub use state::State; pub use title_bar::TitleBar; use crate::container; -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay::{self, Group}; @@ -39,8 +86,9 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ - self, Background, Border, Clipboard, Color, Element, Layout, Length, + self, Background, Border, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; @@ -66,14 +114,18 @@ const THICKNESS_RATIO: f32 = 25.0; /// * Configurable modifier keys /// * [`State`] API to perform actions programmatically (`split`, `swap`, `resize`, etc.) /// -/// ## Example -/// +/// # Example /// ```no_run -/// # use iced_widget::{pane_grid, text}; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # -/// # type PaneGrid<'a, Message> = iced_widget::PaneGrid<'a, Message>; -/// # -/// enum PaneState { +/// use iced::widget::{pane_grid, text}; +/// +/// struct State { +/// panes: pane_grid::State<Pane>, +/// } +/// +/// enum Pane { /// SomePane, /// AnotherKindOfPane, /// } @@ -83,17 +135,17 @@ const THICKNESS_RATIO: f32 = 25.0; /// PaneResized(pane_grid::ResizeEvent), /// } /// -/// let (mut state, _) = pane_grid::State::new(PaneState::SomePane); -/// -/// let pane_grid = -/// PaneGrid::new(&state, |pane, state, is_maximized| { +/// fn view(state: &State) -> Element<'_, Message> { +/// pane_grid(&state.panes, |pane, state, is_maximized| { /// pane_grid::Content::new(match state { -/// PaneState::SomePane => text("This is some pane"), -/// PaneState::AnotherKindOfPane => text("This is another kind of pane"), +/// Pane::SomePane => text("This is some pane"), +/// Pane::AnotherKindOfPane => text("This is another kind of pane"), /// }) /// }) /// .on_drag(Message::PaneDragged) -/// .on_resize(10, Message::PaneResized); +/// .on_resize(10, Message::PaneResized) +/// .into() +/// } /// ``` #[allow(missing_debug_implementations)] pub struct PaneGrid< @@ -105,7 +157,9 @@ pub struct PaneGrid< Theme: Catalog, Renderer: core::Renderer, { - contents: Contents<'a, Content<'a, Message, Theme, Renderer>>, + internal: &'a state::Internal, + panes: Vec<Pane>, + contents: Vec<Content<'a, Message, Theme, Renderer>>, width: Length, height: Length, spacing: f32, @@ -113,6 +167,7 @@ pub struct PaneGrid< on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, on_resize: Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, class: <Theme as Catalog>::Class<'a>, + last_mouse_interaction: Option<mouse::Interaction>, } impl<'a, Message, Theme, Renderer> PaneGrid<'a, Message, Theme, Renderer> @@ -128,29 +183,19 @@ where state: &'a State<T>, view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Theme, Renderer>, ) -> Self { - let contents = if let Some((pane, pane_state)) = - state.maximized.and_then(|pane| { - state.panes.get(&pane).map(|pane_state| (pane, pane_state)) - }) { - Contents::Maximized( - pane, - view(pane, pane_state, true), - Node::Pane(pane), - ) - } else { - Contents::All( - state - .panes - .iter() - .map(|(pane, pane_state)| { - (*pane, view(*pane, pane_state, false)) - }) - .collect(), - &state.internal, - ) - }; + let panes = state.panes.keys().copied().collect(); + let contents = state + .panes + .iter() + .map(|(pane, pane_state)| match state.maximized() { + Some(p) if *pane == p => view(*pane, pane_state, true), + _ => view(*pane, pane_state, false), + }) + .collect(); Self { + internal: &state.internal, + panes, contents, width: Length::Fill, height: Length::Fill, @@ -159,6 +204,7 @@ where on_drag: None, on_resize: None, class: <Theme as Catalog>::default(), + last_mouse_interaction: None, } } @@ -196,7 +242,9 @@ where where F: 'a + Fn(DragEvent) -> Message, { - self.on_drag = Some(Box::new(f)); + if self.internal.maximized().is_none() { + self.on_drag = Some(Box::new(f)); + } self } @@ -213,7 +261,9 @@ where where F: 'a + Fn(ResizeEvent) -> Message, { - self.on_resize = Some((leeway.into().0, Box::new(f))); + if self.internal.maximized().is_none() { + self.on_resize = Some((leeway.into().0, Box::new(f))); + } self } @@ -239,46 +289,114 @@ where } fn drag_enabled(&self) -> bool { - (!self.contents.is_maximized()) + self.internal + .maximized() + .is_none() .then(|| self.on_drag.is_some()) .unwrap_or_default() } + + fn grid_interaction( + &self, + action: &state::Action, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) -> Option<mouse::Interaction> { + if action.picked_pane().is_some() { + return Some(mouse::Interaction::Grabbing); + } + + let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway); + let node = self.internal.layout(); + + let resize_axis = + action.picked_split().map(|(_, axis)| axis).or_else(|| { + resize_leeway.and_then(|leeway| { + let cursor_position = cursor.position()?; + let bounds = layout.bounds(); + + let splits = + node.split_regions(self.spacing, bounds.size()); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + hovered_split( + splits.iter(), + self.spacing + leeway, + relative_cursor, + ) + .map(|(_, axis, _)| axis) + }) + }); + + if let Some(resize_axis) = resize_axis { + return Some(match resize_axis { + Axis::Horizontal => mouse::Interaction::ResizingVertically, + Axis::Vertical => mouse::Interaction::ResizingHorizontally, + }); + } + + None + } } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for PaneGrid<'a, Message, Theme, Renderer> +#[derive(Default)] +struct Memory { + action: state::Action, + order: Vec<Pane>, +} + +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for PaneGrid<'_, Message, Theme, Renderer> where Theme: Catalog, Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { - tree::Tag::of::<state::Action>() + tree::Tag::of::<Memory>() } fn state(&self) -> tree::State { - tree::State::new(state::Action::Idle) + tree::State::new(Memory::default()) } fn children(&self) -> Vec<Tree> { - self.contents - .iter() - .map(|(_, content)| content.state()) - .collect() + self.contents.iter().map(Content::state).collect() } fn diff(&self, tree: &mut Tree) { - match &self.contents { - Contents::All(contents, _) => tree.diff_children_custom( - contents, - |state, (_, content)| content.diff(state), - |(_, content)| content.state(), - ), - Contents::Maximized(_, content, _) => tree.diff_children_custom( - &[content], - |state, content| content.diff(state), - |content| content.state(), - ), - } + let Memory { order, .. } = tree.state.downcast_ref(); + + // `Pane` always increments and is iterated by Ord so new + // states are always added at the end. We can simply remove + // states which no longer exist and `diff_children` will + // diff the remaining values in the correct order and + // add new states at the end + + let mut i = 0; + let mut j = 0; + tree.children.retain(|_| { + let retain = self.panes.get(i) == order.get(j); + + if retain { + i += 1; + } + j += 1; + + retain + }); + + tree.diff_children_custom( + &self.contents, + |state, content| content.diff(state), + Content::state, + ); + + let Memory { order, .. } = tree.state.downcast_mut(); + order.clone_from(&self.panes); } fn size(&self) -> Size<Length> { @@ -295,14 +413,23 @@ where limits: &layout::Limits, ) -> layout::Node { let size = limits.resolve(self.width, self.height, Size::ZERO); - let node = self.contents.layout(); - let regions = node.pane_regions(self.spacing, size); + let regions = self.internal.layout().pane_regions(self.spacing, size); let children = self - .contents + .panes .iter() + .copied() + .zip(&self.contents) .zip(tree.children.iter_mut()) .filter_map(|((pane, content), tree)| { + if self + .internal + .maximized() + .is_some_and(|maximized| maximized != pane) + { + return Some(layout::Node::new(Size::ZERO)); + } + let region = regions.get(&pane)?; let size = Size::new(region.width, region.height); @@ -324,34 +451,39 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { - self.contents + self.panes .iter() + .copied() + .zip(&self.contents) .zip(&mut tree.children) .zip(layout.children()) - .for_each(|(((_pane, content), state), layout)| { + .filter(|(((pane, _), _), _)| { + self.internal + .maximized() + .is_none_or(|maximized| *pane == maximized) + }) + .for_each(|(((_, content), state), layout)| { content.operate(state, layout, renderer, 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<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - let mut event_status = event::Status::Ignored; - - let action = tree.state.downcast_mut::<state::Action>(); - let node = self.contents.layout(); + ) { + let Memory { action, .. } = tree.state.downcast_mut(); + let node = self.internal.layout(); let on_drag = if self.drag_enabled() { &self.on_drag @@ -359,13 +491,36 @@ where &None }; + let picked_pane = action.picked_pane().map(|(pane, _)| pane); + + for (((pane, content), tree), layout) in self + .panes + .iter() + .copied() + .zip(&mut self.contents) + .zip(&mut tree.children) + .zip(layout.children()) + .filter(|(((pane, _), _), _)| { + self.internal + .maximized() + .is_none_or(|maximized| *pane == maximized) + }) + { + let is_picked = picked_pane == Some(pane); + + content.update( + tree, event, layout, cursor, renderer, clipboard, shell, + viewport, is_picked, + ); + } + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { let bounds = layout.bounds(); if let Some(cursor_position) = cursor.position_over(bounds) { - event_status = event::Status::Captured; + shell.capture_event(); match &self.on_resize { Some((leeway, _)) => { @@ -396,7 +551,10 @@ where layout, cursor_position, shell, - self.contents.iter(), + self.panes + .iter() + .copied() + .zip(&self.contents), &self.on_click, on_drag, ); @@ -408,7 +566,7 @@ where layout, cursor_position, shell, - self.contents.iter(), + self.panes.iter().copied().zip(&self.contents), &self.on_click, on_drag, ); @@ -434,8 +592,10 @@ where } } else { let dropped_region = self - .contents + .panes .iter() + .copied() + .zip(&self.contents) .zip(layout.children()) .find_map(|(target, layout)| { layout_region( @@ -461,13 +621,13 @@ where }; shell.publish(on_drag(event)); + } else { + shell.publish(on_drag(DragEvent::Canceled { + pane, + })); } } } - - event_status = event::Status::Captured; - } else if action.picked_split().is_some() { - event_status = event::Status::Captured; } *action = state::Action::Idle; @@ -509,37 +669,48 @@ where ratio, })); - event_status = event::Status::Captured; + shell.capture_event(); } } + } else if action.picked_pane().is_some() { + shell.request_redraw(); } } } _ => {} } - let picked_pane = action.picked_pane().map(|(pane, _)| pane); + if shell.redraw_request() != window::RedrawRequest::NextFrame { + let interaction = self + .grid_interaction(action, layout, cursor) + .or_else(|| { + self.panes + .iter() + .zip(&self.contents) + .zip(layout.children()) + .filter(|((pane, _content), _layout)| { + self.internal + .maximized() + .is_none_or(|maximized| **pane == maximized) + }) + .find_map(|((_pane, content), layout)| { + content.grid_interaction( + layout, + cursor, + on_drag.is_some(), + ) + }) + }) + .unwrap_or(mouse::Interaction::None); - self.contents - .iter_mut() - .zip(&mut tree.children) - .zip(layout.children()) - .map(|(((pane, content), tree), layout)| { - let is_picked = picked_pane == Some(pane); - - content.on_event( - tree, - event.clone(), - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - is_picked, - ) - }) - .fold(event_status, event::Status::merge) + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.last_mouse_interaction = Some(interaction); + } else if self.last_mouse_interaction.is_some_and( + |last_mouse_interaction| last_mouse_interaction != interaction, + ) { + shell.request_redraw(); + } + } } fn mouse_interaction( @@ -550,50 +721,26 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - let action = tree.state.downcast_ref::<state::Action>(); + let Memory { action, .. } = tree.state.downcast_ref(); - if action.picked_pane().is_some() { - return mouse::Interaction::Grabbing; + if let Some(grid_interaction) = + self.grid_interaction(action, layout, cursor) + { + return grid_interaction; } - let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway); - let node = self.contents.layout(); - - let resize_axis = - action.picked_split().map(|(_, axis)| axis).or_else(|| { - resize_leeway.and_then(|leeway| { - let cursor_position = cursor.position()?; - let bounds = layout.bounds(); - - let splits = - node.split_regions(self.spacing, bounds.size()); - - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - hovered_split( - splits.iter(), - self.spacing + leeway, - relative_cursor, - ) - .map(|(_, axis, _)| axis) - }) - }); - - if let Some(resize_axis) = resize_axis { - return match resize_axis { - Axis::Horizontal => mouse::Interaction::ResizingVertically, - Axis::Vertical => mouse::Interaction::ResizingHorizontally, - }; - } - - self.contents + self.panes .iter() + .copied() + .zip(&self.contents) .zip(&tree.children) .zip(layout.children()) - .map(|(((_pane, content), tree), layout)| { + .filter(|(((pane, _), _), _)| { + self.internal + .maximized() + .is_none_or(|maximized| *pane == maximized) + }) + .map(|(((_, content), tree), layout)| { content.mouse_interaction( tree, layout, @@ -617,16 +764,10 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - let action = tree.state.downcast_ref::<state::Action>(); - let node = self.contents.layout(); + let Memory { action, .. } = tree.state.downcast_ref(); + let node = self.internal.layout(); let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway); - let contents = self - .contents - .iter() - .zip(&tree.children) - .map(|((pane, content), tree)| (pane, (content, tree))); - let picked_pane = action.picked_pane().filter(|(_, origin)| { cursor .position() @@ -695,8 +836,18 @@ where let style = Catalog::style(theme, &self.class); - for ((id, (content, tree)), pane_layout) in - contents.zip(layout.children()) + for (((id, content), tree), pane_layout) in self + .panes + .iter() + .copied() + .zip(&self.contents) + .zip(&tree.children) + .zip(layout.children()) + .filter(|(((pane, _), _), _)| { + self.internal + .maximized() + .is_none_or(|maximized| maximized == *pane) + }) { match picked_pane { Some((dragging, origin)) if id == dragging => { @@ -829,13 +980,23 @@ where layout: Layout<'_>, renderer: &Renderer, translation: Vector, - ) -> Option<overlay::Element<'_, Message, Theme, Renderer>> { + ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { let children = self - .contents - .iter_mut() + .panes + .iter() + .copied() + .zip(&mut self.contents) .zip(&mut tree.children) .zip(layout.children()) - .filter_map(|(((_, content), state), layout)| { + .filter_map(|(((pane, content), state), layout)| { + if self + .internal + .maximized() + .is_some_and(|maximized| maximized != pane) + { + return None; + } + content.overlay(state, layout, renderer, translation) }) .collect::<Vec<_>>(); @@ -1084,52 +1245,6 @@ fn hovered_split<'a>( }) } -/// The visible contents of the [`PaneGrid`] -#[derive(Debug)] -pub enum Contents<'a, T> { - /// All panes are visible - All(Vec<(Pane, T)>, &'a state::Internal), - /// A maximized pane is visible - Maximized(Pane, T, Node), -} - -impl<'a, T> Contents<'a, T> { - /// Returns the layout [`Node`] of the [`Contents`] - pub fn layout(&self) -> &Node { - match self { - Contents::All(_, state) => state.layout(), - Contents::Maximized(_, _, layout) => layout, - } - } - - /// Returns an iterator over the values of the [`Contents`] - pub fn iter(&self) -> Box<dyn Iterator<Item = (Pane, &T)> + '_> { - match self { - Contents::All(contents, _) => Box::new( - contents.iter().map(|(pane, content)| (*pane, content)), - ), - Contents::Maximized(pane, content, _) => { - Box::new(std::iter::once((*pane, content))) - } - } - } - - fn iter_mut(&mut self) -> Box<dyn Iterator<Item = (Pane, &mut T)> + '_> { - match self { - Contents::All(contents, _) => Box::new( - contents.iter_mut().map(|(pane, content)| (*pane, content)), - ), - Contents::Maximized(pane, content, _) => { - Box::new(std::iter::once((*pane, content))) - } - } - } - - fn is_maximized(&self) -> bool { - matches!(self, Self::Maximized(..)) - } -} - /// The appearance of a [`PaneGrid`]. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index 30ad52ca..4d63dd18 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -1,12 +1,12 @@ use crate::container; -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{self, Tree}; use crate::core::{ - self, Clipboard, Element, Layout, Point, Rectangle, Shell, Size, Vector, + self, Clipboard, Element, Event, Layout, Point, Rectangle, Shell, Size, + Vector, }; use crate::pane_grid::{Draggable, TitleBar}; @@ -73,7 +73,7 @@ where } } -impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer> +impl<Message, Theme, Renderer> Content<'_, Message, Theme, Renderer> where Theme: container::Catalog, Renderer: core::Renderer, @@ -214,7 +214,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation, ) { let body_layout = if let Some(title_bar) = &self.title_bar { let mut children = layout.children(); @@ -239,10 +239,10 @@ where ); } - pub(crate) fn on_event( + pub(crate) fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, @@ -250,15 +250,13 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, is_picked: bool, - ) -> event::Status { - let mut event_status = event::Status::Ignored; - + ) { let body_layout = if let Some(title_bar) = &mut self.title_bar { let mut children = layout.children(); - event_status = title_bar.on_event( + title_bar.update( &mut tree.children[1], - event.clone(), + event, children.next().unwrap(), cursor, renderer, @@ -272,10 +270,8 @@ where layout }; - let body_status = if is_picked { - event::Status::Ignored - } else { - self.body.as_widget_mut().on_event( + if !is_picked { + self.body.as_widget_mut().update( &mut tree.children[0], event, body_layout, @@ -284,10 +280,33 @@ where clipboard, shell, viewport, - ) - }; + ); + } + } - event_status.merge(body_status) + pub(crate) fn grid_interaction( + &self, + layout: Layout<'_>, + cursor: mouse::Cursor, + drag_enabled: bool, + ) -> Option<mouse::Interaction> { + let title_bar = self.title_bar.as_ref()?; + + let mut children = layout.children(); + let title_bar_layout = children.next().unwrap(); + + let is_over_pick_area = cursor + .position() + .map(|cursor_position| { + title_bar.is_over_pick_area(title_bar_layout, cursor_position) + }) + .unwrap_or_default(); + + if is_over_pick_area && drag_enabled { + return Some(mouse::Interaction::Grab); + } + + None } pub(crate) fn mouse_interaction( @@ -382,8 +401,8 @@ where } } -impl<'a, Message, Theme, Renderer> Draggable - for &Content<'a, Message, Theme, Renderer> +impl<Message, Theme, Renderer> Draggable + for &Content<'_, Message, Theme, Renderer> where Theme: container::Catalog, Renderer: core::Renderer, diff --git a/widget/src/pane_grid/controls.rs b/widget/src/pane_grid/controls.rs new file mode 100644 index 00000000..13b57acb --- /dev/null +++ b/widget/src/pane_grid/controls.rs @@ -0,0 +1,59 @@ +use crate::container; +use crate::core::{self, Element}; + +/// The controls of a [`Pane`]. +/// +/// [`Pane`]: super::Pane +#[allow(missing_debug_implementations)] +pub struct Controls< + 'a, + Message, + Theme = crate::Theme, + Renderer = crate::Renderer, +> where + Theme: container::Catalog, + Renderer: core::Renderer, +{ + pub(super) full: Element<'a, Message, Theme, Renderer>, + pub(super) compact: Option<Element<'a, Message, Theme, Renderer>>, +} + +impl<'a, Message, Theme, Renderer> Controls<'a, Message, Theme, Renderer> +where + Theme: container::Catalog, + Renderer: core::Renderer, +{ + /// Creates a new [`Controls`] with the given content. + pub fn new( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self { + full: content.into(), + compact: None, + } + } + + /// Creates a new [`Controls`] with a full and compact variant. + /// If there is not enough room to show the full variant without overlap, + /// then the compact variant will be shown instead. + pub fn dynamic( + full: impl Into<Element<'a, Message, Theme, Renderer>>, + compact: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self { + full: full.into(), + compact: Some(compact.into()), + } + } +} + +impl<'a, Message, Theme, Renderer> From<Element<'a, Message, Theme, Renderer>> + for Controls<'a, Message, Theme, Renderer> +where + Theme: container::Catalog, + Renderer: core::Renderer, +{ + fn from(value: Element<'a, Message, Theme, Renderer>) -> Self { + Self::new(value) + } +} diff --git a/widget/src/pane_grid/state.rs b/widget/src/pane_grid/state.rs index c20c3b9c..2f8a64ea 100644 --- a/widget/src/pane_grid/state.rs +++ b/widget/src/pane_grid/state.rs @@ -6,7 +6,8 @@ use crate::pane_grid::{ Axis, Configuration, Direction, Edge, Node, Pane, Region, Split, Target, }; -use rustc_hash::FxHashMap; +use std::borrow::Cow; +use std::collections::BTreeMap; /// The state of a [`PaneGrid`]. /// @@ -25,17 +26,12 @@ pub struct State<T> { /// The panes of the [`PaneGrid`]. /// /// [`PaneGrid`]: super::PaneGrid - pub panes: FxHashMap<Pane, T>, + pub panes: BTreeMap<Pane, T>, /// The internal state of the [`PaneGrid`]. /// /// [`PaneGrid`]: super::PaneGrid pub internal: Internal, - - /// The maximized [`Pane`] of the [`PaneGrid`]. - /// - /// [`PaneGrid`]: super::PaneGrid - pub(super) maximized: Option<Pane>, } impl<T> State<T> { @@ -52,16 +48,12 @@ impl<T> State<T> { /// Creates a new [`State`] with the given [`Configuration`]. pub fn with_configuration(config: impl Into<Configuration<T>>) -> Self { - let mut panes = FxHashMap::default(); + let mut panes = BTreeMap::default(); let internal = Internal::from_configuration(&mut panes, config.into(), 0); - State { - panes, - internal, - maximized: None, - } + State { panes, internal } } /// Returns the total amount of panes in the [`State`]. @@ -214,7 +206,7 @@ impl<T> State<T> { } let _ = self.panes.insert(new_pane, state); - let _ = self.maximized.take(); + let _ = self.internal.maximized.take(); Some((new_pane, new_split)) } @@ -228,8 +220,11 @@ impl<T> State<T> { ) { if let Some((state, _)) = self.close(pane) { if let Some((new_pane, _)) = self.split(axis, target, state) { + // Ensure new node corresponds to original closed `Pane` for state continuity + self.relabel(new_pane, pane); + if swap { - self.swap(target, new_pane); + self.swap(target, pane); } } } @@ -259,13 +254,27 @@ impl<T> State<T> { &mut self, axis: Axis, pane: Pane, - swap: bool, + inverse: bool, ) { if let Some((state, _)) = self.close(pane) { - let _ = self.split_node(axis, None, state, swap); + if let Some((new_pane, _)) = + self.split_node(axis, None, state, inverse) + { + // Ensure new node corresponds to original closed `Pane` for state continuity + self.relabel(new_pane, pane); + } } } + fn relabel(&mut self, target: Pane, label: Pane) { + self.swap(target, label); + + let _ = self + .panes + .remove(&target) + .and_then(|state| self.panes.insert(label, state)); + } + /// Swaps the position of the provided panes in the [`State`]. /// /// If you want to swap panes on drag and drop in your [`PaneGrid`], you @@ -303,8 +312,8 @@ impl<T> State<T> { /// Closes the given [`Pane`] and returns its internal state and its closest /// sibling, if it exists. pub fn close(&mut self, pane: Pane) -> Option<(T, Pane)> { - if self.maximized == Some(pane) { - let _ = self.maximized.take(); + if self.internal.maximized == Some(pane) { + let _ = self.internal.maximized.take(); } if let Some(sibling) = self.internal.layout.remove(pane) { @@ -319,7 +328,7 @@ impl<T> State<T> { /// /// [`PaneGrid`]: super::PaneGrid pub fn maximize(&mut self, pane: Pane) { - self.maximized = Some(pane); + self.internal.maximized = Some(pane); } /// Restore the currently maximized [`Pane`] to it's normal size. All panes @@ -327,14 +336,14 @@ impl<T> State<T> { /// /// [`PaneGrid`]: super::PaneGrid pub fn restore(&mut self) { - let _ = self.maximized.take(); + let _ = self.internal.maximized.take(); } /// Returns the maximized [`Pane`] of the [`PaneGrid`]. /// /// [`PaneGrid`]: super::PaneGrid pub fn maximized(&self) -> Option<Pane> { - self.maximized + self.internal.maximized } } @@ -345,6 +354,7 @@ impl<T> State<T> { pub struct Internal { layout: Node, last_id: usize, + maximized: Option<Pane>, } impl Internal { @@ -353,7 +363,7 @@ impl Internal { /// /// [`PaneGrid`]: super::PaneGrid pub fn from_configuration<T>( - panes: &mut FxHashMap<Pane, T>, + panes: &mut BTreeMap<Pane, T>, content: Configuration<T>, next_id: usize, ) -> Self { @@ -390,18 +400,34 @@ impl Internal { } }; - Self { layout, last_id } + Self { + layout, + last_id, + maximized: None, + } + } + + pub(super) fn layout(&self) -> Cow<'_, Node> { + match self.maximized { + Some(pane) => Cow::Owned(Node::Pane(pane)), + None => Cow::Borrowed(&self.layout), + } + } + + pub(super) fn maximized(&self) -> Option<Pane> { + self.maximized } } /// The current action of a [`PaneGrid`]. /// /// [`PaneGrid`]: super::PaneGrid -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Default)] pub enum Action { /// The [`PaneGrid`] is idle. /// /// [`PaneGrid`]: super::PaneGrid + #[default] Idle, /// A [`Pane`] in the [`PaneGrid`] is being dragged. /// @@ -440,10 +466,3 @@ impl Action { } } } - -impl Internal { - /// The layout [`Node`] of the [`Internal`] state - pub fn layout(&self) -> &Node { - &self.layout - } -} diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index c2eeebb7..611c3d67 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -1,14 +1,14 @@ use crate::container; -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{self, Tree}; use crate::core::{ - self, Clipboard, Element, Layout, Padding, Point, Rectangle, Shell, Size, - Vector, + self, Clipboard, Element, Event, Layout, Padding, Point, Rectangle, Shell, + Size, Vector, }; +use crate::pane_grid::controls::Controls; /// The title bar of a [`Pane`]. /// @@ -24,7 +24,7 @@ pub struct TitleBar< Renderer: core::Renderer, { content: Element<'a, Message, Theme, Renderer>, - controls: Option<Element<'a, Message, Theme, Renderer>>, + controls: Option<Controls<'a, Message, Theme, Renderer>>, padding: Padding, always_show_controls: bool, class: Theme::Class<'a>, @@ -51,7 +51,7 @@ where /// Sets the controls of the [`TitleBar`]. pub fn controls( mut self, - controls: impl Into<Element<'a, Message, Theme, Renderer>>, + controls: impl Into<Controls<'a, Message, Theme, Renderer>>, ) -> Self { self.controls = Some(controls.into()); self @@ -98,16 +98,28 @@ where } } -impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer> +impl<Message, Theme, Renderer> TitleBar<'_, Message, Theme, Renderer> where Theme: container::Catalog, Renderer: core::Renderer, { pub(super) fn state(&self) -> Tree { - let children = if let Some(controls) = self.controls.as_ref() { - vec![Tree::new(&self.content), Tree::new(controls)] - } else { - vec![Tree::new(&self.content), Tree::empty()] + let children = match self.controls.as_ref() { + Some(controls) => match controls.compact.as_ref() { + Some(compact) => vec![ + Tree::new(&self.content), + Tree::new(&controls.full), + Tree::new(compact), + ], + None => vec![ + Tree::new(&self.content), + Tree::new(&controls.full), + Tree::empty(), + ], + }, + None => { + vec![Tree::new(&self.content), Tree::empty(), Tree::empty()] + } }; Tree { @@ -117,9 +129,13 @@ where } pub(super) fn diff(&self, tree: &mut Tree) { - if tree.children.len() == 2 { + if tree.children.len() == 3 { if let Some(controls) = self.controls.as_ref() { - tree.children[1].diff(controls); + if let Some(compact) = controls.compact.as_ref() { + tree.children[2].diff(compact); + } + + tree.children[1].diff(&controls.full); } tree.children[0].diff(&self.content); @@ -164,18 +180,42 @@ where if title_layout.bounds().width + controls_layout.bounds().width > padded.bounds().width { - show_title = false; - } + if let Some(compact) = controls.compact.as_ref() { + let compact_layout = children.next().unwrap(); - controls.as_widget().draw( - &tree.children[1], - renderer, - theme, - &inherited_style, - controls_layout, - cursor, - viewport, - ); + compact.as_widget().draw( + &tree.children[2], + renderer, + theme, + &inherited_style, + compact_layout, + cursor, + viewport, + ); + } else { + show_title = false; + + controls.full.as_widget().draw( + &tree.children[1], + renderer, + theme, + &inherited_style, + controls_layout, + cursor, + viewport, + ); + } + } else { + controls.full.as_widget().draw( + &tree.children[1], + renderer, + theme, + &inherited_style, + controls_layout, + cursor, + viewport, + ); + } } } @@ -207,13 +247,20 @@ where let mut children = padded.children(); let title_layout = children.next().unwrap(); - if self.controls.is_some() { + if let Some(controls) = self.controls.as_ref() { let controls_layout = children.next().unwrap(); if title_layout.bounds().width + controls_layout.bounds().width > padded.bounds().width { - !controls_layout.bounds().contains(cursor_position) + if controls.compact.is_some() { + let compact_layout = children.next().unwrap(); + + !compact_layout.bounds().contains(cursor_position) + && !title_layout.bounds().contains(cursor_position) + } else { + !controls_layout.bounds().contains(cursor_position) + } } else { !controls_layout.bounds().contains(cursor_position) && !title_layout.bounds().contains(cursor_position) @@ -244,25 +291,73 @@ where let title_size = title_layout.size(); let node = if let Some(controls) = &self.controls { - let controls_layout = controls.as_widget().layout( + let controls_layout = controls.full.as_widget().layout( &mut tree.children[1], renderer, &layout::Limits::new(Size::ZERO, max_size), ); - let controls_size = controls_layout.size(); - let space_before_controls = max_size.width - controls_size.width; + if title_layout.bounds().width + controls_layout.bounds().width + > max_size.width + { + if let Some(compact) = controls.compact.as_ref() { + let compact_layout = compact.as_widget().layout( + &mut tree.children[2], + renderer, + &layout::Limits::new(Size::ZERO, max_size), + ); - let height = title_size.height.max(controls_size.height); + let compact_size = compact_layout.size(); + let space_before_controls = + max_size.width - compact_size.width; - layout::Node::with_children( - Size::new(max_size.width, height), - vec![ - title_layout, - controls_layout - .move_to(Point::new(space_before_controls, 0.0)), - ], - ) + let height = title_size.height.max(compact_size.height); + + layout::Node::with_children( + Size::new(max_size.width, height), + vec![ + title_layout, + controls_layout, + compact_layout.move_to(Point::new( + space_before_controls, + 0.0, + )), + ], + ) + } else { + let controls_size = controls_layout.size(); + let space_before_controls = + max_size.width - controls_size.width; + + let height = title_size.height.max(controls_size.height); + + layout::Node::with_children( + Size::new(max_size.width, height), + vec![ + title_layout, + controls_layout.move_to(Point::new( + space_before_controls, + 0.0, + )), + ], + ) + } + } else { + let controls_size = controls_layout.size(); + let space_before_controls = + max_size.width - controls_size.width; + + let height = title_size.height.max(controls_size.height); + + layout::Node::with_children( + Size::new(max_size.width, height), + vec![ + title_layout, + controls_layout + .move_to(Point::new(space_before_controls, 0.0)), + ], + ) + } } else { layout::Node::with_children( Size::new(max_size.width, title_size.height), @@ -278,7 +373,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn widget::Operation<Message>, + operation: &mut dyn widget::Operation, ) { let mut children = layout.children(); let padded = children.next().unwrap(); @@ -293,15 +388,33 @@ where if title_layout.bounds().width + controls_layout.bounds().width > padded.bounds().width { - show_title = false; - } + if let Some(compact) = controls.compact.as_ref() { + let compact_layout = children.next().unwrap(); - controls.as_widget().operate( - &mut tree.children[1], - controls_layout, - renderer, - operation, - ); + compact.as_widget().operate( + &mut tree.children[2], + compact_layout, + renderer, + operation, + ); + } else { + show_title = false; + + controls.full.as_widget().operate( + &mut tree.children[1], + controls_layout, + renderer, + operation, + ); + } + } else { + controls.full.as_widget().operate( + &mut tree.children[1], + controls_layout, + renderer, + operation, + ); + } }; if show_title { @@ -314,17 +427,17 @@ where } } - pub(crate) fn on_event( + pub(crate) fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let mut children = layout.children(); let padded = children.next().unwrap(); @@ -332,30 +445,55 @@ where let title_layout = children.next().unwrap(); let mut show_title = true; - let control_status = if let Some(controls) = &mut self.controls { + if let Some(controls) = &mut self.controls { let controls_layout = children.next().unwrap(); + if title_layout.bounds().width + controls_layout.bounds().width > padded.bounds().width { - show_title = false; + if let Some(compact) = controls.compact.as_mut() { + let compact_layout = children.next().unwrap(); + + compact.as_widget_mut().update( + &mut tree.children[2], + event, + compact_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } else { + show_title = false; + + controls.full.as_widget_mut().update( + &mut tree.children[1], + event, + controls_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + } else { + controls.full.as_widget_mut().update( + &mut tree.children[1], + event, + controls_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); } + } - controls.as_widget_mut().on_event( - &mut tree.children[1], - event.clone(), - controls_layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) - } else { - event::Status::Ignored - }; - - let title_status = if show_title { - self.content.as_widget_mut().on_event( + if show_title { + self.content.as_widget_mut().update( &mut tree.children[0], event, title_layout, @@ -364,12 +502,8 @@ where clipboard, shell, viewport, - ) - } else { - event::Status::Ignored - }; - - control_status.merge(title_status) + ); + } } pub(crate) fn mouse_interaction( @@ -396,18 +530,33 @@ where if let Some(controls) = &self.controls { let controls_layout = children.next().unwrap(); - let controls_interaction = controls.as_widget().mouse_interaction( - &tree.children[1], - controls_layout, - cursor, - viewport, - renderer, - ); + let controls_interaction = + controls.full.as_widget().mouse_interaction( + &tree.children[1], + controls_layout, + cursor, + viewport, + renderer, + ); if title_layout.bounds().width + controls_layout.bounds().width > padded.bounds().width { - controls_interaction + if let Some(compact) = controls.compact.as_ref() { + let compact_layout = children.next().unwrap(); + let compact_interaction = + compact.as_widget().mouse_interaction( + &tree.children[2], + compact_layout, + cursor, + viewport, + renderer, + ); + + compact_interaction.max(title_interaction) + } else { + controls_interaction + } } else { controls_interaction.max(title_interaction) } @@ -444,12 +593,36 @@ where controls.as_mut().and_then(|controls| { let controls_layout = children.next()?; - controls.as_widget_mut().overlay( - controls_state, - controls_layout, - renderer, - translation, - ) + if title_layout.bounds().width + + controls_layout.bounds().width + > padded.bounds().width + { + if let Some(compact) = controls.compact.as_mut() { + let compact_state = states.next().unwrap(); + let compact_layout = children.next()?; + + compact.as_widget_mut().overlay( + compact_state, + compact_layout, + renderer, + translation, + ) + } else { + controls.full.as_widget_mut().overlay( + controls_state, + controls_layout, + renderer, + translation, + ) + } + } else { + controls.full.as_widget_mut().overlay( + controls_state, + controls_layout, + renderer, + translation, + ) + } }) }) } diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index edccfdaa..b751fcc3 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -1,17 +1,79 @@ -//! Display a dropdown list of selectable values. +//! Pick lists display a dropdown list of selectable options. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::pick_list; +//! +//! struct State { +//! favorite: Option<Fruit>, +//! } +//! +//! #[derive(Debug, Clone, Copy, PartialEq, Eq)] +//! enum Fruit { +//! Apple, +//! Orange, +//! Strawberry, +//! Tomato, +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! FruitSelected(Fruit), +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! let fruits = [ +//! Fruit::Apple, +//! Fruit::Orange, +//! Fruit::Strawberry, +//! Fruit::Tomato, +//! ]; +//! +//! pick_list( +//! fruits, +//! state.favorite, +//! Message::FruitSelected, +//! ) +//! .placeholder("Select your favorite fruit...") +//! .into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::FruitSelected(fruit) => { +//! state.favorite = Some(fruit); +//! } +//! } +//! } +//! +//! impl std::fmt::Display for Fruit { +//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +//! f.write_str(match self { +//! Self::Apple => "Apple", +//! Self::Orange => "Orange", +//! Self::Strawberry => "Strawberry", +//! Self::Tomato => "Tomato", +//! }) +//! } +//! } +//! ``` use crate::core::alignment; -use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; -use crate::core::text::{self, Paragraph as _, Text}; +use crate::core::text::paragraph; +use crate::core::text::{self, Text}; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ - Background, Border, Clipboard, Color, Element, Layout, Length, Padding, - Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, + Background, Border, Clipboard, Color, Element, Event, Layout, Length, + Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::overlay::menu::{self, Menu}; @@ -19,6 +81,67 @@ use std::borrow::Borrow; use std::f32; /// A widget for selecting a single value from a list of options. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::pick_list; +/// +/// struct State { +/// favorite: Option<Fruit>, +/// } +/// +/// #[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// enum Fruit { +/// Apple, +/// Orange, +/// Strawberry, +/// Tomato, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// FruitSelected(Fruit), +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// let fruits = [ +/// Fruit::Apple, +/// Fruit::Orange, +/// Fruit::Strawberry, +/// Fruit::Tomato, +/// ]; +/// +/// pick_list( +/// fruits, +/// state.favorite, +/// Message::FruitSelected, +/// ) +/// .placeholder("Select your favorite fruit...") +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::FruitSelected(fruit) => { +/// state.favorite = Some(fruit); +/// } +/// } +/// } +/// +/// impl std::fmt::Display for Fruit { +/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// f.write_str(match self { +/// Self::Apple => "Apple", +/// Self::Orange => "Orange", +/// Self::Strawberry => "Strawberry", +/// Self::Tomato => "Tomato", +/// }) +/// } +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct PickList< 'a, @@ -50,6 +173,7 @@ pub struct PickList< handle: Handle<Renderer::Font>, class: <Theme as Catalog>::Class<'a>, menu_class: <Theme as menu::Catalog>::Class<'a>, + last_status: Option<Status>, } impl<'a, T, L, V, Message, Theme, Renderer> @@ -80,11 +204,12 @@ where padding: crate::button::DEFAULT_PADDING, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::default(), font: None, handle: Handle::default(), class: <Theme as Catalog>::default(), menu_class: <Theme as Catalog>::default_menu(), + last_status: None, } } @@ -161,6 +286,19 @@ where self } + /// Sets the style of the [`Menu`]. + #[must_use] + pub fn menu_style( + mut self, + style: impl Fn(&Theme) -> menu::Style + 'a, + ) -> Self + where + <Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>, + { + self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into(); + self + } + /// Sets the style class of the [`PickList`]. #[cfg(feature = "advanced")] #[must_use] @@ -171,6 +309,17 @@ where self.class = class.into(); self } + + /// Sets the style class of the [`Menu`]. + #[cfg(feature = "advanced")] + #[must_use] + pub fn menu_class( + mut self, + class: impl Into<<Theme as menu::Catalog>::Class<'a>>, + ) -> Self { + self.menu_class = class.into(); + self + } } impl<'a, T, L, V, Message, Theme, Renderer> Widget<Message, Theme, Renderer> @@ -225,6 +374,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: self.text_shaping, + wrapping: text::Wrapping::default(), }; for (option, paragraph) in options.iter().zip(state.options.iter_mut()) @@ -277,23 +427,22 @@ where layout::Node::new(size) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { + let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - let state = - tree.state.downcast_mut::<State<Renderer::Paragraph>>(); - if state.is_open { // Event wasn't processed by overlay, so cursor was clicked either outside its // bounds or on the drop-down, either way we close the overlay. @@ -303,7 +452,7 @@ where shell.publish(on_close.clone()); } - event::Status::Captured + shell.capture_event(); } else if cursor.is_over(layout.bounds()) { let selected = self.selected.as_ref().map(Borrow::borrow); @@ -318,17 +467,12 @@ where shell.publish(on_open.clone()); } - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Mouse(mouse::Event::WheelScrolled { delta: mouse::ScrollDelta::Lines { y, .. }, }) => { - let state = - tree.state.downcast_mut::<State<Renderer::Paragraph>>(); - if state.keyboard_modifiers.command() && cursor.is_over(layout.bounds()) && !state.is_open @@ -345,13 +489,13 @@ where let options = self.options.borrow(); let selected = self.selected.as_ref().map(Borrow::borrow); - let next_option = if y < 0.0 { + let next_option = if *y < 0.0 { if let Some(selected) = selected { find_next(selected, options.iter()) } else { options.first() } - } else if y > 0.0 { + } else if *y > 0.0 { if let Some(selected) = selected { find_next(selected, options.iter().rev()) } else { @@ -365,20 +509,34 @@ where shell.publish((self.on_select)(next_option.clone())); } - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - let state = - tree.state.downcast_mut::<State<Renderer::Paragraph>>(); - - state.keyboard_modifiers = modifiers; - - event::Status::Ignored + state.keyboard_modifiers = *modifiers; } - _ => event::Status::Ignored, + _ => {} + }; + + let status = { + let is_hovered = cursor.is_over(layout.bounds()); + + if state.is_open { + Status::Opened { is_hovered } + } else if is_hovered { + Status::Hovered + } else { + Status::Active + } + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.last_status = Some(status); + } else if self + .last_status + .is_some_and(|last_status| last_status != status) + { + shell.request_redraw(); } } @@ -407,7 +565,7 @@ where theme: &Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor: mouse::Cursor, + _cursor: mouse::Cursor, viewport: &Rectangle, ) { let font = self.font.unwrap_or_else(|| renderer.default_font()); @@ -415,18 +573,12 @@ where let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - let is_selected = selected.is_some(); - let status = if state.is_open { - Status::Opened - } else if is_mouse_over { - Status::Hovered - } else { - Status::Active - }; - - let style = Catalog::style(theme, &self.class, status); + let style = Catalog::style( + theme, + &self.class, + self.last_status.unwrap_or(Status::Active), + ); renderer.fill_quad( renderer::Quad { @@ -490,6 +642,7 @@ where horizontal_alignment: alignment::Horizontal::Right, vertical_alignment: alignment::Vertical::Center, shaping, + wrapping: text::Wrapping::default(), }, Point::new( bounds.x + bounds.width - self.padding.right, @@ -519,9 +672,10 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: self.text_shaping, + wrapping: text::Wrapping::default(), }, Point::new(bounds.x + self.padding.left, bounds.center_y()), - if is_selected { + if selected.is_some() { style.text_color } else { style.placeholder_color @@ -598,8 +752,8 @@ struct State<P: text::Paragraph> { keyboard_modifiers: keyboard::Modifiers, is_open: bool, hovered_option: Option<usize>, - options: Vec<P>, - placeholder: P, + options: Vec<paragraph::Plain<P>>, + placeholder: paragraph::Plain<P>, } impl<P: text::Paragraph> State<P> { @@ -611,7 +765,7 @@ impl<P: text::Paragraph> State<P> { is_open: bool::default(), hovered_option: Option::default(), options: Vec::new(), - placeholder: P::default(), + placeholder: paragraph::Plain::default(), } } } @@ -674,11 +828,14 @@ pub enum Status { /// The [`PickList`] is being hovered. Hovered, /// The [`PickList`] is open. - Opened, + Opened { + /// Whether the [`PickList`] is hovered, while open. + is_hovered: bool, + }, } /// The appearance of a pick list. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The text [`Color`] of the pick list. pub text_color: Color, @@ -748,7 +905,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { match status { Status::Active => active, - Status::Hovered | Status::Opened => Style { + Status::Hovered | Status::Opened { .. } => Style { border: Border { color: palette.primary.strong.color, ..active.border diff --git a/widget/src/pin.rs b/widget/src/pin.rs new file mode 100644 index 00000000..afa29398 --- /dev/null +++ b/widget/src/pin.rs @@ -0,0 +1,270 @@ +//! A pin widget positions a widget at some fixed coordinates inside its boundaries. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::Length::Fill; } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::pin; +//! use iced::Fill; +//! +//! enum Message { +//! // ... +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! pin("This text is displayed at coordinates (50, 50)!") +//! .x(50) +//! .y(50) +//! .into() +//! } +//! ``` +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::widget; +use crate::core::{ + self, Clipboard, Element, Event, Layout, Length, Pixels, Point, Rectangle, + Shell, Size, Vector, Widget, +}; + +/// A widget that positions its contents at some fixed coordinates inside of its boundaries. +/// +/// By default, a [`Pin`] widget will try to fill its parent. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::core::Length::Fill; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::pin; +/// use iced::Fill; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// pin("This text is displayed at coordinates (50, 50)!") +/// .x(50) +/// .y(50) +/// .into() +/// } +/// ``` +#[allow(missing_debug_implementations)] +pub struct Pin<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> +where + Renderer: core::Renderer, +{ + content: Element<'a, Message, Theme, Renderer>, + width: Length, + height: Length, + position: Point, +} + +impl<'a, Message, Theme, Renderer> Pin<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + /// Creates a [`Pin`] widget with the given content. + pub fn new( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self { + content: content.into(), + width: Length::Fill, + height: Length::Fill, + position: Point::ORIGIN, + } + } + + /// Sets the width of the [`Pin`]. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Pin`]. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Sets the position of the [`Pin`]; where the pinned widget will be displayed. + pub fn position(mut self, position: impl Into<Point>) -> Self { + self.position = position.into(); + self + } + + /// Sets the X coordinate of the [`Pin`]. + pub fn x(mut self, x: impl Into<Pixels>) -> Self { + self.position.x = x.into().0; + self + } + + /// Sets the Y coordinate of the [`Pin`]. + pub fn y(mut self, y: impl Into<Pixels>) -> Self { + self.position.y = y.into().0; + self + } +} + +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Pin<'_, Message, Theme, Renderer> +where + Renderer: core::Renderer, +{ + fn tag(&self) -> widget::tree::Tag { + self.content.as_widget().tag() + } + + fn state(&self) -> widget::tree::State { + self.content.as_widget().state() + } + + fn children(&self) -> Vec<widget::Tree> { + self.content.as_widget().children() + } + + fn diff(&self, tree: &mut widget::Tree) { + self.content.as_widget().diff(tree); + } + + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut widget::Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width).height(self.height); + + let available = + limits.max() - Size::new(self.position.x, self.position.y); + + let node = self + .content + .as_widget() + .layout(tree, renderer, &layout::Limits::new(Size::ZERO, available)) + .move_to(self.position); + + let size = limits.resolve(self.width, self.height, node.size()); + layout::Node::with_children(size, vec![node]) + } + + fn operate( + &self, + tree: &mut widget::Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.content.as_widget().operate( + tree, + layout.children().next().unwrap(), + renderer, + operation, + ); + } + + fn update( + &mut self, + tree: &mut widget::Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + self.content.as_widget_mut().update( + tree, + event, + layout.children().next().unwrap(), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + + fn mouse_interaction( + &self, + tree: &widget::Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + tree, + layout.children().next().unwrap(), + cursor, + viewport, + renderer, + ) + } + + fn draw( + &self, + tree: &widget::Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + + if let Some(clipped_viewport) = bounds.intersection(viewport) { + self.content.as_widget().draw( + tree, + renderer, + theme, + style, + layout.children().next().unwrap(), + cursor, + &clipped_viewport, + ); + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut widget::Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { + self.content.as_widget_mut().overlay( + tree, + layout.children().next().unwrap(), + renderer, + translation, + ) + } +} + +impl<'a, Message, Theme, Renderer> From<Pin<'a, Message, Theme, Renderer>> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + fn from( + pin: Pin<'a, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(pin) + } +} diff --git a/widget/src/pop.rs b/widget/src/pop.rs new file mode 100644 index 00000000..5add1525 --- /dev/null +++ b/widget/src/pop.rs @@ -0,0 +1,319 @@ +//! Generate messages when content pops in and out of view. +use crate::core::layout; +use crate::core::mouse; +use crate::core::overlay; +use crate::core::renderer; +use crate::core::text; +use crate::core::time::{Duration, Instant}; +use crate::core::widget; +use crate::core::widget::tree::{self, Tree}; +use crate::core::window; +use crate::core::{ + self, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Shell, + Size, Vector, Widget, +}; + +/// A widget that can generate messages when its content pops in and out of view. +/// +/// It can even notify you with anticipation at a given distance! +#[allow(missing_debug_implementations)] +pub struct Pop<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> { + content: Element<'a, Message, Theme, Renderer>, + key: Option<text::Fragment<'a>>, + on_show: Option<Box<dyn Fn(Size) -> Message + 'a>>, + on_resize: Option<Box<dyn Fn(Size) -> Message + 'a>>, + on_hide: Option<Message>, + anticipate: Pixels, + delay: Duration, +} + +impl<'a, Message, Theme, Renderer> Pop<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer, + Message: Clone, +{ + /// Creates a new [`Pop`] widget with the given content. + pub fn new( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self { + Self { + content: content.into(), + key: None, + on_show: None, + on_resize: None, + on_hide: None, + anticipate: Pixels::ZERO, + delay: Duration::ZERO, + } + } + + /// Sets the message to be produced when the content pops into view. + /// + /// The closure will receive the [`Size`] of the content in that moment. + pub fn on_show(mut self, on_show: impl Fn(Size) -> Message + 'a) -> Self { + self.on_show = Some(Box::new(on_show)); + self + } + + /// Sets the message to be produced when the content changes [`Size`] once its in view. + /// + /// The closure will receive the new [`Size`] of the content. + pub fn on_resize( + mut self, + on_resize: impl Fn(Size) -> Message + 'a, + ) -> Self { + self.on_resize = Some(Box::new(on_resize)); + self + } + + /// Sets the message to be produced when the content pops out of view. + pub fn on_hide(mut self, on_hide: Message) -> Self { + self.on_hide = Some(on_hide); + self + } + + /// Sets the key of the [`Pop`] widget, for continuity. + /// + /// If the key changes, the [`Pop`] widget will trigger again. + pub fn key(mut self, key: impl text::IntoFragment<'a>) -> Self { + self.key = Some(key.into_fragment()); + self + } + + /// Sets the distance in [`Pixels`] to use in anticipation of the + /// content popping into view. + /// + /// This can be quite useful to lazily load items in a long scrollable + /// behind the scenes before the user can notice it! + pub fn anticipate(mut self, distance: impl Into<Pixels>) -> Self { + self.anticipate = distance.into(); + self + } + + /// Sets the amount of time to wait before firing an [`on_show`] or + /// [`on_hide`] event; after the content is shown or hidden. + /// + /// When combined with [`key`], this can be useful to debounce key changes. + /// + /// [`on_show`]: Self::on_show + /// [`on_hide`]: Self::on_hide + /// [`key`]: Self::key + pub fn delay(mut self, delay: impl Into<Duration>) -> Self { + self.delay = delay.into(); + self + } +} + +#[derive(Debug, Clone, Default)] +struct State { + has_popped_in: bool, + should_notify_at: Option<(bool, Instant)>, + last_size: Option<Size>, + last_key: Option<String>, +} + +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Pop<'_, Message, Theme, Renderer> +where + Message: Clone, + Renderer: core::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn children(&self) -> Vec<Tree> { + vec![Tree::new(&self.content)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&[&self.content]); + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + if let Event::Window(window::Event::RedrawRequested(now)) = &event { + let state = tree.state.downcast_mut::<State>(); + + if state.has_popped_in + && state.last_key.as_deref() != self.key.as_deref() + { + state.has_popped_in = false; + state.should_notify_at = None; + state.last_key = + self.key.as_ref().cloned().map(text::Fragment::into_owned); + } + + let bounds = layout.bounds(); + let top_left_distance = viewport.distance(bounds.position()); + + let bottom_right_distance = viewport + .distance(bounds.position() + Vector::from(bounds.size())); + + let distance = top_left_distance.min(bottom_right_distance); + + if state.has_popped_in { + if distance <= self.anticipate.0 { + if let Some(on_resize) = &self.on_resize { + let size = bounds.size(); + + if Some(size) != state.last_size { + state.last_size = Some(size); + shell.publish(on_resize(size)); + } + } + } else if self.on_hide.is_some() { + state.has_popped_in = false; + state.should_notify_at = Some((false, *now + self.delay)); + } + } else if self.on_show.is_some() && distance <= self.anticipate.0 { + let size = bounds.size(); + + state.has_popped_in = true; + state.should_notify_at = Some((true, *now + self.delay)); + state.last_size = Some(size); + } + + match &state.should_notify_at { + Some((has_popped_in, at)) if at <= now => { + if *has_popped_in { + if let Some(on_show) = &self.on_show { + shell.publish(on_show(layout.bounds().size())); + } + } else if let Some(on_hide) = &self.on_hide { + shell.publish(on_hide.clone()); + } + + state.should_notify_at = None; + } + Some((_, at)) => { + shell.request_redraw_at(*at); + } + None => {} + } + } + + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + + fn size(&self) -> Size<Length> { + self.content.as_widget().size() + } + + fn size_hint(&self) -> Size<Length> { + self.content.as_widget().size_hint() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + self.content + .as_widget() + .layout(&mut tree.children[0], renderer, limits) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: layout::Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ); + } + + fn operate( + &self, + tree: &mut Tree, + layout: core::Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.content.as_widget().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ); + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: core::Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: core::Layout<'_>, + renderer: &Renderer, + translation: core::Vector, + ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + translation, + ) + } +} + +impl<'a, Message, Theme, Renderer> From<Pop<'a, Message, Theme, Renderer>> + for Element<'a, Message, Theme, Renderer> +where + Renderer: core::Renderer + 'a, + Theme: 'a, + Message: Clone + 'a, +{ + fn from(pop: Pop<'a, Message, Theme, Renderer>) -> Self { + Element::new(pop) + } +} diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index e7821b43..a9b4eb65 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -1,10 +1,31 @@ -//! Provide progress feedback to your users. +//! Progress bars visualize the progression of an extended computer operation, such as a download, file transfer, or installation. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::progress_bar; +//! +//! struct State { +//! progress: f32, +//! } +//! +//! enum Message { +//! // ... +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! progress_bar(0.0..=100.0, state.progress).into() +//! } +//! ``` +use crate::core::border::{self, Border}; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - self, Background, Border, Element, Layout, Length, Rectangle, Size, Theme, + self, Background, Color, Element, Layout, Length, Rectangle, Size, Theme, Widget, }; @@ -14,14 +35,23 @@ use std::ops::RangeInclusive; /// /// # Example /// ```no_run -/// # type ProgressBar<'a> = iced_widget::ProgressBar<'a>; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # -/// let value = 50.0; +/// use iced::widget::progress_bar; /// -/// ProgressBar::new(0.0..=100.0, value); +/// struct State { +/// progress: f32, +/// } +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// progress_bar(0.0..=100.0, state.progress).into() +/// } /// ``` -/// -/// ![Progress bar drawn with `iced_wgpu`](https://user-images.githubusercontent.com/18618951/71662391-a316c200-2d51-11ea-9cef-52758cab85e3.png) #[allow(missing_debug_implementations)] pub struct ProgressBar<'a, Theme = crate::Theme> where @@ -29,8 +59,9 @@ where { range: RangeInclusive<f32>, value: f32, - width: Length, - height: Option<Length>, + length: Length, + girth: Length, + is_vertical: bool, class: Theme::Class<'a>, } @@ -38,8 +69,8 @@ impl<'a, Theme> ProgressBar<'a, Theme> where Theme: Catalog, { - /// The default height of a [`ProgressBar`]. - pub const DEFAULT_HEIGHT: f32 = 30.0; + /// The default girth of a [`ProgressBar`]. + pub const DEFAULT_GIRTH: f32 = 30.0; /// Creates a new [`ProgressBar`]. /// @@ -50,21 +81,30 @@ where ProgressBar { value: value.clamp(*range.start(), *range.end()), range, - width: Length::Fill, - height: None, + length: Length::Fill, + girth: Length::from(Self::DEFAULT_GIRTH), + is_vertical: false, class: Theme::default(), } } /// Sets the width of the [`ProgressBar`]. - pub fn width(mut self, width: impl Into<Length>) -> Self { - self.width = width.into(); + pub fn length(mut self, length: impl Into<Length>) -> Self { + self.length = length.into(); self } /// Sets the height of the [`ProgressBar`]. - pub fn height(mut self, height: impl Into<Length>) -> Self { - self.height = Some(height.into()); + pub fn girth(mut self, girth: impl Into<Length>) -> Self { + self.girth = girth.into(); + self + } + + /// Turns the [`ProgressBar`] into a vertical [`ProgressBar`]. + /// + /// By default, a [`ProgressBar`] is horizontal. + pub fn vertical(mut self) -> Self { + self.is_vertical = true; self } @@ -85,18 +125,34 @@ where self.class = class.into(); self } + + fn width(&self) -> Length { + if self.is_vertical { + self.girth + } else { + self.length + } + } + + fn height(&self) -> Length { + if self.is_vertical { + self.length + } else { + self.girth + } + } } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for ProgressBar<'a, Theme> +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for ProgressBar<'_, Theme> where Theme: Catalog, Renderer: core::Renderer, { fn size(&self) -> Size<Length> { Size { - width: self.width, - height: self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)), + width: self.width(), + height: self.height(), } } @@ -106,11 +162,7 @@ where _renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout::atomic( - limits, - self.width, - self.height.unwrap_or(Length::Fixed(Self::DEFAULT_HEIGHT)), - ) + layout::atomic(limits, self.width(), self.height()) } fn draw( @@ -126,11 +178,16 @@ where let bounds = layout.bounds(); let (range_start, range_end) = self.range.clone().into_inner(); - let active_progress_width = if range_start >= range_end { + let length = if self.is_vertical { + bounds.height + } else { + bounds.width + }; + + let active_progress_length = if range_start >= range_end { 0.0 } else { - bounds.width * (self.value - range_start) - / (range_end - range_start) + length * (self.value - range_start) / (range_end - range_start) }; let style = theme.style(&self.class); @@ -144,14 +201,27 @@ where style.background, ); - if active_progress_width > 0.0 { + if active_progress_length > 0.0 { + let bounds = if self.is_vertical { + Rectangle { + y: bounds.y + bounds.height - active_progress_length, + height: active_progress_length, + ..bounds + } + } else { + Rectangle { + width: active_progress_length, + ..bounds + } + }; + renderer.fill_quad( renderer::Quad { - bounds: Rectangle { - width: active_progress_width, - ..bounds + bounds, + border: Border { + color: Color::TRANSPARENT, + ..style.border }, - border: Border::rounded(style.border.radius), ..renderer::Quad::default() }, style.bar, @@ -175,7 +245,7 @@ where } /// The appearance of a progress bar. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`Background`] of the progress bar. pub background: Background, @@ -218,10 +288,7 @@ impl Catalog for Theme { pub fn primary(theme: &Theme) -> Style { let palette = theme.extended_palette(); - styled( - palette.background.strong.color, - palette.primary.strong.color, - ) + styled(palette.background.strong.color, palette.primary.base.color) } /// The secondary style of a [`ProgressBar`]. @@ -241,6 +308,13 @@ pub fn success(theme: &Theme) -> Style { styled(palette.background.strong.color, palette.success.base.color) } +/// The warning style of a [`ProgressBar`]. +pub fn warning(theme: &Theme) -> Style { + let palette = theme.extended_palette(); + + styled(palette.background.strong.color, palette.warning.base.color) +} + /// The danger style of a [`ProgressBar`]. pub fn danger(theme: &Theme) -> Style { let palette = theme.extended_palette(); @@ -255,6 +329,6 @@ fn styled( Style { background: background.into(), bar: bar.into(), - border: Border::rounded(2), + border: border::rounded(2), } } diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index e064aada..07bb0c50 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -1,30 +1,72 @@ -//! Encode and display information in a QR code. +//! QR codes display information in a type of two-dimensional matrix barcode. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::qr_code; +//! +//! struct State { +//! data: qr_code::Data, +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! // ... +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! qr_code(&state.data).into() +//! } +//! ``` +use crate::Renderer; use crate::canvas; use crate::core::layout; use crate::core::mouse; use crate::core::renderer::{self, Renderer as _}; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Color, Element, Layout, Length, Point, Rectangle, Size, Theme, Vector, - Widget, + Color, Element, Layout, Length, Pixels, Point, Rectangle, Size, Theme, + Vector, Widget, }; -use crate::Renderer; use std::cell::RefCell; use thiserror::Error; -const DEFAULT_CELL_SIZE: u16 = 4; +const DEFAULT_CELL_SIZE: f32 = 4.0; const QUIET_ZONE: usize = 2; /// A type of matrix barcode consisting of squares arranged in a grid which /// can be read by an imaging device, such as a camera. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::qr_code; +/// +/// struct State { +/// data: qr_code::Data, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// qr_code(&state.data).into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct QRCode<'a, Theme = crate::Theme> where Theme: Catalog, { data: &'a Data, - cell_size: u16, + cell_size: f32, class: Theme::Class<'a>, } @@ -42,8 +84,16 @@ where } /// Sets the size of the squares of the grid cell of the [`QRCode`]. - pub fn cell_size(mut self, cell_size: u16) -> Self { - self.cell_size = cell_size; + pub fn cell_size(mut self, cell_size: impl Into<Pixels>) -> Self { + self.cell_size = cell_size.into().0; + self + } + + /// Sets the size of the entire [`QRCode`]. + pub fn total_size(mut self, total_size: impl Into<Pixels>) -> Self { + self.cell_size = + total_size.into().0 / (self.data.width + 2 * QUIET_ZONE) as f32; + self } @@ -66,7 +116,7 @@ where } } -impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'a, Theme> +impl<Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'_, Theme> where Theme: Catalog, { @@ -91,8 +141,8 @@ where _renderer: &Renderer, _limits: &layout::Limits, ) -> layout::Node { - let side_length = (self.data.width + 2 * QUIET_ZONE) as f32 - * f32::from(self.cell_size); + let side_length = + (self.data.width + 2 * QUIET_ZONE) as f32 * self.cell_size; layout::Node::new(Size::new(side_length, side_length)) } diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 6b22961d..0df4d715 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -1,6 +1,63 @@ -//! Create choices using radio buttons. +//! Radio buttons let users choose a single option from a bunch of options. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::{column, radio}; +//! +//! struct State { +//! selection: Option<Choice>, +//! } +//! +//! #[derive(Debug, Clone, Copy)] +//! enum Message { +//! RadioSelected(Choice), +//! } +//! +//! #[derive(Debug, Clone, Copy, PartialEq, Eq)] +//! enum Choice { +//! A, +//! B, +//! C, +//! All, +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! let a = radio( +//! "A", +//! Choice::A, +//! state.selection, +//! Message::RadioSelected, +//! ); +//! +//! let b = radio( +//! "B", +//! Choice::B, +//! state.selection, +//! Message::RadioSelected, +//! ); +//! +//! let c = radio( +//! "C", +//! Choice::C, +//! state.selection, +//! Message::RadioSelected, +//! ); +//! +//! let all = radio( +//! "All of the above", +//! Choice::All, +//! state.selection, +//! Message::RadioSelected +//! ); +//! +//! column![a, b, c, all].into() +//! } +//! ``` use crate::core::alignment; -use crate::core::event::{self, Event}; +use crate::core::border::{self, Border}; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; @@ -8,8 +65,9 @@ use crate::core::text; use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ - Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, + Background, Clipboard, Color, Element, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Theme, Widget, }; @@ -17,54 +75,59 @@ use crate::core::{ /// /// # Example /// ```no_run -/// # type Radio<'a, Message> = -/// # iced_widget::Radio<'a, Message, iced_widget::Theme, iced_widget::renderer::Renderer>; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # -/// # use iced_widget::column; +/// use iced::widget::{column, radio}; +/// +/// struct State { +/// selection: Option<Choice>, +/// } +/// +/// #[derive(Debug, Clone, Copy)] +/// enum Message { +/// RadioSelected(Choice), +/// } +/// /// #[derive(Debug, Clone, Copy, PartialEq, Eq)] -/// pub enum Choice { +/// enum Choice { /// A, /// B, /// C, /// All, /// } /// -/// #[derive(Debug, Clone, Copy)] -/// pub enum Message { -/// RadioSelected(Choice), +/// fn view(state: &State) -> Element<'_, Message> { +/// let a = radio( +/// "A", +/// Choice::A, +/// state.selection, +/// Message::RadioSelected, +/// ); +/// +/// let b = radio( +/// "B", +/// Choice::B, +/// state.selection, +/// Message::RadioSelected, +/// ); +/// +/// let c = radio( +/// "C", +/// Choice::C, +/// state.selection, +/// Message::RadioSelected, +/// ); +/// +/// let all = radio( +/// "All of the above", +/// Choice::All, +/// state.selection, +/// Message::RadioSelected +/// ); +/// +/// column![a, b, c, all].into() /// } -/// -/// let selected_choice = Some(Choice::A); -/// -/// let a = Radio::new( -/// "A", -/// Choice::A, -/// selected_choice, -/// Message::RadioSelected, -/// ); -/// -/// let b = Radio::new( -/// "B", -/// Choice::B, -/// selected_choice, -/// Message::RadioSelected, -/// ); -/// -/// let c = Radio::new( -/// "C", -/// Choice::C, -/// selected_choice, -/// Message::RadioSelected, -/// ); -/// -/// let all = Radio::new( -/// "All of the above", -/// Choice::All, -/// selected_choice, -/// Message::RadioSelected -/// ); -/// -/// let content = column![a, b, c, all]; /// ``` #[allow(missing_debug_implementations)] pub struct Radio<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> @@ -81,8 +144,10 @@ where text_size: Option<Pixels>, text_line_height: text::LineHeight, text_shaping: text::Shaping, + text_wrapping: text::Wrapping, font: Option<Renderer::Font>, class: Theme::Class<'a>, + last_status: Option<Status>, } impl<'a, Message, Theme, Renderer> Radio<'a, Message, Theme, Renderer> @@ -104,7 +169,7 @@ where /// * the label of the [`Radio`] button /// * the current selected value /// * a function that will be called when the [`Radio`] is selected. It - /// receives the value of the radio and must produce a `Message`. + /// receives the value of the radio and must produce a `Message`. pub fn new<F, V>( label: impl Into<String>, value: V, @@ -121,12 +186,14 @@ where label: label.into(), width: Length::Shrink, size: Self::DEFAULT_SIZE, - spacing: Self::DEFAULT_SPACING, //15 + spacing: Self::DEFAULT_SPACING, text_size: None, text_line_height: text::LineHeight::default(), - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::default(), + text_wrapping: text::Wrapping::default(), font: None, class: Theme::default(), + last_status: None, } } @@ -169,6 +236,12 @@ where self } + /// Sets the [`text::Wrapping`] strategy of the [`Radio`] button. + pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self { + self.text_wrapping = wrapping; + self + } + /// Sets the text font of the [`Radio`] button. pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { self.font = Some(font.into()); @@ -194,8 +267,8 @@ where } } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for Radio<'a, Message, Theme, Renderer> +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Radio<'_, Message, Theme, Renderer> where Message: Clone, Theme: Catalog, @@ -244,35 +317,53 @@ where alignment::Horizontal::Left, alignment::Vertical::Top, self.text_shaping, + self.text_wrapping, ) }, ) } - 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 { + ) { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { if cursor.is_over(layout.bounds()) { shell.publish(self.on_click.clone()); - - return event::Status::Captured; + shell.capture_event(); } } _ => {} } - event::Status::Ignored + let current_status = { + let is_mouse_over = cursor.is_over(layout.bounds()); + let is_selected = self.is_selected; + + if is_mouse_over { + Status::Hovered { is_selected } + } else { + Status::Active { is_selected } + } + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.last_status = Some(current_status); + } else if self + .last_status + .is_some_and(|last_status| last_status != current_status) + { + shell.request_redraw(); + } } fn mouse_interaction( @@ -297,21 +388,17 @@ where theme: &Theme, defaults: &renderer::Style, layout: Layout<'_>, - cursor: mouse::Cursor, + _cursor: mouse::Cursor, viewport: &Rectangle, ) { - let is_mouse_over = cursor.is_over(layout.bounds()); - let is_selected = self.is_selected; - let mut children = layout.children(); - let status = if is_mouse_over { - Status::Hovered { is_selected } - } else { - Status::Active { is_selected } - }; - - let style = theme.style(&self.class, status); + let style = theme.style( + &self.class, + self.last_status.unwrap_or(Status::Active { + is_selected: self.is_selected, + }), + ); { let layout = children.next().unwrap(); @@ -342,7 +429,7 @@ where width: bounds.width - dot_size, height: bounds.height - dot_size, }, - border: Border::rounded(dot_size / 2.0), + border: border::rounded(dot_size / 2.0), ..renderer::Quad::default() }, style.dot_color, @@ -352,12 +439,14 @@ where { let label_layout = children.next().unwrap(); + let state: &widget::text::State<Renderer::Paragraph> = + tree.state.downcast_ref(); crate::text::draw( renderer, defaults, label_layout, - tree.state.downcast_ref(), + state.0.raw(), crate::text::Style { color: style.text_color, }, @@ -397,7 +486,7 @@ pub enum Status { } /// The appearance of a radio button. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`Background`] of the radio button. pub background: Background, diff --git a/widget/src/row.rs b/widget/src/row.rs index fa352171..5ffeab49 100644 --- a/widget/src/row.rs +++ b/widget/src/row.rs @@ -1,23 +1,44 @@ //! Distribute content horizontally. -use crate::core::event::{self, Event}; +use crate::core::alignment::{self, Alignment}; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ - Alignment, Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, - Size, Vector, Widget, + Clipboard, Element, Event, Length, Padding, Pixels, Rectangle, Shell, Size, + Vector, Widget, }; /// A container that distributes its contents horizontally. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{button, row}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// row![ +/// "I am to the left!", +/// button("I am in the middle!"), +/// "I am to the right!", +/// ].into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Row<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> { spacing: f32, padding: Padding, width: Length, height: Length, - align_items: Alignment, + align: Alignment, clip: bool, children: Vec<Element<'a, Message, Theme, Renderer>>, } @@ -60,7 +81,7 @@ where padding: Padding::ZERO, width: Length::Shrink, height: Length::Shrink, - align_items: Alignment::Start, + align: Alignment::Start, clip: false, children, } @@ -95,8 +116,8 @@ where } /// Sets the vertical alignment of the contents of the [`Row`] . - pub fn align_items(mut self, align: Alignment) -> Self { - self.align_items = align; + pub fn align_y(mut self, align: impl Into<alignment::Vertical>) -> Self { + self.align = Alignment::from(align.into()); self } @@ -141,9 +162,16 @@ where ) -> Self { children.into_iter().fold(self, Self::push) } + + /// Turns the [`Row`] into a [`Wrapping`] row. + /// + /// The original alignment of the [`Row`] is preserved per row wrapped. + pub fn wrap(self) -> Wrapping<'a, Message, Theme, Renderer> { + Wrapping { row: self } + } } -impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> +impl<Message, Renderer> Default for Row<'_, Message, Renderer> where Renderer: crate::core::Renderer, { @@ -152,8 +180,21 @@ where } } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> +impl<'a, Message, Theme, Renderer: crate::core::Renderer> + FromIterator<Element<'a, Message, Theme, Renderer>> for Row<'a, Message, Theme, Renderer> +{ + fn from_iter< + T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>, + >( + iter: T, + ) -> Self { + Self::with_children(iter) + } +} + +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Row<'_, Message, Theme, Renderer> where Renderer: crate::core::Renderer, { @@ -186,7 +227,7 @@ where self.height, self.padding, self.spacing, - self.align_items, + self.align, &self.children, &mut tree.children, ) @@ -197,7 +238,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children @@ -212,34 +253,28 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.children + ) { + for ((child, state), layout) in self + .children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) - }) - .fold(event::Status::Ignored, event::Status::merge) + { + child.as_widget_mut().update( + state, event, layout, cursor, renderer, clipboard, shell, + viewport, + ); + } } fn mouse_interaction( @@ -274,24 +309,21 @@ where viewport: &Rectangle, ) { if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + let viewport = if self.clip { + &clipped_viewport + } else { + viewport + }; + for ((child, state), layout) in self .children .iter() .zip(&tree.children) .zip(layout.children()) + .filter(|(_, layout)| layout.bounds().intersects(viewport)) { child.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor, - if self.clip { - &clipped_viewport - } else { - viewport - }, + state, renderer, theme, style, layout, cursor, viewport, ); } } @@ -325,3 +357,196 @@ where Self::new(row) } } + +/// A [`Row`] that wraps its contents. +/// +/// Create a [`Row`] first, and then call [`Row::wrap`] to +/// obtain a [`Row`] that wraps its contents. +/// +/// The original alignment of the [`Row`] is preserved per row wrapped. +#[allow(missing_debug_implementations)] +pub struct Wrapping< + 'a, + Message, + Theme = crate::Theme, + Renderer = crate::Renderer, +> { + row: Row<'a, Message, Theme, Renderer>, +} + +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Wrapping<'_, Message, Theme, Renderer> +where + Renderer: crate::core::Renderer, +{ + fn children(&self) -> Vec<Tree> { + self.row.children() + } + + fn diff(&self, tree: &mut Tree) { + self.row.diff(tree); + } + + fn size(&self) -> Size<Length> { + self.row.size() + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits + .width(self.row.width) + .height(self.row.height) + .shrink(self.row.padding); + + let spacing = self.row.spacing; + let max_width = limits.max().width; + + let mut children: Vec<layout::Node> = Vec::new(); + let mut intrinsic_size = Size::ZERO; + let mut row_start = 0; + let mut row_height = 0.0; + let mut x = 0.0; + let mut y = 0.0; + + let align_factor = match self.row.align { + Alignment::Start => 0.0, + Alignment::Center => 2.0, + Alignment::End => 1.0, + }; + + let align = |row_start: std::ops::Range<usize>, + row_height: f32, + children: &mut Vec<layout::Node>| { + if align_factor != 0.0 { + for node in &mut children[row_start] { + let height = node.size().height; + + node.translate_mut(Vector::new( + 0.0, + (row_height - height) / align_factor, + )); + } + } + }; + + for (i, child) in self.row.children.iter().enumerate() { + let node = child.as_widget().layout( + &mut tree.children[i], + renderer, + &limits, + ); + + let child_size = node.size(); + + if x != 0.0 && x + child_size.width > max_width { + intrinsic_size.width = intrinsic_size.width.max(x - spacing); + + align(row_start..i, row_height, &mut children); + + y += row_height + spacing; + x = 0.0; + row_start = i; + row_height = 0.0; + } + + row_height = row_height.max(child_size.height); + + children.push(node.move_to(( + x + self.row.padding.left, + y + self.row.padding.top, + ))); + + x += child_size.width + spacing; + } + + if x != 0.0 { + intrinsic_size.width = intrinsic_size.width.max(x - spacing); + } + + intrinsic_size.height = y + row_height; + align(row_start..children.len(), row_height, &mut children); + + let size = + limits.resolve(self.row.width, self.row.height, intrinsic_size); + + layout::Node::with_children(size.expand(self.row.padding), children) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + self.row.operate(tree, layout, renderer, operation); + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + self.row.update( + tree, event, layout, cursor, renderer, clipboard, shell, viewport, + ); + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.row + .mouse_interaction(tree, layout, cursor, viewport, renderer) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + self.row + .draw(tree, renderer, theme, style, layout, cursor, viewport); + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { + self.row.overlay(tree, layout, renderer, translation) + } +} + +impl<'a, Message, Theme, Renderer> From<Wrapping<'a, Message, Theme, Renderer>> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: crate::core::Renderer + 'a, +{ + fn from(row: Wrapping<'a, Message, Theme, Renderer>) -> Self { + Self::new(row) + } +} diff --git a/widget/src/rule.rs b/widget/src/rule.rs index 1a536d2f..65c0a6dc 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -1,6 +1,23 @@ -//! Display a horizontal or vertical rule for dividing content. +//! Rules divide space horizontally or vertically. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::horizontal_rule; +//! +//! #[derive(Clone)] +//! enum Message { +//! // ..., +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! horizontal_rule(2).into() +//! } +//! ``` use crate::core; -use crate::core::border::{self, Border}; +use crate::core::border; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; @@ -10,6 +27,23 @@ use crate::core::{ }; /// Display a horizontal or vertical rule for dividing content. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::horizontal_rule; +/// +/// #[derive(Clone)] +/// enum Message { +/// // ..., +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// horizontal_rule(2).into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Rule<'a, Theme = crate::Theme> where @@ -64,8 +98,8 @@ where } } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for Rule<'a, Theme> +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Rule<'_, Theme> where Renderer: core::Renderer, Theme: Catalog, @@ -132,7 +166,7 @@ where renderer.fill_quad( renderer::Quad { bounds, - border: Border::rounded(style.radius), + border: border::rounded(style.radius), ..renderer::Quad::default() }, style.color, @@ -153,7 +187,7 @@ where } /// The appearance of a rule. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The color of the rule. pub color: Color, @@ -166,7 +200,7 @@ pub struct Style { } /// The fill mode of a rule. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum FillMode { /// Fill the whole length of the container. Full, diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index 6fc00f87..0c876036 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1,26 +1,69 @@ -//! Navigate an endless amount of content with a scrollbar. -// use crate::container; +//! Scrollables let users navigate an endless amount of content with a scrollbar. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::{column, scrollable, vertical_space}; +//! +//! enum Message { +//! // ... +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! scrollable(column![ +//! "Scroll me!", +//! vertical_space().height(3000), +//! "You did it!", +//! ]).into() +//! } +//! ``` use crate::container; -use crate::core::event::{self, Event}; +use crate::core::border::{self, Border}; use crate::core::keyboard; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; +use crate::core::time::{Duration, Instant}; use crate::core::touch; use crate::core::widget; use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ - self, Background, Border, Clipboard, Color, Element, Layout, Length, - Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, + self, Background, Clipboard, Color, Element, Event, InputMethod, Layout, + Length, Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, + Widget, }; -use crate::runtime::Command; +use crate::runtime::Action; +use crate::runtime::task::{self, Task}; pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; /// A widget that can vertically display an infinite amount of content with a /// scrollbar. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{column, scrollable, vertical_space}; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// scrollable(column![ +/// "Scroll me!", +/// vertical_space().height(3000), +/// "You did it!", +/// ]).into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Scrollable< 'a, @@ -38,6 +81,7 @@ pub struct Scrollable< content: Element<'a, Message, Theme, Renderer>, on_scroll: Option<Box<dyn Fn(Viewport) -> Message + 'a>>, class: Theme::Class<'a>, + last_status: Option<Status>, } impl<'a, Message, Theme, Renderer> Scrollable<'a, Message, Theme, Renderer> @@ -55,36 +99,59 @@ where /// Creates a new [`Scrollable`] with the given [`Direction`]. pub fn with_direction( content: impl Into<Element<'a, Message, Theme, Renderer>>, - direction: Direction, + direction: impl Into<Direction>, ) -> Self { - let content = content.into(); - - debug_assert!( - direction.vertical().is_none() - || !content.as_widget().size_hint().height.is_fill(), - "scrollable content must not fill its vertical scrolling axis" - ); - - debug_assert!( - direction.horizontal().is_none() - || !content.as_widget().size_hint().width.is_fill(), - "scrollable content must not fill its horizontal scrolling axis" - ); - Scrollable { id: None, width: Length::Shrink, height: Length::Shrink, - direction, - content, + direction: direction.into(), + content: content.into(), on_scroll: None, class: Theme::default(), + last_status: None, } + .validate() + } + + fn validate(mut self) -> Self { + let size_hint = self.content.as_widget().size_hint(); + + debug_assert!( + self.direction.vertical().is_none() || !size_hint.height.is_fill(), + "scrollable content must not fill its vertical scrolling axis" + ); + + debug_assert!( + self.direction.horizontal().is_none() || !size_hint.width.is_fill(), + "scrollable content must not fill its horizontal scrolling axis" + ); + + if self.direction.horizontal().is_none() { + self.width = self.width.enclose(size_hint.width); + } + + if self.direction.vertical().is_none() { + self.height = self.height.enclose(size_hint.height); + } + + self + } + + /// Makes the [`Scrollable`] scroll horizontally, with default [`Scrollbar`] settings. + pub fn horizontal(self) -> Self { + self.direction(Direction::Horizontal(Scrollbar::default())) + } + + /// Sets the [`Direction`] of the [`Scrollable`]. + pub fn direction(mut self, direction: impl Into<Direction>) -> Self { + self.direction = direction.into(); + self.validate() } /// Sets the [`Id`] of the [`Scrollable`]. - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); + pub fn id(mut self, id: impl Into<Id>) -> Self { + self.id = Some(id.into()); self } @@ -108,6 +175,69 @@ where self } + /// Anchors the vertical [`Scrollable`] direction to the top. + pub fn anchor_top(self) -> Self { + self.anchor_y(Anchor::Start) + } + + /// Anchors the vertical [`Scrollable`] direction to the bottom. + pub fn anchor_bottom(self) -> Self { + self.anchor_y(Anchor::End) + } + + /// Anchors the horizontal [`Scrollable`] direction to the left. + pub fn anchor_left(self) -> Self { + self.anchor_x(Anchor::Start) + } + + /// Anchors the horizontal [`Scrollable`] direction to the right. + pub fn anchor_right(self) -> Self { + self.anchor_x(Anchor::End) + } + + /// Sets the [`Anchor`] of the horizontal direction of the [`Scrollable`], if applicable. + pub fn anchor_x(mut self, alignment: Anchor) -> Self { + match &mut self.direction { + Direction::Horizontal(horizontal) + | Direction::Both { horizontal, .. } => { + horizontal.alignment = alignment; + } + Direction::Vertical { .. } => {} + } + + self + } + + /// Sets the [`Anchor`] of the vertical direction of the [`Scrollable`], if applicable. + pub fn anchor_y(mut self, alignment: Anchor) -> Self { + match &mut self.direction { + Direction::Vertical(vertical) + | Direction::Both { vertical, .. } => { + vertical.alignment = alignment; + } + Direction::Horizontal { .. } => {} + } + + self + } + + /// Embeds the [`Scrollbar`] into the [`Scrollable`], instead of floating on top of the + /// content. + /// + /// The `spacing` provided will be used as space between the [`Scrollbar`] and the contents + /// of the [`Scrollable`]. + pub fn spacing(mut self, new_spacing: impl Into<Pixels>) -> Self { + match &mut self.direction { + Direction::Horizontal(scrollbar) + | Direction::Vertical(scrollbar) => { + scrollbar.spacing = Some(new_spacing.into().0); + } + Direction::Both { .. } => {} + } + + self + } + /// Sets the style of this [`Scrollable`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self @@ -131,107 +261,138 @@ where #[derive(Debug, Clone, Copy, PartialEq)] pub enum Direction { /// Vertical scrolling - Vertical(Properties), + Vertical(Scrollbar), /// Horizontal scrolling - Horizontal(Properties), + Horizontal(Scrollbar), /// Both vertical and horizontal scrolling Both { /// The properties of the vertical scrollbar. - vertical: Properties, + vertical: Scrollbar, /// The properties of the horizontal scrollbar. - horizontal: Properties, + horizontal: Scrollbar, }, } impl Direction { - /// Returns the [`Properties`] of the horizontal scrollbar, if any. - pub fn horizontal(&self) -> Option<&Properties> { + /// Returns the horizontal [`Scrollbar`], if any. + pub fn horizontal(&self) -> Option<&Scrollbar> { match self { - Self::Horizontal(properties) => Some(properties), + Self::Horizontal(scrollbar) => Some(scrollbar), Self::Both { horizontal, .. } => Some(horizontal), Self::Vertical(_) => None, } } - /// Returns the [`Properties`] of the vertical scrollbar, if any. - pub fn vertical(&self) -> Option<&Properties> { + /// Returns the vertical [`Scrollbar`], if any. + pub fn vertical(&self) -> Option<&Scrollbar> { match self { - Self::Vertical(properties) => Some(properties), + Self::Vertical(scrollbar) => Some(scrollbar), Self::Both { vertical, .. } => Some(vertical), Self::Horizontal(_) => None, } } + + fn align(&self, delta: Vector) -> Vector { + let horizontal_alignment = + self.horizontal().map(|p| p.alignment).unwrap_or_default(); + + let vertical_alignment = + self.vertical().map(|p| p.alignment).unwrap_or_default(); + + let align = |alignment: Anchor, delta: f32| match alignment { + Anchor::Start => delta, + Anchor::End => -delta, + }; + + Vector::new( + align(horizontal_alignment, delta.x), + align(vertical_alignment, delta.y), + ) + } } impl Default for Direction { fn default() -> Self { - Self::Vertical(Properties::default()) + Self::Vertical(Scrollbar::default()) } } -/// Properties of a scrollbar within a [`Scrollable`]. +/// A scrollbar within a [`Scrollable`]. #[derive(Debug, Clone, Copy, PartialEq)] -pub struct Properties { +pub struct Scrollbar { width: f32, margin: f32, scroller_width: f32, - alignment: Alignment, + alignment: Anchor, + spacing: Option<f32>, } -impl Default for Properties { +impl Default for Scrollbar { fn default() -> Self { Self { width: 10.0, margin: 0.0, scroller_width: 10.0, - alignment: Alignment::Start, + alignment: Anchor::Start, + spacing: None, } } } -impl Properties { - /// Creates new [`Properties`] for use in a [`Scrollable`]. +impl Scrollbar { + /// Creates new [`Scrollbar`] for use in a [`Scrollable`]. pub fn new() -> Self { Self::default() } - /// Sets the scrollbar width of the [`Scrollable`] . + /// Sets the scrollbar width of the [`Scrollbar`] . pub fn width(mut self, width: impl Into<Pixels>) -> Self { self.width = width.into().0.max(0.0); self } - /// Sets the scrollbar margin of the [`Scrollable`] . + /// Sets the scrollbar margin of the [`Scrollbar`] . pub fn margin(mut self, margin: impl Into<Pixels>) -> Self { self.margin = margin.into().0; self } - /// Sets the scroller width of the [`Scrollable`] . + /// Sets the scroller width of the [`Scrollbar`] . pub fn scroller_width(mut self, scroller_width: impl Into<Pixels>) -> Self { self.scroller_width = scroller_width.into().0.max(0.0); self } - /// Sets the alignment of the [`Scrollable`] . - pub fn alignment(mut self, alignment: Alignment) -> Self { + /// Sets the [`Anchor`] of the [`Scrollbar`] . + pub fn anchor(mut self, alignment: Anchor) -> Self { self.alignment = alignment; self } + + /// Sets whether the [`Scrollbar`] should be embedded in the [`Scrollable`], using + /// the given spacing between itself and the contents. + /// + /// An embedded [`Scrollbar`] will always be displayed, will take layout space, + /// and will not float over the contents. + pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self { + self.spacing = Some(spacing.into().0); + self + } } -/// Alignment of the scrollable's content relative to it's [`Viewport`] in one direction. +/// The anchor of the scroller of the [`Scrollable`] relative to its [`Viewport`] +/// on a given axis. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub enum Alignment { - /// Content is aligned to the start of the [`Viewport`]. +pub enum Anchor { + /// Scroller is anchoer to the start of the [`Viewport`]. #[default] Start, - /// Content is aligned to the end of the [`Viewport`] + /// Content is aligned to the end of the [`Viewport`]. End, } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for Scrollable<'a, Message, Theme, Renderer> +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Scrollable<'_, Message, Theme, Renderer> where Theme: Catalog, Renderer: core::Renderer, @@ -265,29 +426,55 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout::contained(limits, self.width, self.height, |limits| { - let child_limits = layout::Limits::new( - Size::new(limits.min().width, limits.min().height), - Size::new( - if self.direction.horizontal().is_some() { - f32::INFINITY - } else { - limits.max().width - }, - if self.direction.vertical().is_some() { - f32::MAX - } else { - limits.max().height - }, - ), - ); + let (right_padding, bottom_padding) = match self.direction { + Direction::Vertical(Scrollbar { + width, + margin, + spacing: Some(spacing), + .. + }) => (width + margin * 2.0 + spacing, 0.0), + Direction::Horizontal(Scrollbar { + width, + margin, + spacing: Some(spacing), + .. + }) => (0.0, width + margin * 2.0 + spacing), + _ => (0.0, 0.0), + }; - self.content.as_widget().layout( - &mut tree.children[0], - renderer, - &child_limits, - ) - }) + layout::padded( + limits, + self.width, + self.height, + Padding { + right: right_padding, + bottom: bottom_padding, + ..Padding::ZERO + }, + |limits| { + let child_limits = layout::Limits::new( + Size::new(limits.min().width, limits.min().height), + Size::new( + if self.direction.horizontal().is_some() { + f32::INFINITY + } else { + limits.max().width + }, + if self.direction.vertical().is_some() { + f32::MAX + } else { + limits.max().height + }, + ), + ); + + self.content.as_widget().layout( + &mut tree.children[0], + renderer, + &child_limits, + ) + }, + ) } fn operate( @@ -295,7 +482,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation, ) { let state = tree.state.downcast_mut::<State>(); @@ -306,10 +493,11 @@ where state.translation(self.direction, bounds, content_bounds); operation.scrollable( - state, self.id.as_ref().map(|id| &id.0), bounds, + content_bounds, translation, + state, ); operation.container( @@ -326,17 +514,17 @@ where ); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let state = tree.state.downcast_mut::<State>(); let bounds = layout.bounds(); let cursor_over_scrollable = cursor.position_over(bounds); @@ -350,285 +538,87 @@ where let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = scrollbars.is_mouse_over(cursor); - if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some(scrollbar) = scrollbars.y { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; + let last_offsets = (state.offset_x, state.offset_y); - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - let _ = notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - - return event::Status::Captured; - } + if let Some(last_scrolled) = state.last_scrolled { + let clear_transaction = match event { + Event::Mouse( + mouse::Event::ButtonPressed(_) + | mouse::Event::ButtonReleased(_) + | mouse::Event::CursorLeft, + ) => true, + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + last_scrolled.elapsed() > Duration::from_millis(100) } - _ => {} - } - } else if mouse_over_y_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( - scrollbars.grab_y_scroller(cursor_position), - scrollbars.y, - ) { - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.y_scroller_grabbed_at = Some(scroller_grabbed_at); - - let _ = notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - } - - return event::Status::Captured; - } - _ => {} - } - } - - if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let Some(scrollbar) = scrollbars.x { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - let _ = notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - } - - return event::Status::Captured; - } - _ => {} - } - } else if mouse_over_x_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( - scrollbars.grab_x_scroller(cursor_position), - scrollbars.x, - ) { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.x_scroller_grabbed_at = Some(scroller_grabbed_at); - - let _ = notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ); - - return event::Status::Captured; - } - } - _ => {} - } - } - - let mut event_status = { - let cursor = match cursor_over_scrollable { - Some(cursor_position) - if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => - { - mouse::Cursor::Available( - cursor_position - + state.translation( - self.direction, - bounds, - content_bounds, - ), - ) - } - _ => mouse::Cursor::Unavailable, + _ => last_scrolled.elapsed() > Duration::from_millis(1500), }; - let translation = - state.translation(self.direction, bounds, content_bounds); - - self.content.as_widget_mut().on_event( - &mut tree.children[0], - event.clone(), - content, - cursor, - renderer, - clipboard, - shell, - &Rectangle { - y: bounds.y + translation.y, - x: bounds.x + translation.x, - ..bounds - }, - ) - }; - - if matches!( - event, - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch( - touch::Event::FingerLifted { .. } - | touch::Event::FingerLost { .. } - ) - ) { - state.scroll_area_touched_at = None; - state.x_scroller_grabbed_at = None; - state.y_scroller_grabbed_at = None; - - return event_status; - } - - if let event::Status::Captured = event_status { - return event::Status::Captured; - } - - if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = - event - { - state.keyboard_modifiers = modifiers; - - return event::Status::Ignored; - } - - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - if cursor_over_scrollable.is_none() { - return event::Status::Ignored; - } - - let delta = match delta { - mouse::ScrollDelta::Lines { x, y } => { - // TODO: Configurable speed/friction (?) - let movement = if !cfg!(target_os = "macos") // macOS automatically inverts the axes when Shift is pressed - && state.keyboard_modifiers.shift() - { - Vector::new(y, x) - } else { - Vector::new(x, y) - }; - - movement * 60.0 - } - mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), - }; - - state.scroll(delta, self.direction, bounds, content_bounds); - - event_status = if notify_on_scroll( - state, - &self.on_scroll, - bounds, - content_bounds, - shell, - ) { - event::Status::Captured - } else { - event::Status::Ignored - }; + if clear_transaction { + state.last_scrolled = None; } - Event::Touch(event) - if state.scroll_area_touched_at.is_some() - || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar => - { - match event { - touch::Event::FingerPressed { .. } => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; + } - state.scroll_area_touched_at = Some(cursor_position); - } - touch::Event::FingerMoved { .. } => { - if let Some(scroll_box_touched_at) = - state.scroll_area_touched_at - { - let Some(cursor_position) = cursor.position() + let mut update = || { + if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some(scrollbar) = scrollbars.y { + let Some(cursor_position) = + cursor.land().position() else { - return event::Status::Ignored; + return; }; - let delta = Vector::new( - cursor_position.x - scroll_box_touched_at.x, - cursor_position.y - scroll_box_touched_at.y, - ); - - state.scroll( - delta, - self.direction, + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), bounds, content_bounds, ); - state.scroll_area_touched_at = - Some(cursor_position); + let _ = notify_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); - // TODO: bubble up touch movements if not consumed. - let _ = notify_on_scroll( + shell.capture_event(); + } + } + _ => {} + } + } else if mouse_over_y_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return; + }; + + if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( + scrollbars.grab_y_scroller(cursor_position), + scrollbars.y, + ) { + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.y_scroller_grabbed_at = + Some(scroller_grabbed_at); + + let _ = notify_scroll( state, &self.on_scroll, bounds, @@ -636,16 +626,326 @@ where shell, ); } + + shell.capture_event(); } _ => {} } - - event_status = event::Status::Captured; } - _ => {} + + if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + let Some(cursor_position) = cursor.land().position() + else { + return; + }; + + if let Some(scrollbar) = scrollbars.x { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + let _ = notify_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + + shell.capture_event(); + } + _ => {} + } + } else if mouse_over_x_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return; + }; + + if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( + scrollbars.grab_x_scroller(cursor_position), + scrollbars.x, + ) { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.x_scroller_grabbed_at = + Some(scroller_grabbed_at); + + let _ = notify_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + shell.capture_event(); + } + } + _ => {} + } + } + + if state.last_scrolled.is_none() + || !matches!( + event, + Event::Mouse(mouse::Event::WheelScrolled { .. }) + ) + { + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar + || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available( + cursor_position + + state.translation( + self.direction, + bounds, + content_bounds, + ), + ) + } + _ => mouse::Cursor::Unavailable, + }; + + let had_input_method = shell.input_method().is_enabled(); + + let translation = + state.translation(self.direction, bounds, content_bounds); + + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + content, + cursor, + renderer, + clipboard, + shell, + &Rectangle { + y: bounds.y + translation.y, + x: bounds.x + translation.x, + ..bounds + }, + ); + + if !had_input_method { + if let InputMethod::Enabled { position, .. } = + shell.input_method_mut() + { + *position = *position - translation; + } + } + }; + + if matches!( + event, + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch( + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } + ) + ) { + state.scroll_area_touched_at = None; + state.x_scroller_grabbed_at = None; + state.y_scroller_grabbed_at = None; + + return; + } + + if shell.is_event_captured() { + return; + } + + if let Event::Keyboard(keyboard::Event::ModifiersChanged( + modifiers, + )) = event + { + state.keyboard_modifiers = *modifiers; + + return; + } + + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + if cursor_over_scrollable.is_none() { + return; + } + + let delta = match *delta { + mouse::ScrollDelta::Lines { x, y } => { + let is_shift_pressed = + state.keyboard_modifiers.shift(); + + // macOS automatically inverts the axes when Shift is pressed + let (x, y) = if cfg!(target_os = "macos") + && is_shift_pressed + { + (y, x) + } else { + (x, y) + }; + + let movement = if !is_shift_pressed { + Vector::new(x, y) + } else { + Vector::new(y, x) + }; + + // TODO: Configurable speed/friction (?) + -movement * 60.0 + } + mouse::ScrollDelta::Pixels { x, y } => { + -Vector::new(x, y) + } + }; + + state.scroll( + self.direction.align(delta), + bounds, + content_bounds, + ); + + let has_scrolled = notify_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + let in_transaction = state.last_scrolled.is_some(); + + if has_scrolled || in_transaction { + shell.capture_event(); + } + } + Event::Touch(event) + if state.scroll_area_touched_at.is_some() + || !mouse_over_y_scrollbar + && !mouse_over_x_scrollbar => + { + match event { + touch::Event::FingerPressed { .. } => { + let Some(cursor_position) = cursor.position() + else { + return; + }; + + state.scroll_area_touched_at = + Some(cursor_position); + } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + state.scroll_area_touched_at + { + let Some(cursor_position) = cursor.position() + else { + return; + }; + + let delta = Vector::new( + scroll_box_touched_at.x - cursor_position.x, + scroll_box_touched_at.y - cursor_position.y, + ); + + state.scroll( + self.direction.align(delta), + bounds, + content_bounds, + ); + + state.scroll_area_touched_at = + Some(cursor_position); + + // TODO: bubble up touch movements if not consumed. + let _ = notify_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + } + _ => {} + } + + shell.capture_event(); + } + Event::Window(window::Event::RedrawRequested(_)) => { + let _ = notify_viewport( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + _ => {} + } + }; + + update(); + + let status = if state.y_scroller_grabbed_at.is_some() + || state.x_scroller_grabbed_at.is_some() + { + Status::Dragged { + is_horizontal_scrollbar_dragged: state + .x_scroller_grabbed_at + .is_some(), + is_vertical_scrollbar_dragged: state + .y_scroller_grabbed_at + .is_some(), + is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(), + is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(), + } + } else if cursor_over_scrollable.is_some() { + Status::Hovered { + is_horizontal_scrollbar_hovered: mouse_over_x_scrollbar, + is_vertical_scrollbar_hovered: mouse_over_y_scrollbar, + is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(), + is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(), + } + } else { + Status::Active { + is_horizontal_scrollbar_disabled: scrollbars.is_x_disabled(), + is_vertical_scrollbar_disabled: scrollbars.is_y_disabled(), + } + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.last_status = Some(status); } - event_status + if last_offsets != (state.offset_x, state.offset_y) + || self + .last_status + .is_some_and(|last_status| last_status != status) + { + shell.request_redraw(); + } } fn draw( @@ -687,27 +987,13 @@ where _ => mouse::Cursor::Unavailable, }; - let status = if state.y_scroller_grabbed_at.is_some() - || state.x_scroller_grabbed_at.is_some() - { - Status::Dragged { - is_horizontal_scrollbar_dragged: state - .x_scroller_grabbed_at - .is_some(), - is_vertical_scrollbar_dragged: state - .y_scroller_grabbed_at - .is_some(), - } - } else if cursor_over_scrollable.is_some() { - Status::Hovered { - is_horizontal_scrollbar_hovered: mouse_over_x_scrollbar, - is_vertical_scrollbar_hovered: mouse_over_y_scrollbar, - } - } else { - Status::Active - }; - - let style = theme.style(&self.class, status); + let style = theme.style( + &self.class, + self.last_status.unwrap_or(Status::Active { + is_horizontal_scrollbar_disabled: false, + is_vertical_scrollbar_disabled: false, + }), + ); container::draw_background(renderer, &style.container, layout.bounds()); @@ -725,9 +1011,9 @@ where content_layout, cursor, &Rectangle { - y: bounds.y + translation.y, - x: bounds.x + translation.x, - ..bounds + y: visible_bounds.y + translation.y, + x: visible_bounds.x + translation.x, + ..visible_bounds }, ); }, @@ -736,7 +1022,7 @@ where let draw_scrollbar = |renderer: &mut Renderer, - style: Scrollbar, + style: Rail, scrollbar: &internals::Scrollbar| { if scrollbar.bounds.width > 0.0 && scrollbar.bounds.height > 0.0 @@ -756,21 +1042,23 @@ where ); } - if scrollbar.scroller.bounds.width > 0.0 - && scrollbar.scroller.bounds.height > 0.0 - && (style.scroller.color != Color::TRANSPARENT - || (style.scroller.border.color - != Color::TRANSPARENT - && style.scroller.border.width > 0.0)) - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.scroller.bounds, - border: style.scroller.border, - ..renderer::Quad::default() - }, - style.scroller.color, - ); + if let Some(scroller) = scrollbar.scroller { + if scroller.bounds.width > 0.0 + && scroller.bounds.height > 0.0 + && (style.scroller.color != Color::TRANSPARENT + || (style.scroller.border.color + != Color::TRANSPARENT + && style.scroller.border.width > 0.0)) + { + renderer.fill_quad( + renderer::Quad { + bounds: scroller.bounds, + border: style.scroller.border, + ..renderer::Quad::default() + }, + style.scroller.color, + ); + } } }; @@ -784,7 +1072,7 @@ where if let Some(scrollbar) = scrollbars.y { draw_scrollbar( renderer, - style.vertical_scrollbar, + style.vertical_rail, &scrollbar, ); } @@ -792,7 +1080,7 @@ where if let Some(scrollbar) = scrollbars.x { draw_scrollbar( renderer, - style.horizontal_scrollbar, + style.horizontal_rail, &scrollbar, ); } @@ -827,9 +1115,9 @@ where content_layout, cursor, &Rectangle { - x: bounds.x + translation.x, - y: bounds.y + translation.y, - ..bounds + x: visible_bounds.x + translation.x, + y: visible_bounds.y + translation.y, + ..visible_bounds }, ); } @@ -952,26 +1240,56 @@ impl From<Id> for widget::Id { } } -/// Produces a [`Command`] that snaps the [`Scrollable`] with the given [`Id`] -/// to the provided `percentage` along the x & y axis. -pub fn snap_to<Message: 'static>( - id: Id, - offset: RelativeOffset, -) -> Command<Message> { - Command::widget(operation::scrollable::snap_to(id.0, offset)) +impl From<&'static str> for Id { + fn from(id: &'static str) -> Self { + Self::new(id) + } } -/// Produces a [`Command`] that scrolls the [`Scrollable`] with the given [`Id`] -/// to the provided [`AbsoluteOffset`] along the x & y axis. -pub fn scroll_to<Message: 'static>( - id: Id, - offset: AbsoluteOffset, -) -> Command<Message> { - Command::widget(operation::scrollable::scroll_to(id.0, offset)) +/// Produces a [`Task`] that snaps the [`Scrollable`] with the given [`Id`] +/// to the provided [`RelativeOffset`]. +pub fn snap_to<T>(id: impl Into<Id>, offset: RelativeOffset) -> Task<T> { + task::effect(Action::widget(operation::scrollable::snap_to( + id.into().0, + offset, + ))) } -/// Returns [`true`] if the viewport actually changed. -fn notify_on_scroll<Message>( +/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`] +/// to the provided [`AbsoluteOffset`]. +pub fn scroll_to<T>(id: impl Into<Id>, offset: AbsoluteOffset) -> Task<T> { + task::effect(Action::widget(operation::scrollable::scroll_to( + id.into().0, + offset, + ))) +} + +/// Produces a [`Task`] that scrolls the [`Scrollable`] with the given [`Id`] +/// by the provided [`AbsoluteOffset`]. +pub fn scroll_by<T>(id: impl Into<Id>, offset: AbsoluteOffset) -> Task<T> { + task::effect(Action::widget(operation::scrollable::scroll_by( + id.into().0, + offset, + ))) +} + +fn notify_scroll<Message>( + state: &mut State, + on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, + bounds: Rectangle, + content_bounds: Rectangle, + shell: &mut Shell<'_, Message>, +) -> bool { + if notify_viewport(state, on_scroll, bounds, content_bounds, shell) { + state.last_scrolled = Some(Instant::now()); + + true + } else { + false + } +} + +fn notify_viewport<Message>( state: &mut State, on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, bounds: Rectangle, @@ -1003,7 +1321,9 @@ fn notify_on_scroll<Message>( (a - b).abs() <= f32::EPSILON || (a.is_nan() && b.is_nan()) }; - if unchanged(last_relative_offset.x, current_relative_offset.x) + if last_notified.bounds == bounds + && last_notified.content_bounds == content_bounds + && unchanged(last_relative_offset.x, current_relative_offset.x) && unchanged(last_relative_offset.y, current_relative_offset.y) && unchanged(last_absolute_offset.x, current_absolute_offset.x) && unchanged(last_absolute_offset.y, current_absolute_offset.y) @@ -1012,10 +1332,11 @@ fn notify_on_scroll<Message>( } } + state.last_notified = Some(viewport); + if let Some(on_scroll) = on_scroll { shell.publish(on_scroll(viewport)); } - state.last_notified = Some(viewport); true } @@ -1029,6 +1350,7 @@ struct State { x_scroller_grabbed_at: Option<f32>, keyboard_modifiers: keyboard::Modifiers, last_notified: Option<Viewport>, + last_scrolled: Option<Instant>, } impl Default for State { @@ -1041,6 +1363,7 @@ impl Default for State { x_scroller_grabbed_at: None, keyboard_modifiers: keyboard::Modifiers::default(), last_notified: None, + last_scrolled: None, } } } @@ -1053,9 +1376,18 @@ impl operation::Scrollable for State { fn scroll_to(&mut self, offset: AbsoluteOffset) { State::scroll_to(self, offset); } + + fn scroll_by( + &mut self, + offset: AbsoluteOffset, + bounds: Rectangle, + content_bounds: Rectangle, + ) { + State::scroll_by(self, offset, bounds, content_bounds); + } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] enum Offset { Absolute(f32), Relative(f32), @@ -1077,13 +1409,13 @@ impl Offset { self, viewport: f32, content: f32, - alignment: Alignment, + alignment: Anchor, ) -> f32 { let offset = self.absolute(viewport, content); match alignment { - Alignment::Start => offset, - Alignment::End => ((content - viewport).max(0.0) - offset).max(0.0), + Anchor::Start => offset, + Anchor::End => ((content - viewport).max(0.0) - offset).max(0.0), } } } @@ -1156,34 +1488,13 @@ impl State { pub fn scroll( &mut self, delta: Vector<f32>, - direction: Direction, bounds: Rectangle, content_bounds: Rectangle, ) { - let horizontal_alignment = direction - .horizontal() - .map(|p| p.alignment) - .unwrap_or_default(); - - let vertical_alignment = direction - .vertical() - .map(|p| p.alignment) - .unwrap_or_default(); - - let align = |alignment: Alignment, delta: f32| match alignment { - Alignment::Start => delta, - Alignment::End => -delta, - }; - - let delta = Vector::new( - align(horizontal_alignment, delta.x), - align(vertical_alignment, delta.y), - ); - if bounds.height < content_bounds.height { self.offset_y = Offset::Absolute( (self.offset_y.absolute(bounds.height, content_bounds.height) - - delta.y) + + delta.y) .clamp(0.0, content_bounds.height - bounds.height), ); } @@ -1191,7 +1502,7 @@ impl State { if bounds.width < content_bounds.width { self.offset_x = Offset::Absolute( (self.offset_x.absolute(bounds.width, content_bounds.width) - - delta.x) + + delta.x) .clamp(0.0, content_bounds.width - bounds.width), ); } @@ -1237,6 +1548,16 @@ impl State { self.offset_y = Offset::Absolute(offset.y.max(0.0)); } + /// Scroll by the provided [`AbsoluteOffset`]. + pub fn scroll_by( + &mut self, + offset: AbsoluteOffset, + bounds: Rectangle, + content_bounds: Rectangle, + ) { + self.scroll(Vector::new(offset.x, offset.y), bounds, content_bounds); + } + /// Unsnaps the current scroll position, if snapped, given the bounds of the /// [`Scrollable`] and its contents. pub fn unsnap(&mut self, bounds: Rectangle, content_bounds: Rectangle) { @@ -1302,16 +1623,16 @@ impl Scrollbars { ) -> Self { let translation = state.translation(direction, bounds, content_bounds); - let show_scrollbar_x = direction - .horizontal() - .filter(|_| content_bounds.width > bounds.width); + let show_scrollbar_x = direction.horizontal().filter(|scrollbar| { + scrollbar.spacing.is_some() || content_bounds.width > bounds.width + }); - let show_scrollbar_y = direction - .vertical() - .filter(|_| content_bounds.height > bounds.height); + let show_scrollbar_y = direction.vertical().filter(|scrollbar| { + scrollbar.spacing.is_some() || content_bounds.height > bounds.height + }); let y_scrollbar = if let Some(vertical) = show_scrollbar_y { - let Properties { + let Scrollbar { width, margin, scroller_width, @@ -1345,34 +1666,44 @@ impl Scrollbars { }; let ratio = bounds.height / content_bounds.height; - // min height for easier grabbing with super tall content - let scroller_height = (scrollbar_bounds.height * ratio).max(2.0); - let scroller_offset = - translation.y * ratio * scrollbar_bounds.height / bounds.height; - let scroller_bounds = Rectangle { - x: bounds.x + bounds.width - - total_scrollbar_width / 2.0 - - scroller_width / 2.0, - y: (scrollbar_bounds.y + scroller_offset).max(0.0), - width: scroller_width, - height: scroller_height, + let scroller = if ratio >= 1.0 { + None + } else { + // min height for easier grabbing with super tall content + let scroller_height = + (scrollbar_bounds.height * ratio).max(2.0); + let scroller_offset = + translation.y * ratio * scrollbar_bounds.height + / bounds.height; + + let scroller_bounds = Rectangle { + x: bounds.x + bounds.width + - total_scrollbar_width / 2.0 + - scroller_width / 2.0, + y: (scrollbar_bounds.y + scroller_offset).max(0.0), + width: scroller_width, + height: scroller_height, + }; + + Some(internals::Scroller { + bounds: scroller_bounds, + }) }; Some(internals::Scrollbar { total_bounds: total_scrollbar_bounds, bounds: scrollbar_bounds, - scroller: internals::Scroller { - bounds: scroller_bounds, - }, + scroller, alignment: vertical.alignment, + disabled: content_bounds.height <= bounds.height, }) } else { None }; let x_scrollbar = if let Some(horizontal) = show_scrollbar_x { - let Properties { + let Scrollbar { width, margin, scroller_width, @@ -1406,27 +1737,36 @@ impl Scrollbars { }; let ratio = bounds.width / content_bounds.width; - // min width for easier grabbing with extra wide content - let scroller_length = (scrollbar_bounds.width * ratio).max(2.0); - let scroller_offset = - translation.x * ratio * scrollbar_bounds.width / bounds.width; - let scroller_bounds = Rectangle { - x: (scrollbar_bounds.x + scroller_offset).max(0.0), - y: bounds.y + bounds.height - - total_scrollbar_height / 2.0 - - scroller_width / 2.0, - width: scroller_length, - height: scroller_width, + let scroller = if ratio >= 1.0 { + None + } else { + // min width for easier grabbing with extra wide content + let scroller_length = (scrollbar_bounds.width * ratio).max(2.0); + let scroller_offset = + translation.x * ratio * scrollbar_bounds.width + / bounds.width; + + let scroller_bounds = Rectangle { + x: (scrollbar_bounds.x + scroller_offset).max(0.0), + y: bounds.y + bounds.height + - total_scrollbar_height / 2.0 + - scroller_width / 2.0, + width: scroller_length, + height: scroller_width, + }; + + Some(internals::Scroller { + bounds: scroller_bounds, + }) }; Some(internals::Scrollbar { total_bounds: total_scrollbar_bounds, bounds: scrollbar_bounds, - scroller: internals::Scroller { - bounds: scroller_bounds, - }, + scroller, alignment: horizontal.alignment, + disabled: content_bounds.width <= bounds.width, }) } else { None @@ -1455,34 +1795,42 @@ impl Scrollbars { } } + fn is_y_disabled(&self) -> bool { + self.y.map(|y| y.disabled).unwrap_or(false) + } + + fn is_x_disabled(&self) -> bool { + self.x.map(|x| x.disabled).unwrap_or(false) + } + fn grab_y_scroller(&self, cursor_position: Point) -> Option<f32> { - self.y.and_then(|scrollbar| { - if scrollbar.total_bounds.contains(cursor_position) { - Some(if scrollbar.scroller.bounds.contains(cursor_position) { - (cursor_position.y - scrollbar.scroller.bounds.y) - / scrollbar.scroller.bounds.height - } else { - 0.5 - }) + let scrollbar = self.y?; + let scroller = scrollbar.scroller?; + + if scrollbar.total_bounds.contains(cursor_position) { + Some(if scroller.bounds.contains(cursor_position) { + (cursor_position.y - scroller.bounds.y) / scroller.bounds.height } else { - None - } - }) + 0.5 + }) + } else { + None + } } fn grab_x_scroller(&self, cursor_position: Point) -> Option<f32> { - self.x.and_then(|scrollbar| { - if scrollbar.total_bounds.contains(cursor_position) { - Some(if scrollbar.scroller.bounds.contains(cursor_position) { - (cursor_position.x - scrollbar.scroller.bounds.x) - / scrollbar.scroller.bounds.width - } else { - 0.5 - }) + let scrollbar = self.x?; + let scroller = scrollbar.scroller?; + + if scrollbar.total_bounds.contains(cursor_position) { + Some(if scroller.bounds.contains(cursor_position) { + (cursor_position.x - scroller.bounds.x) / scroller.bounds.width } else { - None - } - }) + 0.5 + }) + } else { + None + } } fn active(&self) -> bool { @@ -1493,14 +1841,15 @@ impl Scrollbars { pub(super) mod internals { use crate::core::{Point, Rectangle}; - use super::Alignment; + use super::Anchor; #[derive(Debug, Copy, Clone)] pub struct Scrollbar { pub total_bounds: Rectangle, pub bounds: Rectangle, - pub scroller: Scroller, - pub alignment: Alignment, + pub scroller: Option<Scroller>, + pub alignment: Anchor, + pub disabled: bool, } impl Scrollbar { @@ -1515,14 +1864,18 @@ pub(super) mod internals { grabbed_at: f32, cursor_position: Point, ) -> f32 { - let percentage = (cursor_position.y - - self.bounds.y - - self.scroller.bounds.height * grabbed_at) - / (self.bounds.height - self.scroller.bounds.height); + if let Some(scroller) = self.scroller { + let percentage = (cursor_position.y + - self.bounds.y + - scroller.bounds.height * grabbed_at) + / (self.bounds.height - scroller.bounds.height); - match self.alignment { - Alignment::Start => percentage, - Alignment::End => 1.0 - percentage, + match self.alignment { + Anchor::Start => percentage, + Anchor::End => 1.0 - percentage, + } + } else { + 0.0 } } @@ -1532,14 +1885,18 @@ pub(super) mod internals { grabbed_at: f32, cursor_position: Point, ) -> f32 { - let percentage = (cursor_position.x - - self.bounds.x - - self.scroller.bounds.width * grabbed_at) - / (self.bounds.width - self.scroller.bounds.width); + if let Some(scroller) = self.scroller { + let percentage = (cursor_position.x + - self.bounds.x + - scroller.bounds.width * grabbed_at) + / (self.bounds.width - scroller.bounds.width); - match self.alignment { - Alignment::Start => percentage, - Alignment::End => 1.0 - percentage, + match self.alignment { + Anchor::Start => percentage, + Anchor::End => 1.0 - percentage, + } + } else { + 0.0 } } } @@ -1556,13 +1913,22 @@ pub(super) mod internals { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Status { /// The [`Scrollable`] can be interacted with. - Active, + Active { + /// Whether or not the horizontal scrollbar is disabled meaning the content isn't overflowing. + is_horizontal_scrollbar_disabled: bool, + /// Whether or not the vertical scrollbar is disabled meaning the content isn't overflowing. + is_vertical_scrollbar_disabled: bool, + }, /// The [`Scrollable`] is being hovered. Hovered { /// Indicates if the horizontal scrollbar is being hovered. is_horizontal_scrollbar_hovered: bool, /// Indicates if the vertical scrollbar is being hovered. is_vertical_scrollbar_hovered: bool, + /// Whether or not the horizontal scrollbar is disabled meaning the content isn't overflowing. + is_horizontal_scrollbar_disabled: bool, + /// Whether or not the vertical scrollbar is disabled meaning the content isn't overflowing. + is_vertical_scrollbar_disabled: bool, }, /// The [`Scrollable`] is being dragged. Dragged { @@ -1570,25 +1936,29 @@ pub enum Status { is_horizontal_scrollbar_dragged: bool, /// Indicates if the vertical scrollbar is being dragged. is_vertical_scrollbar_dragged: bool, + /// Whether or not the horizontal scrollbar is disabled meaning the content isn't overflowing. + is_horizontal_scrollbar_disabled: bool, + /// Whether or not the vertical scrollbar is disabled meaning the content isn't overflowing. + is_vertical_scrollbar_disabled: bool, }, } -/// The appearance of a scrolable. -#[derive(Debug, Clone, Copy)] +/// The appearance of a scrollable. +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`container::Style`] of a scrollable. pub container: container::Style, - /// The vertical [`Scrollbar`] appearance. - pub vertical_scrollbar: Scrollbar, - /// The horizontal [`Scrollbar`] appearance. - pub horizontal_scrollbar: Scrollbar, + /// The vertical [`Rail`] appearance. + pub vertical_rail: Rail, + /// The horizontal [`Rail`] appearance. + pub horizontal_rail: Rail, /// The [`Background`] of the gap between a horizontal and vertical scrollbar. pub gap: Option<Background>, } /// The appearance of the scrollbar of a scrollable. -#[derive(Debug, Clone, Copy)] -pub struct Scrollbar { +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Rail { /// The [`Background`] of a scrollbar. pub background: Option<Background>, /// The [`Border`] of a scrollbar. @@ -1598,7 +1968,7 @@ pub struct Scrollbar { } /// The appearance of the scroller of a scrollable. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Scroller { /// The [`Color`] of the scroller. pub color: Color, @@ -1637,27 +2007,28 @@ impl Catalog for Theme { pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); - let scrollbar = Scrollbar { + let scrollbar = Rail { background: Some(palette.background.weak.color.into()), - border: Border::rounded(2), + border: border::rounded(2), scroller: Scroller { color: palette.background.strong.color, - border: Border::rounded(2), + border: border::rounded(2), }, }; match status { - Status::Active => Style { + Status::Active { .. } => Style { container: container::Style::default(), - vertical_scrollbar: scrollbar, - horizontal_scrollbar: scrollbar, + vertical_rail: scrollbar, + horizontal_rail: scrollbar, gap: None, }, Status::Hovered { is_horizontal_scrollbar_hovered, is_vertical_scrollbar_hovered, + .. } => { - let hovered_scrollbar = Scrollbar { + let hovered_scrollbar = Rail { scroller: Scroller { color: palette.primary.strong.color, ..scrollbar.scroller @@ -1667,12 +2038,12 @@ pub fn default(theme: &Theme, status: Status) -> Style { Style { container: container::Style::default(), - vertical_scrollbar: if is_vertical_scrollbar_hovered { + vertical_rail: if is_vertical_scrollbar_hovered { hovered_scrollbar } else { scrollbar }, - horizontal_scrollbar: if is_horizontal_scrollbar_hovered { + horizontal_rail: if is_horizontal_scrollbar_hovered { hovered_scrollbar } else { scrollbar @@ -1683,8 +2054,9 @@ pub fn default(theme: &Theme, status: Status) -> Style { Status::Dragged { is_horizontal_scrollbar_dragged, is_vertical_scrollbar_dragged, + .. } => { - let dragged_scrollbar = Scrollbar { + let dragged_scrollbar = Rail { scroller: Scroller { color: palette.primary.base.color, ..scrollbar.scroller @@ -1694,12 +2066,12 @@ pub fn default(theme: &Theme, status: Status) -> Style { Style { container: container::Style::default(), - vertical_scrollbar: if is_vertical_scrollbar_dragged { + vertical_rail: if is_vertical_scrollbar_dragged { dragged_scrollbar } else { scrollbar }, - horizontal_scrollbar: if is_horizontal_scrollbar_dragged { + horizontal_rail: if is_horizontal_scrollbar_dragged { dragged_scrollbar } else { scrollbar diff --git a/widget/src/shader.rs b/widget/src/shader.rs index fad2f4eb..6d532e59 100644 --- a/widget/src/shader.rs +++ b/widget/src/shader.rs @@ -1,24 +1,21 @@ //! A custom shader widget for wgpu applications. -mod event; mod program; -pub use event::Event; pub use program::Program; -use crate::core; +use crate::core::event; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Widget}; -use crate::core::window; -use crate::core::{Clipboard, Element, Length, Rectangle, Shell, Size}; +use crate::core::{Clipboard, Element, Event, Length, Rectangle, Shell, Size}; use crate::renderer::wgpu::primitive; use std::marker::PhantomData; +pub use crate::Action; pub use crate::graphics::Viewport; -pub use crate::renderer::wgpu::wgpu; pub use primitive::{Primitive, Storage}; /// A widget which can render custom shaders with Iced's `wgpu` backend. @@ -88,50 +85,35 @@ where layout::atomic(limits, self.width, self.height) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: crate::core::Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let bounds = layout.bounds(); - let custom_shader_event = match event { - core::Event::Mouse(mouse_event) => Some(Event::Mouse(mouse_event)), - core::Event::Keyboard(keyboard_event) => { - Some(Event::Keyboard(keyboard_event)) - } - core::Event::Touch(touch_event) => Some(Event::Touch(touch_event)), - core::Event::Window(_, window::Event::RedrawRequested(instant)) => { - Some(Event::RedrawRequested(instant)) - } - _ => None, - }; + let state = tree.state.downcast_mut::<P::State>(); - if let Some(custom_shader_event) = custom_shader_event { - let state = tree.state.downcast_mut::<P::State>(); + if let Some(action) = self.program.update(state, event, bounds, cursor) + { + let (message, redraw_request, event_status) = action.into_inner(); - let (event_status, message) = self.program.update( - state, - custom_shader_event, - bounds, - cursor, - shell, - ); + shell.request_redraw_at(redraw_request); if let Some(message) = message { shell.publish(message); } - return event_status; + if event_status == event::Status::Captured { + shell.capture_event(); + } } - - event::Status::Ignored } fn mouse_interaction( @@ -192,12 +174,11 @@ where fn update( &self, state: &mut Self::State, - event: Event, + event: &Event, bounds: Rectangle, cursor: mouse::Cursor, - shell: &mut Shell<'_, Message>, - ) -> (event::Status, Option<Message>) { - T::update(self, state, event, bounds, cursor, shell) + ) -> Option<Action<Message>> { + T::update(self, state, event, bounds, cursor) } fn draw( diff --git a/widget/src/shader/event.rs b/widget/src/shader/event.rs deleted file mode 100644 index 005c8725..00000000 --- a/widget/src/shader/event.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Handle events of a custom shader widget. -use crate::core::keyboard; -use crate::core::mouse; -use crate::core::time::Instant; -use crate::core::touch; - -pub use crate::core::event::Status; - -/// A [`Shader`] event. -/// -/// [`Shader`]: crate::Shader -#[derive(Debug, Clone, PartialEq)] -pub enum Event { - /// A mouse event. - Mouse(mouse::Event), - - /// A touch event. - Touch(touch::Event), - - /// A keyboard event. - Keyboard(keyboard::Event), - - /// A window requested a redraw. - RedrawRequested(Instant), -} diff --git a/widget/src/shader/program.rs b/widget/src/shader/program.rs index 902c7c3b..bbea937c 100644 --- a/widget/src/shader/program.rs +++ b/widget/src/shader/program.rs @@ -1,8 +1,7 @@ -use crate::core::event; +use crate::core::Rectangle; use crate::core::mouse; -use crate::core::{Rectangle, Shell}; use crate::renderer::wgpu::Primitive; -use crate::shader; +use crate::shader::{self, Action}; /// The state and logic of a [`Shader`] widget. /// @@ -18,21 +17,20 @@ pub trait Program<Message> { type Primitive: Primitive + 'static; /// Update the internal [`State`] of the [`Program`]. This can be used to reflect state changes - /// based on mouse & other events. You can use the [`Shell`] to publish messages, request a - /// redraw for the window, etc. + /// based on mouse & other events. You can return an [`Action`] to publish a message, request a + /// redraw, or capture the event. /// - /// By default, this method does and returns nothing. + /// By default, this method returns `None`. /// /// [`State`]: Self::State fn update( &self, _state: &mut Self::State, - _event: shader::Event, + _event: &shader::Event, _bounds: Rectangle, _cursor: mouse::Cursor, - _shell: &mut Shell<'_, Message>, - ) -> (event::Status, Option<Message>) { - (event::Status::Ignored, None) + ) -> Option<Action<Message>> { + None } /// Draws the [`Primitive`]. diff --git a/widget/src/slider.rs b/widget/src/slider.rs index a8f1d192..1a2f8b9d 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -1,6 +1,34 @@ -//! Display an interactive selector of a single value from a range of values. -use crate::core::border; -use crate::core::event::{self, Event}; +//! Sliders let users set a value by moving an indicator. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::slider; +//! +//! struct State { +//! value: f32, +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! ValueChanged(f32), +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! slider(0.0..=100.0, state.value, Message::ValueChanged).into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::ValueChanged(value) => { +//! state.value = value; +//! } +//! } +//! } +//! ``` +use crate::core::border::{self, Border}; use crate::core::keyboard; use crate::core::keyboard::key::{self, Key}; use crate::core::layout; @@ -8,9 +36,10 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ - self, Border, Clipboard, Color, Element, Layout, Length, Pixels, Point, - Rectangle, Shell, Size, Theme, Widget, + self, Background, Clipboard, Color, Element, Event, Layout, Length, Pixels, + Point, Rectangle, Shell, Size, Theme, Widget, }; use std::ops::RangeInclusive; @@ -25,19 +54,32 @@ use std::ops::RangeInclusive; /// /// # Example /// ```no_run -/// # type Slider<'a, T, Message> = iced_widget::Slider<'a, Message, T>; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # -/// #[derive(Clone)] -/// pub enum Message { -/// SliderChanged(f32), +/// use iced::widget::slider; +/// +/// struct State { +/// value: f32, /// } /// -/// let value = 50.0; +/// #[derive(Debug, Clone)] +/// enum Message { +/// ValueChanged(f32), +/// } /// -/// Slider::new(0.0..=100.0, value, Message::SliderChanged); +/// fn view(state: &State) -> Element<'_, Message> { +/// slider(0.0..=100.0, state.value, Message::ValueChanged).into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::ValueChanged(value) => { +/// state.value = value; +/// } +/// } +/// } /// ``` -/// -/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true) #[allow(missing_debug_implementations)] pub struct Slider<'a, T, Message, Theme = crate::Theme> where @@ -53,6 +95,7 @@ where width: Length, height: f32, class: Theme::Class<'a>, + status: Option<Status>, } impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme> @@ -70,8 +113,8 @@ where /// * an inclusive range of possible values /// * the current value of the [`Slider`] /// * a function that will be called when the [`Slider`] is dragged. - /// It receives the new value of the [`Slider`] and must produce a - /// `Message`. + /// It receives the new value of the [`Slider`] and must produce a + /// `Message`. pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self where F: 'a + Fn(T) -> Message, @@ -99,6 +142,7 @@ where width: Length::Fill, height: Self::DEFAULT_HEIGHT, class: Theme::default(), + status: None, } } @@ -166,8 +210,8 @@ where } } -impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for Slider<'a, T, Message, Theme> +impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Slider<'_, T, Message, Theme> where T: Copy + Into<f64> + num_traits::FromPrimitive, Message: Clone, @@ -198,29 +242,53 @@ where layout::atomic(limits, self.width, self.height) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let state = tree.state.downcast_mut::<State>(); - let is_dragging = state.is_dragging; - let current_value = self.value; + let mut update = || { + let current_value = self.value; - let locate = |cursor_position: Point| -> Option<T> { - let bounds = layout.bounds(); - let new_value = if cursor_position.x <= bounds.x { - Some(*self.range.start()) - } else if cursor_position.x >= bounds.x + bounds.width { - Some(*self.range.end()) - } else { + let locate = |cursor_position: Point| -> Option<T> { + let bounds = layout.bounds(); + + let new_value = if cursor_position.x <= bounds.x { + Some(*self.range.start()) + } else if cursor_position.x >= bounds.x + bounds.width { + Some(*self.range.end()) + } else { + let step = if state.keyboard_modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + } + .into(); + + let start = (*self.range.start()).into(); + let end = (*self.range.end()).into(); + + let percent = f64::from(cursor_position.x - bounds.x) + / f64::from(bounds.width); + + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; + + T::from_f64(value.min(end)) + }; + + new_value + }; + + let increment = |value: T| -> Option<T> { let step = if state.keyboard_modifiers.shift() { self.shift_step.unwrap_or(self.step) } else { @@ -228,150 +296,158 @@ where } .into(); - let start = (*self.range.start()).into(); - let end = (*self.range.end()).into(); + let steps = (value.into() / step).round(); + let new_value = step * (steps + 1.0); - let percent = f64::from(cursor_position.x - bounds.x) - / f64::from(bounds.width); + if new_value > (*self.range.end()).into() { + return Some(*self.range.end()); + } - let steps = (percent * (end - start) / step).round(); - let value = steps * step + start; - - T::from_f64(value) + T::from_f64(new_value) }; - new_value - }; + let decrement = |value: T| -> Option<T> { + let step = if state.keyboard_modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + } + .into(); - let increment = |value: T| -> Option<T> { - let step = if state.keyboard_modifiers.shift() { - self.shift_step.unwrap_or(self.step) - } else { - self.step - } - .into(); + let steps = (value.into() / step).round(); + let new_value = step * (steps - 1.0); - let steps = (value.into() / step).round(); - let new_value = step * (steps + 1.0); + if new_value < (*self.range.start()).into() { + return Some(*self.range.start()); + } - if new_value > (*self.range.end()).into() { - return Some(*self.range.end()); - } + T::from_f64(new_value) + }; - T::from_f64(new_value) - }; + let change = |new_value: T| { + if (self.value.into() - new_value.into()).abs() > f64::EPSILON { + shell.publish((self.on_change)(new_value)); - let decrement = |value: T| -> Option<T> { - let step = if state.keyboard_modifiers.shift() { - self.shift_step.unwrap_or(self.step) - } else { - self.step - } - .into(); + self.value = new_value; + } + }; - let steps = (value.into() / step).round(); - let new_value = step * (steps - 1.0); + match &event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + if let Some(cursor_position) = + cursor.position_over(layout.bounds()) + { + if state.keyboard_modifiers.command() { + let _ = self.default.map(change); + state.is_dragging = false; + } else { + let _ = locate(cursor_position).map(change); + state.is_dragging = true; + } - if new_value < (*self.range.start()).into() { - return Some(*self.range.start()); - } - - T::from_f64(new_value) - }; - - let change = |new_value: T| { - if (self.value.into() - new_value.into()).abs() > f64::EPSILON { - shell.publish((self.on_change)(new_value)); - - self.value = new_value; - } - }; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(cursor_position) = - cursor.position_over(layout.bounds()) - { - if state.keyboard_modifiers.command() { - let _ = self.default.map(change); + shell.capture_event(); + } + } + Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if state.is_dragging { + if let Some(on_release) = self.on_release.clone() { + shell.publish(on_release); + } state.is_dragging = false; - } else { - let _ = locate(cursor_position).map(change); - state.is_dragging = true; + + shell.capture_event(); } - - return event::Status::Captured; } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - if is_dragging { - if let Some(on_release) = self.on_release.clone() { - shell.publish(on_release); + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if state.is_dragging { + let _ = cursor.position().and_then(locate).map(change); + + shell.capture_event(); } - state.is_dragging = false; - - return event::Status::Captured; } - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if is_dragging { - let _ = cursor.position().and_then(locate).map(change); + Event::Mouse(mouse::Event::WheelScrolled { delta }) + if state.keyboard_modifiers.control() => + { + if cursor.is_over(layout.bounds()) { + let delta = match delta { + mouse::ScrollDelta::Lines { x: _, y } => y, + mouse::ScrollDelta::Pixels { x: _, y } => y, + }; - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { - if cursor.position_over(layout.bounds()).is_some() { - match key { - Key::Named(key::Named::ArrowUp) => { + if *delta < 0.0 { + let _ = decrement(current_value).map(change); + } else { let _ = increment(current_value).map(change); } - Key::Named(key::Named::ArrowDown) => { - let _ = decrement(current_value).map(change); - } - _ => (), + + shell.capture_event(); } - - return event::Status::Captured; } - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - state.keyboard_modifiers = modifiers; - } - _ => {} - } + Event::Keyboard(keyboard::Event::KeyPressed { + key, .. + }) => { + if cursor.is_over(layout.bounds()) { + match key { + Key::Named(key::Named::ArrowUp) => { + let _ = increment(current_value).map(change); + } + Key::Named(key::Named::ArrowDown) => { + let _ = decrement(current_value).map(change); + } + _ => (), + } - event::Status::Ignored + shell.capture_event(); + } + } + Event::Keyboard(keyboard::Event::ModifiersChanged( + modifiers, + )) => { + state.keyboard_modifiers = *modifiers; + } + _ => {} + } + }; + + update(); + + let current_status = if state.is_dragging { + Status::Dragged + } else if cursor.is_over(layout.bounds()) { + Status::Hovered + } else { + Status::Active + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.status = Some(current_status); + } else if self.status.is_some_and(|status| status != current_status) { + shell.request_redraw(); + } } fn draw( &self, - tree: &Tree, + _tree: &Tree, renderer: &mut Renderer, theme: &Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor: mouse::Cursor, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { - let state = tree.state.downcast_ref::<State>(); let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - let style = theme.style( - &self.class, - if state.is_dragging { - Status::Dragged - } else if is_mouse_over { - Status::Hovered - } else { - Status::Active - }, - ); + let style = + theme.style(&self.class, self.status.unwrap_or(Status::Active)); let (handle_width, handle_height, handle_border_radius) = match style.handle.shape { @@ -408,10 +484,10 @@ where width: offset + handle_width / 2.0, height: style.rail.width, }, - border: Border::rounded(style.rail.border_radius), + border: style.rail.border, ..renderer::Quad::default() }, - style.rail.colors.0, + style.rail.backgrounds.0, ); renderer.fill_quad( @@ -422,10 +498,10 @@ where width: bounds.width - offset - handle_width / 2.0, height: style.rail.width, }, - border: Border::rounded(style.rail.border_radius), + border: style.rail.border, ..renderer::Quad::default() }, - style.rail.colors.1, + style.rail.backgrounds.1, ); renderer.fill_quad( @@ -443,7 +519,7 @@ where }, ..renderer::Quad::default() }, - style.handle.color, + style.handle.background, ); } @@ -502,7 +578,7 @@ pub enum Status { } /// The appearance of a slider. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The colors of the rail of the slider. pub rail: Rail, @@ -522,23 +598,23 @@ impl Style { } /// The appearance of a slider rail -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Rail { - /// The colors of the rail of the slider. - pub colors: (Color, Color), + /// The backgrounds of the rail of the slider. + pub backgrounds: (Background, Background), /// The width of the stroke of a slider rail. pub width: f32, - /// The border radius of the corners of the rail. - pub border_radius: border::Radius, + /// The border of the rail. + pub border: Border, } /// The appearance of the handle of a slider. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Handle { /// The shape of the handle. pub shape: HandleShape, - /// The [`Color`] of the handle. - pub color: Color, + /// The [`Background`] of the handle. + pub background: Background, /// The border width of the handle. pub border_width: f32, /// The border [`Color`] of the handle. @@ -546,7 +622,7 @@ pub struct Handle { } /// The shape of the handle of a slider. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum HandleShape { /// A circular handle. Circle { @@ -594,20 +670,24 @@ pub fn default(theme: &Theme, status: Status) -> Style { let palette = theme.extended_palette(); let color = match status { - Status::Active => palette.primary.strong.color, - Status::Hovered => palette.primary.base.color, - Status::Dragged => palette.primary.strong.color, + Status::Active => palette.primary.base.color, + Status::Hovered => palette.primary.strong.color, + Status::Dragged => palette.primary.weak.color, }; Style { rail: Rail { - colors: (color, palette.secondary.base.color), + backgrounds: (color.into(), palette.background.strong.color.into()), width: 4.0, - border_radius: 2.0.into(), + border: Border { + radius: 2.0.into(), + width: 0.0, + color: Color::TRANSPARENT, + }, }, handle: Handle { shape: HandleShape::Circle { radius: 7.0 }, - color, + background: color.into(), border_color: Color::TRANSPARENT, border_width: 0.0, }, diff --git a/widget/src/stack.rs b/widget/src/stack.rs index 5035541b..df9f6162 100644 --- a/widget/src/stack.rs +++ b/widget/src/stack.rs @@ -1,12 +1,12 @@ //! Display content on top of other content. -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; use crate::core::widget::{Operation, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Rectangle, Shell, Size, Vector, Widget, + Clipboard, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector, + Widget, }; /// A container that displays children on top of each other. @@ -116,7 +116,7 @@ where } } -impl<'a, Message, Renderer> Default for Stack<'a, Message, Renderer> +impl<Message, Renderer> Default for Stack<'_, Message, Renderer> where Renderer: crate::core::Renderer, { @@ -189,7 +189,7 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation, ) { operation.container(None, layout.bounds(), &mut |operation| { self.children @@ -204,36 +204,47 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, - cursor: mouse::Cursor, + mut cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.children + ) { + let is_over = cursor.is_over(layout.bounds()); + let end = self.children.len() - 1; + + for (i, ((child, state), layout)) in self + .children .iter_mut() .rev() .zip(tree.children.iter_mut().rev()) .zip(layout.children().rev()) - .map(|((child, state), layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ) - }) - .find(|&status| status == event::Status::Captured) - .unwrap_or(event::Status::Ignored) + .enumerate() + { + child.as_widget_mut().update( + state, event, layout, cursor, renderer, clipboard, shell, + viewport, + ); + + if shell.is_event_captured() { + return; + } + + if i < end && is_over && !cursor.is_levitating() { + let interaction = child.as_widget().mouse_interaction( + state, layout, cursor, viewport, renderer, + ); + + if interaction != mouse::Interaction::None { + cursor = cursor.levitate(); + } + } + } } fn mouse_interaction( @@ -269,15 +280,53 @@ where viewport: &Rectangle, ) { if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { - for (i, ((layer, state), layout)) in self + let layers_below = if cursor.is_over(layout.bounds()) { + self.children + .iter() + .rev() + .zip(tree.children.iter().rev()) + .zip(layout.children().rev()) + .position(|((layer, state), layout)| { + let interaction = layer.as_widget().mouse_interaction( + state, layout, cursor, viewport, renderer, + ); + + interaction != mouse::Interaction::None + }) + .map(|i| self.children.len() - i - 1) + .unwrap_or_default() + } else { + 0 + }; + + let mut layers = self .children .iter() .zip(&tree.children) .zip(layout.children()) - .enumerate() - { - if i > 0 { - renderer.with_layer(clipped_viewport, |renderer| { + .enumerate(); + + let layers = layers.by_ref(); + + let mut draw_layer = + |i, + layer: &Element<'a, Message, Theme, Renderer>, + state, + layout, + cursor| { + if i > 0 { + renderer.with_layer(clipped_viewport, |renderer| { + layer.as_widget().draw( + state, + renderer, + theme, + style, + layout, + cursor, + &clipped_viewport, + ); + }); + } else { layer.as_widget().draw( state, renderer, @@ -287,18 +336,15 @@ where cursor, &clipped_viewport, ); - }); - } else { - layer.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor, - &clipped_viewport, - ); - } + } + }; + + for (i, ((layer, state), layout)) in layers.take(layers_below) { + draw_layer(i, layer, state, layout, mouse::Cursor::Unavailable); + } + + for (i, ((layer, state), layout)) in layers { + draw_layer(i, layer, state, layout, cursor); } } } diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 4551bcad..72ead4f9 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -1,4 +1,20 @@ -//! Display vector graphics in your application. +//! Svg widgets display vector graphics in your application. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::svg; +//! +//! enum Message { +//! // ... +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! svg("tiger.svg").into() +//! } +//! ``` use crate::core::layout; use crate::core::mouse; use crate::core::renderer; @@ -19,6 +35,22 @@ pub use crate::core::svg::Handle; /// /// [`Svg`] images can have a considerable rendering cost when resized, /// specially when they are complex. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::svg; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// svg("tiger.svg").into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Svg<'a, Theme = crate::Theme> where @@ -116,8 +148,8 @@ where } } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for Svg<'a, Theme> +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Svg<'_, Theme> where Renderer: svg::Renderer, Theme: Catalog, @@ -211,11 +243,13 @@ where let render = |renderer: &mut Renderer| { renderer.draw_svg( - self.handle.clone(), - style.color, + svg::Svg { + handle: self.handle.clone(), + color: style.color, + rotation: self.rotation.radians(), + opacity: self.opacity, + }, drawing_bounds, - self.rotation.radians(), - self.opacity, ); }; @@ -289,7 +323,7 @@ impl Catalog for Theme { /// This is just a boxed closure: `Fn(&Theme, Status) -> Style`. pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>; -impl<'a, Theme> From<Style> for StyleFn<'a, Theme> { +impl<Theme> From<Style> for StyleFn<'_, Theme> { fn from(style: Style) -> Self { Box::new(move |_theme, _status| style) } diff --git a/widget/src/text.rs b/widget/src/text.rs index 0d689295..c2243434 100644 --- a/widget/src/text.rs +++ b/widget/src/text.rs @@ -1,6 +1,30 @@ //! Draw and interact with text. -pub use crate::core::widget::text::*; +mod rich; -/// A paragraph. +pub use crate::core::text::{Fragment, Highlighter, IntoFragment, Span}; +pub use crate::core::widget::text::*; +pub use rich::Rich; + +/// A bunch of text. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// 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() +/// } +/// ``` pub type Text<'a, Theme = crate::Theme, Renderer = crate::Renderer> = crate::core::widget::Text<'a, Theme, Renderer>; diff --git a/widget/src/text/rich.rs b/widget/src/text/rich.rs new file mode 100644 index 00000000..4d4a2861 --- /dev/null +++ b/widget/src/text/rich.rs @@ -0,0 +1,566 @@ +use crate::core::alignment; +use crate::core::layout; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::text::{Paragraph, Span}; +use crate::core::widget::text::{ + self, Catalog, LineHeight, Shaping, Style, StyleFn, Wrapping, +}; +use crate::core::widget::tree::{self, Tree}; +use crate::core::{ + self, Clipboard, Color, Element, Event, Layout, Length, Pixels, Point, + Rectangle, Shell, Size, Vector, Widget, +}; + +/// A bunch of [`Rich`] text. +#[allow(missing_debug_implementations)] +pub struct Rich< + 'a, + Link, + Message, + Theme = crate::Theme, + Renderer = crate::Renderer, +> where + Link: Clone + 'static, + Theme: Catalog, + Renderer: core::text::Renderer, +{ + spans: Box<dyn AsRef<[Span<'a, Link, Renderer::Font>]> + 'a>, + size: Option<Pixels>, + line_height: LineHeight, + width: Length, + height: Length, + font: Option<Renderer::Font>, + align_x: alignment::Horizontal, + align_y: alignment::Vertical, + wrapping: Wrapping, + class: Theme::Class<'a>, + hovered_link: Option<usize>, + on_link_click: Option<Box<dyn Fn(Link) -> Message + 'a>>, +} + +impl<'a, Link, Message, Theme, Renderer> + Rich<'a, Link, Message, Theme, Renderer> +where + Link: Clone + 'static, + Theme: Catalog, + Renderer: core::text::Renderer, + Renderer::Font: 'a, +{ + /// Creates a new empty [`Rich`] text. + pub fn new() -> Self { + Self { + spans: Box::new([]), + size: None, + line_height: LineHeight::default(), + width: Length::Shrink, + height: Length::Shrink, + font: None, + align_x: alignment::Horizontal::Left, + align_y: alignment::Vertical::Top, + wrapping: Wrapping::default(), + class: Theme::default(), + hovered_link: None, + on_link_click: None, + } + } + + /// Creates a new [`Rich`] text with the given text spans. + pub fn with_spans( + spans: impl AsRef<[Span<'a, Link, Renderer::Font>]> + 'a, + ) -> Self { + Self { + spans: Box::new(spans), + ..Self::new() + } + } + + /// Sets the default size of the [`Rich`] text. + pub fn size(mut self, size: impl Into<Pixels>) -> Self { + self.size = Some(size.into()); + self + } + + /// Sets the default [`LineHeight`] of the [`Rich`] text. + pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self { + self.line_height = line_height.into(); + self + } + + /// Sets the default font of the [`Rich`] text. + pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self { + self.font = Some(font.into()); + self + } + + /// Sets the width of the [`Rich`] text boundaries. + pub fn width(mut self, width: impl Into<Length>) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Rich`] text boundaries. + pub fn height(mut self, height: impl Into<Length>) -> Self { + self.height = height.into(); + self + } + + /// Centers the [`Rich`] 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 [`Rich`] text. + pub fn align_x( + mut self, + alignment: impl Into<alignment::Horizontal>, + ) -> Self { + self.align_x = alignment.into(); + self + } + + /// Sets the [`alignment::Vertical`] of the [`Rich`] text. + pub fn align_y( + mut self, + alignment: impl Into<alignment::Vertical>, + ) -> Self { + self.align_y = alignment.into(); + self + } + + /// Sets the [`Wrapping`] strategy of the [`Rich`] text. + pub fn wrapping(mut self, wrapping: Wrapping) -> Self { + self.wrapping = wrapping; + self + } + + /// Sets the message that will be produced when a link of the [`Rich`] text + /// is clicked. + pub fn on_link_click( + mut self, + on_link_clicked: impl Fn(Link) -> Message + 'a, + ) -> Self { + self.on_link_click = Some(Box::new(on_link_clicked)); + self + } + + /// Sets the default style of the [`Rich`] text. + #[must_use] + pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.class = (Box::new(style) as StyleFn<'a, Theme>).into(); + self + } + + /// Sets the default [`Color`] of the [`Rich`] text. + pub fn color(self, color: impl Into<Color>) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + self.color_maybe(Some(color)) + } + + /// Sets the default [`Color`] of the [`Rich`] text, if `Some`. + pub fn color_maybe(self, color: Option<impl Into<Color>>) -> Self + where + Theme::Class<'a>: From<StyleFn<'a, Theme>>, + { + let color = color.map(Into::into); + + self.style(move |_theme| Style { color }) + } + + /// Sets the default style class of the [`Rich`] text. + #[cfg(feature = "advanced")] + #[must_use] + pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self { + self.class = class.into(); + self + } +} + +impl<'a, Link, Message, Theme, Renderer> Default + for Rich<'a, Link, Message, Theme, Renderer> +where + Link: Clone + 'a, + Theme: Catalog, + Renderer: core::text::Renderer, + Renderer::Font: 'a, +{ + fn default() -> Self { + Self::new() + } +} + +struct State<Link, P: Paragraph> { + spans: Vec<Span<'static, Link, P::Font>>, + span_pressed: Option<usize>, + paragraph: P, +} + +impl<Link, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Rich<'_, Link, Message, Theme, Renderer> +where + Link: Clone + 'static, + Theme: Catalog, + Renderer: core::text::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::<State<Link, Renderer::Paragraph>>() + } + + fn state(&self) -> tree::State { + tree::State::new(State::<Link, _> { + spans: Vec::new(), + span_pressed: None, + paragraph: Renderer::Paragraph::default(), + }) + } + + fn size(&self) -> Size<Length> { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout( + tree.state + .downcast_mut::<State<Link, Renderer::Paragraph>>(), + renderer, + limits, + self.width, + self.height, + self.spans.as_ref().as_ref(), + self.line_height, + self.size, + self.font, + self.align_x, + self.align_y, + self.wrapping, + ) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + defaults: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if !layout.bounds().intersects(viewport) { + return; + } + + let state = tree + .state + .downcast_ref::<State<Link, Renderer::Paragraph>>(); + + let style = theme.style(&self.class); + + for (index, span) in self.spans.as_ref().as_ref().iter().enumerate() { + let is_hovered_link = self.on_link_click.is_some() + && Some(index) == self.hovered_link; + + if span.highlight.is_some() + || span.underline + || span.strikethrough + || is_hovered_link + { + let translation = layout.position() - Point::ORIGIN; + let regions = state.paragraph.span_bounds(index); + + if let Some(highlight) = span.highlight { + for bounds in ®ions { + let bounds = Rectangle::new( + bounds.position() + - Vector::new( + span.padding.left, + span.padding.top, + ), + bounds.size() + + Size::new( + span.padding.horizontal(), + span.padding.vertical(), + ), + ); + + renderer.fill_quad( + renderer::Quad { + bounds: bounds + translation, + border: highlight.border, + ..Default::default() + }, + highlight.background, + ); + } + } + + if span.underline || span.strikethrough || is_hovered_link { + let size = span + .size + .or(self.size) + .unwrap_or(renderer.default_size()); + + let line_height = span + .line_height + .unwrap_or(self.line_height) + .to_absolute(size); + + let color = span + .color + .or(style.color) + .unwrap_or(defaults.text_color); + + let baseline = translation + + Vector::new( + 0.0, + size.0 + (line_height.0 - size.0) / 2.0, + ); + + if span.underline || is_hovered_link { + for bounds in ®ions { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle::new( + bounds.position() + baseline + - Vector::new(0.0, size.0 * 0.08), + Size::new(bounds.width, 1.0), + ), + ..Default::default() + }, + color, + ); + } + } + + if span.strikethrough { + for bounds in ®ions { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle::new( + bounds.position() + baseline + - Vector::new(0.0, size.0 / 2.0), + Size::new(bounds.width, 1.0), + ), + ..Default::default() + }, + color, + ); + } + } + } + } + } + + text::draw( + renderer, + defaults, + layout, + &state.paragraph, + style, + viewport, + ); + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) { + let Some(on_link_clicked) = &self.on_link_click else { + return; + }; + + let was_hovered = self.hovered_link.is_some(); + + if let Some(position) = cursor.position_in(layout.bounds()) { + let state = tree + .state + .downcast_ref::<State<Link, Renderer::Paragraph>>(); + + self.hovered_link = + state.paragraph.hit_span(position).and_then(|span| { + if self.spans.as_ref().as_ref().get(span)?.link.is_some() { + Some(span) + } else { + None + } + }); + } else { + self.hovered_link = None; + } + + if was_hovered != self.hovered_link.is_some() { + shell.request_redraw(); + } + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let state = tree + .state + .downcast_mut::<State<Link, Renderer::Paragraph>>(); + + if self.hovered_link.is_some() { + state.span_pressed = self.hovered_link; + shell.capture_event(); + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + let state = tree + .state + .downcast_mut::<State<Link, Renderer::Paragraph>>(); + + match state.span_pressed { + Some(span) if Some(span) == self.hovered_link => { + if let Some(link) = self + .spans + .as_ref() + .as_ref() + .get(span) + .and_then(|span| span.link.clone()) + { + shell.publish(on_link_clicked(link)); + } + } + _ => {} + } + + state.span_pressed = None; + } + _ => {} + } + } + + fn mouse_interaction( + &self, + _tree: &Tree, + _layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + if self.hovered_link.is_some() { + mouse::Interaction::Pointer + } else { + mouse::Interaction::None + } + } +} + +fn layout<Link, Renderer>( + state: &mut State<Link, Renderer::Paragraph>, + renderer: &Renderer, + limits: &layout::Limits, + width: Length, + height: Length, + spans: &[Span<'_, Link, Renderer::Font>], + line_height: LineHeight, + size: Option<Pixels>, + font: Option<Renderer::Font>, + horizontal_alignment: alignment::Horizontal, + vertical_alignment: alignment::Vertical, + wrapping: Wrapping, +) -> layout::Node +where + Link: Clone, + Renderer: core::text::Renderer, +{ + layout::sized(limits, width, height, |limits| { + let bounds = limits.max(); + + let size = size.unwrap_or_else(|| renderer.default_size()); + let font = font.unwrap_or_else(|| renderer.default_font()); + + let text_with_spans = || core::Text { + content: spans, + bounds, + size, + line_height, + font, + horizontal_alignment, + vertical_alignment, + shaping: Shaping::Advanced, + wrapping, + }; + + if state.spans != spans { + state.paragraph = + Renderer::Paragraph::with_spans(text_with_spans()); + state.spans = spans.iter().cloned().map(Span::to_static).collect(); + } else { + match state.paragraph.compare(core::Text { + content: (), + bounds, + size, + line_height, + font, + horizontal_alignment, + vertical_alignment, + shaping: Shaping::Advanced, + wrapping, + }) { + core::text::Difference::None => {} + core::text::Difference::Bounds => { + state.paragraph.resize(bounds); + } + core::text::Difference::Shape => { + state.paragraph = + Renderer::Paragraph::with_spans(text_with_spans()); + } + } + } + + state.paragraph.min_bounds() + }) +} + +impl<'a, Link, Message, Theme, Renderer> + FromIterator<Span<'a, Link, Renderer::Font>> + for Rich<'a, Link, Message, Theme, Renderer> +where + Link: Clone + 'a, + Theme: Catalog, + Renderer: core::text::Renderer, + Renderer::Font: 'a, +{ + fn from_iter<T: IntoIterator<Item = Span<'a, Link, Renderer::Font>>>( + spans: T, + ) -> Self { + Self::with_spans(spans.into_iter().collect::<Vec<_>>()) + } +} + +impl<'a, Link, Message, Theme, Renderer> + From<Rich<'a, Link, Message, Theme, Renderer>> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Link: Clone + 'a, + Theme: Catalog + 'a, + Renderer: core::text::Renderer + 'a, +{ + fn from( + text: Rich<'a, Link, Message, Theme, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(text) + } +} diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index 7c0b98ea..7e40a56a 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -1,6 +1,39 @@ -//! Display a multi-line text input for text editing. +//! Text editors display a multi-line text input for text editing. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::text_editor; +//! +//! struct State { +//! content: text_editor::Content, +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! Edit(text_editor::Action) +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! text_editor(&state.content) +//! .placeholder("Type something here...") +//! .on_action(Message::Edit) +//! .into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::Edit(action) => { +//! state.content.perform(action); +//! } +//! } +//! } +//! ``` +use crate::core::alignment; use crate::core::clipboard::{self, Clipboard}; -use crate::core::event::{self, Event}; +use crate::core::input_method; use crate::core::keyboard; use crate::core::keyboard::key; use crate::core::layout::{self, Layout}; @@ -8,21 +41,58 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::text::editor::{Cursor, Editor as _}; use crate::core::text::highlighter::{self, Highlighter}; -use crate::core::text::{self, LineHeight}; +use crate::core::text::{self, LineHeight, Text, Wrapping}; +use crate::core::time::{Duration, Instant}; +use crate::core::widget::operation; use crate::core::widget::{self, Widget}; +use crate::core::window; use crate::core::{ - Background, Border, Color, Element, Length, Padding, Pixels, Rectangle, - Shell, Size, Theme, Vector, + Background, Border, Color, Element, Event, InputMethod, Length, Padding, + Pixels, Point, Rectangle, Shell, Size, SmolStr, Theme, Vector, }; +use std::borrow::Cow; use std::cell::RefCell; use std::fmt; use std::ops::DerefMut; +use std::ops::Range; use std::sync::Arc; -pub use text::editor::{Action, Edit, Motion}; +pub use text::editor::{Action, Edit, Line, LineEnding, Motion}; /// A multi-line text input. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// # +/// use iced::widget::text_editor; +/// +/// struct State { +/// content: text_editor::Content, +/// } +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// Edit(text_editor::Action) +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// text_editor(&state.content) +/// .placeholder("Type something here...") +/// .on_action(Message::Edit) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::Edit(action) => { +/// state.content.perform(action); +/// } +/// } +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct TextEditor< 'a, @@ -36,19 +106,25 @@ pub struct TextEditor< Renderer: text::Renderer, { content: &'a Content<Renderer>, + placeholder: Option<text::Fragment<'a>>, font: Option<Renderer::Font>, text_size: Option<Pixels>, line_height: LineHeight, width: Length, height: Length, + min_height: f32, + max_height: f32, padding: Padding, + wrapping: Wrapping, class: Theme::Class<'a>, + key_binding: Option<Box<dyn Fn(KeyPress) -> Option<Binding<Message>> + 'a>>, on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>, highlighter_settings: Highlighter::Settings, highlighter_format: fn( &Highlighter::Highlight, &Theme, ) -> highlighter::Format<Renderer::Font>, + last_status: Option<Status>, } impl<'a, Message, Theme, Renderer> @@ -61,18 +137,24 @@ where pub fn new(content: &'a Content<Renderer>) -> Self { Self { content, + placeholder: None, font: None, text_size: None, line_height: LineHeight::default(), width: Length::Fill, height: Length::Shrink, + min_height: 0.0, + max_height: f32::INFINITY, padding: Padding::new(5.0), + wrapping: Wrapping::default(), class: Theme::default(), + key_binding: None, on_edit: None, highlighter_settings: (), highlighter_format: |_highlight, _theme| { highlighter::Format::default() }, + last_status: None, } } } @@ -84,12 +166,39 @@ where Theme: Catalog, Renderer: text::Renderer, { + /// Sets the placeholder of the [`TextEditor`]. + pub fn placeholder( + mut self, + placeholder: impl text::IntoFragment<'a>, + ) -> Self { + self.placeholder = Some(placeholder.into_fragment()); + self + } + + /// Sets the width of the [`TextEditor`]. + pub fn width(mut self, width: impl Into<Pixels>) -> Self { + self.width = Length::from(width.into()); + self + } + /// Sets the height of the [`TextEditor`]. pub fn height(mut self, height: impl Into<Length>) -> Self { self.height = height.into(); self } + /// Sets the minimum height of the [`TextEditor`]. + pub fn min_height(mut self, min_height: impl Into<Pixels>) -> Self { + self.min_height = min_height.into().0; + self + } + + /// Sets the maximum height of the [`TextEditor`]. + pub fn max_height(mut self, max_height: impl Into<Pixels>) -> Self { + self.max_height = max_height.into().0; + self + } + /// Sets the message that should be produced when some action is performed in /// the [`TextEditor`]. /// @@ -131,9 +240,34 @@ where self } + /// Sets the [`Wrapping`] strategy of the [`TextEditor`]. + pub fn wrapping(mut self, wrapping: Wrapping) -> Self { + self.wrapping = wrapping; + self + } + + /// Highlights the [`TextEditor`] using the given syntax and theme. + #[cfg(feature = "highlighter")] + pub fn highlight( + self, + syntax: &str, + theme: iced_highlighter::Theme, + ) -> TextEditor<'a, iced_highlighter::Highlighter, Message, Theme, Renderer> + where + Renderer: text::Renderer<Font = crate::core::Font>, + { + self.highlight_with::<iced_highlighter::Highlighter>( + iced_highlighter::Settings { + theme, + token: syntax.to_owned(), + }, + |highlight, _theme| highlight.to_format(), + ) + } + /// Highlights the [`TextEditor`] with the given [`Highlighter`] and /// a strategy to turn its highlights into some text format. - pub fn highlight<H: text::Highlighter>( + pub fn highlight_with<H: text::Highlighter>( self, settings: H::Settings, to_format: fn( @@ -143,19 +277,36 @@ where ) -> TextEditor<'a, H, Message, Theme, Renderer> { TextEditor { content: self.content, + placeholder: self.placeholder, font: self.font, text_size: self.text_size, line_height: self.line_height, width: self.width, height: self.height, + min_height: self.min_height, + max_height: self.max_height, padding: self.padding, + wrapping: self.wrapping, class: self.class, + key_binding: self.key_binding, on_edit: self.on_edit, highlighter_settings: settings, highlighter_format: to_format, + last_status: self.last_status, } } + /// Sets the closure to produce key bindings on key presses. + /// + /// See [`Binding`] for the list of available bindings. + pub fn key_binding( + mut self, + key_binding: impl Fn(KeyPress) -> Option<Binding<Message>> + 'a, + ) -> Self { + self.key_binding = Some(Box::new(key_binding)); + self + } + /// Sets the style of the [`TextEditor`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self @@ -173,6 +324,47 @@ where self.class = class.into(); self } + + fn input_method<'b>( + &self, + state: &'b State<Highlighter>, + renderer: &Renderer, + layout: Layout<'_>, + ) -> InputMethod<&'b str> { + let Some(Focus { + is_window_focused: true, + .. + }) = &state.focus + else { + return InputMethod::Disabled; + }; + + let bounds = layout.bounds(); + let internal = self.content.0.borrow_mut(); + + let text_bounds = bounds.shrink(self.padding); + let translation = text_bounds.position() - Point::ORIGIN; + + let cursor = match internal.editor.cursor() { + Cursor::Caret(position) => position, + Cursor::Selection(ranges) => { + ranges.first().cloned().unwrap_or_default().position() + } + }; + + let line_height = self.line_height.to_absolute( + self.text_size.unwrap_or_else(|| renderer.default_size()), + ); + + let position = + cursor + translation + Vector::new(0.0, f32::from(line_height)); + + InputMethod::Enabled { + position, + purpose: input_method::Purpose::Normal, + preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref), + } + } } /// The content of a [`TextEditor`]. @@ -219,69 +411,47 @@ where } /// Returns the text of the line at the given index, if it exists. - pub fn line( - &self, - index: usize, - ) -> Option<impl std::ops::Deref<Target = str> + '_> { - std::cell::Ref::filter_map(self.0.borrow(), |internal| { - internal.editor.line(index) + pub fn line(&self, index: usize) -> Option<Line<'_>> { + let internal = self.0.borrow(); + let line = internal.editor.line(index)?; + + Some(Line { + text: Cow::Owned(line.text.into_owned()), + ending: line.ending, }) - .ok() } /// Returns an iterator of the text of the lines in the [`Content`]. - pub fn lines( - &self, - ) -> impl Iterator<Item = impl std::ops::Deref<Target = str> + '_> { - struct Lines<'a, Renderer: text::Renderer> { - internal: std::cell::Ref<'a, Internal<Renderer>>, - current: usize, - } - - impl<'a, Renderer: text::Renderer> Iterator for Lines<'a, Renderer> { - type Item = std::cell::Ref<'a, str>; - - fn next(&mut self) -> Option<Self::Item> { - let line = std::cell::Ref::filter_map( - std::cell::Ref::clone(&self.internal), - |internal| internal.editor.line(self.current), - ) - .ok()?; - - self.current += 1; - - Some(line) - } - } - - Lines { - internal: self.0.borrow(), - current: 0, - } + pub fn lines(&self) -> impl Iterator<Item = Line<'_>> { + (0..) + .map(|i| self.line(i)) + .take_while(Option::is_some) + .flatten() } /// Returns the text of the [`Content`]. - /// - /// Lines are joined with `'\n'`. pub fn text(&self) -> String { - let mut text = self.lines().enumerate().fold( - String::new(), - |mut contents, (i, line)| { - if i > 0 { - contents.push('\n'); - } + let mut contents = String::new(); + let mut lines = self.lines().peekable(); - contents.push_str(&line); + while let Some(line) = lines.next() { + contents.push_str(&line.text); - contents - }, - ); - - if !text.ends_with('\n') { - text.push('\n'); + if lines.peek().is_some() { + contents.push_str(if line.ending == LineEnding::None { + LineEnding::default().as_str() + } else { + line.ending.as_str() + }); + } } - text + contents + } + + /// Returns the kind of [`LineEnding`] used for separating lines in the [`Content`]. + pub fn line_ending(&self) -> Option<LineEnding> { + Some(self.line(0)?.ending) } /// Returns the selected text of the [`Content`]. @@ -322,7 +492,8 @@ where /// The state of a [`TextEditor`]. #[derive(Debug)] pub struct State<Highlighter: text::Highlighter> { - is_focused: bool, + focus: Option<Focus>, + preedit: Option<input_method::Preedit>, last_click: Option<mouse::Click>, drag_click: Option<mouse::click::Kind>, partial_scroll: f32, @@ -331,15 +502,60 @@ pub struct State<Highlighter: text::Highlighter> { highlighter_format_address: usize, } -impl<Highlighter: text::Highlighter> State<Highlighter> { - /// Returns whether the [`TextEditor`] is currently focused or not. - pub fn is_focused(&self) -> bool { - self.is_focused +#[derive(Debug, Clone)] +struct Focus { + updated_at: Instant, + now: Instant, + is_window_focused: bool, +} + +impl Focus { + const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; + + fn now() -> Self { + let now = Instant::now(); + + Self { + updated_at: now, + now, + is_window_focused: true, + } + } + + fn is_cursor_visible(&self) -> bool { + self.is_window_focused + && ((self.now - self.updated_at).as_millis() + / Self::CURSOR_BLINK_INTERVAL_MILLIS) + % 2 + == 0 } } -impl<'a, Highlighter, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for TextEditor<'a, Highlighter, Message, Theme, Renderer> +impl<Highlighter: text::Highlighter> State<Highlighter> { + /// Returns whether the [`TextEditor`] is currently focused or not. + pub fn is_focused(&self) -> bool { + self.focus.is_some() + } +} + +impl<Highlighter: text::Highlighter> operation::Focusable + for State<Highlighter> +{ + fn is_focused(&self) -> bool { + self.focus.is_some() + } + + fn focus(&mut self) { + self.focus = Some(Focus::now()); + } + + fn unfocus(&mut self) { + self.focus = None; + } +} + +impl<Highlighter, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for TextEditor<'_, Highlighter, Message, Theme, Renderer> where Highlighter: text::Highlighter, Theme: Catalog, @@ -351,7 +567,8 @@ where fn state(&self) -> widget::tree::State { widget::tree::State::new(State { - is_focused: false, + focus: None, + preedit: None, last_click: None, drag_click: None, partial_scroll: 0.0, @@ -395,13 +612,18 @@ where state.highlighter_settings = self.highlighter_settings.clone(); } - let limits = limits.height(self.height); + let limits = limits + .width(self.width) + .height(self.height) + .min_height(self.min_height) + .max_height(self.max_height); internal.editor.update( limits.shrink(self.padding).max(), self.font.unwrap_or_else(|| renderer.default_font()), self.text_size.unwrap_or_else(|| renderer.default_size()), self.line_height, + self.wrapping, state.highlighter.borrow_mut().deref_mut(), ); @@ -422,90 +644,267 @@ where } } - fn on_event( + fn update( &mut self, tree: &mut widget::Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, - _renderer: &Renderer, + renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let Some(on_edit) = self.on_edit.as_ref() else { - return event::Status::Ignored; + return; }; let state = tree.state.downcast_mut::<State<Highlighter>>(); + let is_redraw = matches!( + event, + Event::Window(window::Event::RedrawRequested(_now)), + ); - let Some(update) = Update::from_event( + match event { + Event::Window(window::Event::Unfocused) => { + if let Some(focus) = &mut state.focus { + focus.is_window_focused = false; + } + } + Event::Window(window::Event::Focused) => { + if let Some(focus) = &mut state.focus { + focus.is_window_focused = true; + focus.updated_at = Instant::now(); + + shell.request_redraw(); + } + } + Event::Window(window::Event::RedrawRequested(now)) => { + if let Some(focus) = &mut state.focus { + if focus.is_window_focused { + focus.now = *now; + + let millis_until_redraw = + Focus::CURSOR_BLINK_INTERVAL_MILLIS + - (focus.now - focus.updated_at).as_millis() + % Focus::CURSOR_BLINK_INTERVAL_MILLIS; + + shell.request_redraw_at( + focus.now + + Duration::from_millis( + millis_until_redraw as u64, + ), + ); + } + } + } + _ => {} + } + + if let Some(update) = Update::from_event( event, state, layout.bounds(), self.padding, cursor, - ) else { - return event::Status::Ignored; - }; + self.key_binding.as_deref(), + ) { + match update { + Update::Click(click) => { + let action = match click.kind() { + mouse::click::Kind::Single => { + Action::Click(click.position()) + } + mouse::click::Kind::Double => Action::SelectWord, + mouse::click::Kind::Triple => Action::SelectLine, + }; - match update { - Update::Click(click) => { - let action = match click.kind() { - mouse::click::Kind::Single => { - Action::Click(click.position()) + state.focus = Some(Focus::now()); + state.last_click = Some(click); + state.drag_click = Some(click.kind()); + + shell.publish(on_edit(action)); + shell.capture_event(); + } + Update::Drag(position) => { + shell.publish(on_edit(Action::Drag(position))); + } + Update::Release => { + state.drag_click = None; + } + Update::Scroll(lines) => { + let bounds = self.content.0.borrow().editor.bounds(); + + if bounds.height >= i32::MAX as f32 { + return; } - mouse::click::Kind::Double => Action::SelectWord, - mouse::click::Kind::Triple => Action::SelectLine, - }; - state.is_focused = true; - state.last_click = Some(click); - state.drag_click = Some(click.kind()); + let lines = lines + state.partial_scroll; + state.partial_scroll = lines.fract(); - shell.publish(on_edit(action)); - } - Update::Scroll(lines) => { - let lines = lines + state.partial_scroll; - state.partial_scroll = lines.fract(); - - shell.publish(on_edit(Action::Scroll { - lines: lines as i32, - })); - } - Update::Unfocus => { - state.is_focused = false; - state.drag_click = None; - } - Update::Release => { - state.drag_click = None; - } - Update::Action(action) => { - shell.publish(on_edit(action)); - } - Update::Copy => { - if let Some(selection) = self.content.selection() { - clipboard.write(clipboard::Kind::Standard, selection); + shell.publish(on_edit(Action::Scroll { + lines: lines as i32, + })); + shell.capture_event(); } - } - Update::Cut => { - if let Some(selection) = self.content.selection() { - clipboard.write(clipboard::Kind::Standard, selection); - shell.publish(on_edit(Action::Edit(Edit::Delete))); - } - } - Update::Paste => { - if let Some(contents) = - clipboard.read(clipboard::Kind::Standard) - { - shell.publish(on_edit(Action::Edit(Edit::Paste( - Arc::new(contents), - )))); + Update::InputMethod(update) => match update { + Ime::Toggle(is_open) => { + state.preedit = + is_open.then(input_method::Preedit::new); + + shell.request_redraw(); + } + Ime::Preedit { content, selection } => { + state.preedit = Some(input_method::Preedit { + content, + selection, + text_size: self.text_size, + }); + + shell.request_redraw(); + } + Ime::Commit(text) => { + shell.publish(on_edit(Action::Edit(Edit::Paste( + Arc::new(text), + )))); + } + }, + Update::Binding(binding) => { + fn apply_binding< + H: text::Highlighter, + R: text::Renderer, + Message, + >( + binding: Binding<Message>, + content: &Content<R>, + state: &mut State<H>, + on_edit: &dyn Fn(Action) -> Message, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) { + let mut publish = + |action| shell.publish(on_edit(action)); + + match binding { + Binding::Unfocus => { + state.focus = None; + state.drag_click = None; + } + Binding::Copy => { + if let Some(selection) = content.selection() { + clipboard.write( + clipboard::Kind::Standard, + selection, + ); + } + } + Binding::Cut => { + if let Some(selection) = content.selection() { + clipboard.write( + clipboard::Kind::Standard, + selection, + ); + + publish(Action::Edit(Edit::Delete)); + } + } + Binding::Paste => { + if let Some(contents) = + clipboard.read(clipboard::Kind::Standard) + { + publish(Action::Edit(Edit::Paste( + Arc::new(contents), + ))); + } + } + Binding::Move(motion) => { + publish(Action::Move(motion)); + } + Binding::Select(motion) => { + publish(Action::Select(motion)); + } + Binding::SelectWord => { + publish(Action::SelectWord); + } + Binding::SelectLine => { + publish(Action::SelectLine); + } + Binding::SelectAll => { + publish(Action::SelectAll); + } + Binding::Insert(c) => { + publish(Action::Edit(Edit::Insert(c))); + } + Binding::Enter => { + publish(Action::Edit(Edit::Enter)); + } + Binding::Backspace => { + publish(Action::Edit(Edit::Backspace)); + } + Binding::Delete => { + publish(Action::Edit(Edit::Delete)); + } + Binding::Sequence(sequence) => { + for binding in sequence { + apply_binding( + binding, content, state, on_edit, + clipboard, shell, + ); + } + } + Binding::Custom(message) => { + shell.publish(message); + } + } + } + + if !matches!(binding, Binding::Unfocus) { + shell.capture_event(); + } + + apply_binding( + binding, + self.content, + state, + on_edit, + clipboard, + shell, + ); + + if let Some(focus) = &mut state.focus { + focus.updated_at = Instant::now(); + } } } } - event::Status::Captured + let status = { + let is_disabled = self.on_edit.is_none(); + let is_hovered = cursor.is_over(layout.bounds()); + + if is_disabled { + Status::Disabled + } else if state.focus.is_some() { + Status::Focused { is_hovered } + } else if is_hovered { + Status::Hovered + } else { + Status::Active + } + }; + + if is_redraw { + self.last_status = Some(status); + + shell.request_input_method( + &self.input_method(state, renderer, layout), + ); + } else if self + .last_status + .is_some_and(|last_status| status != last_status) + { + shell.request_redraw(); + } } fn draw( @@ -513,36 +912,26 @@ where tree: &widget::Tree, renderer: &mut Renderer, theme: &Theme, - defaults: &renderer::Style, + _defaults: &renderer::Style, layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, + _cursor: mouse::Cursor, + _viewport: &Rectangle, ) { let bounds = layout.bounds(); let mut internal = self.content.0.borrow_mut(); let state = tree.state.downcast_ref::<State<Highlighter>>(); + let font = self.font.unwrap_or_else(|| renderer.default_font()); + internal.editor.highlight( - self.font.unwrap_or_else(|| renderer.default_font()), + font, state.highlighter.borrow_mut().deref_mut(), |highlight| (self.highlighter_format)(highlight, theme), ); - let is_disabled = self.on_edit.is_none(); - let is_mouse_over = cursor.is_over(bounds); - - let status = if is_disabled { - Status::Disabled - } else if state.is_focused { - Status::Focused - } else if is_mouse_over { - Status::Hovered - } else { - Status::Active - }; - - let style = theme.style(&self.class, status); + let style = theme + .style(&self.class, self.last_status.unwrap_or(Status::Active)); renderer.fill_quad( renderer::Quad { @@ -553,22 +942,43 @@ where style.background, ); - renderer.fill_editor( - &internal.editor, - bounds.position() - + Vector::new(self.padding.left, self.padding.top), - defaults.text_color, - *viewport, - ); + let text_bounds = bounds.shrink(self.padding); - let translation = Vector::new( - bounds.x + self.padding.left, - bounds.y + self.padding.top, - ); + if internal.editor.is_empty() { + if let Some(placeholder) = self.placeholder.clone() { + renderer.fill_text( + Text { + content: placeholder.into_owned(), + bounds: text_bounds.size(), + size: self + .text_size + .unwrap_or_else(|| renderer.default_size()), + line_height: self.line_height, + font, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + wrapping: self.wrapping, + }, + text_bounds.position(), + style.placeholder, + text_bounds, + ); + } + } else { + renderer.fill_editor( + &internal.editor, + text_bounds.position(), + style.value, + text_bounds, + ); + } - if state.is_focused { + let translation = text_bounds.position() - Point::ORIGIN; + + if let Some(focus) = state.focus.as_ref() { match internal.editor.cursor() { - Cursor::Caret(position) => { + Cursor::Caret(position) if focus.is_cursor_visible() => { let cursor = Rectangle::new( position + translation, @@ -582,15 +992,12 @@ where ), ); - if let Some(clipped_cursor) = bounds.intersection(&cursor) { + if let Some(clipped_cursor) = + text_bounds.intersection(&cursor) + { renderer.fill_quad( renderer::Quad { - bounds: Rectangle { - x: clipped_cursor.x.floor(), - y: clipped_cursor.y, - width: clipped_cursor.width, - height: clipped_cursor.height, - }, + bounds: clipped_cursor, ..renderer::Quad::default() }, style.value, @@ -599,7 +1006,7 @@ where } Cursor::Selection(ranges) => { for range in ranges.into_iter().filter_map(|range| { - bounds.intersection(&(range + translation)) + text_bounds.intersection(&(range + translation)) }) { renderer.fill_quad( renderer::Quad { @@ -610,6 +1017,7 @@ where ); } } + Cursor::Caret(_) => {} } } } @@ -634,6 +1042,18 @@ where mouse::Interaction::default() } } + + fn operate( + &self, + tree: &mut widget::Tree, + layout: Layout<'_>, + _renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + let state = tree.state.downcast_mut::<State<Highlighter>>(); + + operation.focusable(None, layout.bounds(), state); + } } impl<'a, Highlighter, Message, Theme, Renderer> @@ -652,27 +1072,158 @@ where } } -enum Update { - Click(mouse::Click), - Scroll(f32), +/// A binding to an action in the [`TextEditor`]. +#[derive(Debug, Clone, PartialEq)] +pub enum Binding<Message> { + /// Unfocus the [`TextEditor`]. Unfocus, - Release, - Action(Action), + /// Copy the selection of the [`TextEditor`]. Copy, + /// Cut the selection of the [`TextEditor`]. Cut, + /// Paste the clipboard contents in the [`TextEditor`]. Paste, + /// Apply a [`Motion`]. + Move(Motion), + /// Select text with a given [`Motion`]. + Select(Motion), + /// Select the word at the current cursor. + SelectWord, + /// Select the line at the current cursor. + SelectLine, + /// Select the entire buffer. + SelectAll, + /// Insert the given character. + Insert(char), + /// Break the current line. + Enter, + /// Delete the previous character. + Backspace, + /// Delete the next character. + Delete, + /// A sequence of bindings to execute. + Sequence(Vec<Self>), + /// Produce the given message. + Custom(Message), } -impl Update { +/// A key press. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct KeyPress { + /// The key pressed. + pub key: keyboard::Key, + /// The state of the keyboard modifiers. + pub modifiers: keyboard::Modifiers, + /// The text produced by the key press. + pub text: Option<SmolStr>, + /// The current [`Status`] of the [`TextEditor`]. + pub status: Status, +} + +impl<Message> Binding<Message> { + /// Returns the default [`Binding`] for the given key press. + pub fn from_key_press(event: KeyPress) -> Option<Self> { + let KeyPress { + key, + modifiers, + text, + status, + } = event; + + if !matches!(status, Status::Focused { .. }) { + return None; + } + + match key.as_ref() { + keyboard::Key::Named(key::Named::Enter) => Some(Self::Enter), + keyboard::Key::Named(key::Named::Backspace) => { + Some(Self::Backspace) + } + keyboard::Key::Named(key::Named::Delete) + if text.is_none() || text.as_deref() == Some("\u{7f}") => + { + Some(Self::Delete) + } + keyboard::Key::Named(key::Named::Escape) => Some(Self::Unfocus), + keyboard::Key::Character("c") if modifiers.command() => { + Some(Self::Copy) + } + keyboard::Key::Character("x") if modifiers.command() => { + Some(Self::Cut) + } + keyboard::Key::Character("v") + if modifiers.command() && !modifiers.alt() => + { + Some(Self::Paste) + } + keyboard::Key::Character("a") if modifiers.command() => { + Some(Self::SelectAll) + } + _ => { + if let Some(text) = text { + let c = text.chars().find(|c| !c.is_control())?; + + Some(Self::Insert(c)) + } else if let keyboard::Key::Named(named_key) = key.as_ref() { + let motion = motion(named_key)?; + + let motion = if modifiers.macos_command() { + match motion { + Motion::Left => Motion::Home, + Motion::Right => Motion::End, + _ => motion, + } + } else { + motion + }; + + let motion = if modifiers.jump() { + motion.widen() + } else { + motion + }; + + Some(if modifiers.shift() { + Self::Select(motion) + } else { + Self::Move(motion) + }) + } else { + None + } + } + } + } +} + +enum Update<Message> { + Click(mouse::Click), + Drag(Point), + Release, + Scroll(f32), + InputMethod(Ime), + Binding(Binding<Message>), +} + +enum Ime { + Toggle(bool), + Preedit { + content: String, + selection: Option<Range<usize>>, + }, + Commit(String), +} + +impl<Message> Update<Message> { fn from_event<H: Highlighter>( - event: Event, + event: &Event, state: &State<H>, bounds: Rectangle, padding: Padding, cursor: mouse::Cursor, + key_binding: Option<&dyn Fn(KeyPress) -> Option<Binding<Message>>>, ) -> Option<Self> { - let action = |action| Some(Update::Action(action)); - let edit = |edit| action(Action::Edit(edit)); + let binding = |binding| Some(Update::Binding(binding)); match event { Event::Mouse(event) => match event { @@ -683,12 +1234,13 @@ impl Update { let click = mouse::Click::new( cursor_position, + mouse::Button::Left, state.last_click, ); Some(Update::Click(click)) - } else if state.is_focused { - Some(Update::Unfocus) + } else if state.focus.is_some() { + binding(Binding::Unfocus) } else { None } @@ -701,7 +1253,7 @@ impl Update { let cursor_position = cursor.position_in(bounds)? - Vector::new(padding.top, padding.left); - action(Action::Drag(cursor_position)) + Some(Update::Drag(cursor_position)) } _ => None, }, @@ -721,73 +1273,56 @@ impl Update { } _ => None, }, - Event::Keyboard(event) => match event { - keyboard::Event::KeyPressed { - key, - modifiers, - text, - .. - } if state.is_focused => { - match key.as_ref() { - keyboard::Key::Named(key::Named::Enter) => { - return edit(Edit::Enter); - } - keyboard::Key::Named(key::Named::Backspace) => { - return edit(Edit::Backspace); - } - keyboard::Key::Named(key::Named::Delete) => { - return edit(Edit::Delete); - } - keyboard::Key::Named(key::Named::Escape) => { - return Some(Self::Unfocus); - } - keyboard::Key::Character("c") - if modifiers.command() => - { - return Some(Self::Copy); - } - keyboard::Key::Character("x") - if modifiers.command() => - { - return Some(Self::Cut); - } - keyboard::Key::Character("v") - if modifiers.command() && !modifiers.alt() => - { - return Some(Self::Paste); - } - _ => {} - } - - if let Some(text) = text { - if let Some(c) = text.chars().find(|c| !c.is_control()) - { - return edit(Edit::Insert(c)); - } - } - - if let keyboard::Key::Named(named_key) = key.as_ref() { - if let Some(motion) = motion(named_key) { - let motion = if platform::is_jump_modifier_pressed( - modifiers, - ) { - motion.widen() - } else { - motion - }; - - return action(if modifiers.shift() { - Action::Select(motion) - } else { - Action::Move(motion) - }); - } - } - - None + Event::InputMethod(event) => match event { + input_method::Event::Opened | input_method::Event::Closed => { + Some(Update::InputMethod(Ime::Toggle(matches!( + event, + input_method::Event::Opened + )))) + } + input_method::Event::Preedit(content, selection) + if state.focus.is_some() => + { + Some(Update::InputMethod(Ime::Preedit { + content: content.clone(), + selection: selection.clone(), + })) + } + input_method::Event::Commit(content) + if state.focus.is_some() => + { + Some(Update::InputMethod(Ime::Commit(content.clone()))) } _ => None, }, + Event::Keyboard(keyboard::Event::KeyPressed { + key, + modifiers, + text, + .. + }) => { + let status = if state.focus.is_some() { + Status::Focused { + is_hovered: cursor.is_over(bounds), + } + } else { + Status::Active + }; + + let key_press = KeyPress { + key: key.clone(), + modifiers: *modifiers, + text: text.clone(), + status, + }; + + if let Some(key_binding) = key_binding { + key_binding(key_press) + } else { + Binding::from_key_press(key_press) + } + .map(Self::Binding) + } _ => None, } } @@ -807,18 +1342,6 @@ fn motion(key: key::Named) -> Option<Motion> { } } -mod platform { - use crate::core::keyboard; - - pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { - if cfg!(target_os = "macos") { - modifiers.alt() - } else { - modifiers.control() - } - } -} - /// The possible status of a [`TextEditor`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Status { @@ -827,13 +1350,16 @@ pub enum Status { /// The [`TextEditor`] is being hovered. Hovered, /// The [`TextEditor`] is focused. - Focused, + Focused { + /// Whether the [`TextEditor`] is hovered, while focused. + is_hovered: bool, + }, /// The [`TextEditor`] cannot be interacted with. Disabled, } /// The appearance of a text input. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`Background`] of the text input. pub background: Background, @@ -902,7 +1428,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { }, ..active }, - Status::Focused => Style { + Status::Focused { .. } => Style { border: Border { color: palette.primary.strong.color, ..active.border diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index e9f07838..3abead5b 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -1,6 +1,35 @@ -//! Display fields that can be filled with text. +//! Text inputs display fields that can be filled with text. //! -//! A [`TextInput`] has some local [`State`]. +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::text_input; +//! +//! struct State { +//! content: String, +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! ContentChanged(String) +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! text_input("Type something here...", &state.content) +//! .on_input(Message::ContentChanged) +//! .into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::ContentChanged(content) => { +//! state.content = content; +//! } +//! } +//! } +//! ``` mod editor; mod value; @@ -13,13 +42,14 @@ use editor::Editor; use crate::core::alignment; use crate::core::clipboard::{self, Clipboard}; -use crate::core::event::{self, Event}; +use crate::core::input_method; use crate::core::keyboard; use crate::core::keyboard::key; use crate::core::layout; use crate::core::mouse::{self, click}; use crate::core::renderer; -use crate::core::text::{self, Paragraph as _, Text}; +use crate::core::text::paragraph::{self, Paragraph as _}; +use crate::core::text::{self, Text}; use crate::core::time::{Duration, Instant}; use crate::core::touch; use crate::core::widget; @@ -27,32 +57,44 @@ use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ - Background, Border, Color, Element, Layout, Length, Padding, Pixels, Point, - Rectangle, Shell, Size, Theme, Vector, Widget, + Background, Border, Color, Element, Event, InputMethod, Layout, Length, + Padding, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; -use crate::runtime::Command; +use crate::runtime::Action; +use crate::runtime::task::{self, Task}; /// A field that can be filled with text. /// /// # Example /// ```no_run -/// # pub type TextInput<'a, Message> = iced_widget::TextInput<'a, Message>; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # -/// #[derive(Debug, Clone)] -/// enum Message { -/// TextInputChanged(String), +/// use iced::widget::text_input; +/// +/// struct State { +/// content: String, /// } /// -/// let value = "Some text"; +/// #[derive(Debug, Clone)] +/// enum Message { +/// ContentChanged(String) +/// } /// -/// let input = TextInput::new( -/// "This is the placeholder...", -/// value, -/// ) -/// .on_input(Message::TextInputChanged) -/// .padding(10); +/// fn view(state: &State) -> Element<'_, Message> { +/// text_input("Type something here...", &state.content) +/// .on_input(Message::ContentChanged) +/// .into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::ContentChanged(content) => { +/// state.content = content; +/// } +/// } +/// } /// ``` -/// ![Text input drawn by `iced_wgpu`](https://github.com/iced-rs/iced/blob/7760618fb112074bc40b148944521f312152012a/docs/images/text_input.png?raw=true) #[allow(missing_debug_implementations)] pub struct TextInput< 'a, @@ -72,11 +114,13 @@ pub struct TextInput< padding: Padding, size: Option<Pixels>, line_height: text::LineHeight, + alignment: alignment::Horizontal, on_input: Option<Box<dyn Fn(String) -> Message + 'a>>, on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>, on_submit: Option<Message>, icon: Option<Icon<Renderer::Font>>, class: Theme::Class<'a>, + last_status: Option<Status>, } /// The default [`Padding`] of a [`TextInput`]. @@ -101,17 +145,19 @@ where padding: DEFAULT_PADDING, size: None, line_height: text::LineHeight::default(), + alignment: alignment::Horizontal::Left, on_input: None, on_paste: None, on_submit: None, icon: None, class: Theme::default(), + last_status: None, } } /// Sets the [`Id`] of the [`TextInput`]. - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); + pub fn id(mut self, id: impl Into<Id>) -> Self { + self.id = Some(id.into()); self } @@ -125,11 +171,23 @@ where /// the [`TextInput`]. /// /// If this method is not called, the [`TextInput`] will be disabled. - pub fn on_input<F>(mut self, callback: F) -> Self - where - F: 'a + Fn(String) -> Message, - { - self.on_input = Some(Box::new(callback)); + pub fn on_input( + mut self, + on_input: impl Fn(String) -> Message + 'a, + ) -> Self { + self.on_input = Some(Box::new(on_input)); + self + } + + /// Sets the message that should be produced when some text is typed into + /// the [`TextInput`], if `Some`. + /// + /// If `None`, the [`TextInput`] will be disabled. + pub fn on_input_maybe( + mut self, + on_input: Option<impl Fn(String) -> Message + 'a>, + ) -> Self { + self.on_input = on_input.map(|f| Box::new(f) as _); self } @@ -140,6 +198,13 @@ where self } + /// Sets the message that should be produced when the [`TextInput`] is + /// focused and the enter key is pressed, if `Some`. + pub fn on_submit_maybe(mut self, on_submit: Option<Message>) -> Self { + self.on_submit = on_submit; + self + } + /// Sets the message that should be produced when some text is pasted into /// the [`TextInput`]. pub fn on_paste( @@ -150,6 +215,16 @@ where self } + /// Sets the message that should be produced when some text is pasted into + /// the [`TextInput`], if `Some`. + pub fn on_paste_maybe( + mut self, + on_paste: Option<impl Fn(String) -> Message + 'a>, + ) -> Self { + self.on_paste = on_paste.map(|f| Box::new(f) as _); + self + } + /// Sets the [`Font`] of the [`TextInput`]. /// /// [`Font`]: text::Renderer::Font @@ -191,6 +266,15 @@ where self } + /// Sets the horizontal alignment of the [`TextInput`]. + pub fn align_x( + mut self, + alignment: impl Into<alignment::Horizontal>, + ) -> Self { + self.alignment = alignment.into(); + self + } + /// Sets the style of the [`TextInput`]. #[must_use] pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self @@ -238,6 +322,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::default(), }; state.placeholder.update(placeholder_text); @@ -262,6 +347,7 @@ where horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::default(), }; state.icon.update(icon_text); @@ -306,6 +392,54 @@ where } } + fn input_method<'b>( + &self, + state: &'b State<Renderer::Paragraph>, + layout: Layout<'_>, + value: &Value, + ) -> InputMethod<&'b str> { + let Some(Focus { + is_window_focused: true, + .. + }) = &state.is_focused + else { + return InputMethod::Disabled; + }; + + let secure_value = self.is_secure.then(|| value.secure()); + let value = secure_value.as_ref().unwrap_or(value); + + let text_bounds = layout.children().next().unwrap().bounds(); + + let caret_index = match state.cursor.state(value) { + cursor::State::Index(position) => position, + cursor::State::Selection { start, end } => start.min(end), + }; + + let text = state.value.raw(); + let (cursor_x, scroll_offset) = + measure_cursor_and_scroll_offset(text, text_bounds, caret_index); + + let alignment_offset = alignment_offset( + text_bounds.width, + text.min_width(), + self.alignment, + ); + + let x = (text_bounds.x + cursor_x).floor() - scroll_offset + + alignment_offset; + + InputMethod::Enabled { + position: Point::new(x, text_bounds.y + text_bounds.height), + purpose: if self.is_secure { + input_method::Purpose::Secure + } else { + input_method::Purpose::Normal + }, + preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref), + } + } + /// Draws the [`TextInput`] with the given [`Renderer`], overriding its /// [`Value`] if provided. /// @@ -316,7 +450,7 @@ where renderer: &mut Renderer, theme: &Theme, layout: Layout<'_>, - cursor: mouse::Cursor, + _cursor: mouse::Cursor, value: Option<&Value>, viewport: &Rectangle, ) { @@ -332,19 +466,8 @@ where let mut children_layout = layout.children(); let text_bounds = children_layout.next().unwrap().bounds(); - let is_mouse_over = cursor.is_over(bounds); - - let status = if is_disabled { - Status::Disabled - } else if state.is_focused() { - Status::Focused - } else if is_mouse_over { - Status::Hovered - } else { - Status::Active - }; - - let style = theme.style(&self.class, status); + let style = theme + .style(&self.class, self.last_status.unwrap_or(Status::Disabled)); renderer.fill_quad( renderer::Quad { @@ -359,7 +482,7 @@ where let icon_layout = children_layout.next().unwrap(); renderer.fill_paragraph( - &state.icon, + state.icon.raw(), icon_layout.bounds().center(), style.icon, *viewport, @@ -377,16 +500,16 @@ where cursor::State::Index(position) => { let (text_value_width, offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_bounds, position, ); - let is_cursor_visible = ((focus.now - focus.updated_at) - .as_millis() - / CURSOR_BLINK_INTERVAL_MILLIS) - % 2 - == 0; + let is_cursor_visible = !is_disabled + && ((focus.now - focus.updated_at).as_millis() + / CURSOR_BLINK_INTERVAL_MILLIS) + % 2 + == 0; let cursor = if is_cursor_visible { Some(( @@ -414,14 +537,14 @@ where let (left_position, left_offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_bounds, left, ); let (right_position, right_offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_bounds, right, ); @@ -455,9 +578,27 @@ where }; let draw = |renderer: &mut Renderer, viewport| { + let paragraph = if text.is_empty() + && state + .preedit + .as_ref() + .map(|preedit| preedit.content.is_empty()) + .unwrap_or(true) + { + state.placeholder.raw() + } else { + state.value.raw() + }; + + let alignment_offset = alignment_offset( + text_bounds.width, + paragraph.min_width(), + self.alignment, + ); + if let Some((cursor, color)) = cursor { renderer.with_translation( - Vector::new(-offset, 0.0), + Vector::new(alignment_offset - offset, 0.0), |renderer| { renderer.fill_quad(cursor, color); }, @@ -467,13 +608,9 @@ where } renderer.fill_paragraph( - if text.is_empty() { - &state.placeholder - } else { - &state.value - }, + paragraph, Point::new(text_bounds.x, text_bounds.center_y()) - - Vector::new(offset, 0.0), + + Vector::new(alignment_offset - offset, 0.0), if text.is_empty() { style.placeholder } else { @@ -492,8 +629,8 @@ where } } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for TextInput<'a, Message, Theme, Renderer> +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for TextInput<'_, Message, Theme, Renderer> where Message: Clone, Theme: Catalog, @@ -510,12 +647,9 @@ where fn diff(&self, tree: &mut Tree) { let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); - // Unfocus text input if it becomes disabled + // Stop pasting if input becomes disabled if self.on_input.is_none() { - state.last_click = None; - state.is_focused = None; state.is_pasting = None; - state.is_dragging = false; } } @@ -538,27 +672,36 @@ where fn operate( &self, tree: &mut Tree, - _layout: Layout<'_>, + layout: Layout<'_>, _renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation, ) { let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); - operation.focusable(state, self.id.as_ref().map(|id| &id.0)); - operation.text_input(state, self.id.as_ref().map(|id| &id.0)); + operation.focusable( + self.id.as_ref().map(|id| &id.0), + layout.bounds(), + state, + ); + + operation.text_input( + self.id.as_ref().map(|id| &id.0), + layout.bounds(), + state, + ); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let update_cache = |state, value| { replace_paragraph( renderer, @@ -571,26 +714,21 @@ where ); }; - match event { + match &event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { let state = state::<Renderer>(tree); + let cursor_before = state.cursor; - let click_position = if self.on_input.is_some() { - cursor.position_over(layout.bounds()) - } else { - None - }; + let click_position = cursor.position_over(layout.bounds()); state.is_focused = if click_position.is_some() { - state.is_focused.or_else(|| { - let now = Instant::now(); + let now = Instant::now(); - Some(Focus { - updated_at: now, - now, - is_window_focused: true, - }) + Some(Focus { + updated_at: now, + now, + is_window_focused: true, }) } else { None @@ -598,10 +736,24 @@ where if let Some(cursor_position) = click_position { let text_layout = layout.children().next().unwrap(); - let target = cursor_position.x - text_layout.bounds().x; - let click = - mouse::Click::new(cursor_position, state.last_click); + let target = { + let text_bounds = text_layout.bounds(); + + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + self.alignment, + ); + + cursor_position.x - text_bounds.x - alignment_offset + }; + + let click = mouse::Click::new( + cursor_position, + mouse::Button::Left, + state.last_click, + ); match click.kind() { click::Kind::Single => { @@ -661,7 +813,11 @@ where state.last_click = Some(click); - return event::Status::Captured; + if cursor_before != state.cursor { + shell.request_redraw(); + } + + shell.capture_event(); } } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) @@ -675,7 +831,18 @@ where if state.is_dragging { let text_layout = layout.children().next().unwrap(); - let target = position.x - text_layout.bounds().x; + + let target = { + let text_bounds = text_layout.bounds(); + + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + self.alignment, + ); + + position.x - text_bounds.x - alignment_offset + }; let value = if self.is_secure { self.value.secure() @@ -691,11 +858,21 @@ where ) .unwrap_or(0); + let selection_before = state.cursor.selection(&value); + state .cursor .select_range(state.cursor.start(&value), position); - return event::Status::Captured; + if let Some(focus) = &mut state.is_focused { + focus.updated_at = Instant::now(); + } + + if selection_before != state.cursor.selection(&value) { + shell.request_redraw(); + } + + shell.capture_event(); } } Event::Keyboard(keyboard::Event::KeyPressed { @@ -704,12 +881,7 @@ where let state = state::<Renderer>(tree); if let Some(focus) = &mut state.is_focused { - let Some(on_input) = &self.on_input else { - return event::Status::Ignored; - }; - let modifiers = state.keyboard_modifiers; - focus.updated_at = Instant::now(); match key.as_ref() { keyboard::Key::Character("c") @@ -725,12 +897,17 @@ where ); } - return event::Status::Captured; + shell.capture_event(); + return; } keyboard::Key::Character("x") if state.keyboard_modifiers.command() && !self.is_secure => { + let Some(on_input) = &self.on_input else { + return; + }; + if let Some((start, end)) = state.cursor.selection(&self.value) { @@ -746,15 +923,20 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + shell.capture_event(); + focus.updated_at = Instant::now(); update_cache(state, &self.value); - - return event::Status::Captured; + return; } keyboard::Key::Character("v") if state.keyboard_modifiers.command() && !state.keyboard_modifiers.alt() => { + let Some(on_input) = &self.on_input else { + return; + }; + let content = match state.is_pasting.take() { Some(content) => content, None => { @@ -771,7 +953,6 @@ where let mut editor = Editor::new(&mut self.value, &mut state.cursor); - editor.paste(content.clone()); let message = if let Some(paste) = &self.on_paste { @@ -780,24 +961,37 @@ where (on_input)(editor.contents()) }; shell.publish(message); + shell.capture_event(); state.is_pasting = Some(content); - + focus.updated_at = Instant::now(); update_cache(state, &self.value); - - return event::Status::Captured; + return; } keyboard::Key::Character("a") if state.keyboard_modifiers.command() => { + let cursor_before = state.cursor; + state.cursor.select_all(&self.value); - return event::Status::Captured; + if cursor_before != state.cursor { + focus.updated_at = Instant::now(); + + shell.request_redraw(); + } + + shell.capture_event(); + return; } _ => {} } if let Some(text) = text { + let Some(on_input) = &self.on_input else { + return; + }; + state.is_pasting = None; if let Some(c) = @@ -810,12 +1004,11 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + shell.capture_event(); focus.updated_at = Instant::now(); - update_cache(state, &self.value); - - return event::Status::Captured; + return; } } @@ -823,10 +1016,15 @@ where keyboard::Key::Named(key::Named::Enter) => { if let Some(on_submit) = self.on_submit.clone() { shell.publish(on_submit); + shell.capture_event(); } } keyboard::Key::Named(key::Named::Backspace) => { - if platform::is_jump_modifier_pressed(modifiers) + let Some(on_input) = &self.on_input else { + return; + }; + + if modifiers.jump() && state.cursor.selection(&self.value).is_none() { if self.is_secure { @@ -846,11 +1044,17 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + shell.capture_event(); + focus.updated_at = Instant::now(); update_cache(state, &self.value); } keyboard::Key::Named(key::Named::Delete) => { - if platform::is_jump_modifier_pressed(modifiers) + let Some(on_input) = &self.on_input else { + return; + }; + + if modifiers.jump() && state.cursor.selection(&self.value).is_none() { if self.is_secure { @@ -873,13 +1077,99 @@ where let message = (on_input)(editor.contents()); shell.publish(message); + shell.capture_event(); + focus.updated_at = Instant::now(); update_cache(state, &self.value); } + keyboard::Key::Named(key::Named::Home) => { + let cursor_before = state.cursor; + + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + 0, + ); + } else { + state.cursor.move_to(0); + } + + if cursor_before != state.cursor { + focus.updated_at = Instant::now(); + + shell.request_redraw(); + } + + shell.capture_event(); + } + keyboard::Key::Named(key::Named::End) => { + let cursor_before = state.cursor; + + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + self.value.len(), + ); + } else { + state.cursor.move_to(self.value.len()); + } + + if cursor_before != state.cursor { + focus.updated_at = Instant::now(); + + shell.request_redraw(); + } + + shell.capture_event(); + } + keyboard::Key::Named(key::Named::ArrowLeft) + if modifiers.macos_command() => + { + let cursor_before = state.cursor; + + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + 0, + ); + } else { + state.cursor.move_to(0); + } + + if cursor_before != state.cursor { + focus.updated_at = Instant::now(); + + shell.request_redraw(); + } + + shell.capture_event(); + } + keyboard::Key::Named(key::Named::ArrowRight) + if modifiers.macos_command() => + { + let cursor_before = state.cursor; + + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + self.value.len(), + ); + } else { + state.cursor.move_to(self.value.len()); + } + + if cursor_before != state.cursor { + focus.updated_at = Instant::now(); + + shell.request_redraw(); + } + + shell.capture_event(); + } keyboard::Key::Named(key::Named::ArrowLeft) => { - if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure - { + let cursor_before = state.cursor; + + if modifiers.jump() && !self.is_secure { if modifiers.shift() { state .cursor @@ -894,11 +1184,19 @@ where } else { state.cursor.move_left(&self.value); } + + if cursor_before != state.cursor { + focus.updated_at = Instant::now(); + + shell.request_redraw(); + } + + shell.capture_event(); } keyboard::Key::Named(key::Named::ArrowRight) => { - if platform::is_jump_modifier_pressed(modifiers) - && !self.is_secure - { + let cursor_before = state.cursor; + + if modifiers.jump() && !self.is_secure { if modifiers.shift() { state .cursor @@ -913,26 +1211,14 @@ where } else { state.cursor.move_right(&self.value); } - } - keyboard::Key::Named(key::Named::Home) => { - if modifiers.shift() { - state.cursor.select_range( - state.cursor.start(&self.value), - 0, - ); - } else { - state.cursor.move_to(0); - } - } - keyboard::Key::Named(key::Named::End) => { - if modifiers.shift() { - state.cursor.select_range( - state.cursor.start(&self.value), - self.value.len(), - ); - } else { - state.cursor.move_to(self.value.len()); + + if cursor_before != state.cursor { + focus.updated_at = Instant::now(); + + shell.request_redraw(); } + + shell.capture_event(); } keyboard::Key::Named(key::Named::Escape) => { state.is_focused = None; @@ -941,39 +1227,22 @@ where state.keyboard_modifiers = keyboard::Modifiers::default(); - } - keyboard::Key::Named( - key::Named::Tab - | key::Named::ArrowUp - | key::Named::ArrowDown, - ) => { - return event::Status::Ignored; + + shell.capture_event(); } _ => {} } - - return event::Status::Captured; } } Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { let state = state::<Renderer>(tree); if state.is_focused.is_some() { - match key.as_ref() { - keyboard::Key::Character("v") => { - state.is_pasting = None; - } - keyboard::Key::Named( - key::Named::Tab - | key::Named::ArrowUp - | key::Named::ArrowDown, - ) => { - return event::Status::Ignored; - } - _ => {} - } + if let keyboard::Key::Character("v") = key.as_ref() { + state.is_pasting = None; - return event::Status::Captured; + shell.capture_event(); + } } state.is_pasting = None; @@ -981,40 +1250,98 @@ where Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { let state = state::<Renderer>(tree); - state.keyboard_modifiers = modifiers; + state.keyboard_modifiers = *modifiers; } - Event::Window(_, window::Event::Unfocused) => { + Event::InputMethod(event) => match event { + input_method::Event::Opened | input_method::Event::Closed => { + let state = state::<Renderer>(tree); + + state.preedit = + matches!(event, input_method::Event::Opened) + .then(input_method::Preedit::new); + + shell.request_redraw(); + } + input_method::Event::Preedit(content, selection) => { + let state = state::<Renderer>(tree); + + if state.is_focused.is_some() { + state.preedit = Some(input_method::Preedit { + content: content.to_owned(), + selection: selection.clone(), + text_size: self.size, + }); + + shell.request_redraw(); + } + } + input_method::Event::Commit(text) => { + let state = state::<Renderer>(tree); + + if let Some(focus) = &mut state.is_focused { + let Some(on_input) = &self.on_input else { + return; + }; + + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + editor.paste(Value::new(text)); + + focus.updated_at = Instant::now(); + state.is_pasting = None; + + let message = (on_input)(editor.contents()); + shell.publish(message); + shell.capture_event(); + + update_cache(state, &self.value); + } + } + }, + Event::Window(window::Event::Unfocused) => { let state = state::<Renderer>(tree); if let Some(focus) = &mut state.is_focused { focus.is_window_focused = false; } } - Event::Window(_, window::Event::Focused) => { + Event::Window(window::Event::Focused) => { let state = state::<Renderer>(tree); if let Some(focus) = &mut state.is_focused { focus.is_window_focused = true; focus.updated_at = Instant::now(); - shell.request_redraw(window::RedrawRequest::NextFrame); + shell.request_redraw(); } } - Event::Window(_, window::Event::RedrawRequested(now)) => { + Event::Window(window::Event::RedrawRequested(now)) => { let state = state::<Renderer>(tree); if let Some(focus) = &mut state.is_focused { if focus.is_window_focused { - focus.now = now; + if matches!( + state.cursor.state(&self.value), + cursor::State::Index(_) + ) { + focus.now = *now; - let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS - - (now - focus.updated_at).as_millis() - % CURSOR_BLINK_INTERVAL_MILLIS; + let millis_until_redraw = + CURSOR_BLINK_INTERVAL_MILLIS + - (*now - focus.updated_at).as_millis() + % CURSOR_BLINK_INTERVAL_MILLIS; - shell.request_redraw(window::RedrawRequest::At( - now + Duration::from_millis( - millis_until_redraw as u64, - ), + shell.request_redraw_at( + *now + Duration::from_millis( + millis_until_redraw as u64, + ), + ); + } + + shell.request_input_method(&self.input_method( + state, + layout, + &self.value, )); } } @@ -1022,7 +1349,29 @@ where _ => {} } - event::Status::Ignored + let state = state::<Renderer>(tree); + let is_disabled = self.on_input.is_none(); + + let status = if is_disabled { + Status::Disabled + } else if state.is_focused() { + Status::Focused { + is_hovered: cursor.is_over(layout.bounds()), + } + } else if cursor.is_over(layout.bounds()) { + Status::Hovered + } else { + Status::Active + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.last_status = Some(status); + } else if self + .last_status + .is_some_and(|last_status| status != last_status) + { + shell.request_redraw(); + } } fn draw( @@ -1048,7 +1397,7 @@ where ) -> mouse::Interaction { if cursor.is_over(layout.bounds()) { if self.on_input.is_none() { - mouse::Interaction::NotAllowed + mouse::Interaction::Idle } else { mouse::Interaction::Text } @@ -1120,46 +1469,70 @@ impl From<Id> for widget::Id { } } -/// Produces a [`Command`] that focuses the [`TextInput`] with the given [`Id`]. -pub fn focus<Message: 'static>(id: Id) -> Command<Message> { - Command::widget(operation::focusable::focus(id.0)) +impl From<&'static str> for Id { + fn from(id: &'static str) -> Self { + Self::new(id) + } } -/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +impl From<String> for Id { + fn from(id: String) -> Self { + Self::new(id) + } +} + +/// Produces a [`Task`] that returns whether the [`TextInput`] with the given [`Id`] is focused or not. +pub fn is_focused(id: impl Into<Id>) -> Task<bool> { + task::widget(operation::focusable::is_focused(id.into().into())) +} + +/// Produces a [`Task`] that focuses the [`TextInput`] with the given [`Id`]. +pub fn focus<T>(id: impl Into<Id>) -> Task<T> { + task::effect(Action::widget(operation::focusable::focus(id.into().0))) +} + +/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// end. -pub fn move_cursor_to_end<Message: 'static>(id: Id) -> Command<Message> { - Command::widget(operation::text_input::move_cursor_to_end(id.0)) +pub fn move_cursor_to_end<T>(id: impl Into<Id>) -> Task<T> { + task::effect(Action::widget(operation::text_input::move_cursor_to_end( + id.into().0, + ))) } -/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// front. -pub fn move_cursor_to_front<Message: 'static>(id: Id) -> Command<Message> { - Command::widget(operation::text_input::move_cursor_to_front(id.0)) +pub fn move_cursor_to_front<T>(id: impl Into<Id>) -> Task<T> { + task::effect(Action::widget(operation::text_input::move_cursor_to_front( + id.into().0, + ))) } -/// Produces a [`Command`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the +/// Produces a [`Task`] that moves the cursor of the [`TextInput`] with the given [`Id`] to the /// provided position. -pub fn move_cursor_to<Message: 'static>( - id: Id, - position: usize, -) -> Command<Message> { - Command::widget(operation::text_input::move_cursor_to(id.0, position)) +pub fn move_cursor_to<T>(id: impl Into<Id>, position: usize) -> Task<T> { + task::effect(Action::widget(operation::text_input::move_cursor_to( + id.into().0, + position, + ))) } -/// Produces a [`Command`] that selects all the content of the [`TextInput`] with the given [`Id`]. -pub fn select_all<Message: 'static>(id: Id) -> Command<Message> { - Command::widget(operation::text_input::select_all(id.0)) +/// Produces a [`Task`] that selects all the content of the [`TextInput`] with the given [`Id`]. +pub fn select_all<T>(id: impl Into<Id>) -> Task<T> { + task::effect(Action::widget(operation::text_input::select_all( + id.into().0, + ))) } /// The state of a [`TextInput`]. #[derive(Debug, Default, Clone)] pub struct State<P: text::Paragraph> { - value: P, - placeholder: P, - icon: P, + value: paragraph::Plain<P>, + placeholder: paragraph::Plain<P>, + icon: paragraph::Plain<P>, is_focused: Option<Focus>, is_dragging: bool, is_pasting: Option<Value>, + preedit: Option<input_method::Preedit>, last_click: Option<mouse::Click>, cursor: Cursor, keyboard_modifiers: keyboard::Modifiers, @@ -1172,7 +1545,7 @@ fn state<Renderer: text::Renderer>( tree.state.downcast_mut::<State<Renderer::Paragraph>>() } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] struct Focus { updated_at: Instant, now: Instant, @@ -1185,21 +1558,6 @@ impl<P: text::Paragraph> State<P> { Self::default() } - /// Creates a new [`State`], representing a focused [`TextInput`]. - pub fn focused() -> Self { - Self { - value: P::default(), - placeholder: P::default(), - icon: P::default(), - is_focused: None, - is_dragging: false, - is_pasting: None, - last_click: None, - cursor: Cursor::default(), - keyboard_modifiers: keyboard::Modifiers::default(), - } - } - /// Returns whether the [`TextInput`] is currently focused or not. pub fn is_focused(&self) -> bool { self.is_focused.is_some() @@ -1281,18 +1639,6 @@ impl<P: text::Paragraph> operation::TextInput for State<P> { } } -mod platform { - use crate::core::keyboard; - - pub fn is_jump_modifier_pressed(modifiers: keyboard::Modifiers) -> bool { - if cfg!(target_os = "macos") { - modifiers.alt() - } else { - modifiers.control() - } - } -} - fn offset<P: text::Paragraph>( text_bounds: Rectangle, value: &Value, @@ -1307,7 +1653,7 @@ fn offset<P: text::Paragraph>( }; let (_, offset) = measure_cursor_and_scroll_offset( - &state.value, + state.value.raw(), text_bounds, focus_position, ); @@ -1345,6 +1691,7 @@ fn find_cursor_position<P: text::Paragraph>( let char_offset = state .value + .raw() .hit_test(Point::new(x + offset, text_bounds.height / 2.0)) .map(text::Hit::cursor)?; @@ -1374,15 +1721,16 @@ fn replace_paragraph<Renderer>( let mut children_layout = layout.children(); let text_bounds = children_layout.next().unwrap().bounds(); - state.value = Renderer::Paragraph::with_text(Text { + state.value = paragraph::Plain::new(Text { font, line_height, content: &value.to_string(), bounds: Size::new(f32::INFINITY, text_bounds.height), size: text_size, horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::default(), }); } @@ -1396,13 +1744,16 @@ pub enum Status { /// The [`TextInput`] is being hovered. Hovered, /// The [`TextInput`] is focused. - Focused, + Focused { + /// Whether the [`TextInput`] is hovered, while focused. + is_hovered: bool, + }, /// The [`TextInput`] cannot be interacted with. Disabled, } /// The appearance of a text input. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The [`Background`] of the text input. pub background: Background, @@ -1456,10 +1807,10 @@ pub fn default(theme: &Theme, status: Status) -> Style { border: Border { radius: 2.0.into(), width: 1.0, - color: palette.background.strong.color, + color: palette.background.strongest.color, }, icon: palette.background.weak.text, - placeholder: palette.background.strong.color, + placeholder: palette.background.strongest.color, value: palette.background.base.text, selection: palette.primary.weak.color, }; @@ -1473,7 +1824,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { }, ..active }, - Status::Focused => Style { + Status::Focused { .. } => Style { border: Border { color: palette.primary.strong.color, ..active.border @@ -1487,3 +1838,21 @@ pub fn default(theme: &Theme, status: Status) -> Style { }, } } + +fn alignment_offset( + text_bounds_width: f32, + text_min_width: f32, + alignment: alignment::Horizontal, +) -> f32 { + if text_min_width > text_bounds_width { + 0.0 + } else { + match alignment { + alignment::Horizontal::Left => 0.0, + alignment::Horizontal::Center => { + (text_bounds_width - text_min_width) / 2.0 + } + alignment::Horizontal::Right => text_bounds_width - text_min_width, + } + } +} diff --git a/widget/src/text_input/cursor.rs b/widget/src/text_input/cursor.rs index f682b17d..a326fc8f 100644 --- a/widget/src/text_input/cursor.rs +++ b/widget/src/text_input/cursor.rs @@ -2,13 +2,13 @@ use crate::text_input::Value; /// The cursor of a text input. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Cursor { state: State, } /// The state of a [`Cursor`]. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum State { /// Cursor without a selection Index(usize), diff --git a/widget/src/themer.rs b/widget/src/themer.rs index f4597458..cf0845be 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -1,14 +1,13 @@ use crate::container; -use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; -use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; +use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, - Shell, Size, Vector, Widget, + Background, Clipboard, Color, Element, Event, Layout, Length, Point, + Rectangle, Shell, Size, Vector, Widget, }; use std::marker::PhantomData; @@ -64,8 +63,8 @@ where } } -impl<'a, Message, Theme, NewTheme, F, Renderer> Widget<Message, Theme, Renderer> - for Themer<'a, Message, Theme, NewTheme, F, Renderer> +impl<Message, Theme, NewTheme, F, Renderer> Widget<Message, Theme, Renderer> + for Themer<'_, Message, Theme, NewTheme, F, Renderer> where F: Fn(&Theme) -> NewTheme, Renderer: crate::core::Renderer, @@ -104,27 +103,27 @@ where tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation, ) { self.content .as_widget() .operate(tree, layout, renderer, 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<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().update( tree, event, layout, cursor, renderer, clipboard, shell, viewport, - ) + ); } fn mouse_interaction( @@ -188,9 +187,9 @@ where content: overlay::Element<'a, Message, NewTheme, Renderer>, } - impl<'a, Message, Theme, NewTheme, Renderer> + impl<Message, Theme, NewTheme, Renderer> overlay::Overlay<Message, Theme, Renderer> - for Overlay<'a, Message, Theme, NewTheme, Renderer> + for Overlay<'_, Message, Theme, NewTheme, Renderer> where Renderer: crate::core::Renderer, { @@ -219,24 +218,24 @@ 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.content - .on_event(event, layout, cursor, renderer, clipboard, shell) + .update(event, layout, cursor, renderer, clipboard, shell); } fn operate( &mut self, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<Message>, + operation: &mut dyn Operation, ) { self.content.operate(layout, renderer, operation); } diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index ca6e37b0..b711432e 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -1,6 +1,36 @@ -//! Show toggle controls using togglers. +//! Togglers let users make binary choices by toggling a switch. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::toggler; +//! +//! struct State { +//! is_checked: bool, +//! } +//! +//! enum Message { +//! TogglerToggled(bool), +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! toggler(state.is_checked) +//! .label("Toggle me!") +//! .on_toggle(Message::TogglerToggled) +//! .into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::TogglerToggled(is_checked) => { +//! state.is_checked = is_checked; +//! } +//! } +//! } +//! ``` use crate::core::alignment; -use crate::core::event; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; @@ -8,6 +38,7 @@ use crate::core::text; use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ Border, Clipboard, Color, Element, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Theme, Widget, @@ -16,17 +47,34 @@ use crate::core::{ /// A toggler widget. /// /// # Example -/// /// ```no_run -/// # type Toggler<'a, Message> = iced_widget::Toggler<'a, Message>; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # -/// pub enum Message { +/// use iced::widget::toggler; +/// +/// struct State { +/// is_checked: bool, +/// } +/// +/// enum Message { /// TogglerToggled(bool), /// } /// -/// let is_toggled = true; +/// fn view(state: &State) -> Element<'_, Message> { +/// toggler(state.is_checked) +/// .label("Toggle me!") +/// .on_toggle(Message::TogglerToggled) +/// .into() +/// } /// -/// Toggler::new(String::from("Toggle me!"), is_toggled, |b| Message::TogglerToggled(b)); +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::TogglerToggled(is_checked) => { +/// state.is_checked = is_checked; +/// } +/// } +/// } /// ``` #[allow(missing_debug_implementations)] pub struct Toggler< @@ -39,17 +87,19 @@ pub struct Toggler< Renderer: text::Renderer, { is_toggled: bool, - on_toggle: Box<dyn Fn(bool) -> Message + 'a>, - label: Option<String>, + on_toggle: Option<Box<dyn Fn(bool) -> Message + 'a>>, + label: Option<text::Fragment<'a>>, width: Length, size: f32, text_size: Option<Pixels>, text_line_height: text::LineHeight, text_alignment: alignment::Horizontal, text_shaping: text::Shaping, + text_wrapping: text::Wrapping, spacing: f32, font: Option<Renderer::Font>, class: Theme::Class<'a>, + last_status: Option<Status>, } impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer> @@ -68,30 +118,55 @@ where /// * a function that will be called when the [`Toggler`] is toggled. It /// will receive the new state of the [`Toggler`] and must produce a /// `Message`. - pub fn new<F>( - label: impl Into<Option<String>>, - is_toggled: bool, - f: F, - ) -> Self - where - F: 'a + Fn(bool) -> Message, - { + pub fn new(is_toggled: bool) -> Self { Toggler { is_toggled, - on_toggle: Box::new(f), - label: label.into(), - width: Length::Fill, + on_toggle: None, + label: None, + width: Length::Shrink, size: Self::DEFAULT_SIZE, text_size: None, text_line_height: text::LineHeight::default(), text_alignment: alignment::Horizontal::Left, - text_shaping: text::Shaping::Basic, + text_shaping: text::Shaping::default(), + text_wrapping: text::Wrapping::default(), spacing: Self::DEFAULT_SIZE / 2.0, font: None, class: Theme::default(), + last_status: None, } } + /// Sets the label of the [`Toggler`]. + pub fn label(mut self, label: impl text::IntoFragment<'a>) -> Self { + self.label = Some(label.into_fragment()); + self + } + + /// Sets the message that should be produced when a user toggles + /// the [`Toggler`]. + /// + /// If this method is not called, the [`Toggler`] will be disabled. + pub fn on_toggle( + mut self, + on_toggle: impl Fn(bool) -> Message + 'a, + ) -> Self { + self.on_toggle = Some(Box::new(on_toggle)); + self + } + + /// Sets the message that should be produced when a user toggles + /// the [`Toggler`], if `Some`. + /// + /// If `None`, the [`Toggler`] will be disabled. + pub fn on_toggle_maybe( + mut self, + on_toggle: Option<impl Fn(bool) -> Message + 'a>, + ) -> Self { + self.on_toggle = on_toggle.map(|on_toggle| Box::new(on_toggle) as _); + self + } + /// Sets the size of the [`Toggler`]. pub fn size(mut self, size: impl Into<Pixels>) -> Self { self.size = size.into().0; @@ -131,6 +206,12 @@ where self } + /// Sets the [`text::Wrapping`] strategy of the [`Toggler`]. + pub fn text_wrapping(mut self, wrapping: text::Wrapping) -> Self { + self.text_wrapping = wrapping; + self + } + /// Sets the spacing between the [`Toggler`] and the text. pub fn spacing(mut self, spacing: impl Into<Pixels>) -> Self { self.spacing = spacing.into().0; @@ -164,8 +245,8 @@ where } } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for Toggler<'a, Message, Theme, Renderer> +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Toggler<'_, Message, Theme, Renderer> where Theme: Catalog, Renderer: text::Renderer, @@ -216,6 +297,7 @@ where self.text_alignment, alignment::Vertical::Top, self.text_shaping, + self.text_wrapping, ) } else { layout::Node::new(Size::ZERO) @@ -224,31 +306,53 @@ where ) } - 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 { + ) { + let Some(on_toggle) = &self.on_toggle else { + return; + }; + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { let mouse_over = cursor.is_over(layout.bounds()); if mouse_over { - shell.publish((self.on_toggle)(!self.is_toggled)); - - event::Status::Captured - } else { - event::Status::Ignored + shell.publish(on_toggle(!self.is_toggled)); + shell.capture_event(); } } - _ => event::Status::Ignored, + _ => {} + } + + let current_status = if self.on_toggle.is_none() { + Status::Disabled + } else if cursor.is_over(layout.bounds()) { + Status::Hovered { + is_toggled: self.is_toggled, + } + } else { + Status::Active { + is_toggled: self.is_toggled, + } + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.last_status = Some(current_status); + } else if self + .last_status + .is_some_and(|status| status != current_status) + { + shell.request_redraw(); } } @@ -261,7 +365,11 @@ where _renderer: &Renderer, ) -> mouse::Interaction { if cursor.is_over(layout.bounds()) { - mouse::Interaction::Pointer + if self.on_toggle.is_some() { + mouse::Interaction::Pointer + } else { + mouse::Interaction::NotAllowed + } } else { mouse::Interaction::default() } @@ -274,7 +382,7 @@ where theme: &Theme, style: &renderer::Style, layout: Layout<'_>, - cursor: mouse::Cursor, + _cursor: mouse::Cursor, viewport: &Rectangle, ) { /// Makes sure that the border radius of the toggler looks good at every size. @@ -289,31 +397,22 @@ where if self.label.is_some() { let label_layout = children.next().unwrap(); + let state: &widget::text::State<Renderer::Paragraph> = + tree.state.downcast_ref(); crate::text::draw( renderer, style, label_layout, - tree.state.downcast_ref(), + state.0.raw(), crate::text::Style::default(), viewport, ); } let bounds = toggler_layout.bounds(); - let is_mouse_over = cursor.is_over(layout.bounds()); - - let status = if is_mouse_over { - Status::Hovered { - is_toggled: self.is_toggled, - } - } else { - Status::Active { - is_toggled: self.is_toggled, - } - }; - - let style = theme.style(&self.class, status); + let style = theme + .style(&self.class, self.last_status.unwrap_or(Status::Disabled)); let border_radius = bounds.height / BORDER_RADIUS_RATIO; let space = SPACE_RATIO * bounds.height; @@ -392,10 +491,12 @@ pub enum Status { /// Indicates whether the [`Toggler`] is toggled. is_toggled: bool, }, + /// The [`Toggler`] is disabled. + Disabled, } /// The appearance of a toggler. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { /// The background [`Color`] of the toggler. pub background: Color, @@ -452,6 +553,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { palette.background.strong.color } } + Status::Disabled => palette.background.weak.color, }; let foreground = match status { @@ -472,6 +574,7 @@ pub fn default(theme: &Theme, status: Status) -> Style { palette.background.weak.color } } + Status::Disabled => palette.background.base.color, }; Style { diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index 39f2e07d..2d674bca 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -1,6 +1,27 @@ -//! Display a widget over another. +//! Tooltips display a hint of information over some element when hovered. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } } +//! # pub type State = (); +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! use iced::widget::{container, tooltip}; +//! +//! enum Message { +//! // ... +//! } +//! +//! fn view(_state: &State) -> Element<'_, Message> { +//! tooltip( +//! "Hover me to display the tooltip!", +//! container("This is the tooltip contents!") +//! .padding(10) +//! .style(container::rounded_box), +//! tooltip::Position::Bottom, +//! ).into() +//! } +//! ``` use crate::container; -use crate::core::event::{self, Event}; use crate::core::layout::{self, Layout}; use crate::core::mouse; use crate::core::overlay; @@ -8,11 +29,33 @@ use crate::core::renderer; use crate::core::text; use crate::core::widget::{self, Widget}; use crate::core::{ - Clipboard, Element, Length, Padding, Pixels, Point, Rectangle, Shell, Size, - Vector, + Clipboard, Element, Event, Length, Padding, Pixels, Point, Rectangle, + Shell, Size, Vector, }; /// An element to display a widget over another. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{container, tooltip}; +/// +/// enum Message { +/// // ... +/// } +/// +/// fn view(_state: &State) -> Element<'_, Message> { +/// tooltip( +/// "Hover me to display the tooltip!", +/// container("This is the tooltip contents!") +/// .padding(10) +/// .style(container::rounded_box), +/// tooltip::Position::Bottom, +/// ).into() +/// } +/// ``` #[allow(missing_debug_implementations)] pub struct Tooltip< 'a, @@ -99,8 +142,8 @@ where } } -impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for Tooltip<'a, Message, Theme, Renderer> +impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for Tooltip<'_, Message, Theme, Renderer> where Theme: container::Catalog, Renderer: text::Renderer, @@ -146,17 +189,17 @@ where .layout(&mut tree.children[0], renderer, limits) } - fn on_event( + fn update( &mut self, tree: &mut widget::Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let state = tree.state.downcast_mut::<State>(); let was_idle = *state == State::Idle; @@ -170,9 +213,12 @@ where if was_idle != is_idle { shell.invalidate_layout(); + shell.request_redraw(); + } else if !is_idle && self.position == Position::FollowCursor { + shell.request_redraw(); } - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], event, layout, @@ -181,7 +227,7 @@ where clipboard, shell, viewport, - ) + ); } fn mouse_interaction( @@ -326,9 +372,8 @@ where class: &'b Theme::Class<'a>, } -impl<'a, 'b, Message, Theme, Renderer> - overlay::Overlay<Message, Theme, Renderer> - for Overlay<'a, 'b, Message, Theme, Renderer> +impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer> + for Overlay<'_, '_, Message, Theme, Renderer> where Theme: container::Catalog, Renderer: text::Renderer, @@ -425,8 +470,10 @@ where layout::Node::with_children( tooltip_bounds.size(), - vec![tooltip_layout - .translate(Vector::new(self.padding, self.padding))], + vec![ + tooltip_layout + .translate(Vector::new(self.padding, self.padding)), + ], ) .translate(Vector::new(tooltip_bounds.x, tooltip_bounds.y)) } diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index defb442f..436c2345 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -1,11 +1,40 @@ -//! Display an interactive selector of a single value from a range of values. +//! Sliders let users set a value by moving an indicator. +//! +//! # Example +//! ```no_run +//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +//! # +//! use iced::widget::slider; +//! +//! struct State { +//! value: f32, +//! } +//! +//! #[derive(Debug, Clone)] +//! enum Message { +//! ValueChanged(f32), +//! } +//! +//! fn view(state: &State) -> Element<'_, Message> { +//! slider(0.0..=100.0, state.value, Message::ValueChanged).into() +//! } +//! +//! fn update(state: &mut State, message: Message) { +//! match message { +//! Message::ValueChanged(value) => { +//! state.value = value; +//! } +//! } +//! } +//! ``` use std::ops::RangeInclusive; pub use crate::slider::{ - default, Catalog, Handle, HandleShape, Status, Style, StyleFn, + Catalog, Handle, HandleShape, Status, Style, StyleFn, default, }; -use crate::core::event::{self, Event}; +use crate::core::border::Border; use crate::core::keyboard; use crate::core::keyboard::key::{self, Key}; use crate::core::layout::{self, Layout}; @@ -13,8 +42,9 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; +use crate::core::window; use crate::core::{ - self, Border, Clipboard, Element, Length, Pixels, Point, Rectangle, Shell, + self, Clipboard, Element, Event, Length, Pixels, Point, Rectangle, Shell, Size, Widget, }; @@ -28,16 +58,31 @@ use crate::core::{ /// /// # Example /// ```no_run -/// # type VerticalSlider<'a, T, Message> = iced_widget::VerticalSlider<'a, T, Message>; +/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; } +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; /// # -/// #[derive(Clone)] -/// pub enum Message { -/// SliderChanged(f32), +/// use iced::widget::vertical_slider; +/// +/// struct State { +/// value: f32, /// } /// -/// let value = 50.0; +/// #[derive(Debug, Clone)] +/// enum Message { +/// ValueChanged(f32), +/// } /// -/// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged); +/// fn view(state: &State) -> Element<'_, Message> { +/// vertical_slider(0.0..=100.0, state.value, Message::ValueChanged).into() +/// } +/// +/// fn update(state: &mut State, message: Message) { +/// match message { +/// Message::ValueChanged(value) => { +/// state.value = value; +/// } +/// } +/// } /// ``` #[allow(missing_debug_implementations)] pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme> @@ -54,6 +99,7 @@ where width: f32, height: Length, class: Theme::Class<'a>, + status: Option<Status>, } impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme> @@ -71,8 +117,8 @@ where /// * an inclusive range of possible values /// * the current value of the [`VerticalSlider`] /// * a function that will be called when the [`VerticalSlider`] is dragged. - /// It receives the new value of the [`VerticalSlider`] and must produce a - /// `Message`. + /// It receives the new value of the [`VerticalSlider`] and must produce a + /// `Message`. pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self where F: 'a + Fn(T) -> Message, @@ -100,6 +146,7 @@ where width: Self::DEFAULT_WIDTH, height: Length::Fill, class: Theme::default(), + status: None, } } @@ -167,8 +214,8 @@ where } } -impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> - for VerticalSlider<'a, T, Message, Theme> +impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> + for VerticalSlider<'_, T, Message, Theme> where T: Copy + Into<f64> + num_traits::FromPrimitive, Message: Clone, @@ -199,17 +246,17 @@ where layout::atomic(limits, self.width, self.height) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, _renderer: &Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let state = tree.state.downcast_mut::<State>(); let is_dragging = state.is_dragging; let current_value = self.value; @@ -239,7 +286,7 @@ where let steps = (percent * (end - start) / step).round(); let value = steps * step + start; - T::from_f64(value) + T::from_f64(value.min(end)) }; new_value @@ -305,7 +352,7 @@ where state.is_dragging = true; } - return event::Status::Captured; + shell.capture_event(); } } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) @@ -317,7 +364,7 @@ where } state.is_dragging = false; - return event::Status::Captured; + shell.capture_event(); } } Event::Mouse(mouse::Event::CursorMoved { .. }) @@ -325,11 +372,29 @@ where if is_dragging { let _ = cursor.position().and_then(locate).map(change); - return event::Status::Captured; + shell.capture_event(); + } + } + Event::Mouse(mouse::Event::WheelScrolled { delta }) + if state.keyboard_modifiers.control() => + { + if cursor.is_over(layout.bounds()) { + let delta = match *delta { + mouse::ScrollDelta::Lines { x: _, y } => y, + mouse::ScrollDelta::Pixels { x: _, y } => y, + }; + + if delta < 0.0 { + let _ = decrement(current_value).map(change); + } else { + let _ = increment(current_value).map(change); + } + + shell.capture_event(); } } Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { - if cursor.position_over(layout.bounds()).is_some() { + if cursor.is_over(layout.bounds()) { match key { Key::Named(key::Named::ArrowUp) => { let _ = increment(current_value).map(change); @@ -340,42 +405,44 @@ where _ => (), } - return event::Status::Captured; + shell.capture_event(); } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - state.keyboard_modifiers = modifiers; + state.keyboard_modifiers = *modifiers; } _ => {} } - event::Status::Ignored + let current_status = if state.is_dragging { + Status::Dragged + } else if cursor.is_over(layout.bounds()) { + Status::Hovered + } else { + Status::Active + }; + + if let Event::Window(window::Event::RedrawRequested(_now)) = event { + self.status = Some(current_status); + } else if self.status.is_some_and(|status| status != current_status) { + shell.request_redraw(); + } } fn draw( &self, - tree: &Tree, + _tree: &Tree, renderer: &mut Renderer, theme: &Theme, _style: &renderer::Style, layout: Layout<'_>, - cursor: mouse::Cursor, + _cursor: mouse::Cursor, _viewport: &Rectangle, ) { - let state = tree.state.downcast_ref::<State>(); let bounds = layout.bounds(); - let is_mouse_over = cursor.is_over(bounds); - let style = theme.style( - &self.class, - if state.is_dragging { - Status::Dragged - } else if is_mouse_over { - Status::Hovered - } else { - Status::Active - }, - ); + let style = + theme.style(&self.class, self.status.unwrap_or(Status::Active)); let (handle_width, handle_height, handle_border_radius) = match style.handle.shape { @@ -412,10 +479,10 @@ where width: style.rail.width, height: offset + handle_width / 2.0, }, - border: Border::rounded(style.rail.border_radius), + border: style.rail.border, ..renderer::Quad::default() }, - style.rail.colors.1, + style.rail.backgrounds.1, ); renderer.fill_quad( @@ -426,10 +493,10 @@ where width: style.rail.width, height: bounds.height - offset - handle_width / 2.0, }, - border: Border::rounded(style.rail.border_radius), + border: style.rail.border, ..renderer::Quad::default() }, - style.rail.colors.0, + style.rail.backgrounds.0, ); renderer.fill_quad( @@ -447,7 +514,7 @@ where }, ..renderer::Quad::default() }, - style.handle.color, + style.handle.background, ); } diff --git a/winit/Cargo.toml b/winit/Cargo.toml index bb1a8321..d5eb9563 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -17,12 +17,12 @@ workspace = true default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] debug = ["iced_debug/enable"] system = ["sysinfo"] -application = [] +program = [] x11 = ["winit/x11"] wayland = ["winit/wayland"] wayland-dlopen = ["winit/wayland-dlopen"] wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] -multi-window = ["iced_runtime/multi-window"] +unconditional-rendering = [] [dependencies] iced_debug.workspace = true @@ -34,17 +34,13 @@ log.workspace = true rustc-hash.workspace = true thiserror.workspace = true tracing.workspace = true -wasm-bindgen-futures.workspace = true window_clipboard.workspace = true winit.workspace = true sysinfo.workspace = true sysinfo.optional = true -[target.'cfg(target_os = "windows")'.dependencies] -winapi.workspace = true - [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys.workspace = true -web-sys.features = ["Document", "Window"] - +web-sys.features = ["Document", "Window", "HtmlCanvasElement"] +wasm-bindgen-futures.workspace = true diff --git a/winit/README.md b/winit/README.md index 91307970..c60e81f9 100644 --- a/winit/README.md +++ b/winit/README.md @@ -15,15 +15,3 @@ It exposes a renderer-agnostic `Application` trait that can be implemented and t [documentation]: https://docs.rs/iced_winit [`iced_native`]: ../native [`winit`]: https://github.com/rust-windowing/winit - -## Installation -Add `iced_winit` as a dependency in your `Cargo.toml`: - -```toml -iced_winit = "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 diff --git a/winit/src/application.rs b/winit/src/application.rs deleted file mode 100644 index 1a4b35bd..00000000 --- a/winit/src/application.rs +++ /dev/null @@ -1,1157 +0,0 @@ -//! Create interactive, native cross-platform applications. -mod state; - -pub use state::State; - -use crate::conversion; -use crate::core; -use crate::core::mouse; -use crate::core::renderer; -use crate::core::theme; -use crate::core::time::Instant; -use crate::core::widget::operation; -use crate::core::window; -use crate::core::{Color, Event, Point, Size, Theme}; -use crate::debug; -use crate::futures::futures; -use crate::futures::{Executor, Runtime, Subscription}; -use crate::graphics; -use crate::graphics::compositor::{self, Compositor}; -use crate::runtime::clipboard; -use crate::runtime::program::Program; -use crate::runtime::user_interface::{self, UserInterface}; -use crate::runtime::Command; -use crate::{Clipboard, Error, Proxy, Settings}; - -use futures::channel::mpsc; -use futures::channel::oneshot; - -use std::borrow::Cow; -use std::mem::ManuallyDrop; -use std::sync::Arc; - -/// An interactive, native cross-platform application. -/// -/// This trait is the main entrypoint of Iced. Once implemented, you can run -/// your GUI application by simply calling [`run`]. It will run in -/// its own window. -/// -/// An [`Application`] can execute asynchronous actions by returning a -/// [`Command`] in some of its methods. -/// -/// When using an [`Application`] with the `debug` feature enabled, a debug view -/// can be toggled by pressing `F12`. -pub trait Application: Program -where - Self::Theme: DefaultStyle, -{ - /// The data needed to initialize your [`Application`]. - type Flags; - - /// Returns the unique name of the [`Application`]. - fn name() -> &'static str; - - /// Initializes the [`Application`] with the flags provided to - /// [`run`] as part of the [`Settings`]. - /// - /// Here is where you should return the initial state of your app. - /// - /// Additionally, you can return a [`Command`] if you need to perform some - /// async action in the background on startup. This is useful if you want to - /// load state from a file, perform an initial HTTP request, etc. - fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); - - /// Returns the current title of the [`Application`]. - /// - /// This title can be dynamic! The runtime will automatically update the - /// title of your application when necessary. - fn title(&self) -> String; - - /// Returns the current `Theme` of the [`Application`]. - fn theme(&self) -> Self::Theme; - - /// Returns the `Style` variation of the `Theme`. - fn style(&self, theme: &Self::Theme) -> Appearance { - theme.default_style() - } - - /// Returns the event `Subscription` for the current state of the - /// application. - /// - /// The messages produced by the `Subscription` will be handled by - /// [`update`](#tymethod.update). - /// - /// A `Subscription` will be kept alive as long as you keep returning it! - /// - /// By default, it returns an empty subscription. - fn subscription(&self) -> Subscription<Self::Message> { - Subscription::none() - } - - /// Returns the scale factor of the [`Application`]. - /// - /// It can be used to dynamically control the size of the UI at runtime - /// (i.e. zooming). - /// - /// For instance, a scale factor of `2.0` will make widgets twice as big, - /// while a scale factor of `0.5` will shrink them to half their size. - /// - /// By default, it returns `1.0`. - fn scale_factor(&self) -> f64 { - 1.0 - } -} - -/// The appearance of an application. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Appearance { - /// The background [`Color`] of the application. - pub background_color: Color, - - /// The default text [`Color`] of the application. - pub text_color: Color, -} - -/// The default style of an [`Application`]. -pub trait DefaultStyle { - /// Returns the default style of an [`Application`]. - fn default_style(&self) -> Appearance; - - /// Returns a [`Palette`] for the [`Application`], if possible. - fn palette(&self) -> Option<theme::Palette> { - None - } -} - -impl DefaultStyle for Theme { - fn default_style(&self) -> Appearance { - default(self) - } - - fn palette(&self) -> Option<theme::Palette> { - Some(self.palette()) - } -} - -/// The default [`Appearance`] of an [`Application`] with the built-in [`Theme`]. -pub fn default(theme: &Theme) -> Appearance { - let palette = theme.extended_palette(); - - Appearance { - background_color: palette.background.base.color, - text_color: palette.background.base.text, - } -} - -/// Runs an [`Application`] with an executor, compositor, and the provided -/// settings. -pub fn run<A, E, C>( - settings: Settings<A::Flags>, - graphics_settings: graphics::Settings, -) -> Result<(), Error> -where - A: Application + 'static, - E: Executor + 'static, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - use futures::task; - use futures::Future; - use winit::event_loop::EventLoop; - - debug::init(A::name()); - let boot_span = debug::boot(); - - let event_loop = EventLoop::with_user_event() - .build() - .expect("Create event loop"); - - let (proxy, worker) = Proxy::new(event_loop.create_proxy()); - - let runtime = { - let executor = E::new().map_err(Error::ExecutorCreationFailed)?; - executor.spawn(worker); - - Runtime::new(executor, proxy.clone()) - }; - - let (application, init_command) = { - let flags = settings.flags; - - runtime.enter(|| A::new(flags)) - }; - - let id = settings.id; - let title = application.title(); - - let (boot_sender, boot_receiver) = oneshot::channel(); - let (event_sender, event_receiver) = mpsc::unbounded(); - let (control_sender, control_receiver) = mpsc::unbounded(); - - let instance = Box::pin(run_instance::<A, E, C>( - application, - runtime, - proxy, - boot_receiver, - event_receiver, - control_sender, - init_command, - settings.fonts, - boot_span, - )); - - let context = task::Context::from_waker(task::noop_waker_ref()); - - struct Runner<Message: 'static, F, C> { - instance: std::pin::Pin<Box<F>>, - context: task::Context<'static>, - boot: Option<BootConfig<C>>, - sender: mpsc::UnboundedSender<winit::event::Event<Message>>, - receiver: mpsc::UnboundedReceiver<winit::event_loop::ControlFlow>, - error: Option<Error>, - #[cfg(target_arch = "wasm32")] - is_booted: std::rc::Rc<std::cell::RefCell<bool>>, - #[cfg(target_arch = "wasm32")] - queued_events: Vec<winit::event::Event<Message>>, - } - - struct BootConfig<C> { - sender: oneshot::Sender<Boot<C>>, - id: Option<String>, - title: String, - window_settings: window::Settings, - graphics_settings: graphics::Settings, - } - - let runner = Runner { - instance, - context, - boot: Some(BootConfig { - sender: boot_sender, - id, - title, - window_settings: settings.window, - graphics_settings, - }), - sender: event_sender, - receiver: control_receiver, - error: None, - #[cfg(target_arch = "wasm32")] - is_booted: std::rc::Rc::new(std::cell::RefCell::new(false)), - #[cfg(target_arch = "wasm32")] - queued_events: Vec::new(), - }; - - impl<Message, F, C> winit::application::ApplicationHandler<Message> - for Runner<Message, F, C> - where - F: Future<Output = ()>, - C: Compositor + 'static, - { - fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { - let Some(BootConfig { - sender, - id, - title, - window_settings, - graphics_settings, - }) = self.boot.take() - else { - return; - }; - - let should_be_visible = window_settings.visible; - let exit_on_close_request = window_settings.exit_on_close_request; - - #[cfg(target_arch = "wasm32")] - let target = window_settings.platform_specific.target.clone(); - - let window_attributes = conversion::window_attributes( - window_settings, - &title, - event_loop.primary_monitor(), - id, - ) - .with_visible(false); - - log::debug!("Window attributes: {window_attributes:#?}"); - - let window = match event_loop.create_window(window_attributes) { - Ok(window) => Arc::new(window), - Err(error) => { - self.error = Some(Error::WindowCreationFailed(error)); - event_loop.exit(); - return; - } - }; - - let finish_boot = { - let window = window.clone(); - - async move { - let compositor = - C::new(graphics_settings, window.clone()).await?; - - sender - .send(Boot { - window, - compositor, - should_be_visible, - exit_on_close_request, - }) - .ok() - .expect("Send boot event"); - - Ok::<_, graphics::Error>(()) - } - }; - - #[cfg(not(target_arch = "wasm32"))] - if let Err(error) = futures::executor::block_on(finish_boot) { - self.error = Some(Error::GraphicsCreationFailed(error)); - event_loop.exit(); - } - - #[cfg(target_arch = "wasm32")] - { - use winit::platform::web::WindowExtWebSys; - - let canvas = window.canvas().expect("Get window canvas"); - let _ = canvas.set_attribute( - "style", - "display: block; width: 100%; height: 100%", - ); - - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let body = document.body().unwrap(); - - let target = target.and_then(|target| { - body.query_selector(&format!("#{target}")) - .ok() - .unwrap_or(None) - }); - - match target { - Some(node) => { - let _ = node.replace_with_with_node_1(&canvas).expect( - &format!("Could not replace #{}", node.id()), - ); - } - None => { - let _ = body - .append_child(&canvas) - .expect("Append canvas to HTML body"); - } - }; - - let is_booted = self.is_booted.clone(); - - wasm_bindgen_futures::spawn_local(async move { - finish_boot.await.expect("Finish boot!"); - - *is_booted.borrow_mut() = true; - }); - } - } - - fn new_events( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - cause: winit::event::StartCause, - ) { - if self.boot.is_some() { - return; - } - - self.process_event( - event_loop, - winit::event::Event::NewEvents(cause), - ); - } - - fn window_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - window_id: winit::window::WindowId, - event: winit::event::WindowEvent, - ) { - #[cfg(target_os = "windows")] - let is_move_or_resize = matches!( - event, - winit::event::WindowEvent::Resized(_) - | winit::event::WindowEvent::Moved(_) - ); - - self.process_event( - event_loop, - winit::event::Event::WindowEvent { window_id, event }, - ); - - // TODO: Remove when unnecessary - // On Windows, we emulate an `AboutToWait` event after every `Resized` event - // since the event loop does not resume during resize interaction. - // More details: https://github.com/rust-windowing/winit/issues/3272 - #[cfg(target_os = "windows")] - { - if is_move_or_resize { - self.process_event( - event_loop, - winit::event::Event::AboutToWait, - ); - } - } - } - - fn user_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - message: Message, - ) { - self.process_event( - event_loop, - winit::event::Event::UserEvent(message), - ); - } - - fn about_to_wait( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - ) { - self.process_event(event_loop, winit::event::Event::AboutToWait); - } - } - - impl<Message, F, C> Runner<Message, F, C> - where - F: Future<Output = ()>, - { - fn process_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - event: winit::event::Event<Message>, - ) { - // On Wasm, events may start being processed before the compositor - // boots up. We simply queue them and process them once ready. - #[cfg(target_arch = "wasm32")] - if !*self.is_booted.borrow() { - self.queued_events.push(event); - return; - } else if !self.queued_events.is_empty() { - let queued_events = std::mem::take(&mut self.queued_events); - - // This won't infinitely recurse, since we `mem::take` - for event in queued_events { - self.process_event(event_loop, event); - } - } - - if event_loop.exiting() { - return; - } - - self.sender.start_send(event).expect("Send event"); - - let poll = self.instance.as_mut().poll(&mut self.context); - - match poll { - task::Poll::Pending => { - if let Ok(Some(flow)) = self.receiver.try_next() { - event_loop.set_control_flow(flow); - } - } - task::Poll::Ready(_) => { - event_loop.exit(); - } - } - } - } - - #[cfg(not(target_arch = "wasm32"))] - { - let mut runner = runner; - let _ = event_loop.run_app(&mut runner); - - runner.error.map(Err).unwrap_or(Ok(())) - } - - #[cfg(target_arch = "wasm32")] - { - use winit::platform::web::EventLoopExtWebSys; - let _ = event_loop.spawn_app(runner); - - Ok(()) - } -} - -struct Boot<C> { - window: Arc<winit::window::Window>, - compositor: C, - should_be_visible: bool, - exit_on_close_request: bool, -} - -async fn run_instance<A, E, C>( - mut application: A, - mut runtime: Runtime<E, Proxy<A::Message>, A::Message>, - mut proxy: Proxy<A::Message>, - mut boot: oneshot::Receiver<Boot<C>>, - mut event_receiver: mpsc::UnboundedReceiver< - winit::event::Event<A::Message>, - >, - mut control_sender: mpsc::UnboundedSender<winit::event_loop::ControlFlow>, - init_command: Command<A::Message>, - fonts: Vec<Cow<'static, [u8]>>, - boot_span: debug::Span, -) where - A: Application + 'static, - E: Executor + 'static, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - use futures::stream::StreamExt; - use winit::event; - use winit::event_loop::ControlFlow; - - let Boot { - window, - mut compositor, - should_be_visible, - exit_on_close_request, - } = boot.try_recv().ok().flatten().expect("Receive boot"); - - let mut renderer = compositor.create_renderer(); - - for font in fonts { - compositor.load_font(font); - } - - let mut state = State::new(&application, &window); - let mut viewport_version = state.viewport_version(); - let physical_size = state.physical_size(); - - let mut clipboard = Clipboard::connect(&window); - let mut cache = user_interface::Cache::default(); - let mut surface = compositor.create_surface( - window.clone(), - physical_size.width, - physical_size.height, - ); - let mut should_exit = false; - - if should_be_visible { - window.set_visible(true); - } - - run_command( - &application, - &mut compositor, - &mut surface, - &mut cache, - &state, - &mut renderer, - init_command, - &mut runtime, - &mut clipboard, - &mut should_exit, - &mut proxy, - &window, - ); - - let recipes = application.subscription().into_recipes(); - debug::subscriptions_tracked(recipes.len()); - runtime.track(recipes); - - boot_span.finish(); - - let mut user_interface = ManuallyDrop::new(build_user_interface( - &application, - cache, - &mut renderer, - state.logical_size(), - )); - - let mut mouse_interaction = mouse::Interaction::default(); - let mut events = Vec::new(); - let mut messages = Vec::new(); - let mut user_events = 0; - let mut redraw_pending = false; - - while let Some(event) = event_receiver.next().await { - match event { - event::Event::NewEvents( - event::StartCause::Init - | event::StartCause::ResumeTimeReached { .. }, - ) if !redraw_pending => { - window.request_redraw(); - redraw_pending = true; - } - event::Event::PlatformSpecific(event::PlatformSpecific::MacOS( - event::MacOS::ReceivedUrl(url), - )) => { - use crate::core::event; - - events.push(Event::PlatformSpecific( - event::PlatformSpecific::MacOS(event::MacOS::ReceivedUrl( - url, - )), - )); - } - event::Event::UserEvent(message) => { - messages.push(message); - user_events += 1; - } - event::Event::WindowEvent { - event: event::WindowEvent::RedrawRequested { .. }, - .. - } => { - let physical_size = state.physical_size(); - - if physical_size.width == 0 || physical_size.height == 0 { - continue; - } - - let current_viewport_version = state.viewport_version(); - - if viewport_version != current_viewport_version { - let logical_size = state.logical_size(); - - let layout_span = debug::layout(window::Id::MAIN); - user_interface = ManuallyDrop::new( - ManuallyDrop::into_inner(user_interface) - .relayout(logical_size, &mut renderer), - ); - layout_span.finish(); - - compositor.configure_surface( - &mut surface, - physical_size.width, - physical_size.height, - ); - - viewport_version = current_viewport_version; - } - - // TODO: Avoid redrawing all the time by forcing widgets to - // request redraws on state changes - // - // Then, we can use the `interface_state` here to decide if a redraw - // is needed right away, or simply wait until a specific time. - let redraw_event = Event::Window( - window::Id::MAIN, - window::Event::RedrawRequested(Instant::now()), - ); - - let (interface_state, _) = user_interface.update( - &[redraw_event.clone()], - state.cursor(), - &mut renderer, - &mut clipboard, - &mut messages, - ); - - let _ = control_sender.start_send(match interface_state { - user_interface::State::Updated { - redraw_request: Some(redraw_request), - } => match redraw_request { - window::RedrawRequest::NextFrame => { - window.request_redraw(); - - ControlFlow::Wait - } - window::RedrawRequest::At(at) => { - ControlFlow::WaitUntil(at) - } - }, - _ => ControlFlow::Wait, - }); - - runtime.broadcast(redraw_event, core::event::Status::Ignored); - - let draw_span = debug::draw(window::Id::MAIN); - let new_mouse_interaction = user_interface.draw( - &mut renderer, - state.theme(), - &renderer::Style { - text_color: state.text_color(), - }, - state.cursor(), - ); - redraw_pending = false; - draw_span.finish(); - - if new_mouse_interaction != mouse_interaction { - window.set_cursor(conversion::mouse_interaction( - new_mouse_interaction, - )); - - mouse_interaction = new_mouse_interaction; - } - - let present_span = debug::present(window::Id::MAIN); - match compositor.present( - &mut renderer, - &mut surface, - state.viewport(), - state.background_color(), - ) { - Ok(()) => { - present_span.finish(); - } - Err(error) => match error { - // This is an unrecoverable error. - compositor::SurfaceError::OutOfMemory => { - panic!("{error:?}"); - } - _ => { - // Try rendering again next frame. - window.request_redraw(); - } - }, - } - } - event::Event::WindowEvent { - event: window_event, - .. - } => { - if requests_exit(&window_event, state.modifiers()) - && exit_on_close_request - { - break; - } - - #[cfg(feature = "debug")] - if let winit::event::WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key: - winit::keyboard::Key::Named( - winit::keyboard::NamedKey::F12, - ), - state: winit::event::ElementState::Pressed, - repeat: false, - .. - }, - .. - } = &window_event - { - crate::debug::toggle_comet(); - } - - state.update(&window, &window_event); - - if let Some(event) = conversion::window_event( - window::Id::MAIN, - window_event, - state.scale_factor(), - state.modifiers(), - ) { - events.push(event); - } - } - event::Event::AboutToWait => { - if events.is_empty() && messages.is_empty() { - continue; - } - - let interface_state = if events.is_empty() { - user_interface::State::Updated { - redraw_request: None, - } - } else { - let interact_span = debug::interact(window::Id::MAIN); - let (interface_state, statuses) = user_interface.update( - &events, - state.cursor(), - &mut renderer, - &mut clipboard, - &mut messages, - ); - - for (event, status) in - events.drain(..).zip(statuses.into_iter()) - { - runtime.broadcast(event, status); - } - interact_span.finish(); - - interface_state - }; - - if !messages.is_empty() - || matches!( - interface_state, - user_interface::State::Outdated - ) - { - let mut cache = - ManuallyDrop::into_inner(user_interface).into_cache(); - - // Update application - update( - &mut application, - &mut compositor, - &mut surface, - &mut cache, - &mut state, - &mut renderer, - &mut runtime, - &mut clipboard, - &mut should_exit, - &mut proxy, - &mut messages, - &window, - ); - - user_interface = ManuallyDrop::new(build_user_interface( - &application, - cache, - &mut renderer, - state.logical_size(), - )); - - if should_exit { - break; - } - - if user_events > 0 { - proxy.free_slots(user_events); - user_events = 0; - } - } - - if !redraw_pending { - window.request_redraw(); - redraw_pending = true; - } - } - _ => {} - } - } - - // Manually drop the user interface - drop(ManuallyDrop::into_inner(user_interface)); -} - -/// Returns true if the provided event should cause an [`Application`] to -/// exit. -pub fn requests_exit( - event: &winit::event::WindowEvent, - _modifiers: winit::keyboard::ModifiersState, -) -> bool { - use winit::event::WindowEvent; - - match event { - WindowEvent::CloseRequested => true, - #[cfg(target_os = "macos")] - WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key: winit::keyboard::Key::Character(c), - state: winit::event::ElementState::Pressed, - .. - }, - .. - } if c == "q" && _modifiers.super_key() => true, - _ => false, - } -} - -/// Builds a [`UserInterface`] for the provided [`Application`], logging -/// [`struct@Debug`] information accordingly. -pub fn build_user_interface<'a, A: Application>( - application: &'a A, - cache: user_interface::Cache, - renderer: &mut A::Renderer, - size: Size, -) -> UserInterface<'a, A::Message, A::Theme, A::Renderer> -where - A::Theme: DefaultStyle, -{ - let view_span = debug::view(window::Id::MAIN); - let view = application.view(); - view_span.finish(); - - let layout_span = debug::layout(window::Id::MAIN); - let user_interface = UserInterface::build(view, size, cache, renderer); - layout_span.finish(); - - user_interface -} - -/// Updates an [`Application`] by feeding it the provided messages, spawning any -/// resulting [`Command`], and tracking its [`Subscription`]. -pub fn update<A: Application, C, E: Executor>( - application: &mut A, - compositor: &mut C, - surface: &mut C::Surface, - cache: &mut user_interface::Cache, - state: &mut State<A>, - renderer: &mut A::Renderer, - runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, - clipboard: &mut Clipboard, - should_exit: &mut bool, - proxy: &mut Proxy<A::Message>, - messages: &mut Vec<A::Message>, - window: &winit::window::Window, -) where - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - for message in messages.drain(..) { - let update_span = debug::update(&message); - let command = runtime.enter(|| application.update(message)); - - run_command( - application, - compositor, - surface, - cache, - state, - renderer, - command, - runtime, - clipboard, - should_exit, - proxy, - window, - ); - update_span.finish(); - } - - state.synchronize(application, window); - - let recipes = application.subscription().into_recipes(); - debug::subscriptions_tracked(recipes.len()); - runtime.track(recipes); -} - -/// Runs the actions of a [`Command`]. -pub fn run_command<A, C, E>( - application: &A, - compositor: &mut C, - surface: &mut C::Surface, - cache: &mut user_interface::Cache, - state: &State<A>, - renderer: &mut A::Renderer, - command: Command<A::Message>, - runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, - clipboard: &mut Clipboard, - should_exit: &mut bool, - proxy: &mut Proxy<A::Message>, - window: &winit::window::Window, -) where - A: Application, - E: Executor, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - use crate::runtime::command; - use crate::runtime::system; - use crate::runtime::window; - - let actions = command.actions(); - debug::commands_spawned(actions.len()); - - for action in actions { - match action { - command::Action::Future(future) => { - runtime.spawn(future); - } - command::Action::Stream(stream) => { - runtime.run(stream); - } - command::Action::Clipboard(action) => match action { - clipboard::Action::Read(tag, kind) => { - let message = tag(clipboard.read(kind)); - - proxy.send(message); - } - clipboard::Action::Write(contents, kind) => { - clipboard.write(kind, contents); - } - }, - command::Action::Window(action) => match action { - window::Action::Close(_id) => { - *should_exit = true; - } - window::Action::Drag(_id) => { - let _res = window.drag_window(); - } - window::Action::Spawn { .. } => { - log::warn!( - "Spawning a window is only available with \ - multi-window applications." - ); - } - window::Action::Resize(_id, size) => { - let _ = - window.request_inner_size(winit::dpi::LogicalSize { - width: size.width, - height: size.height, - }); - } - window::Action::FetchSize(_id, callback) => { - let size = - window.inner_size().to_logical(window.scale_factor()); - - proxy.send(callback(Size::new(size.width, size.height))); - } - window::Action::FetchMaximized(_id, callback) => { - proxy.send(callback(window.is_maximized())); - } - window::Action::Maximize(_id, maximized) => { - window.set_maximized(maximized); - } - window::Action::FetchMinimized(_id, callback) => { - proxy.send(callback(window.is_minimized())); - } - window::Action::Minimize(_id, minimized) => { - window.set_minimized(minimized); - } - window::Action::FetchPosition(_id, callback) => { - let position = window - .inner_position() - .map(|position| { - let position = position - .to_logical::<f32>(window.scale_factor()); - - Point::new(position.x, position.y) - }) - .ok(); - - proxy.send(callback(position)); - } - window::Action::Move(_id, position) => { - window.set_outer_position(winit::dpi::LogicalPosition { - x: position.x, - y: position.y, - }); - } - window::Action::ChangeMode(_id, mode) => { - window.set_visible(conversion::visible(mode)); - window.set_fullscreen(conversion::fullscreen( - window.current_monitor(), - mode, - )); - } - window::Action::ChangeIcon(_id, icon) => { - window.set_window_icon(conversion::icon(icon)); - } - window::Action::FetchMode(_id, tag) => { - let mode = if window.is_visible().unwrap_or(true) { - conversion::mode(window.fullscreen()) - } else { - core::window::Mode::Hidden - }; - - proxy.send(tag(mode)); - } - window::Action::ToggleMaximize(_id) => { - window.set_maximized(!window.is_maximized()); - } - window::Action::ToggleDecorations(_id) => { - window.set_decorations(!window.is_decorated()); - } - window::Action::RequestUserAttention(_id, user_attention) => { - window.request_user_attention( - user_attention.map(conversion::user_attention), - ); - } - window::Action::GainFocus(_id) => { - window.focus_window(); - } - window::Action::ChangeLevel(_id, level) => { - window.set_window_level(conversion::window_level(level)); - } - window::Action::ShowSystemMenu(_id) => { - if let mouse::Cursor::Available(point) = state.cursor() { - window.show_window_menu(winit::dpi::LogicalPosition { - x: point.x, - y: point.y, - }); - } - } - window::Action::FetchId(_id, tag) => { - proxy.send(tag(window.id().into())); - } - window::Action::RunWithHandle(_id, tag) => { - use window::raw_window_handle::HasWindowHandle; - - if let Ok(handle) = window.window_handle() { - proxy.send(tag(handle)); - } - } - - window::Action::Screenshot(_id, tag) => { - let bytes = compositor.screenshot( - renderer, - surface, - state.viewport(), - state.background_color(), - ); - - proxy.send(tag(window::Screenshot::new( - bytes, - state.physical_size(), - ))); - } - }, - command::Action::System(action) => match action { - system::Action::QueryInformation(_tag) => { - #[cfg(feature = "system")] - { - let graphics_info = compositor.fetch_information(); - let mut proxy = proxy.clone(); - - let _ = std::thread::spawn(move || { - let information = - crate::system::information(graphics_info); - - let message = _tag(information); - - proxy.send(message); - }); - } - } - }, - command::Action::Widget(action) => { - let mut current_cache = std::mem::take(cache); - let mut current_operation = Some(action); - - let mut user_interface = build_user_interface( - application, - current_cache, - renderer, - state.logical_size(), - ); - - while let Some(mut operation) = current_operation.take() { - user_interface.operate(renderer, operation.as_mut()); - - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(message) => { - proxy.send(message); - } - operation::Outcome::Chain(next) => { - current_operation = Some(next); - } - } - } - - current_cache = user_interface.into_cache(); - *cache = current_cache; - } - command::Action::LoadFont { bytes, tagger } => { - // TODO: Error handling (?) - compositor.load_font(bytes); - - proxy.send(tagger(Ok(()))); - } - command::Action::Custom(_) => { - log::warn!("Unsupported custom action in `iced_winit` shell"); - } - } - } -} diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs deleted file mode 100644 index e51f5348..00000000 --- a/winit/src/application/state.rs +++ /dev/null @@ -1,209 +0,0 @@ -use crate::application; -use crate::conversion; -use crate::core::mouse; -use crate::core::{Color, Size}; -use crate::debug; -use crate::graphics::Viewport; -use crate::Application; - -use std::marker::PhantomData; -use winit::event::{Touch, WindowEvent}; -use winit::window::Window; - -/// The state of a windowed [`Application`]. -#[allow(missing_debug_implementations)] -pub struct State<A: Application> -where - A::Theme: application::DefaultStyle, -{ - title: String, - scale_factor: f64, - viewport: Viewport, - viewport_version: usize, - cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, - modifiers: winit::keyboard::ModifiersState, - theme: A::Theme, - appearance: application::Appearance, - application: PhantomData<A>, -} - -impl<A: Application> State<A> -where - A::Theme: application::DefaultStyle, -{ - /// Creates a new [`State`] for the provided [`Application`] and window. - pub fn new(application: &A, window: &Window) -> Self { - let title = application.title(); - let scale_factor = application.scale_factor(); - let theme = application.theme(); - let appearance = application.style(&theme); - - debug::theme_changed(|| application::DefaultStyle::palette(&theme)); - - let viewport = { - let physical_size = window.inner_size(); - - Viewport::with_physical_size( - Size::new(physical_size.width, physical_size.height), - window.scale_factor() * scale_factor, - ) - }; - - Self { - title, - scale_factor, - viewport, - viewport_version: 0, - cursor_position: None, - modifiers: winit::keyboard::ModifiersState::default(), - theme, - appearance, - application: PhantomData, - } - } - - /// Returns the current [`Viewport`] of the [`State`]. - pub fn viewport(&self) -> &Viewport { - &self.viewport - } - - /// Returns the version of the [`Viewport`] of the [`State`]. - /// - /// The version is incremented every time the [`Viewport`] changes. - pub fn viewport_version(&self) -> usize { - self.viewport_version - } - - /// Returns the physical [`Size`] of the [`Viewport`] of the [`State`]. - pub fn physical_size(&self) -> Size<u32> { - self.viewport.physical_size() - } - - /// Returns the logical [`Size`] of the [`Viewport`] of the [`State`]. - pub fn logical_size(&self) -> Size<f32> { - self.viewport.logical_size() - } - - /// Returns the current scale factor of the [`Viewport`] of the [`State`]. - pub fn scale_factor(&self) -> f64 { - self.viewport.scale_factor() - } - - /// Returns the current cursor position of the [`State`]. - pub fn cursor(&self) -> mouse::Cursor { - self.cursor_position - .map(|cursor_position| { - conversion::cursor_position( - cursor_position, - self.viewport.scale_factor(), - ) - }) - .map(mouse::Cursor::Available) - .unwrap_or(mouse::Cursor::Unavailable) - } - - /// Returns the current keyboard modifiers of the [`State`]. - pub fn modifiers(&self) -> winit::keyboard::ModifiersState { - self.modifiers - } - - /// Returns the current theme of the [`State`]. - pub fn theme(&self) -> &A::Theme { - &self.theme - } - - /// Returns the current background [`Color`] of the [`State`]. - pub fn background_color(&self) -> Color { - self.appearance.background_color - } - - /// Returns the current text [`Color`] of the [`State`]. - pub fn text_color(&self) -> Color { - self.appearance.text_color - } - - /// Processes the provided window event and updates the [`State`] - /// accordingly. - pub fn update(&mut self, window: &Window, event: &WindowEvent) { - match event { - WindowEvent::Resized(new_size) => { - let size = Size::new(new_size.width, new_size.height); - - self.viewport = Viewport::with_physical_size( - size, - window.scale_factor() * self.scale_factor, - ); - - self.viewport_version = self.viewport_version.wrapping_add(1); - } - WindowEvent::ScaleFactorChanged { - scale_factor: new_scale_factor, - .. - } => { - let size = self.viewport.physical_size(); - - self.viewport = Viewport::with_physical_size( - size, - new_scale_factor * self.scale_factor, - ); - - self.viewport_version = self.viewport_version.wrapping_add(1); - } - WindowEvent::CursorMoved { position, .. } - | WindowEvent::Touch(Touch { - location: position, .. - }) => { - self.cursor_position = Some(*position); - } - WindowEvent::CursorLeft { .. } => { - self.cursor_position = None; - } - WindowEvent::ModifiersChanged(new_modifiers) => { - self.modifiers = new_modifiers.state(); - } - _ => {} - } - } - - /// Synchronizes the [`State`] with its [`Application`] and its respective - /// window. - /// - /// Normally an [`Application`] should be synchronized with its [`State`] - /// and window after calling [`crate::application::update`]. - pub fn synchronize(&mut self, application: &A, window: &Window) { - // Update window title - let new_title = application.title(); - - if self.title != new_title { - window.set_title(&new_title); - - self.title = new_title; - } - - // Update scale factor and size - let new_scale_factor = application.scale_factor(); - let new_size = window.inner_size(); - let current_size = self.viewport.physical_size(); - - if self.scale_factor != new_scale_factor - || (current_size.width, current_size.height) - != (new_size.width, new_size.height) - { - self.viewport = Viewport::with_physical_size( - Size::new(new_size.width, new_size.height), - window.scale_factor() * new_scale_factor, - ); - self.viewport_version = self.viewport_version.wrapping_add(1); - - self.scale_factor = new_scale_factor; - } - - // Update theme and appearance - self.theme = application.theme(); - self.appearance = application.style(&self.theme); - - debug::theme_changed(|| { - application::DefaultStyle::palette(&self.theme) - }); - } -} diff --git a/winit/src/clipboard.rs b/winit/src/clipboard.rs index 5237ca01..d54a1fe0 100644 --- a/winit/src/clipboard.rs +++ b/winit/src/clipboard.rs @@ -1,6 +1,8 @@ //! Access the clipboard. use crate::core::clipboard::Kind; +use std::sync::Arc; +use winit::window::{Window, WindowId}; /// A buffer for short-term storage and transfer within and between /// applications. @@ -10,18 +12,33 @@ pub struct Clipboard { } enum State { - Connected(window_clipboard::Clipboard), + Connected { + clipboard: window_clipboard::Clipboard, + // Held until drop to satisfy the safety invariants of + // `window_clipboard::Clipboard`. + // + // Note that the field ordering is load-bearing. + #[allow(dead_code)] + window: Arc<Window>, + }, Unavailable, } impl Clipboard { /// Creates a new [`Clipboard`] for the given window. - pub fn connect(window: &winit::window::Window) -> Clipboard { + pub fn connect(window: Arc<Window>) -> Clipboard { + // SAFETY: The window handle will stay alive throughout the entire + // lifetime of the `window_clipboard::Clipboard` because we hold + // the `Arc<Window>` together with `State`, and enum variant fields + // get dropped in declaration order. #[allow(unsafe_code)] - let state = unsafe { window_clipboard::Clipboard::connect(window) } - .ok() - .map(State::Connected) - .unwrap_or(State::Unavailable); + let clipboard = + unsafe { window_clipboard::Clipboard::connect(&window) }; + + let state = match clipboard { + Ok(clipboard) => State::Connected { clipboard, window }, + Err(_) => State::Unavailable, + }; Clipboard { state } } @@ -37,7 +54,7 @@ impl Clipboard { /// Reads the current content of the [`Clipboard`] as text. pub fn read(&self, kind: Kind) -> Option<String> { match &self.state { - State::Connected(clipboard) => match kind { + State::Connected { clipboard, .. } => match kind { Kind::Standard => clipboard.read().ok(), Kind::Primary => clipboard.read_primary().and_then(Result::ok), }, @@ -48,7 +65,7 @@ impl Clipboard { /// Writes the given text contents to the [`Clipboard`]. pub fn write(&mut self, kind: Kind, contents: String) { match &mut self.state { - State::Connected(clipboard) => { + State::Connected { clipboard, .. } => { let result = match kind { Kind::Standard => clipboard.write(contents), Kind::Primary => { @@ -66,6 +83,14 @@ impl Clipboard { State::Unavailable => {} } } + + /// Returns the identifier of the window used to create the [`Clipboard`], if any. + pub fn window_id(&self) -> Option<WindowId> { + match &self.state { + State::Connected { window, .. } => Some(window.id()), + State::Unavailable => None, + } + } } impl crate::core::Clipboard for Clipboard { diff --git a/winit/src/conversion.rs b/winit/src/conversion.rs index ea33e610..ab84afff 100644 --- a/winit/src/conversion.rs +++ b/winit/src/conversion.rs @@ -1,7 +1,8 @@ //! Convert [`winit`] types into [`iced_runtime`] types, and viceversa. //! //! [`winit`]: https://github.com/rust-windowing/winit -//! [`iced_runtime`]: https://github.com/iced-rs/iced/tree/0.12/runtime +//! [`iced_runtime`]: https://github.com/iced-rs/iced/tree/0.13/runtime +use crate::core::input_method; use crate::core::keyboard; use crate::core::mouse; use crate::core::touch; @@ -23,6 +24,12 @@ pub fn window_attributes( width: settings.size.width, height: settings.size.height, }) + .with_maximized(settings.maximized) + .with_fullscreen( + settings + .fullscreen + .then_some(winit::window::Fullscreen::Borderless(None)), + ) .with_resizable(settings.resizable) .with_enabled_buttons(if settings.resizable { winit::window::WindowButtons::all() @@ -73,16 +80,16 @@ pub fn window_attributes( #[cfg(target_os = "windows")] { use winit::platform::windows::WindowAttributesExtWindows; - #[allow(unsafe_code)] - unsafe { - attributes = attributes - .with_parent_window(settings.platform_specific.parent); - } + attributes = attributes .with_drag_and_drop(settings.platform_specific.drag_and_drop); attributes = attributes .with_skip_taskbar(settings.platform_specific.skip_taskbar); + + attributes = attributes.with_undecorated_shadow( + settings.platform_specific.undecorated_shadow, + ); } #[cfg(target_os = "macos")] @@ -105,10 +112,14 @@ pub fn window_attributes( { use winit::platform::x11::WindowAttributesExtX11; - attributes = attributes.with_name( - &settings.platform_specific.application_id, - &settings.platform_specific.application_id, - ); + attributes = attributes + .with_override_redirect( + settings.platform_specific.override_redirect, + ) + .with_name( + &settings.platform_specific.application_id, + &settings.platform_specific.application_id, + ); } #[cfg(feature = "wayland")] { @@ -126,27 +137,24 @@ pub fn window_attributes( /// Converts a winit window event into an iced event. pub fn window_event( - id: window::Id, event: winit::event::WindowEvent, scale_factor: f64, modifiers: winit::keyboard::ModifiersState, ) -> Option<Event> { + use winit::event::Ime; use winit::event::WindowEvent; match event { WindowEvent::Resized(new_size) => { let logical_size = new_size.to_logical(scale_factor); - Some(Event::Window( - id, - window::Event::Resized { - width: logical_size.width, - height: logical_size.height, - }, - )) + Some(Event::Window(window::Event::Resized(Size { + width: logical_size.width, + height: logical_size.height, + }))) } WindowEvent::CloseRequested => { - Some(Event::Window(id, window::Event::CloseRequested)) + Some(Event::Window(window::Event::CloseRequested)) } WindowEvent::CursorMoved { position, .. } => { let position = position.to_logical::<f64>(scale_factor); @@ -191,8 +199,10 @@ pub fn window_event( })) } }, + // Ignore keyboard presses/releases during window focus/unfocus + WindowEvent::KeyboardInput { is_synthetic, .. } if is_synthetic => None, WindowEvent::KeyboardInput { event, .. } => Some(Event::Keyboard({ - let logical_key = { + let key = { #[cfg(not(target_arch = "wasm32"))] { use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; @@ -202,7 +212,7 @@ pub fn window_event( #[cfg(target_arch = "wasm32")] { // TODO: Fix inconsistent API on Wasm - event.logical_key + event.logical_key.clone() } }; @@ -223,9 +233,16 @@ pub fn window_event( }.filter(|text| !text.as_str().chars().any(is_private_use)); let winit::event::KeyEvent { - state, location, .. + state, + location, + logical_key, + physical_key, + .. } = event; - let key = key(logical_key); + + let key = self::key(key); + let modified_key = self::key(logical_key); + let physical_key = self::physical_key(physical_key); let modifiers = self::modifiers(modifiers); let location = match location { @@ -245,6 +262,8 @@ pub fn window_event( winit::event::ElementState::Pressed => { keyboard::Event::KeyPressed { key, + modified_key, + physical_key, modifiers, location, text, @@ -253,6 +272,8 @@ pub fn window_event( winit::event::ElementState::Released => { keyboard::Event::KeyReleased { key, + modified_key, + physical_key, modifiers, location, } @@ -264,22 +285,28 @@ pub fn window_event( self::modifiers(new_modifiers.state()), ))) } - WindowEvent::Focused(focused) => Some(Event::Window( - id, - if focused { - window::Event::Focused - } else { - window::Event::Unfocused - }, - )), + WindowEvent::Ime(event) => Some(Event::InputMethod(match event { + Ime::Enabled => input_method::Event::Opened, + Ime::Preedit(content, size) => input_method::Event::Preedit( + content, + size.map(|(start, end)| (start..end)), + ), + Ime::Commit(content) => input_method::Event::Commit(content), + Ime::Disabled => input_method::Event::Closed, + })), + WindowEvent::Focused(focused) => Some(Event::Window(if focused { + window::Event::Focused + } else { + window::Event::Unfocused + })), WindowEvent::HoveredFile(path) => { - Some(Event::Window(id, window::Event::FileHovered(path.clone()))) + Some(Event::Window(window::Event::FileHovered(path.clone()))) } WindowEvent::DroppedFile(path) => { - Some(Event::Window(id, window::Event::FileDropped(path.clone()))) + Some(Event::Window(window::Event::FileDropped(path.clone()))) } WindowEvent::HoveredFileCancelled => { - Some(Event::Window(id, window::Event::FilesHoveredLeft)) + Some(Event::Window(window::Event::FilesHoveredLeft)) } WindowEvent::Touch(touch) => { Some(Event::Touch(touch_event(touch, scale_factor))) @@ -288,7 +315,7 @@ pub fn window_event( let winit::dpi::LogicalPosition { x, y } = position.to_logical(scale_factor); - Some(Event::Window(id, window::Event::Moved { x, y })) + Some(Event::Window(window::Event::Moved(Point::new(x, y)))) } _ => None, } @@ -434,8 +461,19 @@ pub fn mouse_interaction( winit::window::CursorIcon::EwResize } Interaction::ResizingVertically => winit::window::CursorIcon::NsResize, + Interaction::ResizingDiagonallyUp => { + winit::window::CursorIcon::NeswResize + } + Interaction::ResizingDiagonallyDown => { + winit::window::CursorIcon::NwseResize + } Interaction::NotAllowed => winit::window::CursorIcon::NotAllowed, Interaction::ZoomIn => winit::window::CursorIcon::ZoomIn, + Interaction::ZoomOut => winit::window::CursorIcon::ZoomOut, + Interaction::Cell => winit::window::CursorIcon::Cell, + Interaction::Move => winit::window::CursorIcon::Move, + Interaction::Copy => winit::window::CursorIcon::Copy, + Interaction::Help => winit::window::CursorIcon::Help, } } @@ -513,7 +551,7 @@ pub fn touch_event( } } -/// Converts a `VirtualKeyCode` from [`winit`] to an [`iced`] key code. +/// Converts a `Key` from [`winit`] to an [`iced`] key. /// /// [`winit`]: https://github.com/rust-windowing/winit /// [`iced`]: https://github.com/iced-rs/iced/tree/0.12 @@ -842,7 +880,258 @@ pub fn key(key: winit::keyboard::Key) -> keyboard::Key { } } -/// Converts some [`UserAttention`] into it's `winit` counterpart. +/// Converts a `PhysicalKey` from [`winit`] to an [`iced`] physical key. +/// +/// [`winit`]: https://github.com/rust-windowing/winit +/// [`iced`]: https://github.com/iced-rs/iced/tree/0.12 +pub fn physical_key( + physical_key: winit::keyboard::PhysicalKey, +) -> keyboard::key::Physical { + match physical_key { + winit::keyboard::PhysicalKey::Code(code) => key_code(code) + .map(keyboard::key::Physical::Code) + .unwrap_or(keyboard::key::Physical::Unidentified( + keyboard::key::NativeCode::Unidentified, + )), + winit::keyboard::PhysicalKey::Unidentified(code) => { + keyboard::key::Physical::Unidentified(native_key_code(code)) + } + } +} + +/// Converts a `KeyCode` from [`winit`] to an [`iced`] key code. +/// +/// [`winit`]: https://github.com/rust-windowing/winit +/// [`iced`]: https://github.com/iced-rs/iced/tree/0.12 +pub fn key_code( + key_code: winit::keyboard::KeyCode, +) -> Option<keyboard::key::Code> { + use winit::keyboard::KeyCode; + + Some(match key_code { + KeyCode::Backquote => keyboard::key::Code::Backquote, + KeyCode::Backslash => keyboard::key::Code::Backslash, + KeyCode::BracketLeft => keyboard::key::Code::BracketLeft, + KeyCode::BracketRight => keyboard::key::Code::BracketRight, + KeyCode::Comma => keyboard::key::Code::Comma, + KeyCode::Digit0 => keyboard::key::Code::Digit0, + KeyCode::Digit1 => keyboard::key::Code::Digit1, + KeyCode::Digit2 => keyboard::key::Code::Digit2, + KeyCode::Digit3 => keyboard::key::Code::Digit3, + KeyCode::Digit4 => keyboard::key::Code::Digit4, + KeyCode::Digit5 => keyboard::key::Code::Digit5, + KeyCode::Digit6 => keyboard::key::Code::Digit6, + KeyCode::Digit7 => keyboard::key::Code::Digit7, + KeyCode::Digit8 => keyboard::key::Code::Digit8, + KeyCode::Digit9 => keyboard::key::Code::Digit9, + KeyCode::Equal => keyboard::key::Code::Equal, + KeyCode::IntlBackslash => keyboard::key::Code::IntlBackslash, + KeyCode::IntlRo => keyboard::key::Code::IntlRo, + KeyCode::IntlYen => keyboard::key::Code::IntlYen, + KeyCode::KeyA => keyboard::key::Code::KeyA, + KeyCode::KeyB => keyboard::key::Code::KeyB, + KeyCode::KeyC => keyboard::key::Code::KeyC, + KeyCode::KeyD => keyboard::key::Code::KeyD, + KeyCode::KeyE => keyboard::key::Code::KeyE, + KeyCode::KeyF => keyboard::key::Code::KeyF, + KeyCode::KeyG => keyboard::key::Code::KeyG, + KeyCode::KeyH => keyboard::key::Code::KeyH, + KeyCode::KeyI => keyboard::key::Code::KeyI, + KeyCode::KeyJ => keyboard::key::Code::KeyJ, + KeyCode::KeyK => keyboard::key::Code::KeyK, + KeyCode::KeyL => keyboard::key::Code::KeyL, + KeyCode::KeyM => keyboard::key::Code::KeyM, + KeyCode::KeyN => keyboard::key::Code::KeyN, + KeyCode::KeyO => keyboard::key::Code::KeyO, + KeyCode::KeyP => keyboard::key::Code::KeyP, + KeyCode::KeyQ => keyboard::key::Code::KeyQ, + KeyCode::KeyR => keyboard::key::Code::KeyR, + KeyCode::KeyS => keyboard::key::Code::KeyS, + KeyCode::KeyT => keyboard::key::Code::KeyT, + KeyCode::KeyU => keyboard::key::Code::KeyU, + KeyCode::KeyV => keyboard::key::Code::KeyV, + KeyCode::KeyW => keyboard::key::Code::KeyW, + KeyCode::KeyX => keyboard::key::Code::KeyX, + KeyCode::KeyY => keyboard::key::Code::KeyY, + KeyCode::KeyZ => keyboard::key::Code::KeyZ, + KeyCode::Minus => keyboard::key::Code::Minus, + KeyCode::Period => keyboard::key::Code::Period, + KeyCode::Quote => keyboard::key::Code::Quote, + KeyCode::Semicolon => keyboard::key::Code::Semicolon, + KeyCode::Slash => keyboard::key::Code::Slash, + KeyCode::AltLeft => keyboard::key::Code::AltLeft, + KeyCode::AltRight => keyboard::key::Code::AltRight, + KeyCode::Backspace => keyboard::key::Code::Backspace, + KeyCode::CapsLock => keyboard::key::Code::CapsLock, + KeyCode::ContextMenu => keyboard::key::Code::ContextMenu, + KeyCode::ControlLeft => keyboard::key::Code::ControlLeft, + KeyCode::ControlRight => keyboard::key::Code::ControlRight, + KeyCode::Enter => keyboard::key::Code::Enter, + KeyCode::SuperLeft => keyboard::key::Code::SuperLeft, + KeyCode::SuperRight => keyboard::key::Code::SuperRight, + KeyCode::ShiftLeft => keyboard::key::Code::ShiftLeft, + KeyCode::ShiftRight => keyboard::key::Code::ShiftRight, + KeyCode::Space => keyboard::key::Code::Space, + KeyCode::Tab => keyboard::key::Code::Tab, + KeyCode::Convert => keyboard::key::Code::Convert, + KeyCode::KanaMode => keyboard::key::Code::KanaMode, + KeyCode::Lang1 => keyboard::key::Code::Lang1, + KeyCode::Lang2 => keyboard::key::Code::Lang2, + KeyCode::Lang3 => keyboard::key::Code::Lang3, + KeyCode::Lang4 => keyboard::key::Code::Lang4, + KeyCode::Lang5 => keyboard::key::Code::Lang5, + KeyCode::NonConvert => keyboard::key::Code::NonConvert, + KeyCode::Delete => keyboard::key::Code::Delete, + KeyCode::End => keyboard::key::Code::End, + KeyCode::Help => keyboard::key::Code::Help, + KeyCode::Home => keyboard::key::Code::Home, + KeyCode::Insert => keyboard::key::Code::Insert, + KeyCode::PageDown => keyboard::key::Code::PageDown, + KeyCode::PageUp => keyboard::key::Code::PageUp, + KeyCode::ArrowDown => keyboard::key::Code::ArrowDown, + KeyCode::ArrowLeft => keyboard::key::Code::ArrowLeft, + KeyCode::ArrowRight => keyboard::key::Code::ArrowRight, + KeyCode::ArrowUp => keyboard::key::Code::ArrowUp, + KeyCode::NumLock => keyboard::key::Code::NumLock, + KeyCode::Numpad0 => keyboard::key::Code::Numpad0, + KeyCode::Numpad1 => keyboard::key::Code::Numpad1, + KeyCode::Numpad2 => keyboard::key::Code::Numpad2, + KeyCode::Numpad3 => keyboard::key::Code::Numpad3, + KeyCode::Numpad4 => keyboard::key::Code::Numpad4, + KeyCode::Numpad5 => keyboard::key::Code::Numpad5, + KeyCode::Numpad6 => keyboard::key::Code::Numpad6, + KeyCode::Numpad7 => keyboard::key::Code::Numpad7, + KeyCode::Numpad8 => keyboard::key::Code::Numpad8, + KeyCode::Numpad9 => keyboard::key::Code::Numpad9, + KeyCode::NumpadAdd => keyboard::key::Code::NumpadAdd, + KeyCode::NumpadBackspace => keyboard::key::Code::NumpadBackspace, + KeyCode::NumpadClear => keyboard::key::Code::NumpadClear, + KeyCode::NumpadClearEntry => keyboard::key::Code::NumpadClearEntry, + KeyCode::NumpadComma => keyboard::key::Code::NumpadComma, + KeyCode::NumpadDecimal => keyboard::key::Code::NumpadDecimal, + KeyCode::NumpadDivide => keyboard::key::Code::NumpadDivide, + KeyCode::NumpadEnter => keyboard::key::Code::NumpadEnter, + KeyCode::NumpadEqual => keyboard::key::Code::NumpadEqual, + KeyCode::NumpadHash => keyboard::key::Code::NumpadHash, + KeyCode::NumpadMemoryAdd => keyboard::key::Code::NumpadMemoryAdd, + KeyCode::NumpadMemoryClear => keyboard::key::Code::NumpadMemoryClear, + KeyCode::NumpadMemoryRecall => keyboard::key::Code::NumpadMemoryRecall, + KeyCode::NumpadMemoryStore => keyboard::key::Code::NumpadMemoryStore, + KeyCode::NumpadMemorySubtract => { + keyboard::key::Code::NumpadMemorySubtract + } + KeyCode::NumpadMultiply => keyboard::key::Code::NumpadMultiply, + KeyCode::NumpadParenLeft => keyboard::key::Code::NumpadParenLeft, + KeyCode::NumpadParenRight => keyboard::key::Code::NumpadParenRight, + KeyCode::NumpadStar => keyboard::key::Code::NumpadStar, + KeyCode::NumpadSubtract => keyboard::key::Code::NumpadSubtract, + KeyCode::Escape => keyboard::key::Code::Escape, + KeyCode::Fn => keyboard::key::Code::Fn, + KeyCode::FnLock => keyboard::key::Code::FnLock, + KeyCode::PrintScreen => keyboard::key::Code::PrintScreen, + KeyCode::ScrollLock => keyboard::key::Code::ScrollLock, + KeyCode::Pause => keyboard::key::Code::Pause, + KeyCode::BrowserBack => keyboard::key::Code::BrowserBack, + KeyCode::BrowserFavorites => keyboard::key::Code::BrowserFavorites, + KeyCode::BrowserForward => keyboard::key::Code::BrowserForward, + KeyCode::BrowserHome => keyboard::key::Code::BrowserHome, + KeyCode::BrowserRefresh => keyboard::key::Code::BrowserRefresh, + KeyCode::BrowserSearch => keyboard::key::Code::BrowserSearch, + KeyCode::BrowserStop => keyboard::key::Code::BrowserStop, + KeyCode::Eject => keyboard::key::Code::Eject, + KeyCode::LaunchApp1 => keyboard::key::Code::LaunchApp1, + KeyCode::LaunchApp2 => keyboard::key::Code::LaunchApp2, + KeyCode::LaunchMail => keyboard::key::Code::LaunchMail, + KeyCode::MediaPlayPause => keyboard::key::Code::MediaPlayPause, + KeyCode::MediaSelect => keyboard::key::Code::MediaSelect, + KeyCode::MediaStop => keyboard::key::Code::MediaStop, + KeyCode::MediaTrackNext => keyboard::key::Code::MediaTrackNext, + KeyCode::MediaTrackPrevious => keyboard::key::Code::MediaTrackPrevious, + KeyCode::Power => keyboard::key::Code::Power, + KeyCode::Sleep => keyboard::key::Code::Sleep, + KeyCode::AudioVolumeDown => keyboard::key::Code::AudioVolumeDown, + KeyCode::AudioVolumeMute => keyboard::key::Code::AudioVolumeMute, + KeyCode::AudioVolumeUp => keyboard::key::Code::AudioVolumeUp, + KeyCode::WakeUp => keyboard::key::Code::WakeUp, + KeyCode::Meta => keyboard::key::Code::Meta, + KeyCode::Hyper => keyboard::key::Code::Hyper, + KeyCode::Turbo => keyboard::key::Code::Turbo, + KeyCode::Abort => keyboard::key::Code::Abort, + KeyCode::Resume => keyboard::key::Code::Resume, + KeyCode::Suspend => keyboard::key::Code::Suspend, + KeyCode::Again => keyboard::key::Code::Again, + KeyCode::Copy => keyboard::key::Code::Copy, + KeyCode::Cut => keyboard::key::Code::Cut, + KeyCode::Find => keyboard::key::Code::Find, + KeyCode::Open => keyboard::key::Code::Open, + KeyCode::Paste => keyboard::key::Code::Paste, + KeyCode::Props => keyboard::key::Code::Props, + KeyCode::Select => keyboard::key::Code::Select, + KeyCode::Undo => keyboard::key::Code::Undo, + KeyCode::Hiragana => keyboard::key::Code::Hiragana, + KeyCode::Katakana => keyboard::key::Code::Katakana, + KeyCode::F1 => keyboard::key::Code::F1, + KeyCode::F2 => keyboard::key::Code::F2, + KeyCode::F3 => keyboard::key::Code::F3, + KeyCode::F4 => keyboard::key::Code::F4, + KeyCode::F5 => keyboard::key::Code::F5, + KeyCode::F6 => keyboard::key::Code::F6, + KeyCode::F7 => keyboard::key::Code::F7, + KeyCode::F8 => keyboard::key::Code::F8, + KeyCode::F9 => keyboard::key::Code::F9, + KeyCode::F10 => keyboard::key::Code::F10, + KeyCode::F11 => keyboard::key::Code::F11, + KeyCode::F12 => keyboard::key::Code::F12, + KeyCode::F13 => keyboard::key::Code::F13, + KeyCode::F14 => keyboard::key::Code::F14, + KeyCode::F15 => keyboard::key::Code::F15, + KeyCode::F16 => keyboard::key::Code::F16, + KeyCode::F17 => keyboard::key::Code::F17, + KeyCode::F18 => keyboard::key::Code::F18, + KeyCode::F19 => keyboard::key::Code::F19, + KeyCode::F20 => keyboard::key::Code::F20, + KeyCode::F21 => keyboard::key::Code::F21, + KeyCode::F22 => keyboard::key::Code::F22, + KeyCode::F23 => keyboard::key::Code::F23, + KeyCode::F24 => keyboard::key::Code::F24, + KeyCode::F25 => keyboard::key::Code::F25, + KeyCode::F26 => keyboard::key::Code::F26, + KeyCode::F27 => keyboard::key::Code::F27, + KeyCode::F28 => keyboard::key::Code::F28, + KeyCode::F29 => keyboard::key::Code::F29, + KeyCode::F30 => keyboard::key::Code::F30, + KeyCode::F31 => keyboard::key::Code::F31, + KeyCode::F32 => keyboard::key::Code::F32, + KeyCode::F33 => keyboard::key::Code::F33, + KeyCode::F34 => keyboard::key::Code::F34, + KeyCode::F35 => keyboard::key::Code::F35, + _ => None?, + }) +} + +/// Converts a `NativeKeyCode` from [`winit`] to an [`iced`] native key code. +/// +/// [`winit`]: https://github.com/rust-windowing/winit +/// [`iced`]: https://github.com/iced-rs/iced/tree/0.12 +pub fn native_key_code( + native_key_code: winit::keyboard::NativeKeyCode, +) -> keyboard::key::NativeCode { + use winit::keyboard::NativeKeyCode; + + match native_key_code { + NativeKeyCode::Unidentified => keyboard::key::NativeCode::Unidentified, + NativeKeyCode::Android(code) => { + keyboard::key::NativeCode::Android(code) + } + NativeKeyCode::MacOS(code) => keyboard::key::NativeCode::MacOS(code), + NativeKeyCode::Windows(code) => { + keyboard::key::NativeCode::Windows(code) + } + NativeKeyCode::Xkb(code) => keyboard::key::NativeCode::Xkb(code), + } +} + +/// Converts some [`UserAttention`] into its `winit` counterpart. /// /// [`UserAttention`]: window::UserAttention pub fn user_attention( @@ -858,7 +1147,31 @@ pub fn user_attention( } } -/// Converts some [`window::Icon`] into it's `winit` counterpart. +/// Converts some [`window::Direction`] into a [`winit::window::ResizeDirection`]. +pub fn resize_direction( + resize_direction: window::Direction, +) -> winit::window::ResizeDirection { + match resize_direction { + window::Direction::North => winit::window::ResizeDirection::North, + window::Direction::South => winit::window::ResizeDirection::South, + window::Direction::East => winit::window::ResizeDirection::East, + window::Direction::West => winit::window::ResizeDirection::West, + window::Direction::NorthEast => { + winit::window::ResizeDirection::NorthEast + } + window::Direction::NorthWest => { + winit::window::ResizeDirection::NorthWest + } + window::Direction::SouthEast => { + winit::window::ResizeDirection::SouthEast + } + window::Direction::SouthWest => { + winit::window::ResizeDirection::SouthWest + } + } +} + +/// Converts some [`window::Icon`] into its `winit` counterpart. /// /// Returns `None` if there is an error during the conversion. pub fn icon(icon: window::Icon) -> Option<winit::window::Icon> { @@ -867,6 +1180,17 @@ pub fn icon(icon: window::Icon) -> Option<winit::window::Icon> { winit::window::Icon::from_rgba(pixels, size.width, size.height).ok() } +/// Convertions some [`input_method::Purpose`] to its `winit` counterpart. +pub fn ime_purpose( + purpose: input_method::Purpose, +) -> winit::window::ImePurpose { + match purpose { + input_method::Purpose::Normal => winit::window::ImePurpose::Normal, + input_method::Purpose::Secure => winit::window::ImePurpose::Password, + input_method::Purpose::Terminal => winit::window::ImePurpose::Terminal, + } +} + // See: https://en.wikipedia.org/wiki/Private_Use_Areas fn is_private_use(c: char) -> bool { ('\u{E000}'..='\u{F8FF}').contains(&c) diff --git a/winit/src/lib.rs b/winit/src/lib.rs index e6d58152..d1b2ae99 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -5,13 +5,13 @@ //! `iced_winit` offers some convenient abstractions on top of [`iced_runtime`] //! to quickstart development when using [`winit`]. //! -//! It exposes a renderer-agnostic [`Application`] trait that can be implemented +//! It exposes a renderer-agnostic [`Program`] trait that can be implemented //! and then run with a simple call. The use of this trait is optional. //! //! Additionally, a [`conversion`] module is available for users that decide to //! implement a custom event loop. //! -//! [`iced_runtime`]: https://github.com/iced-rs/iced/tree/0.12/runtime +//! [`iced_runtime`]: https://github.com/iced-rs/iced/tree/0.13/runtime //! [`winit`]: https://github.com/rust-windowing/winit //! [`conversion`]: crate::conversion #![doc( @@ -25,24 +25,23 @@ pub use iced_runtime::debug; pub use iced_runtime::futures; pub use winit; -#[cfg(feature = "multi-window")] -pub mod multi_window; - -#[cfg(feature = "application")] -pub mod application; pub mod clipboard; pub mod conversion; pub mod settings; +#[cfg(feature = "program")] +pub mod program; + #[cfg(feature = "system")] pub mod system; mod error; mod proxy; -#[cfg(feature = "application")] -pub use application::Application; pub use clipboard::Clipboard; pub use error::Error; pub use proxy::Proxy; pub use settings::Settings; + +#[cfg(feature = "program")] +pub use program::Program; diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs deleted file mode 100644 index a05a94d9..00000000 --- a/winit/src/multi_window.rs +++ /dev/null @@ -1,1367 +0,0 @@ -//! Create interactive, native cross-platform applications for WGPU. -mod state; -mod window_manager; - -pub use state::State; - -use crate::conversion; -use crate::core; -use crate::core::mouse; -use crate::core::renderer; -use crate::core::widget::operation; -use crate::core::window; -use crate::core::{Point, Size}; -use crate::debug; -use crate::futures::futures::channel::mpsc; -use crate::futures::futures::channel::oneshot; -use crate::futures::futures::executor; -use crate::futures::futures::task; -use crate::futures::futures::{Future, StreamExt}; -use crate::futures::{Executor, Runtime, Subscription}; -use crate::graphics; -use crate::graphics::{compositor, Compositor}; -use crate::multi_window::window_manager::WindowManager; -use crate::runtime::command::{self, Command}; -use crate::runtime::multi_window::Program; -use crate::runtime::user_interface::{self, UserInterface}; -use crate::{Clipboard, Error, Proxy, Settings}; - -pub use crate::application::{default, Appearance, DefaultStyle}; - -use rustc_hash::FxHashMap; -use std::mem::ManuallyDrop; -use std::sync::Arc; -use std::time::Instant; - -/// An interactive, native, cross-platform, multi-windowed application. -/// -/// This trait is the main entrypoint of multi-window Iced. Once implemented, you can run -/// your GUI application by simply calling [`run`]. It will run in -/// its own window. -/// -/// An [`Application`] can execute asynchronous actions by returning a -/// [`Command`] in some of its methods. -/// -/// When using an [`Application`] with the `debug` feature enabled, a debug view -/// can be toggled by pressing `F12`. -pub trait Application: Program -where - Self::Theme: DefaultStyle, -{ - /// The data needed to initialize your [`Application`]. - type Flags; - - /// Returns the unique name of the [`Application`]. - fn name() -> &'static str; - - /// Initializes the [`Application`] with the flags provided to - /// [`run`] as part of the [`Settings`]. - /// - /// Here is where you should return the initial state of your app. - /// - /// Additionally, you can return a [`Command`] if you need to perform some - /// async action in the background on startup. This is useful if you want to - /// load state from a file, perform an initial HTTP request, etc. - fn new(flags: Self::Flags) -> (Self, Command<Self::Message>); - - /// Returns the current title of the [`Application`]. - /// - /// This title can be dynamic! The runtime will automatically update the - /// title of your application when necessary. - fn title(&self, window: window::Id) -> String; - - /// Returns the current `Theme` of the [`Application`]. - fn theme(&self, window: window::Id) -> Self::Theme; - - /// Returns the `Style` variation of the `Theme`. - fn style(&self, theme: &Self::Theme) -> Appearance { - theme.default_style() - } - - /// Returns the event `Subscription` for the current state of the - /// application. - /// - /// The messages produced by the `Subscription` will be handled by - /// [`update`](#tymethod.update). - /// - /// A `Subscription` will be kept alive as long as you keep returning it! - /// - /// By default, it returns an empty subscription. - fn subscription(&self) -> Subscription<Self::Message> { - Subscription::none() - } - - /// Returns the scale factor of the window of the [`Application`]. - /// - /// It can be used to dynamically control the size of the UI at runtime - /// (i.e. zooming). - /// - /// For instance, a scale factor of `2.0` will make widgets twice as big, - /// while a scale factor of `0.5` will shrink them to half their size. - /// - /// By default, it returns `1.0`. - #[allow(unused_variables)] - fn scale_factor(&self, window: window::Id) -> f64 { - 1.0 - } -} - -/// Runs an [`Application`] with an executor, compositor, and the provided -/// settings. -pub fn run<A, E, C>( - settings: Settings<A::Flags>, - graphics_settings: graphics::Settings, -) -> Result<(), Error> -where - A: Application + 'static, - E: Executor + 'static, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - use winit::event_loop::EventLoop; - - debug::init(A::name()); - let boot_span = debug::boot(); - - let event_loop = EventLoop::with_user_event() - .build() - .expect("Create event loop"); - - let (proxy, worker) = Proxy::new(event_loop.create_proxy()); - - let runtime = { - let executor = E::new().map_err(Error::ExecutorCreationFailed)?; - executor.spawn(worker); - - Runtime::new(executor, proxy.clone()) - }; - - let (application, init_command) = { - let flags = settings.flags; - - runtime.enter(|| A::new(flags)) - }; - - let id = settings.id; - let title = application.title(window::Id::MAIN); - - let (boot_sender, boot_receiver) = oneshot::channel(); - let (event_sender, event_receiver) = mpsc::unbounded(); - let (control_sender, control_receiver) = mpsc::unbounded(); - - let instance = Box::pin(run_instance::<A, E, C>( - application, - runtime, - proxy, - boot_receiver, - event_receiver, - control_sender, - init_command, - boot_span, - )); - - let context = task::Context::from_waker(task::noop_waker_ref()); - - struct Runner<Message: 'static, F, C> { - instance: std::pin::Pin<Box<F>>, - context: task::Context<'static>, - boot: Option<BootConfig<C>>, - sender: mpsc::UnboundedSender<Event<Message>>, - receiver: mpsc::UnboundedReceiver<Control>, - error: Option<Error>, - } - - struct BootConfig<C> { - sender: oneshot::Sender<Boot<C>>, - id: Option<String>, - title: String, - window_settings: window::Settings, - graphics_settings: graphics::Settings, - } - - let mut runner = Runner { - instance, - context, - boot: Some(BootConfig { - sender: boot_sender, - id, - title, - window_settings: settings.window, - graphics_settings, - }), - sender: event_sender, - receiver: control_receiver, - error: None, - }; - - impl<Message, F, C> winit::application::ApplicationHandler<Message> - for Runner<Message, F, C> - where - F: Future<Output = ()>, - C: Compositor, - { - fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) { - let Some(BootConfig { - sender, - id, - title, - window_settings, - graphics_settings, - }) = self.boot.take() - else { - return; - }; - - let should_be_visible = window_settings.visible; - let exit_on_close_request = window_settings.exit_on_close_request; - - let window_attributes = conversion::window_attributes( - window_settings, - &title, - event_loop.primary_monitor(), - id, - ) - .with_visible(false); - - log::debug!("Window attributes: {window_attributes:#?}"); - - let window = match event_loop.create_window(window_attributes) { - Ok(window) => Arc::new(window), - Err(error) => { - self.error = Some(Error::WindowCreationFailed(error)); - event_loop.exit(); - return; - } - }; - - let finish_boot = async move { - let compositor = - C::new(graphics_settings, window.clone()).await?; - - sender - .send(Boot { - window, - compositor, - should_be_visible, - exit_on_close_request, - }) - .ok() - .expect("Send boot event"); - - Ok::<_, graphics::Error>(()) - }; - - if let Err(error) = executor::block_on(finish_boot) { - self.error = Some(Error::GraphicsCreationFailed(error)); - event_loop.exit(); - } - } - - fn new_events( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - cause: winit::event::StartCause, - ) { - if self.boot.is_some() { - return; - } - - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::NewEvents(cause)), - ); - } - - fn window_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - window_id: winit::window::WindowId, - event: winit::event::WindowEvent, - ) { - #[cfg(target_os = "windows")] - let is_move_or_resize = matches!( - event, - winit::event::WindowEvent::Resized(_) - | winit::event::WindowEvent::Moved(_) - ); - - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::WindowEvent { - window_id, - event, - }), - ); - - // TODO: Remove when unnecessary - // On Windows, we emulate an `AboutToWait` event after every `Resized` event - // since the event loop does not resume during resize interaction. - // More details: https://github.com/rust-windowing/winit/issues/3272 - #[cfg(target_os = "windows")] - { - if is_move_or_resize { - self.process_event( - event_loop, - Event::EventLoopAwakened( - winit::event::Event::AboutToWait, - ), - ); - } - } - } - - fn user_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - message: Message, - ) { - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::UserEvent( - message, - )), - ); - } - - fn about_to_wait( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - ) { - self.process_event( - event_loop, - Event::EventLoopAwakened(winit::event::Event::AboutToWait), - ); - } - } - - impl<Message, F, C> Runner<Message, F, C> - where - F: Future<Output = ()>, - C: Compositor, - { - fn process_event( - &mut self, - event_loop: &winit::event_loop::ActiveEventLoop, - event: Event<Message>, - ) { - if event_loop.exiting() { - return; - } - - self.sender.start_send(event).expect("Send event"); - - loop { - let poll = self.instance.as_mut().poll(&mut self.context); - - match poll { - task::Poll::Pending => match self.receiver.try_next() { - Ok(Some(control)) => match control { - Control::ChangeFlow(flow) => { - use winit::event_loop::ControlFlow; - - match (event_loop.control_flow(), flow) { - ( - ControlFlow::WaitUntil(current), - ControlFlow::WaitUntil(new), - ) if new < current => {} - ( - ControlFlow::WaitUntil(target), - ControlFlow::Wait, - ) if target > Instant::now() => {} - _ => { - event_loop.set_control_flow(flow); - } - } - } - Control::CreateWindow { - id, - settings, - title, - monitor, - } => { - let exit_on_close_request = - settings.exit_on_close_request; - - let window = event_loop - .create_window( - conversion::window_attributes( - settings, &title, monitor, None, - ), - ) - .expect("Create window"); - - self.process_event( - event_loop, - Event::WindowCreated { - id, - window, - exit_on_close_request, - }, - ); - } - Control::Exit => { - event_loop.exit(); - } - }, - _ => { - break; - } - }, - task::Poll::Ready(_) => { - event_loop.exit(); - break; - } - }; - } - } - } - - let _ = event_loop.run_app(&mut runner); - - Ok(()) -} - -struct Boot<C> { - window: Arc<winit::window::Window>, - compositor: C, - should_be_visible: bool, - exit_on_close_request: bool, -} - -enum Event<Message: 'static> { - WindowCreated { - id: window::Id, - window: winit::window::Window, - exit_on_close_request: bool, - }, - EventLoopAwakened(winit::event::Event<Message>), -} - -enum Control { - ChangeFlow(winit::event_loop::ControlFlow), - Exit, - CreateWindow { - id: window::Id, - settings: window::Settings, - title: String, - monitor: Option<winit::monitor::MonitorHandle>, - }, -} - -async fn run_instance<A, E, C>( - mut application: A, - mut runtime: Runtime<E, Proxy<A::Message>, A::Message>, - mut proxy: Proxy<A::Message>, - mut boot: oneshot::Receiver<Boot<C>>, - mut event_receiver: mpsc::UnboundedReceiver<Event<A::Message>>, - mut control_sender: mpsc::UnboundedSender<Control>, - init_command: Command<A::Message>, - boot_span: debug::Span, -) where - A: Application + 'static, - E: Executor + 'static, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - use winit::event; - use winit::event_loop::ControlFlow; - - let Boot { - window: main_window, - mut compositor, - should_be_visible, - exit_on_close_request, - } = boot.try_recv().ok().flatten().expect("Receive boot"); - - let mut window_manager = WindowManager::new(); - - let _ = window_manager.insert( - window::Id::MAIN, - main_window.clone(), - &application, - &mut compositor, - exit_on_close_request, - ); - - let main_window = window_manager - .get_mut(window::Id::MAIN) - .expect("Get main window"); - - if should_be_visible { - main_window.raw.set_visible(true); - } - - let mut clipboard = Clipboard::connect(&main_window.raw); - let mut events = { - vec![( - Some(window::Id::MAIN), - core::Event::Window( - window::Id::MAIN, - window::Event::Opened { - position: main_window.position(), - size: main_window.size(), - }, - ), - )] - }; - - let mut ui_caches = FxHashMap::default(); - let mut user_interfaces = ManuallyDrop::new(build_user_interfaces( - &application, - &mut window_manager, - FxHashMap::from_iter([( - window::Id::MAIN, - user_interface::Cache::default(), - )]), - )); - - run_command( - &application, - &mut compositor, - init_command, - &mut runtime, - &mut clipboard, - &mut control_sender, - &mut proxy, - &mut window_manager, - &mut ui_caches, - ); - - let recipes = application.subscription().into_recipes(); - debug::subscriptions_tracked(recipes.len()); - runtime.track(recipes); - - boot_span.finish(); - - let mut messages = Vec::new(); - let mut user_events = 0; - - 'main: while let Some(event) = event_receiver.next().await { - match event { - Event::WindowCreated { - id, - window, - exit_on_close_request, - } => { - let window = window_manager.insert( - id, - Arc::new(window), - &application, - &mut compositor, - exit_on_close_request, - ); - - let logical_size = window.state.logical_size(); - - let _ = user_interfaces.insert( - id, - build_user_interface( - &application, - user_interface::Cache::default(), - &mut window.renderer, - logical_size, - id, - ), - ); - let _ = ui_caches.insert(id, user_interface::Cache::default()); - - events.push(( - Some(id), - core::Event::Window( - id, - window::Event::Opened { - position: window.position(), - size: window.size(), - }, - ), - )); - } - Event::EventLoopAwakened(event) => { - match event { - event::Event::NewEvents( - event::StartCause::Init - | event::StartCause::ResumeTimeReached { .. }, - ) => { - for (_id, window) in window_manager.iter_mut() { - // TODO once widgets can request to be redrawn, we can avoid always requesting a - // redraw - window.raw.request_redraw(); - } - } - event::Event::PlatformSpecific( - event::PlatformSpecific::MacOS( - event::MacOS::ReceivedUrl(url), - ), - ) => { - use crate::core::event; - - events.push(( - None, - event::Event::PlatformSpecific( - event::PlatformSpecific::MacOS( - event::MacOS::ReceivedUrl(url), - ), - ), - )); - } - event::Event::UserEvent(message) => { - messages.push(message); - user_events += 1; - } - event::Event::WindowEvent { - window_id: id, - event: event::WindowEvent::RedrawRequested, - .. - } => { - let Some((id, window)) = - window_manager.get_mut_alias(id) - else { - continue; - }; - - // TODO: Avoid redrawing all the time by forcing widgets to - // request redraws on state changes - // - // Then, we can use the `interface_state` here to decide if a redraw - // is needed right away, or simply wait until a specific time. - let redraw_event = core::Event::Window( - id, - window::Event::RedrawRequested(Instant::now()), - ); - - let cursor = window.state.cursor(); - - let ui = user_interfaces - .get_mut(&id) - .expect("Get user interface"); - - let (ui_state, _) = ui.update( - &[redraw_event.clone()], - cursor, - &mut window.renderer, - &mut clipboard, - &mut messages, - ); - - let draw_span = debug::draw(id); - let new_mouse_interaction = ui.draw( - &mut window.renderer, - window.state.theme(), - &renderer::Style { - text_color: window.state.text_color(), - }, - cursor, - ); - draw_span.finish(); - - if new_mouse_interaction != window.mouse_interaction { - window.raw.set_cursor( - conversion::mouse_interaction( - new_mouse_interaction, - ), - ); - - window.mouse_interaction = new_mouse_interaction; - } - - runtime.broadcast( - redraw_event.clone(), - core::event::Status::Ignored, - ); - - let _ = control_sender.start_send(Control::ChangeFlow( - match ui_state { - user_interface::State::Updated { - redraw_request: Some(redraw_request), - } => match redraw_request { - window::RedrawRequest::NextFrame => { - window.raw.request_redraw(); - - ControlFlow::Wait - } - window::RedrawRequest::At(at) => { - ControlFlow::WaitUntil(at) - } - }, - _ => ControlFlow::Wait, - }, - )); - - let physical_size = window.state.physical_size(); - - if physical_size.width == 0 || physical_size.height == 0 - { - continue; - } - - if window.viewport_version - != window.state.viewport_version() - { - let logical_size = window.state.logical_size(); - - let layout = debug::layout(id); - let ui = user_interfaces - .remove(&id) - .expect("Remove user interface"); - - let _ = user_interfaces.insert( - id, - ui.relayout(logical_size, &mut window.renderer), - ); - layout.finish(); - - let draw = debug::draw(id); - let new_mouse_interaction = user_interfaces - .get_mut(&id) - .expect("Get user interface") - .draw( - &mut window.renderer, - window.state.theme(), - &renderer::Style { - text_color: window.state.text_color(), - }, - window.state.cursor(), - ); - draw.finish(); - - if new_mouse_interaction != window.mouse_interaction - { - window.raw.set_cursor( - conversion::mouse_interaction( - new_mouse_interaction, - ), - ); - - window.mouse_interaction = - new_mouse_interaction; - } - - compositor.configure_surface( - &mut window.surface, - physical_size.width, - physical_size.height, - ); - - window.viewport_version = - window.state.viewport_version(); - } - - let present_span = debug::present(id); - match compositor.present( - &mut window.renderer, - &mut window.surface, - window.state.viewport(), - window.state.background_color(), - ) { - Ok(()) => { - present_span.finish(); - - // TODO: Handle animations! - // Maybe we can use `ControlFlow::WaitUntil` for this. - } - Err(error) => match error { - // This is an unrecoverable error. - compositor::SurfaceError::OutOfMemory => { - panic!("{:?}", error); - } - _ => { - log::error!( - "Error {error:?} when \ - presenting surface." - ); - - // Try rendering all windows again next frame. - for (_id, window) in - window_manager.iter_mut() - { - window.raw.request_redraw(); - } - } - }, - } - } - event::Event::WindowEvent { - event: window_event, - window_id, - } => { - #[cfg(feature = "debug")] - if let winit::event::WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key: - winit::keyboard::Key::Named( - winit::keyboard::NamedKey::F12, - ), - state: winit::event::ElementState::Pressed, - repeat: false, - .. - }, - .. - } = &window_event - { - crate::debug::toggle_comet(); - } - - let Some((id, window)) = - window_manager.get_mut_alias(window_id) - else { - continue; - }; - - if matches!( - window_event, - winit::event::WindowEvent::CloseRequested - ) && window.exit_on_close_request - { - let _ = window_manager.remove(id); - let _ = user_interfaces.remove(&id); - let _ = ui_caches.remove(&id); - - events.push(( - None, - core::Event::Window(id, window::Event::Closed), - )); - - if window_manager.is_empty() { - break 'main; - } - } else { - window.state.update(&window.raw, &window_event); - - if let Some(event) = conversion::window_event( - id, - window_event, - window.state.scale_factor(), - window.state.modifiers(), - ) { - events.push((Some(id), event)); - } - } - } - event::Event::AboutToWait => { - if events.is_empty() && messages.is_empty() { - continue; - } - - let mut uis_stale = false; - - for (id, window) in window_manager.iter_mut() { - let interact = debug::interact(id); - let mut window_events = vec![]; - - events.retain(|(window_id, event)| { - if *window_id == Some(id) || window_id.is_none() - { - window_events.push(event.clone()); - false - } else { - true - } - }); - - if window_events.is_empty() && messages.is_empty() { - continue; - } - - let (ui_state, statuses) = user_interfaces - .get_mut(&id) - .expect("Get user interface") - .update( - &window_events, - window.state.cursor(), - &mut window.renderer, - &mut clipboard, - &mut messages, - ); - - window.raw.request_redraw(); - - if !uis_stale { - uis_stale = matches!( - ui_state, - user_interface::State::Outdated - ); - } - - for (event, status) in window_events - .into_iter() - .zip(statuses.into_iter()) - { - runtime.broadcast(event, status); - } - interact.finish(); - } - - // TODO mw application update returns which window IDs to update - if !messages.is_empty() || uis_stale { - let mut cached_interfaces: FxHashMap< - window::Id, - user_interface::Cache, - > = ManuallyDrop::into_inner(user_interfaces) - .drain() - .map(|(id, ui)| (id, ui.into_cache())) - .collect(); - - // Update application - update( - &mut application, - &mut compositor, - &mut runtime, - &mut clipboard, - &mut control_sender, - &mut proxy, - &mut messages, - &mut window_manager, - &mut cached_interfaces, - ); - - // we must synchronize all window states with application state after an - // application update since we don't know what changed - for (id, window) in window_manager.iter_mut() { - window.state.synchronize( - &application, - id, - &window.raw, - ); - - // TODO once widgets can request to be redrawn, we can avoid always requesting a - // redraw - window.raw.request_redraw(); - } - - // rebuild UIs with the synchronized states - user_interfaces = - ManuallyDrop::new(build_user_interfaces( - &application, - &mut window_manager, - cached_interfaces, - )); - - if user_events > 0 { - proxy.free_slots(user_events); - user_events = 0; - } - } - } - _ => {} - } - } - } - } - - let _ = ManuallyDrop::into_inner(user_interfaces); -} - -/// Builds a window's [`UserInterface`] for the [`Application`]. -fn build_user_interface<'a, A: Application>( - application: &'a A, - cache: user_interface::Cache, - renderer: &mut A::Renderer, - size: Size, - id: window::Id, -) -> UserInterface<'a, A::Message, A::Theme, A::Renderer> -where - A::Theme: DefaultStyle, -{ - let view_span = debug::view(id); - let view = application.view(id); - view_span.finish(); - - let layout_span = debug::layout(id); - let user_interface = UserInterface::build(view, size, cache, renderer); - layout_span.finish(); - - user_interface -} - -/// Updates a multi-window [`Application`] by feeding it messages, spawning any -/// resulting [`Command`], and tracking its [`Subscription`]. -fn update<A: Application, C, E: Executor>( - application: &mut A, - compositor: &mut C, - runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, - clipboard: &mut Clipboard, - control_sender: &mut mpsc::UnboundedSender<Control>, - proxy: &mut Proxy<A::Message>, - messages: &mut Vec<A::Message>, - window_manager: &mut WindowManager<A, C>, - ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>, -) where - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - for message in messages.drain(..) { - let update_span = debug::update(&message); - let command = runtime.enter(|| application.update(message)); - - run_command( - application, - compositor, - command, - runtime, - clipboard, - control_sender, - proxy, - window_manager, - ui_caches, - ); - update_span.finish(); - } - - let recipes = application.subscription().into_recipes(); - debug::subscriptions_tracked(recipes.len()); - runtime.track(recipes); -} - -/// Runs the actions of a [`Command`]. -fn run_command<A, C, E>( - application: &A, - compositor: &mut C, - command: Command<A::Message>, - runtime: &mut Runtime<E, Proxy<A::Message>, A::Message>, - clipboard: &mut Clipboard, - control_sender: &mut mpsc::UnboundedSender<Control>, - proxy: &mut Proxy<A::Message>, - window_manager: &mut WindowManager<A, C>, - ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>, -) where - A: Application, - E: Executor, - C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: DefaultStyle, -{ - use crate::runtime::clipboard; - use crate::runtime::system; - use crate::runtime::window; - - let actions = command.actions(); - debug::commands_spawned(actions.len()); - - for action in actions { - match action { - command::Action::Future(future) => { - runtime.spawn(Box::pin(future)); - } - command::Action::Stream(stream) => { - runtime.run(Box::pin(stream)); - } - command::Action::Clipboard(action) => match action { - clipboard::Action::Read(tag, kind) => { - let message = tag(clipboard.read(kind)); - - proxy.send(message); - } - clipboard::Action::Write(contents, kind) => { - clipboard.write(kind, contents); - } - }, - command::Action::Window(action) => match action { - window::Action::Spawn(id, settings) => { - let monitor = window_manager.last_monitor(); - - control_sender - .start_send(Control::CreateWindow { - id, - settings, - title: application.title(id), - monitor, - }) - .expect("Send control action"); - } - window::Action::Close(id) => { - let _ = window_manager.remove(id); - let _ = ui_caches.remove(&id); - - if window_manager.is_empty() { - control_sender - .start_send(Control::Exit) - .expect("Send control action"); - } - } - window::Action::Drag(id) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = window.raw.drag_window(); - } - } - window::Action::Resize(id, size) => { - if let Some(window) = window_manager.get_mut(id) { - let _ = window.raw.request_inner_size( - winit::dpi::LogicalSize { - width: size.width, - height: size.height, - }, - ); - } - } - window::Action::FetchSize(id, callback) => { - if let Some(window) = window_manager.get_mut(id) { - let size = window - .raw - .inner_size() - .to_logical(window.raw.scale_factor()); - - proxy - .send(callback(Size::new(size.width, size.height))); - } - } - window::Action::FetchMaximized(id, callback) => { - if let Some(window) = window_manager.get_mut(id) { - proxy.send(callback(window.raw.is_maximized())); - } - } - window::Action::Maximize(id, maximized) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_maximized(maximized); - } - } - window::Action::FetchMinimized(id, callback) => { - if let Some(window) = window_manager.get_mut(id) { - proxy.send(callback(window.raw.is_minimized())); - } - } - window::Action::Minimize(id, minimized) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_minimized(minimized); - } - } - window::Action::FetchPosition(id, callback) => { - if let Some(window) = window_manager.get_mut(id) { - let position = window - .raw - .inner_position() - .map(|position| { - let position = position.to_logical::<f32>( - window.raw.scale_factor(), - ); - - Point::new(position.x, position.y) - }) - .ok(); - - proxy.send(callback(position)); - } - } - window::Action::Move(id, position) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_outer_position( - winit::dpi::LogicalPosition { - x: position.x, - y: position.y, - }, - ); - } - } - window::Action::ChangeMode(id, mode) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_visible(conversion::visible(mode)); - window.raw.set_fullscreen(conversion::fullscreen( - window.raw.current_monitor(), - mode, - )); - } - } - window::Action::ChangeIcon(id, icon) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_window_icon(conversion::icon(icon)); - } - } - window::Action::FetchMode(id, tag) => { - if let Some(window) = window_manager.get_mut(id) { - let mode = if window.raw.is_visible().unwrap_or(true) { - conversion::mode(window.raw.fullscreen()) - } else { - core::window::Mode::Hidden - }; - - proxy.send(tag(mode)); - } - } - window::Action::ToggleMaximize(id) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_maximized(!window.raw.is_maximized()); - } - } - window::Action::ToggleDecorations(id) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.set_decorations(!window.raw.is_decorated()); - } - } - window::Action::RequestUserAttention(id, attention_type) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.request_user_attention( - attention_type.map(conversion::user_attention), - ); - } - } - window::Action::GainFocus(id) => { - if let Some(window) = window_manager.get_mut(id) { - window.raw.focus_window(); - } - } - window::Action::ChangeLevel(id, level) => { - if let Some(window) = window_manager.get_mut(id) { - window - .raw - .set_window_level(conversion::window_level(level)); - } - } - window::Action::ShowSystemMenu(id) => { - if let Some(window) = window_manager.get_mut(id) { - if let mouse::Cursor::Available(point) = - window.state.cursor() - { - window.raw.show_window_menu( - winit::dpi::LogicalPosition { - x: point.x, - y: point.y, - }, - ); - } - } - } - window::Action::FetchId(id, tag) => { - if let Some(window) = window_manager.get_mut(id) { - proxy.send(tag(window.raw.id().into())); - } - } - window::Action::RunWithHandle(id, tag) => { - use window::raw_window_handle::HasWindowHandle; - - if let Some(handle) = window_manager - .get_mut(id) - .and_then(|window| window.raw.window_handle().ok()) - { - proxy.send(tag(handle)); - } - } - window::Action::Screenshot(id, tag) => { - if let Some(window) = window_manager.get_mut(id) { - let bytes = compositor.screenshot( - &mut window.renderer, - &mut window.surface, - window.state.viewport(), - window.state.background_color(), - ); - - proxy.send(tag(window::Screenshot::new( - bytes, - window.state.physical_size(), - ))); - } - } - }, - command::Action::System(action) => match action { - system::Action::QueryInformation(_tag) => { - #[cfg(feature = "system")] - { - let graphics_info = compositor.fetch_information(); - let mut proxy = proxy.clone(); - - let _ = std::thread::spawn(move || { - let information = - crate::system::information(graphics_info); - - let message = _tag(information); - - proxy.send(message); - }); - } - } - }, - command::Action::Widget(action) => { - let mut current_operation = Some(action); - - let mut uis = build_user_interfaces( - application, - window_manager, - std::mem::take(ui_caches), - ); - - 'operate: while let Some(mut operation) = - current_operation.take() - { - for (id, ui) in uis.iter_mut() { - if let Some(window) = window_manager.get_mut(*id) { - ui.operate(&window.renderer, operation.as_mut()); - - match operation.finish() { - operation::Outcome::None => {} - operation::Outcome::Some(message) => { - proxy.send(message); - - // operation completed, don't need to try to operate on rest of UIs - break 'operate; - } - operation::Outcome::Chain(next) => { - current_operation = Some(next); - } - } - } - } - } - - *ui_caches = - uis.drain().map(|(id, ui)| (id, ui.into_cache())).collect(); - } - command::Action::LoadFont { bytes, tagger } => { - // TODO: Error handling (?) - compositor.load_font(bytes.clone()); - - proxy.send(tagger(Ok(()))); - } - command::Action::Custom(_) => { - log::warn!("Unsupported custom action in `iced_winit` shell"); - } - } - } -} - -/// Build the user interface for every window. -pub fn build_user_interfaces<'a, A: Application, C>( - application: &'a A, - window_manager: &mut WindowManager<A, C>, - mut cached_user_interfaces: FxHashMap<window::Id, user_interface::Cache>, -) -> FxHashMap<window::Id, UserInterface<'a, A::Message, A::Theme, A::Renderer>> -where - C: Compositor<Renderer = A::Renderer>, - A::Theme: DefaultStyle, -{ - cached_user_interfaces - .drain() - .filter_map(|(id, cache)| { - let window = window_manager.get_mut(id)?; - - Some(( - id, - build_user_interface( - application, - cache, - &mut window.renderer, - window.state.logical_size(), - id, - ), - )) - }) - .collect() -} - -/// Returns true if the provided event should cause an [`Application`] to -/// exit. -pub fn user_force_quit( - event: &winit::event::WindowEvent, - _modifiers: winit::keyboard::ModifiersState, -) -> bool { - match event { - #[cfg(target_os = "macos")] - winit::event::WindowEvent::KeyboardInput { - event: - winit::event::KeyEvent { - logical_key: winit::keyboard::Key::Character(c), - state: winit::event::ElementState::Pressed, - .. - }, - .. - } if c == "q" && _modifiers.super_key() => true, - _ => false, - } -} diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs deleted file mode 100644 index 57a7dc7e..00000000 --- a/winit/src/multi_window/window_manager.rs +++ /dev/null @@ -1,157 +0,0 @@ -use crate::core::mouse; -use crate::core::window::Id; -use crate::core::{Point, Size}; -use crate::graphics::Compositor; -use crate::multi_window::{Application, DefaultStyle, State}; - -use std::collections::BTreeMap; -use std::sync::Arc; -use winit::monitor::MonitorHandle; - -#[allow(missing_debug_implementations)] -pub struct WindowManager<A, C> -where - A: Application, - C: Compositor<Renderer = A::Renderer>, - A::Theme: DefaultStyle, -{ - aliases: BTreeMap<winit::window::WindowId, Id>, - entries: BTreeMap<Id, Window<A, C>>, -} - -impl<A, C> WindowManager<A, C> -where - A: Application, - C: Compositor<Renderer = A::Renderer>, - A::Theme: DefaultStyle, -{ - pub fn new() -> Self { - Self { - aliases: BTreeMap::new(), - entries: BTreeMap::new(), - } - } - - pub fn insert( - &mut self, - id: Id, - window: Arc<winit::window::Window>, - application: &A, - compositor: &mut C, - exit_on_close_request: bool, - ) -> &mut Window<A, C> { - let state = State::new(application, id, &window); - let viewport_version = state.viewport_version(); - let physical_size = state.physical_size(); - let surface = compositor.create_surface( - window.clone(), - physical_size.width, - physical_size.height, - ); - let renderer = compositor.create_renderer(); - - let _ = self.aliases.insert(window.id(), id); - - let _ = self.entries.insert( - id, - Window { - raw: window, - state, - viewport_version, - exit_on_close_request, - surface, - renderer, - mouse_interaction: mouse::Interaction::None, - }, - ); - - self.entries - .get_mut(&id) - .expect("Get window that was just inserted") - } - - pub fn is_empty(&self) -> bool { - self.entries.is_empty() - } - - pub fn iter_mut( - &mut self, - ) -> impl Iterator<Item = (Id, &mut Window<A, C>)> { - self.entries.iter_mut().map(|(k, v)| (*k, v)) - } - - pub fn get_mut(&mut self, id: Id) -> Option<&mut Window<A, C>> { - self.entries.get_mut(&id) - } - - pub fn get_mut_alias( - &mut self, - id: winit::window::WindowId, - ) -> Option<(Id, &mut Window<A, C>)> { - let id = self.aliases.get(&id).copied()?; - - Some((id, self.get_mut(id)?)) - } - - pub fn last_monitor(&self) -> Option<MonitorHandle> { - self.entries.values().last()?.raw.current_monitor() - } - - pub fn remove(&mut self, id: Id) -> Option<Window<A, C>> { - let window = self.entries.remove(&id)?; - let _ = self.aliases.remove(&window.raw.id()); - - Some(window) - } -} - -impl<A, C> Default for WindowManager<A, C> -where - A: Application, - C: Compositor<Renderer = A::Renderer>, - A::Theme: DefaultStyle, -{ - fn default() -> Self { - Self::new() - } -} - -#[allow(missing_debug_implementations)] -pub struct Window<A, C> -where - A: Application, - C: Compositor<Renderer = A::Renderer>, - A::Theme: DefaultStyle, -{ - pub raw: Arc<winit::window::Window>, - pub state: State<A>, - pub viewport_version: u64, - pub exit_on_close_request: bool, - pub mouse_interaction: mouse::Interaction, - pub surface: C::Surface, - pub renderer: A::Renderer, -} - -impl<A, C> Window<A, C> -where - A: Application, - C: Compositor<Renderer = A::Renderer>, - A::Theme: DefaultStyle, -{ - pub fn position(&self) -> Option<Point> { - self.raw - .inner_position() - .ok() - .map(|position| position.to_logical(self.raw.scale_factor())) - .map(|position| Point { - x: position.x, - y: position.y, - }) - } - - pub fn size(&self) -> Size { - let size = self.raw.inner_size().to_logical(self.raw.scale_factor()); - - Size::new(size.width, size.height) - } -} diff --git a/winit/src/program.rs b/winit/src/program.rs new file mode 100644 index 00000000..b8e9a067 --- /dev/null +++ b/winit/src/program.rs @@ -0,0 +1,1579 @@ +//! Create interactive, native cross-platform applications for WGPU. +mod state; +mod window_manager; + +pub use state::State; + +use crate::conversion; +use crate::core; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::theme; +use crate::core::time::Instant; +use crate::core::widget::operation; +use crate::core::window; +use crate::core::{Element, Point, Size}; +use crate::futures::futures::channel::mpsc; +use crate::futures::futures::channel::oneshot; +use crate::futures::futures::task; +use crate::futures::futures::{Future, StreamExt}; +use crate::futures::subscription::{self, Subscription}; +use crate::futures::{Executor, Runtime}; +use crate::graphics; +use crate::graphics::{Compositor, compositor}; +use crate::runtime::debug; +use crate::runtime::user_interface::{self, UserInterface}; +use crate::runtime::{self, Action, Task}; +use crate::{Clipboard, Error, Proxy, Settings}; + +use window_manager::WindowManager; + +use rustc_hash::FxHashMap; +use std::borrow::Cow; +use std::mem::ManuallyDrop; +use std::sync::Arc; + +/// An interactive, native, cross-platform, multi-windowed application. +/// +/// This trait is the main entrypoint of multi-window Iced. Once implemented, you can run +/// your GUI application by simply calling [`run`]. It will run in +/// its own window. +/// +/// A [`Program`] can execute asynchronous actions by returning a +/// [`Task`] in some of its methods. +/// +/// When using a [`Program`] with the `debug` feature enabled, a debug view +/// can be toggled by pressing `F12`. +pub trait Program +where + Self: Sized, + Self::Theme: theme::Base, +{ + /// The type of __messages__ your [`Program`] will produce. + type Message: std::fmt::Debug + Send; + + /// The theme used to draw the [`Program`]. + type Theme; + + /// The [`Executor`] that will run commands and subscriptions. + /// + /// The [default executor] can be a good starting point! + /// + /// [`Executor`]: Self::Executor + /// [default executor]: crate::futures::backend::default::Executor + type Executor: Executor; + + /// The graphics backend to use to draw the [`Program`]. + type Renderer: core::Renderer + core::text::Renderer; + + /// The data needed to initialize your [`Program`]. + type Flags; + + /// Initializes the [`Program`] with the flags provided to + /// [`run`] as part of the [`Settings`]. + /// + /// Here is where you should return the initial state of your app. + /// + /// Additionally, you can return a [`Task`] if you need to perform some + /// async action in the background on startup. This is useful if you want to + /// load state from a file, perform an initial HTTP request, etc. + fn new(flags: Self::Flags) -> (Self, Task<Self::Message>); + + /// Returns the current title of the [`Program`]. + /// + /// This title can be dynamic! The runtime will automatically update the + /// title of your application when necessary. + fn title(&self, window: window::Id) -> String; + + /// Handles a __message__ and updates the state of the [`Program`]. + /// + /// This is where you define your __update logic__. All the __messages__, + /// produced by either user interactions or commands, will be handled by + /// this method. + /// + /// Any [`Task`] returned will be executed immediately in the background by the + /// runtime. + fn update(&mut self, message: Self::Message) -> Task<Self::Message>; + + /// Returns the widgets to display in the [`Program`] for the `window`. + /// + /// These widgets can produce __messages__ based on user interaction. + fn view( + &self, + window: window::Id, + ) -> Element<'_, Self::Message, Self::Theme, Self::Renderer>; + + /// Returns the current `Theme` of the [`Program`]. + fn theme(&self, window: window::Id) -> Self::Theme; + + /// Returns the `Style` variation of the `Theme`. + fn style(&self, theme: &Self::Theme) -> theme::Style { + theme::Base::base(theme) + } + + /// Returns the event `Subscription` for the current state of the + /// application. + /// + /// The messages produced by the `Subscription` will be handled by + /// [`update`](#tymethod.update). + /// + /// A `Subscription` will be kept alive as long as you keep returning it! + /// + /// By default, it returns an empty subscription. + fn subscription(&self) -> Subscription<Self::Message> { + Subscription::none() + } + + /// Returns the scale factor of the window of the [`Program`]. + /// + /// It can be used to dynamically control the size of the UI at runtime + /// (i.e. zooming). + /// + /// For instance, a scale factor of `2.0` will make widgets twice as big, + /// while a scale factor of `0.5` will shrink them to half their size. + /// + /// By default, it returns `1.0`. + #[allow(unused_variables)] + fn scale_factor(&self, window: window::Id) -> f64 { + 1.0 + } +} + +/// Runs a [`Program`] with an executor, compositor, and the provided +/// settings. +pub fn run<P, C>( + settings: Settings, + graphics_settings: graphics::Settings, + window_settings: Option<window::Settings>, + flags: P::Flags, +) -> Result<(), Error> +where + P: Program + 'static, + C: Compositor<Renderer = P::Renderer> + 'static, + P::Theme: theme::Base, +{ + use winit::event_loop::EventLoop; + + let boot_span = debug::boot(); + + let event_loop = EventLoop::with_user_event() + .build() + .expect("Create event loop"); + + let (proxy, worker) = Proxy::new(event_loop.create_proxy()); + + let mut runtime = { + let executor = + P::Executor::new().map_err(Error::ExecutorCreationFailed)?; + executor.spawn(worker); + + Runtime::new(executor, proxy.clone()) + }; + + let (program, task) = runtime.enter(|| P::new(flags)); + let is_daemon = window_settings.is_none(); + + let task = if let Some(window_settings) = window_settings { + let mut task = Some(task); + + let (_id, open) = runtime::window::open(window_settings); + + open.then(move |_| task.take().unwrap_or(Task::none())) + } else { + task + }; + + if let Some(stream) = runtime::task::into_stream(task) { + runtime.run(stream); + } + + runtime.track(subscription::into_recipes( + runtime.enter(|| program.subscription().map(Action::Output)), + )); + + let (event_sender, event_receiver) = mpsc::unbounded(); + let (control_sender, control_receiver) = mpsc::unbounded(); + + let instance = Box::pin(run_instance::<P, C>( + program, + runtime, + proxy.clone(), + event_receiver, + control_sender, + is_daemon, + graphics_settings, + settings.fonts, + )); + + let context = task::Context::from_waker(task::noop_waker_ref()); + + struct Runner<Message: 'static, F> { + instance: std::pin::Pin<Box<F>>, + context: task::Context<'static>, + id: Option<String>, + sender: mpsc::UnboundedSender<Event<Action<Message>>>, + receiver: mpsc::UnboundedReceiver<Control>, + error: Option<Error>, + + #[cfg(target_arch = "wasm32")] + canvas: Option<web_sys::HtmlCanvasElement>, + } + + let runner = Runner { + instance, + context, + id: settings.id, + sender: event_sender, + receiver: control_receiver, + error: None, + + #[cfg(target_arch = "wasm32")] + canvas: None, + }; + + boot_span.finish(); + + impl<Message, F> winit::application::ApplicationHandler<Action<Message>> + for Runner<Message, F> + where + Message: std::fmt::Debug, + F: Future<Output = ()>, + { + fn resumed( + &mut self, + _event_loop: &winit::event_loop::ActiveEventLoop, + ) { + } + + fn new_events( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + cause: winit::event::StartCause, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::NewEvents(cause)), + ); + } + + fn window_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + window_id: winit::window::WindowId, + event: winit::event::WindowEvent, + ) { + #[cfg(target_os = "windows")] + let is_move_or_resize = matches!( + event, + winit::event::WindowEvent::Resized(_) + | winit::event::WindowEvent::Moved(_) + ); + + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::WindowEvent { + window_id, + event, + }), + ); + + // TODO: Remove when unnecessary + // On Windows, we emulate an `AboutToWait` event after every `Resized` event + // since the event loop does not resume during resize interaction. + // More details: https://github.com/rust-windowing/winit/issues/3272 + #[cfg(target_os = "windows")] + { + if is_move_or_resize { + self.process_event( + event_loop, + Event::EventLoopAwakened( + winit::event::Event::AboutToWait, + ), + ); + } + } + } + + fn user_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + action: Action<Message>, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::UserEvent( + action, + )), + ); + } + + fn received_url( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + url: String, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened( + winit::event::Event::PlatformSpecific( + winit::event::PlatformSpecific::MacOS( + winit::event::MacOS::ReceivedUrl(url), + ), + ), + ), + ); + } + + fn about_to_wait( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + ) { + self.process_event( + event_loop, + Event::EventLoopAwakened(winit::event::Event::AboutToWait), + ); + } + } + + impl<Message, F> Runner<Message, F> + where + F: Future<Output = ()>, + { + fn process_event( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + event: Event<Action<Message>>, + ) { + if event_loop.exiting() { + return; + } + + self.sender.start_send(event).expect("Send event"); + + loop { + let poll = self.instance.as_mut().poll(&mut self.context); + + match poll { + task::Poll::Pending => match self.receiver.try_next() { + Ok(Some(control)) => match control { + Control::ChangeFlow(flow) => { + use winit::event_loop::ControlFlow; + + match (event_loop.control_flow(), flow) { + ( + ControlFlow::WaitUntil(current), + ControlFlow::WaitUntil(new), + ) if current < new => {} + ( + ControlFlow::WaitUntil(target), + ControlFlow::Wait, + ) if target > Instant::now() => {} + _ => { + event_loop.set_control_flow(flow); + } + } + } + Control::CreateWindow { + id, + settings, + title, + monitor, + on_open, + } => { + let exit_on_close_request = + settings.exit_on_close_request; + + let visible = settings.visible; + + #[cfg(target_arch = "wasm32")] + let target = + settings.platform_specific.target.clone(); + + let window_attributes = + conversion::window_attributes( + settings, + &title, + monitor + .or(event_loop.primary_monitor()), + self.id.clone(), + ) + .with_visible(false); + + #[cfg(target_arch = "wasm32")] + let window_attributes = { + use winit::platform::web::WindowAttributesExtWebSys; + window_attributes + .with_canvas(self.canvas.take()) + }; + + log::info!( + "Window attributes for id `{id:#?}`: {window_attributes:#?}" + ); + + // On macOS, the `position` in `WindowAttributes` represents the "inner" + // position of the window; while on other platforms it's the "outer" position. + // We fix the inconsistency on macOS by positioning the window after creation. + #[cfg(target_os = "macos")] + let mut window_attributes = window_attributes; + + #[cfg(target_os = "macos")] + let position = + window_attributes.position.take(); + + let window = event_loop + .create_window(window_attributes) + .expect("Create window"); + + #[cfg(target_os = "macos")] + if let Some(position) = position { + window.set_outer_position(position); + } + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::WindowExtWebSys; + + let canvas = window + .canvas() + .expect("Get window canvas"); + + let _ = canvas.set_attribute( + "style", + "display: block; width: 100%; height: 100%", + ); + + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let body = document.body().unwrap(); + + let target = target.and_then(|target| { + body.query_selector(&format!( + "#{target}" + )) + .ok() + .unwrap_or(None) + }); + + match target { + Some(node) => { + let _ = node + .replace_with_with_node_1( + &canvas, + ) + .expect(&format!( + "Could not replace #{}", + node.id() + )); + } + None => { + let _ = body + .append_child(&canvas) + .expect( + "Append canvas to HTML body", + ); + } + }; + } + + self.process_event( + event_loop, + Event::WindowCreated { + id, + window: Arc::new(window), + exit_on_close_request, + make_visible: visible, + on_open, + }, + ); + } + Control::Exit => { + event_loop.exit(); + } + Control::Crash(error) => { + self.error = Some(error); + event_loop.exit(); + } + }, + _ => { + break; + } + }, + task::Poll::Ready(_) => { + event_loop.exit(); + break; + } + }; + } + } + } + + #[cfg(not(target_arch = "wasm32"))] + { + let mut runner = runner; + let _ = event_loop.run_app(&mut runner); + + runner.error.map(Err).unwrap_or(Ok(())) + } + + #[cfg(target_arch = "wasm32")] + { + use winit::platform::web::EventLoopExtWebSys; + let _ = event_loop.spawn_app(runner); + + Ok(()) + } +} + +#[derive(Debug)] +enum Event<Message: 'static> { + WindowCreated { + id: window::Id, + window: Arc<winit::window::Window>, + exit_on_close_request: bool, + make_visible: bool, + on_open: oneshot::Sender<window::Id>, + }, + EventLoopAwakened(winit::event::Event<Message>), +} + +#[derive(Debug)] +enum Control { + ChangeFlow(winit::event_loop::ControlFlow), + Exit, + Crash(Error), + CreateWindow { + id: window::Id, + settings: window::Settings, + title: String, + monitor: Option<winit::monitor::MonitorHandle>, + on_open: oneshot::Sender<window::Id>, + }, +} + +async fn run_instance<P, C>( + mut program: P, + mut runtime: Runtime<P::Executor, Proxy<P::Message>, Action<P::Message>>, + mut proxy: Proxy<P::Message>, + mut event_receiver: mpsc::UnboundedReceiver<Event<Action<P::Message>>>, + mut control_sender: mpsc::UnboundedSender<Control>, + is_daemon: bool, + graphics_settings: graphics::Settings, + default_fonts: Vec<Cow<'static, [u8]>>, +) where + P: Program + 'static, + C: Compositor<Renderer = P::Renderer> + 'static, + P::Theme: theme::Base, +{ + use winit::event; + use winit::event_loop::ControlFlow; + + let mut window_manager = WindowManager::new(); + let mut is_window_opening = !is_daemon; + + let mut compositor = None; + let mut events = Vec::new(); + let mut messages = Vec::new(); + let mut actions = 0; + + let mut ui_caches = FxHashMap::default(); + let mut user_interfaces = ManuallyDrop::new(FxHashMap::default()); + let mut clipboard = Clipboard::unconnected(); + let mut compositor_receiver: Option<oneshot::Receiver<_>> = None; + + loop { + let event = if compositor_receiver.is_some() { + let compositor_receiver = + compositor_receiver.take().expect("Waiting for compositor"); + + match compositor_receiver.await { + Ok(Ok((new_compositor, event))) => { + compositor = Some(new_compositor); + + Some(event) + } + Ok(Err(error)) => { + control_sender + .start_send(Control::Crash( + Error::GraphicsCreationFailed(error), + )) + .expect("Send control action"); + break; + } + Err(error) => { + panic!("Compositor initialization failed: {error}") + } + } + // Empty the queue if possible + } else if let Ok(event) = event_receiver.try_next() { + event + } else { + event_receiver.next().await + }; + + let Some(event) = event else { + break; + }; + + match event { + Event::WindowCreated { + id, + window, + exit_on_close_request, + make_visible, + on_open, + } => { + if compositor.is_none() { + let (compositor_sender, new_compositor_receiver) = + oneshot::channel(); + + compositor_receiver = Some(new_compositor_receiver); + + let create_compositor = { + let default_fonts = default_fonts.clone(); + + async move { + let mut compositor = + C::new(graphics_settings, window.clone()).await; + + if let Ok(compositor) = &mut compositor { + for font in default_fonts { + compositor.load_font(font.clone()); + } + } + + compositor_sender + .send(compositor.map(|compositor| { + ( + compositor, + Event::WindowCreated { + id, + window, + exit_on_close_request, + make_visible, + on_open, + }, + ) + })) + .ok() + .expect("Send compositor"); + } + }; + + #[cfg(not(target_arch = "wasm32"))] + crate::futures::futures::executor::block_on( + create_compositor, + ); + + #[cfg(target_arch = "wasm32")] + { + wasm_bindgen_futures::spawn_local(create_compositor); + } + + continue; + } + + let window = window_manager.insert( + id, + window, + &program, + compositor + .as_mut() + .expect("Compositor must be initialized"), + exit_on_close_request, + ); + + let logical_size = window.state.logical_size(); + + let _ = user_interfaces.insert( + id, + build_user_interface( + &program, + user_interface::Cache::default(), + &mut window.renderer, + logical_size, + id, + ), + ); + let _ = ui_caches.insert(id, user_interface::Cache::default()); + + if make_visible { + window.raw.set_visible(true); + } + + events.push(( + id, + core::Event::Window(window::Event::Opened { + position: window.position(), + size: window.size(), + }), + )); + + if clipboard.window_id().is_none() { + clipboard = Clipboard::connect(window.raw.clone()); + } + + let _ = on_open.send(id); + is_window_opening = false; + } + Event::EventLoopAwakened(event) => { + match event { + event::Event::NewEvents(event::StartCause::Init) => { + for (_id, window) in window_manager.iter_mut() { + window.raw.request_redraw(); + } + } + event::Event::NewEvents( + event::StartCause::ResumeTimeReached { .. }, + ) => { + let now = Instant::now(); + + for (_id, window) in window_manager.iter_mut() { + if let Some(redraw_at) = window.redraw_at { + if redraw_at <= now { + window.raw.request_redraw(); + window.redraw_at = None; + } + } + } + + if let Some(redraw_at) = window_manager.redraw_at() { + let _ = + control_sender.start_send(Control::ChangeFlow( + ControlFlow::WaitUntil(redraw_at), + )); + } else { + let _ = control_sender.start_send( + Control::ChangeFlow(ControlFlow::Wait), + ); + } + } + event::Event::PlatformSpecific( + event::PlatformSpecific::MacOS( + event::MacOS::ReceivedUrl(url), + ), + ) => { + runtime.broadcast( + subscription::Event::PlatformSpecific( + subscription::PlatformSpecific::MacOS( + subscription::MacOS::ReceivedUrl(url), + ), + ), + ); + } + event::Event::UserEvent(action) => { + run_action( + action, + &program, + &mut compositor, + &mut events, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + &mut is_window_opening, + ); + actions += 1; + } + event::Event::WindowEvent { + window_id: id, + event: event::WindowEvent::RedrawRequested, + .. + } => { + let Some(compositor) = &mut compositor else { + continue; + }; + + let Some((id, window)) = + window_manager.get_mut_alias(id) + else { + continue; + }; + + let physical_size = window.state.physical_size(); + + if physical_size.width == 0 || physical_size.height == 0 + { + continue; + } + + if window.viewport_version + != window.state.viewport_version() + { + let logical_size = window.state.logical_size(); + + let layout_span = debug::layout(id); + let ui = user_interfaces + .remove(&id) + .expect("Remove user interface"); + + let _ = user_interfaces.insert( + id, + ui.relayout(logical_size, &mut window.renderer), + ); + layout_span.finish(); + + compositor.configure_surface( + &mut window.surface, + physical_size.width, + physical_size.height, + ); + + window.viewport_version = + window.state.viewport_version(); + } + + let redraw_event = core::Event::Window( + window::Event::RedrawRequested(Instant::now()), + ); + + let cursor = window.state.cursor(); + + let ui = user_interfaces + .get_mut(&id) + .expect("Get user interface"); + + let (ui_state, _) = ui.update( + &[redraw_event.clone()], + cursor, + &mut window.renderer, + &mut clipboard, + &mut messages, + ); + + let draw_span = debug::draw(id); + let new_mouse_interaction = ui.draw( + &mut window.renderer, + window.state.theme(), + &renderer::Style { + text_color: window.state.text_color(), + }, + cursor, + ); + draw_span.finish(); + + if new_mouse_interaction != window.mouse_interaction { + window.raw.set_cursor( + conversion::mouse_interaction( + new_mouse_interaction, + ), + ); + + window.mouse_interaction = new_mouse_interaction; + } + + runtime.broadcast(subscription::Event::Interaction { + window: id, + event: redraw_event, + status: core::event::Status::Ignored, + }); + + if let user_interface::State::Updated { + redraw_request, + input_method, + } = ui_state + { + window.request_redraw(redraw_request); + window.request_input_method(input_method); + } + + window.draw_preedit(); + + let present_span = debug::present(id); + match compositor.present( + &mut window.renderer, + &mut window.surface, + window.state.viewport(), + window.state.background_color(), + ) { + Ok(()) => { + present_span.finish(); + } + Err(error) => match error { + // This is an unrecoverable error. + compositor::SurfaceError::OutOfMemory => { + panic!("{:?}", error); + } + _ => { + present_span.finish(); + + log::error!( + "Error {error:?} when \ + presenting surface." + ); + + // Try rendering all windows again next frame. + for (_id, window) in + window_manager.iter_mut() + { + window.raw.request_redraw(); + } + } + }, + } + } + event::Event::WindowEvent { + event: window_event, + window_id, + } => { + if !is_daemon + && matches!( + window_event, + winit::event::WindowEvent::Destroyed + ) + && !is_window_opening + && window_manager.is_empty() + { + control_sender + .start_send(Control::Exit) + .expect("Send control action"); + + continue; + } + + let Some((id, window)) = + window_manager.get_mut_alias(window_id) + else { + continue; + }; + + if matches!( + window_event, + winit::event::WindowEvent::Resized(_) + ) { + window.raw.request_redraw(); + } + + if matches!( + window_event, + winit::event::WindowEvent::CloseRequested + ) && window.exit_on_close_request + { + run_action( + Action::Window(runtime::window::Action::Close( + id, + )), + &program, + &mut compositor, + &mut events, + &mut messages, + &mut clipboard, + &mut control_sender, + &mut user_interfaces, + &mut window_manager, + &mut ui_caches, + &mut is_window_opening, + ); + } else { + window.state.update(&window.raw, &window_event); + + if let Some(event) = conversion::window_event( + window_event, + window.state.scale_factor(), + window.state.modifiers(), + ) { + events.push((id, event)); + } + } + } + event::Event::AboutToWait => { + if actions > 0 { + proxy.free_slots(actions); + actions = 0; + } + + if events.is_empty() + && messages.is_empty() + && window_manager.is_idle() + { + continue; + } + + let mut uis_stale = false; + + for (id, window) in window_manager.iter_mut() { + let interact_span = debug::interact(id); + let mut window_events = vec![]; + + events.retain(|(window_id, event)| { + if *window_id == id { + window_events.push(event.clone()); + false + } else { + true + } + }); + + if window_events.is_empty() && messages.is_empty() { + continue; + } + + let (ui_state, statuses) = user_interfaces + .get_mut(&id) + .expect("Get user interface") + .update( + &window_events, + window.state.cursor(), + &mut window.renderer, + &mut clipboard, + &mut messages, + ); + + #[cfg(feature = "unconditional-rendering")] + window.request_redraw( + window::RedrawRequest::NextFrame, + ); + + match ui_state { + user_interface::State::Updated { + redraw_request: _redraw_request, + .. + } => { + #[cfg(not( + feature = "unconditional-rendering" + ))] + window.request_redraw(_redraw_request); + } + user_interface::State::Outdated => { + uis_stale = true; + } + } + + for (event, status) in window_events + .into_iter() + .zip(statuses.into_iter()) + { + runtime.broadcast( + subscription::Event::Interaction { + window: id, + event, + status, + }, + ); + } + + interact_span.finish(); + } + + for (id, event) in events.drain(..) { + runtime.broadcast( + subscription::Event::Interaction { + window: id, + event, + status: core::event::Status::Ignored, + }, + ); + } + + if !messages.is_empty() || uis_stale { + let cached_interfaces: FxHashMap< + window::Id, + user_interface::Cache, + > = ManuallyDrop::into_inner(user_interfaces) + .drain() + .map(|(id, ui)| (id, ui.into_cache())) + .collect(); + + update(&mut program, &mut runtime, &mut messages); + + for (id, window) in window_manager.iter_mut() { + window.state.synchronize( + &program, + id, + &window.raw, + ); + + window.raw.request_redraw(); + } + + user_interfaces = + ManuallyDrop::new(build_user_interfaces( + &program, + &mut window_manager, + cached_interfaces, + )); + } + + if let Some(redraw_at) = window_manager.redraw_at() { + let _ = + control_sender.start_send(Control::ChangeFlow( + ControlFlow::WaitUntil(redraw_at), + )); + } else { + let _ = control_sender.start_send( + Control::ChangeFlow(ControlFlow::Wait), + ); + } + } + _ => {} + } + } + } + } + + let _ = ManuallyDrop::into_inner(user_interfaces); +} + +/// Builds a window's [`UserInterface`] for the [`Program`]. +fn build_user_interface<'a, P: Program>( + program: &'a P, + cache: user_interface::Cache, + renderer: &mut P::Renderer, + size: Size, + id: window::Id, +) -> UserInterface<'a, P::Message, P::Theme, P::Renderer> +where + P::Theme: theme::Base, +{ + let view_span = debug::view(id); + let view = program.view(id); + view_span.finish(); + + let layout_span = debug::layout(id); + let user_interface = UserInterface::build(view, size, cache, renderer); + layout_span.finish(); + + user_interface +} + +fn update<P: Program, E: Executor>( + program: &mut P, + runtime: &mut Runtime<E, Proxy<P::Message>, Action<P::Message>>, + messages: &mut Vec<P::Message>, +) where + P::Theme: theme::Base, +{ + for message in messages.drain(..) { + let update_span = debug::update(&message); + let task = runtime.enter(|| program.update(message)); + update_span.finish(); + + if let Some(stream) = runtime::task::into_stream(task) { + runtime.run(stream); + } + } + + let subscription = runtime.enter(|| program.subscription()); + runtime.track(subscription::into_recipes(subscription.map(Action::Output))); +} + +fn run_action<P, C>( + action: Action<P::Message>, + program: &P, + compositor: &mut Option<C>, + events: &mut Vec<(window::Id, core::Event)>, + messages: &mut Vec<P::Message>, + clipboard: &mut Clipboard, + control_sender: &mut mpsc::UnboundedSender<Control>, + interfaces: &mut FxHashMap< + window::Id, + UserInterface<'_, P::Message, P::Theme, P::Renderer>, + >, + window_manager: &mut WindowManager<P, C>, + ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>, + is_window_opening: &mut bool, +) where + P: Program, + C: Compositor<Renderer = P::Renderer> + 'static, + P::Theme: theme::Base, +{ + use crate::runtime::clipboard; + use crate::runtime::system; + use crate::runtime::window; + + match action { + Action::Output(message) => { + messages.push(message); + } + Action::Clipboard(action) => match action { + clipboard::Action::Read { target, channel } => { + let _ = channel.send(clipboard.read(target)); + } + clipboard::Action::Write { target, contents } => { + clipboard.write(target, contents); + } + }, + Action::Window(action) => match action { + window::Action::Open(id, settings, channel) => { + let monitor = window_manager.last_monitor(); + + control_sender + .start_send(Control::CreateWindow { + id, + settings, + title: program.title(id), + monitor, + on_open: channel, + }) + .expect("Send control action"); + + *is_window_opening = true; + } + window::Action::Close(id) => { + let _ = ui_caches.remove(&id); + let _ = interfaces.remove(&id); + + if let Some(window) = window_manager.remove(id) { + if clipboard.window_id() == Some(window.raw.id()) { + *clipboard = window_manager + .first() + .map(|window| window.raw.clone()) + .map(Clipboard::connect) + .unwrap_or_else(Clipboard::unconnected); + } + + events.push(( + id, + core::Event::Window(core::window::Event::Closed), + )); + } + + if window_manager.is_empty() { + *compositor = None; + } + } + window::Action::GetOldest(channel) => { + let id = + window_manager.iter_mut().next().map(|(id, _window)| id); + + let _ = channel.send(id); + } + window::Action::GetLatest(channel) => { + let id = + window_manager.iter_mut().last().map(|(id, _window)| id); + + let _ = channel.send(id); + } + window::Action::Drag(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.drag_window(); + } + } + window::Action::DragResize(id, direction) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.drag_resize_window( + conversion::resize_direction(direction), + ); + } + } + window::Action::Resize(id, size) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.request_inner_size( + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + }, + ); + } + } + window::Action::SetMinSize(id, size) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_min_inner_size(size.map(|size| { + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + } + })); + } + } + window::Action::SetMaxSize(id, size) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_max_inner_size(size.map(|size| { + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + } + })); + } + } + window::Action::SetResizeIncrements(id, increments) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_resize_increments(increments.map(|size| { + winit::dpi::LogicalSize { + width: size.width, + height: size.height, + } + })); + } + } + window::Action::SetResizable(id, resizable) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_resizable(resizable); + } + } + window::Action::GetSize(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let size = window + .raw + .inner_size() + .to_logical(window.raw.scale_factor()); + + let _ = channel.send(Size::new(size.width, size.height)); + } + } + window::Action::GetMaximized(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.is_maximized()); + } + } + window::Action::Maximize(id, maximized) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(maximized); + } + } + window::Action::GetMinimized(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.is_minimized()); + } + } + window::Action::Minimize(id, minimized) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_minimized(minimized); + } + } + window::Action::GetPosition(id, channel) => { + if let Some(window) = window_manager.get(id) { + let position = window + .raw + .outer_position() + .map(|position| { + let position = position + .to_logical::<f32>(window.raw.scale_factor()); + + Point::new(position.x, position.y) + }) + .ok(); + + let _ = channel.send(position); + } + } + window::Action::GetScaleFactor(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let scale_factor = window.raw.scale_factor(); + + let _ = channel.send(scale_factor as f32); + } + } + window::Action::Move(id, position) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_outer_position( + winit::dpi::LogicalPosition { + x: position.x, + y: position.y, + }, + ); + } + } + window::Action::SetMode(id, mode) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_visible(conversion::visible(mode)); + window.raw.set_fullscreen(conversion::fullscreen( + window.raw.current_monitor(), + mode, + )); + } + } + window::Action::SetIcon(id, icon) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_window_icon(conversion::icon(icon)); + } + } + window::Action::GetMode(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let mode = if window.raw.is_visible().unwrap_or(true) { + conversion::mode(window.raw.fullscreen()) + } else { + core::window::Mode::Hidden + }; + + let _ = channel.send(mode); + } + } + window::Action::ToggleMaximize(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_maximized(!window.raw.is_maximized()); + } + } + window::Action::ToggleDecorations(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.set_decorations(!window.raw.is_decorated()); + } + } + window::Action::RequestUserAttention(id, attention_type) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.request_user_attention( + attention_type.map(conversion::user_attention), + ); + } + } + window::Action::GainFocus(id) => { + if let Some(window) = window_manager.get_mut(id) { + window.raw.focus_window(); + } + } + window::Action::SetLevel(id, level) => { + if let Some(window) = window_manager.get_mut(id) { + window + .raw + .set_window_level(conversion::window_level(level)); + } + } + window::Action::ShowSystemMenu(id) => { + if let Some(window) = window_manager.get_mut(id) { + if let mouse::Cursor::Available(point) = + window.state.cursor() + { + window.raw.show_window_menu( + winit::dpi::LogicalPosition { + x: point.x, + y: point.y, + }, + ); + } + } + } + window::Action::GetRawId(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = channel.send(window.raw.id().into()); + } + } + window::Action::RunWithHandle(id, f) => { + use window::raw_window_handle::HasWindowHandle; + + if let Some(handle) = window_manager + .get_mut(id) + .and_then(|window| window.raw.window_handle().ok()) + { + f(handle); + } + } + window::Action::Screenshot(id, channel) => { + if let Some(window) = window_manager.get_mut(id) { + if let Some(compositor) = compositor { + let bytes = compositor.screenshot( + &mut window.renderer, + window.state.viewport(), + window.state.background_color(), + ); + + let _ = channel.send(core::window::Screenshot::new( + bytes, + window.state.physical_size(), + window.state.viewport().scale_factor(), + )); + } + } + } + window::Action::EnableMousePassthrough(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.set_cursor_hittest(false); + } + } + window::Action::DisableMousePassthrough(id) => { + if let Some(window) = window_manager.get_mut(id) { + let _ = window.raw.set_cursor_hittest(true); + } + } + }, + Action::System(action) => match action { + system::Action::QueryInformation(_channel) => { + #[cfg(feature = "system")] + { + if let Some(compositor) = compositor { + let graphics_info = compositor.fetch_information(); + + let _ = std::thread::spawn(move || { + let information = + crate::system::information(graphics_info); + + let _ = _channel.send(information); + }); + } + } + } + }, + Action::Widget(operation) => { + let mut current_operation = Some(operation); + + while let Some(mut operation) = current_operation.take() { + for (id, ui) in interfaces.iter_mut() { + if let Some(window) = window_manager.get_mut(*id) { + ui.operate(&window.renderer, operation.as_mut()); + } + } + + match operation.finish() { + operation::Outcome::None => {} + operation::Outcome::Some(()) => {} + operation::Outcome::Chain(next) => { + current_operation = Some(next); + } + } + } + } + Action::LoadFont { bytes, channel } => { + if let Some(compositor) = compositor { + // TODO: Error handling (?) + compositor.load_font(bytes.clone()); + + let _ = channel.send(Ok(())); + } + } + Action::Exit => { + control_sender + .start_send(Control::Exit) + .expect("Send control action"); + } + } +} + +/// Build the user interface for every window. +pub fn build_user_interfaces<'a, P: Program, C>( + program: &'a P, + window_manager: &mut WindowManager<P, C>, + mut cached_user_interfaces: FxHashMap<window::Id, user_interface::Cache>, +) -> FxHashMap<window::Id, UserInterface<'a, P::Message, P::Theme, P::Renderer>> +where + C: Compositor<Renderer = P::Renderer>, + P::Theme: theme::Base, +{ + cached_user_interfaces + .drain() + .filter_map(|(id, cache)| { + let window = window_manager.get_mut(id)?; + + Some(( + id, + build_user_interface( + program, + cache, + &mut window.renderer, + window.state.logical_size(), + id, + ), + )) + }) + .collect() +} + +/// Returns true if the provided event should cause a [`Program`] to +/// exit. +pub fn user_force_quit( + event: &winit::event::WindowEvent, + _modifiers: winit::keyboard::ModifiersState, +) -> bool { + match event { + #[cfg(target_os = "macos")] + winit::event::WindowEvent::KeyboardInput { + event: + winit::event::KeyEvent { + logical_key: winit::keyboard::Key::Character(c), + state: winit::event::ElementState::Pressed, + .. + }, + .. + } if c == "q" && _modifiers.super_key() => true, + _ => false, + } +} diff --git a/winit/src/multi_window/state.rs b/winit/src/program/state.rs similarity index 85% rename from winit/src/multi_window/state.rs rename to winit/src/program/state.rs index 5368b849..1b844b82 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/program/state.rs @@ -1,17 +1,18 @@ use crate::conversion; -use crate::core::{mouse, window}; use crate::core::{Color, Size}; +use crate::core::{mouse, theme, window}; use crate::graphics::Viewport; -use crate::multi_window::{self, Application}; -use std::fmt::{Debug, Formatter}; +use crate::program::Program; use winit::event::{Touch, WindowEvent}; use winit::window::Window; -/// The state of a multi-windowed [`Application`]. -pub struct State<A: Application> +use std::fmt::{Debug, Formatter}; + +/// The state of a multi-windowed [`Program`]. +pub struct State<P: Program> where - A::Theme: multi_window::DefaultStyle, + P::Theme: theme::Base, { title: String, scale_factor: f64, @@ -19,13 +20,13 @@ where viewport_version: u64, cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, modifiers: winit::keyboard::ModifiersState, - theme: A::Theme, - appearance: multi_window::Appearance, + theme: P::Theme, + style: theme::Style, } -impl<A: Application> Debug for State<A> +impl<P: Program> Debug for State<P> where - A::Theme: multi_window::DefaultStyle, + P::Theme: theme::Base, { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("multi_window::State") @@ -34,25 +35,25 @@ where .field("viewport", &self.viewport) .field("viewport_version", &self.viewport_version) .field("cursor_position", &self.cursor_position) - .field("appearance", &self.appearance) + .field("style", &self.style) .finish() } } -impl<A: Application> State<A> +impl<P: Program> State<P> where - A::Theme: multi_window::DefaultStyle, + P::Theme: theme::Base, { - /// Creates a new [`State`] for the provided [`Application`]'s `window`. + /// Creates a new [`State`] for the provided [`Program`]'s `window`. pub fn new( - application: &A, + application: &P, window_id: window::Id, window: &Window, ) -> Self { let title = application.title(window_id); let scale_factor = application.scale_factor(window_id); let theme = application.theme(window_id); - let appearance = application.style(&theme); + let style = application.style(&theme); let viewport = { let physical_size = window.inner_size(); @@ -71,7 +72,7 @@ where cursor_position: None, modifiers: winit::keyboard::ModifiersState::default(), theme, - appearance, + style, } } @@ -121,18 +122,18 @@ where } /// Returns the current theme of the [`State`]. - pub fn theme(&self) -> &A::Theme { + pub fn theme(&self) -> &P::Theme { &self.theme } /// Returns the current background [`Color`] of the [`State`]. pub fn background_color(&self) -> Color { - self.appearance.background_color + self.style.background_color } /// Returns the current text [`Color`] of the [`State`]. pub fn text_color(&self) -> Color { - self.appearance.text_color + self.style.text_color } /// Processes the provided window event and updates the [`State`] accordingly. @@ -177,14 +178,14 @@ where } } - /// Synchronizes the [`State`] with its [`Application`] and its respective + /// Synchronizes the [`State`] with its [`Program`] and its respective /// window. /// - /// Normally, an [`Application`] should be synchronized with its [`State`] + /// Normally, a [`Program`] should be synchronized with its [`State`] /// and window after calling [`State::update`]. pub fn synchronize( &mut self, - application: &A, + application: &P, window_id: window::Id, window: &Window, ) { @@ -216,6 +217,6 @@ where // Update theme and appearance self.theme = application.theme(window_id); - self.appearance = application.style(&self.theme); + self.style = application.style(&self.theme); } } diff --git a/winit/src/program/window_manager.rs b/winit/src/program/window_manager.rs new file mode 100644 index 00000000..139d787a --- /dev/null +++ b/winit/src/program/window_manager.rs @@ -0,0 +1,422 @@ +use crate::conversion; +use crate::core::alignment; +use crate::core::input_method; +use crate::core::mouse; +use crate::core::renderer; +use crate::core::text; +use crate::core::theme; +use crate::core::time::Instant; +use crate::core::window::{Id, RedrawRequest}; +use crate::core::{ + Color, InputMethod, Padding, Point, Rectangle, Size, Text, Vector, +}; +use crate::graphics::Compositor; +use crate::program::{Program, State}; + +use winit::dpi::{LogicalPosition, LogicalSize}; +use winit::monitor::MonitorHandle; + +use std::collections::BTreeMap; +use std::sync::Arc; + +#[allow(missing_debug_implementations)] +pub struct WindowManager<P, C> +where + P: Program, + C: Compositor<Renderer = P::Renderer>, + P::Theme: theme::Base, +{ + aliases: BTreeMap<winit::window::WindowId, Id>, + entries: BTreeMap<Id, Window<P, C>>, +} + +impl<P, C> WindowManager<P, C> +where + P: Program, + C: Compositor<Renderer = P::Renderer>, + P::Theme: theme::Base, +{ + pub fn new() -> Self { + Self { + aliases: BTreeMap::new(), + entries: BTreeMap::new(), + } + } + + pub fn insert( + &mut self, + id: Id, + window: Arc<winit::window::Window>, + application: &P, + compositor: &mut C, + exit_on_close_request: bool, + ) -> &mut Window<P, C> { + let state = State::new(application, id, &window); + let viewport_version = state.viewport_version(); + let physical_size = state.physical_size(); + let surface = compositor.create_surface( + window.clone(), + physical_size.width, + physical_size.height, + ); + let renderer = compositor.create_renderer(); + + let _ = self.aliases.insert(window.id(), id); + + let _ = self.entries.insert( + id, + Window { + raw: window, + state, + viewport_version, + exit_on_close_request, + surface, + renderer, + mouse_interaction: mouse::Interaction::None, + redraw_at: None, + preedit: None, + ime_state: None, + }, + ); + + self.entries + .get_mut(&id) + .expect("Get window that was just inserted") + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + pub fn is_idle(&self) -> bool { + self.entries + .values() + .all(|window| window.redraw_at.is_none()) + } + + pub fn redraw_at(&self) -> Option<Instant> { + self.entries + .values() + .filter_map(|window| window.redraw_at) + .min() + } + + pub fn first(&self) -> Option<&Window<P, C>> { + self.entries.first_key_value().map(|(_id, window)| window) + } + + pub fn iter_mut( + &mut self, + ) -> impl Iterator<Item = (Id, &mut Window<P, C>)> { + self.entries.iter_mut().map(|(k, v)| (*k, v)) + } + + pub fn get(&self, id: Id) -> Option<&Window<P, C>> { + self.entries.get(&id) + } + + pub fn get_mut(&mut self, id: Id) -> Option<&mut Window<P, C>> { + self.entries.get_mut(&id) + } + + pub fn get_mut_alias( + &mut self, + id: winit::window::WindowId, + ) -> Option<(Id, &mut Window<P, C>)> { + let id = self.aliases.get(&id).copied()?; + + Some((id, self.get_mut(id)?)) + } + + pub fn last_monitor(&self) -> Option<MonitorHandle> { + self.entries.values().last()?.raw.current_monitor() + } + + pub fn remove(&mut self, id: Id) -> Option<Window<P, C>> { + let window = self.entries.remove(&id)?; + let _ = self.aliases.remove(&window.raw.id()); + + Some(window) + } +} + +impl<P, C> Default for WindowManager<P, C> +where + P: Program, + C: Compositor<Renderer = P::Renderer>, + P::Theme: theme::Base, +{ + fn default() -> Self { + Self::new() + } +} + +#[allow(missing_debug_implementations)] +pub struct Window<P, C> +where + P: Program, + C: Compositor<Renderer = P::Renderer>, + P::Theme: theme::Base, +{ + pub raw: Arc<winit::window::Window>, + pub state: State<P>, + pub viewport_version: u64, + pub exit_on_close_request: bool, + pub mouse_interaction: mouse::Interaction, + pub surface: C::Surface, + pub renderer: P::Renderer, + pub redraw_at: Option<Instant>, + preedit: Option<Preedit<P::Renderer>>, + ime_state: Option<(Point, input_method::Purpose)>, +} + +impl<P, C> Window<P, C> +where + P: Program, + C: Compositor<Renderer = P::Renderer>, + P::Theme: theme::Base, +{ + pub fn position(&self) -> Option<Point> { + self.raw + .outer_position() + .ok() + .map(|position| position.to_logical(self.raw.scale_factor())) + .map(|position| Point { + x: position.x, + y: position.y, + }) + } + + pub fn size(&self) -> Size { + let size = self.raw.inner_size().to_logical(self.raw.scale_factor()); + + Size::new(size.width, size.height) + } + + pub fn request_redraw(&mut self, redraw_request: RedrawRequest) { + match redraw_request { + RedrawRequest::NextFrame => { + self.raw.request_redraw(); + self.redraw_at = None; + } + RedrawRequest::At(at) => { + self.redraw_at = Some(at); + } + RedrawRequest::Wait => {} + } + } + + pub fn request_input_method(&mut self, input_method: InputMethod) { + match input_method { + InputMethod::Disabled => { + self.disable_ime(); + } + InputMethod::Enabled { + position, + purpose, + preedit, + } => { + self.enable_ime(position, purpose); + + if let Some(preedit) = preedit { + if preedit.content.is_empty() { + self.preedit = None; + } else { + let mut overlay = + self.preedit.take().unwrap_or_else(Preedit::new); + + overlay.update( + position, + &preedit, + self.state.background_color(), + &self.renderer, + ); + + self.preedit = Some(overlay); + } + } else { + self.preedit = None; + } + } + } + } + + pub fn draw_preedit(&mut self) { + if let Some(preedit) = &self.preedit { + preedit.draw( + &mut self.renderer, + self.state.text_color(), + self.state.background_color(), + &Rectangle::new( + Point::ORIGIN, + self.state.viewport().logical_size(), + ), + ); + } + } + + fn enable_ime(&mut self, position: Point, purpose: input_method::Purpose) { + if self.ime_state.is_none() { + self.raw.set_ime_allowed(true); + } + + if self.ime_state != Some((position, purpose)) { + self.raw.set_ime_cursor_area( + LogicalPosition::new(position.x, position.y), + LogicalSize::new(10, 10), // TODO? + ); + self.raw.set_ime_purpose(conversion::ime_purpose(purpose)); + + self.ime_state = Some((position, purpose)); + } + } + + fn disable_ime(&mut self) { + if self.ime_state.is_some() { + self.raw.set_ime_allowed(false); + self.ime_state = None; + } + + self.preedit = None; + } +} + +struct Preedit<Renderer> +where + Renderer: text::Renderer, +{ + position: Point, + content: Renderer::Paragraph, + spans: Vec<text::Span<'static, (), Renderer::Font>>, +} + +impl<Renderer> Preedit<Renderer> +where + Renderer: text::Renderer, +{ + fn new() -> Self { + Self { + position: Point::ORIGIN, + spans: Vec::new(), + content: Renderer::Paragraph::default(), + } + } + + fn update( + &mut self, + position: Point, + preedit: &input_method::Preedit, + background: Color, + renderer: &Renderer, + ) { + self.position = position; + + let spans = match &preedit.selection { + Some(selection) => { + vec![ + text::Span::new(&preedit.content[..selection.start]), + text::Span::new(if selection.start == selection.end { + "\u{200A}" + } else { + &preedit.content[selection.start..selection.end] + }) + .color(background), + text::Span::new(&preedit.content[selection.end..]), + ] + } + _ => vec![text::Span::new(&preedit.content)], + }; + + if spans != self.spans.as_slice() { + use text::Paragraph as _; + + self.content = Renderer::Paragraph::with_spans(Text { + content: &spans, + bounds: Size::INFINITY, + size: preedit + .text_size + .unwrap_or_else(|| renderer.default_size()), + line_height: text::LineHeight::default(), + font: renderer.default_font(), + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::None, + }); + + self.spans.clear(); + self.spans + .extend(spans.into_iter().map(text::Span::to_static)); + } + } + + fn draw( + &self, + renderer: &mut Renderer, + color: Color, + background: Color, + viewport: &Rectangle, + ) { + use text::Paragraph as _; + + if self.content.min_width() < 1.0 { + return; + } + + let mut bounds = Rectangle::new( + self.position - Vector::new(0.0, self.content.min_height()), + self.content.min_bounds(), + ); + + bounds.x = bounds + .x + .max(viewport.x) + .min(viewport.x + viewport.width - bounds.width); + + bounds.y = bounds + .y + .max(viewport.y) + .min(viewport.y + viewport.height - bounds.height); + + renderer.with_layer(bounds, |renderer| { + renderer.fill_quad( + renderer::Quad { + bounds, + ..Default::default() + }, + background, + ); + + renderer.fill_paragraph( + &self.content, + bounds.position(), + color, + bounds, + ); + + const UNDERLINE: f32 = 2.0; + + renderer.fill_quad( + renderer::Quad { + bounds: bounds.shrink(Padding { + top: bounds.height - UNDERLINE, + ..Default::default() + }), + ..Default::default() + }, + color, + ); + + for span_bounds in self.content.span_bounds(1) { + renderer.fill_quad( + renderer::Quad { + bounds: span_bounds + + (bounds.position() - Point::ORIGIN), + ..Default::default() + }, + color, + ); + } + }); + } +} diff --git a/winit/src/proxy.rs b/winit/src/proxy.rs index 3edc30ad..d8d3f4a2 100644 --- a/winit/src/proxy.rs +++ b/winit/src/proxy.rs @@ -1,20 +1,21 @@ use crate::futures::futures::{ + Future, Sink, StreamExt, channel::mpsc, select, task::{Context, Poll}, - Future, Sink, StreamExt, }; +use crate::runtime::Action; use std::pin::Pin; /// An event loop proxy with backpressure that implements `Sink`. #[derive(Debug)] -pub struct Proxy<Message: 'static> { - raw: winit::event_loop::EventLoopProxy<Message>, - sender: mpsc::Sender<Message>, +pub struct Proxy<T: 'static> { + raw: winit::event_loop::EventLoopProxy<Action<T>>, + sender: mpsc::Sender<Action<T>>, notifier: mpsc::Sender<usize>, } -impl<Message: 'static> Clone for Proxy<Message> { +impl<T: 'static> Clone for Proxy<T> { fn clone(&self) -> Self { Self { raw: self.raw.clone(), @@ -24,12 +25,12 @@ impl<Message: 'static> Clone for Proxy<Message> { } } -impl<Message: 'static> Proxy<Message> { +impl<T: 'static> Proxy<T> { const MAX_SIZE: usize = 100; /// Creates a new [`Proxy`] from an `EventLoopProxy`. pub fn new( - raw: winit::event_loop::EventLoopProxy<Message>, + raw: winit::event_loop::EventLoopProxy<Action<T>>, ) -> (Self, impl Future<Output = ()>) { let (notifier, mut processed) = mpsc::channel(Self::MAX_SIZE); let (sender, mut receiver) = mpsc::channel(Self::MAX_SIZE); @@ -72,16 +73,27 @@ impl<Message: 'static> Proxy<Message> { ) } - /// Sends a `Message` to the event loop. + /// Sends a value to the event loop. /// /// Note: This skips the backpressure mechanism with an unbounded /// channel. Use sparingly! - pub fn send(&mut self, message: Message) + pub fn send(&mut self, value: T) where - Message: std::fmt::Debug, + T: std::fmt::Debug, + { + self.send_action(Action::Output(value)); + } + + /// Sends an action to the event loop. + /// + /// Note: This skips the backpressure mechanism with an unbounded + /// channel. Use sparingly! + pub fn send_action(&mut self, action: Action<T>) + where + T: std::fmt::Debug, { self.raw - .send_event(message) + .send_event(action) .expect("Send message to event loop"); } @@ -92,7 +104,7 @@ impl<Message: 'static> Proxy<Message> { } } -impl<Message: 'static> Sink<Message> for Proxy<Message> { +impl<T: 'static> Sink<Action<T>> for Proxy<T> { type Error = mpsc::SendError; fn poll_ready( @@ -104,9 +116,9 @@ impl<Message: 'static> Sink<Message> for Proxy<Message> { fn start_send( mut self: Pin<&mut Self>, - message: Message, + action: Action<T>, ) -> Result<(), Self::Error> { - self.sender.start_send(message) + self.sender.start_send(action) } fn poll_flush( diff --git a/winit/src/settings.rs b/winit/src/settings.rs index 2e541128..e2bf8abf 100644 --- a/winit/src/settings.rs +++ b/winit/src/settings.rs @@ -1,25 +1,26 @@ //! Configure your application. -use crate::core::window; +use crate::core; use std::borrow::Cow; /// The settings of an application. #[derive(Debug, Clone, Default)] -pub struct Settings<Flags> { +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 [`window::Settings`]. - pub window: window::Settings, - - /// The data needed to initialize an [`Application`]. - /// - /// [`Application`]: crate::Application - pub flags: Flags, - /// The fonts to load on boot. pub fonts: Vec<Cow<'static, [u8]>>, } + +impl From<core::Settings> for Settings { + fn from(settings: core::Settings) -> Self { + Self { + id: settings.id, + fonts: settings.fonts, + } + } +} diff --git a/winit/src/system.rs b/winit/src/system.rs index c5a5b219..0b476773 100644 --- a/winit/src/system.rs +++ b/winit/src/system.rs @@ -1,15 +1,13 @@ //! Access the native system. use crate::graphics::compositor; -use crate::runtime::command::{self, Command}; use crate::runtime::system::{Action, Information}; +use crate::runtime::{self, Task}; /// Query for available system information. -pub fn fetch_information<Message>( - f: impl Fn(Information) -> Message + Send + 'static, -) -> Command<Message> { - Command::single(command::Action::System(Action::QueryInformation( - Box::new(f), - ))) +pub fn fetch_information() -> Task<Information> { + runtime::task::oneshot(|channel| { + runtime::Action::System(Action::QueryInformation(channel)) + }) } pub(crate) fn information( @@ -19,7 +17,11 @@ pub(crate) fn information( let mut system = System::new_all(); system.refresh_all(); - let cpu = system.global_cpu_info(); + let cpu_brand = system + .cpus() + .first() + .map(|cpu| cpu.brand().to_string()) + .unwrap_or_default(); let memory_used = sysinfo::get_current_pid() .and_then(|pid| system.process(pid).ok_or("Process not found")) @@ -31,7 +33,7 @@ pub(crate) fn information( system_kernel: System::kernel_version(), system_version: System::long_os_version(), system_short_version: System::os_version(), - cpu_brand: cpu.brand().into(), + cpu_brand, cpu_cores: system.physical_core_count(), memory_total: system.total_memory(), memory_used,