├── .github ├── copyright.sh └── workflows │ ├── bloat.yml │ ├── ci.yml │ └── gh-pages.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── clippy.toml ├── docs ├── .gitignore ├── README.md ├── book.toml ├── book_examples │ ├── Cargo.toml │ └── src │ │ ├── custom_widgets_md.rs │ │ ├── data_md.rs │ │ ├── env_md.rs │ │ ├── getting_started_2_md.rs │ │ ├── getting_started_md.rs │ │ ├── lens_md.rs │ │ ├── lib.rs │ │ └── widget_md.rs └── src │ ├── 01_overview.md │ ├── 02_getting_started.md │ ├── 03_data.md │ ├── 04_widget.md │ ├── 05_lens.md │ ├── 06_env.md │ ├── 07_resolution_independence.md │ ├── 08_widgets_in_depth.md │ ├── 09_more_information.md │ └── SUMMARY.md ├── druid-derive ├── Cargo.toml ├── README.md ├── src │ ├── attr.rs │ ├── data.rs │ ├── lens.rs │ └── lib.rs └── tests │ ├── data.rs │ ├── ignore.rs │ ├── lens_generic.rs │ ├── ui.rs │ ├── ui │ ├── lens-attributes.rs │ ├── simple-lens.rs │ ├── with-empty-struct.rs │ ├── with-empty-struct.stderr │ ├── with-enum.rs │ ├── with-enum.stderr │ ├── with-snake_case.rs │ ├── with-snake_case.stderr │ ├── with-tuple-struct.rs │ ├── with-tuple-struct.stderr │ ├── with-union.rs │ └── with-union.stderr │ ├── with_lens.rs │ └── with_same.rs ├── druid-shell ├── Cargo.toml ├── README.md ├── build.rs ├── examples │ ├── edit_text.rs │ ├── invalidate.rs │ ├── perftest.rs │ ├── quit.rs │ └── shello.rs └── src │ ├── application.rs │ ├── backend │ ├── gtk │ │ ├── application.rs │ │ ├── clipboard.rs │ │ ├── dialog.rs │ │ ├── error.rs │ │ ├── keycodes.rs │ │ ├── menu.rs │ │ ├── mod.rs │ │ ├── screen.rs │ │ ├── util.rs │ │ └── window.rs │ ├── mac │ │ ├── appkit.rs │ │ ├── application.rs │ │ ├── clipboard.rs │ │ ├── dialog.rs │ │ ├── error.rs │ │ ├── keyboard.rs │ │ ├── menu.rs │ │ ├── mod.rs │ │ ├── screen.rs │ │ ├── text_input.rs │ │ ├── util.rs │ │ └── window.rs │ ├── mod.rs │ ├── shared │ │ ├── keyboard.rs │ │ ├── linux │ │ │ ├── env.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── timer.rs │ │ └── xkb │ │ │ ├── keycodes.rs │ │ │ ├── mod.rs │ │ │ └── xkbcommon_sys.rs │ ├── wayland │ │ ├── .README.md │ │ ├── application.rs │ │ ├── clipboard.rs │ │ ├── display.rs │ │ ├── error.rs │ │ ├── events.rs │ │ ├── keyboard.rs │ │ ├── menu.rs │ │ ├── mod.rs │ │ ├── outputs │ │ │ ├── mod.rs │ │ │ └── output.rs │ │ ├── pointers.rs │ │ ├── screen.rs │ │ ├── surfaces │ │ │ ├── buffers.rs │ │ │ ├── idle.rs │ │ │ ├── layershell.rs │ │ │ ├── mod.rs │ │ │ ├── popup.rs │ │ │ ├── surface.rs │ │ │ └── toplevel.rs │ │ └── window.rs │ ├── web │ │ ├── application.rs │ │ ├── clipboard.rs │ │ ├── error.rs │ │ ├── keycodes.rs │ │ ├── menu.rs │ │ ├── mod.rs │ │ ├── screen.rs │ │ └── window.rs │ ├── windows │ │ ├── accels.rs │ │ ├── application.rs │ │ ├── clipboard.rs │ │ ├── dcomp.rs │ │ ├── dialog.rs │ │ ├── error.rs │ │ ├── keyboard.rs │ │ ├── keycodes.rs │ │ ├── menu.rs │ │ ├── mod.rs │ │ ├── paint.rs │ │ ├── screen.rs │ │ ├── timers.rs │ │ ├── util.rs │ │ └── window.rs │ └── x11 │ │ ├── application.rs │ │ ├── clipboard.rs │ │ ├── dialog.rs │ │ ├── error.rs │ │ ├── menu.rs │ │ ├── mod.rs │ │ ├── screen.rs │ │ ├── util.rs │ │ └── window.rs │ ├── clipboard.rs │ ├── common_util.rs │ ├── dialog.rs │ ├── error.rs │ ├── hotkey.rs │ ├── keyboard.rs │ ├── lib.rs │ ├── menu.rs │ ├── mouse.rs │ ├── platform │ ├── linux.rs │ ├── mac.rs │ └── mod.rs │ ├── region.rs │ ├── scale.rs │ ├── screen.rs │ ├── text.rs │ ├── util.rs │ └── window.rs ├── druid ├── Cargo.toml ├── README.md ├── examples │ ├── anim.rs │ ├── assets │ │ ├── PicWithAlpha.png │ │ ├── tiger.svg │ │ └── xi.image │ ├── async_event.rs │ ├── blocking_function.rs │ ├── calc.rs │ ├── cursor.rs │ ├── custom_widget.rs │ ├── disabled.rs │ ├── either.rs │ ├── event_viewer.rs │ ├── flex.rs │ ├── game_of_life.rs │ ├── hello.rs │ ├── hello_web │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── index.html │ │ ├── index.js │ │ └── src │ │ │ └── lib.rs │ ├── identity.rs │ ├── image.rs │ ├── input_region.rs │ ├── invalidation.rs │ ├── layout.rs │ ├── lens.rs │ ├── list.rs │ ├── markdown_preview.rs │ ├── multiwin.rs │ ├── open_save.rs │ ├── panels.rs │ ├── readme.md │ ├── scroll.rs │ ├── scroll_colors.rs │ ├── slider.rs │ ├── split_demo.rs │ ├── styled_text.rs │ ├── sub_window.rs │ ├── svg.rs │ ├── switches.rs │ ├── tabs.rs │ ├── text.rs │ ├── textbox.rs │ ├── timer.rs │ ├── transparency.rs │ ├── value_formatting │ │ ├── Cargo.toml │ │ ├── README.md │ │ └── src │ │ │ ├── formatters.rs │ │ │ ├── main.rs │ │ │ └── widgets.rs │ ├── view_switcher.rs │ ├── web │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── build.rs │ │ └── src │ │ │ └── lib.rs │ ├── widget_gallery.rs │ └── z_stack.rs ├── resources │ └── i18n │ │ ├── de-DE │ │ └── builtin.ftl │ │ ├── en-US │ │ └── builtin.ftl │ │ └── fr-CA │ │ └── builtin.ftl └── src │ ├── app.rs │ ├── app_delegate.rs │ ├── bloom.rs │ ├── box_constraints.rs │ ├── command.rs │ ├── contexts.rs │ ├── core.rs │ ├── data.rs │ ├── debug_state.rs │ ├── dialog.rs │ ├── env.rs │ ├── event.rs │ ├── ext_event.rs │ ├── lens │ ├── lens.rs │ └── mod.rs │ ├── lib.rs │ ├── localization.rs │ ├── menu │ ├── mod.rs │ └── sys.rs │ ├── mouse.rs │ ├── scroll_component.rs │ ├── sub_window.rs │ ├── tests │ ├── harness.rs │ ├── helpers.rs │ ├── invalidation_tests.rs │ ├── layout_tests.rs │ └── mod.rs │ ├── text │ ├── attribute.rs │ ├── backspace.rs │ ├── editable_text.rs │ ├── font_descriptor.rs │ ├── format.rs │ ├── input_component.rs │ ├── input_methods.rs │ ├── layout.rs │ ├── mod.rs │ ├── movement.rs │ ├── rich_text.rs │ └── storage.rs │ ├── theme.rs │ ├── util.rs │ ├── widget │ ├── added.rs │ ├── align.rs │ ├── aspect_ratio_box.rs │ ├── button.rs │ ├── checkbox.rs │ ├── click.rs │ ├── clip_box.rs │ ├── common.rs │ ├── container.rs │ ├── controller.rs │ ├── disable_if.rs │ ├── either.rs │ ├── env_scope.rs │ ├── flex.rs │ ├── identity_wrapper.rs │ ├── image.rs │ ├── intrinsic_width.rs │ ├── invalidation.rs │ ├── label.rs │ ├── lens_wrap.rs │ ├── list.rs │ ├── maybe.rs │ ├── mod.rs │ ├── padding.rs │ ├── painter.rs │ ├── parse.rs │ ├── progress_bar.rs │ ├── radio.rs │ ├── scope.rs │ ├── scroll.rs │ ├── sized_box.rs │ ├── slider.rs │ ├── spinner.rs │ ├── split.rs │ ├── stepper.rs │ ├── svg.rs │ ├── switch.rs │ ├── tabs.rs │ ├── textbox.rs │ ├── value_textbox.rs │ ├── view_switcher.rs │ ├── widget.rs │ ├── widget_ext.rs │ ├── widget_wrapper.rs │ └── z_stack.rs │ ├── win_handler.rs │ └── window.rs └── rustfmt.toml /.github/copyright.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # If there are new files with headers that can't match the conditions here, 4 | # then the files can be ignored by an additional glob argument via the -g flag. 5 | # For example: 6 | # -g "!src/special_file.rs" 7 | # -g "!src/special_directory" 8 | 9 | # Check all the standard Rust source files 10 | output=$(rg "^// Copyright (19|20)[\d]{2} (.+ and )?the Druid Authors( and .+)?$\n^// SPDX-License-Identifier: Apache-2\.0$\n\n" --files-without-match --multiline -g "*.rs" -g "!{druid-derive/tests/ui,docs/book_examples/src}" .) 11 | 12 | if [ -n "$output" ]; then 13 | echo -e "The following files lack the correct copyright header:\n" 14 | echo $output 15 | echo -e "\n\nPlease add the following header:\n" 16 | echo "// Copyright $(date +%Y) the Druid Authors" 17 | echo "// SPDX-License-Identifier: Apache-2.0" 18 | echo -e "\n... rest of the file ...\n" 19 | exit 1 20 | fi 21 | 22 | echo "All files have correct copyright headers." 23 | exit 0 24 | -------------------------------------------------------------------------------- /.github/workflows/bloat.yml: -------------------------------------------------------------------------------- 1 | on: 2 | issue_comment: 3 | types: [created, edited] 4 | name: bloat check 5 | 6 | jobs: 7 | bloat_check: 8 | runs-on: macOS-latest 9 | name: post binary size change info 10 | # if it isn't an issue comment run every time, otherwise only run if the comment starts with '/bloat' 11 | if: (!startsWith(github.event_name, 'issue_comment') || startsWith(github.event.comment.body, '/bloat')) 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: get revisions 17 | id: get_revs 18 | uses: cmyr/bloat-cmp/get-revs@v2 19 | with: 20 | command: /bloat 21 | myToken: ${{ secrets.GITHUB_TOKEN }} 22 | 23 | - name: fetch refs 24 | run: git fetch origin ${{ steps.get_revs.outputs.fetch }} 25 | if: steps.get_revs.outputs.fetch != '' 26 | 27 | - name: checkout base 28 | uses: actions/checkout@v2 29 | with: 30 | ref: ${{ steps.get_revs.outputs.base }} 31 | 32 | - name: setup stable toolchain 33 | uses: actions-rs/toolchain@v1 34 | with: 35 | toolchain: stable 36 | override: true 37 | 38 | - name: build base 39 | if: steps.get_revs.outputs.base != steps.get_revs.outputs.head 40 | uses: actions-rs/cargo@v1 41 | with: 42 | command: build 43 | args: --release --examples 44 | 45 | - name: get old sizes 46 | if: steps.get_revs.outputs.base != steps.get_revs.outputs.head 47 | id: old 48 | uses: cmyr/bloat-cmp/get-sizes@v2 49 | with: 50 | paths: > 51 | target/release/examples/calc 52 | target/release/examples/scroll_colors 53 | target/release/examples/multiwin 54 | target/release/examples/flex 55 | target/release/examples/styled_text 56 | target/release/examples/custom_widget 57 | 58 | - name: checkout head 59 | uses: actions/checkout@v2 60 | with: 61 | clean: false # avoid rebuilding artifacts unnecessarily 62 | ref: ${{ steps.get_revs.outputs.head }} 63 | 64 | - name: build head 65 | if: steps.get_revs.outputs.base != steps.get_revs.outputs.head 66 | uses: actions-rs/cargo@v1 67 | with: 68 | command: build 69 | args: --release --examples 70 | 71 | - name: get new sizes 72 | if: steps.get_revs.outputs.base != steps.get_revs.outputs.head 73 | id: new 74 | uses: cmyr/bloat-cmp/get-sizes@v2 75 | with: 76 | paths: > 77 | target/release/examples/calc 78 | target/release/examples/scroll_colors 79 | target/release/examples/multiwin 80 | target/release/examples/flex 81 | target/release/examples/styled_text 82 | target/release/examples/custom_widget 83 | 84 | - name: compare 85 | if: steps.get_revs.outputs.base != steps.get_revs.outputs.head 86 | id: bloatcmp 87 | uses: cmyr/bloat-cmp/compare@v2 88 | with: 89 | old: ${{ steps.old.outputs.rawSizes }} 90 | new: ${{ steps.new.outputs.rawSizes }} 91 | 92 | - name: comment 93 | if: steps.get_revs.outputs.base != steps.get_revs.outputs.head 94 | uses: cmyr/bloat-cmp/post-comment@v2 95 | with: 96 | stats: ${{ steps.bloatcmp.outputs.stats }} 97 | myToken: ${{ secrets.GITHUB_TOKEN }} 98 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup mdBook 15 | uses: peaceiris/actions-mdbook@v1 16 | with: 17 | mdbook-version: '0.4.30' 18 | 19 | - run: mdbook build docs 20 | 21 | - name: Deploy 22 | uses: peaceiris/actions-gh-pages@v3 23 | with: 24 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 25 | publish_branch: gh-pages 26 | publish_dir: ./docs/book 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Rust 2 | target/ 3 | Cargo.lock 4 | 5 | ## Intellij Idea 6 | .idea/ 7 | 8 | ## Visual Studio Code 9 | .vscode/ 10 | 11 | ## Sublime Text 12 | *.sublime-workspace 13 | *.sublime-project 14 | 15 | ## Vim 16 | tags 17 | Session.vim 18 | Sessionx.vim 19 | 20 | ## macOS 21 | .DS_Store 22 | .AppleDouble 23 | .LSOverride 24 | 25 | ## Windows 26 | Thumbs.db 27 | ehthumbs.db 28 | ehthumbs_vista.db 29 | *.lnk 30 | 31 | ## Linux 32 | *~ 33 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of Druid's significant contributors. 2 | # 3 | # This does not necessarily list everyone who has contributed code, 4 | # especially since many employees of one corporation may be contributing. 5 | # To see the full list of contributors, see the revision history in 6 | # source control. 7 | Google LLC 8 | Raph Levien 9 | Hilmar Gústafsson 10 | Dmitry Borodin 11 | Kaiyin Zhong 12 | Kaur Kuut 13 | Leopold Luley 14 | Andrey Kabylin 15 | Robert Wittams 16 | Jaap Aarts 17 | Maximilian Köstler 18 | Bruno Dupuis 19 | Christopher Noel Hesse 20 | Marcin Zając 21 | Laura Gallo 22 | Tim Murison 23 | Manmeet Singh 24 | Simon Fell 25 | Nick Larsen 26 | Thomas McAndrew 27 | Jared O'Connell 28 | Матвей Т -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "druid", 5 | "druid-shell", 6 | "druid-derive", 7 | "docs/book_examples", 8 | "druid/examples/web", 9 | "druid/examples/hello_web", 10 | "druid/examples/value_formatting", 11 | ] 12 | default-members = [ 13 | "druid", 14 | "druid-shell", 15 | "druid-derive", 16 | ] 17 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | # The default clippy value for this is 8 bytes, which is chosen to improve performance on 32-bit. 2 | # Given that druid is being designed for the future and already even mobile phones have 64-bit CPUs, 3 | # it makes sense to optimize for 64-bit and accept the performance hits on 32-bit. 4 | # 16 bytes is the number of bytes that fits into two 64-bit CPU registers. 5 | trivial-copy-size-limit = 16 6 | 7 | # The default clippy value for this is 250, which causes warnings for rather simple types 8 | # like Box, which seems overly strict. The new value of 400 is 9 | # a simple guess. It might be worth lowering this, or using the default, in the future. 10 | type-complexity-threshold = 400 -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | book_examples/Cargo.lock 3 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Druid mdBook documentation 2 | This folder contains Druid documentation in mdBook format. mdBook allows documentation written in 3 | markdown to be published as html. This README.md gives some pointers for contributors on how to 4 | edit, preview and publish the documentation. 5 | 6 | ## Editing 7 | mdBook handles writing documentation in a similar way to writing software. Documentation 'source 8 | code' lives in the `docs/src` folder in the form of markdown files. It can be built and published 9 | as html using the mdBook tool. 10 | To edit documentation you edit the corresponding markdown file in `docs/src`. The 11 | `docs/src/SUMMARY.md` file contains the index for the documentation with links to files for all the 12 | chapters. 13 | 14 | ## Preview documentation 15 | To preview the documentation or to host it on your own system for offline viewing the mdBook tool 16 | needs to be installed. The easiest way to install it is from the crates.io repository using cargo. 17 | `cargo install mdbook` 18 | 19 | After this you can start mdBook to serve the documentation locally using 20 | `mdbook serve` from the `docs\` directory. This will serve documentation on `http://localhost:3000` 21 | 22 | ## Publish documentation 23 | To publish documentation to github pages the documentation needs to be built as html and then moved 24 | to the `gh-pages` branch. This can be done manually or by the build server. 25 | To build the documentation from the project root run; 26 | `mdbook build docs` 27 | This will build the documentation to the `docs\book` folder. This folder can then be copied onto the 28 | `gh-pages` branch. This will tell github to publish the documentation. For the Druid repository it 29 | will be hosted on [https://linebender.org/druid/] 30 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Mendelt Siebenga"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Druid" 7 | 8 | [output.html] 9 | site-url = "/druid/" 10 | -------------------------------------------------------------------------------- /docs/book_examples/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "book_examples" 3 | version = "0.1.0" 4 | license = "Apache-2.0" 5 | repository = "https://github.com/linebender/druid" 6 | authors = ["Colin Rofls "] 7 | edition = "2021" 8 | publish = false 9 | 10 | [dependencies] 11 | druid = { path = "../../druid", features = [ "im" ] } 12 | im = { version = "15.1.0" } 13 | -------------------------------------------------------------------------------- /docs/book_examples/src/custom_widgets_md.rs: -------------------------------------------------------------------------------- 1 | use druid::keyboard_types::Key; 2 | use druid::widget::{Controller, Label, Painter, SizedBox, TextBox}; 3 | use druid::{ 4 | Color, Env, Event, EventCtx, PaintCtx, RenderContext, Selector, TimerToken, Widget, WidgetExt, 5 | }; 6 | use std::time::Duration; 7 | 8 | const CORNER_RADIUS: f64 = 4.0; 9 | const STROKE_WIDTH: f64 = 2.0; 10 | 11 | // ANCHOR: color_swatch 12 | fn make_color_swatch() -> Painter { 13 | Painter::new(|ctx: &mut PaintCtx, data: &Color, env: &Env| { 14 | let bounds = ctx.size().to_rect(); 15 | let rounded = bounds.to_rounded_rect(CORNER_RADIUS); 16 | ctx.fill(rounded, data); 17 | ctx.stroke(rounded, &env.get(druid::theme::PRIMARY_DARK), STROKE_WIDTH); 18 | }) 19 | } 20 | // ANCHOR_END: color_swatch 21 | 22 | // ANCHOR: sized_swatch 23 | fn sized_swatch() -> impl Widget { 24 | SizedBox::new(make_color_swatch()).width(20.0).height(20.0) 25 | } 26 | // ANCHOR_END: sized_swatch 27 | 28 | // ANCHOR: background_label 29 | fn background_label() -> impl Widget { 30 | Label::dynamic(|color: &Color, _| { 31 | let (r, g, b, _) = color.as_rgba8(); 32 | format!("#{r:X}{g:X}{b:X}") 33 | }) 34 | .background(make_color_swatch()) 35 | } 36 | // ANCHOR_END: background_label 37 | 38 | // ANCHOR: annoying_textbox 39 | const ACTION: Selector = Selector::new("hello.textbox-action"); 40 | const DELAY: Duration = Duration::from_millis(300); 41 | 42 | struct TextBoxActionController { 43 | timer: Option, 44 | } 45 | 46 | impl TextBoxActionController { 47 | pub fn new() -> Self { 48 | TextBoxActionController { timer: None } 49 | } 50 | } 51 | 52 | impl Controller> for TextBoxActionController { 53 | fn event( 54 | &mut self, 55 | child: &mut TextBox, 56 | ctx: &mut EventCtx, 57 | event: &Event, 58 | data: &mut String, 59 | env: &Env, 60 | ) { 61 | match event { 62 | Event::KeyDown(k) if k.key == Key::Enter => { 63 | ctx.submit_command(ACTION); 64 | } 65 | Event::KeyUp(k) if k.key == Key::Enter => { 66 | self.timer = Some(ctx.request_timer(DELAY)); 67 | child.event(ctx, event, data, env); 68 | } 69 | Event::Timer(token) if Some(*token) == self.timer => { 70 | ctx.submit_command(ACTION); 71 | } 72 | _ => child.event(ctx, event, data, env), 73 | } 74 | } 75 | } 76 | // ANCHOR_END: annoying_textbox 77 | -------------------------------------------------------------------------------- /docs/book_examples/src/data_md.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::rc_buffer)] 2 | 3 | #[derive(Clone, PartialEq)] 4 | struct DateTime(std::time::Instant); 5 | 6 | // ANCHOR: derive 7 | use druid::Data; 8 | use std::sync::Arc; 9 | 10 | #[derive(Clone, Data)] 11 | /// The main model for a todo list application. 12 | struct TodoList { 13 | items: Arc>, 14 | } 15 | 16 | #[derive(Clone, Data)] 17 | /// A single todo item. 18 | struct TodoItem { 19 | category: Category, 20 | title: String, 21 | note: Option, 22 | completed: bool, 23 | 24 | // `Data` is implemented for any `Arc`. 25 | due_date: Option>, 26 | 27 | // You can specify a custom comparison fn 28 | // (anything with the signature (&T, &T) -> bool). 29 | #[data(same_fn = "PartialEq::eq")] 30 | added_date: DateTime, 31 | 32 | // You can specify that a field should 33 | // be skipped when computing same-ness 34 | #[data(ignore)] 35 | debug_timestamp: usize, 36 | } 37 | 38 | #[derive(Clone, Data, PartialEq)] 39 | /// The three types of tasks in the world. 40 | enum Category { 41 | Work, 42 | Play, 43 | Revolution, 44 | } 45 | // ANCHOR_END: derive 46 | -------------------------------------------------------------------------------- /docs/book_examples/src/env_md.rs: -------------------------------------------------------------------------------- 1 | use druid::widget::Label; 2 | use druid::{Color, Key, WidgetExt}; 3 | 4 | // ANCHOR: key_or_value 5 | const IMPORTANT_LABEL_COLOR: Key = Key::new("org.linebender.example.important-label-color"); 6 | const RED: Color = Color::rgb8(0xFF, 0, 0); 7 | 8 | fn make_labels() { 9 | let with_value = Label::<()>::new("Warning!").with_text_color(RED); 10 | let with_key = Label::<()>::new("Warning!").with_text_color(IMPORTANT_LABEL_COLOR); 11 | } 12 | // ANCHOR_END: key_or_value 13 | 14 | // ANCHOR: env_scope 15 | fn scoped_label() { 16 | let my_label = Label::<()>::new("Warning!").env_scope(|env, _| { 17 | env.set(druid::theme::TEXT_COLOR, Color::BLACK); 18 | env.set(druid::theme::TEXT_SIZE_NORMAL, 18.0); 19 | }); 20 | } 21 | // ANCHOR_END: env_scope 22 | -------------------------------------------------------------------------------- /docs/book_examples/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Code samples used in the book, stored here so they are easier to test in CI. 2 | 3 | #![allow(dead_code, unused_variables)] 4 | 5 | mod custom_widgets_md; 6 | mod data_md; 7 | mod env_md; 8 | mod getting_started_2_md; 9 | mod getting_started_md; 10 | mod lens_md; 11 | mod widget_md; 12 | -------------------------------------------------------------------------------- /docs/book_examples/src/widget_md.rs: -------------------------------------------------------------------------------- 1 | // ANCHOR: padded_label 2 | use druid::widget::{Label, Padding}; 3 | 4 | fn padded_label() { 5 | let label: Label<()> = Label::new("Humour me"); 6 | let padded = Padding::new((4.0, 8.0), label); 7 | } 8 | // ANCHOR_END: padded_label 9 | 10 | // ANCHOR: align_center 11 | use druid::widget::Align; 12 | 13 | fn align_center() { 14 | let label: Label<()> = Label::new("Center me"); 15 | let centered = Align::centered(label); 16 | } 17 | // ANCHOR_END: align_center 18 | 19 | // ANCHOR: stepper_builder 20 | use druid::widget::Stepper; 21 | 22 | fn steppers() { 23 | // A Stepper with default parameters 24 | let stepper1 = Stepper::new(); 25 | 26 | // A Stepper that operates over a custom range 27 | let stepper2 = Stepper::new().with_range(10.0, 50.0); 28 | 29 | // A Stepper with a custom range *and* a custom step size, that 30 | // wraps around past its min and max values: 31 | let stepper3 = Stepper::new() 32 | .with_range(10.0, 50.0) 33 | .with_step(2.5) 34 | .with_wraparound(true); 35 | } 36 | // ANCHOR_END: stepper_builder 37 | 38 | #[rustfmt::skip] 39 | mod padded_stepper_raw { 40 | // ANCHOR: padded_stepper_raw 41 | use druid::widget::{Align, Padding, Stepper}; 42 | 43 | fn padded_stepper() { 44 | let stepper = Stepper::new().with_range(10.0, 50.0); 45 | let padding = Padding::new(8.0, stepper); 46 | let padded_and_center_aligned_stepper = Align::centered(padding); 47 | } 48 | // ANCHOR_END: padded_stepper_raw 49 | } 50 | 51 | #[rustfmt::skip] 52 | mod padded_stepper_widgetext { 53 | // ANCHOR: padded_stepper_widgetext 54 | use druid::widget::{Stepper, WidgetExt}; 55 | 56 | fn padded_stepper() { 57 | let padded_and_center_aligned_stepper = 58 | Stepper::new().with_range(10.0, 50.0).padding(8.0).center(); 59 | } 60 | // ANCHOR_END: padded_stepper_widgetext 61 | } 62 | 63 | // ANCHOR: flex_builder 64 | use druid::widget::Flex; 65 | 66 | fn flex_builder() -> Flex<()> { 67 | Flex::column() 68 | .with_child(Label::new("Number One")) 69 | .with_child(Label::new("Number Two")) 70 | .with_child(Label::new("Some Other Number")) 71 | } 72 | // ANCHOR_END: flex_builder 73 | -------------------------------------------------------------------------------- /docs/src/01_overview.md: -------------------------------------------------------------------------------- 1 | # Druid 2 | 3 | **UNMAINTAINED** 4 | 5 | **The Druid project has been discontinued.** 6 | 7 | New development effort moved on to [Xilem], which has a lot of fundamental changes to allow for 8 | a wider variety of applications with better performance, but it also heavily inherits from Druid. 9 | We see [Xilem] as the future of Druid. 10 | 11 | ## Introduction 12 | 13 | Druid is a framework for building simple graphical applications. 14 | 15 | Druid is composed of a number of related projects. [`druid-shell`] is a 16 | low-level library that provides a common abstraction for interacting with the 17 | current OS & window manager. [`piet`] is an abstraction for doing 2D graphics; 18 | [`kurbo`] is a library for 2D geometry; and [`druid`] itself is an opinionated set of 19 | high-level APIs for building cross-platform desktop applications. 20 | 21 | The framework is *data oriented*. It shares many ideas (and is directly inspired by) 22 | contemporary declarative UI frameworks such as [Flutter], [Jetpack Compose], 23 | and [SwiftUI], while also attempting to be conceptually simple and largely 24 | *non-magical*. A programmer familiar with Rust should be able to understand how 25 | Druid works without special difficulty. 26 | 27 | ## Prerequisites 28 | 29 | This tutorial assumes basic familiarity with Rust and a working setup with the basic tooling like 30 | Rustup and Cargo. This tutorial will use stable Rust (v1.65.0 at the time of writing) and the latest 31 | released version of Druid (v0.8). 32 | 33 | ## Key Concepts 34 | 35 | - **[the `Data` trait]**: How you represent your application model. 36 | - **[the `Widget` trait]**: How you represent your UI. 37 | - **[the `Lens` trait]**: How you associate parts of your model with parts of 38 | your UI. 39 | 40 | 41 | [`druid-shell`]: https://docs.rs/druid-shell 42 | [`druid`]: https://docs.rs/druid 43 | [`piet`]: https://docs.rs/piet 44 | [`kurbo`]: https://docs.rs/kurbo 45 | [Flutter]: https://flutter.dev 46 | [Jetpack Compose]: https://developer.android.com/jetpack/compose 47 | [SwiftUI]: https://developer.apple.com/documentation/swiftui 48 | [the `Data` trait]: ./03_data.md 49 | [the `Widget` trait]: ./04_widget.md 50 | [the `Lens` trait]: ./05_lens.md 51 | [Xilem]: https://github.com/linebender/xilem 52 | -------------------------------------------------------------------------------- /docs/src/08_widgets_in_depth.md: -------------------------------------------------------------------------------- 1 | # Create custom widgets 2 | 3 | The `Widget` trait is the heart of Druid, and in any serious application you 4 | will eventually need to create and use custom `Widget`s. 5 | 6 | ## `Painter` and `Controller` 7 | 8 | There are two helper widgets in Druid that let you customize widget behaviour 9 | without needing to implement the full widget trait: [`Painter`] and 10 | [`Controller`]. 11 | 12 | ### Painter 13 | 14 | The [`Painter`] widget lets you draw arbitrary custom content, but cannot 15 | respond to events or otherwise contain update logic. Its general use is to 16 | either provide a custom background to some other widget, or to implement 17 | something like an icon or another graphical element that will be contained in 18 | some other widget. 19 | 20 | For instance, if we had some color data and we wanted to display it as a swatch 21 | with rounded corners, we could use a `Painter`: 22 | 23 | ```rust,noplaypen 24 | {{#include ../book_examples/src/custom_widgets_md.rs:color_swatch}} 25 | ``` 26 | 27 | `Painter` uses all the space that is available to it; if you want to give it a 28 | set size, you must pass it explicit constraints, such as by wrapping it in a 29 | [`SizedBox`]: 30 | 31 | ```rust,noplaypen 32 | {{#include ../book_examples/src/custom_widgets_md.rs:sized_swatch}} 33 | ``` 34 | 35 | One other useful thing about `Painter` is that it can be used as the background 36 | of a [`Container`] widget. If we wanted to have a label that used our swatch 37 | as a background, we could do: 38 | 39 | ```rust,noplaypen 40 | {{#include ../book_examples/src/custom_widgets_md.rs:background_label}} 41 | ``` 42 | 43 | (This uses the [`background`] method on [`WidgetExt`] to embed our label in a 44 | container.) 45 | 46 | ### Controller 47 | 48 | The [`Controller`] trait is sort of the inverse of `Painter`; it is a way to 49 | make widgets that handle events, but don't do any layout or drawing. The idea 50 | here is that you can use some `Controller` type to customize the behaviour of 51 | some set of children. 52 | 53 | The [`Controller`] trait has `event`, `update`, and `lifecycle` methods, just 54 | like [`Widget`]; it does not have `paint` or `layout` methods. Also unlike 55 | [`Widget`], all of its methods are optional; you can override only the method 56 | that you need. 57 | 58 | There's one other difference to the `Controller` methods; it is explicitly 59 | passed a mutable reference to its child in each method, so that it can modify it 60 | or forward events as needed. 61 | 62 | As an arbitrary example, here is how you might use a `Controller` to make a 63 | textbox fire some action (say doing a search) 300ms after the last keypress: 64 | 65 | ```rust,noplaypen 66 | {{#include ../book_examples/src/custom_widgets_md.rs:annoying_textbox}} 67 | ``` 68 | 69 | [`Controller`]: https://docs.rs/druid/latest/druid/widget/trait.Controller.html 70 | [`Widget`]: ./04_widget.md 71 | [`Painter`]: https://docs.rs/druid/latest/druid/widget/struct.Painter.html 72 | [`SizedBox`]: https://docs.rs/druid/latest/druid/widget/struct.SizedBox.html 73 | [`Container`]: https://docs.rs/druid/latest/druid/widget/struct.Container.html 74 | [`WidgetExt`]: https://docs.rs/druid/latest/druid/trait.WidgetExt.html 75 | [`background`]: https://docs.rs/druid/latest/druid/trait.WidgetExt.html#background 76 | -------------------------------------------------------------------------------- /docs/src/09_more_information.md: -------------------------------------------------------------------------------- 1 | # More information 2 | 3 | If you want more information about Druid this document contains links more tutorials, blogposts and 4 | youtube videos. 5 | 6 | ## Related projects 7 | These three projects provide the basis that Druid works on 8 | - [Piet](https://github.com/linebender/piet) An abstraction for 2D graphics. 9 | - [Kurbo](https://github.com/linebender/kurbo) A Rust library for manipulating curves 10 | - [Skribo](https://github.com/linebender/skribo) A Rust library for low-level text layout 11 | 12 | ## Projects using Druid 13 | - [Kondo](https://github.com/tbillington/kondo) Save disk space by cleaning unneeded files from software projects. 14 | - [jack-mixer](https://github.com/derekdreery/jack-mixer) A jack client that provides mixing, levels and a 3-band eq. 15 | - [kiro-synth](https://github.com/chris-zen/kiro-synth) An in progress modular sound synthesizer. 16 | - [psst](https://github.com/jpochyla/psst) A non-Electron GUI Spotify client. 17 | - [flac_music](https://github.com/wandercn/flac_music) A music player. 18 | - *And many more* 19 | 20 | ## Projects that work with Druid (widgets etc) 21 | - *No data filled in here* 22 | 23 | ## Presentations 24 | Some presentations about Druid, its background and related topics have been recorded 25 | - [Declarative UI patterns in Rust](https://youtu.be/xH2x99FTY4k) by Raph Levien at the Bay Area Rust Meetup December 3 2019 26 | - [Data oriented GUI in Rust](https://youtu.be/4YTfxresvS8) by Raph Levien at the Bay Area Rust Meetup June 28 2018 27 | 28 | ## Blog posts 29 | People have been blogging about Druid 30 | - [Building a widget for Druid](https://pauljmiller.com/posts/druid-widget-tutorial.html) a blog post by Paul Miller on how to create custom Widgets that explains lots of Druid on the way 31 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | - [Overview](./01_overview.md) 3 | - [Getting started](./02_getting_started.md) 4 | - [Data trait](./03_data.md) 5 | - [Widget trait](./04_widget.md) 6 | - [Lens trait](./05_lens.md) 7 | - [Env](./06_env.md) 8 | - [Resolution independence](./07_resolution_independence.md) 9 | - [Widgets in depth](./08_widgets_in_depth.md) 10 | --- 11 | - [More information](./09_more_information.md) 12 | -------------------------------------------------------------------------------- /druid-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "druid-derive" 3 | version = "0.5.1" 4 | license = "Apache-2.0" 5 | authors = ["Druid authors"] 6 | description = "derive impls for Druid, a Rust UI toolkit." 7 | repository = "https://github.com/linebender/druid" 8 | edition = "2018" 9 | 10 | [lib] 11 | proc-macro = true 12 | 13 | [package.metadata.docs.rs] 14 | rustdoc-args = ["--cfg", "docsrs"] 15 | default-target = "x86_64-pc-windows-msvc" 16 | 17 | [dependencies] 18 | syn = { version = "1.0.109", features = ["extra-traits"] } 19 | quote = "1.0.37" 20 | proc-macro2 = "1.0.89" 21 | 22 | [dev-dependencies] 23 | druid = { version = "0.8.3", path = "../druid" } 24 | trybuild = "1.0" 25 | 26 | float-cmp = { version = "0.9.0", features = ["std"], default-features = false } 27 | -------------------------------------------------------------------------------- /druid-derive/README.md: -------------------------------------------------------------------------------- 1 | # druid-derive 2 | 3 | This crate contains the implementations of derive macros for [Druid]. 4 | 5 | ## Project status 6 | 7 | **UNMAINTAINED** 8 | 9 | No further development is expected on `druid-derive`. 10 | 11 | [Druid]: https://github.com/linebender/druid 12 | -------------------------------------------------------------------------------- /druid-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! derive macros for Druid. 5 | 6 | #![deny(clippy::trivially_copy_pass_by_ref)] 7 | #![doc( 8 | html_logo_url = "https://raw.githubusercontent.com/linebender/druid/screenshots/images/doc_logo.png" 9 | )] 10 | 11 | extern crate proc_macro; 12 | 13 | mod attr; 14 | mod data; 15 | mod lens; 16 | 17 | use proc_macro::TokenStream; 18 | use syn::parse_macro_input; 19 | 20 | /// Generates implementations of the `Data` trait. 21 | /// 22 | /// This macro supports a `data` field attribute with the following arguments: 23 | /// 24 | /// - `#[data(ignore)]` makes the generated `Data::same` function skip comparing this field. 25 | /// - `#[data(same_fn="foo")]` uses the function `foo` for comparing this field. `foo` should 26 | /// be the name of a function with signature `fn(&T, &T) -> bool`, where `T` is the type of 27 | /// the field. 28 | /// - `#[data(eq)]` is shorthand for `#[data(same_fn = "PartialEq::eq")]` 29 | /// 30 | /// # Example 31 | /// 32 | /// ```rust 33 | /// use druid_derive::Data; 34 | /// 35 | /// #[derive(Clone, Data)] 36 | /// struct State { 37 | /// number: f64, 38 | /// // `Vec` doesn't implement `Data`, so we need to either ignore it or supply a `same_fn`. 39 | /// #[data(eq)] 40 | /// // same as #[data(same_fn="PartialEq::eq")] 41 | /// indices: Vec, 42 | /// // This is just some sort of cache; it isn't important for sameness comparison. 43 | /// #[data(ignore)] 44 | /// cached_indices: Vec, 45 | /// } 46 | /// ``` 47 | #[proc_macro_derive(Data, attributes(data))] 48 | pub fn derive_data(input: TokenStream) -> TokenStream { 49 | let input = parse_macro_input!(input as syn::DeriveInput); 50 | data::derive_data_impl(input) 51 | .unwrap_or_else(|err| err.to_compile_error()) 52 | .into() 53 | } 54 | 55 | /// Generates lenses to access the fields of a struct. 56 | /// 57 | /// An associated constant is defined on the struct for each field, 58 | /// having the same name as the field. 59 | /// 60 | /// This macro supports a `lens` field attribute with the following arguments: 61 | /// 62 | /// - `#[lens(ignore)]` skips creating a lens for one field. 63 | /// - `#[lens(name="foo")]` gives the lens the specified name (instead of the default, which is to 64 | /// create a lens with the same name as the field). 65 | /// 66 | /// # Example 67 | /// 68 | /// ```rust 69 | /// use druid_derive::Lens; 70 | /// 71 | /// #[derive(Lens)] 72 | /// struct State { 73 | /// // The Lens derive will create a `State::text` constant implementing 74 | /// // `druid::Lens` 75 | /// text: String, 76 | /// // The Lens derive will create a `State::lens_number` constant implementing 77 | /// // `druid::Lens` 78 | /// #[lens(name = "lens_number")] 79 | /// number: f64, 80 | /// // The Lens derive won't create anything for this field. 81 | /// #[lens(ignore)] 82 | /// blah: f64, 83 | /// } 84 | /// ``` 85 | #[proc_macro_derive(Lens, attributes(lens))] 86 | pub fn derive_lens(input: TokenStream) -> TokenStream { 87 | let input = parse_macro_input!(input as syn::DeriveInput); 88 | lens::derive_lens_impl(input) 89 | .unwrap_or_else(|err| err.to_compile_error()) 90 | .into() 91 | } 92 | -------------------------------------------------------------------------------- /druid-derive/tests/data.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Test #[derive(Data)] 5 | 6 | use druid::Data; 7 | 8 | #[derive(Data, Clone)] 9 | struct PlainStruct; 10 | 11 | #[derive(Data, Clone)] 12 | struct EmptyTupleStruct(); 13 | 14 | #[derive(Data, Clone)] 15 | 16 | struct SingleTupleStruct(bool); 17 | 18 | #[derive(Data, Clone)] 19 | struct MultiTupleStruct(bool, i64, String); 20 | 21 | #[derive(Data, Clone)] 22 | struct EmptyFieldStruct {} 23 | 24 | #[derive(Data, Clone)] 25 | struct SingleFieldStruct { 26 | a: bool, 27 | } 28 | 29 | #[derive(Data, Clone)] 30 | struct MultiFieldStruct { 31 | a: bool, 32 | b: i64, 33 | c: String, 34 | } 35 | 36 | trait UserTrait {} 37 | 38 | #[derive(Clone, Data)] 39 | struct TypeParamForUserTraitStruct { 40 | a: T, 41 | } 42 | 43 | #[derive(Clone, Data)] 44 | struct TypeParamForUserTraitWithWhereClauseStruct 45 | where 46 | T: UserTrait, 47 | { 48 | b: T, 49 | } 50 | 51 | #[derive(Clone, Data)] 52 | enum TypeParamForUserTraitAndLifetimeEnum { 53 | V1(T), 54 | } 55 | 56 | #[test] 57 | fn test_data_derive_same() { 58 | let plain = PlainStruct; 59 | assert!(plain.same(&plain)); 60 | 61 | let empty_tuple = EmptyTupleStruct(); 62 | assert!(empty_tuple.same(&empty_tuple)); 63 | 64 | let singletuple = SingleTupleStruct(true); 65 | assert!(singletuple.same(&singletuple)); 66 | assert!(!singletuple.same(&SingleTupleStruct(false))); 67 | 68 | let multituple = MultiTupleStruct(false, 33, "Test".to_string()); 69 | assert!(multituple.same(&multituple)); 70 | assert!(!multituple.same(&MultiTupleStruct(true, 33, "Test".to_string()))); 71 | 72 | let empty_field = EmptyFieldStruct {}; 73 | assert!(empty_field.same(&empty_field)); 74 | 75 | let singlefield = SingleFieldStruct { a: true }; 76 | assert!(singlefield.same(&singlefield)); 77 | assert!(!singlefield.same(&SingleFieldStruct { a: false })); 78 | 79 | let multifield = MultiFieldStruct { 80 | a: false, 81 | b: 33, 82 | c: "Test".to_string(), 83 | }; 84 | assert!(multifield.same(&multifield)); 85 | assert!(!multifield.same(&MultiFieldStruct { 86 | a: false, 87 | b: 33, 88 | c: "Fail".to_string() 89 | })); 90 | 91 | #[derive(Clone, Data)] 92 | struct Value(u32); 93 | 94 | impl UserTrait for Value {} 95 | 96 | let v = TypeParamForUserTraitStruct { a: Value(1) }; 97 | assert!(v.same(&v)); 98 | assert!(!v.same(&TypeParamForUserTraitStruct { a: Value(2) })); 99 | 100 | let v = TypeParamForUserTraitWithWhereClauseStruct { b: Value(3) }; 101 | assert!(v.same(&v)); 102 | assert!(!v.same(&TypeParamForUserTraitWithWhereClauseStruct { b: Value(6) })); 103 | 104 | let v = TypeParamForUserTraitAndLifetimeEnum::V1(Value(10)); 105 | assert!(v.same(&v)); 106 | assert!(!v.same(&TypeParamForUserTraitAndLifetimeEnum::V1(Value(12)))); 107 | } 108 | 109 | #[derive(Data, Clone)] 110 | struct DataAttrEq { 111 | #[data(eq)] 112 | f: PanicOnPartialEq, 113 | } 114 | 115 | #[derive(Clone, Copy)] 116 | struct PanicOnPartialEq; 117 | impl PartialEq for PanicOnPartialEq { 118 | fn eq(&self, _other: &Self) -> bool { 119 | panic!("PartialEq::eq called"); 120 | } 121 | } 122 | 123 | #[test] 124 | #[should_panic = "PartialEq::eq called"] 125 | fn data_attr_eq() { 126 | DataAttrEq { 127 | f: PanicOnPartialEq, 128 | } 129 | .same(&DataAttrEq { 130 | f: PanicOnPartialEq, 131 | }); 132 | } 133 | -------------------------------------------------------------------------------- /druid-derive/tests/ignore.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! testing the ignore attribute 5 | 6 | use druid::Data; 7 | 8 | #[test] 9 | fn simple_ignore() { 10 | #[derive(Clone, Data)] 11 | struct Point { 12 | x: f64, 13 | #[data(ignore)] 14 | #[allow(dead_code)] 15 | y: f64, 16 | } 17 | let p1 = Point { x: 0.0, y: 1.0 }; 18 | let p2 = Point { x: 0.0, y: 9.0 }; 19 | assert!(p1.same(&p2)); 20 | } 21 | 22 | #[test] 23 | fn ignore_item_without_data_impl() { 24 | use std::path::PathBuf; 25 | 26 | #[derive(Clone, Data)] 27 | #[allow(dead_code)] 28 | struct CoolStruct { 29 | len: usize, 30 | #[data(ignore)] 31 | #[allow(dead_code)] 32 | path: PathBuf, 33 | } 34 | } 35 | 36 | #[test] 37 | fn tuple_struct() { 38 | #[derive(Clone, Data)] 39 | struct Tup( 40 | usize, 41 | #[data(ignore)] 42 | #[allow(dead_code)] 43 | usize, 44 | ); 45 | 46 | let one = Tup(1, 1); 47 | let two = Tup(1, 5); 48 | assert!(one.same(&two)); 49 | } 50 | 51 | #[test] 52 | fn enums() { 53 | #[derive(Clone, Data)] 54 | enum Hmm { 55 | Named { 56 | one: usize, 57 | #[data(ignore)] 58 | two: usize, 59 | }, 60 | Tuple(#[data(ignore)] usize, usize), 61 | } 62 | 63 | let name_one = Hmm::Named { one: 5, two: 4 }; 64 | let name_two = Hmm::Named { one: 5, two: 42 }; 65 | let tuple_one = Hmm::Tuple(2, 4); 66 | let tuple_two = Hmm::Tuple(9, 4); 67 | 68 | assert!(!name_one.same(&tuple_one)); 69 | assert!(name_one.same(&name_two)); 70 | assert!(tuple_one.same(&tuple_two)); 71 | } 72 | -------------------------------------------------------------------------------- /druid-derive/tests/ui.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // https://github.com/dtolnay/trybuild 5 | // trybuild is a crate that essentially runs cargo on the provided files, and checks the output. 6 | // Tests may suddenly fail after a new compiler release, and there's not much we can do about that. 7 | // If the test suite fails because of trybuild: 8 | // - Update your compiler to the latest stable version. 9 | // - If it still fails, update the stderr snapshots. To do so, run the test suite with 10 | // env variable TRYBUILD=overwrite, and submit the file changes in a PR. 11 | use trybuild::TestCases; 12 | 13 | #[test] 14 | fn ui() { 15 | let t = TestCases::new(); 16 | t.pass("tests/ui/simple-lens.rs"); 17 | t.pass("tests/ui/lens-attributes.rs"); 18 | t.compile_fail("tests/ui/with-empty-struct.rs"); 19 | t.compile_fail("tests/ui/with-tuple-struct.rs"); 20 | t.compile_fail("tests/ui/with-enum.rs"); 21 | t.compile_fail("tests/ui/with-union.rs"); 22 | 23 | t.compile_fail("tests/ui/with-snake_case.rs"); 24 | } 25 | -------------------------------------------------------------------------------- /druid-derive/tests/ui/lens-attributes.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use druid::*; 4 | 5 | #[derive(Lens)] 6 | struct Item { 7 | #[lens(name = "count_lens")] 8 | count: usize, 9 | #[lens(ignore)] 10 | complete: bool, 11 | } 12 | 13 | impl Item { 14 | fn count(&self) -> usize { 15 | self.count 16 | } 17 | fn complete(&mut self) { 18 | self.complete = true; 19 | } 20 | } 21 | 22 | fn main() {} 23 | -------------------------------------------------------------------------------- /druid-derive/tests/ui/simple-lens.rs: -------------------------------------------------------------------------------- 1 | use druid::*; 2 | 3 | #[derive(Lens)] 4 | struct MyThing { 5 | field_1: i32, 6 | field_2: String, 7 | } 8 | 9 | fn main() { 10 | let _ = MyThing::field_1; 11 | let _ = MyThing::field_2; 12 | } 13 | -------------------------------------------------------------------------------- /druid-derive/tests/ui/with-empty-struct.rs: -------------------------------------------------------------------------------- 1 | use druid::*; 2 | 3 | #[derive(Lens)] 4 | struct Foobar; 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /druid-derive/tests/ui/with-empty-struct.stderr: -------------------------------------------------------------------------------- 1 | error: Lens implementations can only be derived from structs with named fields 2 | --> $DIR/with-empty-struct.rs:4:1 3 | | 4 | 4 | struct Foobar; 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /druid-derive/tests/ui/with-enum.rs: -------------------------------------------------------------------------------- 1 | use druid::*; 2 | 3 | #[derive(Lens)] 4 | enum Foobar { 5 | Foo(i32), 6 | Bar, 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /druid-derive/tests/ui/with-enum.stderr: -------------------------------------------------------------------------------- 1 | error: Lens implementations cannot be derived from enums 2 | --> $DIR/with-enum.rs:4:1 3 | | 4 | 4 | enum Foobar { 5 | | ^^^^ 6 | -------------------------------------------------------------------------------- /druid-derive/tests/ui/with-snake_case.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types)] 2 | 3 | use druid::*; 4 | 5 | #[derive(Lens)] 6 | struct my_thing { 7 | field: i32 8 | } 9 | 10 | fn main() {} 11 | -------------------------------------------------------------------------------- /druid-derive/tests/ui/with-snake_case.stderr: -------------------------------------------------------------------------------- 1 | error: Lens implementations can only be derived from CamelCase types 2 | --> $DIR/with-snake_case.rs:6:8 3 | | 4 | 6 | struct my_thing { 5 | | ^^^^^^^^ 6 | -------------------------------------------------------------------------------- /druid-derive/tests/ui/with-tuple-struct.rs: -------------------------------------------------------------------------------- 1 | use druid::*; 2 | 3 | #[derive(Lens)] 4 | struct Foobar(i32, i64); 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /druid-derive/tests/ui/with-tuple-struct.stderr: -------------------------------------------------------------------------------- 1 | error: Lens implementations can only be derived from structs with named fields 2 | --> $DIR/with-tuple-struct.rs:4:1 3 | | 4 | 4 | struct Foobar(i32, i64); 5 | | ^^^^^^ 6 | -------------------------------------------------------------------------------- /druid-derive/tests/ui/with-union.rs: -------------------------------------------------------------------------------- 1 | use druid::*; 2 | 3 | #[derive(Lens)] 4 | union Foobar { 5 | foo: i32, 6 | bar: f64, 7 | } 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /druid-derive/tests/ui/with-union.stderr: -------------------------------------------------------------------------------- 1 | error: Lens implementations cannot be derived from unions 2 | --> $DIR/with-union.rs:4:1 3 | | 4 | 4 | union Foobar { 5 | | ^^^^^ 6 | -------------------------------------------------------------------------------- /druid-derive/tests/with_lens.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use float_cmp::assert_approx_eq; 5 | 6 | use druid::Data; 7 | use druid::Lens; 8 | 9 | #[test] 10 | fn derive_lens() { 11 | #[derive(Lens)] 12 | struct State { 13 | text: String, 14 | #[lens(name = "lens_number")] 15 | number: f64, 16 | #[lens(ignore)] 17 | ignored: f64, 18 | } 19 | 20 | let mut state = State { 21 | text: "1.0".into(), 22 | number: 1.0, 23 | ignored: 2.0, 24 | }; 25 | 26 | let text_lens = State::text; 27 | let number_lens = State::lens_number; //named lens for number 28 | 29 | text_lens.with(&state, |data| assert_eq!(data, "1.0")); 30 | number_lens.with(&state, |data| assert_approx_eq!(f64, *data, 1.0)); 31 | 32 | text_lens.with_mut(&mut state, |data| *data = "2.0".into()); 33 | number_lens.with_mut(&mut state, |data| *data = 2.0); 34 | 35 | assert_eq!(state.text, "2.0"); 36 | assert_approx_eq!(f64, state.number, 2.0); 37 | assert_approx_eq!(f64, state.ignored, 2.0); 38 | } 39 | 40 | #[test] 41 | fn mix_with_data_lens() { 42 | #[derive(Clone, Lens, Data)] 43 | struct State { 44 | #[data(ignore)] 45 | text: String, 46 | #[data(same_fn = "same_sign")] 47 | #[lens(name = "lens_number")] 48 | number: f64, 49 | } 50 | 51 | //test lens 52 | let mut state = State { 53 | text: "1.0".into(), 54 | number: 1.0, 55 | }; 56 | let text_lens = State::text; 57 | let number_lens = State::lens_number; //named lens for number 58 | 59 | text_lens.with(&state, |data| assert_eq!(data, "1.0")); 60 | number_lens.with(&state, |data| assert_approx_eq!(f64, *data, 1.0)); 61 | 62 | text_lens.with_mut(&mut state, |data| *data = "2.0".into()); 63 | number_lens.with_mut(&mut state, |data| *data = 2.0); 64 | 65 | assert_eq!(state.text, "2.0"); 66 | assert_approx_eq!(f64, state.number, 2.0); 67 | 68 | //test data 69 | let two = State { 70 | text: "666".into(), 71 | number: 200.0, 72 | }; 73 | assert!(state.same(&two)) 74 | } 75 | #[allow(clippy::trivially_copy_pass_by_ref)] 76 | fn same_sign(one: &f64, two: &f64) -> bool { 77 | one.signum() == two.signum() 78 | } 79 | -------------------------------------------------------------------------------- /druid-derive/tests/with_same.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use druid::Data; 5 | 6 | #[test] 7 | fn same_fn() { 8 | #[derive(Clone, Data)] 9 | struct Nanana { 10 | bits: f64, 11 | #[data(eq)] 12 | peq: f64, 13 | } 14 | 15 | let one = Nanana { 16 | bits: 1.0, 17 | peq: f64::NAN, 18 | }; 19 | let two = Nanana { 20 | bits: 1.0, 21 | peq: f64::NAN, 22 | }; 23 | 24 | //according to partialeq, two NaNs are never equal 25 | assert!(!one.same(&two)); 26 | 27 | let one = Nanana { 28 | bits: f64::NAN, 29 | peq: 1.0, 30 | }; 31 | let two = Nanana { 32 | bits: f64::NAN, 33 | peq: 1.0, 34 | }; 35 | 36 | // the default 'same' impl uses bitwise equality, so two bitwise-equal NaNs are equal 37 | assert!(one.same(&two)); 38 | } 39 | 40 | #[test] 41 | fn enums() { 42 | #[derive(Debug, Clone, Data)] 43 | enum Hi { 44 | One { 45 | bits: f64, 46 | }, 47 | Two { 48 | #[data(same_fn = "same_sign")] 49 | bits: f64, 50 | }, 51 | Tri(#[data(same_fn = "same_sign")] f64), 52 | } 53 | 54 | let oneone = Hi::One { bits: f64::NAN }; 55 | let onetwo = Hi::One { bits: f64::NAN }; 56 | assert!(oneone.same(&onetwo)); 57 | 58 | let twoone = Hi::Two { bits: -1.1 }; 59 | let twotwo = Hi::Two { 60 | bits: f64::NEG_INFINITY, 61 | }; 62 | assert!(twoone.same(&twotwo)); 63 | 64 | let trione = Hi::Tri(1001.); 65 | let tritwo = Hi::Tri(-1.); 66 | assert!(!trione.same(&tritwo)); 67 | } 68 | #[allow(clippy::trivially_copy_pass_by_ref)] 69 | fn same_sign(one: &f64, two: &f64) -> bool { 70 | one.signum() == two.signum() 71 | } 72 | -------------------------------------------------------------------------------- /druid-shell/README.md: -------------------------------------------------------------------------------- 1 | # druid-shell 2 | 3 | `druid-shell` provides a common interface to the various elements of different platform application 4 | frameworks. It was designed to be used by [Druid], an experimental UI toolkit. 5 | 6 | ## Project status 7 | 8 | **UNMAINTAINED** 9 | 10 | `druid-shell` v0.8 was forked to form [Glazier], which is where some additional development happened. 11 | No further development is expected on `druid-shell` or [Glazier]. 12 | Our recommendation for new apps is to use [Winit]. 13 | 14 | ## Design 15 | 16 | The code in `druid-shell` can be divided into roughly two categories: the 17 | platform agnostic code and types, which are exposed directly, and the 18 | platform-specific implementations of these types, which live in per-backend 19 | directories in `src/backend`. The backend-specific code for the current 20 | backend is re-exported as `druid-shell::backend`. 21 | 22 | `druid-shell` does not generally expose backend types directly. Instead, we 23 | expose wrapper structs that define the common interface, and then call 24 | corresponding methods on the concrete type for the current backend. 25 | 26 | ## Unsafe 27 | 28 | Interacting with system APIs is inherently unsafe. One of the goals of 29 | `druid-shell` is to handle all interaction with these APIs, exposing 30 | a safe interface to `druid` and other possible consumers. 31 | 32 | [Druid]: https://github.com/linebender/druid 33 | [Glazier]: https://github.com/linebender/glazier 34 | [Winit]: https://github.com/rust-windowing/winit 35 | -------------------------------------------------------------------------------- /druid-shell/build.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | #[cfg(not(any(feature = "x11", feature = "wayland")))] 5 | fn main() {} 6 | 7 | #[cfg(any(feature = "x11", feature = "wayland"))] 8 | fn main() { 9 | use pkg_config::probe_library; 10 | use std::env; 11 | use std::path::PathBuf; 12 | 13 | if env::var("CARGO_CFG_TARGET_OS").unwrap() != "freebsd" 14 | && env::var("CARGO_CFG_TARGET_OS").unwrap() != "linux" 15 | && env::var("CARGO_CFG_TARGET_OS").unwrap() != "openbsd" 16 | { 17 | return; 18 | } 19 | 20 | let xkbcommon = probe_library("xkbcommon").unwrap(); 21 | 22 | #[cfg(feature = "x11")] 23 | probe_library("xkbcommon-x11").unwrap(); 24 | 25 | let mut header = "\ 26 | #include 27 | #include 28 | #include " 29 | .to_string(); 30 | 31 | if cfg!(feature = "x11") { 32 | header += " 33 | #include "; 34 | } 35 | 36 | let bindings = bindgen::Builder::default() 37 | // The input header we would like to generate 38 | // bindings for. 39 | .header_contents("wrapper.h", &header) 40 | .clang_args( 41 | xkbcommon 42 | .include_paths 43 | .iter() 44 | .filter_map(|path| path.to_str().map(|s| format!("-I{s}"))), 45 | ) 46 | // Tell cargo to invalidate the built crate whenever any of the 47 | // included header files changed. 48 | .parse_callbacks(Box::new(bindgen::CargoCallbacks)) 49 | .prepend_enum_name(false) 50 | .size_t_is_usize(true) 51 | .allowlist_function("xkb_.*") 52 | .allowlist_type("xkb_.*") 53 | .allowlist_var("XKB_.*") 54 | .allowlist_type("xcb_connection_t") 55 | // this needs var args 56 | .blocklist_function("xkb_context_set_log_fn") 57 | // we use FILE from libc 58 | .blocklist_type("FILE") 59 | .blocklist_type("va_list") 60 | .generate() 61 | .expect("Unable to generate bindings"); 62 | 63 | // Write the bindings to the $OUT_DIR/xkbcommon.rs file. 64 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 65 | bindings 66 | .write_to_file(out_path.join("xkbcommon_sys.rs")) 67 | .expect("Couldn't write bindings!"); 68 | } 69 | -------------------------------------------------------------------------------- /druid-shell/examples/quit.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use std::any::Any; 5 | 6 | use druid_shell::kurbo::{Line, Size}; 7 | use druid_shell::piet::{Color, RenderContext}; 8 | 9 | use druid_shell::{ 10 | Application, HotKey, Menu, Region, SysMods, WinHandler, WindowBuilder, WindowHandle, 11 | }; 12 | 13 | const BG_COLOR: Color = Color::rgb8(0x27, 0x28, 0x22); 14 | const FG_COLOR: Color = Color::rgb8(0xf0, 0xf0, 0xea); 15 | 16 | #[derive(Default)] 17 | struct QuitState { 18 | quit_count: u32, 19 | size: Size, 20 | handle: WindowHandle, 21 | } 22 | 23 | impl WinHandler for QuitState { 24 | fn connect(&mut self, handle: &WindowHandle) { 25 | self.handle = handle.clone(); 26 | } 27 | 28 | fn prepare_paint(&mut self) {} 29 | 30 | fn paint(&mut self, piet: &mut piet_common::Piet, _: &Region) { 31 | let rect = self.size.to_rect(); 32 | piet.fill(rect, &BG_COLOR); 33 | piet.stroke(Line::new((10.0, 50.0), (90.0, 90.0)), &FG_COLOR, 1.0); 34 | } 35 | 36 | fn size(&mut self, size: Size) { 37 | self.size = size; 38 | } 39 | 40 | fn request_close(&mut self) { 41 | self.quit_count += 1; 42 | if self.quit_count >= 5 { 43 | self.handle.close(); 44 | } else { 45 | tracing::info!("Don't wanna quit"); 46 | } 47 | } 48 | 49 | fn destroy(&mut self) { 50 | Application::global().quit() 51 | } 52 | 53 | fn as_any(&mut self) -> &mut dyn Any { 54 | self 55 | } 56 | } 57 | 58 | fn main() { 59 | tracing_subscriber::fmt().init(); 60 | let app = Application::new().unwrap(); 61 | 62 | let mut file_menu = Menu::new(); 63 | file_menu.add_item( 64 | 0x100, 65 | "E&xit", 66 | Some(&HotKey::new(SysMods::Cmd, "q")), 67 | None, 68 | true, 69 | ); 70 | 71 | let mut menubar = Menu::new(); 72 | menubar.add_dropdown(file_menu, "Application", true); 73 | 74 | let mut builder = WindowBuilder::new(app.clone()); 75 | builder.set_handler(Box::::default()); 76 | builder.set_title("Quit example"); 77 | builder.set_menu(menubar); 78 | 79 | let window = builder.build().unwrap(); 80 | window.show(); 81 | 82 | app.run(None); 83 | } 84 | -------------------------------------------------------------------------------- /druid-shell/src/backend/gtk/application.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! GTK implementation of features at the application scope. 5 | 6 | use gtk::gio::prelude::ApplicationExtManual; 7 | use gtk::gio::{ApplicationFlags, Cancellable}; 8 | use gtk::Application as GtkApplication; 9 | 10 | use gtk::prelude::{ApplicationExt, GtkApplicationExt}; 11 | 12 | use crate::application::AppHandler; 13 | 14 | use super::clipboard::Clipboard; 15 | use super::error::Error; 16 | 17 | #[derive(Clone)] 18 | pub(crate) struct Application { 19 | gtk_app: GtkApplication, 20 | } 21 | 22 | impl Application { 23 | pub fn new() -> Result { 24 | // TODO: we should give control over the application ID to the user 25 | let gtk_app = GtkApplication::new( 26 | Some("com.github.linebender.druid"), 27 | // TODO we set this to avoid connecting to an existing running instance 28 | // of "com.github.linebender.druid" after which we would never receive 29 | // the "Activate application" below. See pull request druid#384 30 | // Which shows another way once we have in place a mechanism for 31 | // communication with remote instances. 32 | ApplicationFlags::NON_UNIQUE, 33 | ); 34 | 35 | gtk_app.connect_activate(|_app| { 36 | tracing::info!("gtk: Activated application"); 37 | }); 38 | 39 | if let Err(err) = gtk_app.register(None as Option<&Cancellable>) { 40 | return Err(Error::Error(err)); 41 | } 42 | 43 | Ok(Application { gtk_app }) 44 | } 45 | 46 | #[inline] 47 | pub fn gtk_app(&self) -> &GtkApplication { 48 | &self.gtk_app 49 | } 50 | 51 | pub fn run(self, _handler: Option>) { 52 | self.gtk_app.run(); 53 | } 54 | 55 | pub fn quit(&self) { 56 | match self.gtk_app.active_window() { 57 | None => { 58 | // no application is running, main is not running 59 | } 60 | Some(_) => { 61 | // we still have an active window, close the run loop 62 | self.gtk_app.quit(); 63 | } 64 | } 65 | } 66 | 67 | pub fn clipboard(&self) -> Clipboard { 68 | Clipboard { 69 | selection: gtk::gdk::SELECTION_CLIPBOARD, 70 | } 71 | } 72 | 73 | pub fn get_locale() -> String { 74 | let mut locale: String = gtk::glib::language_names()[0].as_str().into(); 75 | // This is done because the locale parsing library we use expects an unicode locale, but these vars have an ISO locale 76 | if let Some(idx) = locale.chars().position(|c| c == '.' || c == '@') { 77 | locale.truncate(idx); 78 | } 79 | locale 80 | } 81 | } 82 | 83 | impl crate::platform::linux::ApplicationExt for crate::Application { 84 | fn primary_clipboard(&self) -> crate::Clipboard { 85 | crate::Clipboard(Clipboard { 86 | selection: gtk::gdk::SELECTION_PRIMARY, 87 | }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /druid-shell/src/backend/gtk/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! GTK backend errors. 5 | 6 | use std::fmt; 7 | 8 | use gtk::glib::{BoolError, Error as GLibError}; 9 | 10 | /// GTK backend errors. 11 | #[derive(Debug, Clone)] 12 | pub enum Error { 13 | /// Generic GTK error. 14 | Error(GLibError), 15 | /// GTK error that has no information provided by GTK, 16 | /// but may have extra information provided by gtk-rs. 17 | BoolError(BoolError), 18 | } 19 | 20 | impl fmt::Display for Error { 21 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 22 | match self { 23 | Error::Error(err) => write!(f, "GTK Error: {err}"), 24 | Error::BoolError(err) => write!(f, "GTK BoolError: {err}"), 25 | } 26 | } 27 | } 28 | 29 | impl std::error::Error for Error {} 30 | -------------------------------------------------------------------------------- /druid-shell/src/backend/gtk/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! GTK-based backend support 5 | 6 | pub mod application; 7 | pub mod clipboard; 8 | pub mod dialog; 9 | pub mod error; 10 | pub mod keycodes; 11 | pub mod menu; 12 | pub mod screen; 13 | pub mod util; 14 | pub mod window; 15 | -------------------------------------------------------------------------------- /druid-shell/src/backend/gtk/screen.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! GTK Monitors and Screen information. 5 | 6 | use crate::kurbo::{Point, Rect, Size}; 7 | use crate::screen::Monitor; 8 | use gtk::gdk::{Display, DisplayManager, Rectangle}; 9 | 10 | fn translate_gdk_rectangle(r: Rectangle) -> Rect { 11 | Rect::from_origin_size( 12 | Point::new(r.x() as f64, r.y() as f64), 13 | Size::new(r.width() as f64, r.height() as f64), 14 | ) 15 | } 16 | 17 | fn translate_gdk_monitor(mon: gtk::gdk::Monitor) -> Monitor { 18 | let area = translate_gdk_rectangle(mon.geometry()); 19 | Monitor::new( 20 | mon.is_primary(), 21 | area, 22 | translate_gdk_rectangle(mon.workarea()), 23 | ) 24 | } 25 | 26 | pub(crate) fn get_monitors() -> Vec { 27 | if !gtk::is_initialized() { 28 | if let Err(err) = gtk::init() { 29 | tracing::error!("{}", err.message); 30 | return Vec::new(); 31 | } 32 | } 33 | DisplayManager::get() 34 | .list_displays() 35 | .iter() 36 | .flat_map(|display: &Display| { 37 | (0..display.n_monitors()) 38 | .filter_map(move |i| display.monitor(i).map(translate_gdk_monitor)) 39 | }) 40 | .collect() 41 | } 42 | -------------------------------------------------------------------------------- /druid-shell/src/backend/gtk/util.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Utilities, GTK specific. 5 | 6 | pub(crate) fn assert_main_thread() { 7 | assert!(gtk::is_initialized_main_thread()); 8 | } 9 | -------------------------------------------------------------------------------- /druid-shell/src/backend/mac/appkit.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! macOS AppKit bindings. 5 | 6 | #![allow(clippy::upper_case_acronyms, non_snake_case, non_upper_case_globals)] 7 | 8 | use bitflags::bitflags; 9 | use cocoa::base::id; 10 | use cocoa::foundation::NSRect; 11 | use objc::{class, msg_send, sel, sel_impl}; 12 | 13 | #[link(name = "AppKit", kind = "framework")] 14 | extern "C" { 15 | pub static NSRunLoopCommonModes: id; 16 | } 17 | 18 | bitflags! { 19 | pub struct NSTrackingAreaOptions: i32 { 20 | const MouseEnteredAndExited = 1; 21 | const MouseMoved = 1 << 1; 22 | const CursorUpdate = 1 << 2; 23 | // What's 1 << 3? 24 | const ActiveWhenFirstResponder = 1 << 4; 25 | const ActiveInKeyWindow = 1 << 5; 26 | const ActiveInActiveApp = 1 << 6; 27 | const ActiveAlways = 1 << 7; 28 | const AssumeInside = 1 << 8; 29 | const InVisibleRect = 1 << 9; 30 | const EnabledDuringMouseDrag = 1 << 10; 31 | } 32 | } 33 | 34 | pub trait NSTrackingArea: Sized { 35 | unsafe fn alloc(_: Self) -> id { 36 | msg_send![class!(NSTrackingArea), alloc] 37 | } 38 | 39 | unsafe fn initWithRect_options_owner_userInfo( 40 | self, 41 | rect: NSRect, 42 | options: NSTrackingAreaOptions, 43 | owner: id, 44 | userInfo: id, 45 | ) -> id; 46 | } 47 | 48 | impl NSTrackingArea for id { 49 | unsafe fn initWithRect_options_owner_userInfo( 50 | self, 51 | rect: NSRect, 52 | options: NSTrackingAreaOptions, 53 | owner: id, 54 | userInfo: id, 55 | ) -> id { 56 | msg_send![self, initWithRect:rect options:options owner:owner userInfo:userInfo] 57 | } 58 | } 59 | 60 | pub trait NSView: Sized { 61 | unsafe fn addTrackingArea(self, trackingArea: id) -> id; 62 | } 63 | 64 | impl NSView for id { 65 | unsafe fn addTrackingArea(self, trackingArea: id) -> id { 66 | msg_send![self, addTrackingArea: trackingArea] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /druid-shell/src/backend/mac/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! macOS backend errors. 5 | 6 | //TODO: add a backend error for macOS, based on NSError 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct Error; 10 | 11 | impl std::fmt::Display for Error { 12 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 13 | write!(f, "NSError") 14 | } 15 | } 16 | 17 | impl std::error::Error for Error {} 18 | -------------------------------------------------------------------------------- /druid-shell/src/backend/mac/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! macOS `druid-shell` backend. 5 | 6 | #![allow(clippy::let_unit_value)] 7 | 8 | pub mod appkit; 9 | pub mod application; 10 | pub mod clipboard; 11 | pub mod dialog; 12 | pub mod error; 13 | mod keyboard; 14 | pub mod menu; 15 | pub mod screen; 16 | pub mod text_input; 17 | pub mod util; 18 | pub mod window; 19 | -------------------------------------------------------------------------------- /druid-shell/src/backend/mac/util.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Utilities, macOS specific. 5 | 6 | use std::ffi::c_void; 7 | 8 | use cocoa::base::{id, nil, BOOL, YES}; 9 | use cocoa::foundation::{NSAutoreleasePool, NSString, NSUInteger}; 10 | use objc::{class, msg_send, sel, sel_impl}; 11 | 12 | /// Panic if not on the main thread. 13 | /// 14 | /// Many Cocoa operations are only valid on the main thread, and (I think) 15 | /// undefined behavior is possible if invoked from other threads. If so, 16 | /// failing on non main thread is necessary for safety. 17 | pub(crate) fn assert_main_thread() { 18 | unsafe { 19 | let is_main_thread: BOOL = msg_send!(class!(NSThread), isMainThread); 20 | assert_eq!(is_main_thread, YES); 21 | } 22 | } 23 | 24 | /// Create a new NSString from a &str. 25 | pub(crate) fn make_nsstring(s: &str) -> id { 26 | unsafe { NSString::alloc(nil).init_str(s).autorelease() } 27 | } 28 | 29 | pub(crate) fn from_nsstring(s: id) -> String { 30 | unsafe { 31 | let slice = std::slice::from_raw_parts(s.UTF8String() as *const _, s.len()); 32 | let result = std::str::from_utf8_unchecked(slice); 33 | result.into() 34 | } 35 | } 36 | 37 | pub(crate) fn make_nsdata(bytes: &[u8]) -> id { 38 | let dlen = bytes.len() as NSUInteger; 39 | unsafe { 40 | msg_send![class!(NSData), dataWithBytes: bytes.as_ptr() as *const c_void length: dlen] 41 | } 42 | } 43 | 44 | pub(crate) fn from_nsdata(data: id) -> Vec { 45 | unsafe { 46 | let len: NSUInteger = msg_send![data, length]; 47 | let bytes: *const c_void = msg_send![data, bytes]; 48 | let mut out: Vec = Vec::with_capacity(len as usize); 49 | std::ptr::copy_nonoverlapping(bytes as *const u8, out.as_mut_ptr(), len as usize); 50 | out.set_len(len as usize); 51 | out 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /druid-shell/src/backend/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Platform specific implementations. 5 | 6 | // It would be clearer to use cfg_if! macros here, but that breaks rustfmt. 7 | 8 | #[cfg(target_os = "windows")] 9 | mod windows; 10 | #[cfg(target_os = "windows")] 11 | pub use windows::*; 12 | 13 | #[cfg(target_os = "macos")] 14 | mod mac; 15 | #[cfg(target_os = "macos")] 16 | pub use mac::*; 17 | #[cfg(target_os = "macos")] 18 | pub(crate) mod shared; 19 | 20 | #[cfg(all( 21 | feature = "x11", 22 | any(target_os = "freebsd", target_os = "linux", target_os = "openbsd") 23 | ))] 24 | mod x11; 25 | #[cfg(all( 26 | feature = "x11", 27 | any(target_os = "freebsd", target_os = "linux", target_os = "openbsd") 28 | ))] 29 | pub use x11::*; 30 | #[cfg(all( 31 | feature = "x11", 32 | any(target_os = "freebsd", target_os = "linux", target_os = "openbsd") 33 | ))] 34 | pub(crate) mod shared; 35 | 36 | #[cfg(all( 37 | feature = "wayland", 38 | any(target_os = "freebsd", target_os = "linux", target_os = "openbsd") 39 | ))] 40 | mod wayland; 41 | #[cfg(all( 42 | feature = "wayland", 43 | any(target_os = "freebsd", target_os = "linux", target_os = "openbsd") 44 | ))] 45 | pub use wayland::*; 46 | #[cfg(all( 47 | feature = "wayland", 48 | any(target_os = "freebsd", target_os = "linux", target_os = "openbsd") 49 | ))] 50 | pub(crate) mod shared; 51 | 52 | #[cfg(all( 53 | not(feature = "x11"), 54 | not(feature = "wayland"), 55 | any(target_os = "freebsd", target_os = "linux", target_os = "openbsd") 56 | ))] 57 | mod gtk; 58 | #[cfg(all( 59 | not(feature = "x11"), 60 | not(feature = "wayland"), 61 | any(target_os = "freebsd", target_os = "linux", target_os = "openbsd") 62 | ))] 63 | pub use self::gtk::*; 64 | #[cfg(all( 65 | not(feature = "x11"), 66 | not(feature = "wayland"), 67 | any(target_os = "freebsd", target_os = "linux", target_os = "openbsd") 68 | ))] 69 | pub(crate) mod shared; 70 | 71 | #[cfg(target_arch = "wasm32")] 72 | mod web; 73 | #[cfg(target_arch = "wasm32")] 74 | pub use web::*; 75 | -------------------------------------------------------------------------------- /druid-shell/src/backend/shared/linux/env.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | pub fn locale() -> String { 5 | fn locale_env_var(var: &str) -> Option { 6 | match std::env::var(var) { 7 | Ok(s) if s.is_empty() => { 8 | tracing::debug!("locale: ignoring empty env var {}", var); 9 | None 10 | } 11 | Ok(s) => { 12 | tracing::debug!("locale: env var {} found: {:?}", var, &s); 13 | Some(s) 14 | } 15 | Err(std::env::VarError::NotPresent) => { 16 | tracing::debug!("locale: env var {} not found", var); 17 | None 18 | } 19 | Err(std::env::VarError::NotUnicode(_)) => { 20 | tracing::debug!("locale: ignoring invalid unicode env var {}", var); 21 | None 22 | } 23 | } 24 | } 25 | 26 | // from gettext manual 27 | // https://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html#Locale-Environment-Variables 28 | let mut locale = locale_env_var("LANGUAGE") 29 | // the LANGUAGE value is priority list separated by : 30 | // See: https://www.gnu.org/software/gettext/manual/html_node/The-LANGUAGE-variable.html#The-LANGUAGE-variable 31 | .and_then(|locale| locale.split(':').next().map(String::from)) 32 | .or_else(|| locale_env_var("LC_ALL")) 33 | .or_else(|| locale_env_var("LC_MESSAGES")) 34 | .or_else(|| locale_env_var("LANG")) 35 | .unwrap_or_else(|| "en-US".to_string()); 36 | 37 | // This is done because the locale parsing library we use expects an unicode locale, but these vars have an ISO locale 38 | if let Some(idx) = locale.chars().position(|c| c == '.' || c == '@') { 39 | locale.truncate(idx); 40 | } 41 | locale 42 | } 43 | -------------------------------------------------------------------------------- /druid-shell/src/backend/shared/linux/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // environment based utilities 5 | pub mod env; 6 | -------------------------------------------------------------------------------- /druid-shell/src/backend/shared/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Logic that is shared by more than one backend. 5 | 6 | cfg_if::cfg_if! { 7 | if #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "linux", target_os = "openbsd"))] { 8 | mod keyboard; 9 | pub use keyboard::*; 10 | } 11 | } 12 | cfg_if::cfg_if! { 13 | if #[cfg(all(any(target_os = "freebsd", target_os = "linux"), any(feature = "x11", feature = "wayland")))] { 14 | mod timer; 15 | pub(crate) use timer::*; 16 | pub(crate) mod xkb; 17 | pub(crate) mod linux; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /druid-shell/src/backend/shared/timer.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::TimerToken; 5 | use std::{cmp::Ordering, time::Instant}; 6 | 7 | /// A timer is a deadline (`std::Time::Instant`) and a `TimerToken`. 8 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 9 | pub(crate) struct Timer { 10 | deadline: Instant, 11 | token: TimerToken, 12 | pub data: T, 13 | } 14 | 15 | impl Timer { 16 | pub(crate) fn new(deadline: Instant, data: T) -> Self { 17 | let token = TimerToken::next(); 18 | Self { 19 | deadline, 20 | token, 21 | data, 22 | } 23 | } 24 | 25 | pub(crate) fn deadline(&self) -> Instant { 26 | self.deadline 27 | } 28 | 29 | pub(crate) fn token(&self) -> TimerToken { 30 | self.token 31 | } 32 | } 33 | 34 | impl Ord for Timer { 35 | /// Ordering is so that earliest deadline sorts first 36 | // "Earliest deadline first" that a std::collections::BinaryHeap will have the earliest timer 37 | // at its head, which is just what is needed for timer management. 38 | fn cmp(&self, other: &Self) -> Ordering { 39 | self.deadline.cmp(&other.deadline).reverse() 40 | } 41 | } 42 | 43 | impl PartialOrd for Timer { 44 | fn partial_cmp(&self, other: &Self) -> Option { 45 | Some(self.cmp(other)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /druid-shell/src/backend/shared/xkb/xkbcommon_sys.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | #![allow(unused, non_upper_case_globals, non_camel_case_types, non_snake_case)] 5 | // unknown lints to make compile on older rust versions 6 | #![cfg_attr(test, allow(unknown_lints, deref_nullptr))] 7 | // generated code has some redundant static lifetimes, I don't think we can change that. 8 | #![allow(clippy::redundant_static_lifetimes)] 9 | 10 | use nix::libc::FILE; 11 | include!(concat!(env!("OUT_DIR"), "/xkbcommon_sys.rs")); 12 | -------------------------------------------------------------------------------- /druid-shell/src/backend/wayland/.README.md: -------------------------------------------------------------------------------- 1 | ### development notes 2 | - setting `export WAYLAND_DEBUG=1` allows you to see the various API calls and their values sent to wayland. 3 | - wlroots repository was a bunch of examples you can run as a reference to see the output of `WAYLAND_DEBUG`. -------------------------------------------------------------------------------- /druid-shell/src/backend/wayland/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! wayland platform errors. 5 | 6 | use std::{error::Error as StdError, fmt, sync::Arc}; 7 | use wayland_client as wl; 8 | 9 | #[derive(Debug, Clone)] 10 | pub enum Error { 11 | /// Error connecting to wayland server. 12 | Connect(Arc), 13 | /// A wayland global either doesn't exist, or doesn't support the version we need. 14 | Global { 15 | name: String, 16 | version: u32, 17 | inner: Arc, 18 | }, 19 | /// An unexpected error occurred. It's not handled by `druid-shell`/wayland, so you should 20 | /// terminate the app. 21 | Fatal(Arc), 22 | String(ErrorString), 23 | InvalidParent(u32), 24 | /// general error. 25 | Err(Arc), 26 | } 27 | 28 | impl Error { 29 | #[allow(clippy::self_named_constructors)] 30 | pub fn error(e: impl StdError + 'static) -> Self { 31 | Self::Err(Arc::new(e)) 32 | } 33 | 34 | pub fn fatal(e: impl StdError + 'static) -> Self { 35 | Self::Fatal(Arc::new(e)) 36 | } 37 | 38 | pub fn global(name: impl Into, version: u32, inner: wl::GlobalError) -> Self { 39 | Error::Global { 40 | name: name.into(), 41 | version, 42 | inner: Arc::new(inner), 43 | } 44 | } 45 | 46 | pub fn string(s: impl Into) -> Self { 47 | Error::String(ErrorString::from(s)) 48 | } 49 | } 50 | 51 | impl fmt::Display for Error { 52 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 53 | match self { 54 | Self::Connect(e) => write!(f, "could not connect to the wayland server: {e:?}"), 55 | Self::Global { name, version, .. } => write!( 56 | f, 57 | "a required wayland global ({name}@{version}) was unavailable" 58 | ), 59 | Self::Fatal(e) => write!(f, "an unhandled error occurred: {e:?}"), 60 | Self::Err(e) => write!(f, "an unhandled error occurred: {e:?}"), 61 | Self::String(e) => e.fmt(f), 62 | Self::InvalidParent(id) => write!(f, "invalid parent window for popup: {id:?}"), 63 | } 64 | } 65 | } 66 | 67 | impl std::error::Error for Error { 68 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 69 | match self { 70 | Self::Connect(e) => Some(&**e), 71 | Self::Global { inner, .. } => Some(&**inner), 72 | Self::Fatal(e) => Some(&**e), 73 | Self::Err(e) => Some(&**e), 74 | Self::String(e) => Some(e), 75 | Self::InvalidParent(_) => None, 76 | } 77 | } 78 | } 79 | 80 | impl From for Error { 81 | fn from(err: wl::ConnectError) -> Self { 82 | Self::Connect(Arc::new(err)) 83 | } 84 | } 85 | 86 | #[derive(Debug, Clone)] 87 | pub struct ErrorString { 88 | details: String, 89 | } 90 | 91 | impl ErrorString { 92 | pub fn from(s: impl Into) -> Self { 93 | Self { details: s.into() } 94 | } 95 | } 96 | 97 | impl std::fmt::Display for ErrorString { 98 | fn fmt(&self, f: &mut std::fmt::Formatter) -> fmt::Result { 99 | write!(f, "{}", self.details) 100 | } 101 | } 102 | 103 | impl std::error::Error for ErrorString { 104 | fn description(&self) -> &str { 105 | &self.details 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /druid-shell/src/backend/wayland/menu.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | #![allow(unused)] 5 | use super::window::WindowHandle; 6 | use crate::common_util::strip_access_key; 7 | use crate::hotkey::{HotKey, RawMods}; 8 | use crate::keyboard::{KbKey, Modifiers}; 9 | 10 | #[derive(Default, Debug)] 11 | pub struct Menu; 12 | 13 | #[derive(Debug)] 14 | struct MenuItem; 15 | 16 | impl Menu { 17 | pub fn new() -> Menu { 18 | Menu 19 | } 20 | 21 | pub fn new_for_popup() -> Menu { 22 | Menu 23 | } 24 | 25 | pub fn add_dropdown(&mut self, menu: Menu, text: &str, _enabled: bool) { 26 | tracing::warn!("unimplemented"); 27 | } 28 | 29 | pub fn add_item( 30 | &mut self, 31 | _id: u32, 32 | _text: &str, 33 | _key: Option<&HotKey>, 34 | _selected: Option, 35 | _enabled: bool, 36 | ) { 37 | tracing::warn!("unimplemented"); 38 | } 39 | 40 | pub fn add_separator(&mut self) { 41 | tracing::warn!("unimplemented"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /druid-shell/src/backend/wayland/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! wayland platform support 5 | 6 | // TODO: Remove this and fix the non-Send/Sync Arc issues 7 | #![allow(clippy::arc_with_non_send_sync)] 8 | 9 | pub mod application; 10 | pub mod clipboard; 11 | mod display; 12 | pub mod error; 13 | mod events; 14 | pub mod keyboard; 15 | pub mod menu; 16 | mod outputs; 17 | pub mod pointers; 18 | pub mod screen; 19 | pub mod surfaces; 20 | pub mod window; 21 | 22 | /// Little enum to make it clearer what some return values mean. 23 | #[derive(Copy, Clone)] 24 | enum Changed { 25 | Changed, 26 | Unchanged, 27 | } 28 | 29 | impl Changed { 30 | fn is_changed(self) -> bool { 31 | matches!(self, Changed::Changed) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /druid-shell/src/backend/wayland/screen.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! wayland Monitors and Screen information. 5 | 6 | use crate::kurbo::Rect; 7 | 8 | use crate::screen::Monitor; 9 | 10 | use super::error; 11 | use super::outputs; 12 | 13 | fn _get_monitors() -> Result, error::Error> { 14 | let metas = outputs::current()?; 15 | let monitors: Vec = metas 16 | .iter() 17 | .map(|m| { 18 | let rect = Rect::from_origin_size( 19 | (m.position.x as f64, m.position.y as f64), 20 | (m.logical.width as f64, m.logical.height as f64), 21 | ); 22 | Monitor::new(false, rect, rect) 23 | }) 24 | .collect(); 25 | Ok(monitors) 26 | } 27 | 28 | pub(crate) fn get_monitors() -> Vec { 29 | match _get_monitors() { 30 | Ok(m) => m, 31 | Err(cause) => { 32 | tracing::error!( 33 | "unable to detect monitors, failed to connect to wayland server {:?}", 34 | cause 35 | ); 36 | Vec::new() 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /druid-shell/src/backend/wayland/surfaces/idle.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::common_util::IdleCallback; 5 | use crate::window; 6 | 7 | /// This represents different Idle Callback Mechanism 8 | pub(super) enum Kind { 9 | Callback(Box), 10 | Token(window::IdleToken), 11 | } 12 | 13 | impl std::fmt::Debug for Kind { 14 | fn fmt(&self, format: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { 15 | match self { 16 | Kind::Callback(_) => format.debug_struct("Idle(Callback)").finish(), 17 | Kind::Token(token) => format 18 | .debug_struct("Idle(Token)") 19 | .field("token", &token) 20 | .finish(), 21 | } 22 | } 23 | } 24 | 25 | #[derive(Clone)] 26 | pub struct Handle { 27 | pub(super) queue: std::sync::Arc>>, 28 | } 29 | 30 | impl Handle { 31 | /// Add an idle handler, which is called (once) when the message loop 32 | /// is empty. The idle handler will be run from the main UI thread, and 33 | /// won't be scheduled if the associated view has been dropped. 34 | /// 35 | /// Note: the name "idle" suggests that it will be scheduled with a lower 36 | /// priority than other UI events, but that's not necessarily the case. 37 | pub fn add_idle_callback(&self, callback: F) 38 | where 39 | F: FnOnce(&mut dyn window::WinHandler) + Send + 'static, 40 | { 41 | tracing::trace!("add_idle_callback initiated"); 42 | let mut queue = self.queue.lock().unwrap(); 43 | queue.push(Kind::Callback(Box::new(callback))); 44 | } 45 | 46 | pub fn add_idle_token(&self, token: window::IdleToken) { 47 | tracing::trace!("add_idle_token initiated {:?}", token); 48 | let mut queue = self.queue.lock().unwrap(); 49 | queue.push(Kind::Token(token)); 50 | } 51 | } 52 | 53 | pub(crate) fn run(state: &Handle, winhandle: &mut dyn window::WinHandler) { 54 | let queue: Vec<_> = std::mem::take(&mut state.queue.lock().unwrap()); 55 | for item in queue { 56 | match item { 57 | Kind::Callback(it) => it.call(winhandle), 58 | Kind::Token(it) => winhandle.idle(it), 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /druid-shell/src/backend/web/application.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Web implementation of features at the application scope. 5 | 6 | use crate::application::AppHandler; 7 | 8 | use super::clipboard::Clipboard; 9 | use super::error::Error; 10 | 11 | #[derive(Clone)] 12 | pub(crate) struct Application; 13 | 14 | impl Application { 15 | pub fn new() -> Result { 16 | Ok(Application) 17 | } 18 | 19 | pub fn run(self, _handler: Option>) {} 20 | 21 | pub fn quit(&self) {} 22 | 23 | pub fn clipboard(&self) -> Clipboard { 24 | Clipboard 25 | } 26 | 27 | pub fn get_locale() -> String { 28 | web_sys::window() 29 | .and_then(|w| w.navigator().language()) 30 | .unwrap_or_else(|| "en-US".into()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /druid-shell/src/backend/web/clipboard.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Interactions with the browser pasteboard. 5 | 6 | use crate::clipboard::{ClipboardFormat, FormatId}; 7 | 8 | /// The browser clipboard. 9 | #[derive(Debug, Clone, Default)] 10 | pub struct Clipboard; 11 | 12 | impl Clipboard { 13 | /// Put a string onto the system clipboard. 14 | pub fn put_string(&mut self, _s: impl AsRef) { 15 | tracing::warn!("unimplemented"); 16 | } 17 | 18 | /// Put multi-format data on the system clipboard. 19 | pub fn put_formats(&mut self, _formats: &[ClipboardFormat]) { 20 | tracing::warn!("unimplemented"); 21 | } 22 | 23 | /// Get a string from the system clipboard, if one is available. 24 | pub fn get_string(&self) -> Option { 25 | tracing::warn!("unimplemented"); 26 | None 27 | } 28 | 29 | /// Given a list of supported clipboard types, returns the supported type which has 30 | /// highest priority on the system clipboard, or `None` if no types are supported. 31 | pub fn preferred_format(&self, _formats: &[FormatId]) -> Option { 32 | tracing::warn!("unimplemented"); 33 | None 34 | } 35 | 36 | /// Return data in a given format, if available. 37 | /// 38 | /// It is recommended that the `fmt` argument be a format returned by 39 | /// [`Clipboard::preferred_format`] 40 | pub fn get_format(&self, _format: FormatId) -> Option> { 41 | tracing::warn!("unimplemented"); 42 | None 43 | } 44 | 45 | pub fn available_type_names(&self) -> Vec { 46 | tracing::warn!("unimplemented"); 47 | Vec::new() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /druid-shell/src/backend/web/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Web backend errors. 5 | 6 | use wasm_bindgen::JsValue; 7 | 8 | #[derive(Debug, Clone)] 9 | pub enum Error { 10 | NoWindow, 11 | NoDocument, 12 | Js(JsValue), 13 | JsCast, 14 | NoElementById(String), 15 | NoContext, 16 | Unimplemented, 17 | } 18 | 19 | impl std::fmt::Display for Error { 20 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 21 | match self { 22 | Error::NoWindow => write!(f, "No global window found"), 23 | Error::NoDocument => write!(f, "No global document found"), 24 | Error::Js(err) => write!(f, "JavaScript error: {:?}", err.as_string()), 25 | Error::JsCast => write!(f, "JavaScript cast error"), 26 | Error::NoElementById(err) => write!(f, "get_element_by_id error: {err}"), 27 | Error::NoContext => write!(f, "Failed to get a draw context"), 28 | Error::Unimplemented => write!(f, "Requested an unimplemented feature"), 29 | } 30 | } 31 | } 32 | 33 | impl From for Error { 34 | fn from(js: JsValue) -> Error { 35 | Error::Js(js) 36 | } 37 | } 38 | 39 | impl std::error::Error for Error {} 40 | -------------------------------------------------------------------------------- /druid-shell/src/backend/web/keycodes.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Web keycode handling. 5 | 6 | use web_sys::KeyboardEvent; 7 | 8 | use crate::keyboard::{Code, KbKey, KeyEvent, KeyState, Location, Modifiers}; 9 | 10 | /// Convert a web-sys KeyboardEvent into a keyboard-types one. 11 | pub(crate) fn convert_keyboard_event( 12 | event: &KeyboardEvent, 13 | mods: Modifiers, 14 | state: KeyState, 15 | ) -> KeyEvent { 16 | KeyEvent { 17 | state, 18 | key: event.key().parse().unwrap_or(KbKey::Unidentified), 19 | code: convert_code(&event.code()), 20 | location: convert_location(event.location()), 21 | mods, 22 | repeat: event.repeat(), 23 | is_composing: event.is_composing(), 24 | } 25 | } 26 | 27 | fn convert_code(code: &str) -> Code { 28 | code.parse().unwrap_or(Code::Unidentified) 29 | } 30 | 31 | fn convert_location(loc: u32) -> Location { 32 | match loc { 33 | KeyboardEvent::DOM_KEY_LOCATION_LEFT => Location::Left, 34 | KeyboardEvent::DOM_KEY_LOCATION_RIGHT => Location::Right, 35 | KeyboardEvent::DOM_KEY_LOCATION_NUMPAD => Location::Numpad, 36 | // Should be exhaustive but in case not, use reasonable default 37 | _ => Location::Standard, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /druid-shell/src/backend/web/menu.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Safe wrapper for menus. 5 | 6 | use crate::hotkey::HotKey; 7 | 8 | /// A menu object, which can be either a top-level menubar or a 9 | /// submenu. 10 | pub struct Menu; 11 | 12 | impl Drop for Menu { 13 | fn drop(&mut self) { 14 | // TODO 15 | } 16 | } 17 | 18 | impl Menu { 19 | pub fn new() -> Menu { 20 | Menu 21 | } 22 | 23 | pub fn new_for_popup() -> Menu { 24 | Menu 25 | } 26 | 27 | pub fn add_dropdown(&mut self, _menu: Menu, _text: &str, _enabled: bool) { 28 | tracing::warn!("unimplemented"); 29 | } 30 | 31 | pub fn add_item( 32 | &mut self, 33 | _id: u32, 34 | _text: &str, 35 | _key: Option<&HotKey>, 36 | _selected: Option, 37 | _enabled: bool, 38 | ) { 39 | tracing::warn!("unimplemented"); 40 | } 41 | 42 | pub fn add_separator(&mut self) { 43 | tracing::warn!("unimplemented"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /druid-shell/src/backend/web/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Web-based backend support 5 | 6 | pub mod application; 7 | pub mod clipboard; 8 | pub mod error; 9 | pub mod keycodes; 10 | pub mod menu; 11 | pub mod screen; 12 | pub mod window; 13 | -------------------------------------------------------------------------------- /druid-shell/src/backend/web/screen.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Monitor and Screen information ignored for web. 5 | 6 | use crate::screen::Monitor; 7 | 8 | pub(crate) fn get_monitors() -> Vec { 9 | tracing::warn!("Screen::get_monitors() is not implemented for web."); 10 | Vec::new() 11 | } 12 | -------------------------------------------------------------------------------- /druid-shell/src/backend/windows/accels.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Wrappers for Windows of Accelerate Table. 5 | 6 | use std::collections::HashMap; 7 | use std::sync::{Arc, Mutex}; 8 | 9 | use once_cell::sync::Lazy; 10 | use winapi::ctypes::c_int; 11 | use winapi::shared::windef::*; 12 | use winapi::um::winuser::*; 13 | 14 | // NOTE: 15 | // https://docs.microsoft.com/en-us/windows/win32/wsw/thread-safety 16 | // All handles you obtain from functions in Kernel32 are thread-safe, 17 | // unless the MSDN Library article for the function explicitly mentions it is not. 18 | 19 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 20 | struct WindowHandle(HWND); 21 | unsafe impl Send for WindowHandle {} 22 | 23 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 24 | struct AccelHandle(HACCEL); 25 | unsafe impl Send for AccelHandle {} 26 | unsafe impl Sync for AccelHandle {} 27 | 28 | static ACCEL_TABLES: Lazy>>> = 29 | Lazy::new(|| Mutex::new(HashMap::default())); 30 | 31 | /// A Accelerators Table for Windows 32 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 33 | pub(crate) struct AccelTable { 34 | accel: AccelHandle, 35 | } 36 | 37 | impl AccelTable { 38 | fn new(accel: &[ACCEL]) -> AccelTable { 39 | let accel = 40 | unsafe { CreateAcceleratorTableW(accel as *const _ as *mut _, accel.len() as c_int) }; 41 | AccelTable { 42 | accel: AccelHandle(accel), 43 | } 44 | } 45 | 46 | pub(crate) fn handle(&self) -> HACCEL { 47 | self.accel.0 48 | } 49 | } 50 | 51 | pub(crate) fn register_accel(hwnd: HWND, accel: &[ACCEL]) { 52 | let mut table = ACCEL_TABLES.lock().unwrap(); 53 | table.insert(WindowHandle(hwnd), Arc::new(AccelTable::new(accel))); 54 | } 55 | 56 | impl Drop for AccelTable { 57 | fn drop(&mut self) { 58 | unsafe { 59 | DestroyAcceleratorTable(self.accel.0); 60 | } 61 | } 62 | } 63 | 64 | pub(crate) fn find_accels(hwnd: HWND) -> Option> { 65 | let table = ACCEL_TABLES.lock().unwrap(); 66 | table.get(&WindowHandle(hwnd)).cloned() 67 | } 68 | -------------------------------------------------------------------------------- /druid-shell/src/backend/windows/dcomp.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Safe-ish wrappers for DirectComposition and related interfaces. 5 | 6 | // This module could become a general wrapper for DirectComposition, but 7 | // for now we're just using what we need to get a swapchain up. 8 | 9 | use std::ptr::{null, null_mut}; 10 | 11 | use tracing::error; 12 | 13 | use winapi::shared::winerror::SUCCEEDED; 14 | use winapi::um::d3d11::*; 15 | use winapi::um::d3dcommon::{D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP}; 16 | use winapi::um::winnt::HRESULT; 17 | use winapi::Interface; 18 | use wio::com::ComPtr; 19 | 20 | unsafe fn wrap(hr: HRESULT, ptr: *mut T, f: F) -> Result 21 | where 22 | F: Fn(ComPtr) -> U, 23 | T: Interface, 24 | { 25 | if SUCCEEDED(hr) { 26 | Ok(f(ComPtr::from_raw(ptr))) 27 | } else { 28 | Err(hr) 29 | } 30 | } 31 | 32 | pub struct D3D11Device(ComPtr); 33 | 34 | impl D3D11Device { 35 | /// Creates a new device with basic defaults. 36 | pub(crate) fn new_simple() -> Result { 37 | let mut hr = 0; 38 | unsafe { 39 | let mut d3d11_device: *mut ID3D11Device = null_mut(); 40 | // Note: could probably set single threaded in flags for small performance boost. 41 | let flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; 42 | // Prefer hardware but use warp if it's the only driver available. 43 | for driver_type in &[D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP] { 44 | hr = D3D11CreateDevice( 45 | null_mut(), 46 | *driver_type, 47 | null_mut(), 48 | flags, 49 | null(), 50 | 0, 51 | D3D11_SDK_VERSION, 52 | &mut d3d11_device, 53 | null_mut(), 54 | null_mut(), 55 | ); 56 | if SUCCEEDED(hr) { 57 | break; 58 | } 59 | } 60 | if !SUCCEEDED(hr) { 61 | error!("D3D11CreateDevice: 0x{:x}", hr); 62 | } 63 | wrap(hr, d3d11_device, D3D11Device) 64 | } 65 | } 66 | 67 | pub(crate) fn raw_ptr(&mut self) -> *mut ID3D11Device { 68 | self.0.as_raw() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /druid-shell/src/backend/windows/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Errors at the application shell level. 5 | 6 | use std::fmt; 7 | use std::ptr::{null, null_mut}; 8 | 9 | use winapi::shared::minwindef::{DWORD, HLOCAL}; 10 | use winapi::shared::ntdef::LPWSTR; 11 | use winapi::shared::winerror::HRESULT; 12 | use winapi::um::winbase::{ 13 | FormatMessageW, LocalFree, FORMAT_MESSAGE_ALLOCATE_BUFFER, FORMAT_MESSAGE_FROM_SYSTEM, 14 | FORMAT_MESSAGE_IGNORE_INSERTS, FORMAT_MESSAGE_MAX_WIDTH_MASK, 15 | }; 16 | 17 | use super::util::FromWide; 18 | 19 | /// Windows backend error. 20 | #[derive(Debug, Clone)] 21 | pub enum Error { 22 | /// Windows error code. 23 | Hr(HRESULT), 24 | // Maybe include the full error from the direct2d crate. 25 | Direct2D, 26 | /// A function is available on newer version of windows. 27 | OldWindows, 28 | /// The `hwnd` pointer was null. 29 | NullHwnd, 30 | } 31 | 32 | fn hresult_description(hr: HRESULT) -> Option { 33 | unsafe { 34 | let mut message_buffer: LPWSTR = std::ptr::null_mut(); 35 | let format_result = FormatMessageW( 36 | FORMAT_MESSAGE_FROM_SYSTEM 37 | | FORMAT_MESSAGE_ALLOCATE_BUFFER 38 | | FORMAT_MESSAGE_IGNORE_INSERTS 39 | | FORMAT_MESSAGE_MAX_WIDTH_MASK, 40 | null(), 41 | hr as DWORD, 42 | 0, 43 | &mut message_buffer as *mut LPWSTR as LPWSTR, 44 | 0, 45 | null_mut(), 46 | ); 47 | if format_result == 0 || message_buffer.is_null() { 48 | return None; 49 | } 50 | 51 | let result = message_buffer.to_string(); 52 | LocalFree(message_buffer as HLOCAL); 53 | result 54 | } 55 | } 56 | 57 | impl fmt::Display for Error { 58 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 59 | match self { 60 | Error::Hr(hr) => { 61 | write!(f, "HRESULT 0x{hr:x}")?; 62 | if let Some(description) = hresult_description(*hr) { 63 | write!(f, ": {description}")?; 64 | } 65 | Ok(()) 66 | } 67 | Error::Direct2D => write!(f, "Direct2D error"), 68 | Error::OldWindows => write!(f, "Attempted newer API on older Windows"), 69 | Error::NullHwnd => write!(f, "Window handle is Null"), 70 | } 71 | } 72 | } 73 | 74 | impl std::error::Error for Error {} 75 | -------------------------------------------------------------------------------- /druid-shell/src/backend/windows/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Windows implementation of `druid-shell`. 5 | 6 | mod accels; 7 | pub mod application; 8 | pub mod clipboard; 9 | pub mod dcomp; 10 | pub mod dialog; 11 | pub mod error; 12 | mod keyboard; 13 | pub mod menu; 14 | pub mod paint; 15 | pub mod screen; 16 | mod timers; 17 | pub mod util; 18 | pub mod window; 19 | 20 | // https://docs.microsoft.com/en-us/windows/win32/direct2d/render-targets-overview 21 | // ID2D1RenderTarget is the interface. The other resources inherit from it. 22 | // 23 | // A Render Target creates resources for drawing and performs drawing operations. 24 | // 25 | // - ID2D1HwndRenderTarget objects render content to a window. 26 | // - ID2D1DCRenderTarget objects render to a GDI device context. 27 | // - bitmap render target objects render to off-screen bitmap. 28 | // - DXGI render target objects render to a DXGI surface for use with Direct3D. 29 | // 30 | // https://docs.microsoft.com/en-us/windows/win32/direct2d/devices-and-device-contexts 31 | // A Device Context, ID2D1DeviceContext, is available as of windows 7 platform update. This 32 | // is the minimum compatibility target for Druid. We are not making an effort to do 33 | // RenderTarget only. 34 | // 35 | // Basically, go from HwndRenderTarget or DxgiSurfaceRenderTarget (2d or 3d) to a Device Context. 36 | // Go back up for particular needs. 37 | 38 | use piet_common::d2d::DeviceContext; 39 | use std::fmt::{Debug, Display, Formatter}; 40 | use winapi::shared::winerror::HRESULT; 41 | use winapi::um::d2d1::ID2D1RenderTarget; 42 | use wio::com::ComPtr; 43 | 44 | #[derive(Clone)] 45 | pub struct DxgiSurfaceRenderTarget { 46 | ptr: ComPtr, 47 | } 48 | 49 | impl DxgiSurfaceRenderTarget { 50 | /// construct from raw ptr 51 | /// 52 | /// # Safety 53 | /// TODO 54 | pub unsafe fn from_raw(raw: *mut ID2D1RenderTarget) -> Self { 55 | DxgiSurfaceRenderTarget { 56 | ptr: ComPtr::from_raw(raw), 57 | } 58 | } 59 | 60 | /// cast to DeviceContext 61 | /// 62 | /// # Safety 63 | /// TODO 64 | pub unsafe fn as_device_context(&self) -> Option { 65 | self.ptr 66 | .cast() 67 | .ok() 68 | .map(|com_ptr| DeviceContext::new(com_ptr)) 69 | } 70 | } 71 | 72 | // error handling 73 | pub enum Error { 74 | WinapiError(HRESULT), 75 | } 76 | 77 | impl From for Error { 78 | fn from(hr: HRESULT) -> Error { 79 | Error::WinapiError(hr) 80 | } 81 | } 82 | 83 | impl Debug for Error { 84 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 85 | match self { 86 | Error::WinapiError(hr) => write!(f, "hresult {hr:x}"), 87 | } 88 | } 89 | } 90 | 91 | impl Display for Error { 92 | fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { 93 | match self { 94 | Error::WinapiError(hr) => write!(f, "hresult {hr:x}"), 95 | } 96 | } 97 | } 98 | 99 | impl std::error::Error for Error { 100 | fn description(&self) -> &str { 101 | "winapi error" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /druid-shell/src/backend/windows/paint.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2017 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Bureaucracy to create render targets for painting. 5 | //! 6 | //! Note that these are currently implemented using hwnd render targets 7 | //! because they are are (relatively) easy, but for high performance we want 8 | //! dxgi render targets so we can use present options for minimal 9 | //! invalidation and low-latency frame timing. 10 | 11 | use std::ptr::null_mut; 12 | 13 | use winapi::ctypes::c_void; 14 | use winapi::shared::dxgi::*; 15 | use winapi::shared::dxgi1_2::*; 16 | use winapi::shared::dxgiformat::*; 17 | use winapi::shared::winerror::*; 18 | use winapi::um::d2d1::*; 19 | use winapi::um::dcommon::*; 20 | use winapi::Interface; 21 | 22 | use piet_common::d2d::D2DFactory; 23 | 24 | use crate::backend::windows::DxgiSurfaceRenderTarget; 25 | use crate::scale::Scale; 26 | 27 | use super::error::Error; 28 | use super::util::as_result; 29 | use super::window::SCALE_TARGET_DPI; 30 | 31 | /// Create a render target from a DXGI swapchain. 32 | /// 33 | /// TODO: probably want to create a DeviceContext, it's more flexible. 34 | pub(crate) unsafe fn create_render_target_dxgi( 35 | d2d_factory: &D2DFactory, 36 | swap_chain: *mut IDXGISwapChain1, 37 | scale: Scale, 38 | transparent: bool, 39 | ) -> Result { 40 | let mut buffer: *mut IDXGISurface = null_mut(); 41 | as_result((*swap_chain).GetBuffer( 42 | 0, 43 | &IDXGISurface::uuidof(), 44 | &mut buffer as *mut _ as *mut *mut c_void, 45 | ))?; 46 | let props = D2D1_RENDER_TARGET_PROPERTIES { 47 | _type: D2D1_RENDER_TARGET_TYPE_DEFAULT, 48 | pixelFormat: D2D1_PIXEL_FORMAT { 49 | format: DXGI_FORMAT_B8G8R8A8_UNORM, 50 | alphaMode: if transparent { 51 | D2D1_ALPHA_MODE_PREMULTIPLIED 52 | } else { 53 | D2D1_ALPHA_MODE_IGNORE 54 | }, 55 | }, 56 | dpiX: (scale.x() * SCALE_TARGET_DPI) as f32, 57 | dpiY: (scale.y() * SCALE_TARGET_DPI) as f32, 58 | usage: D2D1_RENDER_TARGET_USAGE_NONE, 59 | minLevel: D2D1_FEATURE_LEVEL_DEFAULT, 60 | }; 61 | 62 | let mut render_target: *mut ID2D1RenderTarget = null_mut(); 63 | let res = 64 | (*d2d_factory.get_raw()).CreateDxgiSurfaceRenderTarget(buffer, &props, &mut render_target); 65 | (*buffer).Release(); 66 | if SUCCEEDED(res) { 67 | // TODO: maybe use builder 68 | Ok(DxgiSurfaceRenderTarget::from_raw(render_target)) 69 | } else { 70 | Err(res.into()) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /druid-shell/src/backend/windows/screen.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Windows Monitors and Screen information. 5 | 6 | use super::error::Error; 7 | use std::mem::size_of; 8 | use std::ptr::null_mut; 9 | use tracing::warn; 10 | use winapi::shared::minwindef::*; 11 | use winapi::shared::windef::*; 12 | use winapi::shared::winerror::*; 13 | use winapi::um::errhandlingapi::GetLastError; 14 | use winapi::um::winuser::*; 15 | 16 | use crate::kurbo::Rect; 17 | use crate::screen::Monitor; 18 | 19 | unsafe extern "system" fn monitorenumproc( 20 | hmonitor: HMONITOR, 21 | _hdc: HDC, 22 | _lprect: LPRECT, 23 | _lparam: LPARAM, 24 | ) -> BOOL { 25 | let rect = RECT { 26 | left: 0, 27 | top: 0, 28 | right: 0, 29 | bottom: 0, 30 | }; 31 | let mut info = MONITORINFO { 32 | cbSize: size_of::() as u32, 33 | rcMonitor: rect, 34 | rcWork: rect, 35 | dwFlags: 0, 36 | }; 37 | if GetMonitorInfoW(hmonitor, &mut info) == 0 { 38 | warn!( 39 | "failed to get Monitor Info: {}", 40 | Error::Hr(HRESULT_FROM_WIN32(GetLastError())) 41 | ); 42 | }; 43 | let primary = info.dwFlags == MONITORINFOF_PRIMARY; 44 | let rect = Rect::new( 45 | info.rcMonitor.left as f64, 46 | info.rcMonitor.top as f64, 47 | info.rcMonitor.right as f64, 48 | info.rcMonitor.bottom as f64, 49 | ); 50 | let work_rect = Rect::new( 51 | info.rcWork.left as f64, 52 | info.rcWork.top as f64, 53 | info.rcWork.right as f64, 54 | info.rcWork.bottom as f64, 55 | ); 56 | let monitors = _lparam as *mut Vec; 57 | (*monitors).push(Monitor::new(primary, rect, work_rect)); 58 | TRUE 59 | } 60 | 61 | pub(crate) fn get_monitors() -> Vec { 62 | unsafe { 63 | let monitors = Vec::::new(); 64 | let ptr = &monitors as *const Vec; 65 | if EnumDisplayMonitors(null_mut(), null_mut(), Some(monitorenumproc), ptr as isize) == 0 { 66 | warn!( 67 | "Failed to Enumerate Display Monitors: {}", 68 | Error::Hr(HRESULT_FROM_WIN32(GetLastError())) 69 | ); 70 | }; 71 | monitors 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /druid-shell/src/backend/windows/timers.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Timer state. 5 | 6 | use std::collections::BTreeSet; 7 | use std::time::Instant; 8 | 9 | use crate::window::TimerToken; 10 | 11 | pub struct TimerSlots { 12 | // Note: we can remove this when checked_duration_since lands. 13 | next_fresh_id: u64, 14 | free_slots: BTreeSet, 15 | } 16 | 17 | impl TimerSlots { 18 | pub fn new(starting_ix: u64) -> TimerSlots { 19 | TimerSlots { 20 | next_fresh_id: starting_ix, 21 | free_slots: Default::default(), 22 | } 23 | } 24 | 25 | pub fn alloc(&mut self) -> TimerToken { 26 | if let Some(first) = self.free_slots.iter().next().cloned() { 27 | self.free_slots.remove(&first); 28 | TimerToken::from_raw(first) 29 | } else { 30 | let result = self.next_fresh_id; 31 | self.next_fresh_id += 1; 32 | TimerToken::from_raw(result) 33 | } 34 | } 35 | 36 | pub fn free(&mut self, token: TimerToken) { 37 | let id = token.into_raw(); 38 | if self.next_fresh_id == id + 1 { 39 | self.next_fresh_id -= 1; 40 | } else { 41 | self.free_slots.insert(id); 42 | } 43 | } 44 | 45 | /// Compute an elapsed value for SetTimer (in ms) 46 | pub fn compute_elapsed(&self, deadline: Instant) -> u32 { 47 | deadline 48 | .checked_duration_since(Instant::now()) 49 | .map(|d| d.as_millis() as u32) 50 | .unwrap_or(0) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /druid-shell/src/backend/x11/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Errors at the application shell level. 5 | 6 | use std::fmt; 7 | use std::sync::Arc; 8 | 9 | #[derive(Debug, Clone)] 10 | pub enum Error { 11 | XError(Arc), 12 | } 13 | 14 | impl fmt::Display for Error { 15 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 16 | let Error::XError(e) = self; 17 | e.fmt(f) 18 | } 19 | } 20 | 21 | impl std::error::Error for Error {} 22 | 23 | impl From for Error { 24 | fn from(err: x11rb::x11_utils::X11Error) -> Error { 25 | Error::XError(Arc::new(x11rb::errors::ReplyError::X11Error(err))) 26 | } 27 | } 28 | 29 | impl From for Error { 30 | fn from(err: x11rb::errors::ReplyError) -> Error { 31 | Error::XError(Arc::new(err)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /druid-shell/src/backend/x11/menu.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! X11 menus implementation. 5 | 6 | use crate::hotkey::HotKey; 7 | 8 | pub struct Menu; 9 | 10 | impl Menu { 11 | pub fn new() -> Menu { 12 | // TODO(x11/menus): implement Menu::new (currently a no-op) 13 | tracing::warn!("Menu::new is currently unimplemented for X11 backend."); 14 | Menu {} 15 | } 16 | 17 | pub fn new_for_popup() -> Menu { 18 | // TODO(x11/menus): implement Menu::new_for_popup (currently a no-op) 19 | tracing::warn!("Menu::new_for_popup is currently unimplemented for X11 backend."); 20 | Menu {} 21 | } 22 | 23 | pub fn add_dropdown(&mut self, mut _menu: Menu, _text: &str, _enabled: bool) { 24 | // TODO(x11/menus): implement Menu::add_dropdown (currently a no-op) 25 | tracing::warn!("Menu::add_dropdown is currently unimplemented for X11 backend."); 26 | } 27 | 28 | pub fn add_item( 29 | &mut self, 30 | _id: u32, 31 | _text: &str, 32 | _key: Option<&HotKey>, 33 | _selected: Option, 34 | _enabled: bool, 35 | ) { 36 | // TODO(x11/menus): implement Menu::add_item (currently a no-op) 37 | tracing::warn!("Menu::add_item is currently unimplemented for X11 backend."); 38 | } 39 | 40 | pub fn add_separator(&mut self) { 41 | // TODO(x11/menus): implement Menu::add_separator (currently a no-op) 42 | tracing::warn!("Menu::add_separator is currently unimplemented for X11 backend."); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /druid-shell/src/backend/x11/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! X11 implementation of `druid-shell`. 5 | 6 | // TODO(x11/render_improvements): screen is currently flashing when resizing in perftest. 7 | // Might be related to the "sleep scheduler" in XWindow::render()? 8 | // TODO(x11/render_improvements): double-buffering / present strategies / etc? 9 | 10 | // # Notes on error handling in X11 11 | // 12 | // In XCB, errors are reported asynchronously by default, by sending them to the event 13 | // loop. You can also request a synchronous error for a given call; we use this in 14 | // window initialization, but otherwise we take the async route. 15 | // 16 | // When checking for X11 errors synchronously, there are two places where the error could 17 | // happen. An error on the request means the connection is broken. There's no need for 18 | // extra error context here, because the fact that the connection broke has nothing to do 19 | // with what we're trying to do. An error on the reply means there was something wrong with 20 | // the request, and so we add context. This convention is used throughout the x11 backend. 21 | 22 | #[macro_use] 23 | mod util; 24 | 25 | pub mod application; 26 | pub mod clipboard; 27 | pub mod dialog; 28 | pub mod error; 29 | pub mod menu; 30 | pub mod screen; 31 | pub mod window; 32 | -------------------------------------------------------------------------------- /druid-shell/src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Errors at the application shell level. 5 | 6 | use std::fmt; 7 | use std::sync::Arc; 8 | 9 | use crate::backend::error as backend; 10 | 11 | /// Shell errors. 12 | #[derive(Debug, Clone)] 13 | pub enum Error { 14 | /// The Application instance has already been created. 15 | ApplicationAlreadyExists, 16 | /// Tried to use the application after it had been dropped. 17 | ApplicationDropped, 18 | /// The window has already been destroyed. 19 | WindowDropped, 20 | /// Platform specific error. 21 | Platform(backend::Error), 22 | /// Other miscellaneous error. 23 | Other(Arc), 24 | } 25 | 26 | impl fmt::Display for Error { 27 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 28 | match self { 29 | Error::ApplicationAlreadyExists => { 30 | write!(f, "An application instance has already been created.") 31 | } 32 | Error::ApplicationDropped => { 33 | write!( 34 | f, 35 | "The application this operation requires has been dropped." 36 | ) 37 | } 38 | Error::Platform(err) => fmt::Display::fmt(err, f), 39 | Error::WindowDropped => write!(f, "The window has already been destroyed."), 40 | Error::Other(s) => write!(f, "{s}"), 41 | } 42 | } 43 | } 44 | 45 | impl std::error::Error for Error {} 46 | 47 | impl From for Error { 48 | fn from(src: anyhow::Error) -> Error { 49 | Error::Other(Arc::new(src)) 50 | } 51 | } 52 | 53 | impl From for Error { 54 | fn from(src: backend::Error) -> Error { 55 | Error::Platform(src) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /druid-shell/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Platform abstraction for Druid toolkit. 5 | //! 6 | //! `druid-shell` is an abstraction around a given platform UI & application 7 | //! framework. It provides common types, which then defer to a platform-defined 8 | //! implementation. 9 | //! 10 | //! # Env 11 | //! 12 | //! For testing and debugging, `druid-shell` can change its behavior based on environment 13 | //! variables. Here is a list of environment variables that `druid-shell` supports: 14 | //! 15 | //! - `DRUID_SHELL_DISABLE_X11_PRESENT`: if this is set and `druid-shell` is using the `x11` 16 | //! backend, it will avoid using the Present extension. 17 | 18 | #![warn(rustdoc::broken_intra_doc_links)] 19 | #![allow(clippy::new_without_default)] 20 | #![deny(clippy::trivially_copy_pass_by_ref)] 21 | #![doc( 22 | html_logo_url = "https://raw.githubusercontent.com/linebender/druid/screenshots/images/doc_logo.png" 23 | )] 24 | // This is overeager right now, see https://github.com/rust-lang/rust-clippy/issues/8494 25 | #![allow(clippy::iter_overeager_cloned)] 26 | 27 | // Rename `gtk_rs` back to `gtk`. 28 | // This allows us to use `gtk` as the feature name. 29 | // The `target_os` requirement is there to exclude anything `wasm` like. 30 | #[cfg(all( 31 | any(target_os = "freebsd", target_os = "linux", target_os = "openbsd"), 32 | feature = "gtk" 33 | ))] 34 | extern crate gtk_rs as gtk; 35 | 36 | #[cfg(feature = "image")] 37 | pub use piet::image_crate as image; 38 | pub use piet::kurbo; 39 | pub use piet_common as piet; 40 | 41 | // Reexport the version of `raw_window_handle` we are using. 42 | #[cfg(feature = "raw-win-handle")] 43 | pub use raw_window_handle; 44 | 45 | #[macro_use] 46 | mod util; 47 | 48 | mod application; 49 | mod backend; 50 | mod clipboard; 51 | mod common_util; 52 | mod dialog; 53 | mod error; 54 | mod hotkey; 55 | mod keyboard; 56 | mod menu; 57 | mod mouse; 58 | mod region; 59 | mod scale; 60 | mod screen; 61 | mod window; 62 | 63 | pub mod platform; 64 | pub mod text; 65 | 66 | pub use application::{AppHandler, Application}; 67 | pub use clipboard::{Clipboard, ClipboardFormat, FormatId}; 68 | pub use common_util::Counter; 69 | pub use dialog::{FileDialogOptions, FileInfo, FileSpec}; 70 | pub use error::Error; 71 | pub use hotkey::{HotKey, RawMods, SysMods}; 72 | pub use keyboard::{Code, IntoKey, KbKey, KeyEvent, KeyState, Location, Modifiers}; 73 | pub use menu::Menu; 74 | pub use mouse::{Cursor, CursorDesc, MouseButton, MouseButtons, MouseEvent}; 75 | pub use region::Region; 76 | pub use scale::{Scalable, Scale, ScaledArea}; 77 | pub use screen::{Monitor, Screen}; 78 | pub use window::{ 79 | FileDialogToken, IdleHandle, IdleToken, TextFieldToken, TimerToken, WinHandler, WindowBuilder, 80 | WindowHandle, WindowLevel, WindowState, 81 | }; 82 | 83 | pub use keyboard_types; 84 | -------------------------------------------------------------------------------- /druid-shell/src/menu.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::backend::menu as backend; 5 | use crate::hotkey::HotKey; 6 | 7 | /// A menu object. 8 | /// 9 | /// This may be a window menu, an application menu (macOS) or a context (right-click) 10 | /// menu. 11 | /// 12 | /// # Configuring menus 13 | /// 14 | /// Currently, a menu and its items cannot be changed once created. If you need 15 | /// to change anything about a menu (for instance, disabling or selecting items) 16 | /// you need to create a new menu with the desired properties. 17 | pub struct Menu(pub(crate) backend::Menu); 18 | 19 | impl Menu { 20 | /// Create a new empty window or application menu. 21 | pub fn new() -> Menu { 22 | Menu(backend::Menu::new()) 23 | } 24 | 25 | /// Create a new empty context menu. 26 | /// 27 | /// Some platforms distinguish between these types of menus, and some 28 | /// do not. 29 | pub fn new_for_popup() -> Menu { 30 | Menu(backend::Menu::new_for_popup()) 31 | } 32 | 33 | /// Consume this `Menu`, returning the platform menu object. 34 | pub(crate) fn into_inner(self) -> backend::Menu { 35 | self.0 36 | } 37 | 38 | /// Add the provided `Menu` as a submenu of self, with the provided title. 39 | pub fn add_dropdown(&mut self, menu: Menu, text: &str, enabled: bool) { 40 | self.0.add_dropdown(menu.0, text, enabled) 41 | } 42 | 43 | /// Add an item to this menu. 44 | /// 45 | /// The `id` should uniquely identify this item. If the user selects this 46 | /// item, the responsible [`WinHandler`]'s [`command`] method will 47 | /// be called with this `id`. If the `enabled` argument is false, the menu 48 | /// item will be grayed out; the hotkey will also be disabled. 49 | /// If the `selected` argument is `true`, the menu will have a checkmark 50 | /// or platform appropriate equivalent indicating that it is currently selected. 51 | /// The `key` argument is an optional [`HotKey`] that will be registered 52 | /// with the system. 53 | /// 54 | /// 55 | /// [`WinHandler`]: crate::WinHandler 56 | /// [`command`]: crate::WinHandler::command 57 | pub fn add_item( 58 | &mut self, 59 | id: u32, 60 | text: &str, 61 | key: Option<&HotKey>, 62 | selected: Option, 63 | enabled: bool, 64 | ) { 65 | self.0.add_item(id, text, key, selected, enabled) 66 | } 67 | 68 | /// Add a separator to the menu. 69 | pub fn add_separator(&mut self) { 70 | self.0.add_separator() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /druid-shell/src/platform/linux.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Linux specific extensions. 5 | use crate::Clipboard; 6 | 7 | /// Linux specific extensions to [`Application`] 8 | /// 9 | /// [`Application`]: crate::Application 10 | pub trait ApplicationExt { 11 | /// Returns a handle to the primary system clipboard. 12 | /// 13 | /// This is useful for middle mouse paste. 14 | fn primary_clipboard(&self) -> Clipboard; 15 | } 16 | 17 | #[cfg(test)] 18 | #[allow(unused_imports)] 19 | mod test { 20 | use crate::Application; 21 | 22 | use super::*; 23 | use static_assertions as sa; 24 | // TODO: impl ApplicationExt for wayland 25 | #[cfg(not(feature = "wayland"))] 26 | sa::assert_impl_all!(Application: ApplicationExt); 27 | } 28 | -------------------------------------------------------------------------------- /druid-shell/src/platform/mac.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! macOS specific extensions. 5 | 6 | /// macOS specific extensions to [`Application`] 7 | /// 8 | /// [`Application`]: crate::Application 9 | pub trait ApplicationExt { 10 | /// Hide the application this window belongs to. (cmd+H) 11 | fn hide(&self); 12 | 13 | /// Hide all other applications. (cmd+opt+H) 14 | fn hide_others(&self); 15 | 16 | /// Sets the global application menu, on platforms where there is one. 17 | /// 18 | /// On platforms with no global application menu, this has no effect. 19 | fn set_menu(&self, menu: crate::Menu); 20 | } 21 | 22 | #[cfg(test)] 23 | mod test { 24 | use crate::Application; 25 | 26 | use super::*; 27 | use static_assertions as sa; 28 | sa::assert_impl_all!(Application: ApplicationExt); 29 | } 30 | -------------------------------------------------------------------------------- /druid-shell/src/platform/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Platorm specific extensions. 5 | 6 | #[cfg(any( 7 | doc, 8 | any(target_os = "freebsd", target_os = "linux", target_os = "openbsd") 9 | ))] 10 | pub mod linux; 11 | 12 | #[cfg(any(doc, target_os = "macos"))] 13 | pub mod mac; 14 | -------------------------------------------------------------------------------- /druid-shell/src/screen.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Module to get information about monitors 5 | 6 | use crate::backend; 7 | use crate::kurbo::Rect; 8 | use std::fmt; 9 | use std::fmt::Display; 10 | 11 | /// Monitor struct containing data about a monitor on the system 12 | /// 13 | /// Use [`Screen::get_monitors`] to return a `Vec` of all the monitors on the system 14 | /// 15 | /// [`Screen::get_monitors`]: Screen::get_monitors 16 | #[derive(Clone, Debug, PartialEq)] 17 | pub struct Monitor { 18 | primary: bool, 19 | rect: Rect, 20 | // TODO: Work area, cross_platform 21 | // https://developer.apple.com/documentation/appkit/nsscreen/1388369-visibleframe 22 | // https://developer.gnome.org/gdk3/stable/GdkMonitor.html#gdk-monitor-get-workarea 23 | // https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-monitorinfo 24 | // Unsure about x11 25 | work_rect: Rect, 26 | } 27 | 28 | impl Monitor { 29 | #[allow(dead_code)] 30 | pub(crate) fn new(primary: bool, rect: Rect, work_rect: Rect) -> Self { 31 | Monitor { 32 | primary, 33 | rect, 34 | work_rect, 35 | } 36 | } 37 | /// Returns true if the monitor is the primary monitor. 38 | /// The primary monitor has its origin at (0, 0) in virtual screen coordinates. 39 | pub fn is_primary(&self) -> bool { 40 | self.primary 41 | } 42 | /// Returns the monitor rectangle in virtual screen coordinates. 43 | pub fn virtual_rect(&self) -> Rect { 44 | self.rect 45 | } 46 | 47 | /// Returns the monitor working rectangle in virtual screen coordinates. 48 | /// The working rectangle excludes certain things like the dock and menubar on mac, 49 | /// and the taskbar on windows. 50 | pub fn virtual_work_rect(&self) -> Rect { 51 | self.work_rect 52 | } 53 | } 54 | 55 | impl Display for Monitor { 56 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 57 | if self.primary { 58 | write!(f, "Primary ")?; 59 | } else { 60 | write!(f, "Secondary ")?; 61 | } 62 | write!( 63 | f, 64 | "({}, {})({}, {})", 65 | self.rect.x0, self.rect.x1, self.rect.y0, self.rect.y1 66 | )?; 67 | Ok(()) 68 | } 69 | } 70 | 71 | /// Information about the screen and monitors 72 | pub struct Screen {} 73 | 74 | impl Screen { 75 | /// Returns a vector of all the [`monitors`] on the system. 76 | /// 77 | /// [`monitors`]: Monitor 78 | pub fn get_monitors() -> Vec { 79 | backend::screen::get_monitors() 80 | } 81 | 82 | /// Returns the bounding rectangle of the total virtual screen space in pixels. 83 | pub fn get_display_rect() -> Rect { 84 | Self::get_monitors() 85 | .iter() 86 | .map(|x| x.virtual_rect()) 87 | .fold(Rect::ZERO, |a, b| a.union(b)) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /druid/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /druid/examples/anim.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! An example of an animating widget. It is just a widget that 5 | //! requests an animation frame when it needs to, and draws the frame in the 6 | //! `paint` method. 7 | //! Once the animation is over it simply stops requesting animation frames. 8 | //! Usually we would put the state in the `Data`, but for things like animation 9 | //! we don't. This is because the animation state is not useful to know for the 10 | //! rest of the app. If this is something the rest of your widgets should know 11 | //! about, you could put it in the `data`. 12 | 13 | // On Windows platform, don't show a console when opening the app. 14 | #![windows_subsystem = "windows"] 15 | 16 | use std::f64::consts::PI; 17 | 18 | use druid::kurbo::{Circle, Line}; 19 | use druid::widget::prelude::*; 20 | use druid::{AppLauncher, Color, LocalizedString, Point, Vec2, WindowDesc}; 21 | 22 | struct AnimWidget { 23 | t: f64, 24 | } 25 | 26 | impl Widget<()> for AnimWidget { 27 | fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut (), _env: &Env) { 28 | match event { 29 | Event::MouseDown(_) => { 30 | self.t = 0.0; 31 | ctx.request_anim_frame(); 32 | } 33 | Event::AnimFrame(interval) => { 34 | ctx.request_paint(); 35 | self.t += (*interval as f64) * 1e-9; 36 | if self.t < 1.0 { 37 | ctx.request_anim_frame(); 38 | } else { 39 | // We might have t>1.0 at the end of the animation, 40 | // we want to make sure the line points up at the 41 | // end of the animation. 42 | self.t = 0.0; 43 | } 44 | } 45 | _ => (), 46 | } 47 | } 48 | 49 | fn lifecycle(&mut self, _ctx: &mut LifeCycleCtx, _event: &LifeCycle, _data: &(), _env: &Env) {} 50 | 51 | fn update(&mut self, _ctx: &mut UpdateCtx, _old_data: &(), _data: &(), _env: &Env) {} 52 | 53 | fn layout( 54 | &mut self, 55 | _layout_ctx: &mut LayoutCtx, 56 | bc: &BoxConstraints, 57 | _data: &(), 58 | _env: &Env, 59 | ) -> Size { 60 | bc.constrain((100.0, 100.0)) 61 | } 62 | 63 | fn paint(&mut self, ctx: &mut PaintCtx, _data: &(), _env: &Env) { 64 | let t = self.t; 65 | let center = Point::new(50.0, 50.0); 66 | ctx.paint_with_z_index(1, move |ctx| { 67 | let ambit = center + 45.0 * Vec2::from_angle((0.75 + t) * 2.0 * PI); 68 | ctx.stroke(Line::new(center, ambit), &Color::WHITE, 1.0); 69 | }); 70 | 71 | ctx.fill(Circle::new(center, 50.0), &Color::BLACK); 72 | } 73 | } 74 | 75 | pub fn main() { 76 | let window = WindowDesc::new(AnimWidget { t: 0.0 }).title( 77 | LocalizedString::new("anim-demo-window-title") 78 | .with_placeholder("You spin me right round..."), 79 | ); 80 | AppLauncher::with_window(window) 81 | .log_to_console() 82 | .launch(()) 83 | .expect("launch failed"); 84 | } 85 | -------------------------------------------------------------------------------- /druid/examples/assets/PicWithAlpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/druid/b831b5fe1597d5ec1fc3379cc1b39f3dd106e220/druid/examples/assets/PicWithAlpha.png -------------------------------------------------------------------------------- /druid/examples/assets/xi.image: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linebender/druid/b831b5fe1597d5ec1fc3379cc1b39f3dd106e220/druid/examples/assets/xi.image -------------------------------------------------------------------------------- /druid/examples/async_event.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! An example of sending commands from another thread. 5 | //! This is useful when you want to have some kind of 6 | //! generated content (like here), or some task that just 7 | //! takes a long time but don't want to block the main thread 8 | //! (waiting on an http request, some cpu intensive work etc.) 9 | 10 | // On Windows platform, don't show a console when opening the app. 11 | #![windows_subsystem = "windows"] 12 | 13 | use instant::Instant; 14 | use std::thread; 15 | use std::time::Duration; 16 | 17 | use druid::widget::Painter; 18 | use druid::{AppLauncher, Color, RenderContext, Widget, WidgetExt, WindowDesc}; 19 | 20 | pub fn main() { 21 | let window = WindowDesc::new(make_ui()).title("External Event Demo"); 22 | 23 | let launcher = AppLauncher::with_window(window); 24 | 25 | // If we want to create commands from another thread `launcher.get_external_handle()` 26 | // should be used. For sending commands from within widgets you can always call 27 | // `ctx.submit_command` 28 | let event_sink = launcher.get_external_handle(); 29 | // We create a new thread and generate colours in it. 30 | // This happens on a second thread so that we can run the UI in the 31 | // main thread. Generating some colours nicely follows the pattern for what 32 | // should be done like this: generating something over time 33 | // (like this or reacting to external events), or something that takes a 34 | // long time and shouldn't block main UI updates. 35 | thread::spawn(move || generate_colors(event_sink)); 36 | 37 | launcher 38 | .log_to_console() 39 | .launch(Color::BLACK) 40 | .expect("launch failed"); 41 | } 42 | 43 | fn generate_colors(event_sink: druid::ExtEventSink) { 44 | // This function is called in a separate thread, and runs until the program ends. 45 | // We take an `ExtEventSink` as an argument, we can use this event sink to send 46 | // commands to the main thread. Every time we generate a new colour we send it 47 | // to the main thread. 48 | let start_time = Instant::now(); 49 | let mut color = Color::WHITE; 50 | 51 | loop { 52 | let time_since_start = (Instant::now() - start_time).as_nanos(); 53 | let (r, g, b, _) = color.as_rgba8(); 54 | 55 | // there is no logic here; it's a very silly way of mutating the color. 56 | color = match (time_since_start % 2, time_since_start % 3) { 57 | (0, _) => Color::rgb8(r.wrapping_add(3), g, b), 58 | (_, 0) => Color::rgb8(r, g.wrapping_add(3), b), 59 | (_, _) => Color::rgb8(r, g, b.wrapping_add(3)), 60 | }; 61 | 62 | // schedule idle callback to change the data 63 | event_sink.add_idle_callback(move |data: &mut Color| { 64 | *data = color; 65 | }); 66 | thread::sleep(Duration::from_millis(20)); 67 | } 68 | } 69 | 70 | fn make_ui() -> impl Widget { 71 | Painter::new(|ctx, data, _env| { 72 | let rect = ctx.size().to_rounded_rect(5.0); 73 | ctx.fill(rect, data); 74 | }) 75 | .fix_width(300.0) 76 | .fix_height(300.0) 77 | .padding(10.0) 78 | .center() 79 | } 80 | -------------------------------------------------------------------------------- /druid/examples/either.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! An example using the Either widget to show/hide a slider. 5 | //! This is a very simple example, it uses a bool to determine 6 | //! which widget gets shown. 7 | 8 | // On Windows platform, don't show a console when opening the app. 9 | #![windows_subsystem = "windows"] 10 | 11 | use druid::widget::prelude::*; 12 | use druid::widget::{Checkbox, Either, Flex, Label, Slider}; 13 | use druid::{AppLauncher, Data, Lens, WidgetExt, WindowDesc}; 14 | 15 | #[derive(Clone, Default, Data, Lens)] 16 | struct AppState { 17 | which: bool, 18 | value: f64, 19 | } 20 | 21 | fn ui_builder() -> impl Widget { 22 | // Our UI consists of a column with a button and an `Either` widget 23 | let button = Checkbox::new("Toggle slider") 24 | .lens(AppState::which) 25 | .padding(5.0); 26 | 27 | // The `Either` widget has two children, only one of which is visible at a time. 28 | // To determine which child is visible, you pass it a closure that takes the 29 | // `Data` and the `Env` and returns a bool; if it returns `true`, the first 30 | // widget will be visible, and if `false`, the second. 31 | let either = Either::new( 32 | |data, _env| data.which, 33 | Slider::new().lens(AppState::value).padding(5.0), 34 | Label::new("Click to reveal slider").padding(5.0), 35 | ); 36 | Flex::column().with_child(button).with_child(either) 37 | } 38 | 39 | pub fn main() { 40 | let main_window = WindowDesc::new(ui_builder()).title("Switcheroo"); 41 | AppLauncher::with_window(main_window) 42 | .log_to_console() 43 | .launch(AppState::default()) 44 | .expect("launch failed"); 45 | } 46 | -------------------------------------------------------------------------------- /druid/examples/hello.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! This is a very small example of how to setup a Druid application. 5 | //! It does the almost bare minimum while still being useful. 6 | 7 | // On Windows platform, don't show a console when opening the app. 8 | #![windows_subsystem = "windows"] 9 | 10 | use druid::widget::prelude::*; 11 | use druid::widget::{Flex, Label, TextBox}; 12 | use druid::{AppLauncher, Data, Lens, UnitPoint, WidgetExt, WindowDesc}; 13 | 14 | const VERTICAL_WIDGET_SPACING: f64 = 20.0; 15 | const TEXT_BOX_WIDTH: f64 = 200.0; 16 | 17 | #[derive(Clone, Data, Lens)] 18 | struct HelloState { 19 | name: String, 20 | } 21 | 22 | pub fn main() { 23 | // describe the main window 24 | let main_window = WindowDesc::new(build_root_widget()) 25 | .title("Hello World!") 26 | .window_size((400.0, 400.0)); 27 | 28 | // create the initial app state 29 | let initial_state: HelloState = HelloState { 30 | name: "World".into(), 31 | }; 32 | 33 | // start the application. Here we pass in the application state. 34 | AppLauncher::with_window(main_window) 35 | .log_to_console() 36 | .launch(initial_state) 37 | .expect("Failed to launch application"); 38 | } 39 | 40 | fn build_root_widget() -> impl Widget { 41 | // a label that will determine its text based on the current app data. 42 | let label = Label::new(|data: &HelloState, _env: &Env| { 43 | if data.name.is_empty() { 44 | "Hello anybody!?".to_string() 45 | } else { 46 | format!("Hello {}!", data.name) 47 | } 48 | }) 49 | .with_text_size(32.0); 50 | 51 | // a textbox that modifies `name`. 52 | let textbox = TextBox::new() 53 | .with_placeholder("Who are we greeting?") 54 | .with_text_size(18.0) 55 | .fix_width(TEXT_BOX_WIDTH) 56 | .lens(HelloState::name); 57 | 58 | // arrange the two widgets vertically, with some padding 59 | Flex::column() 60 | .with_child(label) 61 | .with_spacer(VERTICAL_WIDGET_SPACING) 62 | .with_child(textbox) 63 | .align_vertical(UnitPoint::CENTER) 64 | } 65 | -------------------------------------------------------------------------------- /druid/examples/hello_web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hello-web" 3 | version = "0.1.0" 4 | license = "Apache-2.0" 5 | description = "Minimal web example" 6 | repository = "https://github.com/linebender/druid" 7 | edition = "2021" 8 | publish = false 9 | 10 | [lib] 11 | crate-type = ["cdylib", "rlib"] 12 | 13 | [dependencies] 14 | druid = { path="../.." } 15 | 16 | wasm-bindgen = "0.2.95" 17 | console_error_panic_hook = "0.1.7" 18 | -------------------------------------------------------------------------------- /druid/examples/hello_web/README.md: -------------------------------------------------------------------------------- 1 | # Druid web hello world 2 | 3 | This is a minimal example of building a single Druid application for the web. 4 | To build all the Druid examples for the web, check out the `web` example directory. 5 | 6 | ## Building 7 | 8 | You will need `cargo` and `wasm-pack` for building the code and a simple 9 | server like [`http`](https://crates.io/crates/https) for serving the web page. 10 | 11 | First build with. 12 | 13 | ``` 14 | > wasm-pack build --target web --dev 15 | ``` 16 | 17 | This generates a JavaScript module that exports the `wasm_main` function that's 18 | been annotated with the `#[wasm_bindgen]` macro. Leave off the `--dev` flag 19 | if you're doing a release build. 20 | 21 | Now run 22 | 23 | ``` 24 | > http 25 | ``` 26 | 27 | which should start serving this directory. 28 | 29 | Finally, point your browser to the appropriate localhost url (usually http://localhost:8000) and you 30 | should see your app. 31 | 32 | When you make changes to the project, re-run `wasm-pack build --target web --dev` and you can 33 | see the changes in your browser when you refresh -- no need to restart `http`. 34 | -------------------------------------------------------------------------------- /druid/examples/hello_web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Druid web example 6 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /druid/examples/hello_web/index.js: -------------------------------------------------------------------------------- 1 | import init, { wasm_main } from "./pkg/hello_web.js"; 2 | 3 | async function run() { 4 | await init(); 5 | wasm_main(); 6 | } 7 | 8 | run(); 9 | -------------------------------------------------------------------------------- /druid/examples/hello_web/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use druid::widget::{Align, Flex, Label, TextBox}; 5 | use druid::{AppLauncher, Data, Env, Lens, Widget, WidgetExt, WindowDesc}; 6 | 7 | use wasm_bindgen::prelude::*; 8 | 9 | const VERTICAL_WIDGET_SPACING: f64 = 20.0; 10 | const TEXT_BOX_WIDTH: f64 = 200.0; 11 | 12 | #[derive(Clone, Data, Lens)] 13 | struct HelloState { 14 | name: String, 15 | } 16 | 17 | // This wrapper function is the primary modification we're making to the vanilla 18 | // hello.rs example. 19 | #[wasm_bindgen] 20 | pub fn wasm_main() { 21 | // This hook is necessary to get panic messages in the console 22 | std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 23 | main() 24 | } 25 | 26 | pub fn main() { 27 | // describe the main window 28 | // 29 | // Window title is set in index.html and window size is ignored on the web, 30 | // so can we leave those off. 31 | let main_window = WindowDesc::new(build_root_widget()); 32 | 33 | // create the initial app state 34 | let initial_state = HelloState { 35 | name: "World".into(), 36 | }; 37 | 38 | // start the application 39 | AppLauncher::with_window(main_window) 40 | .launch(initial_state) 41 | .expect("Failed to launch application"); 42 | } 43 | 44 | fn build_root_widget() -> impl Widget { 45 | // a label that will determine its text based on the current app data. 46 | let label = Label::new(|data: &HelloState, _env: &Env| format!("Hello {}!", data.name)); 47 | // a textbox that modifies `name`. 48 | let textbox = TextBox::new() 49 | .with_placeholder("Who are we greeting?") 50 | .fix_width(TEXT_BOX_WIDTH) 51 | .lens(HelloState::name); 52 | 53 | // arrange the two widgets vertically, with some padding 54 | let layout = Flex::column() 55 | .with_child(label) 56 | .with_spacer(VERTICAL_WIDGET_SPACING) 57 | .with_child(textbox); 58 | 59 | // center the two widgets in the available space 60 | Align::centered(layout) 61 | } 62 | -------------------------------------------------------------------------------- /druid/examples/layout.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! This example shows how to construct a basic layout, 5 | //! using columns, rows, and loops, for repeated Widgets. 6 | 7 | // On Windows platform, don't show a console when opening the app. 8 | #![windows_subsystem = "windows"] 9 | 10 | use druid::widget::{AspectRatioBox, Button, Flex, Label, LineBreaking}; 11 | use druid::{AppLauncher, Color, Widget, WidgetExt, WindowDesc}; 12 | 13 | fn build_app() -> impl Widget { 14 | // Usually we put all the widgets in one big tree using builder-style 15 | // methods. Sometimes we split them up in declarations to increase 16 | // readability. In this case we also have some recurring elements, 17 | // we add those in a loop later on. 18 | let mut col = Flex::column().with_child( 19 | // The `Flex`'s first child is another Flex! In this case it is 20 | // a row. 21 | Flex::row() 22 | // The row has its own children. 23 | .with_child( 24 | Label::new("One") 25 | .fix_width(60.0) 26 | .background(Color::rgb8(0x77, 0x77, 0)) 27 | .border(Color::WHITE, 3.0) 28 | .center(), 29 | ) 30 | // Spacing element that will fill all available space in 31 | // between label and a button. Notice that weight is non-zero. 32 | // We could have achieved a similar result with expanding the 33 | // width and setting the main-axis-allignment to SpaceBetween. 34 | .with_flex_spacer(1.0) 35 | .with_child(Button::new("Two").padding(20.)) 36 | // After we added all the children, we can set some more 37 | // values using builder-style methods. Since these methods 38 | // dont return the original `Flex` but a SizedBox and Container 39 | // respectively, we have to put these at the end. 40 | .fix_height(100.0) 41 | //turquoise 42 | .background(Color::rgb8(0, 0x77, 0x88)), 43 | ); 44 | 45 | for i in 0..5 { 46 | // Give a larger weight to one of the buttons for it to 47 | // occupy more space. 48 | let weight = if i == 2 { 3.0 } else { 1.0 }; 49 | // call `expand_height` to force the buttons to use all their provided flex 50 | col.add_flex_child(Button::new(format!("Button #{i}")).expand_height(), weight); 51 | } 52 | 53 | // aspect ratio box 54 | let aspect_ratio_label = Label::new("This is an aspect-ratio box. Notice how the text will overflow if the box becomes too small.") 55 | .with_text_color(Color::BLACK) 56 | .with_line_break_mode(LineBreaking::WordWrap) 57 | .center(); 58 | let aspect_ratio_box = AspectRatioBox::new(aspect_ratio_label, 4.0) 59 | .border(Color::BLACK, 1.0) 60 | .background(Color::WHITE); 61 | col.add_flex_child(aspect_ratio_box.center(), 1.0); 62 | 63 | // This method asks Druid to draw colored rectangles around our widgets, 64 | // so we can visually inspect their layout rectangles. 65 | col.debug_paint_layout() 66 | } 67 | 68 | pub fn main() { 69 | let window = WindowDesc::new(build_app()).title("Very flexible"); 70 | 71 | AppLauncher::with_window(window) 72 | .log_to_console() 73 | .launch(0) 74 | .expect("launch failed"); 75 | } 76 | -------------------------------------------------------------------------------- /druid/examples/lens.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! This example shows basic usage of Lens 5 | 6 | // On Windows platform, don't show a console when opening the app. 7 | #![windows_subsystem = "windows"] 8 | 9 | use druid::widget::Slider; 10 | use druid::widget::{CrossAxisAlignment, Flex, Label, TextBox}; 11 | use druid::{AppLauncher, Data, Env, Lens, LocalizedString, Widget, WidgetExt, WindowDesc}; 12 | 13 | pub fn main() { 14 | let main_window = WindowDesc::new(ui_builder()) 15 | .title(LocalizedString::new("lens-demo-window-title").with_placeholder("Lens Demo")); 16 | let data = MyComplexState { 17 | term: "hello".into(), 18 | scale: 0.0, 19 | }; 20 | 21 | AppLauncher::with_window(main_window) 22 | .launch(data) 23 | .expect("launch failed"); 24 | } 25 | 26 | #[derive(Clone, Debug, Data, Lens)] 27 | struct MyComplexState { 28 | #[lens(name = "term_lens")] 29 | term: String, 30 | scale: f64, 31 | } 32 | 33 | fn ui_builder() -> impl Widget { 34 | // `TextBox` is of type `Widget` 35 | // via `.lens` we get it to be of type `Widget` 36 | let searchbar = TextBox::new().lens(MyComplexState::term_lens); 37 | 38 | // `Slider` is of type `Widget` 39 | // via `.lens` we get it to be of type `Widget` 40 | let slider = Slider::new().lens(MyComplexState::scale); 41 | 42 | let label = Label::new(|d: &MyComplexState, _: &Env| format!("{}: {:.2}", d.term, d.scale)); 43 | 44 | Flex::column() 45 | .cross_axis_alignment(CrossAxisAlignment::Center) 46 | .with_child(label) 47 | .with_default_spacer() 48 | .with_child( 49 | Flex::row() 50 | .cross_axis_alignment(CrossAxisAlignment::Center) 51 | .with_child(searchbar) 52 | .with_default_spacer() 53 | .with_child(slider), 54 | ) 55 | .center() 56 | } 57 | -------------------------------------------------------------------------------- /druid/examples/open_save.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Usage of file open and saving. 5 | 6 | // On Windows platform, don't show a console when opening the app. 7 | #![windows_subsystem = "windows"] 8 | 9 | use druid::widget::{Align, Button, Flex, TextBox}; 10 | use druid::{ 11 | commands, AppDelegate, AppLauncher, Command, DelegateCtx, Env, FileDialogOptions, FileSpec, 12 | Handled, LocalizedString, Target, Widget, WindowDesc, 13 | }; 14 | 15 | struct Delegate; 16 | 17 | pub fn main() { 18 | let main_window = WindowDesc::new(ui_builder()) 19 | .title(LocalizedString::new("open-save-demo").with_placeholder("Opening/Saving Demo")); 20 | let data = "Type here.".to_owned(); 21 | AppLauncher::with_window(main_window) 22 | .delegate(Delegate) 23 | .log_to_console() 24 | .launch(data) 25 | .expect("launch failed"); 26 | } 27 | 28 | fn ui_builder() -> impl Widget { 29 | let rs = FileSpec::new("Rust source", &["rs"]); 30 | let txt = FileSpec::new("Text file", &["txt"]); 31 | let other = FileSpec::new("Bogus file", &["foo", "bar", "baz"]); 32 | // The options can also be generated at runtime, 33 | // so to show that off we create a String for the default save name. 34 | let default_save_name = String::from("MyFile.txt"); 35 | let save_dialog_options = FileDialogOptions::new() 36 | .allowed_types(vec![rs, txt, other]) 37 | .default_type(txt) 38 | .default_name(default_save_name) 39 | .name_label("Target") 40 | .title("Choose a target for this lovely file") 41 | .button_text("Export"); 42 | let open_dialog_options = save_dialog_options 43 | .clone() 44 | .default_name("MySavedFile.txt") 45 | .name_label("Source") 46 | .title("Where did you put that file?") 47 | .button_text("Import"); 48 | 49 | let input = TextBox::new(); 50 | let save = Button::new("Save").on_click(move |ctx, _, _| { 51 | ctx.submit_command(druid::commands::SHOW_SAVE_PANEL.with(save_dialog_options.clone())) 52 | }); 53 | let open = Button::new("Open").on_click(move |ctx, _, _| { 54 | ctx.submit_command(druid::commands::SHOW_OPEN_PANEL.with(open_dialog_options.clone())) 55 | }); 56 | 57 | let mut col = Flex::column(); 58 | col.add_child(input); 59 | col.add_spacer(8.0); 60 | col.add_child(save); 61 | col.add_child(open); 62 | Align::centered(col) 63 | } 64 | 65 | impl AppDelegate for Delegate { 66 | fn command( 67 | &mut self, 68 | _ctx: &mut DelegateCtx, 69 | _target: Target, 70 | cmd: &Command, 71 | data: &mut String, 72 | _env: &Env, 73 | ) -> Handled { 74 | if let Some(file_info) = cmd.get(commands::SAVE_FILE_AS) { 75 | if let Err(e) = std::fs::write(file_info.path(), &data[..]) { 76 | println!("Error writing file: {e}"); 77 | } 78 | return Handled::Yes; 79 | } 80 | if let Some(file_info) = cmd.get(commands::OPEN_FILE) { 81 | match std::fs::read_to_string(file_info.path()) { 82 | Ok(s) => { 83 | let first_line = s.lines().next().unwrap_or(""); 84 | *data = first_line.to_owned(); 85 | } 86 | Err(e) => { 87 | println!("Error opening file: {e}"); 88 | } 89 | } 90 | return Handled::Yes; 91 | } 92 | Handled::No 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /druid/examples/panels.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! This example shows how to construct a basic layout. 5 | 6 | // On Windows platform, don't show a console when opening the app. 7 | #![windows_subsystem = "windows"] 8 | 9 | use druid::kurbo::Circle; 10 | use druid::widget::{Flex, Label, Painter}; 11 | use druid::{ 12 | AppLauncher, Color, LinearGradient, LocalizedString, PlatformError, RenderContext, UnitPoint, 13 | Widget, WidgetExt, WindowDesc, 14 | }; 15 | 16 | const DARK_GREY: Color = Color::grey8(0x3a); 17 | const DARKER_GREY: Color = Color::grey8(0x11); 18 | const LIGHTER_GREY: Color = Color::grey8(0xbb); 19 | 20 | fn build_app() -> impl Widget<()> { 21 | let gradient = LinearGradient::new( 22 | UnitPoint::TOP_LEFT, 23 | UnitPoint::BOTTOM_RIGHT, 24 | (DARKER_GREY, LIGHTER_GREY), 25 | ); 26 | 27 | // a custom background 28 | let polka_dots = Painter::new(|ctx, _, _| { 29 | let bounds = ctx.size().to_rect(); 30 | let dot_diam = bounds.width().max(bounds.height()) / 20.; 31 | let dot_spacing = dot_diam * 1.8; 32 | for y in 0..((bounds.height() / dot_diam).ceil() as usize) { 33 | for x in 0..((bounds.width() / dot_diam).ceil() as usize) { 34 | let x_offset = (y % 2) as f64 * (dot_spacing / 2.0); 35 | let x = x as f64 * dot_spacing + x_offset; 36 | let y = y as f64 * dot_spacing; 37 | let circ = Circle::new((x, y), dot_diam / 2.0); 38 | let purp = Color::rgb(1.0, 0.22, 0.76); 39 | ctx.fill(circ, &purp); 40 | } 41 | } 42 | }); 43 | 44 | Flex::column() 45 | .with_flex_child( 46 | Flex::row() 47 | .with_flex_child( 48 | Label::new("top left") 49 | .center() 50 | .border(DARK_GREY, 4.0) 51 | .padding(10.0), 52 | 1.0, 53 | ) 54 | .with_flex_child( 55 | Label::new("top right") 56 | .center() 57 | .background(DARK_GREY) 58 | .padding(10.0), 59 | 1.0, 60 | ), 61 | 1.0, 62 | ) 63 | .with_flex_child( 64 | Flex::row() 65 | .with_flex_child( 66 | Label::new("bottom left") 67 | .center() 68 | .background(gradient) 69 | .rounded(10.0) 70 | .padding(10.0), 71 | 1.0, 72 | ) 73 | .with_flex_child( 74 | Label::new("bottom right") 75 | .center() 76 | .border(LIGHTER_GREY, 4.0) 77 | .background(polka_dots) 78 | .rounded(10.0) 79 | .padding(10.0), 80 | 1.0, 81 | ), 82 | 1.0, 83 | ) 84 | } 85 | 86 | pub fn main() -> Result<(), PlatformError> { 87 | let main_window = WindowDesc::new(build_app()) 88 | .title(LocalizedString::new("panels-demo-window-title").with_placeholder("Fancy Boxes!")); 89 | AppLauncher::with_window(main_window) 90 | .log_to_console() 91 | .launch(())?; 92 | 93 | Ok(()) 94 | } 95 | -------------------------------------------------------------------------------- /druid/examples/scroll.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Shows a scroll widget, and also demonstrates how widgets that paint 5 | //! outside their bounds can specify their paint region. 6 | 7 | // On Windows platform, don't show a console when opening the app. 8 | #![windows_subsystem = "windows"] 9 | 10 | use druid::kurbo::Circle; 11 | use druid::piet::RadialGradient; 12 | use druid::widget::prelude::*; 13 | use druid::widget::{Flex, Padding}; 14 | use druid::{AppLauncher, Data, Insets, LocalizedString, Rect, WidgetExt, WindowDesc}; 15 | 16 | pub fn main() { 17 | let window = WindowDesc::new(build_widget()) 18 | .title(LocalizedString::new("scroll-demo-window-title").with_placeholder("Scroll demo")); 19 | AppLauncher::with_window(window) 20 | .log_to_console() 21 | .launch(0u32) 22 | .expect("launch failed"); 23 | } 24 | 25 | fn build_widget() -> impl Widget { 26 | let mut col = Flex::column(); 27 | for i in 0..30 { 28 | col.add_child(Padding::new(3.0, OverPainter(i))); 29 | } 30 | col.scroll() 31 | } 32 | 33 | /// A widget that paints outside of its bounds. 34 | struct OverPainter(u64); 35 | 36 | const INSETS: Insets = Insets::uniform(50.); 37 | 38 | impl Widget for OverPainter { 39 | fn event(&mut self, _: &mut EventCtx, _: &Event, _: &mut T, _: &Env) {} 40 | 41 | fn lifecycle(&mut self, _: &mut LifeCycleCtx, _: &LifeCycle, _: &T, _: &Env) {} 42 | 43 | fn update(&mut self, _: &mut UpdateCtx, _: &T, _: &T, _: &Env) {} 44 | 45 | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, _: &T, _: &Env) -> Size { 46 | ctx.set_paint_insets(INSETS); 47 | bc.constrain(Size::new(100., 100.)) 48 | } 49 | 50 | fn paint(&mut self, ctx: &mut PaintCtx, _: &T, env: &Env) { 51 | let rect = Rect::ZERO.with_size(ctx.size()); 52 | let color = env.get_debug_color(self.0); 53 | let radius = (rect + INSETS).size().height / 2.0; 54 | let circle = Circle::new(rect.center(), radius); 55 | let grad = RadialGradient::new(1.0, (color, color.with_alpha(0.0))); 56 | ctx.fill(circle, &grad); 57 | ctx.stroke(rect, &color, 2.0); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /druid/examples/scroll_colors.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! This example allows to play with scroll bars over different color tones. 5 | 6 | // On Windows platform, don't show a console when opening the app. 7 | #![windows_subsystem = "windows"] 8 | 9 | use druid::widget::{Container, Flex, Scroll, SizedBox}; 10 | use druid::{AppLauncher, Color, LocalizedString, Widget, WindowDesc}; 11 | 12 | fn build_app() -> impl Widget { 13 | let mut col = Flex::column(); 14 | let rows = 30; 15 | let cols = 30; 16 | 17 | for i in 0..cols { 18 | let mut row = Flex::row(); 19 | let col_progress = i as f64 / cols as f64; 20 | 21 | for j in 0..rows { 22 | let row_progress = j as f64 / rows as f64; 23 | 24 | row.add_child( 25 | Container::new(SizedBox::empty().width(200.0).height(200.0)) 26 | .background(Color::rgb(1.0 * col_progress, 1.0 * row_progress, 1.0)), 27 | ); 28 | } 29 | 30 | col.add_child(row); 31 | } 32 | 33 | Scroll::new(col) 34 | } 35 | 36 | pub fn main() { 37 | let main_window = WindowDesc::new(build_app()).title( 38 | LocalizedString::new("scroll-colors-demo-window-title").with_placeholder("Rainbows!"), 39 | ); 40 | let data = 0_u32; 41 | AppLauncher::with_window(main_window) 42 | .log_to_console() 43 | .launch(data) 44 | .expect("launch failed"); 45 | } 46 | -------------------------------------------------------------------------------- /druid/examples/slider.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! This is a demo of the settings of Slider, RangeSlider and Annotated. 5 | //! It contains a `Slider` and `RangeSlider`. 6 | //! Every time the `RangeSlider` is moved the range of the `Slider` is updated. 7 | 8 | // On Windows platform, don't show a console when opening the app. 9 | #![windows_subsystem = "windows"] 10 | 11 | use druid::widget::prelude::*; 12 | use druid::widget::{ 13 | Axis, CrossAxisAlignment, Flex, KnobStyle, Label, RangeSlider, Slider, ViewSwitcher, 14 | }; 15 | use druid::{AppLauncher, Color, Data, KeyOrValue, Lens, UnitPoint, WidgetExt, WindowDesc}; 16 | 17 | const VERTICAL_WIDGET_SPACING: f64 = 20.0; 18 | 19 | #[derive(Clone, Data, Lens)] 20 | struct AppState { 21 | range: (f64, f64), 22 | value: f64, 23 | } 24 | 25 | pub fn main() { 26 | // describe the main window 27 | let main_window = WindowDesc::new(build_root_widget()) 28 | .title("Slider Demo!") 29 | .window_size((400.0, 400.0)); 30 | 31 | // create the initial app state 32 | let initial_state: AppState = AppState { 33 | range: (2.0, 8.0), 34 | value: 5.0, 35 | }; 36 | 37 | // start the application. Here we pass in the application state. 38 | AppLauncher::with_window(main_window) 39 | .log_to_console() 40 | .launch(initial_state) 41 | .expect("Failed to launch application"); 42 | } 43 | 44 | fn build_root_widget() -> impl Widget { 45 | let range = Flex::row() 46 | .with_child(Label::dynamic(|value: &(f64, f64), _| { 47 | format!("Value Range: {value:?}") 48 | })) 49 | .with_default_spacer() 50 | .with_child( 51 | RangeSlider::new() 52 | .with_range(0.0, 20.0) 53 | .with_step(1.0) 54 | .track_color(KeyOrValue::Concrete(Color::RED)) 55 | .fix_width(250.0), 56 | ) 57 | .lens(AppState::range); 58 | 59 | let value = Flex::row() 60 | .with_child(Label::dynamic(|value: &AppState, _| { 61 | format!("Value: {:?}", value.value) 62 | })) 63 | .with_default_spacer() 64 | .with_child(ViewSwitcher::new( 65 | |data: &AppState, _| data.range, 66 | |range, _, _| { 67 | Slider::new() 68 | .with_range(range.0, range.1) 69 | .track_color(KeyOrValue::Concrete(Color::RED)) 70 | .knob_style(KnobStyle::Wedge) 71 | .axis(Axis::Vertical) 72 | .with_step(0.25) 73 | .annotated(1.0, 0.25) 74 | .fix_height(250.0) 75 | .lens(AppState::value) 76 | .boxed() 77 | }, 78 | )); 79 | 80 | // arrange the two widgets vertically, with some padding 81 | Flex::column() 82 | .with_child(range) 83 | .with_spacer(VERTICAL_WIDGET_SPACING) 84 | .with_child(value) 85 | .cross_axis_alignment(CrossAxisAlignment::End) 86 | .align_vertical(UnitPoint::RIGHT) 87 | .padding(20.0) 88 | } 89 | -------------------------------------------------------------------------------- /druid/examples/split_demo.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! This example demonstrates the `Split` widget 5 | 6 | // On Windows platform, don't show a console when opening the app. 7 | #![windows_subsystem = "windows"] 8 | 9 | use druid::piet::Color; 10 | use druid::widget::{Align, Container, Label, Padding, Split}; 11 | use druid::{AppLauncher, LocalizedString, Widget, WindowDesc}; 12 | 13 | fn build_app() -> impl Widget { 14 | let fixed_cols = Padding::new( 15 | 10.0, 16 | Container::new( 17 | Split::columns( 18 | Align::centered(Label::new("Left Split")), 19 | Align::centered(Label::new("Right Split")), 20 | ) 21 | .split_point(0.5), 22 | ) 23 | .border(Color::WHITE, 1.0), 24 | ); 25 | let fixed_rows = Padding::new( 26 | 10.0, 27 | Container::new( 28 | Split::rows( 29 | Align::centered(Label::new("Top Split")), 30 | Align::centered(Label::new("Bottom Split")), 31 | ) 32 | .split_point(0.4) 33 | .bar_size(3.0), 34 | ) 35 | .border(Color::WHITE, 1.0), 36 | ); 37 | let draggable_cols = Padding::new( 38 | 10.0, 39 | Container::new( 40 | Split::columns( 41 | Align::centered(Label::new("Split A")), 42 | Align::centered(Label::new("Split B")), 43 | ) 44 | .split_point(0.5) 45 | .draggable(true) 46 | .solid_bar(true) 47 | .min_size(60.0, 60.0), 48 | ) 49 | .border(Color::WHITE, 1.0), 50 | ); 51 | Padding::new( 52 | 10.0, 53 | Container::new( 54 | Split::rows( 55 | Split::rows(fixed_cols, fixed_rows) 56 | .split_point(0.33) 57 | .bar_size(3.0) 58 | .min_bar_area(3.0) 59 | .draggable(true), 60 | draggable_cols, 61 | ) 62 | .split_point(0.75) 63 | .bar_size(5.0) 64 | .min_bar_area(11.0) 65 | .draggable(true), 66 | ) 67 | .border(Color::WHITE, 1.0), 68 | ) 69 | } 70 | 71 | pub fn main() { 72 | let window = WindowDesc::new(build_app()) 73 | .title(LocalizedString::new("split-demo-window-title").with_placeholder("Split Demo")); 74 | AppLauncher::with_window(window) 75 | .log_to_console() 76 | .launch(0u32) 77 | .expect("launch failed"); 78 | } 79 | -------------------------------------------------------------------------------- /druid/examples/svg.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! This example shows how to draw an SVG. 5 | 6 | // On Windows platform, don't show a console when opening the app. 7 | #![windows_subsystem = "windows"] 8 | 9 | use tracing::error; 10 | 11 | use druid::{ 12 | widget::{Flex, Svg, SvgData, WidgetExt}, 13 | AppLauncher, LocalizedString, Widget, WindowDesc, 14 | }; 15 | 16 | pub fn main() { 17 | let main_window = WindowDesc::new(ui_builder()) 18 | .title(LocalizedString::new("svg-demo-window-title").with_placeholder("Rawr!")); 19 | let data = 0_u32; 20 | AppLauncher::with_window(main_window) 21 | .log_to_console() 22 | .launch(data) 23 | .expect("launch failed"); 24 | } 25 | 26 | fn ui_builder() -> impl Widget { 27 | let tiger_svg = match include_str!("./assets/tiger.svg").parse::() { 28 | Ok(svg) => svg, 29 | Err(err) => { 30 | error!("{}", err); 31 | error!("Using an empty SVG instead."); 32 | SvgData::default() 33 | } 34 | }; 35 | 36 | let mut col = Flex::column(); 37 | 38 | col.add_flex_child(Svg::new(tiger_svg.clone()).fix_width(60.0).center(), 1.0); 39 | col.add_flex_child(Svg::new(tiger_svg.clone()), 1.0); 40 | col.add_flex_child(Svg::new(tiger_svg), 1.0); 41 | col.debug_paint_layout() 42 | } 43 | -------------------------------------------------------------------------------- /druid/examples/switches.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Example of switches 5 | 6 | // On Windows platform, don't show a console when opening the app. 7 | #![windows_subsystem = "windows"] 8 | 9 | #[allow(deprecated)] 10 | use druid::widget::Parse; 11 | use druid::widget::{ 12 | Checkbox, Flex, Label, LensWrap, MainAxisAlignment, Padding, Stepper, Switch, TextBox, 13 | WidgetExt, 14 | }; 15 | use druid::{AppLauncher, Data, Lens, LensExt, LocalizedString, Widget, WindowDesc}; 16 | 17 | #[derive(Clone, Data, Lens)] 18 | struct DemoState { 19 | value: bool, 20 | stepper_value: f64, 21 | } 22 | 23 | fn build_widget() -> impl Widget { 24 | let mut col = Flex::column(); 25 | let mut row = Flex::row(); 26 | let switch = LensWrap::new(Switch::new(), DemoState::value); 27 | let check_box = LensWrap::new(Checkbox::new(""), DemoState::value); 28 | let switch_label = Label::new("Setting label"); 29 | 30 | row.add_child(Padding::new(5.0, switch_label)); 31 | row.add_child(Padding::new(5.0, switch)); 32 | row.add_child(Padding::new(5.0, check_box)); 33 | 34 | let stepper = LensWrap::new( 35 | Stepper::new() 36 | .with_range(0.0, 10.0) 37 | .with_step(0.5) 38 | .with_wraparound(false), 39 | DemoState::stepper_value, 40 | ); 41 | 42 | let mut textbox_row = Flex::row(); 43 | // TODO: Replace Parse usage with TextBox::with_formatter 44 | #[allow(deprecated)] 45 | let textbox = LensWrap::new( 46 | Parse::new(TextBox::new()), 47 | DemoState::stepper_value.map(|x| Some(*x), |x, y| *x = y.unwrap_or(0.0)), 48 | ); 49 | textbox_row.add_child(Padding::new(5.0, textbox)); 50 | textbox_row.add_child(Padding::new(5.0, stepper.center())); 51 | 52 | let mut label_row = Flex::row(); 53 | 54 | let label = Label::new(|data: &DemoState, _env: &_| { 55 | format!("Stepper value: {0:.2}", data.stepper_value) 56 | }); 57 | 58 | label_row.add_child(Padding::new(5.0, label)); 59 | 60 | col.set_main_axis_alignment(MainAxisAlignment::Center); 61 | col.add_child(Padding::new(5.0, row)); 62 | col.add_child(Padding::new(5.0, textbox_row)); 63 | col.add_child(Padding::new(5.0, label_row)); 64 | col.center() 65 | } 66 | 67 | pub fn main() { 68 | let window = WindowDesc::new(build_widget()) 69 | .title(LocalizedString::new("switch-demo-window-title").with_placeholder("Switch Demo")); 70 | AppLauncher::with_window(window) 71 | .log_to_console() 72 | .launch(DemoState { 73 | value: true, 74 | stepper_value: 1.0, 75 | }) 76 | .expect("launch failed"); 77 | } 78 | -------------------------------------------------------------------------------- /druid/examples/transparency.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! An example of a transparent window background. 5 | //! Useful for dropdowns, tooltips and other overlay windows. 6 | 7 | // On Windows platform, don't show a console when opening the app. 8 | #![windows_subsystem = "windows"] 9 | 10 | use druid::widget::prelude::*; 11 | use druid::widget::{Flex, Label, Painter, TextBox, WidgetExt}; 12 | use druid::{kurbo::Circle, widget::Controller}; 13 | use druid::{AppLauncher, Color, Lens, Rect, WindowDesc}; 14 | 15 | #[derive(Clone, Data, Lens)] 16 | struct HelloState { 17 | name: String, 18 | } 19 | 20 | struct DragController; 21 | 22 | impl> Controller for DragController { 23 | fn event( 24 | &mut self, 25 | _child: &mut W, 26 | ctx: &mut EventCtx, 27 | event: &Event, 28 | _data: &mut T, 29 | _env: &Env, 30 | ) { 31 | if let Event::MouseMove(_) = event { 32 | ctx.window().handle_titlebar(true); 33 | } 34 | } 35 | } 36 | 37 | pub fn main() { 38 | let window = WindowDesc::new(build_root_widget()) 39 | .show_titlebar(false) 40 | .window_size((512., 512.)) 41 | .transparent(true) 42 | .resizable(true) 43 | .title("Transparent background"); 44 | 45 | AppLauncher::with_window(window) 46 | .log_to_console() 47 | .launch(HelloState { name: "".into() }) 48 | .expect("launch failed"); 49 | } 50 | 51 | fn build_root_widget() -> impl Widget { 52 | // Draw red circle, and two semi-transparent rectangles 53 | let circle_and_rects = Painter::new(|ctx, _data, _env| { 54 | let boundaries = ctx.size().to_rect(); 55 | let center = (boundaries.width() / 2., boundaries.height() / 2.); 56 | let circle = Circle::new(center, center.0.min(center.1)); 57 | ctx.fill(circle, &Color::RED); 58 | 59 | let rect1 = Rect::new(0., 0., boundaries.width() / 2., boundaries.height() / 2.); 60 | ctx.fill(rect1, &Color::rgba8(0x0, 0xff, 0, 125)); 61 | 62 | let rect2 = Rect::new( 63 | boundaries.width() / 2., 64 | boundaries.height() / 2., 65 | boundaries.width(), 66 | boundaries.height(), 67 | ); 68 | ctx.fill(rect2, &Color::rgba8(0x0, 0x0, 0xff, 125)); 69 | }); 70 | 71 | // This textbox modifies the label, idea here is to test that the background 72 | // invalidation works when you type to the textbox 73 | let textbox = TextBox::new() 74 | .with_placeholder("Type to test clearing") 75 | .with_text_size(18.0) 76 | .lens(HelloState::name) 77 | .fix_width(250.); 78 | 79 | let label = Label::new(|data: &HelloState, _env: &Env| { 80 | if data.name.is_empty() { 81 | "Text: ".to_string() 82 | } else { 83 | format!("Text: {}!", data.name) 84 | } 85 | }) 86 | .with_text_color(Color::RED) 87 | .with_text_size(32.0); 88 | 89 | Flex::column() 90 | .with_flex_child(circle_and_rects.expand().controller(DragController), 10.0) 91 | .with_spacer(4.0) 92 | .with_child(textbox) 93 | .with_spacer(4.0) 94 | .with_child(label) 95 | } 96 | -------------------------------------------------------------------------------- /druid/examples/value_formatting/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "value-formatting" 3 | version = "0.1.0" 4 | license = "Apache-2.0" 5 | repository = "https://github.com/linebender/druid" 6 | authors = ["Colin Rofls "] 7 | edition = "2021" 8 | publish = false 9 | 10 | [dependencies] 11 | druid = { path = "../../" } 12 | -------------------------------------------------------------------------------- /druid/examples/value_formatting/README.md: -------------------------------------------------------------------------------- 1 | # Validation 2 | 3 | This example demonstrates how to add a formatter to a textbox in order to ensure 4 | that the textbox contains valid data. 5 | 6 | The example is reasonably complex, for a number of reasons: firstly there are 7 | currently no built-in implementations of the `Formatter` trait included in 8 | Druid, which means we have to write versions for the example, and additionally 9 | the mechanism for reporting errors that occur during formatting are difficult to 10 | handle in Druid's current architecture, and involve some fairly ugly use of 11 | `Command` in order to function. 12 | -------------------------------------------------------------------------------- /druid/examples/view_switcher.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! This example demonstrates the `ViewSwitcher` widget 5 | 6 | // On Windows platform, don't show a console when opening the app. 7 | #![windows_subsystem = "windows"] 8 | 9 | use druid::widget::{Button, Flex, Label, Split, TextBox, ViewSwitcher}; 10 | use druid::{AppLauncher, Data, Env, Lens, LocalizedString, Widget, WidgetExt, WindowDesc}; 11 | 12 | #[derive(Clone, Data, Lens)] 13 | struct AppState { 14 | current_view: u32, 15 | current_text: String, 16 | } 17 | 18 | pub fn main() { 19 | let main_window = WindowDesc::new(make_ui()).title(LocalizedString::new("View Switcher")); 20 | let data = AppState { 21 | current_view: 0, 22 | current_text: "Edit me!".to_string(), 23 | }; 24 | AppLauncher::with_window(main_window) 25 | .log_to_console() 26 | .launch(data) 27 | .expect("launch failed"); 28 | } 29 | 30 | fn make_ui() -> impl Widget { 31 | let mut switcher_column = Flex::column(); 32 | switcher_column.add_child( 33 | Label::new(|data: &u32, _env: &Env| format!("Current view: {data}")) 34 | .lens(AppState::current_view), 35 | ); 36 | for i in 0..6 { 37 | switcher_column.add_spacer(80.); 38 | switcher_column.add_child( 39 | Button::new(format!("View {i}")) 40 | .on_click(move |_event, data: &mut u32, _env| { 41 | *data = i; 42 | }) 43 | .lens(AppState::current_view), 44 | ); 45 | } 46 | 47 | let view_switcher = ViewSwitcher::new( 48 | |data: &AppState, _env| data.current_view, 49 | |selector, _data, _env| match selector { 50 | 0 => Box::new(Label::new("Simple Label").center()), 51 | 1 => Box::new( 52 | Button::new("Simple Button").on_click(|_event, _data, _env| { 53 | println!("Simple button clicked!"); 54 | }), 55 | ), 56 | 2 => Box::new( 57 | Button::new("Another Simple Button").on_click(|_event, _data, _env| { 58 | println!("Another simple button clicked!"); 59 | }), 60 | ), 61 | 3 => Box::new( 62 | Flex::column() 63 | .with_flex_child(Label::new("Here is a label").center(), 1.0) 64 | .with_flex_child( 65 | Button::new("Button").on_click(|_event, _data, _env| { 66 | println!("Complex button clicked!"); 67 | }), 68 | 1.0, 69 | ) 70 | .with_flex_child(TextBox::new().lens(AppState::current_text), 1.0) 71 | .with_flex_child( 72 | Label::new(|data: &String, _env: &Env| format!("Value entered: {data}")) 73 | .lens(AppState::current_text), 74 | 1.0, 75 | ), 76 | ), 77 | 4 => Box::new( 78 | Split::columns( 79 | Label::new("Left split").center(), 80 | Label::new("Right split").center(), 81 | ) 82 | .draggable(true), 83 | ), 84 | _ => Box::new(Label::new("Unknown").center()), 85 | }, 86 | ); 87 | 88 | Flex::row() 89 | .with_child(switcher_column) 90 | .with_flex_child(view_switcher, 1.0) 91 | } 92 | -------------------------------------------------------------------------------- /druid/examples/web/.gitignore: -------------------------------------------------------------------------------- 1 | src/examples.in 2 | src/examples 3 | index.html 4 | html 5 | -------------------------------------------------------------------------------- /druid/examples/web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "druid-web-examples" 3 | version = "0.1.0" 4 | license = "Apache-2.0" 5 | description = "Scaffolding for Druid web examples" 6 | repository = "https://github.com/linebender/druid" 7 | edition = "2021" 8 | publish = false 9 | 10 | [lib] 11 | crate-type = ["cdylib", "rlib"] 12 | 13 | [dependencies] 14 | druid = { path="../..", features = ["im", "image", "png"] } 15 | tracing = "0.1.40" 16 | wasm-bindgen = "0.2.95" 17 | console_error_panic_hook = "0.1.7" 18 | log = "0.4.22" 19 | instant = { version = "0.1.13", features = ["wasm-bindgen"] } 20 | 21 | [target.'cfg(not(target_arch="wasm32"))'.dependencies] 22 | simple_logger = { version = "1.16.0", default-features = false } 23 | -------------------------------------------------------------------------------- /druid/examples/web/README.md: -------------------------------------------------------------------------------- 1 | # Druid web examples 2 | 3 | This crate generates and builds all the necessary files for deploying `druid` examples to the web. 4 | 5 | ## Building 6 | 7 | You will need `cargo` and `wasm-pack` for building the code and a simple 8 | server like [`http`](https://crates.io/crates/https) for serving the web pages. 9 | 10 | First build with 11 | 12 | ``` 13 | > wasm-pack build --target web --dev 14 | ``` 15 | 16 | This step has two main functions: 17 | 18 | 1. It generates an HTML document for each of the `druid` examples with a script that 19 | calls the appropriate function in the JavaScript module exposing the raw Wasm. 20 | 2. It builds the Wasm binary which exposes all functions annotated with `#[wasm_bindgen]`. 21 | 3. It builds the JavaScript module that loads the Wasm binary and binds all the exposed 22 | functions to JavaScript functions so they can be called directly from JavaScript. 23 | 24 | To preview the build in a web browser, run 25 | 26 | ``` 27 | > http 28 | ``` 29 | 30 | which should start serving the crate root folder containing `index.html`. 31 | 32 | Finally, point your browser to the appropriate localhost url (usually http://localhost:8000) and you 33 | should see a list of HTML documents -- one for each example. 34 | 35 | When you make changes to the project, re-run `wasm-pack build --target web --dev` and you can 36 | see the changes in your browser when you refresh -- no need to restart `http`. 37 | 38 | ## New Examples 39 | 40 | New examples that can be built against the web target should have an associated 41 | `impl_example!()` entry added to `lib.rs`. Examples that don't 42 | support the web target should be specified in the `EXCEPTIONS` list defined 43 | at the top of the `build.rs` script. 44 | -------------------------------------------------------------------------------- /druid/examples/web/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use wasm_bindgen::prelude::*; 5 | 6 | // This line includes an automatically generated (in build.rs) examples module. 7 | // This particular mechanism is chosen to avoid any kinds of modifications to committed files at 8 | // build time, keeping the source tree clean from build artifacts. 9 | include!("examples.in"); 10 | 11 | // This macro constructs a `wasm_bindgen` entry point to the given example from the examples 12 | // directory. 13 | // 14 | // There are three ways to call this macro: 15 | // 16 | // 1. impl_example!(); 17 | // Creates the binding for an example whose main fn returns nothing (or unit). 18 | // 19 | // 2. impl_example!(.unwrap()); 20 | // Creates the binding for an example whose main fn returns a Result which is immediately 21 | // unwrapped. 22 | // 23 | // 3. impl_example!(, ()); 24 | // Creates a wasm binding named , which calls into the Rust example fn given by 25 | // . This can be used to make a different wasm binding name than the 26 | // name of the original example itself (e.g. it was used for the `switch` example to avoid name 27 | // collisions with the JavaScript `switch` statement). 28 | macro_rules! impl_example { 29 | ($wasm_fn:ident, $expr:expr) => { 30 | #[wasm_bindgen] 31 | pub fn $wasm_fn() { 32 | std::panic::set_hook(Box::new(console_error_panic_hook::hook)); 33 | $expr; 34 | } 35 | }; 36 | ($fn:ident) => { 37 | impl_example!($fn, examples::$fn::main()); 38 | }; 39 | ($fn:ident.unwrap()) => { 40 | impl_example!($fn, examples::$fn::main().unwrap()); 41 | }; 42 | } 43 | 44 | // Below is a list of examples that can be built for the web. 45 | // Please add the examples that cannot be built to the EXCEPTIONS list in build.rs. 46 | impl_example!(anim); 47 | impl_example!(calc); 48 | impl_example!(cursor); 49 | impl_example!(custom_widget); 50 | impl_example!(disabled); 51 | impl_example!(either); 52 | impl_example!(event_viewer); 53 | impl_example!(flex); 54 | impl_example!(game_of_life); 55 | impl_example!(hello); 56 | impl_example!(identity); 57 | impl_example!(image); 58 | impl_example!(invalidation); 59 | impl_example!(layout); 60 | impl_example!(lens); 61 | impl_example!(list); 62 | impl_example!(multiwin); 63 | impl_example!(open_save); 64 | impl_example!(panels.unwrap()); 65 | impl_example!(scroll_colors); 66 | impl_example!(scroll); 67 | impl_example!(slider); 68 | impl_example!(split_demo); 69 | impl_example!(styled_text.unwrap()); 70 | impl_example!(switches); 71 | impl_example!(timer); 72 | impl_example!(tabs); 73 | impl_example!(textbox); 74 | impl_example!(transparency); 75 | impl_example!(view_switcher); 76 | impl_example!(widget_gallery); 77 | impl_example!(text); 78 | -------------------------------------------------------------------------------- /druid/examples/z_stack.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! A simple test of overlapping widgets. 5 | 6 | // On Windows platform, don't show a console when opening the app. 7 | #![windows_subsystem = "windows"] 8 | 9 | use druid::widget::prelude::*; 10 | use druid::widget::{Button, Label, ZStack}; 11 | use druid::{AppLauncher, Data, Lens, UnitPoint, Vec2, WindowDesc}; 12 | 13 | #[derive(Clone, Data, Lens)] 14 | struct State { 15 | counter: usize, 16 | } 17 | 18 | pub fn main() { 19 | // describe the main window 20 | let main_window = WindowDesc::new(build_root_widget()) 21 | .title("Hello World!") 22 | .window_size((400.0, 400.0)); 23 | 24 | // create the initial app state 25 | let initial_state: State = State { counter: 0 }; 26 | 27 | // start the application. Here we pass in the application state. 28 | AppLauncher::with_window(main_window) 29 | .log_to_console() 30 | .launch(initial_state) 31 | .expect("Failed to launch application"); 32 | } 33 | 34 | fn build_root_widget() -> impl Widget { 35 | ZStack::new( 36 | Button::from_label(Label::dynamic(|state: &State, _| { 37 | format!( 38 | "Very large button with text! Count up (currently {})", 39 | state.counter 40 | ) 41 | })) 42 | .on_click(|_, state: &mut State, _| state.counter += 1), 43 | ) 44 | .with_child( 45 | Button::new("Reset").on_click(|_, state: &mut State, _| state.counter = 0), 46 | Vec2::new(1.0, 1.0), 47 | Vec2::ZERO, 48 | UnitPoint::LEFT, 49 | Vec2::new(10.0, 0.0), 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /druid/resources/i18n/de-DE/builtin.ftl: -------------------------------------------------------------------------------- 1 | -app-name = Druid 2 | 3 | hello-counter = Der aktuelle Wert ist { $count } 4 | 5 | # The 'application' menu on macOS 6 | macos-menu-about-app = Über { -app-name } 7 | macos-menu-preferences = Einstellungen... 8 | macos-menu-hide-app = { -app-name } ausblenden 9 | macos-menu-hide-others = Andere ausblenden 10 | macos-menu-show-all = Alle einblenden 11 | macos-menu-services = Dienste 12 | macos-menu-application-menu = { -app-name } 13 | macos-menu-quit-app = { -app-name } beenden 14 | 15 | # common 'File' menu items 16 | common-menu-file-menu = Datei 17 | common-menu-file-new = Neu 18 | common-menu-file-new-window = Neues Fenster 19 | 20 | common-menu-file-open = Öffnen... 21 | common-menu-file-close = Schließen 22 | 23 | common-menu-file-save = Speichern 24 | # used for new files, if we need to show a dialog 25 | common-menu-file-save-ellipsis = Speichern... 26 | common-menu-file-save-as = Speichern als... 27 | 28 | common-menu-file-page-setup = Seiteneinstellungen... 29 | common-menu-file-print = Drucken... 30 | 31 | # windows 'File' menu items 32 | win-menu-file-exit = Beenden 33 | 34 | # common 'Edit' menu items. 35 | common-menu-edit-menu = Bearbeiten 36 | 37 | common-menu-cut = Ausschneiden 38 | common-menu-copy = Kopieren 39 | common-menu-paste = Einfügen 40 | common-menu-undo = Rückgängig 41 | common-menu-redo = Wiederherstellen 42 | -------------------------------------------------------------------------------- /druid/resources/i18n/en-US/builtin.ftl: -------------------------------------------------------------------------------- 1 | -app-name = Druid 2 | 3 | hello-counter = Current value is { $count } 4 | 5 | # The 'application' menu on macOS 6 | macos-menu-about-app = About { -app-name } 7 | macos-menu-preferences = Preferences... 8 | macos-menu-hide-app = Hide { -app-name } 9 | macos-menu-hide-others = Hide Others 10 | macos-menu-show-all = Show All 11 | macos-menu-services = Services 12 | macos-menu-application-menu = { -app-name } 13 | macos-menu-quit-app = Quit { -app-name } 14 | 15 | # common 'File' menu items 16 | common-menu-file-menu = File 17 | common-menu-file-new = New 18 | common-menu-file-new-window = New Window 19 | 20 | common-menu-file-open = Open... 21 | common-menu-file-close = Close 22 | 23 | common-menu-file-save = Save 24 | # used for new files, if we need to show a dialog 25 | common-menu-file-save-ellipsis = Save... 26 | common-menu-file-save-as = Save As... 27 | 28 | common-menu-file-page-setup = Page Setup... 29 | common-menu-file-print = Print... 30 | 31 | # windows 'File' menu items 32 | win-menu-file-exit = Exit 33 | 34 | # common 'Edit' menu items. 35 | common-menu-edit-menu = Edit 36 | 37 | common-menu-cut = Cut 38 | common-menu-copy = Copy 39 | common-menu-paste = Paste 40 | common-menu-undo = Undo 41 | common-menu-redo = Redo 42 | -------------------------------------------------------------------------------- /druid/resources/i18n/fr-CA/builtin.ftl: -------------------------------------------------------------------------------- 1 | -app-name = Druide 2 | 3 | hello-counter = La valeur actuelle est { $count } 4 | 5 | # The 'application' menu on macOS 6 | macos-menu-about-app = Àpropos du { -app-name } 7 | macos-menu-preferences = Préférences... 8 | macos-menu-hide-app = Masquer { -app-name } 9 | macos-menu-hide-others = Masquer les autres 10 | macos-menu-show-all = Tout afficher 11 | macos-menu-services = Services 12 | macos-menu-application-menu = { -app-name } 13 | macos-menu-quit-app = Quitter { -app-name } 14 | 15 | # common 'file' menu items 16 | common-menu-file-menu = Ficher 17 | common-menu-file-new = Nouveau 18 | common-menu-file-new-window = Nouvelle fenêtre 19 | 20 | common-menu-file-open = Ouvrir... 21 | common-menu-file-close = Fermer 22 | 23 | common-menu-file-save = Enregistrer 24 | # used for new files, if we need to show a dialog 25 | common-menu-file-save-ellipsis = Enregistrer... 26 | common-menu-file-save-as = Enregistrer sous... 27 | 28 | common-menu-file-page-setup = Format d'impression... 29 | common-menu-file-print = Imprimer... 30 | 31 | # windows 'File' menu items 32 | win-menu-file-exit = Quitter 33 | 34 | # common 'Edit' menu items. 35 | common-menu-edit-menu = Édition 36 | 37 | common-menu-cut = Couper 38 | common-menu-copy = Copier 39 | common-menu-paste = Coller 40 | common-menu-undo = Annuler 41 | common-menu-redo = Rétablir 42 | -------------------------------------------------------------------------------- /druid/src/debug_state.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! A data structure for representing widget trees. 5 | 6 | use std::collections::HashMap; 7 | 8 | /// A description widget and its children, clonable and comparable, meant 9 | /// for testing and debugging. This is extremely not optimized. 10 | #[derive(Default, Clone, PartialEq, Eq)] 11 | pub struct DebugState { 12 | /// The widget's type as a human-readable string. 13 | pub display_name: String, 14 | /// If a widget has a "central" value (for instance, a textbox's contents), 15 | /// it is stored here. 16 | pub main_value: String, 17 | /// Untyped values that reveal useful information about the widget. 18 | pub other_values: HashMap, 19 | /// Debug info of child widgets. 20 | pub children: Vec, 21 | } 22 | 23 | impl std::fmt::Debug for DebugState { 24 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 25 | if self.other_values.is_empty() && self.children.is_empty() && self.main_value.is_empty() { 26 | f.write_str(&self.display_name) 27 | } else if self.other_values.is_empty() && self.children.is_empty() { 28 | f.debug_tuple(&self.display_name) 29 | .field(&self.main_value) 30 | .finish() 31 | } else if self.other_values.is_empty() && self.main_value.is_empty() { 32 | let mut f_tuple = f.debug_tuple(&self.display_name); 33 | for child in &self.children { 34 | f_tuple.field(child); 35 | } 36 | f_tuple.finish() 37 | } else { 38 | let mut f_struct = f.debug_struct(&self.display_name); 39 | if !self.main_value.is_empty() { 40 | f_struct.field("_main_value_", &self.main_value); 41 | } 42 | for (key, value) in self.other_values.iter() { 43 | f_struct.field(key, &value); 44 | } 45 | if !self.children.is_empty() { 46 | f_struct.field("children", &self.children); 47 | } 48 | f_struct.finish() 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /druid/src/lens/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Implementations of [`Lens`], a way of focusing on subfields of data. 5 | //! 6 | //! Lenses are useful whenever a widget only needs access to a subfield of a larger struct or 7 | //! generally access to part of a larger value. 8 | //! 9 | //! For example: If one wants to embed a [`TextBox`](crate::widget::TextBox) in a widget with 10 | //! a `Data` type that is not `String`, they need to specify how to access a `String` from 11 | //! within the `Data`. 12 | //! 13 | //! ``` 14 | //! use druid::{Data, Lens, Widget, WidgetExt, widget::{TextBox, Flex}}; 15 | //! 16 | //! #[derive(Clone, Debug, Data, Lens)] 17 | //! struct MyState { 18 | //! search_term: String, 19 | //! scale: f64, 20 | //! // ... 21 | //! } 22 | //! 23 | //! 24 | //! fn my_sidebar() -> impl Widget { 25 | //! // `TextBox` is of type `Widget` 26 | //! // via `.lens` we get it to be of type `Widget`. 27 | //! // `MyState::search_term` is a lens generated by the `derive(Lens)` macro, 28 | //! // that provides access to the search_term field. 29 | //! let searchbar = TextBox::new().lens(MyState::search_term); 30 | //! 31 | //! // ... 32 | //! 33 | //! // We can now use `searchbar` just like any other `Widget` 34 | //! Flex::column().with_child(searchbar) 35 | //! } 36 | //! ``` 37 | //! 38 | //! Most of the time, if you want to create your own lenses, you need to use 39 | //! [`#[derive(Lens)]`](druid_derive::Lens). 40 | 41 | #[allow(clippy::module_inception)] 42 | #[macro_use] 43 | mod lens; 44 | pub use lens::{ 45 | Constant, Deref, Field, Identity, InArc, Index, Lens, LensExt, Map, Ref, Then, Unit, 46 | }; 47 | -------------------------------------------------------------------------------- /druid/src/text/font_descriptor.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Font attributes 5 | 6 | use crate::{Data, FontFamily, FontStyle, FontWeight}; 7 | 8 | /// A collection of attributes that describe a font. 9 | /// 10 | /// This is provided as a convenience; library consumers may wish to have 11 | /// a single type that represents a specific font face at a specific size. 12 | #[derive(Debug, Data, Clone, PartialEq)] 13 | pub struct FontDescriptor { 14 | /// The font's [`FontFamily`]. 15 | pub family: FontFamily, 16 | /// The font's size. 17 | pub size: f64, 18 | /// The font's [`FontWeight`]. 19 | pub weight: FontWeight, 20 | /// The font's [`FontStyle`]. 21 | pub style: FontStyle, 22 | } 23 | 24 | impl FontDescriptor { 25 | /// Create a new descriptor with the provided [`FontFamily`]. 26 | pub const fn new(family: FontFamily) -> Self { 27 | FontDescriptor { 28 | family, 29 | size: crate::piet::util::DEFAULT_FONT_SIZE, 30 | weight: FontWeight::REGULAR, 31 | style: FontStyle::Regular, 32 | } 33 | } 34 | 35 | /// Buider-style method to set the descriptor's font size. 36 | pub const fn with_size(mut self, size: f64) -> Self { 37 | self.size = size; 38 | self 39 | } 40 | 41 | /// Buider-style method to set the descriptor's [`FontWeight`]. 42 | pub const fn with_weight(mut self, weight: FontWeight) -> Self { 43 | self.weight = weight; 44 | self 45 | } 46 | 47 | /// Buider-style method to set the descriptor's [`FontStyle`]. 48 | pub const fn with_style(mut self, style: FontStyle) -> Self { 49 | self.style = style; 50 | self 51 | } 52 | } 53 | 54 | impl Default for FontDescriptor { 55 | fn default() -> Self { 56 | FontDescriptor { 57 | family: Default::default(), 58 | weight: Default::default(), 59 | style: Default::default(), 60 | size: crate::piet::util::DEFAULT_FONT_SIZE, 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /druid/src/text/input_methods.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Types related to input method editing. 5 | //! 6 | //! Most IME-related code is in `druid-shell`; these are helper types used 7 | //! exclusively in `druid`. 8 | 9 | use std::rc::Rc; 10 | 11 | use crate::shell::text::InputHandler; 12 | use crate::WidgetId; 13 | 14 | /// A trait for input handlers registered by widgets. 15 | /// 16 | /// A widget registers itself as accepting text input by calling 17 | /// [`LifeCycleCtx::register_text_input`] while handling the 18 | /// [`LifeCycle::WidgetAdded`] event. 19 | /// 20 | /// The widget does not explicitly *deregister* afterwards; rather anytime 21 | /// the widget tree changes, `druid` will call [`is_alive`] on each registered 22 | /// `ImeHandlerRef`, and deregister those that return `false`. 23 | /// 24 | /// [`LifeCycle::WidgetAdded`]: crate::LifeCycle::WidgetAdded 25 | /// [`LifeCycleCtx::register_text_input`]: crate::LifeCycleCtx::register_text_input 26 | /// [`is_alive`]: ImeHandlerRef::is_alive 27 | pub trait ImeHandlerRef { 28 | /// Returns `true` if this handler is still active. 29 | fn is_alive(&self) -> bool; 30 | /// Mark the session as locked, and return a handle. 31 | /// 32 | /// The lock can be read-write or read-only, indicated by the `mutable` flag. 33 | /// 34 | /// if [`is_alive`] is `true`, this should always return `Some(_)`. 35 | /// 36 | /// [`is_alive`]: ImeHandlerRef::is_alive 37 | fn acquire(&self, mutable: bool) -> Option>; 38 | /// Mark the session as released. 39 | fn release(&self) -> bool; 40 | } 41 | 42 | /// A type we use to keep track of which widgets are responsible for which 43 | /// ime sessions. 44 | #[derive(Clone)] 45 | pub(crate) struct TextFieldRegistration { 46 | pub widget_id: WidgetId, 47 | pub document: Rc, 48 | } 49 | 50 | impl TextFieldRegistration { 51 | pub fn is_alive(&self) -> bool { 52 | self.document.is_alive() 53 | } 54 | } 55 | 56 | impl std::fmt::Debug for TextFieldRegistration { 57 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 58 | f.debug_struct("TextFieldRegistration") 59 | .field("widget_id", &self.widget_id) 60 | .field("is_alive", &self.document.is_alive()) 61 | .finish() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /druid/src/text/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Editing and displaying text. 5 | 6 | mod attribute; 7 | mod backspace; 8 | mod editable_text; 9 | mod font_descriptor; 10 | mod format; 11 | mod input_component; 12 | mod input_methods; 13 | mod layout; 14 | mod movement; 15 | mod rich_text; 16 | mod storage; 17 | 18 | pub use crate::piet::{FontFamily, FontStyle, FontWeight, TextAlignment}; 19 | pub use druid_shell::text::{ 20 | Action as TextAction, Affinity, Direction, Event as ImeInvalidation, InputHandler, Movement, 21 | Selection, VerticalMovement, WritingDirection, 22 | }; 23 | 24 | pub use self::attribute::{Attribute, AttributeSpans, Link}; 25 | pub use self::backspace::offset_for_delete_backwards; 26 | pub use self::editable_text::{EditableText, EditableTextCursor, StringCursor}; 27 | pub use self::font_descriptor::FontDescriptor; 28 | pub use self::format::{Formatter, ParseFormatter, Validation, ValidationError}; 29 | pub use self::layout::{LayoutMetrics, TextLayout}; 30 | pub use self::movement::movement; 31 | pub use input_component::{EditSession, TextComponent}; 32 | pub use input_methods::ImeHandlerRef; 33 | pub use rich_text::{AttributesAdder, RichText, RichTextBuilder}; 34 | pub use storage::{ArcStr, EnvUpdateCtx, TextStorage}; 35 | 36 | pub(crate) use input_methods::TextFieldRegistration; 37 | -------------------------------------------------------------------------------- /druid/src/text/storage.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Storing text. 5 | 6 | use std::sync::Arc; 7 | 8 | use crate::env::KeyLike; 9 | use crate::piet::{PietTextLayoutBuilder, TextStorage as PietTextStorage}; 10 | use crate::{Data, Env}; 11 | 12 | use super::attribute::Link; 13 | use crate::UpdateCtx; 14 | 15 | /// A type that represents text that can be displayed. 16 | pub trait TextStorage: PietTextStorage + Data { 17 | /// If this TextStorage object manages style spans, it should implement 18 | /// this method and update the provided builder with its spans, as required. 19 | #[allow(unused_variables)] 20 | fn add_attributes(&self, builder: PietTextLayoutBuilder, env: &Env) -> PietTextLayoutBuilder { 21 | builder 22 | } 23 | 24 | /// This is called whenever the Env changes and should return true 25 | /// if the layout should be rebuilt. 26 | #[allow(unused_variables)] 27 | fn env_update(&self, ctx: &EnvUpdateCtx) -> bool { 28 | false 29 | } 30 | 31 | /// Any additional [`Link`] attributes on this text. 32 | /// 33 | /// If this `TextStorage` object manages link attributes, it should implement this 34 | /// method and return any attached [`Link`]s. 35 | /// 36 | /// Unlike other attributes, links are managed in Druid, not in [`piet`]; as such they 37 | /// require a separate API. 38 | /// 39 | /// [`Link`]: super::attribute::Link 40 | /// [`piet`]: crate::piet 41 | fn links(&self) -> &[Link] { 42 | &[] 43 | } 44 | } 45 | 46 | /// Provides information about keys change for more fine grained invalidation 47 | pub struct EnvUpdateCtx<'a, 'b>(&'a UpdateCtx<'a, 'b>); 48 | 49 | impl<'a, 'b> EnvUpdateCtx<'a, 'b> { 50 | /// Create an [`EnvUpdateCtx`] for [`Widget::update`]. 51 | /// 52 | /// [`Widget::update`]: crate::Widget::update 53 | pub(crate) fn for_update(ctx: &'a UpdateCtx<'a, 'b>) -> Self { 54 | Self(ctx) 55 | } 56 | 57 | /// Returns `true` if the given key has changed since the last [`env_update`] 58 | /// call. 59 | /// 60 | /// See [`UpdateCtx::env_key_changed`] for more details. 61 | /// 62 | /// [`env_update`]: TextStorage::env_update 63 | pub fn env_key_changed(&self, key: &impl KeyLike) -> bool { 64 | self.0.env_key_changed(key) 65 | } 66 | } 67 | 68 | /// A reference counted string slice. 69 | /// 70 | /// This is a data-friendly way to represent strings in Druid. Unlike `String` 71 | /// it cannot be mutated, but unlike `String` it can be cheaply cloned. 72 | pub type ArcStr = Arc; 73 | 74 | impl TextStorage for ArcStr {} 75 | 76 | impl TextStorage for String {} 77 | 78 | impl TextStorage for Arc {} 79 | -------------------------------------------------------------------------------- /druid/src/util.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Miscellaneous utility functions. 5 | 6 | use std::collections::HashMap; 7 | use std::hash::Hash; 8 | use std::mem; 9 | 10 | /// Panic in debug and tracing::error in release mode. 11 | /// 12 | /// This macro is in some way a combination of `panic` and `debug_assert`, 13 | /// but it will log the provided message instead of ignoring it in release builds. 14 | /// 15 | /// It's useful when a backtrace would aid debugging but a crash can be avoided in release. 16 | macro_rules! debug_panic { 17 | () => { ... }; 18 | ($msg:expr) => { 19 | if cfg!(debug_assertions) { 20 | panic!($msg); 21 | } else { 22 | tracing::error!($msg); 23 | } 24 | }; 25 | ($msg:expr,) => { debug_panic!($msg) }; 26 | ($fmt:expr, $($arg:tt)+) => { 27 | if cfg!(debug_assertions) { 28 | panic!($fmt, $($arg)*); 29 | } else { 30 | tracing::error!($fmt, $($arg)*); 31 | } 32 | }; 33 | } 34 | 35 | /// Fast path for equal type extend + drain. 36 | pub trait ExtendDrain { 37 | /// Extend the collection by draining the entries from `source`. 38 | /// 39 | /// This function may swap the underlying memory locations, 40 | /// so keep that in mind if one of the collections has a large allocation 41 | /// and it should keep that allocation. 42 | #[allow(dead_code)] 43 | fn extend_drain(&mut self, source: &mut Self); 44 | } 45 | 46 | impl ExtendDrain for HashMap 47 | where 48 | K: Eq + Hash + Copy, 49 | V: Copy, 50 | { 51 | // Benchmarking this vs just extend+drain with a 10k entry map. 52 | // 53 | // running 2 tests 54 | // test bench_extend ... bench: 1,971 ns/iter (+/- 566) 55 | // test bench_extend_drain ... bench: 0 ns/iter (+/- 0) 56 | fn extend_drain(&mut self, source: &mut Self) { 57 | if !source.is_empty() { 58 | if self.is_empty() { 59 | // If the target is empty we can just swap the pointers. 60 | mem::swap(self, source); 61 | } else { 62 | // Otherwise we need to fall back to regular extend-drain. 63 | self.extend(source.drain()); 64 | } 65 | } 66 | } 67 | } 68 | 69 | /// An enum for specifying whether an event was handled. 70 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 71 | pub enum Handled { 72 | /// An event was already handled, and shouldn't be propagated to other event handlers. 73 | Yes, 74 | /// An event has not yet been handled. 75 | No, 76 | } 77 | 78 | impl Handled { 79 | /// Has the event been handled yet? 80 | pub fn is_handled(self) -> bool { 81 | self == Handled::Yes 82 | } 83 | } 84 | 85 | impl From for Handled { 86 | /// Returns `Handled::Yes` if `handled` is true, and `Handled::No` otherwise. 87 | fn from(handled: bool) -> Handled { 88 | if handled { 89 | Handled::Yes 90 | } else { 91 | Handled::No 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /druid/src/widget/added.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! A [`Controller`] widget that responds to [`LifeCycle::WidgetAdded`] event. 5 | //! 6 | //! [`Controller`]: crate::widget::Controller 7 | //! [`LifeCycle::WidgetAdded`]: crate::LifeCycle::WidgetAdded 8 | 9 | use crate::widget::Controller; 10 | use crate::{Data, Env, LifeCycleCtx, Widget}; 11 | use tracing::{instrument, trace}; 12 | 13 | /// This [`Controller`] widget responds to [`LifeCycle::WidgetAdded`] event 14 | /// with the provided closure. Pass this and a child widget to [`ControllerHost`] 15 | /// to respond to the event when the child widget is added to the widget tree. 16 | /// This is also available, for convenience, as an `on_added` method 17 | /// via [`WidgetExt`]. 18 | /// 19 | /// [`Controller`]: crate::widget::Controller 20 | /// [`ControllerHost`]: crate::widget::ControllerHost 21 | /// [`WidgetExt`]: crate::widget::WidgetExt 22 | /// [`LifeCycle::WidgetAdded`]: crate::LifeCycle::WidgetAdded 23 | pub struct Added { 24 | /// A closure that will be invoked when the child widget is added 25 | /// to the widget tree 26 | action: Box, 27 | } 28 | 29 | impl> Added { 30 | /// Create a new [`Controller`] widget to respond to widget added to tree event. 31 | pub fn new(action: impl Fn(&mut W, &mut LifeCycleCtx, &T, &Env) + 'static) -> Self { 32 | Self { 33 | action: Box::new(action), 34 | } 35 | } 36 | } 37 | 38 | impl> Controller for Added { 39 | #[instrument( 40 | name = "Added", 41 | level = "trace", 42 | skip(self, child, ctx, event, data, env) 43 | )] 44 | fn lifecycle( 45 | &mut self, 46 | child: &mut W, 47 | ctx: &mut LifeCycleCtx, 48 | event: &crate::LifeCycle, 49 | data: &T, 50 | env: &Env, 51 | ) { 52 | if let crate::LifeCycle::WidgetAdded = event { 53 | trace!("Widget added"); 54 | (self.action)(child, ctx, data, env); 55 | } 56 | child.lifecycle(ctx, event, data, env) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /druid/src/widget/click.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! A clickable [`Controller`] widget. 5 | 6 | use crate::widget::Controller; 7 | use crate::{Data, Env, Event, EventCtx, LifeCycle, LifeCycleCtx, MouseButton, Widget}; 8 | use tracing::{instrument, trace}; 9 | 10 | /// A clickable [`Controller`] widget. Pass this and a child widget to a 11 | /// [`ControllerHost`] to make the child interactive. More conveniently, this is 12 | /// available as an [`on_click`] method via [`WidgetExt`]. 13 | /// 14 | /// This is an alternative to the standard [`Button`] widget, for when you want 15 | /// to make an arbitrary widget clickable. 16 | /// 17 | /// The child widget will also be updated on [`LifeCycle::HotChanged`] and 18 | /// mouse down, which can be useful for painting based on `ctx.is_active()` 19 | /// and `ctx.is_hot()`. 20 | /// 21 | /// [`ControllerHost`]: super::ControllerHost 22 | /// [`on_click`]: super::WidgetExt::on_click 23 | /// [`WidgetExt`]: super::WidgetExt 24 | /// [`Button`]: super::Button 25 | pub struct Click { 26 | /// A closure that will be invoked when the child widget is clicked. 27 | action: Box, 28 | } 29 | 30 | impl Click { 31 | /// Create a new clickable [`Controller`] widget. 32 | pub fn new(action: impl Fn(&mut EventCtx, &mut T, &Env) + 'static) -> Self { 33 | Click { 34 | action: Box::new(action), 35 | } 36 | } 37 | } 38 | 39 | impl> Controller for Click { 40 | #[instrument( 41 | name = "Click", 42 | level = "trace", 43 | skip(self, child, ctx, event, data, env) 44 | )] 45 | fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { 46 | match event { 47 | Event::MouseDown(mouse_event) => { 48 | if mouse_event.button == MouseButton::Left && !ctx.is_disabled() { 49 | ctx.set_active(true); 50 | ctx.request_paint(); 51 | trace!("Widget {:?} pressed", ctx.widget_id()); 52 | } 53 | } 54 | Event::MouseUp(mouse_event) => { 55 | if ctx.is_active() && mouse_event.button == MouseButton::Left { 56 | ctx.set_active(false); 57 | if ctx.is_hot() && !ctx.is_disabled() { 58 | (self.action)(ctx, data, env); 59 | } 60 | ctx.request_paint(); 61 | trace!("Widget {:?} released", ctx.widget_id()); 62 | } 63 | } 64 | _ => {} 65 | } 66 | 67 | child.event(ctx, event, data, env); 68 | } 69 | 70 | #[instrument( 71 | name = "Click", 72 | level = "trace", 73 | skip(self, child, ctx, event, data, env) 74 | )] 75 | fn lifecycle( 76 | &mut self, 77 | child: &mut W, 78 | ctx: &mut LifeCycleCtx, 79 | event: &LifeCycle, 80 | data: &T, 81 | env: &Env, 82 | ) { 83 | if let LifeCycle::HotChanged(_) | LifeCycle::FocusChanged(_) = event { 84 | ctx.request_paint(); 85 | } 86 | 87 | child.lifecycle(ctx, event, data, env); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /druid/src/widget/common.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::{Affine, Data, Size}; 5 | 6 | // These are based on https://api.flutter.dev/flutter/painting/BoxFit.html 7 | /// Strategies for inscribing a rectangle inside another rectangle. 8 | #[derive(Default, Clone, Data, Copy, PartialEq, Eq)] 9 | pub enum FillStrat { 10 | /// As large as possible without changing aspect ratio of image and all of image shown 11 | #[default] 12 | Contain, 13 | /// As large as possible with no dead space so that some of the image may be clipped 14 | Cover, 15 | /// Fill the widget with no dead space, aspect ratio of widget is used 16 | Fill, 17 | /// Fill the height with the images aspect ratio, some of the image may be clipped 18 | FitHeight, 19 | /// Fill the width with the images aspect ratio, some of the image may be clipped 20 | FitWidth, 21 | /// Do not scale 22 | None, 23 | /// Scale down to fit but do not scale up 24 | ScaleDown, 25 | } 26 | 27 | impl FillStrat { 28 | /// Calculate an origin and scale for an image with a given `FillStrat`. 29 | /// 30 | /// This takes some properties of a widget and a fill strategy and returns an affine matrix 31 | /// used to position and scale the image in the widget. 32 | pub fn affine_to_fill(self, parent: Size, fit_box: Size) -> Affine { 33 | let raw_scalex = parent.width / fit_box.width; 34 | let raw_scaley = parent.height / fit_box.height; 35 | 36 | let (scalex, scaley) = match self { 37 | FillStrat::Contain => { 38 | let scale = raw_scalex.min(raw_scaley); 39 | (scale, scale) 40 | } 41 | FillStrat::Cover => { 42 | let scale = raw_scalex.max(raw_scaley); 43 | (scale, scale) 44 | } 45 | FillStrat::Fill => (raw_scalex, raw_scaley), 46 | FillStrat::FitHeight => (raw_scaley, raw_scaley), 47 | FillStrat::FitWidth => (raw_scalex, raw_scalex), 48 | FillStrat::ScaleDown => { 49 | let scale = raw_scalex.min(raw_scaley).min(1.0); 50 | (scale, scale) 51 | } 52 | FillStrat::None => (1.0, 1.0), 53 | }; 54 | 55 | let origin_x = (parent.width - (fit_box.width * scalex)) / 2.0; 56 | let origin_y = (parent.height - (fit_box.height * scaley)) / 2.0; 57 | 58 | Affine::new([scalex, 0., 0., scaley, origin_x, origin_y]) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /druid/src/widget/disable_if.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::debug_state::DebugState; 5 | use crate::{ 6 | BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, 7 | Point, Size, UpdateCtx, Widget, WidgetPod, 8 | }; 9 | 10 | /// A widget wrapper which disables the child widget if the provided closure return true. 11 | /// 12 | /// See [`is_disabled`] or [`set_disabled`] for more info about disabled state. 13 | /// 14 | /// [`is_disabled`]: crate::EventCtx::is_disabled 15 | /// [`set_disabled`]: crate::EventCtx::set_disabled 16 | pub struct DisabledIf { 17 | child: WidgetPod, 18 | disabled_if: Box bool>, 19 | } 20 | 21 | impl> DisabledIf { 22 | /// Creates a new `DisabledIf` widget with the child widget and the closure to decide if the 23 | /// widget should be [`disabled`]. 24 | /// 25 | /// [`disabled`]: crate::EventCtx::is_disabled 26 | pub fn new(widget: W, disabled_if: impl Fn(&T, &Env) -> bool + 'static) -> Self { 27 | DisabledIf { 28 | child: WidgetPod::new(widget), 29 | disabled_if: Box::new(disabled_if), 30 | } 31 | } 32 | } 33 | 34 | impl> Widget for DisabledIf { 35 | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { 36 | self.child.event(ctx, event, data, env); 37 | } 38 | 39 | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { 40 | if let LifeCycle::WidgetAdded = event { 41 | ctx.set_disabled((self.disabled_if)(data, env)); 42 | } 43 | self.child.lifecycle(ctx, event, data, env); 44 | } 45 | 46 | fn update(&mut self, ctx: &mut UpdateCtx, _: &T, data: &T, env: &Env) { 47 | ctx.set_disabled((self.disabled_if)(data, env)); 48 | self.child.update(ctx, data, env); 49 | } 50 | 51 | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { 52 | let size = self.child.layout(ctx, bc, data, env); 53 | self.child.set_origin(ctx, Point::ZERO); 54 | ctx.set_baseline_offset(self.child.baseline_offset()); 55 | size 56 | } 57 | 58 | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { 59 | self.child.paint(ctx, data, env); 60 | } 61 | 62 | fn debug_state(&self, data: &T) -> DebugState { 63 | DebugState { 64 | display_name: self.short_type_name().to_string(), 65 | children: vec![self.child.widget().debug_state(data)], 66 | ..Default::default() 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /druid/src/widget/identity_wrapper.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! A widget that provides an explicit identity to a child. 5 | 6 | use crate::debug_state::DebugState; 7 | use crate::kurbo::Size; 8 | use crate::widget::prelude::*; 9 | use crate::widget::WidgetWrapper; 10 | use crate::Data; 11 | use tracing::instrument; 12 | 13 | /// A wrapper that adds an identity to an otherwise anonymous widget. 14 | pub struct IdentityWrapper { 15 | id: WidgetId, 16 | child: W, 17 | } 18 | 19 | impl IdentityWrapper { 20 | /// Assign an identity to a widget. 21 | pub fn wrap(child: W, id: WidgetId) -> IdentityWrapper { 22 | IdentityWrapper { id, child } 23 | } 24 | } 25 | 26 | impl> Widget for IdentityWrapper { 27 | #[instrument( 28 | name = "IdentityWrapper", 29 | level = "trace", 30 | skip(self, ctx, event, data, env) 31 | )] 32 | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { 33 | self.child.event(ctx, event, data, env); 34 | } 35 | 36 | #[instrument( 37 | name = "IdentityWrapper", 38 | level = "trace", 39 | skip(self, ctx, event, data, env) 40 | )] 41 | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { 42 | self.child.lifecycle(ctx, event, data, env) 43 | } 44 | 45 | #[instrument( 46 | name = "IdentityWrapper", 47 | level = "trace", 48 | skip(self, ctx, old_data, data, env) 49 | )] 50 | fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) { 51 | self.child.update(ctx, old_data, data, env); 52 | } 53 | 54 | #[instrument( 55 | name = "IdentityWrapper", 56 | level = "trace", 57 | skip(self, ctx, bc, data, env) 58 | )] 59 | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { 60 | self.child.layout(ctx, bc, data, env) 61 | } 62 | 63 | #[instrument(name = "IdentityWrapper", level = "trace", skip(self, ctx, data, env))] 64 | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { 65 | self.child.paint(ctx, data, env); 66 | } 67 | 68 | fn id(&self) -> Option { 69 | Some(self.id) 70 | } 71 | 72 | fn debug_state(&self, data: &T) -> DebugState { 73 | DebugState { 74 | display_name: self.short_type_name().to_string(), 75 | children: vec![self.child.debug_state(data)], 76 | ..Default::default() 77 | } 78 | } 79 | } 80 | 81 | impl WidgetWrapper for IdentityWrapper { 82 | widget_wrapper_body!(W, child); 83 | } 84 | -------------------------------------------------------------------------------- /druid/src/widget/intrinsic_width.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! A widget that sizes its child to the child's maximum intrinsic width. 5 | 6 | use crate::widget::Axis; 7 | use crate::{ 8 | BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Size, 9 | UpdateCtx, Widget, 10 | }; 11 | 12 | /// A widget that sizes its child to the child's maximum intrinsic width. 13 | /// 14 | /// This widget is useful, for example, when unlimited width is available and you would like a child 15 | /// that would otherwise attempt to expand infinitely to instead size itself to a more reasonable 16 | /// width. 17 | /// 18 | /// The constraints that this widget passes to its child will adhere to the parent's 19 | /// constraints, so if the constraints are not large enough to satisfy the child's maximum intrinsic 20 | /// width, then the child will get less width than it otherwise would. Likewise, if the minimum 21 | /// width constraint is larger than the child's maximum intrinsic width, the child will be given 22 | /// more width than it otherwise would. 23 | pub struct IntrinsicWidth { 24 | child: Box>, 25 | } 26 | 27 | impl IntrinsicWidth { 28 | /// Wrap the given `child` in this widget. 29 | pub fn new(child: impl Widget + 'static) -> Self { 30 | Self { 31 | child: Box::new(child), 32 | } 33 | } 34 | } 35 | 36 | impl Widget for IntrinsicWidth { 37 | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { 38 | self.child.event(ctx, event, data, env); 39 | } 40 | 41 | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { 42 | self.child.lifecycle(ctx, event, data, env); 43 | } 44 | 45 | fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) { 46 | self.child.update(ctx, old_data, data, env); 47 | } 48 | 49 | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { 50 | let iw = self 51 | .child 52 | .compute_max_intrinsic(Axis::Horizontal, ctx, bc, data, env); 53 | let new_bc = bc.shrink_max_width_to(iw); 54 | 55 | self.child.layout(ctx, &new_bc, data, env) 56 | } 57 | 58 | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { 59 | self.child.paint(ctx, data, env); 60 | } 61 | 62 | fn compute_max_intrinsic( 63 | &mut self, 64 | axis: Axis, 65 | ctx: &mut LayoutCtx, 66 | bc: &BoxConstraints, 67 | data: &T, 68 | env: &Env, 69 | ) -> f64 { 70 | match axis { 71 | Axis::Horizontal => self.child.compute_max_intrinsic(axis, ctx, bc, data, env), 72 | Axis::Vertical => { 73 | if !bc.is_width_bounded() { 74 | let w = self 75 | .child 76 | .compute_max_intrinsic(Axis::Horizontal, ctx, bc, data, env); 77 | let new_bc = bc.shrink_max_width_to(w); 78 | self.child 79 | .compute_max_intrinsic(axis, ctx, &new_bc, data, env) 80 | } else { 81 | self.child.compute_max_intrinsic(axis, ctx, bc, data, env) 82 | } 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /druid/src/widget/invalidation.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::debug_state::DebugState; 5 | use crate::widget::prelude::*; 6 | use crate::Data; 7 | use tracing::instrument; 8 | 9 | /// A widget that draws semi-transparent rectangles of changing colors to help debug invalidation 10 | /// regions. 11 | pub struct DebugInvalidation { 12 | child: W, 13 | debug_color: u64, 14 | marker: std::marker::PhantomData, 15 | } 16 | 17 | impl> DebugInvalidation { 18 | /// Wraps a widget in a `DebugInvalidation`. 19 | pub fn new(child: W) -> Self { 20 | Self { 21 | child, 22 | debug_color: 0, 23 | marker: std::marker::PhantomData, 24 | } 25 | } 26 | } 27 | 28 | impl> Widget for DebugInvalidation { 29 | #[instrument( 30 | name = "DebugInvalidation", 31 | level = "trace", 32 | skip(self, ctx, event, data, env) 33 | )] 34 | fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { 35 | self.child.event(ctx, event, data, env); 36 | } 37 | 38 | #[instrument( 39 | name = "DebugInvalidation", 40 | level = "trace", 41 | skip(self, ctx, event, data, env) 42 | )] 43 | fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { 44 | self.child.lifecycle(ctx, event, data, env) 45 | } 46 | 47 | #[instrument( 48 | name = "DebugInvalidation", 49 | level = "trace", 50 | skip(self, ctx, old_data, data, env) 51 | )] 52 | fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) { 53 | self.child.update(ctx, old_data, data, env); 54 | } 55 | 56 | #[instrument( 57 | name = "DebugInvalidation", 58 | level = "trace", 59 | skip(self, ctx, bc, data, env) 60 | )] 61 | fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { 62 | self.child.layout(ctx, bc, data, env) 63 | } 64 | 65 | #[instrument( 66 | name = "DebugInvalidation", 67 | level = "trace", 68 | skip(self, ctx, data, env) 69 | )] 70 | fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { 71 | self.child.paint(ctx, data, env); 72 | 73 | let color = env.get_debug_color(self.debug_color); 74 | let stroke_width = 2.0; 75 | let region = ctx.region().rects().to_owned(); 76 | for rect in ®ion { 77 | let rect = rect.inset(-stroke_width / 2.0); 78 | ctx.stroke(rect, &color, stroke_width); 79 | } 80 | self.debug_color += 1; 81 | } 82 | 83 | fn id(&self) -> Option { 84 | self.child.id() 85 | } 86 | 87 | fn debug_state(&self, data: &T) -> DebugState { 88 | DebugState { 89 | display_name: self.short_type_name().to_string(), 90 | children: vec![self.child.debug_state(data)], 91 | ..Default::default() 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /druid/src/widget/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | //! Common widgets. 5 | 6 | // First as it defines macros 7 | #[macro_use] 8 | mod widget_wrapper; 9 | 10 | mod added; 11 | mod align; 12 | mod aspect_ratio_box; 13 | mod button; 14 | mod checkbox; 15 | mod click; 16 | mod clip_box; 17 | mod common; 18 | mod container; 19 | mod controller; 20 | mod disable_if; 21 | mod either; 22 | mod env_scope; 23 | mod flex; 24 | mod identity_wrapper; 25 | mod image; 26 | mod intrinsic_width; 27 | mod invalidation; 28 | mod label; 29 | mod lens_wrap; 30 | mod list; 31 | mod maybe; 32 | mod padding; 33 | mod painter; 34 | mod parse; 35 | mod progress_bar; 36 | mod radio; 37 | mod scope; 38 | mod scroll; 39 | mod sized_box; 40 | mod slider; 41 | mod spinner; 42 | mod split; 43 | mod stepper; 44 | #[cfg(feature = "svg")] 45 | #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] 46 | mod svg; 47 | mod switch; 48 | mod tabs; 49 | mod textbox; 50 | mod value_textbox; 51 | mod view_switcher; 52 | #[allow(clippy::module_inception)] 53 | mod widget; 54 | mod widget_ext; 55 | mod z_stack; 56 | 57 | pub use self::image::Image; 58 | pub use added::Added; 59 | pub use align::Align; 60 | pub use aspect_ratio_box::AspectRatioBox; 61 | pub use button::Button; 62 | pub use checkbox::Checkbox; 63 | pub use click::Click; 64 | pub use clip_box::{ClipBox, Viewport}; 65 | pub use common::FillStrat; 66 | pub use container::Container; 67 | pub use controller::{Controller, ControllerHost}; 68 | pub use disable_if::DisabledIf; 69 | pub use either::Either; 70 | pub use env_scope::EnvScope; 71 | pub use flex::{Axis, CrossAxisAlignment, Flex, FlexParams, MainAxisAlignment}; 72 | pub use identity_wrapper::IdentityWrapper; 73 | pub use intrinsic_width::IntrinsicWidth; 74 | pub use label::{Label, LabelText, LineBreaking, RawLabel}; 75 | pub use lens_wrap::LensWrap; 76 | pub use list::{List, ListIter}; 77 | pub use maybe::Maybe; 78 | pub use padding::Padding; 79 | pub use painter::{BackgroundBrush, Painter}; 80 | #[allow(deprecated)] 81 | pub use parse::Parse; 82 | pub use progress_bar::ProgressBar; 83 | pub use radio::{Radio, RadioGroup}; 84 | pub use scope::{DefaultScopePolicy, LensScopeTransfer, Scope, ScopePolicy, ScopeTransfer}; 85 | pub use scroll::Scroll; 86 | pub use sized_box::SizedBox; 87 | pub use slider::{KnobStyle, RangeSlider, Slider}; 88 | pub use spinner::Spinner; 89 | pub use split::Split; 90 | pub use stepper::Stepper; 91 | #[cfg(feature = "svg")] 92 | pub use svg::{Svg, SvgData}; 93 | pub use switch::Switch; 94 | pub use tabs::{AddTab, TabInfo, Tabs, TabsEdge, TabsPolicy, TabsState, TabsTransition}; 95 | pub use textbox::TextBox; 96 | pub use value_textbox::{TextBoxEvent, ValidationDelegate, ValueTextBox}; 97 | pub use view_switcher::ViewSwitcher; 98 | pub use widget::{Widget, WidgetId}; 99 | pub use widget_ext::WidgetExt; 100 | pub use widget_wrapper::WidgetWrapper; 101 | pub use z_stack::ZStack; 102 | 103 | /// The types required to implement a [`Widget`]. 104 | pub mod prelude { 105 | // Wildcard because rustdoc has trouble inlining docs of two things called Data 106 | pub use crate::data::*; 107 | 108 | #[doc(inline)] 109 | pub use crate::{ 110 | BoxConstraints, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, 111 | RenderContext, Size, UpdateCtx, Widget, WidgetId, 112 | }; 113 | } 114 | -------------------------------------------------------------------------------- /druid/src/widget/widget_wrapper.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 the Druid Authors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// A trait for widgets that wrap a single child to expose that child for access and mutation 5 | pub trait WidgetWrapper { 6 | /// The type of the wrapped widget. 7 | /// Maybe we would like to constrain this to `Widget` (if existential bounds were supported). 8 | /// Any other scheme leads to `T` being unconstrained in unification at some point 9 | type Wrapped; 10 | /// Get immutable access to the wrapped child 11 | fn wrapped(&self) -> &Self::Wrapped; 12 | /// Get mutable access to the wrapped child 13 | fn wrapped_mut(&mut self) -> &mut Self::Wrapped; 14 | } 15 | 16 | /// A macro to help implementation of WidgetWrapper for a direct wrapper. 17 | /// Use it in the body of the impl. 18 | /// 19 | #[macro_export] 20 | macro_rules! widget_wrapper_body { 21 | ($wrapped:ty, $field:ident) => { 22 | type Wrapped = $wrapped; 23 | 24 | fn wrapped(&self) -> &Self::Wrapped { 25 | &self.$field 26 | } 27 | 28 | fn wrapped_mut(&mut self) -> &mut Self::Wrapped { 29 | &mut self.$field 30 | } 31 | }; 32 | } 33 | 34 | /// A macro to help implementation of WidgetWrapper for a wrapper of a typed pod. 35 | /// Use it in the body of the impl. 36 | /// 37 | #[macro_export] 38 | macro_rules! widget_wrapper_pod_body { 39 | ($wrapped:ty, $field:ident) => { 40 | type Wrapped = $wrapped; 41 | 42 | fn wrapped(&self) -> &Self::Wrapped { 43 | self.$field.widget() 44 | } 45 | 46 | fn wrapped_mut(&mut self) -> &mut Self::Wrapped { 47 | self.$field.widget_mut() 48 | } 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | use_field_init_shorthand = true 3 | newline_style = "Unix" 4 | --------------------------------------------------------------------------------