├── .github ├── ISSUE_TEMPLATE │ ├── BUG-REPORT.yml │ └── config.yml └── workflows │ ├── audit.yml │ ├── doc.yml │ ├── document.yml │ ├── format.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── counter │ ├── Cargo.toml │ ├── README.md │ ├── index.html │ └── src │ │ └── main.rs ├── pokedex │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── pong │ ├── Cargo.toml │ └── src │ │ ├── layer.rs │ │ ├── main.rs │ │ └── theme.rs └── stopwatch │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── main.rs │ └── theme.rs ├── justfile └── src ├── keyframes.rs ├── keyframes ├── cards.rs ├── helpers.rs └── toggler.rs ├── lib.rs ├── reexports ├── iced.rs ├── libcosmic.rs └── mod.rs ├── timeline.rs ├── utils.rs ├── widget.rs └── widget ├── cards.rs ├── cosmic_toggler.rs └── toggler.rs /.github/ISSUE_TEMPLATE/BUG-REPORT.yml: -------------------------------------------------------------------------------- 1 | name: I have a problem with the library 2 | description: File a bug report. 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this bug report! 9 | - type: checkboxes 10 | attributes: 11 | label: Is there an existing issue for this? 12 | description: | 13 | Please, search [the existing issues] and see if an issue already exists for the bug you encountered. 14 | 15 | [the existing issues]: https://github.com/iced-rs/iced/issues 16 | options: 17 | - label: I have searched the existing issues. 18 | required: true 19 | - type: checkboxes 20 | attributes: 21 | label: Is this issue related to iced? 22 | description: | 23 | If your application is crashing during startup or you are observing graphical glitches, there is a chance it may be caused by incompatible hardware or outdated graphics drivers. 24 | 25 | Before filing an issue... 26 | 27 | - If you are using `wgpu`, you need an environment that supports Vulkan, Metal, or DirectX 12. Please, make sure you can run [the `wgpu` examples]. 28 | - If you are using `glow`, you need support for OpenGL 2.1+. Please, make sure you can run [the `glow` examples]. 29 | 30 | If you have any issues running any of the examples, make sure your graphics drivers are up-to-date. If the issues persist, please report them to the authors of the libraries directly! 31 | 32 | [the `wgpu` examples]: https://github.com/gfx-rs/wgpu/tree/master/wgpu/examples 33 | [the `glow` examples]: https://github.com/grovesNL/glow/tree/main/examples 34 | options: 35 | - label: My hardware is compatible and my graphics drivers are up-to-date. 36 | required: true 37 | - type: textarea 38 | attributes: 39 | label: What happened? 40 | id: what-happened 41 | description: | 42 | What problem are you having? Please, also provide the steps to reproduce it. 43 | 44 | If the issue happens with a particular program, please share an [SSCCE]. 45 | 46 | [SSCCE]: http://sscce.org/ 47 | validations: 48 | required: true 49 | - type: textarea 50 | attributes: 51 | label: What is the expected behavior? 52 | id: what-expected 53 | description: What were you expecting to happen? 54 | validations: 55 | required: true 56 | - type: dropdown 57 | id: version 58 | attributes: 59 | label: Version 60 | description: | 61 | We only offer support for the latest release on crates.io and the `master` branch on this repository. Which version are you using? Please make sure you are using the latest patch available (e.g. run `cargo update`). 62 | 63 | If you are using an older release, please upgrade to the latest one before filing an issue. 64 | options: 65 | - master 66 | - 0.7 67 | validations: 68 | required: true 69 | - type: dropdown 70 | id: os 71 | attributes: 72 | label: Operative System 73 | description: Which operative system are you using? 74 | options: 75 | - Windows 76 | - macOS 77 | - Linux 78 | validations: 79 | required: true 80 | - type: textarea 81 | id: logs 82 | attributes: 83 | label: Do you have any log output? 84 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 85 | render: shell 86 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: I have a question 4 | url: https://github.com/iced-rs/iced/discussions/new?category=q-a 5 | about: Open a discussion with a Q&A format. 6 | - name: I want to start a discussion 7 | url: https://github.com/iced-rs/iced/discussions/new 8 | about: Open a new discussion if you have any suggestions, ideas, feature requests, or simply want to show off something you've made. 9 | - name: I want to chat with other users of the library 10 | url: https://discord.com/invite/3xZJ65GAhd 11 | about: Join the Discord Server and get involved with the community! 12 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Audit 2 | on: [push] 3 | jobs: 4 | dependencies: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: hecrj/setup-rust-action@v1 8 | - name: Install cargo-audit 9 | run: cargo install cargo-audit 10 | - uses: actions/checkout@master 11 | - name: Audit dependencies 12 | run: cargo audit 13 | -------------------------------------------------------------------------------- /.github/workflows/doc.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | on: [push, pull_request] 3 | jobs: 4 | all: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: hecrj/setup-rust-action@v1 8 | with: 9 | components: clippy 10 | - uses: actions/checkout@master 11 | - name: Check Docs 12 | run: cargo doc --workspace 13 | -------------------------------------------------------------------------------- /.github/workflows/document.yml: -------------------------------------------------------------------------------- 1 | name: Document 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | all: 8 | runs-on: ubuntu-20.04 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | steps: 12 | - uses: hecrj/setup-rust-action@v1 13 | with: 14 | rust-version: nightly 15 | - uses: actions/checkout@v2 16 | - name: Generate documentation 17 | run: | 18 | RUSTDOCFLAGS="--cfg docsrs" \ 19 | cargo doc --no-deps --all-features \ 20 | -p iced_core \ 21 | -p iced_style \ 22 | -p iced_futures \ 23 | -p iced_native \ 24 | -p iced_lazy \ 25 | -p iced_graphics \ 26 | -p iced_wgpu \ 27 | -p iced_glow \ 28 | -p iced_winit \ 29 | -p iced_glutin \ 30 | -p iced 31 | - name: Write CNAME file 32 | run: echo 'docs.iced.rs' > ./target/doc/CNAME 33 | - name: Publish documentation 34 | uses: peaceiris/actions-gh-pages@v3 35 | with: 36 | deploy_key: ${{ secrets.DOCS_DEPLOY_KEY }} 37 | external_repository: iced-rs/docs 38 | publish_dir: ./target/doc 39 | force_orphan: true 40 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Format 2 | on: [push, pull_request] 3 | jobs: 4 | all: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: hecrj/setup-rust-action@v1 8 | with: 9 | components: rustfmt 10 | - uses: actions/checkout@master 11 | - name: Check format 12 | run: cargo fmt --all -- --check --verbose 13 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: [push, pull_request] 3 | jobs: 4 | all: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: hecrj/setup-rust-action@v1 8 | with: 9 | components: clippy 10 | - uses: actions/checkout@master 11 | - name: Install cargo-deb 12 | run: cargo install cargo-deb 13 | - uses: actions/checkout@master 14 | - name: Install dependencies 15 | run: | 16 | export DEBIAN_FRONTED=noninteractive 17 | sudo apt-get -qq update 18 | sudo apt-get install -y libxkbcommon-dev 19 | - name: Check lints iced 20 | run: cargo clippy --workspace --no-default-features --features=once_cell --all-targets --no-deps -- -D warnings 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push, pull_request] 3 | jobs: 4 | native: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | os: [ubuntu-latest, windows-latest, macOS-latest] 9 | rust: [stable, beta] 10 | steps: 11 | - uses: hecrj/setup-rust-action@v1 12 | with: 13 | rust-version: ${{ matrix.rust }} 14 | - uses: actions/checkout@master 15 | - name: Install dependencies 16 | if: matrix.os == 'ubuntu-latest' 17 | run: | 18 | export DEBIAN_FRONTED=noninteractive 19 | sudo apt-get -qq update 20 | sudo apt-get install -y libxkbcommon-dev 21 | - name: Run tests 22 | run: | 23 | cargo test --verbose --workspace 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.check.overrideCommand": ["just", "check-json"], 3 | "git-blame.gitWebUrl": "" 4 | } 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cosmic-time" 3 | version = "0.4.0" 4 | edition = "2021" 5 | description = "An animation Crate for libcosmic and Cosmic DE" 6 | authors = ["Brock Szuszczewicz "] 7 | license = "MIT" 8 | repository = "https://github.com/pop-os/cosmic-time" 9 | documentation = "https://docs.rs/cosmic-time" 10 | keywords = ["gui", "animation", "interface", "widgets", "libcosmic"] 11 | categories = ["gui"] 12 | 13 | [features] 14 | once_cell = ["dep:once_cell"] 15 | 16 | [workspace] 17 | members = [] 18 | 19 | [dependencies] 20 | libcosmic = { git = "https://github.com/pop-os/libcosmic/", default-features = false, features = [ 21 | "tokio", 22 | ] } 23 | once_cell = { version = "1.18.0", optional = true } 24 | float-cmp = "0.9" 25 | 26 | # [patch.'https://github.com/pop-os/libcosmic'] 27 | # libcosmic = { path = "../libcosmic" } 28 | # cosmic-config = { path = "../libcosmic/cosmic-config" } 29 | # cosmic-theme = { path = "../libcosmic/cosmic-theme" } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Pop!_OS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # COSMIC TIME 2 | ## An animation toolkit for [Iced](https://github.com/iced-rs/iced) 3 | 4 | > This Project was built for [Cosmic DE](https://github.com/pop-os/cosmic-epoch). Though this will work for any project that depends on [Iced](https://github.com/iced-rs/iced). 5 | 6 | 7 | The goal of this project is to provide a simple API to build and show 8 | complex animations efficiently in applications built with Iced-rs/Iced. 9 | 10 | ## Project Goals: 11 | * Full compatibility with Iced and The Elm Architecture. 12 | * Ease of use. 13 | * No math required for any animation. 14 | * No heap allocations in render loop. 15 | * Provide additional animatable widgets. 16 | * Custom widget support (create your own!). 17 | 18 | ## Overview 19 | To wire cosmic-time into Iced there are five steps to do. 20 | 21 | 1. Create a [`Timeline`] This is the type that controls the animations. 22 | ```rust 23 | struct Counter { 24 | timeline: Timeline 25 | } 26 | 27 | // ~ SNIP 28 | 29 | impl Application for Counter { 30 | // ~ SNIP 31 | fn new(_flags: ()) -> (Self, Command) { 32 | (Self { timeline: Timeline::new()}, Command::none()) 33 | } 34 | } 35 | ``` 36 | 2. Add at least one animation to your timeline. This can be done in your 37 | Application's `new()` or `update()`, or both! 38 | ```rust 39 | static CONTAINER: Lazy = Lazy::new(id::Container::unique); 40 | 41 | let animation = chain![ 42 | CONTAINER, 43 | container(Duration::ZERO).width(10), 44 | container(Duration::from_secs(10)).width(100) 45 | ]; 46 | self.timeline.set_chain(animation).start(); 47 | 48 | ``` 49 | There are some different things here! 50 | > static CONTAINER: Lazy = Lazy::new(id::Container::unique); 51 | 52 | Cosmic Time refers to each animation with an Id. We export our own, but they are 53 | Identical to the widget Id's Iced uses for widget operations. 54 | Each animatable widget needs an Id. And each Id can only refer to one animation. 55 | 56 | > let animation = chain![ 57 | 58 | Cosmic Time refers to animations as [`Chain`]s because of how we build then. 59 | Each Keyframe is linked together like a chain. The Cosmic Time API doesn't 60 | say "change your width from 10 to 100". We define each state we want the 61 | widget to have `.width(10)` at `Duration::ZERO` then `.width(100)` at 62 | `Duration::from_secs(10)`. Where the `Duration` is the time after the previous 63 | keyframe. This is why we call the animations chains. We cannot get to the 64 | next state without animating though all previous Keyframes. 65 | 66 | > self.timeline.set_chain(animation).start(); 67 | 68 | Then we need to add the animation to the [`Timeline`]. We call this `.set_chain`, 69 | because there can only be one chain per Id. 70 | If we `set_chain` with a different animation with the same Id, the first one is 71 | replaced. This a actually a feature not a bug! 72 | As well you can set multiple animations at once: 73 | `self.timeline.set_chain(animation1).set_chain(animation2).start()` 74 | 75 | > .start() 76 | 77 | This one function call is important enough that we should look at it specifically. 78 | Cosmic Time is atomic, given the animation state held in the [`Timeline`] at any 79 | given time the global animations will be the exact same. The value used to 80 | calculate any animation's interpolation is global. And we use `.start()` to 81 | sync them together. 82 | Say you have two 5 seconds animations running at the same time. They should end 83 | at the same time right? That all depends on when the widget thinks it's animation 84 | should start. `.start()` tells all pending animations to start at the moment that 85 | `.start()` is called. This guarantees they stay in sync. 86 | IMPORTANT! Be sure to only call `.start()` once per call to `update()`. 87 | The below is incorrect! 88 | ```rust 89 | self.timeline.set_chain(animation1).start(); 90 | self.timeline.set_chain(animation2).start(); 91 | ``` 92 | That code will compile, but will result in the animations not being in sync. 93 | 94 | 3. Add the Cosmic time Subscription 95 | ```rust 96 | fn subscription(&self) -> Subscription { 97 | self.timeline.as_subscription::().map(Message::Tick) 98 | } 99 | ``` 100 | 101 | 4. Map the subscription to update the timeline's state: 102 | ```rust 103 | fn update(&mut self, message: Message) -> Command { 104 | match message { 105 | Message::Tick(now) => self.timeline.now(now), 106 | } 107 | } 108 | ``` 109 | If you skip this step your animations will not progress! 110 | 111 | 5. Show the widget in your `view()`! 112 | ```rust 113 | anim!(CONTIANER, &self.timeline, contents) 114 | ``` 115 | 116 | All done! 117 | There is a bit of wiring to get Cosmic Time working, but after that it's only 118 | a few lines to create rather complex animations! 119 | See the Pong example to see how a full game of pong can be implemented in 120 | only a few lines! 121 | 122 | Done: 123 | - [x] No heap allocations in animation render loop 124 | - [x] Compile time type guarantee that animation id will match correct animation to correct widget type. 125 | - [x] Animatable container widget 126 | - [x] Looping animations 127 | - [x] Animation easing 128 | - [x] test for easing 129 | - [x] add space widget 130 | - [x] add button widget 131 | - [x] add row widget 132 | - [x] add column widget 133 | - [x] add toggle button widget 134 | - [x] Use iced 0.8 135 | - [x] use iced 0.8's framerate subscription 136 | - [x] Add logic for different animation Ease values 137 | - [x] Documentation 138 | - [x] optimize for `as_subscription` logic 139 | - [x] Add pause for animations 140 | - [x] Lazy keyframes. (Keyframes that can use the position of a previous (active or not) animation to start another animation.) 141 | 142 | TODOs: 143 | - [ ] Add container and space animiations between arbitraty length units. 144 | Example: Length::Shrink to Length::Fixed(10.) and/or Length::Fill to Length::Shrink 145 | Only fixed Length::Fixed(_) are supported currently. 146 | - [ ] Add `Cosmic` cargo feature for compatibility with both iced and System76's temporary fork. 147 | - [ ] Low motion accesability detection to disable animations. 148 | - [ ] general animation logic tests 149 | - [ ] Work on web via wasm-unknown-unknown builds 150 | - [ ] physics based animations 151 | - [ ] Figure out what else needs to be on this list 152 | 153 | #### Map of iced version to required cosmic-time version. 154 | |Iced Version|Required Cosmic Time Version| 155 | |------------|----------------------------| 156 | |0.8| 1.2| 157 | |0.9| 2.0| 158 | -------------------------------------------------------------------------------- /examples/counter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "counter" 3 | version = "0.1.0" 4 | authors = ["Héctor Ramón Jiménez ", "Brock Szuszczewicz "] 5 | edition = "2021" 6 | publish = false 7 | 8 | [dependencies] 9 | cosmic-time = { path = "../..", default-features = false, features = ["iced", "once_cell"] } 10 | -------------------------------------------------------------------------------- /examples/counter/README.md: -------------------------------------------------------------------------------- 1 | ## Counter 2 | 3 | This is the example to get you started! 4 | This is a modified version of the iced `Counter` example, but with animations! 5 | Make sure to be familiar with iced's `Aplication` trait. Cosmic-time requires it. 6 | 7 | Code is filled with comments to explain how this works :) 8 | -------------------------------------------------------------------------------- /examples/counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Counter - Iced 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/counter/src/main.rs: -------------------------------------------------------------------------------- 1 | use iced::widget::{button, column, text}; 2 | use iced::{ 3 | executor, 4 | time::{Duration, Instant}, 5 | Alignment, Application, Command, Element, Event, Length, Settings, Subscription, Theme, 6 | }; 7 | 8 | use cosmic_time::{self, anim, chain, id, once_cell::sync::Lazy, reexports::iced, Timeline}; 9 | 10 | static CONTAINER: Lazy = Lazy::new(id::Container::unique); 11 | 12 | pub fn main() -> iced::Result { 13 | Counter::run(Settings::default()) 14 | } 15 | 16 | struct Counter { 17 | value: i32, 18 | timeline: Timeline, 19 | } 20 | 21 | #[derive(Debug, Clone, Copy)] 22 | enum Message { 23 | IncrementPressed, 24 | DecrementPressed, 25 | Tick(Instant), 26 | } 27 | 28 | impl Application for Counter { 29 | type Executor = executor::Default; 30 | type Message = Message; 31 | type Theme = Theme; 32 | type Flags = (); 33 | 34 | fn new(_flags: ()) -> (Self, Command) { 35 | use cosmic_time::container; 36 | // This is new! This is how we build a timeline! 37 | // These values can be created at anytime, but because this example is 38 | // simple and we want to animate from application init, we will build the 39 | // timeline Struct and the "timeline" itself here. 40 | // Though more complicated applications will likely do this in the `update` 41 | let mut timeline = Timeline::new(); 42 | let animation = chain![ 43 | CONTAINER, 44 | container(Duration::ZERO).width(0.).height(100.), 45 | container(Duration::from_secs(2)).width(200.).height(100.), 46 | container(Duration::from_secs(2)) 47 | .width(200.) 48 | .height(300.) 49 | .padding([0, 0, 0, 0]), 50 | container(Duration::from_secs(2)) 51 | .width(700.) 52 | .height(300.) 53 | .padding([0, 0, 0, 500]), 54 | container(Duration::from_secs(2)) 55 | .width(150.) 56 | .height(150.) 57 | .padding([0, 0, 0, 0]), 58 | ]; 59 | 60 | // Notice how we had to specify the start and end of the widget dimensions? 61 | // Iced's default values for widgets are usually not animatable, because 62 | // they are unknown until the layout is built after the update. 63 | // because the goal of cosmic-time is to output regular widgets in the view, 64 | // we do the same here. Thus we must specify the start and end values of the 65 | // animation. To animate from a width of 50 to 100, we need two keyframes. 66 | // This example has multiple animated values, but if you look each one specifies 67 | // the value at each keyframe. 68 | // Notice how we specify the height of 300 again in `four`? That is because 69 | // cosmic-time assumes that the timeline is continuous. Try deleting it, 70 | // the height will animate smoothly from 300 to 150 right through keyframe `four`! 71 | 72 | timeline.set_chain(animation).start(); 73 | // `Start` is very important! Your animation won't "start" without it. 74 | // Cosmic-time tries to be atomic, meaning that keyframes defined in the 75 | // same function call all start at the same time. Because there is process time 76 | // between creating each keyframe it would be possible that two keyframes defined 77 | // with the same delay may lag behind eachother! Most of the time this would be 78 | // a single digit number of microseconds, but it might not! 79 | // So just be aware, when adding keyframes with a `Duration`, that keyframe's 80 | // time length is "`Duration` from the next `start` function call." 81 | 82 | (Self { value: 0, timeline }, Command::none()) 83 | } 84 | 85 | fn title(&self) -> String { 86 | String::from("Counter - Cosmic-Time") 87 | } 88 | 89 | fn subscription(&self) -> Subscription { 90 | // This is the magic that lets us animaate. Cosmic-time looks 91 | // at what timeline you have built and decides for you how often your 92 | // application should redraw for you! When the animation is done idle 93 | // or finished, cosmic-time will keep your applicaiton idle! 94 | self.timeline.as_subscription::().map(Message::Tick) 95 | } 96 | 97 | fn update(&mut self, message: Message) -> Command { 98 | match message { 99 | Message::IncrementPressed => { 100 | self.value += 1; 101 | } 102 | Message::DecrementPressed => { 103 | self.value -= 1; 104 | } 105 | Message::Tick(now) => self.timeline.now(now), 106 | } 107 | Command::none() 108 | } 109 | 110 | fn view(&self) -> Element { 111 | // Now we build a contaienr widget from the timeline above. 112 | // Cosmic-time just generates standard iced widgets. Style them like you would 113 | // any other! If you have a widget with a constant width, and animated height, 114 | // just define the width with a `width` method like any other widget, then 115 | // animate the height in your view! Only control the animatable values with 116 | // cosmic-time, all others should be in your view! 117 | anim!( 118 | CONTAINER, 119 | &self.timeline, 120 | column![ 121 | button("Increment") 122 | .on_press(Message::IncrementPressed) 123 | .width(Length::Fill), 124 | text(self.value).size(50).height(Length::Fill), 125 | button("Decrement") 126 | .on_press(Message::DecrementPressed) 127 | .width(Length::Fill) 128 | ] 129 | .padding(20) 130 | .align_items(Alignment::Center), 131 | ) 132 | .into() 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /examples/pokedex/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pokedex" 3 | version = "0.1.0" 4 | authors = ["Héctor Ramón Jiménez "] 5 | edition = "2021" 6 | publish = false 7 | 8 | [dependencies] 9 | iced = { git = "https://github.com/iced-rs/iced", rev = "5540ac0", features = ["image", "debug", "tokio"] } 10 | cosmic-time = { path = "../..", default-features = false, features = ["iced", "once_cell"] } 11 | serde_json = "1.0" 12 | 13 | [dependencies.serde] 14 | version = "1.0" 15 | features = ["derive"] 16 | 17 | [dependencies.reqwest] 18 | version = "0.11" 19 | default-features = false 20 | features = ["json", "rustls-tls"] 21 | 22 | [dependencies.rand] 23 | version = "0.7" 24 | features = ["wasm-bindgen"] 25 | -------------------------------------------------------------------------------- /examples/pokedex/README.md: -------------------------------------------------------------------------------- 1 | # Pokédex 2 | An application that loads a random Pokédex entry using the [PokéAPI]. 3 | 4 | All the example code can be found in the __[`main`](src/main.rs)__ file. 5 | 6 |
7 | 8 | 9 | 10 |
11 | 12 | You can run it on native platforms with `cargo run`: 13 | ``` 14 | cargo run --package pokedex 15 | ``` 16 | 17 | [PokéAPI]: https://pokeapi.co/ 18 | -------------------------------------------------------------------------------- /examples/pokedex/src/main.rs: -------------------------------------------------------------------------------- 1 | use iced::futures; 2 | use iced::widget::{self, column, container, image, row, text}; 3 | use iced::{ 4 | time::{Duration, Instant}, 5 | Alignment, Application, Color, Command, Element, Event, Length, Settings, Subscription, Theme, 6 | }; 7 | 8 | use cosmic_time::{ 9 | self, anim, chain, id, once_cell::sync::Lazy, reexports::iced, Back, Bounce, Circular, Ease, 10 | Elastic, Exponential, Linear, Quadratic, Quartic, Quintic, Sinusoidal, Timeline, 11 | }; 12 | 13 | static SPACE: Lazy = Lazy::new(id::Space::unique); 14 | 15 | const EASE_IN: [Ease; 10] = [ 16 | Ease::Linear(Linear::InOut), 17 | Ease::Quadratic(Quadratic::In), 18 | Ease::Quartic(Quartic::In), 19 | Ease::Quintic(Quintic::In), 20 | Ease::Sinusoidal(Sinusoidal::In), 21 | Ease::Exponential(Exponential::In), 22 | Ease::Circular(Circular::In), 23 | Ease::Elastic(Elastic::InOut), 24 | Ease::Back(Back::Out), 25 | Ease::Bounce(Bounce::In), 26 | ]; 27 | 28 | const EASE_OUT: [Ease; 10] = [ 29 | Ease::Linear(Linear::InOut), 30 | Ease::Quadratic(Quadratic::Out), 31 | Ease::Quartic(Quartic::Out), 32 | Ease::Quintic(Quintic::Out), 33 | Ease::Sinusoidal(Sinusoidal::Out), 34 | Ease::Exponential(Exponential::Out), 35 | Ease::Circular(Circular::Out), 36 | Ease::Elastic(Elastic::InOut), 37 | Ease::Back(Back::In), 38 | Ease::Bounce(Bounce::Out), 39 | ]; 40 | 41 | pub fn main() -> iced::Result { 42 | Pokedex::run(Settings::default()) 43 | } 44 | 45 | #[derive(Debug)] 46 | #[allow(clippy::large_enum_variant)] 47 | enum Pokedex { 48 | Loading, 49 | Loaded { pokemon: Pokemon }, 50 | Errored, 51 | } 52 | 53 | #[derive(Debug, Clone)] 54 | enum Message { 55 | PokemonFound(Result), 56 | Search, 57 | Tick(Instant), 58 | } 59 | 60 | impl Application for Pokedex { 61 | type Message = Message; 62 | type Theme = Theme; 63 | type Executor = iced::executor::Default; 64 | type Flags = (); 65 | 66 | fn new(_flags: ()) -> (Pokedex, Command) { 67 | assert_eq!(EASE_IN.len(), EASE_OUT.len()); 68 | ( 69 | Pokedex::Loading, 70 | Command::perform(Pokemon::search(), Message::PokemonFound), 71 | ) 72 | } 73 | 74 | fn title(&self) -> String { 75 | let subtitle = match self { 76 | Pokedex::Loading => "Loading", 77 | Pokedex::Loaded { pokemon, .. } => &pokemon.name, 78 | Pokedex::Errored { .. } => "Whoops!", 79 | }; 80 | 81 | format!("{subtitle} - Pokédex") 82 | } 83 | 84 | fn subscription(&self) -> Subscription { 85 | match self { 86 | Pokedex::Loaded { pokemon } => pokemon 87 | .timeline 88 | .as_subscription::() 89 | .map(Message::Tick), 90 | _ => Subscription::none(), 91 | } 92 | } 93 | 94 | fn update(&mut self, message: Message) -> Command { 95 | match message { 96 | Message::Tick(now) => { 97 | if let Pokedex::Loaded { pokemon, .. } = self { 98 | pokemon.timeline.now(now); 99 | } 100 | Command::none() 101 | } 102 | Message::PokemonFound(Ok(pokemon)) => { 103 | *self = Pokedex::Loaded { pokemon }; 104 | 105 | Command::none() 106 | } 107 | Message::PokemonFound(Err(_error)) => { 108 | *self = Pokedex::Errored; 109 | 110 | Command::none() 111 | } 112 | Message::Search => match self { 113 | Pokedex::Loading => Command::none(), 114 | _ => { 115 | *self = Pokedex::Loading; 116 | 117 | Command::perform(Pokemon::search(), Message::PokemonFound) 118 | } 119 | }, 120 | } 121 | } 122 | 123 | fn view(&self) -> Element { 124 | let content = match self { 125 | Pokedex::Loading => { 126 | column![text("Searching for Pokémon...").size(40),].width(Length::Shrink) 127 | } 128 | Pokedex::Loaded { pokemon } => column![ 129 | pokemon.view(), 130 | button("Keep searching!").on_press(Message::Search) 131 | ] 132 | .max_width(500) 133 | .spacing(20) 134 | .align_items(Alignment::End), 135 | Pokedex::Errored => column![ 136 | text("Whoops! Something went wrong...").size(40), 137 | button("Try again").on_press(Message::Search) 138 | ] 139 | .spacing(20) 140 | .align_items(Alignment::End), 141 | }; 142 | 143 | container(content) 144 | .width(Length::Fill) 145 | .height(Length::Fill) 146 | .center_x() 147 | .center_y() 148 | .into() 149 | } 150 | } 151 | 152 | #[derive(Debug, Clone)] 153 | struct Pokemon { 154 | timeline: Timeline, 155 | number: u16, 156 | name: String, 157 | description: String, 158 | image: image::Handle, 159 | } 160 | 161 | impl Pokemon { 162 | const TOTAL: u16 = 807; 163 | 164 | fn view(&self) -> Element { 165 | row![ 166 | column![ 167 | anim!(SPACE, &self.timeline), 168 | image::viewer(self.image.clone()) 169 | ], 170 | column![ 171 | row![ 172 | text(&self.name).size(30).width(Length::Fill), 173 | text(format!("#{}", self.number)) 174 | .size(20) 175 | .style(Color::from([0.5, 0.5, 0.5])), 176 | ] 177 | .align_items(Alignment::Center) 178 | .spacing(20), 179 | self.description.as_ref(), 180 | ] 181 | .spacing(20), 182 | ] 183 | .height(400.) 184 | .spacing(20) 185 | .align_items(Alignment::Center) 186 | .into() 187 | } 188 | 189 | async fn search() -> Result { 190 | use cosmic_time::space; 191 | use rand::Rng; 192 | use serde::Deserialize; 193 | 194 | #[derive(Debug, Deserialize)] 195 | struct Entry { 196 | name: String, 197 | flavor_text_entries: Vec, 198 | } 199 | 200 | #[derive(Debug, Deserialize)] 201 | struct FlavorText { 202 | flavor_text: String, 203 | language: Language, 204 | } 205 | 206 | #[derive(Debug, Deserialize)] 207 | struct Language { 208 | name: String, 209 | } 210 | 211 | let id = { 212 | let mut rng = rand::rngs::OsRng; 213 | 214 | rng.gen_range(0, Pokemon::TOTAL) 215 | }; 216 | 217 | let fetch_entry = async { 218 | let url = format!("https://pokeapi.co/api/v2/pokemon-species/{id}"); 219 | 220 | reqwest::get(&url).await?.json().await 221 | }; 222 | 223 | let (entry, image): (Entry, _) = 224 | futures::future::try_join(fetch_entry, Self::fetch_image(id)).await?; 225 | 226 | let description = entry 227 | .flavor_text_entries 228 | .iter() 229 | .find(|text| text.language.name == "en") 230 | .ok_or(Error::LanguageError)?; 231 | 232 | // This example is nice and simple! 233 | // besides te subscription logic this is it. 234 | // The timeline is created with each pokemon, and the animation is 235 | // created the same time. All we have to do is store the timeline 236 | // somewhere so it can be mapped to a subscription. Remember that 237 | // you do not have to create a timeline for each animation. A timeline 238 | // can hold many animations! 239 | let mut timeline = Timeline::new(); 240 | let rand = rand::thread_rng().gen_range(0, EASE_IN.len()); 241 | 242 | // Print ease type to terminal if in debug build (`cargo run`) 243 | //#[cfg(debug_assertaions)] 244 | println!( 245 | "Ease in with {:?}, and out with {:?}", 246 | EASE_IN[rand], EASE_OUT[rand] 247 | ); 248 | 249 | let animation = chain![ 250 | SPACE, 251 | space(Duration::ZERO).height(50.), 252 | space(Duration::from_millis(1500)) 253 | .height(250.) 254 | .ease(EASE_IN[rand]), 255 | space(Duration::from_millis(3000)) 256 | .height(50.) 257 | .ease(EASE_OUT[rand]) 258 | ] 259 | .loop_forever(); 260 | 261 | timeline.set_chain(animation).start(); 262 | 263 | Ok(Pokemon { 264 | timeline, 265 | number: id, 266 | name: entry.name.to_uppercase(), 267 | description: description 268 | .flavor_text 269 | .chars() 270 | .map(|c| if c.is_control() { ' ' } else { c }) 271 | .collect(), 272 | image, 273 | }) 274 | } 275 | 276 | async fn fetch_image(id: u16) -> Result { 277 | let url = format!( 278 | "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{id}.png" 279 | ); 280 | 281 | #[cfg(not(target_arch = "wasm32"))] 282 | { 283 | let bytes = reqwest::get(&url).await?.bytes().await?; 284 | 285 | Ok(image::Handle::from_memory(bytes.as_ref().to_vec())) 286 | } 287 | 288 | #[cfg(target_arch = "wasm32")] 289 | Ok(image::Handle::from_path(url)) 290 | } 291 | } 292 | 293 | #[derive(Debug, Clone)] 294 | enum Error { 295 | APIError, 296 | LanguageError, 297 | } 298 | 299 | impl From for Error { 300 | fn from(error: reqwest::Error) -> Error { 301 | dbg!(error); 302 | 303 | Error::APIError 304 | } 305 | } 306 | 307 | fn button(text: &str) -> widget::Button<'_, Message> { 308 | widget::button(text).padding(10) 309 | } 310 | -------------------------------------------------------------------------------- /examples/pong/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pong" 3 | version = "0.1.0" 4 | authors = ["Brock Szuszczewicz "] 5 | edition = "2021" 6 | publish = false 7 | 8 | [dependencies] 9 | cosmic-time = { path = "../..", default-features = false, features = ["iced", "once_cell"] } 10 | rand = "0.8.5" 11 | -------------------------------------------------------------------------------- /examples/pong/src/layer.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This file isn't specific to cosmic time. And is not necessary 3 | * for you to review. All this does is allow for the ball to 4 | * layer on top of the play board. 5 | * 6 | */ 7 | 8 | use cosmic_time::reexports::iced_core::{self, Vector}; 9 | use iced_core::widget::{self, Tree}; 10 | use iced_core::{ 11 | event, layout, mouse, overlay, renderer, Clipboard, Color, Element, Event, Layout, Length, 12 | Point, Rectangle, Shell, Size, Widget, 13 | }; 14 | 15 | use crate::theme::Theme; 16 | 17 | /// A simple widget that layers one above another. 18 | pub struct Layer<'a, Message, Renderer> { 19 | base: Element<'a, Message, Theme, Renderer>, 20 | layer: Element<'a, Message, Theme, Renderer>, 21 | } 22 | 23 | impl<'a, Message, Renderer> Layer<'a, Message, Renderer> { 24 | /// Returns a new [`Layer`] 25 | pub fn new( 26 | base: impl Into>, 27 | layer: impl Into>, 28 | ) -> Self { 29 | Self { 30 | base: base.into(), 31 | layer: layer.into(), 32 | } 33 | } 34 | } 35 | 36 | impl<'a, Message, Renderer> Widget for Layer<'a, Message, Renderer> 37 | where 38 | Renderer: iced_core::Renderer, 39 | Message: Clone, 40 | { 41 | fn children(&self) -> Vec { 42 | vec![Tree::new(&self.base), Tree::new(&self.layer)] 43 | } 44 | 45 | fn diff(&self, tree: &mut Tree) { 46 | tree.diff_children(&[&self.base, &self.layer]); 47 | } 48 | 49 | fn size(&self) -> Size { 50 | self.base.as_widget().size() 51 | } 52 | 53 | fn layout( 54 | &self, 55 | tree: &mut Tree, 56 | renderer: &Renderer, 57 | limits: &layout::Limits, 58 | ) -> layout::Node { 59 | self.base 60 | .as_widget() 61 | .layout(&mut tree.children[0], renderer, limits) 62 | } 63 | 64 | fn on_event( 65 | &mut self, 66 | state: &mut Tree, 67 | event: Event, 68 | layout: Layout<'_>, 69 | cursor_position: mouse::Cursor, 70 | renderer: &Renderer, 71 | clipboard: &mut dyn Clipboard, 72 | shell: &mut Shell<'_, Message>, 73 | viewport: &Rectangle, 74 | ) -> event::Status { 75 | self.base.as_widget_mut().on_event( 76 | &mut state.children[0], 77 | event, 78 | layout, 79 | cursor_position, 80 | renderer, 81 | clipboard, 82 | shell, 83 | viewport, 84 | ) 85 | } 86 | 87 | fn draw( 88 | &self, 89 | state: &Tree, 90 | renderer: &mut Renderer, 91 | theme: &Theme, 92 | style: &renderer::Style, 93 | layout: Layout<'_>, 94 | cursor_position: mouse::Cursor, 95 | viewport: &Rectangle, 96 | ) { 97 | self.base.as_widget().draw( 98 | &state.children[0], 99 | renderer, 100 | theme, 101 | style, 102 | layout, 103 | cursor_position, 104 | viewport, 105 | ); 106 | } 107 | 108 | fn overlay<'b>( 109 | &'b mut self, 110 | state: &'b mut Tree, 111 | layout: Layout<'_>, 112 | _renderer: &Renderer, 113 | ) -> Option> { 114 | Some(overlay::Element::new( 115 | layout.position(), 116 | Box::new(Overlay { 117 | content: &mut self.layer, 118 | tree: &mut state.children[1], 119 | size: layout.bounds().size(), 120 | }), 121 | )) 122 | } 123 | 124 | fn mouse_interaction( 125 | &self, 126 | state: &Tree, 127 | layout: Layout<'_>, 128 | cursor_position: mouse::Cursor, 129 | viewport: &Rectangle, 130 | renderer: &Renderer, 131 | ) -> mouse::Interaction { 132 | self.base.as_widget().mouse_interaction( 133 | &state.children[0], 134 | layout, 135 | cursor_position, 136 | viewport, 137 | renderer, 138 | ) 139 | } 140 | 141 | fn operate( 142 | &self, 143 | state: &mut Tree, 144 | layout: Layout<'_>, 145 | renderer: &Renderer, 146 | operation: &mut dyn widget::Operation<()>, 147 | ) { 148 | self.base 149 | .as_widget() 150 | .operate(&mut state.children[0], layout, renderer, operation); 151 | } 152 | } 153 | 154 | struct Overlay<'a, 'b, Message, Renderer> { 155 | content: &'b mut Element<'a, Message, Theme, Renderer>, 156 | tree: &'b mut Tree, 157 | size: Size, 158 | } 159 | 160 | impl<'a, 'b, Message, Renderer> overlay::Overlay 161 | for Overlay<'a, 'b, Message, Renderer> 162 | where 163 | Renderer: iced_core::Renderer, 164 | Message: Clone, 165 | { 166 | fn layout( 167 | &mut self, 168 | renderer: &Renderer, 169 | _bounds: Size, 170 | position: Point, 171 | _translation: Vector, 172 | ) -> layout::Node { 173 | let limits = layout::Limits::new(Size::ZERO, self.size) 174 | .width(Length::Fill) 175 | .height(Length::Fill); 176 | 177 | let child = self 178 | .content 179 | .as_widget() 180 | .layout(&mut self.tree.children[0], renderer, &limits); 181 | layout::Node::with_children(self.size, vec![child]).move_to(position) 182 | } 183 | 184 | fn on_event( 185 | &mut self, 186 | event: Event, 187 | layout: Layout<'_>, 188 | cursor_position: mouse::Cursor, 189 | renderer: &Renderer, 190 | clipboard: &mut dyn Clipboard, 191 | shell: &mut Shell<'_, Message>, 192 | ) -> event::Status { 193 | self.content.as_widget_mut().on_event( 194 | self.tree, 195 | event, 196 | layout.children().next().unwrap(), 197 | cursor_position, 198 | renderer, 199 | clipboard, 200 | shell, 201 | &layout.bounds(), 202 | ) 203 | } 204 | 205 | fn draw( 206 | &self, 207 | renderer: &mut Renderer, 208 | theme: &Theme, 209 | style: &renderer::Style, 210 | layout: Layout<'_>, 211 | cursor_position: mouse::Cursor, 212 | ) { 213 | renderer.fill_quad( 214 | renderer::Quad { 215 | bounds: layout.bounds(), 216 | ..Default::default() 217 | }, 218 | Color { 219 | a: 0., 220 | ..Color::BLACK 221 | }, 222 | ); 223 | 224 | self.content.as_widget().draw( 225 | self.tree, 226 | renderer, 227 | theme, 228 | style, 229 | layout.children().next().unwrap(), 230 | cursor_position, 231 | &layout.bounds(), 232 | ); 233 | } 234 | 235 | fn operate( 236 | &mut self, 237 | layout: Layout<'_>, 238 | renderer: &Renderer, 239 | operation: &mut dyn widget::Operation<()>, 240 | ) { 241 | self.content.as_widget().operate( 242 | self.tree, 243 | layout.children().next().unwrap(), 244 | renderer, 245 | operation, 246 | ); 247 | } 248 | 249 | fn mouse_interaction( 250 | &self, 251 | layout: Layout<'_>, 252 | cursor_position: mouse::Cursor, 253 | viewport: &Rectangle, 254 | renderer: &Renderer, 255 | ) -> mouse::Interaction { 256 | self.content.as_widget().mouse_interaction( 257 | self.tree, 258 | layout.children().next().unwrap(), 259 | cursor_position, 260 | viewport, 261 | renderer, 262 | ) 263 | } 264 | } 265 | 266 | impl<'a, Message, Renderer> From> 267 | for Element<'a, Message, Theme, Renderer> 268 | where 269 | Renderer: 'a + iced_core::Renderer, 270 | Message: 'a + Clone, 271 | { 272 | fn from(layer: Layer<'a, Message, Renderer>) -> Self { 273 | Element::new(layer) 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /examples/pong/src/main.rs: -------------------------------------------------------------------------------- 1 | use cosmic_time::reexports::iced_core::keyboard::key::Named; 2 | use cosmic_time::reexports::iced_futures::event::listen_raw; 3 | use iced::event; 4 | use iced::keyboard; 5 | 6 | use iced::widget::{column, container, row, Space}; 7 | use iced::{executor, Application, Command, Event, Length, Settings, Subscription}; 8 | use iced_core::window; 9 | 10 | use cosmic_time::{ 11 | self, anim, chain, id, 12 | once_cell::sync::Lazy, 13 | reexports::{iced, iced_core}, 14 | Duration, Instant, Speed, Timeline, 15 | }; 16 | 17 | use rand::prelude::*; 18 | 19 | mod layer; 20 | mod theme; 21 | use layer::Layer; 22 | use theme::{widget::Element, Theme}; 23 | 24 | static PADDLE_LEFT: Lazy = Lazy::new(id::Space::unique); 25 | static PADDLE_RIGHT: Lazy = Lazy::new(id::Space::unique); 26 | static BALL_X: Lazy = Lazy::new(id::Space::unique); 27 | static BALL_Y: Lazy = Lazy::new(id::Space::unique); 28 | 29 | pub fn main() -> iced::Result { 30 | Pong::run(Settings::default()) 31 | } 32 | 33 | struct Pong { 34 | timeline: Timeline, 35 | window: Window, 36 | in_play: bool, 37 | rng: ThreadRng, 38 | left: Direction, 39 | right: Direction, 40 | } 41 | 42 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 43 | enum Direction { 44 | Up, 45 | Down, 46 | } 47 | 48 | #[derive(Debug, Clone, Copy, Default)] 49 | struct Window { 50 | width: f32, 51 | height: f32, 52 | paddle_height: f32, 53 | paddle_width: f32, 54 | } 55 | 56 | #[derive(Debug, Clone, Copy)] 57 | enum Message { 58 | Tick(Instant), 59 | Paddle(Paddle), 60 | WindowResized(u32, u32), 61 | } 62 | 63 | #[derive(Debug, Clone, Copy)] 64 | enum Paddle { 65 | LeftUp, 66 | LeftDown, 67 | RightUp, 68 | RightDown, 69 | } 70 | 71 | impl Application for Pong { 72 | type Executor = executor::Default; 73 | type Message = Message; 74 | type Theme = Theme; 75 | type Flags = (); 76 | 77 | fn new(_flags: ()) -> (Self, Command) { 78 | ( 79 | Pong { 80 | timeline: Timeline::new(), 81 | window: Window::default(), 82 | rng: thread_rng(), 83 | in_play: false, 84 | left: Direction::Up, 85 | right: Direction::Up, 86 | }, 87 | Command::none(), 88 | ) 89 | } 90 | 91 | fn title(&self) -> String { 92 | String::from("Pong - Cosmic-Time") 93 | } 94 | 95 | fn subscription(&self) -> Subscription { 96 | Subscription::batch(vec![ 97 | self.timeline.as_subscription::().map(Message::Tick), 98 | listen_raw(|event, status| match (event, status) { 99 | ( 100 | Event::Keyboard(keyboard::Event::KeyPressed { key, .. }), 101 | event::Status::Ignored, 102 | ) => match key { 103 | keyboard::Key::Character(c) if c == "w" => { 104 | Some(Message::Paddle(Paddle::LeftUp)) 105 | } 106 | keyboard::Key::Character(c) if c == "s" => { 107 | Some(Message::Paddle(Paddle::LeftDown)) 108 | } 109 | keyboard::Key::Named(Named::ArrowUp) => Some(Message::Paddle(Paddle::RightUp)), 110 | keyboard::Key::Named(Named::ArrowDown) => { 111 | Some(Message::Paddle(Paddle::RightDown)) 112 | } 113 | _ => None, 114 | }, 115 | ( 116 | Event::Window(_, window::Event::Resized { width, height }), 117 | event::Status::Ignored, 118 | ) => Some(Message::WindowResized(width, height)), 119 | _ => None, 120 | }), 121 | ]) 122 | } 123 | 124 | fn update(&mut self, message: Message) -> Command { 125 | match message { 126 | Message::Tick(now) => self.timeline.now(now), 127 | Message::Paddle(p) => { 128 | let animation = match p { 129 | Paddle::LeftUp => self.anim_left(Direction::Up), 130 | Paddle::RightUp => self.anim_right(Direction::Up), 131 | Paddle::LeftDown => self.anim_left(Direction::Down), 132 | Paddle::RightDown => self.anim_right(Direction::Down), 133 | }; 134 | 135 | // Start game on first player movement 136 | if !self.in_play { 137 | self.in_play = true; 138 | let vertical_bounce = self.rand_vertical_bounce(); 139 | let horizontal_bounce = self.rand_horizontal_bounce(); 140 | let _ = self 141 | .timeline 142 | .set_chain(vertical_bounce) 143 | .set_chain(horizontal_bounce); 144 | } 145 | if let Some(a) = animation { 146 | self.timeline.set_chain(a) 147 | } else { 148 | &mut self.timeline 149 | } 150 | .start(); 151 | } 152 | Message::WindowResized(width, height) => { 153 | let width = width as f32; 154 | let height = height as f32; 155 | self.window = Window { 156 | width, 157 | height, 158 | paddle_width: width * 0.03, 159 | paddle_height: height * 0.2, 160 | }; 161 | 162 | self.in_play = false; 163 | let x = self.init_ball_x(); 164 | let y = self.init_ball_y(); 165 | self.timeline.set_chain(x).set_chain(y).start(); 166 | } 167 | } 168 | Command::none() 169 | } 170 | 171 | fn view(&self) -> Element { 172 | let width = self.window.paddle_width; 173 | let height = self.window.paddle_height; 174 | 175 | let paddle_left = container(Space::new(width, height)).style(theme::Container::Paddle); 176 | let paddle_right = container(Space::new(width, height)).style(theme::Container::Paddle); 177 | 178 | let content = row![ 179 | column![anim!(PADDLE_LEFT, &self.timeline), paddle_left], 180 | Space::new(Length::Fill, Length::Fill), 181 | column![anim!(PADDLE_RIGHT, &self.timeline), paddle_right], 182 | ]; 183 | 184 | let ball = column![ 185 | anim!(BALL_Y, &self.timeline), 186 | row![ 187 | anim!(BALL_X, &self.timeline), 188 | container(Space::new(width, width)).style(theme::Container::Ball) 189 | ] 190 | ]; 191 | 192 | Layer::new(content, ball).into() 193 | } 194 | } 195 | 196 | impl Pong { 197 | fn anim_left(&mut self, direction: Direction) -> Option { 198 | use cosmic_time::lazy::space as lazy; 199 | use cosmic_time::space; 200 | let height = self.window.height - self.window.paddle_height; 201 | 202 | if self.left != direction { 203 | self.left = direction; 204 | Some( 205 | match direction { 206 | Direction::Down => chain![ 207 | PADDLE_LEFT, 208 | // OOh here are the lazy keyframes! 209 | // This means that this animation will start at wherever the previous 210 | // animation left off! 211 | // Lazy still takes a duration, this will usually be `Duration::ZERO` 212 | // like regular animations, but you can put them anywhere in your 213 | // animation chain. Meaning that you would have an animation start 214 | // at the previous animations's interupted location, animate to elsewhere, 215 | // then go back to that spot! 216 | // 217 | // Also notice the speed here is per_millis! This is important. 218 | // The animation is only as granular as your definition in the chain. 219 | // If you animation time is not in exact seconds, I highly recommend 220 | // using a smaller unit. 221 | lazy(Duration::ZERO), 222 | space(Speed::per_millis(0.3)).height(height), 223 | ], 224 | Direction::Up => chain![ 225 | PADDLE_LEFT, 226 | lazy(Duration::ZERO), 227 | space(Speed::per_millis(0.3)).height(0.) 228 | ], 229 | } 230 | .into(), 231 | ) 232 | } else { 233 | None 234 | } 235 | } 236 | 237 | fn anim_right(&mut self, direction: Direction) -> Option { 238 | use cosmic_time::lazy::space as lazy; 239 | use cosmic_time::space; 240 | let height = self.window.height - self.window.paddle_height; 241 | 242 | if self.right != direction { 243 | self.right = direction; 244 | Some( 245 | match direction { 246 | Direction::Down => chain![ 247 | PADDLE_RIGHT, 248 | lazy(Duration::ZERO), 249 | space(Speed::per_millis(0.3)).height(height), 250 | ], 251 | Direction::Up => chain![ 252 | PADDLE_RIGHT, 253 | lazy(Duration::ZERO), 254 | space(Speed::per_millis(0.3)).height(0.) 255 | ], 256 | } 257 | .into(), 258 | ) 259 | } else { 260 | None 261 | } 262 | } 263 | 264 | fn init_ball_y(&mut self) -> cosmic_time::Chain { 265 | use cosmic_time::space; 266 | let min = self.window.height * 0.3; 267 | let max = self.window.height - min - self.window.paddle_height; 268 | let height = self.rng.gen_range(min..max); 269 | 270 | chain![BALL_Y, space(Duration::ZERO).height(height)].into() 271 | } 272 | 273 | fn init_ball_x(&mut self) -> cosmic_time::Chain { 274 | use cosmic_time::space; 275 | let min = self.window.width * 0.3; 276 | let max = self.window.width - min - self.window.paddle_width; 277 | let width = self.rng.gen_range(min..max); 278 | 279 | chain![BALL_X, space(Duration::ZERO).width(width)].into() 280 | } 281 | 282 | fn rand_vertical_bounce(&mut self) -> cosmic_time::Chain { 283 | use cosmic_time::lazy::space as lazy; 284 | use cosmic_time::space; 285 | let speed = 100. * self.rng.gen_range(0.9..1.1); 286 | let height = self.window.height - self.window.paddle_width; 287 | 288 | if self.rng.gen() { 289 | chain![ 290 | BALL_Y, 291 | lazy(Duration::ZERO), 292 | space(Speed::per_secs(speed)).height(height), 293 | space(Speed::per_secs(speed)).height(0.), 294 | lazy(Speed::per_secs(speed)) 295 | ] 296 | } else { 297 | chain![ 298 | BALL_Y, 299 | lazy(Duration::ZERO), 300 | space(Speed::per_secs(speed)).height(0.), 301 | space(Speed::per_secs(speed)).height(height), 302 | lazy(Speed::per_secs(speed)) 303 | ] 304 | } 305 | .loop_forever() 306 | .into() 307 | } 308 | 309 | fn rand_horizontal_bounce(&mut self) -> cosmic_time::Chain { 310 | use cosmic_time::lazy::space as lazy; 311 | use cosmic_time::space; 312 | let speed = 100. * self.rng.gen_range(0.9..1.1); 313 | let width = self.window.width - self.window.paddle_width; 314 | 315 | if self.rng.gen() { 316 | chain![ 317 | BALL_X, 318 | lazy(Duration::ZERO), 319 | space(Speed::per_secs(speed)).width(width), 320 | space(Speed::per_secs(speed)).width(0.), 321 | lazy(Speed::per_secs(speed)) 322 | ] 323 | } else { 324 | chain![ 325 | BALL_X, 326 | lazy(Duration::ZERO), 327 | space(Speed::per_secs(speed)).width(0.), 328 | space(Speed::per_secs(speed)).width(width), 329 | lazy(Speed::per_secs(speed)) 330 | ] 331 | } 332 | .loop_forever() 333 | .into() 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /examples/pong/src/theme.rs: -------------------------------------------------------------------------------- 1 | use cosmic_time::reexports::iced; 2 | use cosmic_time::reexports::iced_core::Border; 3 | use iced::widget::{container, text}; 4 | use iced::{application, color}; 5 | 6 | #[derive(Debug, Clone, Copy, Default)] 7 | pub struct Theme; 8 | 9 | impl application::StyleSheet for Theme { 10 | type Style = (); 11 | 12 | fn appearance(&self, _style: &Self::Style) -> application::Appearance { 13 | application::Appearance { 14 | background_color: color!(0x00, 0x00, 0x00), 15 | text_color: color!(0xff, 0xff, 0xff), 16 | } 17 | } 18 | } 19 | 20 | impl text::StyleSheet for Theme { 21 | type Style = (); 22 | 23 | fn appearance(&self, _style: Self::Style) -> text::Appearance { 24 | text::Appearance { 25 | color: color!(0xeb, 0xdb, 0xb2).into(), 26 | } 27 | } 28 | } 29 | 30 | #[derive(Debug, Clone, Copy, Default)] 31 | pub enum Container { 32 | #[default] 33 | Default, 34 | Paddle, 35 | Ball, 36 | } 37 | 38 | impl container::StyleSheet for Theme { 39 | type Style = Container; 40 | 41 | fn appearance(&self, style: &Self::Style) -> container::Appearance { 42 | match style { 43 | Container::Default => container::Appearance { 44 | background: Some(color!(0, 0, 0).into()), 45 | ..Default::default() 46 | }, 47 | Container::Paddle => container::Appearance { 48 | background: Some(color!(0xff, 0xff, 0xff).into()), 49 | ..Default::default() 50 | }, 51 | Container::Ball => container::Appearance { 52 | background: Some(color!(0xff, 0xff, 0xff).into()), 53 | border: Border { 54 | radius: 100000.0.into(), 55 | ..Default::default() 56 | }, 57 | ..Default::default() 58 | }, 59 | } 60 | } 61 | } 62 | 63 | pub mod widget { 64 | #![allow(dead_code)] 65 | use cosmic_time::reexports::iced; 66 | 67 | use crate::theme::Theme; 68 | 69 | pub type Renderer = iced::Renderer; 70 | pub type Element<'a, Message> = iced::Element<'a, Message, Theme, Renderer>; 71 | pub type Container<'a, Message> = iced::widget::Container<'a, Message, Renderer>; 72 | } 73 | -------------------------------------------------------------------------------- /examples/stopwatch/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stopwatch" 3 | version = "0.1.0" 4 | authors = ["Héctor Ramón Jiménez ", "Brock Szuszczewicz "] 5 | edition = "2021" 6 | publish = false 7 | 8 | [dependencies] 9 | iced = { git = "https://github.com/iced-rs/iced", rev = "5540ac0", features = ["smol"] } 10 | cosmic-time = { path = "../..", default-features = false, features = ["iced", "once_cell"] } 11 | 12 | -------------------------------------------------------------------------------- /examples/stopwatch/README.md: -------------------------------------------------------------------------------- 1 | ## Stopwatch 2 | 3 | A watch with start/stop and reset buttons showcasing how to listen to time. 4 | 5 | The __[`main`]__ file contains all the code of the example. 6 | 7 |
8 | 9 | 10 | 11 |
12 | 13 | You can run it with `cargo run`: 14 | ``` 15 | cargo run --package stopwatch 16 | ``` 17 | 18 | [`main`]: src/main.rs 19 | -------------------------------------------------------------------------------- /examples/stopwatch/src/main.rs: -------------------------------------------------------------------------------- 1 | use iced::alignment; 2 | use iced::executor; 3 | use iced::widget::{button, column, row, text}; 4 | use iced::{Alignment, Application, Command, Event, Length, Settings, Subscription}; 5 | 6 | mod theme; 7 | use self::widget::Element; 8 | use theme::Theme; 9 | 10 | use cosmic_time::{self, anim, chain, id, once_cell::sync::Lazy, Sinusoidal, Timeline}; 11 | 12 | static BUTTON: Lazy = Lazy::new(id::StyleButton::unique); 13 | static CONTAINER: Lazy = Lazy::new(id::StyleContainer::unique); 14 | 15 | use std::time::{Duration, Instant}; 16 | 17 | pub fn main() -> iced::Result { 18 | Stopwatch::run(Settings::default()) 19 | } 20 | 21 | struct Stopwatch { 22 | timeline: Timeline, 23 | duration: Duration, 24 | state: State, 25 | } 26 | 27 | enum State { 28 | Idle, 29 | Ticking { last_tick: Instant }, 30 | } 31 | 32 | #[derive(Debug, Clone)] 33 | enum Message { 34 | Toggle, 35 | Reset, 36 | Tick(Instant), 37 | } 38 | 39 | impl Application for Stopwatch { 40 | type Message = Message; 41 | type Theme = Theme; 42 | type Executor = executor::Default; 43 | type Flags = (); 44 | 45 | fn new(_flags: ()) -> (Stopwatch, Command) { 46 | let mut timeline = Timeline::new(); 47 | timeline.set_chain_paused(anim_background()).start(); 48 | ( 49 | Stopwatch { 50 | timeline, 51 | duration: Duration::default(), 52 | state: State::Idle, 53 | }, 54 | Command::none(), 55 | ) 56 | } 57 | 58 | fn title(&self) -> String { 59 | String::from("Stopwatch - Cosmic-Time") 60 | } 61 | 62 | fn update(&mut self, message: Message) -> Command { 63 | match message { 64 | Message::Toggle => match self.state { 65 | State::Idle => { 66 | self.state = State::Ticking { 67 | last_tick: Instant::now(), 68 | }; 69 | self.timeline 70 | .set_chain(anim_to_destructive()) 71 | .resume(CONTAINER.clone()) 72 | .start(); 73 | } 74 | State::Ticking { .. } => { 75 | self.state = State::Idle; 76 | self.timeline 77 | .set_chain(anim_to_primary()) 78 | .pause(CONTAINER.clone()) 79 | .start(); 80 | } 81 | }, 82 | Message::Tick(now) => { 83 | self.timeline.now(now); 84 | if let State::Ticking { last_tick } = &mut self.state { 85 | self.duration += now - *last_tick; 86 | *last_tick = now; 87 | } 88 | } 89 | Message::Reset => { 90 | self.duration = Duration::default(); 91 | match self.state { 92 | State::Idle => self.timeline.set_chain_paused(anim_background()).start(), 93 | State::Ticking { .. } => self.timeline.set_chain(anim_background()).start(), 94 | } 95 | } 96 | } 97 | 98 | Command::none() 99 | } 100 | 101 | fn subscription(&self) -> Subscription { 102 | self.timeline.as_subscription::().map(Message::Tick) 103 | } 104 | 105 | fn view(&self) -> Element { 106 | const MINUTE: u64 = 60; 107 | const HOUR: u64 = 60 * MINUTE; 108 | 109 | let seconds = self.duration.as_secs(); 110 | 111 | let duration = text(format!( 112 | "{:0>2}:{:0>2}:{:0>2}.{:0>2}", 113 | seconds / HOUR, 114 | (seconds % HOUR) / MINUTE, 115 | seconds % MINUTE, 116 | self.duration.subsec_millis() / 10, 117 | )) 118 | .size(40); 119 | 120 | let button = |label| { 121 | button(text(label).horizontal_alignment(alignment::Horizontal::Center)) 122 | .padding(10) 123 | .width(Length::Fixed(80.)) 124 | }; 125 | 126 | // must match the same order that the function used to parse into `u8`s 127 | let buttons = |i: u8| match i { 128 | 0 => theme::Button::Primary, 129 | 1 => theme::Button::Secondary, 130 | 2 => theme::Button::Positive, 131 | 3 => theme::Button::Destructive, 132 | 4 => theme::Button::Text, 133 | _ => panic!("custom is not supported"), 134 | }; 135 | 136 | let toggle_button = { 137 | let label = match self.state { 138 | State::Idle => "Start", 139 | State::Ticking { .. } => "Stop", 140 | }; 141 | 142 | anim!( 143 | BUTTON, 144 | buttons, 145 | &self.timeline, 146 | text(label).horizontal_alignment(alignment::Horizontal::Center), 147 | ) 148 | .padding(10) 149 | .width(Length::Fixed(80.)) 150 | .on_press(Message::Toggle) 151 | }; 152 | 153 | let reset_button = button("Reset") 154 | .style(theme::Button::Secondary) 155 | .on_press(Message::Reset); 156 | 157 | let controls = row![toggle_button, reset_button].spacing(20); 158 | 159 | let content = column![duration, controls] 160 | .align_items(Alignment::Center) 161 | .spacing(20); 162 | 163 | anim!( 164 | CONTAINER, 165 | // Cool! Because we implemented the function on our custom, theme's type, adding 166 | // the map argument is easy! 167 | theme::Container::map(), 168 | &self.timeline, 169 | content, 170 | ) 171 | .width(Length::Fill) 172 | .height(Length::Fill) 173 | .center_x() 174 | .center_y() 175 | .into() 176 | } 177 | } 178 | 179 | fn anim_to_primary() -> cosmic_time::Chain { 180 | use cosmic_time::style_button; 181 | chain![ 182 | BUTTON, 183 | style_button(Duration::ZERO).style(button_u8(theme::Button::Destructive)), 184 | style_button(Duration::from_millis(500)).style(button_u8(theme::Button::Primary)) 185 | ] 186 | .into() 187 | } 188 | 189 | fn anim_to_destructive() -> cosmic_time::Chain { 190 | use cosmic_time::style_button; 191 | chain![ 192 | BUTTON, 193 | style_button(Duration::ZERO).style(button_u8(theme::Button::Primary)), 194 | style_button(Duration::from_millis(500)).style(button_u8(theme::Button::Destructive)) 195 | ] 196 | .into() 197 | } 198 | 199 | fn anim_background() -> cosmic_time::Chain { 200 | use cosmic_time::style_container; 201 | chain![ 202 | CONTAINER, 203 | style_container(Duration::ZERO).style(theme::Container::Red), 204 | style_container(Duration::from_secs(1)) 205 | // Notice how we can just pass the enum value here, where in the `anim_to_primary/destructive` 206 | // we have to use the fucntion `button_u8`? Because we use a implemented a custom iced theme, 207 | // we can just impl Into on the enum, and it works here! 208 | .style(theme::Container::Green) 209 | .ease(Sinusoidal::In), 210 | style_container(Duration::from_secs(2)) 211 | .style(theme::Container::Blue) 212 | .ease(Sinusoidal::In), 213 | style_container(Duration::from_secs(3)) 214 | .style(theme::Container::Red) 215 | .ease(Sinusoidal::In) 216 | ] 217 | .loop_forever() 218 | .into() 219 | } 220 | 221 | // Style implementations 222 | 223 | // Here the button example uses Iced's default theme 224 | // enum. So we have to have some helper functions to make it work. 225 | // we also have another closture, `buttons`, in `fn view()` 226 | // 227 | // For themining reasons, this actually isn't iced's default 228 | // button theme, but the implementation here for button is what you 229 | // would have to do to use the iced type in your project. 230 | 231 | // the enum's default must be 0 232 | fn button_u8(style: theme::Button) -> u8 { 233 | match style { 234 | theme::Button::Primary => 0, 235 | theme::Button::Secondary => 1, 236 | theme::Button::Positive => 2, 237 | theme::Button::Destructive => 3, 238 | theme::Button::Text => 4, 239 | _ => panic!("Custom is not supported"), 240 | } 241 | } 242 | 243 | // But! if we are useing a custom theme then 244 | // the code cleans up quite a bit. 245 | 246 | impl From for u8 { 247 | fn from(style: theme::Container) -> Self { 248 | match style { 249 | theme::Container::White => 0, 250 | theme::Container::Red => 1, 251 | theme::Container::Green => 2, 252 | theme::Container::Blue => 3, 253 | } 254 | } 255 | } 256 | 257 | impl theme::Container { 258 | fn map() -> fn(u8) -> theme::Container { 259 | |i: u8| match i { 260 | 0 => theme::Container::White, 261 | 1 => theme::Container::Red, 262 | 2 => theme::Container::Green, 263 | 3 => theme::Container::Blue, 264 | _ => panic!("Impossible"), 265 | } 266 | } 267 | } 268 | 269 | // Just for themeing, not a part of this example. 270 | mod widget { 271 | #![allow(dead_code)] 272 | use crate::theme::Theme; 273 | 274 | pub type Renderer = iced::Renderer; 275 | pub type Element<'a, Message> = iced::Element<'a, Message, Theme, Renderer>; 276 | pub type Container<'a, Message> = iced::widget::Container<'a, Message, Renderer>; 277 | pub type Button<'a, Message> = iced::widget::Button<'a, Message, Renderer>; 278 | } 279 | -------------------------------------------------------------------------------- /examples/stopwatch/src/theme.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is not specific to cosmic-time. 3 | * The relevant code to this example is in main.rs. 4 | * This is just code to make an iced theme, so 5 | * the stopwatch example can be prettier, and 6 | * show the andvantages of style animations 7 | * with a custom theme. 8 | * 9 | */ 10 | 11 | use iced::widget::{button, container, text}; 12 | use iced::{application, color, Shadow, Vector}; 13 | use iced::{Background as B, Border}; 14 | 15 | #[derive(Default)] 16 | pub struct Theme; 17 | 18 | impl application::StyleSheet for Theme { 19 | type Style = (); 20 | 21 | fn appearance(&self, _style: &Self::Style) -> application::Appearance { 22 | application::Appearance { 23 | background_color: color!(0xff, 0xff, 0xff), 24 | text_color: color!(0xff, 0x00, 0x00), 25 | } 26 | } 27 | } 28 | 29 | impl text::StyleSheet for Theme { 30 | type Style = (); 31 | 32 | fn appearance(&self, _style: Self::Style) -> text::Appearance { 33 | text::Appearance { 34 | color: color!(0xff, 0xff, 0xff).into(), 35 | } 36 | } 37 | } 38 | 39 | #[derive(Debug, Clone, Copy, Default)] 40 | pub enum Container { 41 | #[default] 42 | White, 43 | Red, 44 | Green, 45 | Blue, 46 | } 47 | 48 | impl container::StyleSheet for Theme { 49 | type Style = Container; 50 | 51 | fn appearance(&self, style: &Self::Style) -> container::Appearance { 52 | match style { 53 | Container::White => container::Appearance { 54 | background: Some(B::Color(color!(0xd1, 0xd5, 0xdb))), 55 | text_color: Some(color!(0x00, 0x00, 0x00)), 56 | ..Default::default() 57 | }, 58 | Container::Red => container::Appearance { 59 | background: Some(B::Color(color!(0xfc, 0xa5, 0xa5))), 60 | text_color: Some(color!(0x00, 0x00, 0x00)), 61 | ..Default::default() 62 | }, 63 | Container::Green => container::Appearance { 64 | background: Some(B::Color(color!(0xb3, 0xf2, 0x64))), 65 | text_color: Some(color!(0x00, 0x00, 0x00)), 66 | ..Default::default() 67 | }, 68 | Container::Blue => container::Appearance { 69 | background: Some(B::Color(color!(0x93, 0xc5, 0xfd))), 70 | text_color: Some(color!(0x00, 0x00, 0x00)), 71 | ..Default::default() 72 | }, 73 | } 74 | } 75 | } 76 | 77 | #[derive(Default)] 78 | #[allow(dead_code)] 79 | pub enum Button { 80 | #[default] 81 | Primary, 82 | Secondary, 83 | Positive, 84 | Destructive, 85 | Text, 86 | Custom(Box>), 87 | } 88 | 89 | impl button::StyleSheet for Theme { 90 | type Style = Button; 91 | 92 | fn active(&self, style: &Self::Style) -> button::Appearance { 93 | match style { 94 | Button::Primary => button::Appearance { 95 | background: Some(color!(0x25, 0x63, 0xeb).into()), 96 | text_color: color!(0x00, 0x00, 0x00), 97 | shadow_offset: Vector::new(3., 3.), 98 | border: Border { 99 | radius: 10.0.into(), 100 | width: 10.0, 101 | color: color!(0x25, 0x63, 0xeb), 102 | }, 103 | ..Default::default() 104 | }, 105 | Button::Secondary => button::Appearance { 106 | background: Some(color!(0x3c, 0x38, 0x36).into()), 107 | border: Border { 108 | radius: 10.0.into(), 109 | ..Default::default() 110 | }, 111 | shadow_offset: Vector::new(3., 3.), 112 | text_color: color!(0xff, 0xff, 0xff), 113 | ..Default::default() 114 | }, 115 | Button::Destructive => button::Appearance { 116 | background: Some(color!(0xdc, 0x26, 0x26).into()), 117 | shadow_offset: Vector::new(5., 5.), 118 | text_color: color!(0xff, 0xff, 0xff), 119 | border: Border { 120 | radius: 10.0.into(), 121 | color: color!(0xdc, 0x26, 0x26), 122 | width: 10.0, 123 | }, 124 | shadow: Shadow::default(), 125 | }, 126 | _ => panic!("This isn't a custom style exmaple, just skipping these for now"), 127 | } 128 | } 129 | 130 | fn hovered(&self, style: &Self::Style) -> button::Appearance { 131 | self.active(style) 132 | } 133 | 134 | fn pressed(&self, style: &Self::Style) -> button::Appearance { 135 | self.active(style) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | check *args: (check-cosmic args) (check-iced args) 2 | 3 | check-cosmic *args: 4 | cargo clippy --no-default-features --features wayland-libcosmic {{args}} -- -W clippy::pedantic 5 | cargo clippy --no-default-features --features winit-libcosmic {{args}} -- -W clippy::pedantic 6 | 7 | check-iced *args: 8 | cargo clippy --no-default-features --features iced {{args}} -- -W clippy::pedantic 9 | 10 | check-json: (check '--message-format=json') 11 | 12 | # Auto-apply recommend clippy fixes, and format code. 13 | fix: (check "--fix --allow-dirty --allow-staged") 14 | @cargo fmt 15 | -------------------------------------------------------------------------------- /src/keyframes.rs: -------------------------------------------------------------------------------- 1 | mod cards; 2 | mod helpers; 3 | mod toggler; 4 | 5 | pub use cards::Cards; 6 | pub use helpers::cards; 7 | pub use helpers::id; 8 | pub use helpers::lazy; 9 | pub use helpers::{chain, toggler}; 10 | pub use toggler::Toggler; 11 | /// The macro used to cleanly and efficently build an animation chain. 12 | /// Works for ann Id's that implement `into_chain` and `into_chain_with_children` 13 | #[macro_export] 14 | macro_rules! chain{ 15 | ($id:expr) => { 16 | $id.clone().into_chain() 17 | }; 18 | ($id:expr, $($x:expr),+ $(,)?) => { 19 | $id.clone().into_chain_with_children(vec![$($x),+]) 20 | }; 21 | } 22 | 23 | /// The macro used to clean up animation's view code. 24 | #[macro_export] 25 | macro_rules! anim{ 26 | ($id:expr, $($x:expr),+ $(,)?) => { 27 | $id.clone().as_widget($($x),+) 28 | }; 29 | } 30 | 31 | /// Repeat Limit 32 | #[derive(Debug, Copy, Clone, PartialEq, Default)] 33 | pub enum Repeat { 34 | /// Never repeat 35 | #[default] 36 | Never, 37 | /// Repeat forever 38 | Forever, 39 | } 40 | -------------------------------------------------------------------------------- /src/keyframes/cards.rs: -------------------------------------------------------------------------------- 1 | use cosmic::iced_core::widget::Id as IcedId; 2 | use cosmic::widget::icon::Handle; 3 | use cosmic::Element; 4 | 5 | use crate::keyframes::Repeat; 6 | use crate::timeline::Frame; 7 | use crate::{cards, chain, lazy::cards as lazy, Duration, Ease, Linear, MovementType}; 8 | 9 | /// A Cards's animation Id. Used for linking animation built in `update()` with widget output in `view()` 10 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 11 | pub struct Id(IcedId); 12 | const ANIM_DURATION: f32 = 100.; 13 | 14 | impl Id { 15 | /// Creates a custom [`Id`]. 16 | pub fn new(id: impl Into>) -> Self { 17 | Self(IcedId::new(id)) 18 | } 19 | 20 | /// Creates a unique [`Id`]. 21 | /// 22 | /// This function produces a different [`Id`] every time it is called. 23 | #[must_use] 24 | pub fn unique() -> Self { 25 | Self(IcedId::unique()) 26 | } 27 | 28 | /// Used by [`chain!`] macro 29 | #[must_use] 30 | pub fn into_chain(self) -> Chain { 31 | Chain::new(self) 32 | } 33 | 34 | /// Used by [`chain!`] macro 35 | #[must_use] 36 | pub fn into_chain_with_children(self, children: Vec) -> Chain { 37 | Chain::with_children(self, children) 38 | } 39 | 40 | /// Used by [`crate::anim!`] macro 41 | #[allow(clippy::too_many_arguments)] 42 | pub fn as_widget<'a, Message, F, G>( 43 | self, 44 | timeline: &crate::Timeline, 45 | card_inner_elements: Vec>, 46 | on_clear_all: Message, 47 | on_show_more: Option, 48 | on_activate: Option, 49 | show_more_label: &'a str, 50 | show_less_label: &'a str, 51 | clear_all_label: &'a str, 52 | show_less_icon: Option, 53 | expanded: bool, 54 | ) -> crate::widget::Cards<'a, Message, cosmic::Renderer> 55 | where 56 | F: 'a + Fn(Chain, bool) -> Message, 57 | G: 'a + Fn(usize) -> Message, 58 | 59 | Message: 'static + Clone, 60 | { 61 | Cards::as_widget( 62 | self, 63 | timeline, 64 | card_inner_elements, 65 | on_clear_all, 66 | on_show_more, 67 | on_activate, 68 | show_more_label, 69 | show_less_label, 70 | clear_all_label, 71 | show_less_icon, 72 | expanded, 73 | ) 74 | } 75 | } 76 | 77 | impl From for IcedId { 78 | fn from(id: Id) -> Self { 79 | id.0 80 | } 81 | } 82 | 83 | #[derive(Debug, Clone)] 84 | /// An animation, where each keyframe is "chained" together. 85 | pub struct Chain { 86 | id: Id, 87 | links: Vec, 88 | repeat: Repeat, 89 | } 90 | 91 | impl Chain { 92 | /// Crate a new [`Cards`] animation chain. 93 | /// You probably don't want to use use directly, and should 94 | /// use the [`chain`] macro. 95 | #[must_use] 96 | pub fn new(id: Id) -> Self { 97 | Chain { 98 | id, 99 | links: Vec::new(), 100 | repeat: Repeat::Never, 101 | } 102 | } 103 | 104 | /// Create a chain pre-fulled with children. 105 | /// You probably don't want to use use directly, and should 106 | /// use the [`chain`] macro. 107 | #[must_use] 108 | pub fn with_children(id: Id, children: Vec) -> Self { 109 | Chain { 110 | id, 111 | links: children, 112 | repeat: Repeat::Never, 113 | } 114 | } 115 | 116 | /// Link another keyframe, (very similar to push) 117 | /// You probably don't want to use use directly, and should 118 | /// use the [`chain`] macro. 119 | #[must_use] 120 | pub fn link(mut self, toggler: Cards) -> Self { 121 | self.links.push(toggler); 122 | self 123 | } 124 | 125 | /// Sets the animation to loop forever. 126 | #[must_use] 127 | pub fn loop_forever(mut self) -> Self { 128 | self.repeat = Repeat::Forever; 129 | self 130 | } 131 | 132 | /// Sets the animation to only loop once. 133 | /// This is the default, and only useful to 134 | /// stop an animation that was previously set 135 | /// to loop forever. 136 | #[must_use] 137 | pub fn loop_once(mut self) -> Self { 138 | self.repeat = Repeat::Never; 139 | self 140 | } 141 | 142 | /// Returns the default animation for animating the cards to "on" 143 | #[must_use] 144 | pub fn on(id: Id, anim_multiplier: f32) -> Self { 145 | let duration = (ANIM_DURATION * anim_multiplier.round()) as u64; 146 | chain!( 147 | id, 148 | lazy(Duration::ZERO), 149 | cards(Duration::from_millis(duration)).percent(1.0), 150 | ) 151 | } 152 | 153 | /// Returns the default animation for animating the cards to "off" 154 | #[must_use] 155 | pub fn off(id: Id, anim_multiplier: f32) -> Self { 156 | let duration = (ANIM_DURATION * anim_multiplier.round()) as u64; 157 | chain!( 158 | id, 159 | lazy(Duration::ZERO), 160 | cards(Duration::from_millis(duration)).percent(0.0), 161 | ) 162 | } 163 | } 164 | 165 | impl From for crate::timeline::Chain { 166 | fn from(chain: Chain) -> Self { 167 | crate::timeline::Chain::new( 168 | chain.id.into(), 169 | chain.repeat, 170 | chain 171 | .links 172 | .into_iter() 173 | .map(std::convert::Into::into) 174 | .collect::>(), 175 | ) 176 | } 177 | } 178 | 179 | #[must_use = "Keyframes are intended to be used in an animation chain."] 180 | #[derive(Debug, Clone, Copy)] 181 | pub struct Cards { 182 | at: MovementType, 183 | ease: Ease, 184 | percent: f32, 185 | is_eager: bool, 186 | } 187 | 188 | impl Cards { 189 | pub fn new(at: impl Into) -> Cards { 190 | let at = at.into(); 191 | Cards { 192 | at, 193 | ease: Linear::InOut.into(), 194 | percent: 1.0, 195 | is_eager: true, 196 | } 197 | } 198 | 199 | pub fn lazy(at: impl Into) -> Cards { 200 | let at = at.into(); 201 | Cards { 202 | at, 203 | ease: Linear::InOut.into(), 204 | percent: 1.0, 205 | is_eager: false, 206 | } 207 | } 208 | 209 | #[allow(clippy::too_many_arguments)] 210 | pub fn as_widget<'a, Message, F, G>( 211 | id: Id, 212 | timeline: &crate::Timeline, 213 | card_inner_elements: Vec>, 214 | on_clear_all: Message, 215 | on_show_more: Option, 216 | on_activate: Option, 217 | show_more_label: &'a str, 218 | show_less_label: &'a str, 219 | clear_all_label: &'a str, 220 | show_less_icon: Option, 221 | expanded: bool, 222 | ) -> crate::widget::Cards<'a, Message, cosmic::Renderer> 223 | where 224 | F: 'a + Fn(Chain, bool) -> Message, 225 | G: 'a + Fn(usize) -> Message, 226 | 227 | Message: Clone + 'static, 228 | { 229 | crate::widget::Cards::new( 230 | id.clone(), 231 | card_inner_elements, 232 | on_clear_all, 233 | on_show_more, 234 | on_activate, 235 | show_more_label, 236 | show_less_label, 237 | clear_all_label, 238 | show_less_icon, 239 | expanded, 240 | ) 241 | .percent( 242 | timeline 243 | .get(&id.into(), 0) 244 | .map_or(if expanded { 1.0 } else { 0.0 }, |m| m.value), 245 | ) 246 | } 247 | 248 | pub fn percent(mut self, percent: f32) -> Self { 249 | self.percent = percent; 250 | self 251 | } 252 | 253 | pub fn ease>(mut self, ease: E) -> Self { 254 | self.ease = ease.into(); 255 | self 256 | } 257 | } 258 | 259 | #[rustfmt::skip] 260 | impl From for Vec> { 261 | fn from(cards: Cards) -> Vec> { 262 | if cards.is_eager { 263 | vec![Some(Frame::eager(cards.at, cards.percent, cards.ease))] // 0 = animation percent completion 264 | } else { 265 | vec![Some(Frame::lazy(cards.at, 0., cards.ease))] // lazy evaluates for all values 266 | } 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/keyframes/helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::keyframes::Cards; 2 | use crate::keyframes::Toggler; 3 | 4 | use crate::MovementType; 5 | 6 | /// Create a toggler keyframe. 7 | /// Needs to be added into a chain. See [`crate::chain!`] macro. 8 | pub fn toggler(at: impl Into) -> Toggler { 9 | Toggler::new(at) 10 | } 11 | 12 | /// Create a cards keyframe. 13 | /// Needs to be added into a chain. See [`crate::chain!`] macro. 14 | pub fn cards(at: impl Into) -> Cards { 15 | Cards::new(at) 16 | } 17 | 18 | /// A slightly different import to clean up makeing lazy keyframes. 19 | pub mod lazy { 20 | use crate::keyframes::Cards; 21 | use crate::keyframes::Toggler; 22 | use crate::MovementType; 23 | 24 | /// Create a lazy toggler keyframe. 25 | /// Needs to be added into a chain. See [`crate::chain!`] macro. 26 | pub fn toggler(at: impl Into) -> Toggler { 27 | Toggler::lazy(at) 28 | } 29 | 30 | /// Create a lazy toggler keyframe. 31 | /// Needs to be added into a chain. See [`crate::chain!`] macro. 32 | pub fn cards(at: impl Into) -> Cards { 33 | Cards::lazy(at) 34 | } 35 | } 36 | 37 | /// A slightly different import to clean up makeing animation Ids. 38 | pub mod id { 39 | pub use crate::keyframes::cards::Id as Cards; 40 | pub use crate::keyframes::toggler::Id as Toggler; 41 | } 42 | 43 | /// Direct access to `Chain`s for widget that may return an animation 44 | /// in a message. 45 | pub mod chain { 46 | pub use crate::keyframes::cards::Chain as Cards; 47 | pub use crate::keyframes::toggler::Chain as Toggler; 48 | } 49 | -------------------------------------------------------------------------------- /src/keyframes/toggler.rs: -------------------------------------------------------------------------------- 1 | use crate::reexports::iced_core::{text, widget::Id as IcedId, Renderer as IcedRenderer}; 2 | 3 | use crate::keyframes::Repeat; 4 | use crate::timeline::Frame; 5 | use crate::{chain, lazy::toggler as lazy, toggler, Duration, Ease, Linear, MovementType}; 6 | 7 | /// A Toggler's animation Id. Used for linking animation built in `update()` with widget output in `view()` 8 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 9 | pub struct Id(IcedId); 10 | const ANIM_DURATION: f32 = 100.; 11 | 12 | impl Id { 13 | /// Creates a custom [`Id`]. 14 | pub fn new(id: impl Into>) -> Self { 15 | Self(IcedId::new(id)) 16 | } 17 | 18 | /// Creates a unique [`Id`]. 19 | /// 20 | /// This function produces a different [`Id`] every time it is called. 21 | #[must_use] 22 | pub fn unique() -> Self { 23 | Self(IcedId::unique()) 24 | } 25 | 26 | /// Used by [`chain!`] macro 27 | #[must_use] 28 | pub fn into_chain(self) -> Chain { 29 | Chain::new(self) 30 | } 31 | 32 | /// Used by [`chain!`] macro 33 | #[must_use] 34 | pub fn into_chain_with_children(self, children: Vec) -> Chain { 35 | Chain::with_children(self, children) 36 | } 37 | 38 | /// Used by [`crate::anim!`] macro 39 | pub fn as_widget<'a, Message, Renderer, F>( 40 | self, 41 | timeline: &crate::Timeline, 42 | label: impl Into>, 43 | is_toggled: bool, 44 | f: F, 45 | ) -> crate::widget::Toggler<'a, Message, Renderer> 46 | where 47 | Renderer: IcedRenderer + text::Renderer, 48 | F: 'a + Fn(Chain, bool) -> Message, 49 | { 50 | Toggler::as_widget(self, timeline, label, is_toggled, f) 51 | } 52 | } 53 | 54 | impl From for IcedId { 55 | fn from(id: Id) -> Self { 56 | id.0 57 | } 58 | } 59 | 60 | #[derive(Debug, Clone)] 61 | /// An animation, where each keyframe is "chained" together. 62 | pub struct Chain { 63 | id: Id, 64 | links: Vec, 65 | repeat: Repeat, 66 | } 67 | 68 | impl Chain { 69 | /// Crate a new Toggler animation chain. 70 | /// You probably don't want to use use directly, and should 71 | /// use the [`chain!`] macro. 72 | #[must_use] 73 | pub fn new(id: Id) -> Self { 74 | Chain { 75 | id, 76 | links: Vec::new(), 77 | repeat: Repeat::Never, 78 | } 79 | } 80 | 81 | /// Create a chain pre-fulled with children. 82 | /// You probably don't want to use use directly, and should 83 | /// use the [`chain!`] macro. 84 | #[must_use] 85 | pub fn with_children(id: Id, children: Vec) -> Self { 86 | Chain { 87 | id, 88 | links: children, 89 | repeat: Repeat::Never, 90 | } 91 | } 92 | 93 | /// Link another keyframe, (very similar to push) 94 | /// You probably don't want to use use directly, and should 95 | /// use the [`chain!`] macro. 96 | #[must_use] 97 | pub fn link(mut self, toggler: Toggler) -> Self { 98 | self.links.push(toggler); 99 | self 100 | } 101 | 102 | /// Sets the animation to loop forever. 103 | #[must_use] 104 | pub fn loop_forever(mut self) -> Self { 105 | self.repeat = Repeat::Forever; 106 | self 107 | } 108 | 109 | /// Sets the animation to only loop once. 110 | /// This is the default, and only useful to 111 | /// stop an animation that was previously set 112 | /// to loop forever. 113 | #[must_use] 114 | pub fn loop_once(mut self) -> Self { 115 | self.repeat = Repeat::Never; 116 | self 117 | } 118 | 119 | /// Returns the default animation for animating the toggler to "on" 120 | #[must_use] 121 | pub fn on(id: Id, anim_multiplier: f32) -> Self { 122 | let duration = (ANIM_DURATION * anim_multiplier.round()) as u64; 123 | chain!( 124 | id, 125 | lazy(Duration::ZERO), 126 | toggler(Duration::from_millis(duration)).percent(1.0), 127 | ) 128 | } 129 | 130 | /// Returns the default animation for animating the toggler to "off" 131 | #[must_use] 132 | pub fn off(id: Id, anim_multiplier: f32) -> Self { 133 | let duration = (ANIM_DURATION * anim_multiplier.round()) as u64; 134 | chain!( 135 | id, 136 | lazy(Duration::ZERO), 137 | toggler(Duration::from_millis(duration)).percent(0.0), 138 | ) 139 | } 140 | } 141 | 142 | impl From for crate::timeline::Chain { 143 | fn from(chain: Chain) -> Self { 144 | crate::timeline::Chain::new( 145 | chain.id.into(), 146 | chain.repeat, 147 | chain 148 | .links 149 | .into_iter() 150 | .map(std::convert::Into::into) 151 | .collect::>(), 152 | ) 153 | } 154 | } 155 | 156 | #[must_use = "Keyframes are intended to be used in an animation chain."] 157 | #[derive(Debug, Clone, Copy)] 158 | pub struct Toggler { 159 | at: MovementType, 160 | ease: Ease, 161 | percent: f32, 162 | is_eager: bool, 163 | } 164 | 165 | impl Toggler { 166 | pub fn new(at: impl Into) -> Toggler { 167 | let at = at.into(); 168 | Toggler { 169 | at, 170 | ease: Linear::InOut.into(), 171 | percent: 1.0, 172 | is_eager: true, 173 | } 174 | } 175 | 176 | pub fn lazy(at: impl Into) -> Toggler { 177 | let at = at.into(); 178 | Toggler { 179 | at, 180 | ease: Linear::InOut.into(), 181 | percent: 1.0, 182 | is_eager: false, 183 | } 184 | } 185 | 186 | pub fn as_widget<'a, Message, Renderer, F>( 187 | id: Id, 188 | timeline: &crate::Timeline, 189 | label: impl Into>, 190 | is_toggled: bool, 191 | f: F, 192 | ) -> crate::widget::Toggler<'a, Message, Renderer> 193 | where 194 | Renderer: IcedRenderer + text::Renderer, 195 | F: 'a + Fn(Chain, bool) -> Message, 196 | { 197 | crate::widget::Toggler::new(id.clone(), label, is_toggled, f).percent( 198 | timeline 199 | .get(&id.into(), 0) 200 | .map_or(if is_toggled { 1.0 } else { 0.0 }, |m| m.value), 201 | ) 202 | } 203 | 204 | pub fn percent(mut self, percent: f32) -> Self { 205 | self.percent = percent; 206 | self 207 | } 208 | 209 | pub fn ease>(mut self, ease: E) -> Self { 210 | self.ease = ease.into(); 211 | self 212 | } 213 | } 214 | 215 | #[rustfmt::skip] 216 | impl From for Vec> { 217 | fn from(toggler: Toggler) -> Vec> { 218 | if toggler.is_eager { 219 | vec![Some(Frame::eager(toggler.at, toggler.percent, toggler.ease))] // 0 = animation percent completion 220 | } else { 221 | vec![Some(Frame::lazy(toggler.at, 0., toggler.ease))] // lazy evaluates for all values 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! An animation toolkit for [Iced](https://github.com/iced-rs/iced) 2 | //! 3 | //! > This Project was build for [Cosmic DE](https://github.com/pop-os/cosmic-epoch). Though this will work for any project that depends on [Iced](https://github.com/iced-rs/iced). 4 | //! 5 | //! 6 | //! The goal of this project is to provide a simple API to build and show 7 | //! complex animations efficiently in applications built with Iced-rs/Iced. 8 | //! 9 | //! # Project Goals: 10 | //! * Full compatibility with Iced and The Elm Architecture. 11 | //! * Ease of use. 12 | //! * No math required for any animation. 13 | //! * No heap allocations in render loop. 14 | //! * Provide additional animatable widgets. 15 | //! * Custom widget support (create your own!). 16 | //! 17 | //! # Overview 18 | //! To wire cosmic-time into Iced there are five steps to do. 19 | //! 20 | //! 1. Create a [`Timeline`] This is the type that controls the animations. 21 | //! ```ignore 22 | //! struct Counter { 23 | //! timeline: Timeline 24 | //! } 25 | //! 26 | //! // ~ SNIP 27 | //! 28 | //! impl Application for Counter { 29 | //! // ~ SNIP 30 | //! fn new(_flags: ()) -> (Self, Command) { 31 | //! (Self { timeline: Timeline::new()}, Command::none()) 32 | //! } 33 | //! } 34 | //! ``` 35 | //! 2. Add at least one animation to your timeline. This can be done in your 36 | //! Application's `new()` or `update()`, or both! 37 | //! ```ignore 38 | //! static CONTAINER: Lazy = Lazy::new(id::Container::unique); 39 | //! 40 | //! let animation = chain![ 41 | //! CONTAINER, 42 | //! container(Duration::ZERO).width(10), 43 | //! container(Duration::from_secs(10)).width(100) 44 | //! ]; 45 | //! self.timeline.set_chain(animation).start(); 46 | //! 47 | //! ``` 48 | //! There are some different things here! 49 | //! > static CONTAINER: Lazy = `Lazy::new(id::Container::unique`); 50 | //! 51 | //! Cosmic Time refers to each animation with an Id. We export our own, but they are 52 | //! Identical to the widget Id's Iced uses for widget operations. 53 | //! Each animatable widget needs an Id. And each Id can only refer to one animation. 54 | //! 55 | //! > let animation = chain![ 56 | //! 57 | //! Cosmic Time refers to animations as [`Chain`]s because of how we build then. 58 | //! Each Keyframe is linked together like a chain. The Cosmic Time API doesn't 59 | //! say "change your width from 10 to 100". We define each state we want the 60 | //! widget to have `.width(10)` at `Duration::ZERO` then `.width(100)` at 61 | //! `Duration::from_secs(10)`. Where the `Duration` is the time after the previous 62 | //! keyframe. This is why we call the animations chains. We cannot get to the 63 | //! next state without animating though all previous Keyframes. 64 | //! 65 | //! > `self.timeline.set_chain(animation).start`(); 66 | //! 67 | //! Then we need to add the animation to the [`Timeline`]. We call this `.set_chain`, 68 | //! because there can only be one chain per Id. 69 | //! If we `set_chain` with a different animation with the same Id, the first one is 70 | //! replaced. This a actually a feature not a bug! 71 | //! As well you can set multiple animations at once: 72 | //! `self.timeline.set_chain(animation1).set_chain(animation2).start()` 73 | //! 74 | //! > .start() 75 | //! 76 | //! This one function call is important enough that we should look at it specifically. 77 | //! Cosmic Time is atomic, given the animation state held in the [`Timeline`] at any 78 | //! given time the global animations will be the exact same. The value used to 79 | //! calculate any animation's interpolation is global. And we use `.start()` to 80 | //! sync them together. 81 | //! Say you have two 5 seconds animations running at the same time. They should end 82 | //! at the same time right? That all depends on when the widget thinks it's animation 83 | //! should start. `.start()` tells all pending animations to start at the moment that 84 | //! `.start()` is called. This guarantees they stay in sync. 85 | //! IMPORTANT! Be sure to only call `.start()` once per call to `update()`. 86 | //! The below is incorrect! 87 | //! ```ignore 88 | //! self.timeline.set_chain(animation1).start(); 89 | //! self.timeline.set_chain(animation2).start(); 90 | //! ``` 91 | //! That code will compile, but will result in the animations not being in sync. 92 | //! 93 | //! 3. Add the Cosmic time Subscription 94 | //! ```ignore 95 | //! fn subscription(&self) -> Subscription { 96 | //! self.timeline.as_subscription::().map(Message::Tick) 97 | //! } 98 | //! ``` 99 | //! 100 | //! 4. Map the subscription to update the timeline's state: 101 | //! ```ignore 102 | //! fn update(&mut self, message: Message) -> Command { 103 | //! match message { 104 | //! Message::Tick(now) => self.timeline.now(now), 105 | //! } 106 | //! } 107 | //! ``` 108 | //! If you skip this step your animations will not progress! 109 | //! 110 | //! 5. Show the widget in your `view()`! 111 | //! ```ignore 112 | //! anim!(CONTIANER, &self.timeline, contents) 113 | //! ``` 114 | //! 115 | //! All done! 116 | //! There is a bit of wiring to get Cosmic Time working, but after that it's only 117 | //! a few lines to create rather complex animations! 118 | //! See the Pong example to see how a full game of pong can be implemented in 119 | //! only a few lines! 120 | #![deny( 121 | missing_debug_implementations, 122 | missing_docs, 123 | unused_results, 124 | clippy::extra_unused_lifetimes, 125 | clippy::from_over_into, 126 | clippy::needless_borrow, 127 | clippy::new_without_default, 128 | clippy::useless_conversion 129 | )] 130 | #![forbid(unsafe_code, rust_2018_idioms)] 131 | #![allow( 132 | clippy::cast_possible_truncation, 133 | clippy::cast_sign_loss, 134 | clippy::inherent_to_string, 135 | clippy::type_complexity 136 | )] 137 | #![cfg_attr(docsrs, feature(doc_cfg))] 138 | pub mod reexports; 139 | /// The main timeline for your animations! 140 | pub mod timeline; 141 | /// Additional Widgets that Cosmic Time uses for more advanced animations. 142 | pub mod widget; 143 | 144 | mod keyframes; 145 | mod utils; 146 | 147 | pub use crate::keyframes::{cards, chain, id, lazy, toggler, Repeat}; 148 | pub use crate::timeline::{Chain, Timeline}; 149 | 150 | pub use cosmic::iced::time::{Duration, Instant}; 151 | 152 | #[cfg(feature = "once_cell")] 153 | pub use once_cell; 154 | 155 | const PI: f32 = std::f32::consts::PI; 156 | 157 | /// A simple linear interpolation calculation function. 158 | /// p = `percent_complete` in decimal form 159 | #[must_use] 160 | pub fn lerp(start: f32, end: f32, p: f32) -> f32 { 161 | (1.0 - p) * start + p * end 162 | } 163 | 164 | /// A simple animation percentage flip calculation function. 165 | #[must_use] 166 | pub fn flip(num: f32) -> f32 { 167 | 1.0 - num 168 | } 169 | 170 | /// A trait that all ease's need to implement to be used. 171 | pub trait Tween: std::fmt::Debug + Copy { 172 | /// Takes a linear percentage, and returns tweened value. 173 | /// p = percent complete as decimal 174 | fn tween(&self, p: f32) -> f32; 175 | } 176 | 177 | /// Speed Controlled Animation use this type. 178 | /// Rather than specifying the time (`Duration`) 179 | /// between links in the animation chain, this 180 | /// type auto-calculates the time for you. 181 | /// Very useful with lazy keyframes. 182 | /// Designed to have an API very similar to `std::time::Duration` 183 | #[derive(Debug, Copy, Clone)] 184 | pub enum Speed { 185 | /// Whole number of seconds to move per second. 186 | PerSecond(f32), 187 | /// Whole number of millisseconds to move per millisecond. 188 | PerMillis(f32), 189 | /// Whole number of microseconds to move per microseconds. 190 | PerMicros(f32), 191 | /// Whole number of nanoseconds to move per nanosecond. 192 | PerNanoSe(f32), 193 | } 194 | 195 | impl Speed { 196 | /// Creates a new `Speed` from the specified number of whole seconds. 197 | #[must_use] 198 | pub fn per_secs(speed: f32) -> Self { 199 | Speed::PerSecond(speed) 200 | } 201 | 202 | /// Creates a new `Speed` from the specified number of whole milliseconds. 203 | #[must_use] 204 | pub fn per_millis(speed: f32) -> Self { 205 | Speed::PerMillis(speed) 206 | } 207 | 208 | /// Creates a new `Speed` from the specified number of whole microseconds. 209 | #[must_use] 210 | pub fn per_micros(speed: f32) -> Self { 211 | Speed::PerMicros(speed) 212 | } 213 | 214 | /// Creates a new `Speed` from the specified number of whole nanoseconds. 215 | #[must_use] 216 | pub fn per_nanos(speed: f32) -> Self { 217 | Speed::PerNanoSe(speed) 218 | } 219 | 220 | fn calc_duration(self, first: f32, second: f32) -> Duration { 221 | match self { 222 | Speed::PerSecond(speed) => { 223 | ((first - second).abs() / speed).round() as u32 * Duration::from_nanos(1e9 as u64) 224 | } 225 | Speed::PerMillis(speed) => { 226 | ((first - second).abs() / speed).round() as u32 * Duration::from_nanos(1e6 as u64) 227 | } 228 | Speed::PerMicros(speed) => { 229 | ((first - second).abs() / speed).round() as u32 * Duration::from_nanos(1000) 230 | } 231 | Speed::PerNanoSe(speed) => { 232 | ((first - second).abs() / speed).round() as u32 * Duration::from_nanos(1) 233 | } 234 | } 235 | } 236 | } 237 | 238 | /// A container type so that the API user can specify Either 239 | /// Time controlled animations, or speed controlled animations. 240 | #[derive(Debug, Copy, Clone)] 241 | pub enum MovementType { 242 | /// Keyframe is time controlled. 243 | Duration(Duration), 244 | /// keyframe is speed controlled. 245 | Speed(Speed), 246 | } 247 | 248 | impl From for MovementType { 249 | fn from(duration: Duration) -> Self { 250 | MovementType::Duration(duration) 251 | } 252 | } 253 | 254 | impl From for MovementType { 255 | fn from(speed: Speed) -> Self { 256 | MovementType::Speed(speed) 257 | } 258 | } 259 | 260 | macro_rules! tween { 261 | ($($x:ident),*) => { 262 | #[derive(Debug, Copy, Clone)] 263 | /// A container type for all types of animations easings. 264 | pub enum Ease { 265 | $( 266 | /// A container for $x 267 | $x($x), 268 | )* 269 | } 270 | 271 | impl Tween for Ease { 272 | fn tween(&self, p: f32) -> f32 { 273 | match self { 274 | $( 275 | Ease::$x(ease) => ease.tween(p), 276 | )* 277 | } 278 | } 279 | } 280 | }; 281 | } 282 | 283 | tween!( 284 | Linear, 285 | Quadratic, 286 | Cubic, 287 | Quartic, 288 | Quintic, 289 | Sinusoidal, 290 | Exponential, 291 | Circular, 292 | Elastic, 293 | Back, 294 | Bounce 295 | ); 296 | 297 | /// Used to set a linear animation easing. 298 | /// The default for most animations. 299 | #[derive(Debug, Copy, Clone)] 300 | pub enum Linear { 301 | /// Modeled after the line y = x 302 | InOut, 303 | } 304 | 305 | impl Tween for Linear { 306 | fn tween(&self, p: f32) -> f32 { 307 | p 308 | } 309 | } 310 | 311 | impl From for Ease { 312 | fn from(linear: Linear) -> Self { 313 | Ease::Linear(linear) 314 | } 315 | } 316 | 317 | /// Used to set a quadratic animation easing. 318 | #[derive(Debug, Copy, Clone)] 319 | pub enum Quadratic { 320 | /// Modeled after the parabola y = x^2 321 | In, 322 | /// Modeled after the parabola y = -x^2 + 2x 323 | Out, 324 | /// Modeled after the piecewise quadratic 325 | /// y = (1/2)((2x)^2) ; [0, 0.5) 326 | /// y = -(1/2)((2x-1)*(2x-3) - 1) ; [0.5, 1] 327 | InOut, 328 | /// A Bezier Curve TODO 329 | Bezier(i32), 330 | } 331 | 332 | impl Tween for Quadratic { 333 | fn tween(&self, p: f32) -> f32 { 334 | match self { 335 | Quadratic::In => p.powi(2), 336 | Quadratic::Out => -(p * (p - 2.)), 337 | Quadratic::InOut => { 338 | if p < 0.5 { 339 | 2. * p.powi(2) 340 | } else { 341 | (-2. * p.powi(2)) + p.mul_add(4., -1.) 342 | } 343 | } 344 | Quadratic::Bezier(_n) => p, 345 | } 346 | } 347 | } 348 | 349 | impl From for Ease { 350 | fn from(quadratic: Quadratic) -> Self { 351 | Ease::Quadratic(quadratic) 352 | } 353 | } 354 | 355 | /// Used to set a cubic animation easing. 356 | #[derive(Debug, Copy, Clone)] 357 | pub enum Cubic { 358 | /// Modeled after the cubic y = x^3 359 | In, 360 | /// Modeled after the cubic y = (x-1)^3 + 1 361 | Out, 362 | /// Modeled after the piecewise cubic 363 | /// y = (1/2)((2x)^3) ; [0, 0.5] 364 | /// y = (1/2)((2x-2)^3 + 2) ; [0.5, 1] 365 | InOut, 366 | } 367 | 368 | impl Tween for Cubic { 369 | fn tween(&self, p: f32) -> f32 { 370 | match self { 371 | Cubic::In => p.powi(3), 372 | Cubic::Out => { 373 | let q = p - 1.; 374 | q.powi(3) + 1. 375 | } 376 | Cubic::InOut => { 377 | if p < 0.5 { 378 | 4. * p.powi(3) 379 | } else { 380 | let q = p.mul_add(2., -2.); 381 | (q.powi(3)).mul_add(0.5, 1.) 382 | } 383 | } 384 | } 385 | } 386 | } 387 | 388 | impl From for Ease { 389 | fn from(cubic: Cubic) -> Self { 390 | Ease::Cubic(cubic) 391 | } 392 | } 393 | 394 | /// Used to set a quartic animation easing. 395 | #[derive(Debug, Copy, Clone)] 396 | pub enum Quartic { 397 | /// Modeled after the quartic y = x^4 398 | In, 399 | /// Modeled after the quartic y = 1 - (x - 1)^4 400 | Out, 401 | /// Modeled after the piecewise quartic 402 | /// y = (1/2)((2x)^4) ; [0, 0.5] 403 | /// y = -(1/2)((2x-2)^4 -2) ; [0.5, 1] 404 | InOut, 405 | } 406 | 407 | impl Tween for Quartic { 408 | fn tween(&self, p: f32) -> f32 { 409 | match self { 410 | Quartic::In => p.powi(4), 411 | Quartic::Out => { 412 | let q = p - 1.; 413 | (q.powi(3)).mul_add(1. - p, 1.) 414 | } 415 | Quartic::InOut => { 416 | if p < 0.5 { 417 | 8. * p.powi(4) 418 | } else { 419 | let q = p - 1.; 420 | (q.powi(4)).mul_add(-8., 1.) 421 | } 422 | } 423 | } 424 | } 425 | } 426 | 427 | impl From for Ease { 428 | fn from(quartic: Quartic) -> Self { 429 | Ease::Quartic(quartic) 430 | } 431 | } 432 | 433 | /// Used to set a quintic animation easing. 434 | #[derive(Debug, Copy, Clone)] 435 | pub enum Quintic { 436 | /// Modeled after the quintic y = x^5 437 | In, 438 | /// Modeled after the quintic y = (x - 1)^5 + 1 439 | Out, 440 | /// Modeled after the piecewise quintic 441 | /// y = (1/2)((2x)^5) ; [0, 0.5] 442 | /// y = (1/2)((2x-2)^5 + 2) ; [0.5, 1] 443 | InOut, 444 | } 445 | 446 | impl Tween for Quintic { 447 | fn tween(&self, p: f32) -> f32 { 448 | match self { 449 | Quintic::In => p.powi(5), 450 | Quintic::Out => { 451 | let q = p - 1.; 452 | q.powi(5) + 1. 453 | } 454 | Quintic::InOut => { 455 | if p < 0.5 { 456 | 16. * p.powi(5) 457 | } else { 458 | let q = (2. * p) - 2.; 459 | q.powi(5).mul_add(0.5, 1.) 460 | } 461 | } 462 | } 463 | } 464 | } 465 | 466 | impl From for Ease { 467 | fn from(quintic: Quintic) -> Self { 468 | Ease::Quintic(quintic) 469 | } 470 | } 471 | 472 | /// Used to set a sinusoildal animation easing. 473 | #[derive(Debug, Copy, Clone)] 474 | pub enum Sinusoidal { 475 | /// Modeled after eighth sinusoidal wave y = 1 - cos((x * PI) / 2) 476 | In, 477 | /// Modeled after eigth sinusoidal wave y = sin((x * PI) / 2) 478 | Out, 479 | /// Modeled after quarter sinusoidal wave y = -0.5 * (cos(x * PI) - 1); 480 | InOut, 481 | } 482 | 483 | impl Tween for Sinusoidal { 484 | fn tween(&self, p: f32) -> f32 { 485 | match self { 486 | Sinusoidal::In => 1. - ((p * PI) / 2.).cos(), 487 | Sinusoidal::Out => ((p * PI) / 2.).sin(), 488 | Sinusoidal::InOut => -0.5 * ((p * PI).cos() - 1.), 489 | } 490 | } 491 | } 492 | 493 | impl From for Ease { 494 | fn from(sinusoidal: Sinusoidal) -> Self { 495 | Ease::Sinusoidal(sinusoidal) 496 | } 497 | } 498 | 499 | /// Used to set an exponential animation easing. 500 | #[derive(Debug, Copy, Clone)] 501 | pub enum Exponential { 502 | /// Modeled after the piecewise exponential 503 | /// y = 0 ; [0, 0] 504 | /// y = 2^(10x-10) ; [0, 1] 505 | In, 506 | /// Modeled after the piecewise exponential 507 | /// y = 1 - 2^(-10x) ; [0, 1] 508 | /// y = 1 ; [1, 1] 509 | Out, 510 | /// Modeled after the piecewise exponential 511 | /// y = 0 ; [0, 0 ] 512 | /// y = 2^(20x - 10) / 2 ; [0, 0.5] 513 | /// y = 1 - 0.5*2^(-10(2x - 1)) ; [0.5, 1] 514 | /// y = 1 ; [1, 1 ] 515 | InOut, 516 | } 517 | 518 | impl Tween for Exponential { 519 | fn tween(&self, p: f32) -> f32 { 520 | match self { 521 | Exponential::In => { 522 | if p == 0. { 523 | 0. 524 | } else { 525 | 2_f32.powf(10. * p - 10.) 526 | } 527 | } 528 | Exponential::Out => { 529 | if p == 1. { 530 | 1. 531 | } else { 532 | 1. - 2_f32.powf(-10. * p) 533 | } 534 | } 535 | Exponential::InOut => { 536 | if p == 0. { 537 | 0. 538 | } else if p == 1. { 539 | 1. 540 | } else if p < 0.5 { 541 | 2_f32.powf(p.mul_add(20., -10.)) * 0.5 542 | } else { 543 | 2_f32.powf(p.mul_add(-20., 10.)).mul_add(-0.5, 1.) 544 | } 545 | } 546 | } 547 | } 548 | } 549 | 550 | impl From for Ease { 551 | fn from(exponential: Exponential) -> Self { 552 | Ease::Exponential(exponential) 553 | } 554 | } 555 | 556 | /// Used to set an circular animation easing. 557 | #[derive(Debug, Copy, Clone)] 558 | pub enum Circular { 559 | /// Modeled after shifted quadrant IV of unit circle. y = 1 - sqrt(1 - x^2) 560 | In, 561 | /// Modeled after shifted quadrant II of unit circle. y = sqrt(1 - (x - 1)^ 2) 562 | Out, 563 | /// Modeled after the piecewise circular function 564 | /// y = (1/2)(1 - sqrt(1 - 2x^2)) ; [0, 0.5) 565 | /// y = (1/2)(sqrt(1 - ((-2x + 2)^2)) + 1) ; [0.5, 1] 566 | InOut, 567 | } 568 | 569 | impl Tween for Circular { 570 | fn tween(&self, p: f32) -> f32 { 571 | match self { 572 | Circular::In => 1.0 - (1. - (p.powi(2))).sqrt(), 573 | Circular::Out => ((2. - p) * p).sqrt(), 574 | Circular::InOut => { 575 | if p < 0.5 { 576 | 0.5 * (1. - (1. - (2. * p).powi(2)).sqrt()) 577 | } else { 578 | 0.5 * ((1. - (-2. * p + 2.).powi(2)).sqrt() + 1.) 579 | } 580 | } 581 | } 582 | } 583 | } 584 | 585 | impl From for Ease { 586 | fn from(circular: Circular) -> Self { 587 | Ease::Circular(circular) 588 | } 589 | } 590 | 591 | /// Used to set an elastic animation easing. 592 | #[derive(Debug, Copy, Clone)] 593 | pub enum Elastic { 594 | /// Modeled after damped sin wave: y = sin(13×π/2 x)×2^(10 (x - 1)) 595 | In, 596 | /// Modeled after damped piecewise sin wave: 597 | /// y = 2^(-10 x) sin((x×10 - 0.75) (2×π/3)) + 1 [0, 1] 598 | /// y = 1 [1, 1] 599 | Out, 600 | /// Modeled after the piecewise exponentially-damped sine wave: 601 | /// y = 2^(10 (2 x - 1) - 1) sin(13 π x) [0, 0.5] 602 | /// y = 1/2 (2 - 2^(-10 (2 x - 1)) sin(13 π x)) [0.5, 1] 603 | InOut, 604 | } 605 | 606 | impl Tween for Elastic { 607 | fn tween(&self, p: f32) -> f32 { 608 | match self { 609 | Elastic::In => (13. * (PI / 2.) * p).sin() * 2_f32.powf(10. * (p - 1.)), 610 | Elastic::Out => { 611 | if p == 1. { 612 | 1. 613 | } else { 614 | 2_f32.powf(-10. * p) * ((10. * p - 0.75) * ((2. * PI) / 3.)).sin() + 1. 615 | } 616 | } 617 | Elastic::InOut => { 618 | if p < 0.5 { 619 | 2_f32.powf(10. * (2. * p - 1.) - 1.) * (13. * PI * p).sin() 620 | } else { 621 | 0.5 * (2. - 2_f32.powf(-20. * p + 10.) * (13. * PI * p).sin()) 622 | } 623 | } 624 | } 625 | } 626 | } 627 | 628 | impl From for Ease { 629 | fn from(elastic: Elastic) -> Self { 630 | Ease::Elastic(elastic) 631 | } 632 | } 633 | 634 | /// Used to set a back animation easing. 635 | #[derive(Debug, Copy, Clone)] 636 | pub enum Back { 637 | /// Modeled after the function: y = 2.70158 * x^3 + x^2 * (-1.70158) 638 | In, 639 | /// Modeled after the function: y = 1 + 2.70158 (x - 1)^3 + 1.70158 (x - 1)^2 640 | Out, 641 | /// Modeled after the piecewise function: 642 | /// y = (2x)^2 * (1/2 * ((2.5949095 + 1) * 2x - 2.5949095)) [0, 0.5] 643 | /// y = 1/2 * ((2 x - 2)^2 * ((2.5949095 + 1) * (2x - 2) + 2.5949095) + 2) [0.5, 1] 644 | InOut, 645 | } 646 | 647 | impl Tween for Back { 648 | fn tween(&self, p: f32) -> f32 { 649 | match self { 650 | Back::In => 2.70158 * p.powi(3) - 1.70158 * p.powi(2), 651 | Back::Out => { 652 | let q: f32 = p - 1.; 653 | 1. + 2.70158 * q.powi(3) + 1.70158 * q.powi(2) 654 | } 655 | Back::InOut => { 656 | let c = 2.594_909_5; 657 | if p < 0.5 { 658 | let q = 2. * p; 659 | q.powi(2) * (0.5 * ((c + 1.) * q - c)) 660 | } else { 661 | let q = 2. * p - 2.; 662 | 0.5 * (q.powi(2) * ((c + 1.) * q + c) + 2.) 663 | } 664 | } 665 | } 666 | } 667 | } 668 | 669 | impl From for Ease { 670 | fn from(back: Back) -> Self { 671 | Ease::Back(back) 672 | } 673 | } 674 | 675 | /// Used to set a bounce animation easing. 676 | #[derive(Debug, Copy, Clone)] 677 | pub enum Bounce { 678 | /// Bounce before animating in. 679 | In, 680 | /// Bounce against end point. 681 | Out, 682 | /// Bounce before animating in, then against the end point. 683 | InOut, 684 | } 685 | 686 | impl Bounce { 687 | fn bounce_ease_in(p: f32) -> f32 { 688 | 1. - Bounce::bounce_ease_out(1. - p) 689 | } 690 | 691 | fn bounce_ease_out(p: f32) -> f32 { 692 | if p < 4. / 11. { 693 | (121. * p.powi(2)) / 16. 694 | } else if p < 8. / 11. { 695 | (363. / 40. * p.powi(2)) - 99. / 10. * p + 17. / 5. 696 | } else if p < 9. / 10. { 697 | 4356. / 361. * p.powi(2) - 35442. / 1805. * p + 16061. / 1805. 698 | } else { 699 | 54. / 5. * p.powi(2) - 513. / 25. * p + 268. / 25. 700 | } 701 | } 702 | } 703 | 704 | impl Tween for Bounce { 705 | fn tween(&self, p: f32) -> f32 { 706 | match self { 707 | Bounce::In => Bounce::bounce_ease_in(p), 708 | Bounce::Out => Bounce::bounce_ease_out(p), 709 | Bounce::InOut => { 710 | if p < 0.5 { 711 | 0.5 * Bounce::bounce_ease_in(p * 2.) 712 | } else { 713 | 0.5 + 0.5 * Bounce::bounce_ease_out(p * 2. - 1.) 714 | } 715 | } 716 | } 717 | } 718 | } 719 | 720 | impl From for Ease { 721 | fn from(bounce: Bounce) -> Self { 722 | Ease::Bounce(bounce) 723 | } 724 | } 725 | 726 | #[cfg(test)] 727 | mod test { 728 | #![allow(clippy::excessive_precision)] 729 | use super::*; 730 | 731 | fn r(val: f32) -> f32 { 732 | (val * 10E+5).round() / 10E+5 733 | } 734 | 735 | #[test] 736 | fn linear() { 737 | assert_eq!(0.0, Linear::InOut.tween(0.0)); 738 | assert_eq!(0.1, Linear::InOut.tween(0.1)); 739 | assert_eq!(0.2, Linear::InOut.tween(0.2)); 740 | assert_eq!(0.3, Linear::InOut.tween(0.3)); 741 | assert_eq!(0.4, Linear::InOut.tween(0.4)); 742 | assert_eq!(0.5, Linear::InOut.tween(0.5)); 743 | assert_eq!(0.6, Linear::InOut.tween(0.6)); 744 | assert_eq!(0.7, Linear::InOut.tween(0.7)); 745 | assert_eq!(0.8, Linear::InOut.tween(0.8)); 746 | assert_eq!(0.9, Linear::InOut.tween(0.9)); 747 | assert_eq!(1.0, Linear::InOut.tween(1.0)); 748 | } 749 | 750 | #[test] 751 | // Modeled after the parabola y = x^2 752 | fn quadratic_in() { 753 | assert_eq!(0.00, r(Quadratic::In.tween(0.0))); 754 | assert_eq!(0.01, r(Quadratic::In.tween(0.1))); 755 | assert_eq!(0.04, r(Quadratic::In.tween(0.2))); 756 | assert_eq!(0.09, r(Quadratic::In.tween(0.3))); 757 | assert_eq!(0.16, r(Quadratic::In.tween(0.4))); 758 | assert_eq!(0.25, r(Quadratic::In.tween(0.5))); 759 | assert_eq!(0.36, r(Quadratic::In.tween(0.6))); 760 | assert_eq!(0.49, r(Quadratic::In.tween(0.7))); 761 | assert_eq!(0.64, r(Quadratic::In.tween(0.8))); 762 | assert_eq!(0.81, r(Quadratic::In.tween(0.9))); 763 | assert_eq!(1.00, r(Quadratic::In.tween(1.0))); 764 | } 765 | 766 | #[test] 767 | // Modeled after the parabola y = -x^2 + 2x 768 | fn quadratic_out() { 769 | assert_eq!(0.00, r(Quadratic::Out.tween(0.0))); 770 | assert_eq!(0.19, r(Quadratic::Out.tween(0.1))); 771 | assert_eq!(0.36, r(Quadratic::Out.tween(0.2))); 772 | assert_eq!(0.51, r(Quadratic::Out.tween(0.3))); 773 | assert_eq!(0.64, r(Quadratic::Out.tween(0.4))); 774 | assert_eq!(0.75, r(Quadratic::Out.tween(0.5))); 775 | assert_eq!(0.84, r(Quadratic::Out.tween(0.6))); 776 | assert_eq!(0.91, r(Quadratic::Out.tween(0.7))); 777 | assert_eq!(0.96, r(Quadratic::Out.tween(0.8))); 778 | assert_eq!(0.99, r(Quadratic::Out.tween(0.9))); 779 | assert_eq!(1.00, r(Quadratic::Out.tween(1.0))); 780 | } 781 | 782 | #[test] 783 | // Modeled after the piecewise quadratic 784 | // y = (1/2)((2x)^2) ; [0, 0.5) 785 | // y = -(1/2)((2x-1)*(2x-3) - 1) ; [0.5, 1] 786 | fn quadratic_inout() { 787 | assert_eq!(0.00, r(Quadratic::InOut.tween(0.0))); 788 | assert_eq!(0.02, r(Quadratic::InOut.tween(0.1))); 789 | assert_eq!(0.08, r(Quadratic::InOut.tween(0.2))); 790 | assert_eq!(0.18, r(Quadratic::InOut.tween(0.3))); 791 | assert_eq!(0.32, r(Quadratic::InOut.tween(0.4))); 792 | assert_eq!(0.50, r(Quadratic::InOut.tween(0.5))); 793 | assert_eq!(0.68, r(Quadratic::InOut.tween(0.6))); 794 | assert_eq!(0.82, r(Quadratic::InOut.tween(0.7))); 795 | assert_eq!(0.92, r(Quadratic::InOut.tween(0.8))); 796 | assert_eq!(0.98, r(Quadratic::InOut.tween(0.9))); 797 | assert_eq!(1.00, r(Quadratic::InOut.tween(1.0))); 798 | } 799 | 800 | // TODO Bezier 801 | 802 | #[test] 803 | // Modeled after the cubic y = x^3 804 | fn cubic_in() { 805 | assert_eq!(0.000, r(Cubic::In.tween(0.0))); 806 | assert_eq!(0.001, r(Cubic::In.tween(0.1))); 807 | assert_eq!(0.008, r(Cubic::In.tween(0.2))); 808 | assert_eq!(0.027, r(Cubic::In.tween(0.3))); 809 | assert_eq!(0.064, r(Cubic::In.tween(0.4))); 810 | assert_eq!(0.125, r(Cubic::In.tween(0.5))); 811 | assert_eq!(0.216, r(Cubic::In.tween(0.6))); 812 | assert_eq!(0.343, r(Cubic::In.tween(0.7))); 813 | assert_eq!(0.512, r(Cubic::In.tween(0.8))); 814 | assert_eq!(0.729, r(Cubic::In.tween(0.9))); 815 | assert_eq!(1.000, r(Cubic::In.tween(1.0))); 816 | } 817 | 818 | #[test] 819 | // Modeled after the cubic y = (x-1)^3 + 1 820 | fn cubic_out() { 821 | assert_eq!(0.000, r(Cubic::Out.tween(0.0))); 822 | assert_eq!(0.271, r(Cubic::Out.tween(0.1))); 823 | assert_eq!(0.488, r(Cubic::Out.tween(0.2))); 824 | assert_eq!(0.657, r(Cubic::Out.tween(0.3))); 825 | assert_eq!(0.784, r(Cubic::Out.tween(0.4))); 826 | assert_eq!(0.875, r(Cubic::Out.tween(0.5))); 827 | assert_eq!(0.936, r(Cubic::Out.tween(0.6))); 828 | assert_eq!(0.973, r(Cubic::Out.tween(0.7))); 829 | assert_eq!(0.992, r(Cubic::Out.tween(0.8))); 830 | assert_eq!(0.999, r(Cubic::Out.tween(0.9))); 831 | assert_eq!(1.000, r(Cubic::Out.tween(1.0))); 832 | } 833 | 834 | #[test] 835 | // Modeled after the piecewise cubic 836 | // y = (1/2)((2x)^3) ; [0, 0.5] 837 | // y = (1/2)((2x-2)^3 + 2) ; [0.5, 1] 838 | fn cubic_inout() { 839 | assert_eq!(0.000, r(Cubic::InOut.tween(0.0))); 840 | assert_eq!(0.004, r(Cubic::InOut.tween(0.1))); 841 | assert_eq!(0.032, r(Cubic::InOut.tween(0.2))); 842 | assert_eq!(0.108, r(Cubic::InOut.tween(0.3))); 843 | assert_eq!(0.256, r(Cubic::InOut.tween(0.4))); 844 | assert_eq!(0.500, r(Cubic::InOut.tween(0.5))); 845 | assert_eq!(0.744, r(Cubic::InOut.tween(0.6))); 846 | assert_eq!(0.892, r(Cubic::InOut.tween(0.7))); 847 | assert_eq!(0.968, r(Cubic::InOut.tween(0.8))); 848 | assert_eq!(0.996, r(Cubic::InOut.tween(0.9))); 849 | assert_eq!(1.000, r(Cubic::InOut.tween(1.0))); 850 | } 851 | 852 | #[test] 853 | // Modeled after the quartic y = x^4 854 | fn quartic_in() { 855 | assert_eq!(0.0000, r(Quartic::In.tween(0.0))); 856 | assert_eq!(0.0001, r(Quartic::In.tween(0.1))); 857 | assert_eq!(0.0016, r(Quartic::In.tween(0.2))); 858 | assert_eq!(0.0081, r(Quartic::In.tween(0.3))); 859 | assert_eq!(0.0256, r(Quartic::In.tween(0.4))); 860 | assert_eq!(0.0625, r(Quartic::In.tween(0.5))); 861 | assert_eq!(0.1296, r(Quartic::In.tween(0.6))); 862 | assert_eq!(0.2401, r(Quartic::In.tween(0.7))); 863 | assert_eq!(0.4096, r(Quartic::In.tween(0.8))); 864 | assert_eq!(0.6561, r(Quartic::In.tween(0.9))); 865 | assert_eq!(1.0000, r(Quartic::In.tween(1.0))); 866 | } 867 | 868 | #[test] 869 | // Modeled after the quartic y = 1 - (x - 1)^4 870 | fn quartic_out() { 871 | assert_eq!(0.0000, r(Quartic::Out.tween(0.0))); 872 | assert_eq!(0.3439, r(Quartic::Out.tween(0.1))); 873 | assert_eq!(0.5904, r(Quartic::Out.tween(0.2))); 874 | assert_eq!(0.7599, r(Quartic::Out.tween(0.3))); 875 | assert_eq!(0.8704, r(Quartic::Out.tween(0.4))); 876 | assert_eq!(0.9375, r(Quartic::Out.tween(0.5))); 877 | assert_eq!(0.9744, r(Quartic::Out.tween(0.6))); 878 | assert_eq!(0.9919, r(Quartic::Out.tween(0.7))); 879 | assert_eq!(0.9984, r(Quartic::Out.tween(0.8))); 880 | assert_eq!(0.9999, r(Quartic::Out.tween(0.9))); 881 | assert_eq!(1.0000, r(Quartic::Out.tween(1.0))); 882 | } 883 | 884 | #[test] 885 | // Modeled after the piecewise quartic 886 | // y = (1/2)((2x)^4) ; [0, 0.5] 887 | // y = -(1/2)((2x-2)^4 -2) ; [0.5, 1] 888 | fn quartic_inout() { 889 | assert_eq!(0.0000, r(Quartic::InOut.tween(0.0))); 890 | assert_eq!(0.0008, r(Quartic::InOut.tween(0.1))); 891 | assert_eq!(0.0128, r(Quartic::InOut.tween(0.2))); 892 | assert_eq!(0.0648, r(Quartic::InOut.tween(0.3))); 893 | assert_eq!(0.2048, r(Quartic::InOut.tween(0.4))); 894 | assert_eq!(0.5000, r(Quartic::InOut.tween(0.5))); 895 | assert_eq!(0.7952, r(Quartic::InOut.tween(0.6))); 896 | assert_eq!(0.9352, r(Quartic::InOut.tween(0.7))); 897 | assert_eq!(0.9872, r(Quartic::InOut.tween(0.8))); 898 | assert_eq!(0.9992, r(Quartic::InOut.tween(0.9))); 899 | assert_eq!(1.0000, r(Quartic::InOut.tween(1.0))); 900 | } 901 | 902 | #[test] 903 | // Modeled after the quartic y = x^5 904 | fn quintic_in() { 905 | assert_eq!(0.00000, r(Quintic::In.tween(0.0))); 906 | assert_eq!(0.00001, r(Quintic::In.tween(0.1))); 907 | assert_eq!(0.00032, r(Quintic::In.tween(0.2))); 908 | assert_eq!(0.00243, r(Quintic::In.tween(0.3))); 909 | assert_eq!(0.01024, r(Quintic::In.tween(0.4))); 910 | assert_eq!(0.03125, r(Quintic::In.tween(0.5))); 911 | assert_eq!(0.07776, r(Quintic::In.tween(0.6))); 912 | assert_eq!(0.16807, r(Quintic::In.tween(0.7))); 913 | assert_eq!(0.32768, r(Quintic::In.tween(0.8))); 914 | assert_eq!(0.59049, r(Quintic::In.tween(0.9))); 915 | assert_eq!(1.00000, r(Quintic::In.tween(1.0))); 916 | } 917 | 918 | #[test] 919 | // Modeled after the quintic y = (x - 1)^5 + 1 920 | fn quintic_out() { 921 | assert_eq!(0.00000, r(Quintic::Out.tween(0.0))); 922 | assert_eq!(0.40951, r(Quintic::Out.tween(0.1))); 923 | assert_eq!(0.67232, r(Quintic::Out.tween(0.2))); 924 | assert_eq!(0.83193, r(Quintic::Out.tween(0.3))); 925 | assert_eq!(0.92224, r(Quintic::Out.tween(0.4))); 926 | assert_eq!(0.96875, r(Quintic::Out.tween(0.5))); 927 | assert_eq!(0.98976, r(Quintic::Out.tween(0.6))); 928 | assert_eq!(0.99757, r(Quintic::Out.tween(0.7))); 929 | assert_eq!(0.99968, r(Quintic::Out.tween(0.8))); 930 | assert_eq!(0.99999, r(Quintic::Out.tween(0.9))); 931 | assert_eq!(1.00000, r(Quintic::Out.tween(1.0))); 932 | } 933 | 934 | #[test] 935 | // Modeled after the piecewise quintic 936 | // y = (1/2)((2x)^5) ; [0, 0.5] 937 | // y = (1/2)((2x-2)^5 + 2) ; [0.5, 1] 938 | fn quintic_inout() { 939 | assert_eq!(0.00000, r(Quintic::InOut.tween(0.0))); 940 | assert_eq!(0.00016, r(Quintic::InOut.tween(0.1))); 941 | assert_eq!(0.00512, r(Quintic::InOut.tween(0.2))); 942 | assert_eq!(0.03888, r(Quintic::InOut.tween(0.3))); 943 | assert_eq!(0.16384, r(Quintic::InOut.tween(0.4))); 944 | assert_eq!(0.50000, r(Quintic::InOut.tween(0.5))); 945 | assert_eq!(0.83616, r(Quintic::InOut.tween(0.6))); 946 | assert_eq!(0.96112, r(Quintic::InOut.tween(0.7))); 947 | assert_eq!(0.99488, r(Quintic::InOut.tween(0.8))); 948 | assert_eq!(0.99984, r(Quintic::InOut.tween(0.9))); 949 | assert_eq!(1.00000, r(Quintic::InOut.tween(1.0))); 950 | } 951 | 952 | #[test] 953 | // Modeled after eighth sinusoidal wave y = 1 - cos((x * PI) / 2) 954 | fn sinusoidal_in() { 955 | assert_eq!(0.000_000, r(Sinusoidal::In.tween(0.0))); 956 | assert_eq!(0.012_312, r(Sinusoidal::In.tween(0.1))); 957 | assert_eq!(0.048_943, r(Sinusoidal::In.tween(0.2))); 958 | assert_eq!(0.108_993, r(Sinusoidal::In.tween(0.3))); 959 | assert_eq!(0.190_983, r(Sinusoidal::In.tween(0.4))); 960 | assert_eq!(0.292_893, r(Sinusoidal::In.tween(0.5))); 961 | assert_eq!(0.412_215, r(Sinusoidal::In.tween(0.6))); 962 | assert_eq!(0.546_010, r(Sinusoidal::In.tween(0.7))); 963 | assert_eq!(0.690_983, r(Sinusoidal::In.tween(0.8))); 964 | assert_eq!(0.843_566, r(Sinusoidal::In.tween(0.9))); 965 | assert_eq!(1.000_000, r(Sinusoidal::In.tween(1.0))); 966 | } 967 | 968 | #[test] 969 | #[allow(clippy::approx_constant)] 970 | // Modeled after eigth sinusoidal wave y = sin((x * PI) / 2) 971 | fn sinusoidal_out() { 972 | assert_eq!(0.000_000, r(Sinusoidal::Out.tween(0.0))); 973 | assert_eq!(0.156_434, r(Sinusoidal::Out.tween(0.1))); 974 | assert_eq!(0.309_017, r(Sinusoidal::Out.tween(0.2))); 975 | assert_eq!(0.453_991, r(Sinusoidal::Out.tween(0.3))); 976 | assert_eq!(0.587_785, r(Sinusoidal::Out.tween(0.4))); 977 | assert_eq!(0.707_107, r(Sinusoidal::Out.tween(0.5))); 978 | assert_eq!(0.809_017, r(Sinusoidal::Out.tween(0.6))); 979 | assert_eq!(0.891_007, r(Sinusoidal::Out.tween(0.7))); 980 | assert_eq!(0.951_057, r(Sinusoidal::Out.tween(0.8))); 981 | assert_eq!(0.987_688, r(Sinusoidal::Out.tween(0.9))); 982 | assert_eq!(1.000_000, r(Sinusoidal::Out.tween(1.0))); 983 | } 984 | 985 | #[test] 986 | // Modeled after quarter sinusoidal wave y = -0.5 * (cos(x * PI) - 1); 987 | fn sinusoidal_inout() { 988 | assert_eq!(0.000_000, r(Sinusoidal::InOut.tween(0.0))); 989 | assert_eq!(0.024_472, r(Sinusoidal::InOut.tween(0.1))); 990 | assert_eq!(0.095_492, r(Sinusoidal::InOut.tween(0.2))); 991 | assert_eq!(0.206_107, r(Sinusoidal::InOut.tween(0.3))); 992 | assert_eq!(0.345_492, r(Sinusoidal::InOut.tween(0.4))); 993 | assert_eq!(0.500_000, r(Sinusoidal::InOut.tween(0.5))); 994 | assert_eq!(0.654_509, r(Sinusoidal::InOut.tween(0.6))); 995 | assert_eq!(0.793_893, r(Sinusoidal::InOut.tween(0.7))); 996 | assert_eq!(0.904_509, r(Sinusoidal::InOut.tween(0.8))); 997 | assert_eq!(0.975_528, r(Sinusoidal::InOut.tween(0.9))); 998 | assert_eq!(1.000_000, r(Sinusoidal::InOut.tween(1.0))); 999 | } 1000 | 1001 | #[test] 1002 | // Modeled after the piecewise exponential 1003 | // y = 0 ; [0, 0] 1004 | // y = 2^(10x-10) ; [0, 1] 1005 | fn exponential_in() { 1006 | assert_eq!(0.000_000, r(Exponential::In.tween(0.0))); 1007 | assert_eq!(0.001_953, r(Exponential::In.tween(0.1))); 1008 | assert_eq!(0.003_906, r(Exponential::In.tween(0.2))); 1009 | assert_eq!(0.007_813, r(Exponential::In.tween(0.3))); 1010 | assert_eq!(0.015_625, r(Exponential::In.tween(0.4))); 1011 | assert_eq!(0.031_250, r(Exponential::In.tween(0.5))); 1012 | assert_eq!(0.062_500, r(Exponential::In.tween(0.6))); 1013 | assert_eq!(0.125_000, r(Exponential::In.tween(0.7))); 1014 | assert_eq!(0.250_000, r(Exponential::In.tween(0.8))); 1015 | assert_eq!(0.500_000, r(Exponential::In.tween(0.9))); 1016 | assert_eq!(1.000_000, r(Exponential::In.tween(1.0))); 1017 | } 1018 | 1019 | #[test] 1020 | // Modeled after the piecewise exponential 1021 | // y = 1 - 2^(-10x) ; [0, 1] 1022 | // y = 1 ; [1, 1] 1023 | fn exponential_out() { 1024 | assert_eq!(0.000_000, r(Exponential::Out.tween(0.0))); 1025 | assert_eq!(0.500_000, r(Exponential::Out.tween(0.1))); 1026 | assert_eq!(0.750_000, r(Exponential::Out.tween(0.2))); 1027 | assert_eq!(0.875_000, r(Exponential::Out.tween(0.3))); 1028 | assert_eq!(0.937_500, r(Exponential::Out.tween(0.4))); 1029 | assert_eq!(0.968_750, r(Exponential::Out.tween(0.5))); 1030 | assert_eq!(0.984_375, r(Exponential::Out.tween(0.6))); 1031 | assert_eq!(0.992_188, r(Exponential::Out.tween(0.7))); 1032 | assert_eq!(0.996_094, r(Exponential::Out.tween(0.8))); 1033 | assert_eq!(0.998_047, r(Exponential::Out.tween(0.9))); 1034 | assert_eq!(1.000_000, r(Exponential::Out.tween(1.0))); 1035 | } 1036 | 1037 | #[test] 1038 | // Modeled after the piecewise exponential 1039 | // y = 0 ; [0, 0 ] 1040 | // y = 2^(20x - 10) / 2 ; [0, 0.5] 1041 | // y = 1 - 0.5*2^(-20x + 10)) ; [0.5, 1] 1042 | // y = 1 ; [1, 1 ] 1043 | fn exponential_inout() { 1044 | assert_eq!(0.000_000, r(Exponential::InOut.tween(0.0))); 1045 | assert_eq!(0.001_953, r(Exponential::InOut.tween(0.1))); 1046 | assert_eq!(0.007_813, r(Exponential::InOut.tween(0.2))); 1047 | assert_eq!(0.031_250, r(Exponential::InOut.tween(0.3))); 1048 | assert_eq!(0.125_000, r(Exponential::InOut.tween(0.4))); 1049 | assert_eq!(0.500_000, r(Exponential::InOut.tween(0.5))); 1050 | assert_eq!(0.875_000, r(Exponential::InOut.tween(0.6))); 1051 | assert_eq!(0.968_750, r(Exponential::InOut.tween(0.7))); 1052 | assert_eq!(0.992_188, r(Exponential::InOut.tween(0.8))); 1053 | assert_eq!(0.998_047, r(Exponential::InOut.tween(0.9))); 1054 | assert_eq!(1.000_000, r(Exponential::InOut.tween(1.0))); 1055 | } 1056 | 1057 | #[test] 1058 | // Modeled after shifted quadrant IV of unit circle. y = 1 - sqrt(1 - x^2) 1059 | fn circular_in() { 1060 | assert_eq!(0.000_000, r(Circular::In.tween(0.0))); 1061 | assert_eq!(0.005_013, r(Circular::In.tween(0.1))); 1062 | assert_eq!(0.020_204, r(Circular::In.tween(0.2))); 1063 | assert_eq!(0.046_061, r(Circular::In.tween(0.3))); 1064 | assert_eq!(0.083_485, r(Circular::In.tween(0.4))); 1065 | assert_eq!(0.133_975, r(Circular::In.tween(0.5))); 1066 | assert_eq!(0.200_000, r(Circular::In.tween(0.6))); 1067 | assert_eq!(0.285_857, r(Circular::In.tween(0.7))); 1068 | assert_eq!(0.400_000, r(Circular::In.tween(0.8))); 1069 | assert_eq!(0.564_110, r(Circular::In.tween(0.9))); 1070 | assert_eq!(1.000_000, r(Circular::In.tween(1.0))); 1071 | } 1072 | 1073 | #[test] 1074 | // Modeled after shifted quadrant II of unit circle. y = sqrt(1 - (x - 1)^ 2) 1075 | fn circular_out() { 1076 | assert_eq!(0.000_000, r(Circular::Out.tween(0.0))); 1077 | assert_eq!(0.435_890, r(Circular::Out.tween(0.1))); 1078 | assert_eq!(0.600_000, r(Circular::Out.tween(0.2))); 1079 | assert_eq!(0.714_143, r(Circular::Out.tween(0.3))); 1080 | assert_eq!(0.800_000, r(Circular::Out.tween(0.4))); 1081 | assert_eq!(0.866_025, r(Circular::Out.tween(0.5))); 1082 | assert_eq!(0.916_515, r(Circular::Out.tween(0.6))); 1083 | assert_eq!(0.953_939, r(Circular::Out.tween(0.7))); 1084 | assert_eq!(0.979_796, r(Circular::Out.tween(0.8))); 1085 | assert_eq!(0.994_987, r(Circular::Out.tween(0.9))); 1086 | assert_eq!(1.000_000, r(Circular::Out.tween(1.0))); 1087 | } 1088 | 1089 | #[test] 1090 | // Modeled after the piecewise circular function 1091 | // y = (1/2)(1 - sqrt(1 - (2x)^2)) ; [0, 0.5) 1092 | // y = (1/2)(sqrt(1 - ((-2x + 2)^2)) + 1) ; [0.5, 1] 1093 | fn circular_inout() { 1094 | assert_eq!(0.000_000, r(Circular::InOut.tween(0.0))); 1095 | assert_eq!(0.010_102, r(Circular::InOut.tween(0.1))); 1096 | assert_eq!(0.041_742, r(Circular::InOut.tween(0.2))); 1097 | assert_eq!(0.100_000, r(Circular::InOut.tween(0.3))); 1098 | assert_eq!(0.200_000, r(Circular::InOut.tween(0.4))); 1099 | assert_eq!(0.500_000, r(Circular::InOut.tween(0.5))); 1100 | assert_eq!(0.800_000, r(Circular::InOut.tween(0.6))); 1101 | assert_eq!(0.900_000, r(Circular::InOut.tween(0.7))); 1102 | assert_eq!(0.958_258, r(Circular::InOut.tween(0.8))); 1103 | assert_eq!(0.989_898, r(Circular::InOut.tween(0.9))); 1104 | assert_eq!(1.000_000, r(Circular::InOut.tween(1.0))); 1105 | } 1106 | 1107 | #[test] 1108 | #[rustfmt::skip] 1109 | // Modeled after damped sin wave: y = sin(13 * π/2 * x) * 2^(10 (x - 1)) 1110 | fn elastic_in() { 1111 | assert_eq!( 0.000_000, r(Elastic::In.tween(0.0))); 1112 | assert_eq!( 0.001_740, r(Elastic::In.tween(0.1))); 1113 | assert_eq!(-0.003_160, r(Elastic::In.tween(0.2))); 1114 | assert_eq!(-0.001_222, r(Elastic::In.tween(0.3))); 1115 | assert_eq!( 0.014_860, r(Elastic::In.tween(0.4))); 1116 | assert_eq!(-0.022_097, r(Elastic::In.tween(0.5))); 1117 | assert_eq!(-0.019_313, r(Elastic::In.tween(0.6))); 1118 | assert_eq!( 0.123_461, r(Elastic::In.tween(0.7))); 1119 | assert_eq!(-0.146_947, r(Elastic::In.tween(0.8))); 1120 | assert_eq!(-0.226_995, r(Elastic::In.tween(0.9))); 1121 | assert_eq!( 1.000_000, r(Elastic::In.tween(1.0))); 1122 | } 1123 | 1124 | #[test] 1125 | // Modeled after damped piecewise sin wave: 1126 | // y = 1 - 2^(-10 x) sin((13 π)/(2 (x + 1))) ; [0, 1] 1127 | // y = 1 [1, 1] 1128 | fn elastic_out() { 1129 | assert_eq!(0.000_000, r(Elastic::Out.tween(0.0))); 1130 | assert_eq!(1.250_000, r(Elastic::Out.tween(0.1))); 1131 | assert_eq!(1.125_000, r(Elastic::Out.tween(0.2))); 1132 | assert_eq!(0.875_000, r(Elastic::Out.tween(0.3))); 1133 | assert_eq!(1.031_250, r(Elastic::Out.tween(0.4))); 1134 | assert_eq!(1.015_625, r(Elastic::Out.tween(0.5))); 1135 | assert_eq!(0.984_375, r(Elastic::Out.tween(0.6))); 1136 | assert_eq!(1.003_906, r(Elastic::Out.tween(0.7))); 1137 | assert_eq!(1.001_953, r(Elastic::Out.tween(0.8))); 1138 | assert_eq!(0.998_047, r(Elastic::Out.tween(0.9))); 1139 | assert_eq!(1.000_000, r(Elastic::Out.tween(1.0))); 1140 | } 1141 | 1142 | #[test] 1143 | #[rustfmt::skip] 1144 | // Modeled after the piecewise exponentially-damped sine wave: 1145 | // y = 2^(10 (2 x - 1) - 1) sin(13 π x) [0, 0.5] 1146 | // y = 1/2 (2 - 2^(-10 (2 x - 1)) sin(13 π x)) [0.5, 1] 1147 | fn elastic_inout() { 1148 | assert_eq!( 0.000_000, r(Elastic::InOut.tween(0.0))); 1149 | assert_eq!(-0.001_580, r(Elastic::InOut.tween(0.1))); 1150 | assert_eq!( 0.007_430, r(Elastic::InOut.tween(0.2))); 1151 | assert_eq!(-0.009_657, r(Elastic::InOut.tween(0.3))); 1152 | assert_eq!(-0.073_473, r(Elastic::InOut.tween(0.4))); 1153 | assert_eq!( 0.500_000, r(Elastic::InOut.tween(0.5))); 1154 | assert_eq!( 1.073_473, r(Elastic::InOut.tween(0.6))); 1155 | assert_eq!( 1.009_657, r(Elastic::InOut.tween(0.7))); 1156 | assert_eq!( 0.992_570, r(Elastic::InOut.tween(0.8))); 1157 | assert_eq!( 1.001_580, r(Elastic::InOut.tween(0.9))); 1158 | assert_eq!( 1.000_000, r(Elastic::InOut.tween(1.0))); 1159 | } 1160 | 1161 | #[test] 1162 | #[rustfmt::skip] 1163 | fn back_in() { 1164 | // Modeled after the function: y = 2.70158 * x^3 + x^2 * (-1.70158) 1165 | assert_eq!( 0.000_000, r(Back::In.tween(0.0))); 1166 | assert_eq!(-0.014_314, r(Back::In.tween(0.1))); 1167 | assert_eq!(-0.046_451, r(Back::In.tween(0.2))); 1168 | assert_eq!(-0.080_200, r(Back::In.tween(0.3))); 1169 | assert_eq!(-0.099_352, r(Back::In.tween(0.4))); 1170 | assert_eq!(-0.087_698, r(Back::In.tween(0.5))); 1171 | assert_eq!(-0.029_028, r(Back::In.tween(0.6))); 1172 | assert_eq!( 0.092_868, r(Back::In.tween(0.7))); 1173 | assert_eq!( 0.294_198, r(Back::In.tween(0.8))); 1174 | assert_eq!( 0.591_172, r(Back::In.tween(0.9))); 1175 | assert_eq!( 1.000_000, r(Back::In.tween(1.0))); 1176 | } 1177 | 1178 | #[test] 1179 | fn back_out() { 1180 | // Modeled after the function: y = 1 + 2.70158 (x - 1)^3 + 1.70158 (x - 1)^2 1181 | assert_eq!(0.000_000, r(Back::Out.tween(0.0))); 1182 | assert_eq!(0.408_828, r(Back::Out.tween(0.1))); 1183 | assert_eq!(0.705_802, r(Back::Out.tween(0.2))); 1184 | assert_eq!(0.907_132, r(Back::Out.tween(0.3))); 1185 | assert_eq!(1.029_027, r(Back::Out.tween(0.4))); 1186 | assert_eq!(1.087_698, r(Back::Out.tween(0.5))); 1187 | assert_eq!(1.099_352, r(Back::Out.tween(0.6))); 1188 | assert_eq!(1.0802, r(Back::Out.tween(0.7))); 1189 | assert_eq!(1.046_451, r(Back::Out.tween(0.8))); 1190 | assert_eq!(1.014_314, r(Back::Out.tween(0.9))); 1191 | assert_eq!(1.000_000, r(Back::Out.tween(1.0))); 1192 | } 1193 | 1194 | #[test] 1195 | #[rustfmt::skip] 1196 | fn back_inout() { 1197 | // Modeled after the piecewise function: 1198 | // y = (2x)^2 * (1/2 * ((2.5949095 + 1) * 2x - 2.5949095)) [0, 0.5] 1199 | // y = 1/2 * ((2 x - 2)^2 * ((2.5949095 + 1) * (2x - 2) + 2.5949095) + 2) [0.5, 1] 1200 | assert_eq!( 0.000_000, r(Back::InOut.tween(0.0))); 1201 | assert_eq!(-0.037_519, r(Back::InOut.tween(0.1))); 1202 | assert_eq!(-0.092_556, r(Back::InOut.tween(0.2))); 1203 | assert_eq!(-0.078_833, r(Back::InOut.tween(0.3))); 1204 | assert_eq!( 0.089_926, r(Back::InOut.tween(0.4))); 1205 | assert_eq!( 0.500_000, r(Back::InOut.tween(0.5))); 1206 | assert_eq!( 0.910_074, r(Back::InOut.tween(0.6))); 1207 | assert_eq!( 1.078_834, r(Back::InOut.tween(0.7))); 1208 | assert_eq!( 1.092_556, r(Back::InOut.tween(0.8))); 1209 | assert_eq!( 1.037_519, r(Back::InOut.tween(0.9))); 1210 | assert_eq!( 1.000_000, r(Back::InOut.tween(1.0))); 1211 | } 1212 | 1213 | #[test] 1214 | #[rustfmt::skip] 1215 | fn bounce_in() { 1216 | assert_eq!(0.000_000, r(Bounce::In.tween(0.0))); 1217 | assert_eq!( 1e-6, r(Bounce::In.tween(0.1))); 1218 | assert_eq!(0.087_757, r(Bounce::In.tween(0.2))); 1219 | assert_eq!(0.083_250, r(Bounce::In.tween(0.3))); 1220 | assert_eq!(0.273_000, r(Bounce::In.tween(0.4))); 1221 | assert_eq!(0.281_250, r(Bounce::In.tween(0.5))); 1222 | assert_eq!(0.108_000, r(Bounce::In.tween(0.6))); 1223 | assert_eq!(0.319_375, r(Bounce::In.tween(0.7))); 1224 | assert_eq!(0.697_500, r(Bounce::In.tween(0.8))); 1225 | assert_eq!(0.924_375, r(Bounce::In.tween(0.9))); 1226 | assert_eq!(1.000_000, r(Bounce::In.tween(1.0))); 1227 | } 1228 | 1229 | #[test] 1230 | fn bounce_out() { 1231 | assert_eq!(0.000_000, r(Bounce::Out.tween(0.0))); 1232 | assert_eq!(0.075_625, r(Bounce::Out.tween(0.1))); 1233 | assert_eq!(0.302_500, r(Bounce::Out.tween(0.2))); 1234 | assert_eq!(0.680_625, r(Bounce::Out.tween(0.3))); 1235 | assert_eq!(0.892_000, r(Bounce::Out.tween(0.4))); 1236 | assert_eq!(0.718_750, r(Bounce::Out.tween(0.5))); 1237 | assert_eq!(0.727_000, r(Bounce::Out.tween(0.6))); 1238 | assert_eq!(0.916_750, r(Bounce::Out.tween(0.7))); 1239 | assert_eq!(0.912_243, r(Bounce::Out.tween(0.8))); 1240 | assert_eq!(0.999_999, r(Bounce::Out.tween(0.9))); 1241 | assert_eq!(1.000_000, r(Bounce::Out.tween(1.0))); 1242 | } 1243 | 1244 | #[test] 1245 | fn bounce_inout() { 1246 | assert_eq!(0.000_000, r(Bounce::InOut.tween(0.0))); 1247 | assert_eq!(0.043_878, r(Bounce::InOut.tween(0.1))); 1248 | assert_eq!(0.136_500, r(Bounce::InOut.tween(0.2))); 1249 | assert_eq!(0.054_000, r(Bounce::InOut.tween(0.3))); 1250 | assert_eq!(0.348_750, r(Bounce::InOut.tween(0.4))); 1251 | assert_eq!(0.500_000, r(Bounce::InOut.tween(0.5))); 1252 | assert_eq!(0.651_250, r(Bounce::InOut.tween(0.6))); 1253 | assert_eq!(0.946_000, r(Bounce::InOut.tween(0.7))); 1254 | assert_eq!(0.863_500, r(Bounce::InOut.tween(0.8))); 1255 | assert_eq!(0.956_121, r(Bounce::InOut.tween(0.9))); 1256 | assert_eq!(1.000_000, r(Bounce::InOut.tween(1.0))); 1257 | } 1258 | } 1259 | -------------------------------------------------------------------------------- /src/reexports/iced.rs: -------------------------------------------------------------------------------- 1 | pub use iced; 2 | pub use iced_core; 3 | pub use iced_futures; 4 | pub use iced_runtime; 5 | pub use iced_style; 6 | pub use iced_widget; 7 | pub use iced_widget::button::Catalog as ButtonStyleSheet; 8 | -------------------------------------------------------------------------------- /src/reexports/libcosmic.rs: -------------------------------------------------------------------------------- 1 | pub use cosmic::iced; 2 | pub use cosmic::iced_core; 3 | pub use cosmic::iced_futures; 4 | pub use cosmic::iced_runtime; 5 | pub use cosmic::iced_widget; 6 | pub use cosmic::widget::button::Catalog as ButtonStyleSheet; 7 | pub use cosmic::Theme; 8 | -------------------------------------------------------------------------------- /src/reexports/mod.rs: -------------------------------------------------------------------------------- 1 | //! Reexports of all the modules in this crate. 2 | 3 | mod libcosmic; 4 | pub use self::libcosmic::{ 5 | iced, iced_core, iced_futures, iced_runtime, iced_widget, ButtonStyleSheet, Theme, 6 | }; 7 | -------------------------------------------------------------------------------- /src/timeline.rs: -------------------------------------------------------------------------------- 1 | mod imports { 2 | pub use cosmic::iced::time::{Duration, Instant}; 3 | pub use cosmic::iced_core::widget; 4 | pub use cosmic::iced_futures::subscription::Subscription; 5 | } 6 | 7 | use imports::{widget, Duration, Instant, Subscription}; 8 | 9 | use std::cmp::Ordering; 10 | use std::collections::HashMap; 11 | 12 | use crate::keyframes::Repeat; 13 | use crate::{lerp, Ease, MovementType, Tween}; 14 | 15 | /// This holds all the data for your animations. 16 | /// tracks: this holds all data for active animations 17 | /// pendings: this holds all data that hasn't been `.start()`ed yet. 18 | /// now: This is the instant that is used to calculate animation interpolations. 19 | #[derive(Debug, Clone)] 20 | pub struct Timeline { 21 | // Hash map of widget::id to track, where each track is made of subtracks 22 | tracks: HashMap>)>, 23 | // Pending keyframes. Need to call `start` to finalize start time and move into `tracks` 24 | pendings: HashMap, 25 | // Global animation interp value. Use `timeline.now(instant)`, where instant is the value 26 | // passed from the `timeline.as_subscription` value. 27 | now: Option, 28 | } 29 | 30 | impl std::default::Default for Timeline { 31 | fn default() -> Self { 32 | Self::new() 33 | } 34 | } 35 | 36 | /// All "keyframes" have their own chain to make the API friendly. 37 | /// But all chain types need to `impl Into<>` this chain type, so that 38 | /// the [`Timeline`] can hold and manipulate that data. 39 | #[derive(Debug, Clone)] 40 | pub struct Chain { 41 | /// The Id that refers to this animation. Same Id type that Iced uses. 42 | pub id: widget::Id, 43 | /// Should we loop this animation? This field decides that. 44 | pub repeat: Repeat, 45 | links: Vec>>, 46 | } 47 | 48 | impl Chain { 49 | /// Create a new chain. 50 | pub fn new(id: widget::Id, repeat: Repeat, links: impl Into>>>) -> Self { 51 | let links = links.into(); 52 | Chain { id, repeat, links } 53 | } 54 | } 55 | 56 | #[derive(Debug, Clone)] 57 | enum Pending { 58 | Chain(Repeat, Vec>>, Pause), 59 | Pause, 60 | Resume, 61 | PauseAll, 62 | ResumeAll, 63 | } 64 | 65 | /// A Frame is the exact value of the modifier at a given time. 66 | /// Advanced use only. 67 | /// You do not need this type unless you are making your own custom animate-able widget. 68 | /// A `Frame::Eager` refers to a known widget's modifier state. This is for most cases 69 | /// like "animate to width 10". 70 | /// A `Frame::Lazy` is for continueing a previous animation, either midway through 71 | /// the animation, or even after the animation was completed. 72 | #[derive(Debug, Clone, Copy)] 73 | pub enum Frame { 74 | /// Keyframe time, !!VALUE AT TIME!!, ease type into value 75 | Eager(MovementType, f32, Ease), 76 | /// Keyframe time, !!DEFAULT FALLBACK VALUE!!, ease type into value 77 | Lazy(MovementType, f32, Ease), 78 | } 79 | 80 | impl Frame { 81 | /// Create an Eager Frame. 82 | pub fn eager(movement_type: impl Into, value: f32, ease: Ease) -> Self { 83 | let movement_type = movement_type.into(); 84 | Frame::Eager(movement_type, value, ease) 85 | } 86 | 87 | /// Create an Lazy Frame. 88 | pub fn lazy(movement_type: impl Into, default: f32, ease: Ease) -> Self { 89 | let movement_type = movement_type.into(); 90 | Frame::Lazy(movement_type, default, ease) 91 | } 92 | 93 | /// You almost certainly do not need this function. 94 | /// Used in `timeline::start` to guarentee that we have the same 95 | /// time of an animation, not the API convinient [`MovementType`]. 96 | #[must_use] 97 | pub fn to_subframe(self, time: Instant) -> SubFrame { 98 | let (value, ease) = match self { 99 | Frame::Eager(_movement_type, value, ease) => (value, ease), 100 | _ => panic!("Call 'to_eager' first"), 101 | }; 102 | 103 | SubFrame::new(time, value, ease) 104 | } 105 | 106 | /// You almost certainly do not need this function. 107 | /// Converts a Lazy [`Frame`] to an Eager [`Frame`]. 108 | pub fn to_eager(&mut self, timeline: &Timeline, id: &widget::Id, index: usize) { 109 | *self = if let Frame::Lazy(movement_type, default, ease) = *self { 110 | let value = timeline.get(id, index).map_or(default, |i| i.value); 111 | Frame::Eager(movement_type, value, ease) 112 | } else { 113 | *self 114 | } 115 | } 116 | 117 | fn get_value(&self) -> f32 { 118 | match self { 119 | Frame::Eager(_, value, _) => *value, 120 | _ => panic!("call 'to_eager' first"), 121 | } 122 | } 123 | 124 | /// You almost certainly do not need this function. 125 | /// Get the duration of a [`Frame`] 126 | #[must_use] 127 | pub fn get_duration(self, previous: &Self) -> Duration { 128 | match self { 129 | Frame::Eager(movement_type, value, _ease) => match movement_type { 130 | MovementType::Duration(duration) => duration, 131 | MovementType::Speed(speed) => speed.calc_duration(previous.get_value(), value), 132 | }, 133 | _ => panic!("Call 'to_eager' first"), 134 | } 135 | } 136 | } 137 | 138 | /// The metadata of an animation. Used by [`Timeline`]. 139 | #[derive(Clone, Debug)] 140 | pub struct Meta { 141 | /// Does the animation repeat? The decides that. 142 | pub repeat: Repeat, 143 | /// The specific time the animation started at. 144 | pub start: Instant, 145 | /// The time that the animation will end. 146 | /// Used to optimize [`Timeline::as_subscription`] 147 | pub end: Instant, 148 | /// The length of time the animation will last 149 | pub length: Duration, 150 | /// Is the animation paused? This decides that. 151 | pub pause: Pause, 152 | } 153 | 154 | impl Meta { 155 | /// Creates new metadata for an animation. 156 | #[must_use] 157 | pub fn new( 158 | repeat: Repeat, 159 | start: Instant, 160 | end: Instant, 161 | length: Duration, 162 | pause: Pause, 163 | ) -> Self { 164 | Meta { 165 | repeat, 166 | start, 167 | end, 168 | length, 169 | pause, 170 | } 171 | } 172 | 173 | /// Sets the animation to be paused. 174 | /// If you are an end user of Cosmic Time, you do not want this. 175 | /// You want the `pause` function on [`Timeline`]. 176 | pub fn pause(&mut self, now: Instant) { 177 | if let Pause::Resumed(delay) = self.pause { 178 | self.pause = Pause::Paused(relative_time(&now.checked_sub(delay).unwrap(), self)); 179 | } else { 180 | self.pause = Pause::Paused(relative_time(&now, self)); 181 | } 182 | } 183 | 184 | /// Sets the animation to be resumed. 185 | /// If you are an end user of Cosmic Time, you do not want this. 186 | /// You want the `resume` function on [`Timeline`]. 187 | pub fn resume(&mut self, now: Instant) { 188 | if let Pause::Paused(start) = self.pause { 189 | self.pause = Pause::Resumed(now - start); 190 | } 191 | } 192 | } 193 | 194 | /// A type to help guarentee that a paused animation has the correct data 195 | /// to be resumed and/or continue animating. 196 | #[derive(Debug, Clone, Copy)] 197 | pub enum Pause { 198 | /// Currently paused, with the relative instant into the animation it was paused at. 199 | Paused(Instant), 200 | /// Has never been paused 201 | NoPause, 202 | /// The animation was paused, but no longer. The duration is required for the 203 | /// offset of the animation. 204 | Resumed(Duration), 205 | } 206 | 207 | impl Pause { 208 | /// A conviniece function to check if an animation is playing. 209 | #[must_use] 210 | pub fn is_playing(&self) -> bool { 211 | !matches!(self, Pause::Paused(_)) 212 | } 213 | } 214 | 215 | /// A Cosmic Time internal type to make animation interpolation 216 | /// calculations more efficient. 217 | /// an intermediary type. This lets the timeline easily 218 | /// interpolate between keyframes. Keyframe implementations 219 | /// shouldn't have to know about this type. The Instant for this 220 | /// (and thus the keyframe itself) is applied with `start` 221 | #[derive(Debug, Clone)] 222 | pub struct SubFrame { 223 | /// The value, same as a [`Frame`] 224 | pub value: f32, 225 | /// The ease used to interpolate into this. 226 | pub ease: Ease, 227 | /// The Instant of this. Converted from duration in [`Frame`] 228 | pub at: Instant, 229 | } 230 | 231 | impl SubFrame { 232 | /// Creates a new `SubFrame`. 233 | #[must_use] 234 | pub fn new(at: Instant, value: f32, ease: Ease) -> Self { 235 | SubFrame { value, ease, at } 236 | } 237 | } 238 | 239 | // equal if instants are equal 240 | impl PartialEq for SubFrame { 241 | fn eq(&self, other: &Self) -> bool { 242 | self.at == other.at 243 | } 244 | } 245 | 246 | impl Eq for SubFrame {} 247 | 248 | // by default sort by time. 249 | impl Ord for SubFrame { 250 | fn cmp(&self, other: &Self) -> Ordering { 251 | self.at.cmp(&other.at) 252 | } 253 | } 254 | 255 | // by default sort by time. 256 | impl PartialOrd for SubFrame { 257 | fn partial_cmp(&self, other: &Self) -> Option { 258 | Some(self.at.cmp(&other.at)) 259 | } 260 | } 261 | 262 | /// Returned from [`Timeline::get`] 263 | /// Has all the data needed for simple animtions, 264 | /// and for style-like animations where some data 265 | /// is held in the widget, and not passed to [`Timeline`]. 266 | #[derive(Debug, Clone, Copy)] 267 | pub struct Interped { 268 | /// The previous ['Frame']'s value 269 | pub previous: f32, 270 | /// The nexy ['Frame']'s value 271 | pub next: f32, 272 | /// The interpolated value. 273 | pub value: f32, 274 | /// The percent done of this link in the chain. 275 | pub percent: f32, 276 | } 277 | 278 | impl Timeline { 279 | /// Creates a new [`Timeline`]. If you don't find this function you are going 280 | /// to have a bad time. 281 | #[must_use] 282 | pub fn new() -> Self { 283 | Timeline { 284 | tracks: HashMap::new(), 285 | pendings: HashMap::new(), 286 | now: None, 287 | } 288 | } 289 | 290 | /// If you accidently manage to `set_chain`, but then decide to undo that. 291 | /// If you need this there is probably a better way to re-write your code. 292 | pub fn remove_pending(&mut self) { 293 | self.pendings.clear(); 294 | } 295 | 296 | fn get_now(&self) -> Instant { 297 | match self.now { 298 | Some(now) => now, 299 | None => Instant::now(), 300 | } 301 | } 302 | 303 | /// Need to pause an animation? Use this! Pass the same widget Id 304 | /// used to create the chain. 305 | pub fn pause(&mut self, id: impl Into) -> &mut Self { 306 | let id = id.into(); 307 | let _ = self.pendings.insert(id, Pending::Pause); 308 | self 309 | } 310 | 311 | /// Need to resume an animation? Use this! Pass the same widget Id 312 | /// used to pause the chain. 313 | pub fn resume(&mut self, id: impl Into) -> &mut Self { 314 | let id = id.into(); 315 | let _ = self.pendings.insert(id, Pending::Resume); 316 | self 317 | } 318 | 319 | /// Hammer Time? Pause all animations with this. 320 | pub fn pause_all(&mut self) -> &mut Self { 321 | let _ = self 322 | .pendings 323 | .insert(widget::Id::unique(), Pending::PauseAll); 324 | self 325 | } 326 | 327 | /// Resume all animations. 328 | pub fn resume_all(&mut self) -> &mut Self { 329 | let _ = self 330 | .pendings 331 | .insert(widget::Id::unique(), Pending::ResumeAll); 332 | self 333 | } 334 | 335 | /// Add an animation chain to the timeline! 336 | /// Each animation Id is unique. It is imposible to use the same Id 337 | /// for two animations. 338 | pub fn set_chain(&mut self, chain: impl Into) -> &mut Self { 339 | self.set_chain_with_options(chain, Pause::NoPause) 340 | } 341 | 342 | /// Like `set_chain` but the animation will start paused on it's first frame. 343 | pub fn set_chain_paused(&mut self, chain: impl Into) -> &mut Self { 344 | self.set_chain_with_options(chain, Pause::Paused(Instant::now())) 345 | } 346 | 347 | fn set_chain_with_options(&mut self, chain: impl Into, pause: Pause) -> &mut Self { 348 | // TODO should be removed. Used iterators for pre-release 349 | // cosmic-time implementation. Keyframes should just pass a Vec> 350 | let chain = chain.into(); 351 | let id = chain.id; 352 | let repeat = chain.repeat; 353 | 354 | let _ = self 355 | .pendings 356 | .insert(id, Pending::Chain(repeat, chain.links, pause)); 357 | self 358 | } 359 | 360 | /// Remove's any animation. Usually not necessary, unless you may have 361 | /// a very large animation that needs to be "garage collected" when done. 362 | pub fn clear_chain(&mut self, id: impl Into) -> &mut Self { 363 | let id = id.into(); 364 | let _ = self.tracks.remove(&id); 365 | self 366 | } 367 | 368 | /// Use this in your `update()`. 369 | /// Updates the timeline's time so that animations can continue atomically. 370 | pub fn now(&mut self, now: Instant) { 371 | self.now = Some(now); 372 | } 373 | 374 | /// Starts all pending animations. 375 | pub fn start(&mut self) { 376 | self.start_at(Instant::now()); 377 | } 378 | 379 | /// Starts all pending animations at some other time that isn't now. 380 | pub fn start_at(&mut self, now: Instant) { 381 | let mut pendings = std::mem::take(&mut self.pendings); 382 | for (id, pending) in pendings.drain() { 383 | match pending { 384 | Pending::Chain(repeat, chain, pause) => { 385 | let mut end = now; 386 | // The time that the chain was `set_chain_paused` is not 387 | // necessaritly the same as the atomic pause time used here. 388 | // Fix that here. 389 | let pause = if let Pause::Paused(_instant) = pause { 390 | Pause::Paused(now) 391 | } else { 392 | pause 393 | }; 394 | 395 | let cols = chain[0].len(); 396 | let rows = chain.len(); 397 | let mut peekable = chain.into_iter().peekable(); 398 | let mut specific_chain = Vec::with_capacity(rows); 399 | while let Some(current) = peekable.next() { 400 | let time = end; 401 | if let Some(next) = peekable.peek() { 402 | let mut counter = 0; 403 | if let Some((c_frame, n_frame)) = 404 | current.iter().zip(next.iter()).find(|(c_frame, n_frame)| { 405 | counter += 1; 406 | c_frame.is_some() && n_frame.is_some() 407 | }) 408 | { 409 | let mut c = c_frame.expect("Previous check guarentees saftey"); 410 | let mut n = n_frame.expect("Previous check guarentees saftey"); 411 | c.to_eager(self, &id, counter - 1); 412 | n.to_eager(self, &id, counter - 1); 413 | let duration = n.get_duration(&c); 414 | end += duration; 415 | } 416 | } 417 | 418 | let specific_row = current.into_iter().enumerate().fold( 419 | Vec::with_capacity(cols), 420 | |mut acc, (i, maybe_frame)| { 421 | if let Some(mut frame) = maybe_frame { 422 | frame.to_eager(self, &id, i); 423 | acc.push(Some(frame.to_subframe(time))) 424 | } else { 425 | acc.push(None) 426 | } 427 | acc 428 | }, 429 | ); 430 | specific_chain.push(specific_row); 431 | } 432 | let transposed = specific_chain.into_iter().fold( 433 | vec![Vec::new(); cols], 434 | |mut acc: Vec>, row| { 435 | row.into_iter().enumerate().for_each(|(j, maybe_item)| { 436 | if let Some(item) = maybe_item { 437 | acc[j].push(item) 438 | } 439 | }); 440 | acc 441 | }, 442 | ); 443 | 444 | let meta = Meta::new(repeat, now, end, end - now, pause); 445 | let _ = self.tracks.insert(id, (meta, transposed)); 446 | } 447 | Pending::Pause => { 448 | if let Some((meta, _track)) = self.tracks.get_mut(&id) { 449 | meta.pause(now); 450 | } 451 | } 452 | Pending::Resume => { 453 | if let Some((meta, _track)) = self.tracks.get_mut(&id) { 454 | meta.resume(now); 455 | } 456 | } 457 | Pending::PauseAll => { 458 | for (meta, _track) in self.tracks.values_mut() { 459 | meta.pause(now); 460 | } 461 | } 462 | Pending::ResumeAll => { 463 | for (meta, _track) in self.tracks.values_mut() { 464 | meta.resume(now); 465 | } 466 | } 467 | } 468 | } 469 | self.now(now); 470 | } 471 | 472 | /// Get the [`Interped`] value for an animation. 473 | /// Use internaly by Cosmic Time. 474 | /// index is the index that the keyframe arbitratily assigns to each 475 | /// widget modifier (think width/height). 476 | #[must_use] 477 | pub fn get(&self, id: &widget::Id, index: usize) -> Option { 478 | let now = self.get_now(); 479 | // Get requested modifier_timeline or skip 480 | let (meta, mut modifier_timeline) = if let Some((meta, chain)) = self.tracks.get(id) { 481 | if let Some(modifier_timeline) = chain.get(index) { 482 | (meta, modifier_timeline.iter()) 483 | } else { 484 | return None; 485 | } 486 | } else { 487 | return None; 488 | }; 489 | 490 | let relative_now = match meta.pause { 491 | Pause::NoPause => relative_time(&now, meta), 492 | Pause::Resumed(delay) => relative_time(&now.checked_sub(delay).unwrap(), meta), 493 | Pause::Paused(time) => relative_time(&time, meta), 494 | }; 495 | 496 | // Loop through modifier_timeline, returning the interpolated value if possible. 497 | let mut accumulator: Option<&SubFrame> = None; 498 | loop { 499 | match (accumulator, modifier_timeline.next()) { 500 | // Found first element in timeline 501 | (None, Some(modifier)) => accumulator = Some(modifier), 502 | // No Elements in timeline 503 | (None, None) => return None, 504 | // Accumulator found in previous loop, but no greater value. Means animation duration has expired. 505 | (Some(acc), None) => { 506 | return Some(Interped { 507 | previous: acc.value, 508 | next: acc.value, 509 | percent: 1.0, 510 | value: acc.value, 511 | }); 512 | } 513 | // Found accumulator in middle-ish of timeline 514 | (Some(acc), Some(modifier)) => { 515 | // Can not interpolate between this one and next value? 516 | if relative_now >= modifier.at || acc.value == modifier.value { 517 | accumulator = Some(modifier); 518 | // Can interpolate between these two, thus calculate and return that value. 519 | } else { 520 | let elapsed = relative_now.duration_since(acc.at).as_millis() as f32; 521 | let duration = (modifier.at - acc.at).as_millis() as f32; 522 | 523 | let previous = acc.value; 524 | let next = modifier.value; 525 | let percent = modifier.ease.tween(elapsed / duration); 526 | let value = lerp( 527 | acc.value, 528 | modifier.value, 529 | modifier.ease.tween(elapsed / duration), 530 | ); 531 | 532 | return Some(Interped { 533 | previous, 534 | next, 535 | value, 536 | percent, 537 | }); 538 | } 539 | } 540 | } 541 | } 542 | } 543 | 544 | /// Check if the timeline is idle 545 | /// The timeline is considered idle if all animations meet 546 | /// one of the final criteria: 547 | /// 1. Played until completion 548 | /// 2. Paused 549 | /// 3. Does not loop forever 550 | #[must_use] 551 | pub fn is_idle(&self) -> bool { 552 | let now = self.now; 553 | !(now.is_some() 554 | && self.tracks.values().any(|track| { 555 | (track.0.repeat == Repeat::Forever && track.0.pause.is_playing()) 556 | || (track.0.end >= now.unwrap() && track.0.pause.is_playing()) 557 | })) 558 | } 559 | 560 | // /// Efficiently request redraws for animations. 561 | // /// Automatically checks if animations are in a state where redraws arn't necessary. 562 | // #[cfg(not(feature = "libcosmic"))] 563 | // pub fn as_subscription(&self) -> Subscription { 564 | // if self.is_idle() { 565 | // Subscription::none() 566 | // } else { 567 | // iced::window::frames() 568 | // } 569 | // } 570 | 571 | /// Efficiently request redraws for animations. 572 | /// Automatically checks if animations are in a state where redraws arn't necessary. 573 | pub fn as_subscription(&self) -> Subscription<(cosmic::iced::window::Id, Instant)> { 574 | if self.is_idle() { 575 | Subscription::none() 576 | } else { 577 | cosmic::iced_runtime::window::frames() // ~120FPS 578 | } 579 | } 580 | } 581 | 582 | // Used for animations that loop. 583 | // Given the current `Instant`, it returns the relative instant in the animation that 584 | // corresponds with the first loop of the animation. 585 | fn relative_time(now: &Instant, meta: &Meta) -> Instant { 586 | if meta.repeat == Repeat::Never { 587 | *now 588 | } else { 589 | let repeat_num = (*now - meta.start).as_millis() / meta.length.as_millis(); 590 | let reduce_by = repeat_num * meta.length.as_millis(); 591 | now.checked_sub(Duration::from_millis( 592 | reduce_by.clamp(0, u64::MAX.into()).try_into().unwrap(), 593 | )) 594 | .expect("Your animatiion has been runnning for 5.84x10^6 centuries.") 595 | } 596 | } 597 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MPIT 2 | 3 | //! Utility functions for handling data in this library. 4 | 5 | use crate::reexports::iced_core::{ 6 | layout::{Limits, Node}, 7 | Point, Size, 8 | }; 9 | 10 | /// Collect iterator into static array without panicking or collecting into a Vec. 11 | /// 12 | /// Initializes with `T::default()`, then takes `SIZE` values from the iterator. 13 | pub fn static_array_from_iter( 14 | iter: impl Iterator, 15 | ) -> [T; SIZE] { 16 | let mut array = [T::default(); SIZE]; 17 | 18 | for (id, value) in iter.take(SIZE).enumerate() { 19 | array[id] = value; 20 | } 21 | 22 | array 23 | } 24 | 25 | /// Produces a [`Node`] with two children nodes one right next to each other. 26 | pub fn next_to_each_other( 27 | limits: &Limits, 28 | spacing: f32, 29 | left: impl FnOnce(&Limits) -> Node, 30 | right: impl FnOnce(&Limits) -> Node, 31 | ) -> Node { 32 | let mut right_node = right(limits); 33 | let right_size = right_node.size(); 34 | 35 | let left_limits = limits.shrink(Size::new(right_size.width + spacing, 0.0)); 36 | let mut left_node = left(&left_limits); 37 | let left_size = left_node.size(); 38 | 39 | let (left_y, right_y) = if left_size.height > right_size.height { 40 | (0.0, (left_size.height - right_size.height) / 2.0) 41 | } else { 42 | ((right_size.height - left_size.height) / 2.0, 0.0) 43 | }; 44 | 45 | left_node = left_node.move_to(Point::new(0.0, left_y)); 46 | right_node = right_node.move_to(Point::new(left_size.width + spacing, right_y)); 47 | 48 | Node::with_children( 49 | Size::new( 50 | left_size.width + spacing + right_size.width, 51 | left_size.height.max(right_size.height), 52 | ), 53 | vec![left_node, right_node], 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/widget.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::too_many_arguments)] 2 | pub mod cards; 3 | pub mod cosmic_toggler; 4 | 5 | pub use cards::Cards; 6 | pub use cosmic_toggler::Toggler; 7 | 8 | /// A convenience type to optimize style-able widgets, 9 | /// to only do the "expensize" style calculations if needed. 10 | #[derive(Debug)] 11 | pub enum StyleType { 12 | /// The style is either default, or set manually in the `view`. 13 | Static(T), 14 | /// The stlye is being animated. Blend between the two values. 15 | Blend(T, T, f32), 16 | } 17 | -------------------------------------------------------------------------------- /src/widget/cards.rs: -------------------------------------------------------------------------------- 1 | //! An expandable stack of cards 2 | use self::iced_core::{ 3 | border::Radius, event::Status, id::Id, layout::Node, renderer::Quad, widget::Tree, Element, 4 | Length, Size, Vector, Widget, 5 | }; 6 | use cosmic::widget::style::Catalog; 7 | use cosmic::{ 8 | iced_core::{self, Border, Shadow}, 9 | widget::{button, card::style::Style, column, icon, icon::Handle, row, text}, 10 | }; 11 | use float_cmp::approx_eq; 12 | 13 | use crate::{chain, id}; 14 | 15 | const ICON_SIZE: u16 = 16; 16 | const TOP_SPACING: u16 = 4; 17 | const VERTICAL_SPACING: f32 = 8.0; 18 | const PADDING: u16 = 16; 19 | const BG_CARD_VISIBLE_HEIGHT: f32 = 4.0; 20 | const BG_CARD_BORDER_RADIUS: f32 = 8.0; 21 | const BG_CARD_MARGIN_STEP: f32 = 8.0; 22 | 23 | /// get an expandable stack of cards 24 | #[allow(clippy::too_many_arguments)] 25 | pub fn cards<'a, Message, F, G>( 26 | id: id::Cards, 27 | card_inner_elements: Vec>, 28 | on_clear_all: Message, 29 | on_show_more: Option, 30 | on_activate: Option, 31 | show_more_label: &'a str, 32 | show_less_label: &'a str, 33 | clear_all_label: &'a str, 34 | show_less_icon: Option, 35 | expanded: bool, 36 | ) -> Cards<'a, Message, cosmic::Renderer> 37 | where 38 | Message: 'static + Clone, 39 | F: 'a + Fn(chain::Cards, bool) -> Message, 40 | G: 'a + Fn(usize) -> Message, 41 | { 42 | Cards::new( 43 | id, 44 | card_inner_elements, 45 | on_clear_all, 46 | on_show_more, 47 | on_activate, 48 | show_more_label, 49 | show_less_label, 50 | clear_all_label, 51 | show_less_icon, 52 | expanded, 53 | ) 54 | } 55 | 56 | impl<'a, Message, Renderer> Cards<'a, Message, Renderer> 57 | where 58 | Renderer: iced_core::text::Renderer, 59 | { 60 | fn fully_expanded(&self) -> bool { 61 | self.expanded 62 | && self.elements.len() > 1 63 | && self.can_show_more 64 | && approx_eq!(f32, self.percent, 1.0) 65 | } 66 | 67 | fn fully_unexpanded(&self) -> bool { 68 | self.elements.len() == 1 69 | || (!self.expanded && (!self.can_show_more || approx_eq!(f32, self.percent, 0.0))) 70 | } 71 | } 72 | 73 | /// An expandable stack of cards. 74 | #[allow(missing_debug_implementations)] 75 | pub struct Cards<'a, Message, Renderer = cosmic::Renderer> 76 | where 77 | Renderer: iced_core::text::Renderer, 78 | { 79 | _id: Id, 80 | show_less_button: Element<'a, Message, cosmic::Theme, Renderer>, 81 | clear_all_button: Element<'a, Message, cosmic::Theme, Renderer>, 82 | elements: Vec>, 83 | expanded: bool, 84 | can_show_more: bool, 85 | width: Length, 86 | percent: f32, 87 | anim_multiplier: f32, 88 | } 89 | 90 | impl<'a, Message> Cards<'a, Message, cosmic::Renderer> 91 | where 92 | Message: Clone + 'static, 93 | { 94 | /// Get an expandable stack of cards 95 | #[allow(clippy::too_many_arguments)] 96 | pub fn new( 97 | id: id::Cards, 98 | card_inner_elements: Vec>, 99 | on_clear_all: Message, 100 | on_show_more: Option, 101 | on_activate: Option, 102 | show_more_label: &'a str, 103 | show_less_label: &'a str, 104 | clear_all_label: &'a str, 105 | show_less_icon: Option, 106 | expanded: bool, 107 | ) -> Self 108 | where 109 | F: 'a + Fn(chain::Cards, bool) -> Message, 110 | G: 'a + Fn(usize) -> Message, 111 | { 112 | let can_show_more = card_inner_elements.len() > 1 && on_show_more.is_some(); 113 | 114 | Self { 115 | can_show_more, 116 | _id: Id::unique(), 117 | show_less_button: { 118 | let mut show_less_children = Vec::with_capacity(3); 119 | if let Some(source) = show_less_icon { 120 | show_less_children.push(icon(source).size(ICON_SIZE).into()); 121 | } 122 | show_less_children.push(text::body(show_less_label).width(Length::Shrink).into()); 123 | show_less_children.push( 124 | icon::from_name("pan-up-symbolic") 125 | .size(ICON_SIZE) 126 | .icon() 127 | .into(), 128 | ); 129 | let off_animation = chain::Cards::off(id.clone(), 1.0); 130 | 131 | let button_content = row::with_children(show_less_children) 132 | .align_y(iced_core::Alignment::Center) 133 | .spacing(TOP_SPACING) 134 | .width(Length::Shrink); 135 | 136 | Element::from( 137 | button::custom(button_content) 138 | .class(cosmic::theme::Button::Text) 139 | .width(Length::Shrink) 140 | .on_press_maybe(on_show_more.as_ref().map(|f| f(off_animation, false))) 141 | .padding([PADDING / 2, PADDING]), 142 | ) 143 | }, 144 | clear_all_button: Element::from( 145 | button::custom(text(clear_all_label)) 146 | .class(cosmic::theme::Button::Text) 147 | .width(Length::Shrink) 148 | .on_press(on_clear_all) 149 | .padding([PADDING / 2, PADDING]), 150 | ), 151 | elements: card_inner_elements 152 | .into_iter() 153 | .enumerate() 154 | .map(|(i, w)| { 155 | let custom_content = if i == 0 && !expanded && can_show_more { 156 | column::with_capacity(2) 157 | .push(w) 158 | .push(text::caption(show_more_label)) 159 | .spacing(VERTICAL_SPACING) 160 | .align_x(iced_core::Alignment::Center) 161 | .into() 162 | } else { 163 | w 164 | }; 165 | 166 | let b = cosmic::iced::widget::button(custom_content) 167 | .class(cosmic::theme::iced::Button::Card) 168 | .padding(PADDING); 169 | if i == 0 && !expanded && can_show_more { 170 | let on_animation = chain::Cards::on(id.clone(), 1.0); 171 | b.on_press_maybe(on_show_more.as_ref().map(|f| f(on_animation, true))) 172 | } else { 173 | b.on_press_maybe(on_activate.as_ref().map(|f| f(i))) 174 | } 175 | .into() 176 | }) 177 | // we will set the width of the container to shrink, then when laying out the top bar 178 | // we will set the fill limit to the max of the shrink top bar width and the max shrink width of the 179 | // cards 180 | .collect(), 181 | width: Length::Shrink, 182 | percent: if expanded { 1.0 } else { 0.0 }, 183 | anim_multiplier: 1.0, 184 | expanded, 185 | } 186 | } 187 | 188 | /// Set the width of the cards stack 189 | #[must_use] 190 | pub fn width(mut self, width: Length) -> Self { 191 | self.width = width; 192 | self 193 | } 194 | 195 | #[must_use] 196 | /// The percent completion of the card stack animation. 197 | /// This is indented to automated cosmic-time use, and shouldn't 198 | /// need to be called manually. 199 | pub fn percent(mut self, percent: f32) -> Self { 200 | self.percent = percent; 201 | self 202 | } 203 | 204 | #[must_use] 205 | /// The default animation time is 100ms, to speed up the toggle 206 | /// animation use a value less than 1.0, and to slow down the 207 | /// animation use a value greater than 1.0. 208 | pub fn anim_multiplier(mut self, multiplier: f32) -> Self { 209 | self.anim_multiplier = multiplier; 210 | self 211 | } 212 | } 213 | 214 | impl<'a, Message, Renderer> Widget 215 | for Cards<'a, Message, Renderer> 216 | where 217 | Message: 'a + Clone, 218 | Renderer: 'a + iced_core::Renderer + iced_core::text::Renderer, 219 | { 220 | fn children(&self) -> Vec { 221 | [&self.show_less_button, &self.clear_all_button] 222 | .iter() 223 | .map(|w| Tree::new(w.as_widget())) 224 | .chain(self.elements.iter().map(|w| Tree::new(w.as_widget()))) 225 | .collect() 226 | } 227 | 228 | fn diff(&mut self, tree: &mut Tree) { 229 | let mut children: Vec<_> = vec![ 230 | self.show_less_button.as_widget_mut(), 231 | self.clear_all_button.as_widget_mut(), 232 | ] 233 | .into_iter() 234 | .chain( 235 | self.elements 236 | .iter_mut() 237 | .map(iced_core::Element::as_widget_mut), 238 | ) 239 | .collect(); 240 | 241 | tree.diff_children(children.as_mut_slice()); 242 | } 243 | 244 | #[allow(clippy::too_many_lines)] 245 | fn layout( 246 | &self, 247 | tree: &mut Tree, 248 | renderer: &Renderer, 249 | limits: &iced_core::layout::Limits, 250 | ) -> iced_core::layout::Node { 251 | let mut children = Vec::with_capacity(1 + self.elements.len()); 252 | let mut size = Size::new(0.0, 0.0); 253 | let tree_children = &mut tree.children; 254 | if self.elements.is_empty() { 255 | return Node::with_children(Size::new(1., 1.), children); 256 | } 257 | 258 | let fully_expanded: bool = self.fully_expanded(); 259 | let fully_unexpanded: bool = self.fully_unexpanded(); 260 | 261 | let show_less = &self.show_less_button; 262 | let clear_all = &self.clear_all_button; 263 | 264 | let show_less_node = if self.can_show_more { 265 | show_less 266 | .as_widget() 267 | .layout(&mut tree_children[0], renderer, limits) 268 | } else { 269 | Node::new(Size::default()) 270 | }; 271 | let clear_all_node = clear_all 272 | .as_widget() 273 | .layout(&mut tree_children[1], renderer, limits); 274 | size.width += show_less_node.size().width + clear_all_node.size().width; 275 | 276 | let custom_limits = limits.min_width(size.width); 277 | for (c, t) in self.elements.iter().zip(tree_children[2..].iter_mut()) { 278 | let card_node = c.as_widget().layout(t, renderer, &custom_limits); 279 | size.width = size.width.max(card_node.size().width); 280 | } 281 | 282 | if fully_expanded { 283 | let show_less = &self.show_less_button; 284 | let clear_all = &self.clear_all_button; 285 | 286 | let show_less_node = if self.can_show_more { 287 | show_less 288 | .as_widget() 289 | .layout(&mut tree_children[0], renderer, limits) 290 | } else { 291 | Node::new(Size::default()) 292 | }; 293 | let clear_all_node = if self.can_show_more { 294 | let mut n = clear_all 295 | .as_widget() 296 | .layout(&mut tree_children[1], renderer, limits); 297 | let clear_all_node_size = n.size(); 298 | n = clear_all_node 299 | .translate(Vector::new(size.width - clear_all_node_size.width, 0.0)); 300 | size.height += show_less_node.size().height.max(n.size().height) + VERTICAL_SPACING; 301 | n 302 | } else { 303 | Node::new(Size::default()) 304 | }; 305 | 306 | children.push(show_less_node); 307 | children.push(clear_all_node); 308 | } 309 | 310 | let custom_limits = limits 311 | .min_width(size.width) 312 | .max_width(size.width) 313 | .width(Length::Fixed(size.width)); 314 | 315 | for (i, (c, t)) in self 316 | .elements 317 | .iter() 318 | .zip(tree_children[2..].iter_mut()) 319 | .enumerate() 320 | { 321 | let progress = self.percent * size.height; 322 | let card_node = c 323 | .as_widget() 324 | .layout(t, renderer, &custom_limits) 325 | .translate(Vector::new(0.0, progress)); 326 | 327 | size.height = size.height.max(progress + card_node.size().height); 328 | 329 | children.push(card_node); 330 | 331 | if fully_unexpanded { 332 | let width = children.last().unwrap().bounds().width; 333 | 334 | // push the background card nodes 335 | for i in 1..self.elements.len().min(3) { 336 | // height must be 16px for 8px padding 337 | // but we only want 4px visible 338 | 339 | let margin = f32::from(u8::try_from(i).unwrap()) * BG_CARD_MARGIN_STEP; 340 | let node = 341 | Node::new(Size::new(width - 2.0 * margin, BG_CARD_BORDER_RADIUS * 2.0)) 342 | .translate(Vector::new( 343 | margin, 344 | size.height - BG_CARD_BORDER_RADIUS * 2.0 + BG_CARD_VISIBLE_HEIGHT, 345 | )); 346 | size.height += BG_CARD_VISIBLE_HEIGHT; 347 | children.push(node); 348 | } 349 | break; 350 | } 351 | 352 | if i + 1 < self.elements.len() { 353 | size.height += VERTICAL_SPACING; 354 | } 355 | } 356 | 357 | Node::with_children(size, children) 358 | } 359 | 360 | fn draw( 361 | &self, 362 | state: &iced_core::widget::Tree, 363 | renderer: &mut Renderer, 364 | theme: &cosmic::Theme, 365 | style: &iced_core::renderer::Style, 366 | layout: iced_core::Layout<'_>, 367 | cursor: iced_core::mouse::Cursor, 368 | viewport: &iced_core::Rectangle, 369 | ) { 370 | // there are 4 cases for drawing 371 | // 1. empty entries list 372 | // Nothing to draw 373 | // 2. un-expanded 374 | // go through the layout, draw the card, the inner card, and the bg cards 375 | // 3. expanding / unexpanding 376 | // go through the layout. draw each card and its inner card 377 | // 4. expanded => 378 | // go through the layout. draw the top bar, and do all of 3 379 | // cards may be hovered 380 | // any buttons may have a hover state as well 381 | if self.elements.is_empty() { 382 | return; 383 | } 384 | 385 | let fully_unexpanded = self.fully_unexpanded(); 386 | let fully_expanded = self.fully_expanded(); 387 | 388 | let mut layout = layout.children(); 389 | let mut tree_children = state.children.iter(); 390 | 391 | if fully_expanded { 392 | let show_less = &self.show_less_button; 393 | let clear_all = &self.clear_all_button; 394 | 395 | let show_less_layout = layout.next().unwrap(); 396 | let clear_all_layout = layout.next().unwrap(); 397 | 398 | show_less.as_widget().draw( 399 | tree_children.next().unwrap(), 400 | renderer, 401 | theme, 402 | style, 403 | show_less_layout, 404 | cursor, 405 | viewport, 406 | ); 407 | 408 | clear_all.as_widget().draw( 409 | tree_children.next().unwrap(), 410 | renderer, 411 | theme, 412 | style, 413 | clear_all_layout, 414 | cursor, 415 | viewport, 416 | ); 417 | } else { 418 | _ = tree_children.next(); 419 | _ = tree_children.next(); 420 | } 421 | 422 | // Draw first to appear behind 423 | if fully_unexpanded { 424 | let card_layout = layout.next().unwrap(); 425 | let appearance = theme.default(); 426 | let bg_layout = layout.collect::>(); 427 | for (i, layout) in (0..2).zip(bg_layout.into_iter()).rev() { 428 | renderer.fill_quad( 429 | Quad { 430 | bounds: layout.bounds(), 431 | border: Border { 432 | radius: Radius::from([ 433 | 0.0, 434 | 0.0, 435 | BG_CARD_BORDER_RADIUS, 436 | BG_CARD_BORDER_RADIUS, 437 | ]), 438 | ..Default::default() 439 | }, 440 | shadow: Shadow::default(), 441 | }, 442 | if i == 0 { 443 | appearance.card_1 444 | } else { 445 | appearance.card_2 446 | }, 447 | ); 448 | } 449 | self.elements[0].as_widget().draw( 450 | tree_children.next().unwrap(), 451 | renderer, 452 | theme, 453 | style, 454 | card_layout, 455 | cursor, 456 | viewport, 457 | ); 458 | } else { 459 | let layout = layout.collect::>(); 460 | // draw in reverse order so later cards appear behind earlier cards 461 | for ((inner, layout), c_state) in self 462 | .elements 463 | .iter() 464 | .rev() 465 | .zip(layout.into_iter().rev()) 466 | .zip(tree_children.rev()) 467 | { 468 | inner 469 | .as_widget() 470 | .draw(c_state, renderer, theme, style, layout, cursor, viewport); 471 | } 472 | } 473 | } 474 | 475 | fn on_event( 476 | &mut self, 477 | state: &mut Tree, 478 | event: iced_core::Event, 479 | layout: iced_core::Layout<'_>, 480 | cursor: iced_core::mouse::Cursor, 481 | renderer: &Renderer, 482 | clipboard: &mut dyn iced_core::Clipboard, 483 | shell: &mut iced_core::Shell<'_, Message>, 484 | viewport: &iced_core::Rectangle, 485 | ) -> iced_core::event::Status { 486 | let mut status = iced_core::event::Status::Ignored; 487 | 488 | if self.elements.is_empty() { 489 | return status; 490 | } 491 | 492 | let mut layout = layout.children(); 493 | let mut tree_children = state.children.iter_mut(); 494 | let fully_expanded = self.fully_expanded(); 495 | let fully_unexpanded = self.fully_unexpanded(); 496 | let show_less_state = tree_children.next(); 497 | let clear_all_state = tree_children.next(); 498 | 499 | if fully_expanded { 500 | let c_layout = layout.next().unwrap(); 501 | let state = show_less_state.unwrap(); 502 | status = status.merge(self.show_less_button.as_widget_mut().on_event( 503 | state, 504 | event.clone(), 505 | c_layout, 506 | cursor, 507 | renderer, 508 | clipboard, 509 | shell, 510 | viewport, 511 | )); 512 | 513 | if status == Status::Captured { 514 | return status; 515 | } 516 | 517 | let c_layout = layout.next().unwrap(); 518 | let state = clear_all_state.unwrap(); 519 | status = status.merge(self.clear_all_button.as_widget_mut().on_event( 520 | state, 521 | event.clone(), 522 | c_layout, 523 | cursor, 524 | renderer, 525 | clipboard, 526 | shell, 527 | viewport, 528 | )); 529 | } 530 | 531 | if status == Status::Captured { 532 | return status; 533 | } 534 | 535 | for ((inner, layout), c_state) in self.elements.iter_mut().zip(layout).zip(tree_children) { 536 | status = status.merge(inner.as_widget_mut().on_event( 537 | c_state, 538 | event.clone(), 539 | layout, 540 | cursor, 541 | renderer, 542 | clipboard, 543 | shell, 544 | viewport, 545 | )); 546 | if status == Status::Captured || fully_unexpanded { 547 | break; 548 | } 549 | } 550 | 551 | status 552 | } 553 | 554 | fn size(&self) -> Size { 555 | Size::new(self.width, Length::Shrink) 556 | } 557 | } 558 | 559 | impl<'a, Message> From> for Element<'a, Message, cosmic::Theme, cosmic::Renderer> 560 | where 561 | Message: Clone + 'a, 562 | { 563 | fn from(cards: Cards<'a, Message>) -> Self { 564 | Self::new(cards) 565 | } 566 | } 567 | -------------------------------------------------------------------------------- /src/widget/cosmic_toggler.rs: -------------------------------------------------------------------------------- 1 | //! Show toggle controls using togglers. 2 | 3 | use cosmic::{iced_core::Border, iced_widget::toggler::Status}; 4 | use iced_core::{ 5 | alignment, event, layout, mouse, renderer, text, 6 | widget::{self, tree, Tree}, 7 | Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, 8 | }; 9 | 10 | use crate::{ 11 | chain, id, lerp, 12 | reexports::{iced, iced_core, iced_widget}, 13 | }; 14 | pub use cosmic::iced_widget::toggler::{Catalog, Style}; 15 | 16 | /// A toggler widget. 17 | #[allow(missing_debug_implementations)] 18 | pub struct Toggler<'a, Message, Renderer> 19 | where 20 | Renderer: text::Renderer, 21 | { 22 | id: id::Toggler, 23 | is_toggled: bool, 24 | on_toggle: Box Message + 'a>, 25 | label: Option, 26 | width: Length, 27 | size: f32, 28 | text_size: Option, 29 | text_line_height: text::LineHeight, 30 | text_alignment: alignment::Horizontal, 31 | text_shaping: text::Shaping, 32 | spacing: f32, 33 | font: Option, 34 | percent: f32, 35 | anim_multiplier: f32, 36 | } 37 | 38 | impl<'a, Message, Renderer> Toggler<'a, Message, Renderer> 39 | where 40 | Renderer: text::Renderer, 41 | { 42 | /// The default size of a [`Toggler`]. 43 | pub const DEFAULT_SIZE: f32 = 24.0; 44 | 45 | /// Creates a new [`Toggler`]. 46 | /// 47 | /// It expects: 48 | /// * a boolean describing whether the [`Toggler`] is checked or not 49 | /// * An optional label for the [`Toggler`] 50 | /// * a function that will be called when the [`Toggler`] is toggled. It 51 | /// will receive the new state of the [`Toggler`] and must produce a 52 | /// `Message`. 53 | pub fn new(id: id::Toggler, label: impl Into>, is_toggled: bool, f: F) -> Self 54 | where 55 | F: 'a + Fn(chain::Toggler, bool) -> Message, 56 | { 57 | Toggler { 58 | id, 59 | is_toggled, 60 | on_toggle: Box::new(f), 61 | label: label.into(), 62 | width: Length::Fill, 63 | size: Self::DEFAULT_SIZE, 64 | text_size: None, 65 | text_line_height: text::LineHeight::default(), 66 | text_alignment: alignment::Horizontal::Left, 67 | text_shaping: text::Shaping::Advanced, 68 | spacing: 0.0, 69 | font: None, 70 | percent: if is_toggled { 1.0 } else { 0.0 }, 71 | anim_multiplier: 1.0, 72 | } 73 | } 74 | 75 | /// Sets the size of the [`Toggler`]. 76 | pub fn size(mut self, size: impl Into) -> Self { 77 | self.size = size.into().0; 78 | self 79 | } 80 | 81 | /// Sets the width of the [`Toggler`]. 82 | pub fn width(mut self, width: impl Into) -> Self { 83 | self.width = width.into(); 84 | self 85 | } 86 | 87 | /// Sets the text size o the [`Toggler`]. 88 | pub fn text_size(mut self, text_size: impl Into) -> Self { 89 | self.text_size = Some(text_size.into().0); 90 | self 91 | } 92 | 93 | /// Sets the text [`LineHeight`] of the [`Toggler`]. 94 | pub fn text_line_height(mut self, line_height: impl Into) -> Self { 95 | self.text_line_height = line_height.into(); 96 | self 97 | } 98 | 99 | /// Sets the horizontal alignment of the text of the [`Toggler`] 100 | pub fn text_alignment(mut self, alignment: alignment::Horizontal) -> Self { 101 | self.text_alignment = alignment; 102 | self 103 | } 104 | 105 | /// Sets the [`text::Shaping`] strategy of the [`Toggler`]. 106 | pub fn text_shaping(mut self, shaping: text::Shaping) -> Self { 107 | self.text_shaping = shaping; 108 | self 109 | } 110 | 111 | /// Sets the spacing between the [`Toggler`] and the text. 112 | pub fn spacing(mut self, spacing: impl Into) -> Self { 113 | self.spacing = spacing.into().0; 114 | self 115 | } 116 | 117 | /// Sets the [`Font`] of the text of the [`Toggler`] 118 | /// 119 | /// [`Font`]: cosmic::iced::text::Renderer::Font 120 | pub fn font(mut self, font: impl Into) -> Self { 121 | self.font = Some(font.into()); 122 | self 123 | } 124 | 125 | /// The percent completion of the toggler animation. 126 | /// This is indented to automated cosmic-time use, and shouldn't 127 | /// need to be called manually. 128 | pub fn percent(mut self, percent: f32) -> Self { 129 | self.percent = percent; 130 | self 131 | } 132 | } 133 | 134 | impl<'a, Message, Renderer> Widget 135 | for Toggler<'a, Message, Renderer> 136 | where 137 | Renderer: text::Renderer, 138 | { 139 | fn size(&self) -> Size { 140 | Size::new(self.width, Length::Shrink) 141 | } 142 | 143 | fn state(&self) -> tree::State { 144 | tree::State::new(widget::text::State::::default()) 145 | } 146 | 147 | fn layout( 148 | &self, 149 | tree: &mut Tree, 150 | renderer: &Renderer, 151 | limits: &layout::Limits, 152 | ) -> layout::Node { 153 | let limits = limits.width(self.width); 154 | 155 | crate::utils::next_to_each_other( 156 | &limits, 157 | self.spacing, 158 | |limits| { 159 | if let Some(label) = self.label.as_deref() { 160 | let state = tree 161 | .state 162 | .downcast_mut::>(); 163 | 164 | let node = iced_core::widget::text::layout( 165 | state, 166 | renderer, 167 | limits, 168 | self.width, 169 | Length::Shrink, 170 | label, 171 | self.text_line_height, 172 | self.text_size.map(iced::Pixels), 173 | self.font, 174 | self.text_alignment, 175 | alignment::Vertical::Top, 176 | self.text_shaping, 177 | cosmic::iced_core::text::Wrapping::default(), 178 | ); 179 | match self.width { 180 | Length::Fill => { 181 | let size = node.size(); 182 | layout::Node::with_children( 183 | Size::new(limits.width(Length::Fill).max().width, size.height), 184 | vec![node], 185 | ) 186 | } 187 | _ => node, 188 | } 189 | } else { 190 | layout::Node::new(iced_core::Size::ZERO) 191 | } 192 | }, 193 | |_| layout::Node::new(Size::new(48., 24.)), 194 | ) 195 | } 196 | 197 | fn on_event( 198 | &mut self, 199 | _state: &mut Tree, 200 | event: Event, 201 | layout: Layout<'_>, 202 | cursor_position: mouse::Cursor, 203 | _renderer: &Renderer, 204 | _clipboard: &mut dyn Clipboard, 205 | shell: &mut Shell<'_, Message>, 206 | _viewport: &Rectangle, 207 | ) -> event::Status { 208 | match event { 209 | Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { 210 | let mouse_over = cursor_position.is_over(layout.bounds()); 211 | 212 | if mouse_over { 213 | if self.is_toggled { 214 | let off_animation = 215 | chain::Toggler::off(self.id.clone(), self.anim_multiplier); 216 | shell.publish((self.on_toggle)(off_animation, !self.is_toggled)); 217 | } else { 218 | let on_animation = 219 | chain::Toggler::on(self.id.clone(), self.anim_multiplier); 220 | shell.publish((self.on_toggle)(on_animation, !self.is_toggled)); 221 | } 222 | 223 | event::Status::Captured 224 | } else { 225 | event::Status::Ignored 226 | } 227 | } 228 | _ => event::Status::Ignored, 229 | } 230 | } 231 | 232 | fn mouse_interaction( 233 | &self, 234 | _state: &Tree, 235 | layout: Layout<'_>, 236 | cursor_position: mouse::Cursor, 237 | _viewport: &Rectangle, 238 | _renderer: &Renderer, 239 | ) -> mouse::Interaction { 240 | if cursor_position.is_over(layout.bounds()) { 241 | mouse::Interaction::Pointer 242 | } else { 243 | mouse::Interaction::default() 244 | } 245 | } 246 | 247 | fn draw( 248 | &self, 249 | tree: &Tree, 250 | renderer: &mut Renderer, 251 | theme: &cosmic::Theme, 252 | style: &renderer::Style, 253 | layout: Layout<'_>, 254 | cursor_position: mouse::Cursor, 255 | viewport: &Rectangle, 256 | ) { 257 | let mut children = layout.children(); 258 | 259 | if let Some(_label) = &self.label { 260 | let label_layout = children.next().unwrap(); 261 | let state: &iced_widget::text::State = tree.state.downcast_ref(); 262 | iced_widget::text::draw( 263 | renderer, 264 | style, 265 | label_layout, 266 | state.0.raw(), 267 | iced_widget::text::Style::default(), 268 | viewport, 269 | ); 270 | } 271 | 272 | let toggler_layout = children.next().unwrap(); 273 | let bounds = toggler_layout.bounds(); 274 | 275 | let is_mouse_over = cursor_position.is_over(bounds); 276 | 277 | let style = blend_appearances( 278 | theme.style( 279 | &(), 280 | if is_mouse_over { 281 | Status::Hovered { is_toggled: false } 282 | } else { 283 | Status::Active { is_toggled: false } 284 | }, 285 | ), 286 | theme.style( 287 | &(), 288 | if is_mouse_over { 289 | Status::Hovered { is_toggled: true } 290 | } else { 291 | Status::Active { is_toggled: true } 292 | }, 293 | ), 294 | self.percent, 295 | ); 296 | 297 | let space = style.handle_margin; 298 | 299 | let toggler_background_bounds = Rectangle { 300 | x: bounds.x, 301 | y: bounds.y, 302 | width: bounds.width, 303 | height: bounds.height, 304 | }; 305 | 306 | renderer.fill_quad( 307 | renderer::Quad { 308 | bounds: toggler_background_bounds, 309 | border: Border { 310 | radius: style.border_radius, 311 | ..Default::default() 312 | }, 313 | ..renderer::Quad::default() 314 | }, 315 | style.background, 316 | ); 317 | 318 | let toggler_foreground_bounds = Rectangle { 319 | x: bounds.x 320 | + lerp( 321 | space, 322 | bounds.width - space - (bounds.height - (2.0 * space)), 323 | self.percent, 324 | ), 325 | 326 | y: bounds.y + space, 327 | width: bounds.height - (2.0 * space), 328 | height: bounds.height - (2.0 * space), 329 | }; 330 | 331 | renderer.fill_quad( 332 | renderer::Quad { 333 | bounds: toggler_foreground_bounds, 334 | border: Border { 335 | radius: style.handle_radius, 336 | ..Default::default() 337 | }, 338 | ..renderer::Quad::default() 339 | }, 340 | style.foreground, 341 | ); 342 | } 343 | } 344 | 345 | impl<'a, Message, Renderer> From> 346 | for Element<'a, Message, cosmic::Theme, Renderer> 347 | where 348 | Message: 'a, 349 | Renderer: 'a + text::Renderer, 350 | { 351 | fn from( 352 | toggler: Toggler<'a, Message, Renderer>, 353 | ) -> Element<'a, Message, cosmic::Theme, Renderer> { 354 | Element::new(toggler) 355 | } 356 | } 357 | 358 | fn blend_appearances(first: Style, mut other: Style, percent: f32) -> Style { 359 | if percent == 0. { 360 | first 361 | } else if percent == 1. { 362 | other 363 | } else { 364 | let first_background = first.background.into_linear(); 365 | 366 | let other_background = std::mem::take(&mut other.background).into_linear(); 367 | 368 | other.background = crate::utils::static_array_from_iter::( 369 | first_background 370 | .iter() 371 | .zip(other_background.iter()) 372 | .map(|(o, t)| o * (1.0 - percent) + t * percent), 373 | ) 374 | .into(); 375 | 376 | other 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /src/widget/toggler.rs: -------------------------------------------------------------------------------- 1 | //! Show toggle controls using togglers. 2 | 3 | use crate::reexports::{iced_core, iced_style, iced_widget}; 4 | use crate::utils::static_array_from_iter; 5 | use iced_core::alignment; 6 | use iced_core::event; 7 | use iced_core::layout; 8 | use iced_core::mouse; 9 | use iced_core::renderer; 10 | use iced_core::text; 11 | 12 | use iced::Size; 13 | use iced_core::widget::{self, tree, Tree}; 14 | use iced_core::{ 15 | border::Border, color, Clipboard, Color, Element, Event, Layout, Length, Pixels, Rectangle, 16 | Shell, Widget, 17 | }; 18 | 19 | use crate::{chain, id, lerp}; 20 | 21 | pub use iced_widget::toggler::{Appearance, StyleSheet}; 22 | 23 | /// A toggler widget. 24 | /// 25 | #[allow(missing_debug_implementations)] 26 | pub struct Toggler<'a, Message, Theme, Renderer> 27 | where 28 | Renderer: text::Renderer, 29 | Theme: iced_widget::toggler::Catalog, 30 | { 31 | id: id::Toggler, 32 | is_toggled: bool, 33 | on_toggle: Box Message + 'a>, 34 | label: Option, 35 | width: Length, 36 | size: f32, 37 | text_size: Option, 38 | text_alignment: alignment::Horizontal, 39 | spacing: f32, 40 | font: Option<::Font>, 41 | style: ::Style, 42 | percent: f32, 43 | anim_multiplier: f32, 44 | } 45 | 46 | impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer> 47 | where 48 | Renderer: text::Renderer, 49 | Theme: iced_widget::toggler::Catalog, 50 | { 51 | /// The default size of a [`Toggler`]. 52 | pub const DEFAULT_SIZE: f32 = 20.0; 53 | 54 | /// Creates a new [`Toggler`]. 55 | /// 56 | /// It expects: 57 | /// * a boolean describing whether the [`Toggler`] is checked or not 58 | /// * An optional label for the [`Toggler`] 59 | /// * a function that will be called when the [`Toggler`] is toggled. It 60 | /// will receive the new state of the [`Toggler`] and must produce a 61 | /// `Message`. 62 | pub fn new(id: id::Toggler, label: impl Into>, is_toggled: bool, f: F) -> Self 63 | where 64 | F: 'a + Fn(chain::Toggler, bool) -> Message, 65 | { 66 | Toggler { 67 | id, 68 | is_toggled, 69 | on_toggle: Box::new(f), 70 | label: label.into(), 71 | width: Length::Fill, 72 | size: Self::DEFAULT_SIZE, 73 | text_size: None, 74 | text_alignment: alignment::Horizontal::Left, 75 | spacing: 0.0, 76 | font: None, 77 | style: Default::default(), 78 | percent: if is_toggled { 1.0 } else { 0.0 }, 79 | anim_multiplier: 1.0, 80 | } 81 | } 82 | 83 | /// Sets the size of the [`Toggler`]. 84 | pub fn size(mut self, size: impl Into) -> Self { 85 | self.size = size.into().0; 86 | self 87 | } 88 | 89 | /// Sets the width of the [`Toggler`]. 90 | pub fn width(mut self, width: impl Into) -> Self { 91 | self.width = width.into(); 92 | self 93 | } 94 | 95 | /// Sets the text size o the [`Toggler`]. 96 | pub fn text_size(mut self, text_size: impl Into) -> Self { 97 | self.text_size = Some(text_size.into().0); 98 | self 99 | } 100 | 101 | /// Sets the horizontal alignment of the text of the [`Toggler`] 102 | pub fn text_alignment(mut self, alignment: alignment::Horizontal) -> Self { 103 | self.text_alignment = alignment; 104 | self 105 | } 106 | 107 | /// Sets the spacing between the [`Toggler`] and the text. 108 | pub fn spacing(mut self, spacing: impl Into) -> Self { 109 | self.spacing = spacing.into().0; 110 | self 111 | } 112 | 113 | /// Sets the [`Font`] of the text of the [`Toggler`] 114 | /// 115 | /// [`Font`]: iced_core::text::Renderer::Font 116 | pub fn font(mut self, font: ::Font) -> Self { 117 | self.font = Some(font); 118 | self 119 | } 120 | 121 | /// Sets the style of the [`Toggler`]. 122 | pub fn style(mut self, style: impl Into<::Style>) -> Self { 123 | self.style = style.into(); 124 | self 125 | } 126 | 127 | /// The percent completion of the toggler animation. 128 | /// This is indented to automated cosmic-time use, and shouldn't 129 | /// need to be called manually. 130 | pub fn percent(mut self, percent: f32) -> Self { 131 | self.percent = percent; 132 | self 133 | } 134 | 135 | /// The default animation time is 100ms, to speed up the toggle 136 | /// animation use a value less than 1.0, and to slow down the 137 | /// animation use a value greater than 1.0. 138 | pub fn anim_multiplier(mut self, multiplier: f32) -> Self { 139 | self.anim_multiplier = multiplier; 140 | self 141 | } 142 | } 143 | 144 | impl<'a, Message, Theme, Renderer> Widget 145 | for Toggler<'a, Message, Theme, Renderer> 146 | where 147 | Renderer: text::Renderer, 148 | Theme: StyleSheet + widget::text::Catalog, 149 | { 150 | fn size(&self) -> Size { 151 | Size { 152 | width: self.width, 153 | height: Length::Shrink, 154 | } 155 | } 156 | 157 | fn state(&self) -> tree::State { 158 | tree::State::new(widget::text::State::::default()) 159 | } 160 | 161 | fn layout( 162 | &self, 163 | tree: &mut Tree, 164 | renderer: &Renderer, 165 | limits: &layout::Limits, 166 | ) -> layout::Node { 167 | let limits = limits.width(self.width); 168 | 169 | layout::next_to_each_other( 170 | &limits, 171 | self.spacing, 172 | |_| layout::Node::new(iced_core::Size::new(2.0 * self.size, self.size)), 173 | |limits| { 174 | if let Some(label) = self.label.as_deref() { 175 | let state = tree 176 | .state 177 | .downcast_mut::>(); 178 | 179 | iced_core::widget::text::layout( 180 | state, 181 | renderer, 182 | limits, 183 | self.width, 184 | Length::Shrink, 185 | label, 186 | iced_core::text::LineHeight::default(), 187 | self.text_size.map(iced::Pixels), 188 | self.font, 189 | self.text_alignment, 190 | alignment::Vertical::Top, 191 | iced_core::text::Shaping::Advanced, 192 | ) 193 | } else { 194 | layout::Node::new(iced_core::Size::ZERO) 195 | } 196 | }, 197 | ) 198 | } 199 | 200 | fn on_event( 201 | &mut self, 202 | _state: &mut Tree, 203 | event: Event, 204 | layout: Layout<'_>, 205 | cursor_position: mouse::Cursor, 206 | _renderer: &Renderer, 207 | _clipboard: &mut dyn Clipboard, 208 | shell: &mut Shell<'_, Message>, 209 | _viewport: &Rectangle, 210 | ) -> event::Status { 211 | match event { 212 | Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { 213 | let mouse_over = cursor_position.is_over(layout.bounds()); 214 | 215 | if mouse_over { 216 | if self.is_toggled { 217 | let off_animation = 218 | chain::Toggler::off(self.id.clone(), self.anim_multiplier); 219 | shell.publish((self.on_toggle)(off_animation, !self.is_toggled)); 220 | } else { 221 | let on_animation = 222 | chain::Toggler::on(self.id.clone(), self.anim_multiplier); 223 | shell.publish((self.on_toggle)(on_animation, !self.is_toggled)); 224 | } 225 | 226 | event::Status::Captured 227 | } else { 228 | event::Status::Ignored 229 | } 230 | } 231 | _ => event::Status::Ignored, 232 | } 233 | } 234 | 235 | fn mouse_interaction( 236 | &self, 237 | _state: &Tree, 238 | layout: Layout<'_>, 239 | cursor_position: mouse::Cursor, 240 | _viewport: &Rectangle, 241 | _renderer: &Renderer, 242 | ) -> mouse::Interaction { 243 | if cursor_position.is_over(layout.bounds()) { 244 | mouse::Interaction::Pointer 245 | } else { 246 | mouse::Interaction::default() 247 | } 248 | } 249 | 250 | fn draw( 251 | &self, 252 | tree: &Tree, 253 | renderer: &mut Renderer, 254 | theme: &Theme, 255 | style: &renderer::Style, 256 | layout: Layout<'_>, 257 | cursor_position: mouse::Cursor, 258 | viewport: &Rectangle, 259 | ) { 260 | /// Makes sure that the border radius of the toggler looks good at every size. 261 | const BORDER_RADIUS_RATIO: f32 = 32.0 / 13.0; 262 | 263 | /// The space ratio between the background Quad and the Toggler bounds, and 264 | /// between the background Quad and foreground Quad. 265 | const SPACE_RATIO: f32 = 0.05; 266 | 267 | let mut children = layout.children(); 268 | 269 | if self.label.is_some() { 270 | let label_layout = children.next().unwrap(); 271 | 272 | iced_widget::text::draw( 273 | renderer, 274 | style, 275 | label_layout, 276 | tree.state.downcast_ref(), 277 | iced_widget::text::Style::default(), 278 | viewport, 279 | ); 280 | } 281 | 282 | let toggler_layout = children.next().unwrap(); 283 | let bounds = toggler_layout.bounds(); 284 | 285 | let is_mouse_over = cursor_position.is_over(bounds); 286 | 287 | let style = if is_mouse_over { 288 | blend_appearances( 289 | theme.hovered(&self.style, false), 290 | theme.hovered(&self.style, true), 291 | self.percent, 292 | ) 293 | } else { 294 | blend_appearances( 295 | theme.active(&self.style, false), 296 | theme.active(&self.style, true), 297 | self.percent, 298 | ) 299 | }; 300 | 301 | let border_radius = bounds.height / BORDER_RADIUS_RATIO; 302 | let space = SPACE_RATIO * bounds.height; 303 | 304 | let toggler_background_bounds = Rectangle { 305 | x: bounds.x + space, 306 | y: bounds.y + space, 307 | width: bounds.width - (2.0 * space), 308 | height: bounds.height - (2.0 * space), 309 | }; 310 | 311 | renderer.fill_quad( 312 | renderer::Quad { 313 | bounds: toggler_background_bounds, 314 | border: Border { 315 | radius: border_radius.into(), 316 | width: 1.0, 317 | color: style.background_border.unwrap_or(style.background), 318 | }, 319 | shadow: Default::default(), 320 | }, 321 | style.background, 322 | ); 323 | 324 | let toggler_foreground_bounds = Rectangle { 325 | x: bounds.x 326 | + lerp( 327 | 2.0 * space, 328 | bounds.width - 2.0 * space - (bounds.height - (4.0 * space)), 329 | self.percent, 330 | ), 331 | y: bounds.y + (2.0 * space), 332 | width: bounds.height - (4.0 * space), 333 | height: bounds.height - (4.0 * space), 334 | }; 335 | 336 | renderer.fill_quad( 337 | renderer::Quad { 338 | bounds: toggler_foreground_bounds, 339 | border: Border { 340 | radius: border_radius.into(), 341 | width: 1.0, 342 | color: style.foreground_border.unwrap_or(style.foreground), 343 | }, 344 | shadow: Default::default(), 345 | }, 346 | style.foreground, 347 | ); 348 | } 349 | } 350 | 351 | impl<'a, Message, Theme, Renderer> From> 352 | for Element<'a, Message, Theme, Renderer> 353 | where 354 | Message: 'a, 355 | Renderer: 'a + text::Renderer, 356 | Theme: StyleSheet + widget::text::Catalog + 'a, 357 | { 358 | fn from( 359 | toggler: Toggler<'a, Message, Theme, Renderer>, 360 | ) -> Element<'a, Message, Theme, Renderer> { 361 | Element::new(toggler) 362 | } 363 | } 364 | 365 | fn blend_appearances( 366 | one: iced_widget::toggler::Appearance, 367 | mut two: iced_widget::toggler::Appearance, 368 | percent: f32, 369 | ) -> iced_widget::toggler::Appearance { 370 | if percent == 0. { 371 | one 372 | } else if percent == 1. { 373 | two 374 | } else { 375 | let background = static_array_from_iter::( 376 | one.background 377 | .into_linear() 378 | .iter() 379 | .zip(two.background.into_linear().iter()) 380 | .map(|(o, t)| lerp(*o, *t, percent)), 381 | ); 382 | 383 | let border_one: Color = one.background_border.unwrap_or(color!(0, 0, 0)); 384 | let border_two: Color = two.background_border.unwrap_or(color!(0, 0, 0)); 385 | 386 | let new_border = static_array_from_iter::( 387 | border_one 388 | .into_linear() 389 | .iter() 390 | .zip(border_two.into_linear().iter()) 391 | .map(|(o, t)| lerp(*o, *t, percent)), 392 | ); 393 | 394 | let foreground = static_array_from_iter::( 395 | one.foreground 396 | .into_linear() 397 | .iter() 398 | .zip(two.foreground.into_linear().iter()) 399 | .map(|(o, t)| lerp(*o, *t, percent)), 400 | ); 401 | 402 | let f_border_one: Color = one.foreground_border.unwrap_or(color!(0, 0, 0)); 403 | let f_border_two: Color = two.foreground_border.unwrap_or(color!(0, 0, 0)); 404 | let new_f_border = static_array_from_iter::( 405 | f_border_one 406 | .into_linear() 407 | .iter() 408 | .zip(f_border_two.into_linear().iter()) 409 | .map(|(o, t)| lerp(*o, *t, percent)), 410 | ); 411 | 412 | two.background = background.into(); 413 | two.background_border = Some(new_border.into()); 414 | two.foreground = foreground.into(); 415 | two.foreground_border = Some(new_f_border.into()); 416 | two 417 | } 418 | } 419 | --------------------------------------------------------------------------------