├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── README.tpl ├── doc └── releasing.md ├── examples ├── input-handling.rs ├── png-base64.rs ├── png-file.rs ├── sdl-audio.rs └── themes.rs ├── filter_readme.sed ├── justfile ├── release.toml ├── rustfmt.nightly.toml └── src ├── display.rs ├── lib.rs ├── output_image.rs ├── output_settings.rs ├── theme.rs └── window ├── mod.rs └── sdl_window.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: jamwaffles 4 | patreon: 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - Version of embedded-graphics-simulator in use (if applicable): [version here] 2 | - Version of embedded-graphics in use (if applicable): [version here] 3 | 4 | Please link to the driver on if applicable. 5 | 6 | ## Description of the problem/feature request/other 7 | 8 | [description here] 9 | 10 | ## Test case (if applicable) 11 | 12 | ```rust 13 | // Failing test case demonstrating the issue here 14 | ``` 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thank you for helping out with embedded-graphics-simulator development! Please: 2 | 3 | - [ ] Check that you've added passing tests and documentation 4 | - [ ] Add an example where applicable 5 | - [ ] Add a `CHANGELOG.md` entry in the **Unreleased** section under the appropriate heading (**Added**, **Fixed**, etc) if your changes affect the **public API** 6 | - [ ] Run `rustfmt` on the project 7 | - [ ] Run `just build` (Linux/macOS only) and make sure it passes. If you use Windows, check that CI passes once you've opened the PR. 8 | 9 | ## PR description 10 | 11 | [add your PR description here] 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: dtolnay/rust-toolchain@stable 10 | - uses: actions/checkout@v4 11 | - run: cargo build 12 | 13 | build-no-sdl: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: dtolnay/rust-toolchain@stable 17 | - uses: actions/checkout@v4 18 | - run: cargo build --no-default-features 19 | 20 | check-formatting-and-docs: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: dtolnay/rust-toolchain@stable 24 | - uses: taiki-e/install-action@just 25 | - uses: taiki-e/install-action@v2 26 | with: 27 | tool: cargo-readme 28 | - uses: actions/checkout@v4 29 | - run: cargo fmt --check 30 | - run: just check-readme 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | target/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | .DS_Store 6 | .idea/ 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | [`embedded-graphics-simulator`](https://crates.io/crates/embedded-graphics-simulator) is an SDL-based simulator for testing, debugging and developing [`embedded-graphics`](https://crates.io/crates/embedded-graphics) applications. 4 | 5 | 6 | 7 | ## [Unreleased] - ReleaseDate 8 | 9 | ### Added 10 | 11 | - [#63](https://github.com/embedded-graphics/simulator/pull/63) Added support for custom binary color themes (`BinaryColorTheme::Custom`). 12 | - [#62](https://github.com/embedded-graphics/simulator/pull/62) Added an SDL based audio example (sdl-audio.rs). 13 | 14 | ### Changed 15 | 16 | - **(breaking)** [#65](https://github.com/embedded-graphics/simulator/pull/65) Bump Minimum Supported Rust Version (MSRV) to latest stable. 17 | 18 | ## [0.7.0] - 2024-09-10 19 | 20 | - **(breaking)** [#55](https://github.com/embedded-graphics/simulator/pull/55) Bump the following crate dependencies: `image` to 0.25.1, `base64` to 0.22.1, `sdl2` to 0.37.0 21 | 22 | ## [0.6.0] - 2023-11-26 23 | 24 | ### Changed 25 | 26 | - **(breaking)** [#49](https://github.com/embedded-graphics/simulator/pull/49) Bump Minimum Supported Rust Version (MSRV) to 1.71.1. 27 | - **(breaking)** [#52](https://github.com/embedded-graphics/simulator/pull/52) Bump `image` crate dependency to 0.24.7. 28 | 29 | ## [0.5.0] - 2023-05-14 30 | 31 | ### Changed 32 | 33 | - **(breaking)** [#46](https://github.com/embedded-graphics/simulator/pull/46) Bump minimum embedded-graphics version from 0.7.1 to 0.8. 34 | 35 | ## [0.4.1] - 2023-03-06 36 | 37 | ### Added 38 | 39 | - [#45](https://github.com/embedded-graphics/simulator/pull/45) Added `OutputSettingsBuilder::max_fps` to set the maximum FPS of the simulator. 40 | 41 | ### Changed 42 | 43 | - [#45](https://github.com/embedded-graphics/simulator/pull/45) Limit simulator to 60FPS by default. 44 | 45 | ## [0.4.0] - 2022-09-19 46 | 47 | ### Changed 48 | 49 | - [#34](https://github.com/embedded-graphics/simulator/pull/34) Bump minimum embedded-graphics version from 0.7.0 to 0.7.1. 50 | - **(breaking)** [#44](https://github.com/embedded-graphics/simulator/pull/44) Bump Minimum Supported Rust Version (MSRV) to 1.61. 51 | 52 | ## [0.3.0] - 2021-06-05 53 | 54 | ## [0.3.0-beta.3] - 2021-06-04 55 | 56 | ### Added 57 | 58 | - [#28](https://github.com/embedded-graphics/simulator/pull/28) Added `SimulatorDisplay::to_{be,le,ne}_bytes` to convert the display content to raw image data. 59 | - [#29](https://github.com/embedded-graphics/simulator/pull/29) Added `SimulatorDisplay::load_png`. 60 | - [#29](https://github.com/embedded-graphics/simulator/pull/29) Added support for `EG_SIMULATOR_CHECK`, `EG_SIMULATOR_CHECK_RAW` and `EG_SIMULATOR_DUMP_RAW` environment variables. 61 | - [#29](https://github.com/embedded-graphics/simulator/pull/29) A limited version of `Window` can now be used without the `with-sdl` feature enabled. Event handling isn't available if SDL support is disabled. 62 | - [#30](https://github.com/embedded-graphics/simulator/pull/30) Added `SimulatorDisplay::diff`. 63 | 64 | ### Changed 65 | 66 | - **(breaking)** [#29](https://github.com/embedded-graphics/simulator/pull/29) Color types used in `Window::update`and `Window::show_static` must now implement `From`. 67 | 68 | ### Fixed 69 | 70 | - [#28](https://github.com/embedded-graphics/simulator/pull/28) Fixed panic for zero sized `SimulatorDisplay`s. 71 | 72 | ## [0.3.0-beta.2] - 2021-05-04 73 | 74 | ### Added 75 | 76 | - [#25](https://github.com/embedded-graphics/simulator/pull/25) Added `OutputImage` to export PNG files and base64 encoded PNGs. 77 | - [#25](https://github.com/embedded-graphics/simulator/pull/25) Added `BinaryColorTheme::Inverted`. 78 | 79 | ### Changed 80 | 81 | - **(breaking)** [#25](https://github.com/embedded-graphics/simulator/pull/25) Removed `SimulatorDisplay::to_image_buffer`. Use `to_rgb_output_image` or `to_grayscale_output_image` instead. 82 | 83 | ## [0.3.0-beta.1] - 2021-04-24 84 | 85 | ### Changed 86 | 87 | - [#24](https://github.com/embedded-graphics/simulator/pull/24) Upgrade to embedded-graphics 0.7.0-beta.1. 88 | 89 | ## [0.3.0-alpha.2] - 2021-02-05 90 | 91 | ### Added 92 | 93 | - [#16](https://github.com/embedded-graphics/simulator/pull/16) Re-export `sdl2` types. 94 | 95 | ## [0.3.0-alpha.1] - 2021-01-07 96 | 97 | ## [0.2.1] - 2020-07-29 98 | 99 | > Note: PR numbers from this point onwards are from the old `embedded-graphics/embedded-graphics` repository. New PR numbers above this note refer to PRs in the `embedded-graphics/simulator` repository. 100 | 101 | ### Added 102 | 103 | - [#298](https://github.com/embedded-graphics/embedded-graphics/pull/298) Added the `with-sdl` option (enabled by default) to allow optionally disabling SDL2 support. 104 | - [#271](https://github.com/embedded-graphics/embedded-graphics/pull/271) Add `MouseMove` event support to simulator. 105 | 106 | ## [0.2.0] - 2020-03-20 107 | 108 | ### Added 109 | 110 | - **(breaking)** #266 Added [image](https://crates.io/crates/image) support and PNG export. See the `README.md` for information about how to use these features. The API for creating windows was changed to make the output settings independent of the `Window` type. The pixel scaling and theme settings were moved to a new `OutputSettings` struct, that can be built using the `OutputSettingsBuilder`. `WindowBuilder` was removed and replaced by a `Window::new(title, &output_settings)` function. 111 | 112 | ## [0.2.0-beta.2] - 2020-02-17 113 | 114 | ### Added 115 | 116 | - #183 Added limited mouse and keyboard event handling to the simulator in order to simulate input devices such as touch screens, buttons, or rotary encoders. 117 | - #171 Added a more complex `analog-clock` example to the simulator - [check it out](https://github.com/embedded-graphics/embedded-graphics/tree/embedded-graphics-v0.6.0-alpha.3/simulator/examples/analog-clock.rs) for some more in-depth usage of Embedded Graphics. 118 | 119 | ### Fixed 120 | 121 | - #192 Performance of drawing in the simulator is increased. 122 | - #218 Test README examples in CI and update them to work with latest crate versions. 123 | 124 | ### Changed 125 | 126 | - **(breaking)** The simulator API changed. 127 | - #203 updated simulator screenshots and added them to README 128 | 129 | ## 0.2.0-alpha.1 130 | 131 | ### Fixed 132 | 133 | - The TGA example in the simulator now draws the image correctly 134 | 135 | ## 0.1.0 136 | 137 | ### Changed 138 | 139 | - The simulator is now [available on crates.io](https://crates.io/crates/embedded-graphics-simulator) as a standalone crate. You can now create simulated displays for testing out embedded_graphics code or showing off cool examples. 140 | - The builtin simulator now supports colour pixel types, like `RGB565`. 141 | 142 | 143 | [unreleased]: https://github.com/embedded-graphics/embedded-graphics-simulator/compare/v0.7.0...HEAD 144 | [0.7.0]: https://github.com/embedded-graphics/embedded-graphics-simulator/compare/v0.6.0...v0.7.0 145 | [0.6.0]: https://github.com/embedded-graphics/embedded-graphics-simulator/compare/v0.5.0...v0.6.0 146 | [0.5.0]: https://github.com/embedded-graphics/embedded-graphics-simulator/compare/v0.4.1...v0.5.0 147 | [0.4.1]: https://github.com/embedded-graphics/embedded-graphics-simulator/compare/v0.4.0...v0.4.1 148 | 149 | [0.4.0]: https://github.com/embedded-graphics/embedded-graphics-simulator/compare/v0.3.0...v0.4.0 150 | [0.3.0]: https://github.com/embedded-graphics/embedded-graphics-simulator/compare/v0.3.0-beta.3...v0.3.0 151 | [0.3.0-beta.3]: https://github.com/embedded-graphics/embedded-graphics-simulator/compare/v0.3.0-beta.2...v0.3.0-beta.3 152 | [0.3.0-beta.2]: https://github.com/embedded-graphics/embedded-graphics-simulator/compare/v0.3.0-beta.1...v0.3.0-beta.2 153 | [0.3.0-beta.1]: https://github.com/embedded-graphics/embedded-graphics-simulator/compare/v0.3.0-alpha.2...v0.3.0-beta.1 154 | [0.3.0-alpha.2]: https://github.com/embedded-graphics/embedded-graphics-simulator/compare/v0.3.0-alpha.1...v0.3.0-alpha.2 155 | [0.3.0-alpha.1]: https://github.com/embedded-graphics/simulator/compare/after-split...v0.3.0-alpha.1 156 | [0.2.1]: https://github.com/embedded-graphics/embedded-graphics/compare/embedded-graphics-simulator-v0.2.0...embedded-graphics-simulator-v0.2.1 157 | [0.2.0]: https://github.com/embedded-graphics/embedded-graphics/compare/embedded-graphics-simulator-v0.2.0-beta.2...embedded-graphics-simulator-v0.2.0 158 | [0.2.0-beta.2]: https://github.com/embedded-graphics/embedded-graphics/compare/embedded-graphics-simulator-v0.2.0-alpha.1...embedded-graphics-simulator-v0.2.0-beta.2 159 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | version = "0.7.0" 3 | name = "embedded-graphics-simulator" 4 | description = "Embedded graphics simulator" 5 | authors = ["James Waples ", "Ralf Fuest "] 6 | repository = "https://github.com/embedded-graphics/simulator" 7 | documentation = "https://docs.rs/embedded-graphics-simulator" 8 | categories = ["embedded", "no-std"] 9 | keywords = ["embedded-graphics", "simulator", "graphics", "embedded"] 10 | readme = "./README.md" 11 | license = "MIT OR Apache-2.0" 12 | edition = "2021" 13 | exclude = [ 14 | "/.circleci/", 15 | "/.github/", 16 | ".gitignore", 17 | ] 18 | 19 | [badges] 20 | circle-ci = { repository = "embedded-graphics/simulator", branch = "master" } 21 | 22 | [dependencies] 23 | image = { version = "0.25.1", default-features=false, features=["png"] } 24 | base64 = "0.22.1" 25 | embedded-graphics = "0.8.1" 26 | sdl2 = { version = "0.37.0", optional = true } 27 | ouroboros = { version = "0.18.0", optional = true } 28 | 29 | [features] 30 | default = ["with-sdl"] 31 | fixed_point = ["embedded-graphics/fixed_point"] 32 | with-sdl = ["sdl2", "ouroboros"] 33 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 James Waples 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Embedded graphics simulator 2 | 3 | [![Build Status](https://circleci.com/gh/embedded-graphics/simulator/tree/master.svg?style=shield)](https://circleci.com/gh/embedded-graphics/simulator/tree/master) 4 | [![Crates.io](https://img.shields.io/crates/v/embedded-graphics-simulator.svg)](https://crates.io/crates/embedded-graphics-simulator) 5 | [![Docs.rs](https://docs.rs/embedded-graphics-simulator/badge.svg)](https://docs.rs/embedded-graphics-simulator) 6 | [![embedded-graphics on Matrix](https://img.shields.io/matrix/rust-embedded-graphics:matrix.org)](https://matrix.to/#/#rust-embedded-graphics:matrix.org) 7 | 8 | ## [Documentation](https://docs.rs/embedded-graphics-simulator) 9 | 10 | ![It can display all sorts of embedded-graphics test code.](https://raw.githubusercontent.com/embedded-graphics/embedded-graphics/master/assets/simulator-demo.png) 11 | 12 | The simulator can be used to test and debug 13 | [embedded-graphics](https://crates.io/crates/embedded-graphics) code, or produce examples and 14 | interactive demos to show off embedded graphics features. 15 | 16 | ## [Examples](https://github.com/embedded-graphics/examples) 17 | 18 | More simulator examples can be found in the [examples repository](https://github.com/embedded-graphics/examples). 19 | 20 | ### Simulate a 128x64 SSD1306 OLED 21 | 22 | ```rust 23 | use embedded_graphics::{ 24 | pixelcolor::BinaryColor, 25 | prelude::*, 26 | primitives::{Circle, Line, Rectangle, PrimitiveStyle}, 27 | mono_font::{ascii::FONT_6X9, MonoTextStyle}, 28 | text::Text, 29 | }; 30 | use embedded_graphics_simulator::{BinaryColorTheme, SimulatorDisplay, Window, OutputSettingsBuilder}; 31 | 32 | fn main() -> Result<(), core::convert::Infallible> { 33 | let mut display = SimulatorDisplay::::new(Size::new(128, 64)); 34 | 35 | let line_style = PrimitiveStyle::with_stroke(BinaryColor::On, 1); 36 | let text_style = MonoTextStyle::new(&FONT_6X9, BinaryColor::On); 37 | 38 | Circle::new(Point::new(72, 8), 48) 39 | .into_styled(line_style) 40 | .draw(&mut display)?; 41 | 42 | Line::new(Point::new(48, 16), Point::new(8, 16)) 43 | .into_styled(line_style) 44 | .draw(&mut display)?; 45 | 46 | Line::new(Point::new(48, 16), Point::new(64, 32)) 47 | .into_styled(line_style) 48 | .draw(&mut display)?; 49 | 50 | Rectangle::new(Point::new(79, 15), Size::new(34, 34)) 51 | .into_styled(line_style) 52 | .draw(&mut display)?; 53 | 54 | Text::new("Hello World!", Point::new(5, 5), text_style).draw(&mut display)?; 55 | 56 | let output_settings = OutputSettingsBuilder::new() 57 | .theme(BinaryColorTheme::OledBlue) 58 | .build(); 59 | Window::new("Hello World", &output_settings).show_static(&display); 60 | 61 | Ok(()) 62 | } 63 | ``` 64 | 65 | ## Setup 66 | 67 | The simulator uses SDL2 and its development libraries which must be installed to build and run 68 | it. 69 | 70 | ### Linux (`apt`) 71 | 72 | ```bash 73 | sudo apt install libsdl2-dev 74 | ``` 75 | 76 | ### macOS (`brew`) 77 | 78 | ```bash 79 | brew install sdl2 80 | ``` 81 | 82 | Users on Apple silicon or with custom installation directories will need to 83 | set `LIBRARY_PATH` for the linker to find the installed SDL2 package: 84 | 85 | ```bash 86 | export LIBRARY_PATH="$LIBRARY_PATH:$(brew --prefix)/lib" 87 | ``` 88 | More information can be found in the 89 | [SDL2 documentation](https://github.com/Rust-SDL2/rust-sdl2#homebrew). 90 | 91 | ### Windows 92 | 93 | The Windows install process is a bit more involved, but it _does_ work. See [the Rust-SDL2 94 | crate's README](https://github.com/Rust-SDL2/rust-sdl2) for instructions. There are multiple 95 | ways to get it working, but probably the simplest method is copying the binaries as shown 96 | [here](https://github.com/Rust-SDL2/rust-sdl2#windows-msvc). 97 | 98 | ## Creating screenshots 99 | 100 | Screenshots of programs, that use `Window` to display a simulated display, can be created by 101 | setting the `EG_SIMULATOR_DUMP` or `EG_SIMULATOR_DUMP_RAW` environment variable: 102 | 103 | ```bash 104 | EG_SIMULATOR_DUMP=screenshot.png cargo run 105 | ``` 106 | 107 | By setting the variable the display passed to the first `Window::update` call gets exported as 108 | a PNG file to the specified path. After the file is exported the process is terminated. 109 | 110 | The difference between `EG_SIMULATOR_DUMP` and `EG_SIMULATOR_DUMP_RAW` is that the first method 111 | applies the output settings before exporting the PNG file and the later dumps the unaltered 112 | display content. 113 | 114 | ## Exporting images 115 | 116 | If a program doesn't require to display a window and only needs to export one or more images, a 117 | `SimulatorDisplay` can also be converted to an `image` crate 118 | `ImageBuffer` by using the `to_rgb_output_image` or `to_grayscale_output_image` methods. 119 | The resulting buffer can then be used to save the display content to any format supported by 120 | `image`. 121 | 122 | ## Using the simulator in CI 123 | 124 | The simulator supports two environment variables to check if the display content matches a 125 | reference PNG file: `EG_SIMULATOR_CHECK` and `EG_SIMULATOR_CHECK_RAW`. If the display content 126 | of the first `Window::update` call doesn't match the reference image the process exits with a 127 | non zero exit exit code. Otherwise the process will exit with a zero exit code. 128 | 129 | ```bash 130 | EG_SIMULATOR_CHECK=screenshot.png cargo run || echo "Display doesn't match PNG file" 131 | ``` 132 | 133 | `EG_SIMULATOR_CHECK` assumes that the reference image was created using the same 134 | `OutputSetting`s, while `EG_SIMULATOR_CHECK_RAW` assumes an unstyled reference image. 135 | 136 | ## Usage without SDL2 137 | 138 | When the simulator is used in headless/CI environments that don't require showing a window, SDL2 139 | support can be disabled. This removes the requirement of SDL2 being installed on the target machine, 140 | but still allows the simulator to be used to generate images. 141 | 142 | The `with-sdl` feature is enabled by default and can be disabled by adding `default-features = false` to the dependency: 143 | 144 | ```toml 145 | [dependencies.embedded-graphics-simulator] 146 | version = "0.2.0" 147 | default-features = false 148 | ``` 149 | 150 | See the [Choosing 151 | Features](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#choosing-features) 152 | Cargo manifest documentation for more details. 153 | 154 | 155 | ## Minimum supported Rust version 156 | 157 | The `embedded-graphics-simulator` crate is tested to compile on the latest stable Rust release. 158 | 159 | ## License 160 | 161 | Licensed under either of 162 | 163 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 164 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 165 | 166 | at your option. 167 | 168 | ### Contribution 169 | 170 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the 171 | work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 172 | additional terms or conditions. 173 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | # Embedded graphics simulator 2 | 3 | [![Build Status](https://circleci.com/gh/embedded-graphics/simulator/tree/master.svg?style=shield)](https://circleci.com/gh/embedded-graphics/simulator/tree/master) 4 | [![Crates.io](https://img.shields.io/crates/v/embedded-graphics-simulator.svg)](https://crates.io/crates/embedded-graphics-simulator) 5 | [![Docs.rs](https://docs.rs/embedded-graphics-simulator/badge.svg)](https://docs.rs/embedded-graphics-simulator) 6 | [![embedded-graphics on Matrix](https://img.shields.io/matrix/rust-embedded-graphics:matrix.org)](https://matrix.to/#/#rust-embedded-graphics:matrix.org) 7 | 8 | ## [Documentation](https://docs.rs/embedded-graphics-simulator) 9 | 10 | {{readme}} 11 | 12 | ## Minimum supported Rust version 13 | 14 | The `embedded-graphics-simulator` crate is tested to compile on the latest stable Rust release. 15 | 16 | ## License 17 | 18 | Licensed under either of 19 | 20 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 21 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 22 | 23 | at your option. 24 | 25 | ### Contribution 26 | 27 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the 28 | work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 29 | additional terms or conditions. 30 | -------------------------------------------------------------------------------- /doc/releasing.md: -------------------------------------------------------------------------------- 1 | # Release process 2 | 3 | Target audience: crate maintainers who wish to release `embedded-graphics-simulator`. 4 | 5 | > Please take a cautious approach to this. If any step doesn't feel right or doesn't succeed smoothly, stop and rectify any issues before continuing. 6 | 7 | ## On GitHub 8 | 9 | - Check that all desired PRs are merged and all desired issues are closed/resolved. 10 | - Check that the latest master build passed in CircleCI. 11 | 12 | ## On your local machine 13 | 14 | - `cd` to the repository root 15 | - Check that `cargo-release` is installed and available in `$PATH`: 16 | 17 | ```bash 18 | cargo release --version 19 | ``` 20 | 21 | - Ensure you have the latest changes with `git switch master` and `git pull --rebase` 22 | - Check that your local repository is clean with no uncommitted changes and no unpushed commits. Ideally, use `git reset --hard origin/master` to ensure your local state is up to date with `origin/master`. You may need to change `origin` to the name of the remote pointing to . 23 | - Before a **stable** release: 24 | - Search the repository for any `TODO` or `FIXME` comments. If any need resolving before release, stop this process and fix them with one or more PRs. 25 | - Check that the crate version in `Cargo.toml` matches the latest released versions on . 26 | - Run `just build` to ensure the build passes locally. 27 | - If the build fails for any reason, stop the release process and fix any issues by creating PRs. The upstream master branch must remain the source of truth. Restart this checklist once `just build` passes. 28 | - Double check the release level (major, minor, patch) 29 | - Release the crate: 30 | 31 | ```bash 32 | cargo release --push-remote 33 | ``` 34 | 35 | Where `` is `major`, `minor`, `patch`, or a specific SemVer version number, and where `` is the git remote for the upstream repository `embedded-graphics/simulator`. 36 | 37 | ## Post release 38 | 39 | - Check that the release command pushed a Git tag when the crate was published, something like `v0.3.0-beta.1` or `v0.3.1`. 40 | - For the new tag, go to its page at e.g. , click Edit tag and draft a release: 41 | 42 | - Copy and paste the tag into the `Release title` field. 43 | - Copy and paste the latest released section out of the crate's `CHANGELOG.md` file into the `Describe this release` field. Do not include the version header, e.g.: 44 | 45 | ```markdown 46 | ### Added 47 | 48 | - [#111](https://github.com/embedded-graphics/simulator/pull/111) Added something 49 | 50 | ### Removed 51 | 52 | - [#222](https://github.com/embedded-graphics/simulator/pull/222) Removed a thing 53 | ``` 54 | 55 | - For `alpha` or `beta` releases, check the `This is a pre-release` checkbox. 56 | - Hit Publish release 57 | 58 | - Check that the release is displayed on the [repository homepage](https://github.com/embedded-graphics/simulator). 59 | - Post a link to the released tag (e.g. ) to the embedded-graphics Matrix room at 60 | - If you are @jamwaffles, post a Tweet tagging @rustembedded with a happy announcement message. 61 | 62 | - Check the other repositories in the [embedded-graphics organization](https://github.com/embedded-graphics) for dependencies on `embedded-graphics-simulator`. The version should be updated to the latest releases made whilst following this guide. 63 | -------------------------------------------------------------------------------- /examples/input-handling.rs: -------------------------------------------------------------------------------- 1 | //! # Example: Input Handling 2 | //! 3 | //! This example allows you to move a red circle to the location of a click on the simulator 4 | //! screen, or move the circle using the arrow keys. Although input handling is not a part of the 5 | //! embedded-graphics API, the simulator can be used to emulate input controls in order to 6 | //! represent more complex UI systems such as touch screens. 7 | 8 | extern crate embedded_graphics; 9 | extern crate embedded_graphics_simulator; 10 | 11 | use embedded_graphics::{ 12 | pixelcolor::Rgb888, 13 | prelude::*, 14 | primitives::{Circle, PrimitiveStyle}, 15 | }; 16 | use embedded_graphics_simulator::{ 17 | sdl2::Keycode, OutputSettings, SimulatorDisplay, SimulatorEvent, Window, 18 | }; 19 | 20 | const BACKGROUND_COLOR: Rgb888 = Rgb888::BLACK; 21 | const FOREGROUND_COLOR: Rgb888 = Rgb888::RED; 22 | const KEYBOARD_DELTA: i32 = 20; 23 | 24 | fn move_circle( 25 | display: &mut SimulatorDisplay, 26 | old_center: Point, 27 | new_center: Point, 28 | ) -> Result<(), core::convert::Infallible> { 29 | // Clear old circle 30 | Circle::with_center(old_center, 200) 31 | .into_styled(PrimitiveStyle::with_fill(BACKGROUND_COLOR)) 32 | .draw(display)?; 33 | 34 | // Draw circle at new location 35 | Circle::with_center(new_center, 200) 36 | .into_styled(PrimitiveStyle::with_fill(FOREGROUND_COLOR)) 37 | .draw(display)?; 38 | 39 | Ok(()) 40 | } 41 | 42 | fn main() -> Result<(), core::convert::Infallible> { 43 | let mut display: SimulatorDisplay = SimulatorDisplay::new(Size::new(800, 480)); 44 | let mut window = Window::new("Click to move circle", &OutputSettings::default()); 45 | 46 | let mut position = Point::new(200, 200); 47 | Circle::with_center(position, 200) 48 | .into_styled(PrimitiveStyle::with_fill(FOREGROUND_COLOR)) 49 | .draw(&mut display)?; 50 | 51 | 'running: loop { 52 | window.update(&display); 53 | 54 | for event in window.events() { 55 | match event { 56 | SimulatorEvent::Quit => break 'running, 57 | SimulatorEvent::KeyDown { keycode, .. } => { 58 | let delta = match keycode { 59 | Keycode::Left => Point::new(-KEYBOARD_DELTA, 0), 60 | Keycode::Right => Point::new(KEYBOARD_DELTA, 0), 61 | Keycode::Up => Point::new(0, -KEYBOARD_DELTA), 62 | Keycode::Down => Point::new(0, KEYBOARD_DELTA), 63 | _ => Point::zero(), 64 | }; 65 | let new_position = position + delta; 66 | move_circle(&mut display, position, new_position)?; 67 | position = new_position; 68 | } 69 | SimulatorEvent::MouseButtonUp { point, .. } => { 70 | move_circle(&mut display, position, point)?; 71 | position = point; 72 | } 73 | _ => {} 74 | } 75 | } 76 | } 77 | 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /examples/png-base64.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics::{ 2 | mono_font::{ascii::FONT_10X20, MonoTextStyle}, 3 | pixelcolor::BinaryColor, 4 | prelude::*, 5 | text::{Alignment, Baseline, Text, TextStyleBuilder}, 6 | }; 7 | use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay}; 8 | 9 | fn main() { 10 | let mut display = SimulatorDisplay::::new(Size::new(256, 64)); 11 | 12 | let large_text = MonoTextStyle::new(&FONT_10X20, BinaryColor::On); 13 | let centered = TextStyleBuilder::new() 14 | .baseline(Baseline::Middle) 15 | .alignment(Alignment::Center) 16 | .build(); 17 | 18 | Text::with_text_style( 19 | "embedded-graphics", 20 | display.bounding_box().center(), 21 | large_text, 22 | centered, 23 | ) 24 | .draw(&mut display) 25 | .unwrap(); 26 | 27 | let output_settings = OutputSettingsBuilder::new().scale(2).build(); 28 | let output_image = display.to_grayscale_output_image(&output_settings); 29 | 30 | println!( 31 | "", 32 | output_image.to_base64_png().unwrap() 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/png-file.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics::{ 2 | mono_font::{ascii::FONT_10X20, MonoTextStyle}, 3 | pixelcolor::BinaryColor, 4 | prelude::*, 5 | text::{Alignment, Baseline, Text, TextStyleBuilder}, 6 | }; 7 | use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay}; 8 | 9 | fn main() { 10 | let mut display = SimulatorDisplay::::new(Size::new(256, 64)); 11 | 12 | let large_text = MonoTextStyle::new(&FONT_10X20, BinaryColor::On); 13 | let centered = TextStyleBuilder::new() 14 | .baseline(Baseline::Middle) 15 | .alignment(Alignment::Center) 16 | .build(); 17 | 18 | Text::with_text_style( 19 | "embedded-graphics", 20 | display.bounding_box().center(), 21 | large_text, 22 | centered, 23 | ) 24 | .draw(&mut display) 25 | .unwrap(); 26 | 27 | let output_settings = OutputSettingsBuilder::new().scale(2).build(); 28 | let output_image = display.to_rgb_output_image(&output_settings); 29 | 30 | let path = std::env::args_os() 31 | .nth(1) 32 | .expect("expected PNG file name argument"); 33 | output_image.save_png(path).unwrap(); 34 | } 35 | -------------------------------------------------------------------------------- /examples/sdl-audio.rs: -------------------------------------------------------------------------------- 1 | //! # Example: SDL Audio 2 | //! 3 | //! This example demonstrates how SDL can be used not only to implement virtual displays, but at the same time 4 | //! to use it as an audio device. Here we implement an oscillator with a modulation of its pitch. 5 | 6 | use std::sync::{ 7 | atomic::{AtomicBool, Ordering}, 8 | Arc, 9 | }; 10 | 11 | use embedded_graphics::{ 12 | mono_font::{ascii::FONT_6X10, MonoTextStyle}, 13 | pixelcolor::BinaryColor, 14 | prelude::*, 15 | text::Text, 16 | }; 17 | use embedded_graphics_simulator::{ 18 | sdl2::Keycode, OutputSettingsBuilder, SimulatorDisplay, SimulatorEvent, Window, 19 | }; 20 | use sdl2::audio::{AudioCallback, AudioSpecDesired}; 21 | 22 | const SAMPLE_RATE: i32 = 44100; 23 | 24 | const PITCH_MIN: f32 = 440.0; 25 | const PITCH_MAX: f32 = 10000.0; 26 | 27 | const PERIOD: f32 = 0.5; // seconds 28 | const SAMPLES_PER_PERIOD: f32 = SAMPLE_RATE as f32 * PERIOD; 29 | const PITCH_CHANGE_PER_SAMPLE: f32 = (PITCH_MAX - PITCH_MIN) / SAMPLES_PER_PERIOD; 30 | 31 | fn main() -> Result<(), core::convert::Infallible> { 32 | // Prepare the audio "engine" with gate control 33 | let gate = Arc::new(AtomicBool::new(false)); 34 | let audio_wrapper = AudioWrapper::new(gate.clone()); 35 | 36 | let audio_spec = AudioSpecDesired { 37 | freq: Some(SAMPLE_RATE), 38 | channels: Some(1), 39 | samples: Some(32), 40 | }; 41 | 42 | // Initialize the SDL audio subsystem. 43 | // 44 | // `sdl2` allows multiple instances of the SDL context to exist, which makes 45 | // it possible to access SDL subsystems which aren't used by the simulator. 46 | // But keep in mind that only one `EventPump` can exists and the simulator 47 | // window creation will fail if the `EventPump` is claimed in advance. 48 | let sdl = sdl2::init().unwrap(); 49 | let audio_subsystem = sdl.audio().unwrap(); 50 | 51 | // Start audio playback by opening the device and setting the custom callback. 52 | let audio_device = audio_subsystem 53 | .open_playback(None, &audio_spec, |_| audio_wrapper) 54 | .unwrap(); 55 | audio_device.resume(); 56 | 57 | let output_settings = OutputSettingsBuilder::new() 58 | .scale(4) 59 | .theme(embedded_graphics_simulator::BinaryColorTheme::OledWhite) 60 | .build(); 61 | 62 | let mut window = Window::new("Simulator audio example", &output_settings); 63 | 64 | let text_style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On); 65 | let text_position = Point::new(25, 30); 66 | let text = Text::new("Press space...", text_position, text_style); 67 | 68 | let mut display: SimulatorDisplay = SimulatorDisplay::new(Size::new(128, 64)); 69 | text.draw(&mut display).unwrap(); 70 | 'running: loop { 71 | window.update(&display); 72 | 73 | for event in window.events() { 74 | match event { 75 | SimulatorEvent::Quit => break 'running, 76 | SimulatorEvent::KeyDown { 77 | keycode, repeat, .. 78 | } if keycode == Keycode::Space && !repeat => { 79 | gate.store(true, Ordering::SeqCst); 80 | display.clear(BinaryColor::On).unwrap(); 81 | } 82 | SimulatorEvent::KeyUp { keycode, .. } => match keycode { 83 | Keycode::Space => { 84 | gate.store(false, Ordering::SeqCst); 85 | display.clear(BinaryColor::Off).unwrap(); 86 | text.draw(&mut display).unwrap(); 87 | } 88 | _ => {} 89 | }, 90 | _ => {} 91 | } 92 | } 93 | } 94 | 95 | Ok(()) 96 | } 97 | 98 | struct AudioWrapper { 99 | gate: Arc, 100 | phase: f32, 101 | pitch: f32, 102 | } 103 | 104 | impl AudioWrapper { 105 | fn new(gate: Arc) -> Self { 106 | Self { 107 | gate, 108 | phase: 0.0, 109 | pitch: PITCH_MIN, 110 | } 111 | } 112 | } 113 | 114 | impl AudioCallback for AudioWrapper { 115 | type Channel = f32; 116 | 117 | fn callback(&mut self, out: &mut [f32]) { 118 | let gate = self.gate.load(Ordering::SeqCst); 119 | if !gate { 120 | self.pitch = PITCH_MIN; 121 | out.fill(0.0); 122 | return; 123 | } 124 | 125 | for x in out.iter_mut() { 126 | self.phase += self.pitch / SAMPLE_RATE as f32; 127 | *x = self.phase.sin(); 128 | 129 | if self.pitch > PITCH_MAX { 130 | self.pitch = PITCH_MIN; 131 | } 132 | 133 | self.pitch += PITCH_CHANGE_PER_SAMPLE; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /examples/themes.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics::{ 2 | mono_font::{ascii::FONT_10X20, MonoTextStyle}, 3 | pixelcolor::BinaryColor, 4 | prelude::*, 5 | text::{Alignment, Baseline, Text, TextStyleBuilder}, 6 | }; 7 | use embedded_graphics_simulator::{ 8 | BinaryColorTheme, OutputSettingsBuilder, SimulatorDisplay, Window, 9 | }; 10 | 11 | fn main() { 12 | let mut display = SimulatorDisplay::::new(Size::new(256, 64)); 13 | 14 | let large_text = MonoTextStyle::new(&FONT_10X20, BinaryColor::On); 15 | let centered = TextStyleBuilder::new() 16 | .baseline(Baseline::Middle) 17 | .alignment(Alignment::Center) 18 | .build(); 19 | 20 | Text::with_text_style( 21 | "embedded-graphics", 22 | display.bounding_box().center(), 23 | large_text, 24 | centered, 25 | ) 26 | .draw(&mut display) 27 | .unwrap(); 28 | 29 | // Uncomment one of the `theme` lines to use a different theme. 30 | let output_settings = OutputSettingsBuilder::new() 31 | //.theme(BinaryColorTheme::LcdGreen) 32 | //.theme(BinaryColorTheme::LcdWhite) 33 | .theme(BinaryColorTheme::LcdBlue) 34 | //.theme(BinaryColorTheme::OledBlue) 35 | //.theme(BinaryColorTheme::OledWhite) 36 | .build(); 37 | 38 | let mut window = Window::new("Themes", &output_settings); 39 | window.show_static(&display); 40 | } 41 | -------------------------------------------------------------------------------- /filter_readme.sed: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/porglezomp/pixel-canvas/blob/develop/gen-readme.sh 2 | 3 | # Remove footer-reference-style doc links like "[`Foo`]: ./foo/trait.Foo.html" 4 | /\[.+\]: .*(struct|enum|trait|type|fn|index)\./d 5 | 6 | # Remove intra-doc links like "[`Foo`]: foo::Foo" 7 | /\[.+\]: .*::/d 8 | 9 | # Remove inline-style doc links like "[`Foo`](./foo/trait.Foo.html)", 10 | # leaving just "`Foo`" in its place 11 | s/\[(.+)\]\(.*(struct|enum|trait|type|fn|index).*\)/\1/g 12 | 13 | # Remove square braces from footer-reference-style inline links like "[`Foo`]", 14 | # leaving "`Foo`" in its place 15 | s/\[(`[^]]*`)\]([^\(:]|$)/\1\2/g 16 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | target_dir := "target" 2 | 3 | #---------- 4 | # Building 5 | #---------- 6 | 7 | build: check-formatting test test-all build-simulator check-readme generate-docs 8 | 9 | # Build the simulator 10 | build-simulator: 11 | cargo build --release --no-default-features 12 | 13 | # Run cargo test in release mode 14 | test: 15 | cargo test --release 16 | 17 | # Run cargo test in release mode with all features enabled 18 | test-all: 19 | cargo test --release --all-features 20 | 21 | # Check the formatting 22 | check-formatting: 23 | cargo fmt --all -- --check 24 | 25 | #------ 26 | # Docs 27 | #------ 28 | 29 | # Generates the docs 30 | generate-docs: 31 | cargo clean --doc 32 | cargo doc --all-features 33 | 34 | # Runs cargo-deadlinks on the docs 35 | check-links: generate-docs 36 | cargo deadlinks 37 | 38 | #---------------------- 39 | # README.md generation 40 | # --------------------- 41 | 42 | # Generate README.md for a single crate 43 | generate-readme: (_build-readme) 44 | cp {{target_dir}}/README.md README.md 45 | 46 | # Check README.md for a single crate 47 | @check-readme: (_build-readme) 48 | diff -q {{target_dir}}/README.md README.md || ( \ 49 | echo -e "\033[1;31mError:\033[0m README.md needs to be regenerated."; \ 50 | echo -e " Run 'just generate-readme' to regenerate.\n"; \ 51 | exit 1 \ 52 | ) 53 | 54 | # Builds README.md for a single crate 55 | _build-readme: 56 | #!/usr/bin/env bash 57 | set -e -o pipefail 58 | mkdir -p {{target_dir}}/readme 59 | echo "Building README.md" 60 | cargo readme | sed -E -f filter_readme.sed > {{target_dir}}/README.md 61 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | pre-release-replacements = [ 2 | {file="CHANGELOG.md", prerelease=true, search="[Uu]nreleased", replace="{{version}}"}, 3 | {file="CHANGELOG.md", prerelease=true, search="\\.\\.\\.HEAD", replace="...{{tag_name}}"}, 4 | {file="CHANGELOG.md", prerelease=true, search="ReleaseDate", replace="{{date}}"}, 5 | {file="CHANGELOG.md", prerelease=true, search="", replace="\n\n## [Unreleased] - ReleaseDate"}, 6 | {file="CHANGELOG.md", prerelease=true, search="", replace="\n[unreleased]: https://github.com/embedded-graphics/{{crate_name}}/compare/{{tag_name}}...HEAD"}, 7 | ] 8 | tag-message = "Release {{crate_name}} {{version}}" 9 | pre-release-commit-message = "Release {{crate_name}} {{version}}" 10 | -------------------------------------------------------------------------------- /rustfmt.nightly.toml: -------------------------------------------------------------------------------- 1 | # Run with `cargo +nightly fmt -- --config-path ./rustfmt.nightly.toml` 2 | # This config only works in nightly. It can be used to clean up doc comments without having to do it 3 | # manually. 4 | 5 | format_code_in_doc_comments = true 6 | merge_imports = true 7 | format_macro_matchers = true 8 | format_macro_bodies = true 9 | -------------------------------------------------------------------------------- /src/display.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::TryFrom, fs::File, io::BufReader, path::Path}; 2 | 3 | use embedded_graphics::{ 4 | pixelcolor::{raw::ToBytes, BinaryColor, Gray8, Rgb888}, 5 | prelude::*, 6 | }; 7 | 8 | use crate::{output_image::OutputImage, output_settings::OutputSettings}; 9 | 10 | /// Simulator display. 11 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 12 | pub struct SimulatorDisplay { 13 | size: Size, 14 | pub(crate) pixels: Box<[C]>, 15 | } 16 | 17 | impl SimulatorDisplay { 18 | /// Creates a new display filled with a color. 19 | /// 20 | /// This constructor can be used if `C` doesn't implement `From` or another 21 | /// default color is wanted. 22 | pub fn with_default_color(size: Size, default_color: C) -> Self { 23 | let pixel_count = size.width as usize * size.height as usize; 24 | let pixels = vec![default_color; pixel_count].into_boxed_slice(); 25 | 26 | SimulatorDisplay { size, pixels } 27 | } 28 | 29 | /// Returns the color of the pixel at a point. 30 | /// 31 | /// # Panics 32 | /// 33 | /// Panics if `point` is outside the display. 34 | pub fn get_pixel(&self, point: Point) -> C { 35 | self.point_to_index(point) 36 | .and_then(|index| self.pixels.get(index).copied()) 37 | .expect("can't get point outside of display") 38 | } 39 | 40 | fn point_to_index(&self, point: Point) -> Option { 41 | if let Ok((x, y)) = <(u32, u32)>::try_from(point) { 42 | if x < self.size.width && y < self.size.height { 43 | return Some((x + y * self.size.width) as usize); 44 | } 45 | } 46 | 47 | None 48 | } 49 | 50 | /// Compares the content of this display with another display. 51 | /// 52 | /// If both displays are equal `None` is returned, otherwise a difference image is returned. 53 | /// All pixels that are different will be filled with `BinaryColor::On` and all equal pixels 54 | /// with `BinaryColor::Off`. 55 | /// 56 | /// # Panics 57 | /// 58 | /// Panics if the both display don't have the same size. 59 | pub fn diff(&self, other: &SimulatorDisplay) -> Option> { 60 | assert!( 61 | self.size == other.size, 62 | // TODO: use Display impl for Size 63 | "both displays must have the same size (self: {}x{}, other: {}x{})", 64 | self.size.width, 65 | self.size.height, 66 | other.size.width, 67 | other.size.height, 68 | ); 69 | 70 | let pixels = self 71 | .bounding_box() 72 | .points() 73 | .map(|p| BinaryColor::from(self.get_pixel(p) != other.get_pixel(p))) 74 | .collect::>() 75 | .into_boxed_slice(); 76 | 77 | if pixels.iter().any(|p| *p == BinaryColor::On) { 78 | Some(SimulatorDisplay { 79 | pixels, 80 | size: self.size, 81 | }) 82 | } else { 83 | None 84 | } 85 | } 86 | } 87 | 88 | impl SimulatorDisplay 89 | where 90 | C: PixelColor + From, 91 | { 92 | /// Creates a new display. 93 | /// 94 | /// The display is filled with `C::from(BinaryColor::Off)`. 95 | pub fn new(size: Size) -> Self { 96 | Self::with_default_color(size, C::from(BinaryColor::Off)) 97 | } 98 | } 99 | 100 | impl SimulatorDisplay 101 | where 102 | C: PixelColor + Into, 103 | { 104 | /// Converts the display contents into a RGB output image. 105 | /// 106 | /// # Examples 107 | /// 108 | /// ```rust 109 | /// use embedded_graphics::{pixelcolor::Rgb888, prelude::*}; 110 | /// use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay}; 111 | /// 112 | /// let output_settings = OutputSettingsBuilder::new().scale(2).build(); 113 | /// 114 | /// let display = SimulatorDisplay::::new(Size::new(128, 64)); 115 | /// 116 | /// // draw something to the display 117 | /// 118 | /// let output_image = display.to_rgb_output_image(&output_settings); 119 | /// assert_eq!(output_image.size(), Size::new(256, 128)); 120 | /// 121 | /// // use output image: 122 | /// // example: output_image.save_png("out.png")?; 123 | /// ``` 124 | pub fn to_rgb_output_image(&self, output_settings: &OutputSettings) -> OutputImage { 125 | let mut output = OutputImage::new(self, output_settings); 126 | output.update(self); 127 | 128 | output 129 | } 130 | 131 | /// Converts the display contents into a grayscale output image. 132 | /// 133 | /// # Examples 134 | /// 135 | /// ```rust 136 | /// use embedded_graphics::{pixelcolor::Gray8, prelude::*}; 137 | /// use embedded_graphics_simulator::{OutputSettingsBuilder, SimulatorDisplay}; 138 | /// 139 | /// let output_settings = OutputSettingsBuilder::new().scale(2).build(); 140 | /// 141 | /// let display = SimulatorDisplay::::new(Size::new(128, 64)); 142 | /// 143 | /// // draw something to the display 144 | /// 145 | /// let output_image = display.to_grayscale_output_image(&output_settings); 146 | /// assert_eq!(output_image.size(), Size::new(256, 128)); 147 | /// 148 | /// // use output image: 149 | /// // example: output_image.save_png("out.png")?; 150 | /// ``` 151 | pub fn to_grayscale_output_image( 152 | &self, 153 | output_settings: &OutputSettings, 154 | ) -> OutputImage { 155 | let mut output = OutputImage::new(self, output_settings); 156 | output.update(self); 157 | 158 | output 159 | } 160 | } 161 | 162 | impl SimulatorDisplay 163 | where 164 | C: PixelColor + ToBytes, 165 | ::Bytes: AsRef<[u8]>, 166 | { 167 | /// Converts the display content to big endian raw data. 168 | pub fn to_be_bytes(&self) -> Vec { 169 | self.to_bytes(ToBytes::to_be_bytes) 170 | } 171 | 172 | /// Converts the display content to little endian raw data. 173 | pub fn to_le_bytes(&self) -> Vec { 174 | self.to_bytes(ToBytes::to_le_bytes) 175 | } 176 | 177 | /// Converts the display content to native endian raw data. 178 | pub fn to_ne_bytes(&self) -> Vec { 179 | self.to_bytes(ToBytes::to_ne_bytes) 180 | } 181 | 182 | fn to_bytes(&self, pixel_to_bytes: F) -> Vec 183 | where 184 | F: Fn(C) -> C::Bytes, 185 | { 186 | let mut bytes = Vec::new(); 187 | 188 | if C::Raw::BITS_PER_PIXEL >= 8 { 189 | for pixel in self.pixels.iter() { 190 | bytes.extend_from_slice(pixel_to_bytes(*pixel).as_ref()) 191 | } 192 | } else { 193 | let pixels_per_byte = 8 / C::Raw::BITS_PER_PIXEL; 194 | 195 | for row in self.pixels.chunks(self.size.width as usize) { 196 | for byte_pixels in row.chunks(pixels_per_byte) { 197 | let mut value = 0; 198 | 199 | for pixel in byte_pixels { 200 | value <<= C::Raw::BITS_PER_PIXEL; 201 | value |= pixel.to_be_bytes().as_ref()[0]; 202 | } 203 | 204 | value <<= C::Raw::BITS_PER_PIXEL * (pixels_per_byte - byte_pixels.len()); 205 | 206 | bytes.push(value); 207 | } 208 | } 209 | } 210 | 211 | bytes 212 | } 213 | } 214 | 215 | impl SimulatorDisplay 216 | where 217 | C: PixelColor + From, 218 | { 219 | /// Loads a PNG file. 220 | pub fn load_png>(path: P) -> image::ImageResult { 221 | let png_file = BufReader::new(File::open(path)?); 222 | let image = image::load(png_file, image::ImageFormat::Png)?.to_rgb8(); 223 | 224 | let pixels = image 225 | .pixels() 226 | .map(|p| Rgb888::new(p[0], p[1], p[2]).into()) 227 | .collect(); 228 | 229 | Ok(Self { 230 | size: Size::new(image.width(), image.height()), 231 | pixels, 232 | }) 233 | } 234 | } 235 | 236 | impl DrawTarget for SimulatorDisplay { 237 | type Color = C; 238 | type Error = core::convert::Infallible; 239 | 240 | fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> 241 | where 242 | I: IntoIterator>, 243 | { 244 | for Pixel(point, color) in pixels.into_iter() { 245 | if let Some(index) = self.point_to_index(point) { 246 | self.pixels[index] = color; 247 | } 248 | } 249 | 250 | Ok(()) 251 | } 252 | } 253 | 254 | impl OriginDimensions for SimulatorDisplay { 255 | fn size(&self) -> Size { 256 | self.size 257 | } 258 | } 259 | 260 | #[cfg(test)] 261 | mod tests { 262 | use super::*; 263 | 264 | use embedded_graphics::{ 265 | pixelcolor::{Gray2, Gray4, Rgb565}, 266 | primitives::{Circle, Line, PrimitiveStyle}, 267 | }; 268 | 269 | #[test] 270 | fn rgb_output_image() { 271 | let mut display = SimulatorDisplay::::new(Size::new(2, 4)); 272 | 273 | Line::new(Point::new(0, 0), Point::new(1, 3)) 274 | .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) 275 | .draw(&mut display) 276 | .unwrap(); 277 | 278 | let image = display.to_rgb_output_image(&OutputSettings::default()); 279 | assert_eq!(image.size(), display.size()); 280 | 281 | let expected: &[u8] = &[ 282 | 255, 255, 255, 0, 0, 0, // 283 | 255, 255, 255, 0, 0, 0, // 284 | 0, 0, 0, 255, 255, 255, // 285 | 0, 0, 0, 255, 255, 255, // 286 | ]; 287 | assert_eq!(image.data.as_ref(), expected); 288 | } 289 | 290 | #[test] 291 | fn grayscale_image_buffer() { 292 | let mut display = SimulatorDisplay::::new(Size::new(2, 4)); 293 | 294 | Line::new(Point::new(0, 0), Point::new(1, 3)) 295 | .into_styled(PrimitiveStyle::with_stroke(BinaryColor::On, 1)) 296 | .draw(&mut display) 297 | .unwrap(); 298 | 299 | let image = display.to_grayscale_output_image(&OutputSettings::default()); 300 | assert_eq!(image.size(), display.size()); 301 | 302 | let expected: &[u8] = &[ 303 | 255, 0, // 304 | 255, 0, // 305 | 0, 255, // 306 | 0, 255, // 307 | ]; 308 | assert_eq!(image.data.as_ref(), expected); 309 | } 310 | 311 | #[test] 312 | fn to_bytes_u1() { 313 | let display = SimulatorDisplay { 314 | size: Size::new(9, 3), 315 | pixels: [ 316 | 1, 0, 0, 0, 0, 0, 0, 1, 0, // 317 | 0, 1, 0, 0, 0, 0, 1, 0, 1, // 318 | 0, 0, 1, 0, 0, 1, 0, 0, 0, // 319 | ] 320 | .iter() 321 | .map(|c| BinaryColor::from(*c != 0)) 322 | .collect::>() 323 | .into_boxed_slice(), 324 | }; 325 | 326 | let expected = [ 327 | 0b10000001, 0b00000000, // 328 | 0b01000010, 0b10000000, // 329 | 0b00100100, 0b00000000, // 330 | ]; 331 | assert_eq!(&display.to_be_bytes(), &expected); 332 | assert_eq!(&display.to_le_bytes(), &expected); 333 | assert_eq!(&display.to_ne_bytes(), &expected); 334 | } 335 | 336 | #[test] 337 | fn to_bytes_u2() { 338 | let display = SimulatorDisplay { 339 | size: Size::new(5, 2), 340 | pixels: [ 341 | 0, 1, 2, 3, 0, // 342 | 1, 0, 3, 2, 1, // 343 | ] 344 | .iter() 345 | .map(|c| Gray2::new(*c)) 346 | .collect::>() 347 | .into_boxed_slice(), 348 | }; 349 | 350 | let expected = [ 351 | 0b00011011, 0b00000000, // 352 | 0b01001110, 0b01000000, // 353 | ]; 354 | assert_eq!(&display.to_be_bytes(), &expected); 355 | assert_eq!(&display.to_le_bytes(), &expected); 356 | assert_eq!(&display.to_ne_bytes(), &expected); 357 | } 358 | 359 | #[test] 360 | fn to_bytes_u4() { 361 | let display = SimulatorDisplay { 362 | size: Size::new(5, 4), 363 | pixels: [ 364 | 0x0, 0x1, 0x2, 0x3, 0x4, // 365 | 0x5, 0x6, 0x7, 0x8, 0x9, // 366 | 0xA, 0xB, 0xC, 0xD, 0xE, // 367 | 0xF, 0x0, 0x0, 0x0, 0x0, // 368 | ] 369 | .iter() 370 | .map(|c| Gray4::new(*c)) 371 | .collect::>() 372 | .into_boxed_slice(), 373 | }; 374 | 375 | let expected = [ 376 | 0x01, 0x23, 0x40, // 377 | 0x56, 0x78, 0x90, // 378 | 0xAB, 0xCD, 0xE0, // 379 | 0xF0, 0x00, 0x00, // 380 | ]; 381 | assert_eq!(&display.to_be_bytes(), &expected); 382 | assert_eq!(&display.to_le_bytes(), &expected); 383 | assert_eq!(&display.to_ne_bytes(), &expected); 384 | } 385 | 386 | #[test] 387 | fn to_bytes_u8() { 388 | let expected = [ 389 | 1, 2, 3, // 390 | 11, 12, 13, // 391 | ]; 392 | 393 | let display = SimulatorDisplay { 394 | size: Size::new(3, 2), 395 | pixels: expected 396 | .iter() 397 | .copied() 398 | .map(Gray8::new) 399 | .collect::>() 400 | .into_boxed_slice(), 401 | }; 402 | 403 | assert_eq!(&display.to_be_bytes(), &expected); 404 | assert_eq!(&display.to_le_bytes(), &expected); 405 | assert_eq!(&display.to_ne_bytes(), &expected); 406 | } 407 | 408 | #[test] 409 | fn to_bytes_u16() { 410 | let expected = vec![Rgb565::new(0x10, 0x00, 0x00), Rgb565::new(0x00, 0x00, 0x01)]; 411 | 412 | let display = SimulatorDisplay { 413 | size: Size::new(2, 1), 414 | pixels: expected.clone().into_boxed_slice(), 415 | }; 416 | 417 | assert_eq!(&display.to_be_bytes(), &[0x80, 0x00, 0x00, 0x01]); 418 | assert_eq!(&display.to_le_bytes(), &[0x00, 0x80, 0x01, 0x00]); 419 | } 420 | 421 | #[test] 422 | fn to_bytes_u24() { 423 | let expected = vec![Rgb888::new(0x80, 0x00, 0x00), Rgb888::new(0x00, 0x00, 0x01)]; 424 | 425 | let display = SimulatorDisplay { 426 | size: Size::new(2, 1), 427 | pixels: expected.clone().into_boxed_slice(), 428 | }; 429 | 430 | assert_eq!( 431 | &display.to_be_bytes(), 432 | &[0x80, 0x00, 0x00, 0x00, 0x00, 0x01] 433 | ); 434 | assert_eq!( 435 | &display.to_le_bytes(), 436 | &[0x00, 0x00, 0x80, 0x01, 0x00, 0x00] 437 | ); 438 | } 439 | 440 | #[test] 441 | fn diff_equal() { 442 | let display = SimulatorDisplay::::new(Size::new(4, 6)); 443 | let expected = display.clone(); 444 | 445 | assert_eq!(display.diff(&expected), None); 446 | } 447 | 448 | #[test] 449 | fn diff_not_equal() { 450 | let circle = Circle::new(Point::zero(), 3); 451 | 452 | let mut display = SimulatorDisplay::::new(Size::new(4, 6)); 453 | let expected = display.clone(); 454 | 455 | circle 456 | .into_styled(PrimitiveStyle::with_fill(BinaryColor::On)) 457 | .draw(&mut display) 458 | .unwrap(); 459 | 460 | assert_eq!(display.diff(&expected), Some(display)); 461 | } 462 | 463 | #[test] 464 | #[should_panic(expected = "both displays must have the same size (self: 4x6, other: 4x5)")] 465 | fn diff_wrong_size() { 466 | let display = SimulatorDisplay::::new(Size::new(4, 6)); 467 | let expected = SimulatorDisplay::::new(Size::new(4, 5)); 468 | 469 | assert_eq!(display.diff(&expected), None); 470 | } 471 | } 472 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! ![It can display all sorts of embedded-graphics test code.](https://raw.githubusercontent.com/embedded-graphics/embedded-graphics/master/assets/simulator-demo.png) 2 | //! 3 | //! The simulator can be used to test and debug 4 | //! [embedded-graphics](https://crates.io/crates/embedded-graphics) code, or produce examples and 5 | //! interactive demos to show off embedded graphics features. 6 | //! 7 | //! # [Examples](https://github.com/embedded-graphics/examples) 8 | //! 9 | //! More simulator examples can be found in the [examples repository](https://github.com/embedded-graphics/examples). 10 | //! 11 | //! ## Simulate a 128x64 SSD1306 OLED 12 | //! 13 | //! ```rust,no_run 14 | //! use embedded_graphics::{ 15 | //! pixelcolor::BinaryColor, 16 | //! prelude::*, 17 | //! primitives::{Circle, Line, Rectangle, PrimitiveStyle}, 18 | //! mono_font::{ascii::FONT_6X9, MonoTextStyle}, 19 | //! text::Text, 20 | //! }; 21 | //! use embedded_graphics_simulator::{BinaryColorTheme, SimulatorDisplay, Window, OutputSettingsBuilder}; 22 | //! 23 | //! fn main() -> Result<(), core::convert::Infallible> { 24 | //! let mut display = SimulatorDisplay::::new(Size::new(128, 64)); 25 | //! 26 | //! let line_style = PrimitiveStyle::with_stroke(BinaryColor::On, 1); 27 | //! let text_style = MonoTextStyle::new(&FONT_6X9, BinaryColor::On); 28 | //! 29 | //! Circle::new(Point::new(72, 8), 48) 30 | //! .into_styled(line_style) 31 | //! .draw(&mut display)?; 32 | //! 33 | //! Line::new(Point::new(48, 16), Point::new(8, 16)) 34 | //! .into_styled(line_style) 35 | //! .draw(&mut display)?; 36 | //! 37 | //! Line::new(Point::new(48, 16), Point::new(64, 32)) 38 | //! .into_styled(line_style) 39 | //! .draw(&mut display)?; 40 | //! 41 | //! Rectangle::new(Point::new(79, 15), Size::new(34, 34)) 42 | //! .into_styled(line_style) 43 | //! .draw(&mut display)?; 44 | //! 45 | //! Text::new("Hello World!", Point::new(5, 5), text_style).draw(&mut display)?; 46 | //! 47 | //! let output_settings = OutputSettingsBuilder::new() 48 | //! .theme(BinaryColorTheme::OledBlue) 49 | //! .build(); 50 | //! Window::new("Hello World", &output_settings).show_static(&display); 51 | //! 52 | //! Ok(()) 53 | //! } 54 | //! ``` 55 | //! 56 | //! # Setup 57 | //! 58 | //! The simulator uses SDL2 and its development libraries which must be installed to build and run 59 | //! it. 60 | //! 61 | //! ## Linux (`apt`) 62 | //! 63 | //! ```bash 64 | //! sudo apt install libsdl2-dev 65 | //! ``` 66 | //! 67 | //! ## macOS (`brew`) 68 | //! 69 | //! ```bash 70 | //! brew install sdl2 71 | //! ``` 72 | //! 73 | //! Users on Apple silicon or with custom installation directories will need to 74 | //! set `LIBRARY_PATH` for the linker to find the installed SDL2 package: 75 | //! 76 | //! ```bash 77 | //! export LIBRARY_PATH="$LIBRARY_PATH:$(brew --prefix)/lib" 78 | //! ``` 79 | //! More information can be found in the 80 | //! [SDL2 documentation](https://github.com/Rust-SDL2/rust-sdl2#homebrew). 81 | //! 82 | //! ## Windows 83 | //! 84 | //! The Windows install process is a bit more involved, but it _does_ work. See [the Rust-SDL2 85 | //! crate's README](https://github.com/Rust-SDL2/rust-sdl2) for instructions. There are multiple 86 | //! ways to get it working, but probably the simplest method is copying the binaries as shown 87 | //! [here](https://github.com/Rust-SDL2/rust-sdl2#windows-msvc). 88 | //! 89 | //! # Creating screenshots 90 | //! 91 | //! Screenshots of programs, that use [`Window`] to display a simulated display, can be created by 92 | //! setting the `EG_SIMULATOR_DUMP` or `EG_SIMULATOR_DUMP_RAW` environment variable: 93 | //! 94 | //! ```bash 95 | //! EG_SIMULATOR_DUMP=screenshot.png cargo run 96 | //! ``` 97 | //! 98 | //! By setting the variable the display passed to the first [`Window::update`] call gets exported as 99 | //! a PNG file to the specified path. After the file is exported the process is terminated. 100 | //! 101 | //! The difference between `EG_SIMULATOR_DUMP` and `EG_SIMULATOR_DUMP_RAW` is that the first method 102 | //! applies the output settings before exporting the PNG file and the later dumps the unaltered 103 | //! display content. 104 | //! 105 | //! # Exporting images 106 | //! 107 | //! If a program doesn't require to display a window and only needs to export one or more images, a 108 | //! [`SimulatorDisplay`] can also be converted to an [`image`] crate 109 | //! [`ImageBuffer`] by using the [`to_rgb_output_image`] or [`to_grayscale_output_image`] methods. 110 | //! The resulting buffer can then be used to save the display content to any format supported by 111 | //! [`image`]. 112 | //! 113 | //! # Using the simulator in CI 114 | //! 115 | //! The simulator supports two environment variables to check if the display content matches a 116 | //! reference PNG file: `EG_SIMULATOR_CHECK` and `EG_SIMULATOR_CHECK_RAW`. If the display content 117 | //! of the first [`Window::update`] call doesn't match the reference image the process exits with a 118 | //! non zero exit exit code. Otherwise the process will exit with a zero exit code. 119 | //! 120 | //! ```bash 121 | //! EG_SIMULATOR_CHECK=screenshot.png cargo run || echo "Display doesn't match PNG file" 122 | //! ``` 123 | //! 124 | //! `EG_SIMULATOR_CHECK` assumes that the reference image was created using the same 125 | //! `OutputSetting`s, while `EG_SIMULATOR_CHECK_RAW` assumes an unstyled reference image. 126 | //! 127 | //! # Usage without SDL2 128 | //! 129 | //! When the simulator is used in headless/CI environments that don't require showing a window, SDL2 130 | //! support can be disabled. This removes the requirement of SDL2 being installed on the target machine, 131 | //! but still allows the simulator to be used to generate images. 132 | //! 133 | //! The `with-sdl` feature is enabled by default and can be disabled by adding `default-features = false` to the dependency: 134 | //! 135 | //! ```toml 136 | //! [dependencies.embedded-graphics-simulator] 137 | //! version = "0.2.0" 138 | //! default-features = false 139 | //! ``` 140 | //! 141 | //! See the [Choosing 142 | //! Features](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#choosing-features) 143 | //! Cargo manifest documentation for more details. 144 | //! 145 | //! [`ImageBuffer`]: image::ImageBuffer 146 | //! [`to_rgb_output_image`]: SimulatorDisplay::to_rgb_output_image 147 | //! [`to_grayscale_output_image`]: SimulatorDisplay::to_grayscale_output_image 148 | 149 | #![deny( 150 | missing_docs, 151 | rustdoc::broken_intra_doc_links, 152 | rustdoc::private_intra_doc_links 153 | )] 154 | 155 | mod display; 156 | mod output_image; 157 | mod output_settings; 158 | mod theme; 159 | mod window; 160 | 161 | #[cfg(feature = "with-sdl")] 162 | pub use window::SimulatorEvent; 163 | 164 | /// Re-exported types from sdl2 crate. 165 | /// 166 | /// The types in this module are used in the [`SimulatorEvent`] enum and are re-exported from the 167 | /// `sdl2` crate to make it possible to use them without adding a dependency to `sdl2`. 168 | #[cfg(feature = "with-sdl")] 169 | pub mod sdl2 { 170 | pub use sdl2::{ 171 | keyboard::{Keycode, Mod}, 172 | mouse::{MouseButton, MouseWheelDirection}, 173 | }; 174 | } 175 | 176 | pub use crate::{ 177 | display::SimulatorDisplay, 178 | output_image::OutputImage, 179 | output_settings::{OutputSettings, OutputSettingsBuilder}, 180 | theme::BinaryColorTheme, 181 | window::Window, 182 | }; 183 | -------------------------------------------------------------------------------- /src/output_image.rs: -------------------------------------------------------------------------------- 1 | use std::{convert::TryFrom, marker::PhantomData, path::Path}; 2 | 3 | use base64::Engine; 4 | use embedded_graphics::{ 5 | pixelcolor::{raw::ToBytes, Gray8, Rgb888, RgbColor}, 6 | prelude::*, 7 | primitives::Rectangle, 8 | }; 9 | use image::{ 10 | codecs::png::{CompressionType, FilterType, PngEncoder}, 11 | ImageBuffer, ImageEncoder, Luma, Rgb, 12 | }; 13 | 14 | use crate::{display::SimulatorDisplay, output_settings::OutputSettings}; 15 | 16 | /// Output image. 17 | /// 18 | /// An output image is the result of applying [`OutputSettings`] to a [`SimulatorDisplay`]. It can 19 | /// be used to save a simulator display to a PNG file. 20 | /// 21 | #[derive(Debug, PartialEq, Eq, Clone)] 22 | pub struct OutputImage { 23 | size: Size, 24 | pub(crate) data: Box<[u8]>, 25 | pub(crate) output_settings: OutputSettings, 26 | color_type: PhantomData, 27 | } 28 | 29 | impl OutputImage 30 | where 31 | C: PixelColor + From + ToBytes, 32 | ::Bytes: AsRef<[u8]>, 33 | { 34 | /// Creates a new output image. 35 | pub(crate) fn new( 36 | display: &SimulatorDisplay, 37 | output_settings: &OutputSettings, 38 | ) -> Self 39 | where 40 | DisplayC: PixelColor + Into, 41 | { 42 | let size = output_settings.framebuffer_size(display); 43 | 44 | // Create an empty pixel buffer, filled with the background color. 45 | let background_color = C::from(output_settings.theme.convert(Rgb888::BLACK)).to_be_bytes(); 46 | let data = background_color 47 | .as_ref() 48 | .iter() 49 | .copied() 50 | .cycle() 51 | .take(size.width as usize * size.height as usize * background_color.as_ref().len()) 52 | .collect::>() 53 | .into_boxed_slice(); 54 | 55 | Self { 56 | size, 57 | data, 58 | output_settings: output_settings.clone(), 59 | color_type: PhantomData, 60 | } 61 | } 62 | 63 | /// Updates the image from a [`SimulatorDisplay`]. 64 | pub fn update(&mut self, display: &SimulatorDisplay) 65 | where 66 | DisplayC: PixelColor + Into, 67 | { 68 | let pixel_pitch = (self.output_settings.scale + self.output_settings.pixel_spacing) as i32; 69 | let pixel_size = Size::new(self.output_settings.scale, self.output_settings.scale); 70 | 71 | for p in display.bounding_box().points() { 72 | let raw_color = display.get_pixel(p).into(); 73 | let themed_color = self.output_settings.theme.convert(raw_color); 74 | let output_color = C::from(themed_color).to_be_bytes(); 75 | let output_color = output_color.as_ref(); 76 | 77 | for p in Rectangle::new(p * pixel_pitch, pixel_size).points() { 78 | if let Ok((x, y)) = <(u32, u32)>::try_from(p) { 79 | let start_index = (x + y * self.size.width) as usize * output_color.len(); 80 | 81 | self.data[start_index..start_index + output_color.len()] 82 | .copy_from_slice(output_color) 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | impl OutputImage { 90 | /// Saves the image content to a PNG file. 91 | pub fn save_png>(&self, path: PATH) -> image::ImageResult<()> { 92 | let png = self.encode_png()?; 93 | 94 | std::fs::write(path, png)?; 95 | 96 | Ok(()) 97 | } 98 | 99 | /// Returns the image as a base64 encoded PNG. 100 | pub fn to_base64_png(&self) -> image::ImageResult { 101 | let png = self.encode_png()?; 102 | 103 | Ok(base64::engine::general_purpose::STANDARD.encode(png)) 104 | } 105 | 106 | fn encode_png(&self) -> image::ImageResult> { 107 | let mut png = Vec::new(); 108 | 109 | PngEncoder::new_with_quality(&mut png, CompressionType::Best, FilterType::default()) 110 | .write_image( 111 | self.data.as_ref(), 112 | self.size.width, 113 | self.size.height, 114 | C::IMAGE_COLOR_TYPE.into(), 115 | )?; 116 | 117 | Ok(png) 118 | } 119 | 120 | /// Returns the output image as an [`image`] crate [`ImageBuffer`]. 121 | pub fn as_image_buffer(&self) -> ImageBuffer { 122 | ImageBuffer::from_raw(self.size.width, self.size.height, self.data.as_ref()).unwrap() 123 | } 124 | } 125 | 126 | impl OriginDimensions for OutputImage { 127 | fn size(&self) -> Size { 128 | self.size 129 | } 130 | } 131 | 132 | pub trait OutputImageColor { 133 | type ImageColor: image::Pixel + 'static; 134 | const IMAGE_COLOR_TYPE: image::ColorType; 135 | } 136 | 137 | impl OutputImageColor for Gray8 { 138 | type ImageColor = Luma; 139 | const IMAGE_COLOR_TYPE: image::ColorType = image::ColorType::L8; 140 | } 141 | 142 | impl OutputImageColor for Rgb888 { 143 | type ImageColor = Rgb; 144 | const IMAGE_COLOR_TYPE: image::ColorType = image::ColorType::Rgb8; 145 | } 146 | -------------------------------------------------------------------------------- /src/output_settings.rs: -------------------------------------------------------------------------------- 1 | use crate::{display::SimulatorDisplay, theme::BinaryColorTheme}; 2 | use embedded_graphics::prelude::*; 3 | 4 | /// Output settings. 5 | #[derive(Debug, PartialEq, Eq, Clone)] 6 | pub struct OutputSettings { 7 | /// Pixel scale. 8 | pub scale: u32, 9 | /// Spacing between pixels. 10 | pub pixel_spacing: u32, 11 | /// Binary color theme. 12 | pub theme: BinaryColorTheme, 13 | /// Maximum frames per second shown in the window. 14 | pub max_fps: u32, 15 | } 16 | 17 | impl OutputSettings { 18 | /// Calculates the size of the framebuffer required to display the scaled display. 19 | pub(crate) fn framebuffer_size(&self, display: &SimulatorDisplay) -> Size 20 | where 21 | C: PixelColor, 22 | { 23 | let width = display.size().width; 24 | let height = display.size().height; 25 | let output_width = width * self.scale + width.saturating_sub(1) * self.pixel_spacing; 26 | let output_height = height * self.scale + height.saturating_sub(1) * self.pixel_spacing; 27 | 28 | Size::new(output_width, output_height) 29 | } 30 | } 31 | 32 | #[cfg(feature = "with-sdl")] 33 | impl OutputSettings { 34 | /// Translates a output coordinate to the corresponding display coordinate. 35 | pub(crate) const fn output_to_display(&self, output_point: Point) -> Point { 36 | let pitch = self.pixel_pitch() as i32; 37 | Point::new(output_point.x / pitch, output_point.y / pitch) 38 | } 39 | 40 | pub(crate) const fn pixel_pitch(&self) -> u32 { 41 | self.scale + self.pixel_spacing 42 | } 43 | } 44 | 45 | impl Default for OutputSettings { 46 | fn default() -> Self { 47 | OutputSettingsBuilder::new().build() 48 | } 49 | } 50 | 51 | /// Output settings builder. 52 | #[derive(Default)] 53 | pub struct OutputSettingsBuilder { 54 | scale: Option, 55 | pixel_spacing: Option, 56 | theme: BinaryColorTheme, 57 | max_fps: Option, 58 | } 59 | 60 | impl OutputSettingsBuilder { 61 | /// Creates new output settings builder. 62 | pub fn new() -> Self { 63 | Self::default() 64 | } 65 | 66 | /// Sets the pixel scale. 67 | /// 68 | /// A scale of `2` or higher is useful for viewing the simulator on high DPI displays. 69 | /// 70 | /// # Panics 71 | /// 72 | /// Panics if the scale is set to `0`. 73 | pub fn scale(mut self, scale: u32) -> Self { 74 | assert!(scale > 0, "scale must be > 0"); 75 | 76 | self.scale = Some(scale); 77 | 78 | self 79 | } 80 | 81 | /// Sets the binary color theme. 82 | /// 83 | /// The binary color theme defines the mapping between the two display colors 84 | /// and the output. The variants provided by the [`BinaryColorTheme`] enum 85 | /// simulate the color scheme of commonly used display types. 86 | /// 87 | /// Most binary color displays are relatively small individual pixels 88 | /// are hard to recognize on higher resolution screens. Because of this 89 | /// some scaling is automatically applied to the output when a theme is 90 | /// set and no scaling was specified explicitly. 91 | /// 92 | /// Note that a theme should only be set when an monochrome display is used. 93 | /// Setting a theme when using a color display will cause an corrupted output. 94 | /// 95 | pub fn theme(mut self, theme: BinaryColorTheme) -> Self { 96 | self.theme = theme; 97 | 98 | self.scale.get_or_insert(3); 99 | self.pixel_spacing.get_or_insert(1); 100 | 101 | self 102 | } 103 | 104 | /// Sets the gap between pixels. 105 | /// 106 | /// Most lower resolution displays have visible gaps between individual pixels. 107 | /// This effect can be simulated by setting the pixel spacing to a value greater 108 | /// than `0`. 109 | pub fn pixel_spacing(mut self, pixel_spacing: u32) -> Self { 110 | self.pixel_spacing = Some(pixel_spacing); 111 | 112 | self 113 | } 114 | 115 | /// Sets the FPS limit of the window. 116 | pub fn max_fps(mut self, max_fps: u32) -> Self { 117 | self.max_fps = Some(max_fps); 118 | 119 | self 120 | } 121 | 122 | /// Builds the output settings. 123 | pub fn build(self) -> OutputSettings { 124 | OutputSettings { 125 | scale: self.scale.unwrap_or(1), 126 | pixel_spacing: self.pixel_spacing.unwrap_or(0), 127 | theme: self.theme, 128 | max_fps: self.max_fps.unwrap_or(60), 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/theme.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics::pixelcolor::{Rgb888, RgbColor}; 2 | 3 | /// Color theme for binary displays 4 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] 5 | pub enum BinaryColorTheme { 6 | /// A simple on/off, non-styled display with black background and white pixels 7 | #[default] 8 | Default, 9 | 10 | /// Inverted colors. 11 | Inverted, 12 | 13 | /// An on/off classic LCD-like display with white background 14 | LcdWhite, 15 | 16 | /// An on/off classic LCD-like display with green background and dark grey pixels 17 | LcdGreen, 18 | 19 | /// An on/off LCD-like display with light blue background and blue-white pixels 20 | LcdBlue, 21 | 22 | /// An on/off OLED-like display with a black background and white pixels 23 | OledWhite, 24 | 25 | /// An on/off OLED-like display with a dark blue background and light blue pixels 26 | OledBlue, 27 | 28 | /// Custom binary color theme/mapping 29 | Custom { 30 | /// The color used for the "off" state pixels. 31 | color_off: Rgb888, 32 | /// The color used for the "on" state pixels. 33 | color_on: Rgb888, 34 | }, 35 | } 36 | 37 | fn map_color(color: Rgb888, color_off: Rgb888, color_on: Rgb888) -> Rgb888 { 38 | match color { 39 | Rgb888::BLACK => color_off, 40 | _ => color_on, 41 | } 42 | } 43 | 44 | impl BinaryColorTheme { 45 | /// Gets the theme's pixel color for a given pixel state. 46 | pub(crate) fn convert(self, color: Rgb888) -> Rgb888 { 47 | match self { 48 | BinaryColorTheme::Default => color, 49 | BinaryColorTheme::Custom { 50 | color_off, 51 | color_on, 52 | } => map_color(color, color_off, color_on), 53 | BinaryColorTheme::Inverted => { 54 | Rgb888::new(255 - color.r(), 255 - color.g(), 255 - color.b()) 55 | } 56 | BinaryColorTheme::LcdWhite => { 57 | map_color(color, Rgb888::new(245, 245, 245), Rgb888::new(32, 32, 32)) 58 | } 59 | BinaryColorTheme::LcdGreen => { 60 | map_color(color, Rgb888::new(120, 185, 50), Rgb888::new(32, 32, 32)) 61 | } 62 | BinaryColorTheme::LcdBlue => { 63 | map_color(color, Rgb888::new(70, 80, 230), Rgb888::new(230, 230, 255)) 64 | } 65 | BinaryColorTheme::OledBlue => { 66 | map_color(color, Rgb888::new(0, 20, 40), Rgb888::new(0, 210, 255)) 67 | } 68 | BinaryColorTheme::OledWhite => map_color(color, Rgb888::new(20, 20, 20), Rgb888::WHITE), 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/window/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env, 3 | fs::File, 4 | io::BufReader, 5 | ops::Deref, 6 | process, thread, 7 | time::{Duration, Instant}, 8 | }; 9 | 10 | use embedded_graphics::{pixelcolor::Rgb888, prelude::*}; 11 | 12 | use crate::{ 13 | display::SimulatorDisplay, output_image::OutputImage, output_settings::OutputSettings, 14 | }; 15 | 16 | #[cfg(feature = "with-sdl")] 17 | mod sdl_window; 18 | 19 | #[cfg(feature = "with-sdl")] 20 | pub use sdl_window::{SdlWindow, SimulatorEvent}; 21 | 22 | /// Simulator window 23 | #[allow(dead_code)] 24 | pub struct Window { 25 | framebuffer: Option>, 26 | #[cfg(feature = "with-sdl")] 27 | sdl_window: Option, 28 | title: String, 29 | output_settings: OutputSettings, 30 | desired_loop_duration: Duration, 31 | frame_start: Instant, 32 | } 33 | 34 | impl Window { 35 | /// Creates a new simulator window. 36 | pub fn new(title: &str, output_settings: &OutputSettings) -> Self { 37 | Self { 38 | framebuffer: None, 39 | #[cfg(feature = "with-sdl")] 40 | sdl_window: None, 41 | title: String::from(title), 42 | output_settings: output_settings.clone(), 43 | desired_loop_duration: Duration::from_millis(1000 / output_settings.max_fps as u64), 44 | frame_start: Instant::now(), 45 | } 46 | } 47 | 48 | /// Updates the window. 49 | pub fn update(&mut self, display: &SimulatorDisplay) 50 | where 51 | C: PixelColor + Into + From, 52 | { 53 | if let Ok(path) = env::var("EG_SIMULATOR_CHECK") { 54 | let output = display.to_rgb_output_image(&self.output_settings); 55 | 56 | let png_file = BufReader::new(File::open(path).unwrap()); 57 | let expected = image::load(png_file, image::ImageFormat::Png) 58 | .unwrap() 59 | .to_rgb8(); 60 | 61 | let png_size = Size::new(expected.width(), expected.height()); 62 | 63 | assert!( 64 | output.size().eq(&png_size), 65 | "display dimensions don't match PNG dimensions (display: {}x{}, PNG: {}x{})", 66 | output.size().width, 67 | output.size().height, 68 | png_size.width, 69 | png_size.height 70 | ); 71 | 72 | assert!( 73 | output 74 | .as_image_buffer() 75 | .as_raw() 76 | .eq(&expected.as_raw().deref()), 77 | "display content doesn't match PNG file", 78 | ); 79 | 80 | process::exit(0); 81 | } 82 | 83 | if let Ok(path) = env::var("EG_SIMULATOR_CHECK_RAW") { 84 | let expected = SimulatorDisplay::load_png(path).unwrap(); 85 | 86 | assert!( 87 | display.size().eq(&expected.size()), 88 | "display dimensions don't match PNG dimensions (display: {}x{}, PNG: {}x{})", 89 | display.size().width, 90 | display.size().height, 91 | expected.size().width, 92 | expected.size().height 93 | ); 94 | 95 | assert!( 96 | display.pixels.eq(&expected.pixels), 97 | "display content doesn't match PNG file", 98 | ); 99 | 100 | process::exit(0); 101 | } 102 | 103 | if let Ok(path) = env::var("EG_SIMULATOR_DUMP") { 104 | display 105 | .to_rgb_output_image(&self.output_settings) 106 | .save_png(path) 107 | .unwrap(); 108 | process::exit(0); 109 | } 110 | 111 | if let Ok(path) = env::var("EG_SIMULATOR_DUMP_RAW") { 112 | display 113 | .to_rgb_output_image(&OutputSettings::default()) 114 | .save_png(path) 115 | .unwrap(); 116 | process::exit(0); 117 | } 118 | 119 | #[cfg(feature = "with-sdl")] 120 | { 121 | if self.framebuffer.is_none() { 122 | self.framebuffer = Some(OutputImage::new(display, &self.output_settings)); 123 | } 124 | 125 | if self.sdl_window.is_none() { 126 | self.sdl_window = Some(SdlWindow::new(display, &self.title, &self.output_settings)); 127 | } 128 | 129 | let framebuffer = self.framebuffer.as_mut().unwrap(); 130 | let sdl_window = self.sdl_window.as_mut().unwrap(); 131 | 132 | framebuffer.update(display); 133 | sdl_window.update(framebuffer); 134 | } 135 | 136 | thread::sleep( 137 | (self.frame_start + self.desired_loop_duration) 138 | .saturating_duration_since(Instant::now()), 139 | ); 140 | 141 | self.frame_start = Instant::now(); 142 | } 143 | 144 | /// Shows a static display. 145 | /// 146 | /// This methods updates the window once and loops until the simulator window 147 | /// is closed. 148 | pub fn show_static(&mut self, display: &SimulatorDisplay) 149 | where 150 | C: PixelColor + Into + From, 151 | { 152 | self.update(display); 153 | 154 | #[cfg(feature = "with-sdl")] 155 | 'running: loop { 156 | if self.events().any(|e| e == SimulatorEvent::Quit) { 157 | break 'running; 158 | } 159 | thread::sleep(Duration::from_millis(20)); 160 | } 161 | } 162 | 163 | /// Returns an iterator of all captured SimulatorEvents. 164 | /// 165 | /// # Panics 166 | /// 167 | /// Panics if called before [`update`](Self::update) is called at least once. 168 | #[cfg(feature = "with-sdl")] 169 | pub fn events(&mut self) -> impl Iterator + '_ { 170 | self.sdl_window 171 | .as_mut() 172 | .unwrap() 173 | .events(&self.output_settings) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/window/sdl_window.rs: -------------------------------------------------------------------------------- 1 | use embedded_graphics::{ 2 | pixelcolor::Rgb888, 3 | prelude::{PixelColor, Point, Size}, 4 | }; 5 | use sdl2::{ 6 | event::Event, 7 | keyboard::{Keycode, Mod}, 8 | mouse::{MouseButton, MouseWheelDirection}, 9 | pixels::PixelFormatEnum, 10 | render::{Canvas, Texture, TextureCreator}, 11 | video::WindowContext, 12 | EventPump, 13 | }; 14 | 15 | use crate::{OutputImage, OutputSettings, SimulatorDisplay}; 16 | 17 | /// A derivation of [`sdl2::event::Event`] mapped to embedded-graphics coordinates 18 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 19 | pub enum SimulatorEvent { 20 | /// A keypress event, fired on keyUp 21 | KeyUp { 22 | /// The key being released 23 | keycode: Keycode, 24 | /// Any modifier being held at the time of keyup 25 | keymod: Mod, 26 | /// Whether the key is repeating 27 | repeat: bool, 28 | }, 29 | /// A keypress event, fired on keyDown 30 | KeyDown { 31 | /// The key being pressed 32 | keycode: Keycode, 33 | /// Any modifier being held at the time of keydown 34 | keymod: Mod, 35 | /// Whether the key is repeating 36 | repeat: bool, 37 | }, 38 | /// A mouse click event, fired on mouseUp 39 | MouseButtonUp { 40 | /// The mouse button being released 41 | mouse_btn: MouseButton, 42 | /// The location of the mouse in Simulator coordinates 43 | point: Point, 44 | }, 45 | /// A mouse click event, fired on mouseDown 46 | MouseButtonDown { 47 | /// The mouse button being pressed 48 | mouse_btn: MouseButton, 49 | /// The location of the mouse in Simulator coordinates 50 | point: Point, 51 | }, 52 | /// A mouse wheel event 53 | MouseWheel { 54 | /// The scroll wheel delta in the x and y direction 55 | scroll_delta: Point, 56 | /// The directionality of the scroll (normal or flipped) 57 | direction: MouseWheelDirection, 58 | }, 59 | /// Mouse move event 60 | MouseMove { 61 | /// The current mouse position 62 | point: Point, 63 | }, 64 | /// An exit event 65 | Quit, 66 | } 67 | 68 | pub struct SdlWindow { 69 | canvas: Canvas, 70 | event_pump: EventPump, 71 | window_texture: SdlWindowTexture, 72 | size: Size, 73 | } 74 | 75 | impl SdlWindow { 76 | pub fn new( 77 | display: &SimulatorDisplay, 78 | title: &str, 79 | output_settings: &OutputSettings, 80 | ) -> Self 81 | where 82 | C: PixelColor + Into, 83 | { 84 | let sdl_context = sdl2::init().unwrap(); 85 | let video_subsystem = sdl_context.video().unwrap(); 86 | 87 | let size = output_settings.framebuffer_size(display); 88 | 89 | let window = video_subsystem 90 | .window(title, size.width, size.height) 91 | .position_centered() 92 | .build() 93 | .unwrap(); 94 | 95 | let canvas = window.into_canvas().build().unwrap(); 96 | let event_pump = sdl_context.event_pump().unwrap(); 97 | 98 | let window_texture = SdlWindowTextureBuilder { 99 | texture_creator: canvas.texture_creator(), 100 | texture_builder: |creator: &TextureCreator| { 101 | creator 102 | .create_texture_streaming(PixelFormatEnum::RGB24, size.width, size.height) 103 | .unwrap() 104 | }, 105 | } 106 | .build(); 107 | 108 | Self { 109 | canvas, 110 | event_pump, 111 | window_texture, 112 | size, 113 | } 114 | } 115 | 116 | pub fn update(&mut self, framebuffer: &OutputImage) { 117 | self.window_texture.with_mut(|fields| { 118 | fields 119 | .texture 120 | .update( 121 | None, 122 | framebuffer.data.as_ref(), 123 | self.size.width as usize * 3, 124 | ) 125 | .unwrap(); 126 | }); 127 | 128 | self.canvas 129 | .copy(self.window_texture.borrow_texture(), None, None) 130 | .unwrap(); 131 | self.canvas.present(); 132 | } 133 | 134 | /// Handle events 135 | /// Return an iterator of all captured SimulatorEvent 136 | pub fn events( 137 | &mut self, 138 | output_settings: &OutputSettings, 139 | ) -> impl Iterator + '_ { 140 | let output_settings = output_settings.clone(); 141 | self.event_pump 142 | .poll_iter() 143 | .filter_map(move |event| match event { 144 | Event::Quit { .. } 145 | | Event::KeyDown { 146 | keycode: Some(Keycode::Escape), 147 | .. 148 | } => Some(SimulatorEvent::Quit), 149 | Event::KeyDown { 150 | keycode, 151 | keymod, 152 | repeat, 153 | .. 154 | } => keycode.map(|valid_keycode| SimulatorEvent::KeyDown { 155 | keycode: valid_keycode, 156 | keymod, 157 | repeat, 158 | }), 159 | Event::KeyUp { 160 | keycode, 161 | keymod, 162 | repeat, 163 | .. 164 | } => keycode.map(|valid_keycode| SimulatorEvent::KeyUp { 165 | keycode: valid_keycode, 166 | keymod, 167 | repeat, 168 | }), 169 | Event::MouseButtonUp { 170 | x, y, mouse_btn, .. 171 | } => { 172 | let point = output_settings.output_to_display(Point::new(x, y)); 173 | Some(SimulatorEvent::MouseButtonUp { point, mouse_btn }) 174 | } 175 | Event::MouseButtonDown { 176 | x, y, mouse_btn, .. 177 | } => { 178 | let point = output_settings.output_to_display(Point::new(x, y)); 179 | Some(SimulatorEvent::MouseButtonDown { point, mouse_btn }) 180 | } 181 | Event::MouseWheel { 182 | x, y, direction, .. 183 | } => Some(SimulatorEvent::MouseWheel { 184 | scroll_delta: Point::new(x, y), 185 | direction, 186 | }), 187 | Event::MouseMotion { x, y, .. } => { 188 | let point = output_settings.output_to_display(Point::new(x, y)); 189 | Some(SimulatorEvent::MouseMove { point }) 190 | } 191 | _ => None, 192 | }) 193 | } 194 | } 195 | 196 | #[ouroboros::self_referencing] 197 | struct SdlWindowTexture { 198 | texture_creator: TextureCreator, 199 | #[borrows(texture_creator)] 200 | #[covariant] 201 | texture: Texture<'this>, 202 | } 203 | --------------------------------------------------------------------------------