├── .devcontainer └── devcontainer.json ├── .github └── workflows │ ├── ci.yml │ └── docs.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── docs └── img │ ├── inspector.png │ ├── quickstart.png │ └── widget-gallery.png ├── editor-core ├── Cargo.toml └── src │ ├── buffer │ ├── diff.rs │ ├── mod.rs │ ├── rope_text.rs │ └── test.rs │ ├── char_buffer.rs │ ├── chars.rs │ ├── command.rs │ ├── cursor.rs │ ├── editor.rs │ ├── indent.rs │ ├── lib.rs │ ├── line_ending.rs │ ├── mode.rs │ ├── movement.rs │ ├── paragraph.rs │ ├── register.rs │ ├── selection.rs │ ├── soft_tab.rs │ ├── util.rs │ └── word.rs ├── examples ├── color_palette │ ├── Cargo.toml │ ├── README.md │ ├── color_palette.png │ └── src │ │ └── main.rs ├── context │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── counter-simple │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── counter │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── dyn-container │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── editor │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── files │ ├── Cargo.toml │ └── src │ │ ├── files.rs │ │ ├── lib.rs │ │ └── main.rs ├── flight_booker │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── keyboard_handler │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── layout │ ├── Cargo.toml │ └── src │ │ ├── draggable_sidebar.rs │ │ ├── holy_grail.rs │ │ ├── left_sidebar.rs │ │ ├── main.rs │ │ ├── right_sidebar.rs │ │ └── tab_navigation.rs ├── responsive │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── stacks │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── dyn_stack.rs │ │ ├── main.rs │ │ ├── stack.rs │ │ ├── stack_from_iter.rs │ │ └── virtual_stack.rs ├── syntax-editor │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── themes │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── timer │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── todo-complex │ ├── Cargo.toml │ └── src │ │ ├── app_config.rs │ │ ├── main.rs │ │ ├── todo.rs │ │ └── todo_state.rs ├── tokio-timer │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── view-transition │ ├── Cargo.toml │ └── src │ │ ├── main.rs │ │ ├── music_player.rs │ │ └── music_player │ │ └── svg.rs ├── virtual_list │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── webgpu │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── fonts │ │ ├── DejaVu-LICENSE │ │ ├── DejaVuSerif.ttf │ │ ├── FiraMono-LICENSE │ │ ├── FiraMono-Medium.ttf │ │ ├── FiraSans-LICENSE │ │ └── FiraSans-Medium.ttf │ ├── index.html │ └── src │ │ ├── lib.rs │ │ └── main.rs ├── widget-gallery │ ├── Cargo.toml │ ├── assets │ │ ├── ferris.png │ │ ├── ferris.svg │ │ └── sunflower.jpg │ └── src │ │ ├── animation.rs │ │ ├── buttons.rs │ │ ├── canvas.rs │ │ ├── checkbox.rs │ │ ├── clipboard.rs │ │ ├── context_menu.rs │ │ ├── draggable.rs │ │ ├── dropdown.rs │ │ ├── dropped_file.rs │ │ ├── form.rs │ │ ├── images.rs │ │ ├── inputs.rs │ │ ├── labels.rs │ │ ├── lists.rs │ │ ├── main.rs │ │ ├── radio_buttons.rs │ │ ├── rich_text.rs │ │ └── slider.rs ├── window-icon │ ├── Cargo.toml │ ├── assets │ │ ├── ferris.png │ │ └── ferris.svg │ └── src │ │ └── main.rs ├── window-scale │ ├── Cargo.toml │ └── src │ │ └── main.rs └── window-size │ ├── Cargo.toml │ └── src │ └── main.rs ├── reactive ├── Cargo.toml ├── src │ ├── base.rs │ ├── context.rs │ ├── derived.rs │ ├── effect.rs │ ├── id.rs │ ├── impls.rs │ ├── lib.rs │ ├── memo.rs │ ├── read.rs │ ├── runtime.rs │ ├── scope.rs │ ├── signal.rs │ ├── trigger.rs │ └── write.rs └── tests │ └── effect.rs ├── renderer ├── Cargo.toml └── src │ ├── gpu_resources.rs │ ├── lib.rs │ ├── swash.rs │ └── text │ ├── attrs.rs │ ├── layout.rs │ └── mod.rs ├── src ├── action.rs ├── animate.rs ├── app.rs ├── app_delegate.rs ├── app_handle.rs ├── app_state.rs ├── border_path_iter.rs ├── clipboard.rs ├── context.rs ├── dropped_file.rs ├── easing.rs ├── event.rs ├── ext_event.rs ├── file.rs ├── file_action.rs ├── id.rs ├── inspector.rs ├── inspector │ ├── data.rs │ └── view.rs ├── keyboard.rs ├── lib.rs ├── menu.rs ├── nav.rs ├── pointer.rs ├── profiler.rs ├── renderer.rs ├── responsive.rs ├── screen_layout.rs ├── style.rs ├── theme.rs ├── touchpad.rs ├── unit.rs ├── update.rs ├── view.rs ├── view_state.rs ├── view_storage.rs ├── view_tuple.rs ├── views │ ├── button.rs │ ├── canvas.rs │ ├── checkbox.rs │ ├── clip.rs │ ├── container.rs │ ├── decorator.rs │ ├── drag_resize_window_area.rs │ ├── drag_window_area.rs │ ├── dropdown.rs │ ├── dyn_container.rs │ ├── dyn_stack.rs │ ├── dyn_view.rs │ ├── editor │ │ ├── actions.rs │ │ ├── color.rs │ │ ├── command.rs │ │ ├── gutter.rs │ │ ├── id.rs │ │ ├── keypress │ │ │ ├── key.rs │ │ │ ├── mod.rs │ │ │ └── press.rs │ │ ├── layout.rs │ │ ├── listener.rs │ │ ├── mod.rs │ │ ├── movement.rs │ │ ├── phantom_text.rs │ │ ├── text.rs │ │ ├── text_document.rs │ │ ├── view.rs │ │ └── visual_line.rs │ ├── empty.rs │ ├── img.rs │ ├── label.rs │ ├── list.rs │ ├── mod.rs │ ├── radio_button.rs │ ├── resizable.rs │ ├── rich_text.rs │ ├── scroll.rs │ ├── slider.rs │ ├── stack.rs │ ├── svg.rs │ ├── tab.rs │ ├── text_editor.rs │ ├── text_input.rs │ ├── toggle_button.rs │ ├── tooltip.rs │ ├── value_container.rs │ ├── virtual_list.rs │ └── virtual_stack.rs ├── window.rs ├── window_handle.rs ├── window_id.rs └── window_tracking.rs ├── tiny_skia ├── Cargo.toml └── src │ └── lib.rs ├── vello ├── Cargo.toml └── src │ └── lib.rs └── vger ├── Cargo.toml └── src └── lib.rs /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ghcr.io/lapce/lapdev-devcontainer-lapce", 3 | "forwardPorts": [6080], 4 | "portsAttributes": { 5 | "6080": { 6 | "label": "VNC", 7 | } 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | types: [opened, synchronize, reopened, ready_for_review] 7 | 8 | name: CI 9 | 10 | concurrency: 11 | group: ${{ github.ref }}-${{ github.workflow }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | build: 16 | name: Rust on ${{ matrix.os }} 17 | if: github.event.pull_request.draft == false 18 | needs: [fmt, clippy] 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | os: 23 | - macos-latest 24 | - ubuntu-latest 25 | - windows-latest 26 | runs-on: ${{ matrix.os }} 27 | steps: 28 | - name: Checkout repo 29 | uses: actions/checkout@v4 30 | 31 | - name: Install dependencies on Ubuntu 32 | if: startsWith(matrix.os, 'ubuntu') 33 | run: | 34 | sudo apt-get -y update 35 | sudo apt-get -y install clang libwayland-dev libxkbcommon-x11-dev pkg-config libvulkan-dev libxcb-shape0-dev libxcb-xfixes0-dev 36 | 37 | - name: Update toolchain 38 | run: | 39 | rustup update --no-self-update 40 | 41 | - name: Cache Rust dependencies 42 | uses: Swatinem/rust-cache@v2 43 | 44 | - name: Fetch dependencies 45 | run: cargo fetch 46 | 47 | - name: Build 48 | run: cargo build 49 | 50 | - name: Run tests 51 | run: cargo test --workspace 52 | 53 | - name: Run doc tests 54 | run: cargo test --doc --workspace 55 | 56 | 57 | fmt: 58 | name: Rustfmt 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Checkout repo 62 | uses: actions/checkout@v4 63 | 64 | - name: Update toolchain & add rustfmt 65 | run: | 66 | rustup update 67 | rustup component add rustfmt 68 | 69 | - name: Run rustfmt 70 | run: cargo fmt --all --check 71 | 72 | clippy: 73 | name: Clippy on ${{ matrix.os }} 74 | strategy: 75 | fail-fast: false 76 | matrix: 77 | os: 78 | - macos-latest 79 | - ubuntu-latest 80 | - windows-latest 81 | runs-on: ${{ matrix.os }} 82 | 83 | steps: 84 | - name: Checkout repo 85 | uses: actions/checkout@v4 86 | 87 | - name: Update toolchain & add clippy 88 | run: | 89 | rustup update --no-self-update 90 | rustup component add clippy 91 | 92 | - name: Install dependencies on Ubuntu 93 | if: startsWith(matrix.os, 'ubuntu') 94 | run: | 95 | sudo apt-get -y update 96 | sudo apt-get -y install clang libwayland-dev libxkbcommon-x11-dev pkg-config libvulkan-dev 97 | 98 | - name: Cache Rust dependencies 99 | uses: Swatinem/rust-cache@v2 100 | 101 | - name: Fetch dependencies 102 | run: cargo fetch 103 | 104 | - name: Run clippy 105 | run: cargo clippy -- --deny warnings 106 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy documentation 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: "pages" 15 | cancel-in-progress: false 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Setup Pages 24 | id: pages 25 | uses: actions/configure-pages@v4 26 | 27 | - name: Update toolchain 28 | run: rustup install nightly 29 | 30 | - name: Build docs 31 | env: 32 | RUSTDOCFLAGS: "-Z unstable-options --enable-index-page" 33 | run: | 34 | cargo +nightly doc --no-deps --workspace --lib --release -Z unstable-options -Z rustdoc-scrape-examples 35 | chmod -c -R +rX "target/doc" | while read line; do 36 | echo "::warning title=Invalid file permissions automatically fixed::$line" 37 | done 38 | 39 | - name: Upload artifact 40 | uses: actions/upload-pages-artifact@v3 41 | with: 42 | path: ./target/doc 43 | 44 | deploy: 45 | environment: 46 | name: github-pages 47 | url: ${{ steps.deployment.outputs.page_url }} 48 | runs-on: ubuntu-latest 49 | needs: build 50 | steps: 51 | - name: Deploy to GitHub Pages 52 | id: deployment 53 | uses: actions/deploy-pages@v4 54 | 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | /.lapce 4 | /.idea 5 | .DS_Store 6 | rustc-ice* 7 | *.orig 8 | *.db 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Floem aims to be a high performance UI library. New contributions are always welcome! 4 | 5 | The best way to start contributing to Floem is to learn how it works by looking at the examples. If you think there are more examples that can be added, feel free to send in a PR! 6 | 7 | Alternatively, you can have a look at issues to see if there is anything you'd like to pick up. 8 | 9 | Filing bugs with clear, reproducible steps is a great way to contribute as well. 10 | 11 | It's not a good library if there isn't good documentation. So, all contributions to the documentation are very appreciated! 12 | 13 | If you would like a conversation with the developers of Floem, you can join in the #floem channel on this [Discord](https://discord.gg/RB6cRYerXX). 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Floem 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/img/inspector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapce/floem/fab1fe93c9405edaaf0b931ceec72a99248f2852/docs/img/inspector.png -------------------------------------------------------------------------------- /docs/img/quickstart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapce/floem/fab1fe93c9405edaaf0b931ceec72a99248f2852/docs/img/quickstart.png -------------------------------------------------------------------------------- /docs/img/widget-gallery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapce/floem/fab1fe93c9405edaaf0b931ceec72a99248f2852/docs/img/widget-gallery.png -------------------------------------------------------------------------------- /editor-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floem-editor-core" 3 | version.workspace = true 4 | edition = "2021" 5 | repository = "https://github.com/lapce/floem" 6 | license.workspace = true 7 | description = "The core of the floem text editor" 8 | 9 | [dependencies] 10 | serde = { workspace = true, optional = true } 11 | strum = { workspace = true } 12 | strum_macros = { workspace = true } 13 | 14 | lapce-xi-rope = { workspace = true } 15 | 16 | itertools = "0.14.0" 17 | bitflags = "2.4.2" 18 | memchr = "2.7.1" 19 | 20 | [features] 21 | serde = ["dep:serde"] 22 | -------------------------------------------------------------------------------- /editor-core/src/chars.rs: -------------------------------------------------------------------------------- 1 | /// Determine whether a character is a line ending. 2 | #[inline] 3 | pub fn char_is_line_ending(ch: char) -> bool { 4 | matches!(ch, '\u{000A}') 5 | } 6 | 7 | /// Determine whether a character qualifies as (non-line-break) 8 | /// whitespace. 9 | #[inline] 10 | pub fn char_is_whitespace(ch: char) -> bool { 11 | // TODO: this is a naive binary categorization of whitespace 12 | // characters. For display, word wrapping, etc. we'll need a better 13 | // categorization based on e.g. breaking vs non-breaking spaces 14 | // and whether they're zero-width or not. 15 | match ch { 16 | //'\u{1680}' | // Ogham Space Mark (here for completeness, but usually displayed as a dash, not as whitespace) 17 | '\u{0009}' | // Character Tabulation 18 | '\u{0020}' | // Space 19 | '\u{00A0}' | // No-break Space 20 | '\u{180E}' | // Mongolian Vowel Separator 21 | '\u{202F}' | // Narrow No-break Space 22 | '\u{205F}' | // Medium Mathematical Space 23 | '\u{3000}' | // Ideographic Space 24 | '\u{FEFF}' // Zero Width No-break Space 25 | => true, 26 | 27 | // En Quad, Em Quad, En Space, Em Space, Three-per-em Space, 28 | // Four-per-em Space, Six-per-em Space, Figure Space, 29 | // Punctuation Space, Thin Space, Hair Space, Zero Width Space. 30 | ch if ('\u{2000}' ..= '\u{200B}').contains(&ch) => true, 31 | 32 | _ => false, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /editor-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod buffer; 2 | pub mod char_buffer; 3 | pub mod chars; 4 | pub mod command; 5 | pub mod cursor; 6 | pub mod editor; 7 | pub mod indent; 8 | pub mod line_ending; 9 | pub mod mode; 10 | pub mod movement; 11 | pub mod paragraph; 12 | pub mod register; 13 | pub mod selection; 14 | pub mod soft_tab; 15 | pub mod util; 16 | pub mod word; 17 | 18 | pub use lapce_xi_rope as xi_rope; 19 | -------------------------------------------------------------------------------- /editor-core/src/mode.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Write; 2 | 3 | use bitflags::bitflags; 4 | #[cfg(feature = "serde")] 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[derive(Clone, Debug, PartialEq, Eq)] 8 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 9 | pub enum MotionMode { 10 | Delete { count: usize }, 11 | Yank { count: usize }, 12 | Indent, 13 | Outdent, 14 | } 15 | 16 | impl MotionMode { 17 | pub fn count(&self) -> usize { 18 | match self { 19 | MotionMode::Delete { count } => *count, 20 | MotionMode::Yank { count } => *count, 21 | MotionMode::Indent => 1, 22 | MotionMode::Outdent => 1, 23 | } 24 | } 25 | } 26 | 27 | #[derive(Clone, PartialEq, Eq, Hash, Debug, Copy, Default, PartialOrd, Ord)] 28 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 29 | pub enum VisualMode { 30 | #[default] 31 | Normal, 32 | Linewise, 33 | Blockwise, 34 | } 35 | 36 | #[derive(Clone, PartialEq, Eq, Hash, Debug, Copy, PartialOrd, Ord)] 37 | pub enum Mode { 38 | Normal, 39 | Insert, 40 | Visual(VisualMode), 41 | Terminal, 42 | } 43 | 44 | bitflags! { 45 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 46 | pub struct Modes: u32 { 47 | const NORMAL = 0x1; 48 | const INSERT = 0x2; 49 | const VISUAL = 0x4; 50 | const TERMINAL = 0x8; 51 | } 52 | } 53 | 54 | impl From for Modes { 55 | fn from(mode: Mode) -> Self { 56 | match mode { 57 | Mode::Normal => Self::NORMAL, 58 | Mode::Insert => Self::INSERT, 59 | Mode::Visual(_) => Self::VISUAL, 60 | Mode::Terminal => Self::TERMINAL, 61 | } 62 | } 63 | } 64 | 65 | impl Modes { 66 | pub fn parse(modes_str: &str) -> Self { 67 | let mut this = Self::empty(); 68 | 69 | for c in modes_str.chars() { 70 | match c { 71 | 'i' | 'I' => this.set(Self::INSERT, true), 72 | 'n' | 'N' => this.set(Self::NORMAL, true), 73 | 'v' | 'V' => this.set(Self::VISUAL, true), 74 | 't' | 'T' => this.set(Self::TERMINAL, true), 75 | _ => {} 76 | } 77 | } 78 | 79 | this 80 | } 81 | } 82 | 83 | impl std::fmt::Display for Modes { 84 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 85 | let bits = [ 86 | (Self::INSERT, 'i'), 87 | (Self::NORMAL, 'n'), 88 | (Self::VISUAL, 'v'), 89 | (Self::TERMINAL, 't'), 90 | ]; 91 | for (bit, chr) in bits { 92 | if self.contains(bit) { 93 | f.write_char(chr)?; 94 | } 95 | } 96 | 97 | Ok(()) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /editor-core/src/register.rs: -------------------------------------------------------------------------------- 1 | use crate::mode::VisualMode; 2 | 3 | pub trait Clipboard { 4 | fn get_string(&mut self) -> Option; 5 | fn put_string(&mut self, s: impl AsRef); 6 | } 7 | 8 | #[derive(Clone, Default)] 9 | pub struct RegisterData { 10 | pub content: String, 11 | pub mode: VisualMode, 12 | } 13 | 14 | #[derive(Clone, Default)] 15 | pub struct Register { 16 | pub unnamed: RegisterData, 17 | last_yank: RegisterData, 18 | } 19 | 20 | pub enum RegisterKind { 21 | Delete, 22 | Yank, 23 | } 24 | 25 | impl Register { 26 | pub fn add(&mut self, kind: RegisterKind, data: RegisterData) { 27 | match kind { 28 | RegisterKind::Delete => self.add_delete(data), 29 | RegisterKind::Yank => self.add_yank(data), 30 | } 31 | } 32 | 33 | pub fn add_delete(&mut self, data: RegisterData) { 34 | self.unnamed = data; 35 | } 36 | 37 | pub fn add_yank(&mut self, data: RegisterData) { 38 | self.unnamed = data.clone(); 39 | self.last_yank = data; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /editor-core/src/util.rs: -------------------------------------------------------------------------------- 1 | use core::str::FromStr; 2 | use std::collections::HashMap; 3 | 4 | /// If the character is an opening bracket return Some(true), if closing, return Some(false) 5 | pub fn matching_pair_direction(c: char) -> Option { 6 | Some(match c { 7 | '{' => true, 8 | '}' => false, 9 | '(' => true, 10 | ')' => false, 11 | '[' => true, 12 | ']' => false, 13 | _ => return None, 14 | }) 15 | } 16 | 17 | pub fn matching_char(c: char) -> Option { 18 | Some(match c { 19 | '{' => '}', 20 | '}' => '{', 21 | '(' => ')', 22 | ')' => '(', 23 | '[' => ']', 24 | ']' => '[', 25 | _ => return None, 26 | }) 27 | } 28 | 29 | /// If the given character is a parenthesis, returns its matching bracket 30 | pub fn matching_bracket_general(char: char) -> Option 31 | where 32 | &'static str: ToStaticTextType, 33 | { 34 | let pair = match char { 35 | '{' => "}", 36 | '}' => "{", 37 | '(' => ")", 38 | ')' => "(", 39 | '[' => "]", 40 | ']' => "[", 41 | _ => return None, 42 | }; 43 | Some(pair.to_static()) 44 | } 45 | 46 | pub trait ToStaticTextType: 'static { 47 | fn to_static(self) -> R; 48 | } 49 | 50 | impl ToStaticTextType for &'static str { 51 | #[inline] 52 | fn to_static(self) -> &'static str { 53 | self 54 | } 55 | } 56 | 57 | impl ToStaticTextType for &'static str { 58 | #[inline] 59 | fn to_static(self) -> char { 60 | char::from_str(self).unwrap() 61 | } 62 | } 63 | 64 | impl ToStaticTextType for &'static str { 65 | #[inline] 66 | fn to_static(self) -> String { 67 | self.to_string() 68 | } 69 | } 70 | 71 | impl ToStaticTextType for char { 72 | #[inline] 73 | fn to_static(self) -> char { 74 | self 75 | } 76 | } 77 | 78 | impl ToStaticTextType for String { 79 | #[inline] 80 | fn to_static(self) -> String { 81 | self 82 | } 83 | } 84 | 85 | pub fn has_unmatched_pair(line: &str) -> bool { 86 | let mut count = HashMap::new(); 87 | let mut pair_first = HashMap::new(); 88 | for c in line.chars().rev() { 89 | if let Some(left) = matching_pair_direction(c) { 90 | let key = if left { c } else { matching_char(c).unwrap() }; 91 | let pair_count = *count.get(&key).unwrap_or(&0i32); 92 | pair_first.entry(key).or_insert(left); 93 | if left { 94 | count.insert(key, pair_count - 1); 95 | } else { 96 | count.insert(key, pair_count + 1); 97 | } 98 | } 99 | } 100 | for (_, pair_count) in count.iter() { 101 | if *pair_count < 0 { 102 | return true; 103 | } 104 | } 105 | for (_, left) in pair_first.iter() { 106 | if *left { 107 | return true; 108 | } 109 | } 110 | false 111 | } 112 | 113 | pub fn str_is_pair_left(c: &str) -> bool { 114 | if c.chars().count() == 1 { 115 | let c = c.chars().next().unwrap(); 116 | if matching_pair_direction(c).unwrap_or(false) { 117 | return true; 118 | } 119 | } 120 | false 121 | } 122 | 123 | pub fn str_matching_pair(c: &str) -> Option { 124 | if c.chars().count() == 1 { 125 | let c = c.chars().next().unwrap(); 126 | return matching_char(c); 127 | } 128 | None 129 | } 130 | -------------------------------------------------------------------------------- /examples/color_palette/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "color_palette" 3 | edition = "2021" 4 | license.workspace = true 5 | version.workspace = true 6 | 7 | [dependencies] 8 | im.workspace = true 9 | floem = { path = "../.." } 10 | palette = "0.7.6" 11 | -------------------------------------------------------------------------------- /examples/color_palette/README.md: -------------------------------------------------------------------------------- 1 | ## Color palette 2 | 3 | A color palette generator 4 | 5 |
6 | 7 |
8 | -------------------------------------------------------------------------------- /examples/color_palette/color_palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapce/floem/fab1fe93c9405edaaf0b931ceec72a99248f2852/examples/color_palette/color_palette.png -------------------------------------------------------------------------------- /examples/context/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "context" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | im.workspace = true 8 | floem = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/context/src/main.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | keyboard::{Key, NamedKey}, 3 | peniko::color::palette, 4 | peniko::Color, 5 | reactive::{provide_context, use_context}, 6 | views::{empty, label, v_stack, Decorators}, 7 | IntoView, View, 8 | }; 9 | 10 | fn colored_label(text: String) -> impl IntoView { 11 | let color: Color = use_context().unwrap(); 12 | label(move || text.clone()).style(move |s| s.color(color)) 13 | } 14 | 15 | fn context_container( 16 | color: Color, 17 | name: String, 18 | view_fn: impl Fn() -> V, 19 | ) -> impl IntoView { 20 | provide_context(color); 21 | 22 | v_stack((colored_label(name), view_fn())).style(move |s| { 23 | s.padding(10) 24 | .border(1) 25 | .border_color(color) 26 | .row_gap(5) 27 | .items_center() 28 | }) 29 | } 30 | 31 | fn app_view() -> impl IntoView { 32 | provide_context(palette::css::BLACK); 33 | 34 | let view = v_stack(( 35 | colored_label(String::from("app_view")), 36 | context_container( 37 | palette::css::HOT_PINK, 38 | String::from("Nested context 1"), 39 | || { 40 | context_container(palette::css::BLUE, String::from("Nested context 2"), || { 41 | context_container(palette::css::GREEN, String::from("Nested context 3"), empty) 42 | }) 43 | }, 44 | ), 45 | )) 46 | .style(|s| { 47 | s.width_full() 48 | .height_full() 49 | .items_center() 50 | .justify_center() 51 | .row_gap(5) 52 | }); 53 | 54 | let id = view.id(); 55 | view.on_key_up( 56 | Key::Named(NamedKey::F11), 57 | |m| m.is_empty(), 58 | move |_| id.inspect(), 59 | ) 60 | } 61 | 62 | fn main() { 63 | floem::launch(app_view); 64 | } 65 | -------------------------------------------------------------------------------- /examples/counter-simple/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "counter-simple" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | im.workspace = true 8 | floem = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/counter-simple/src/main.rs: -------------------------------------------------------------------------------- 1 | use floem::prelude::*; 2 | 3 | fn main() { 4 | floem::launch(counter_view); 5 | } 6 | 7 | fn counter_view() -> impl IntoView { 8 | let mut counter = RwSignal::new(0); 9 | 10 | h_stack(( 11 | button("Increment").action(move || counter += 1), 12 | label(move || format!("Value: {counter}")), 13 | button("Decrement").action(move || counter -= 1), 14 | )) 15 | .style(|s| s.size_full().items_center().justify_center().gap(10)) 16 | } 17 | -------------------------------------------------------------------------------- /examples/counter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "counter" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | im.workspace = true 8 | floem = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/counter/src/main.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | keyboard::{Key, NamedKey}, 3 | peniko::color::palette, 4 | peniko::Color, 5 | reactive::{create_signal, SignalGet, SignalUpdate}, 6 | unit::UnitExt, 7 | views::{dyn_view, Decorators, LabelClass, LabelCustomStyle}, 8 | IntoView, View, 9 | }; 10 | 11 | fn app_view() -> impl IntoView { 12 | let (counter, set_counter) = create_signal(0); 13 | let view = ( 14 | dyn_view(move || format!("Value: {}", counter.get())), 15 | counter.style(|s| s.padding(10.0)), 16 | ( 17 | "Increment" 18 | .style(|s| { 19 | s.border_radius(10.0) 20 | .padding(10.0) 21 | .background(palette::css::WHITE) 22 | .box_shadow_blur(5.0) 23 | .focus_visible(|s| s.outline(2.).outline_color(palette::css::BLUE)) 24 | .hover(|s| s.background(palette::css::LIGHT_GREEN)) 25 | .active(|s| { 26 | s.color(palette::css::WHITE) 27 | .background(palette::css::DARK_GREEN) 28 | }) 29 | }) 30 | .on_click_stop({ 31 | move |_| { 32 | set_counter.update(|value| *value += 1); 33 | } 34 | }) 35 | .keyboard_navigable(), 36 | "Decrement" 37 | .on_click_stop({ 38 | move |_| { 39 | set_counter.update(|value| *value -= 1); 40 | } 41 | }) 42 | .style(|s| { 43 | s.box_shadow_blur(5.0) 44 | .background(palette::css::WHITE) 45 | .border_radius(10.0) 46 | .padding(10.0) 47 | .margin_left(10.0) 48 | .focus_visible(|s| s.outline(2.).outline_color(palette::css::BLUE)) 49 | .hover(|s| s.background(Color::from_rgb8(244, 67, 54))) 50 | .active(|s| s.color(palette::css::WHITE).background(palette::css::RED)) 51 | }) 52 | .keyboard_navigable(), 53 | "Reset to 0" 54 | .on_click_stop(move |_| { 55 | println!("Reset counter pressed"); // will not fire if button is disabled 56 | set_counter.update(|value| *value = 0); 57 | }) 58 | .disabled(move || counter.get() == 0) 59 | .style(|s| { 60 | s.box_shadow_blur(5.0) 61 | .border_radius(10.0) 62 | .padding(10.0) 63 | .margin_left(10.0) 64 | .background(palette::css::LIGHT_BLUE) 65 | .focus_visible(|s| s.outline(2.).outline_color(palette::css::BLUE)) 66 | .disabled(|s| s.background(palette::css::LIGHT_GRAY)) 67 | .hover(|s| s.background(palette::css::LIGHT_YELLOW)) 68 | .active(|s| { 69 | s.color(palette::css::WHITE) 70 | .background(palette::css::YELLOW_GREEN) 71 | }) 72 | }) 73 | .keyboard_navigable(), 74 | ) 75 | .style(|s| s.custom_style_class(|s: LabelCustomStyle| s.selectable(false))), 76 | ) 77 | .style(|s| { 78 | s.size(100.pct(), 100.pct()) 79 | .flex_col() 80 | .items_center() 81 | .justify_center() 82 | }); 83 | 84 | let id = view.id(); 85 | view.on_key_up( 86 | Key::Named(NamedKey::F11), 87 | |m| m.is_empty(), 88 | move |_| id.inspect(), 89 | ) 90 | } 91 | 92 | fn main() { 93 | floem::launch(app_view); 94 | } 95 | -------------------------------------------------------------------------------- /examples/dyn-container/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dyn-container" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | im.workspace = true 8 | floem = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/dyn-container/src/main.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | animate::Animation, 3 | reactive::{create_rw_signal, RwSignal, SignalGet, SignalUpdate}, 4 | views::*, 5 | IntoView, 6 | }; 7 | 8 | #[derive(Clone, Copy, PartialEq)] 9 | enum ViewSwitcher { 10 | One, 11 | Two, 12 | } 13 | impl ViewSwitcher { 14 | fn toggle(&mut self) { 15 | *self = match self { 16 | ViewSwitcher::One => ViewSwitcher::Two, 17 | ViewSwitcher::Two => ViewSwitcher::One, 18 | }; 19 | } 20 | 21 | fn view(&self, state: RwSignal) -> impl IntoView { 22 | match self { 23 | ViewSwitcher::One => view_one().into_any(), 24 | ViewSwitcher::Two => view_two(state).into_any(), 25 | } 26 | .animation(Animation::scale_effect) 27 | .clip() 28 | } 29 | } 30 | 31 | fn main() { 32 | floem::launch(app_view); 33 | } 34 | 35 | fn app_view() -> impl IntoView { 36 | let view = create_rw_signal(ViewSwitcher::One); 37 | 38 | v_stack(( 39 | button("Switch views").action(move || view.update(|which| which.toggle())), 40 | dyn_container(move || view.get(), move |which| which.view(view)) 41 | .style(|s| s.border(1.).border_radius(5)), 42 | )) 43 | .style(|s| { 44 | s.width_full() 45 | .height_full() 46 | .items_center() 47 | .justify_center() 48 | .gap(20) 49 | }) 50 | } 51 | 52 | fn view_one() -> impl IntoView { 53 | // container used to make the text clip evenly on both sides while animating 54 | container("A view").style(|s| s.size(100, 100).items_center().justify_center()) 55 | } 56 | 57 | fn view_two(view: RwSignal) -> impl IntoView { 58 | v_stack(( 59 | "Another view", 60 | button("Switch back").action(move || view.set(ViewSwitcher::One)), 61 | )) 62 | .style(|s| { 63 | s.row_gap(10.0) 64 | .size(150, 100) 65 | .items_center() 66 | .justify_center() 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /examples/editor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "editor" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | im.workspace = true 8 | floem = { path = "../..", features = ["editor"] } 9 | -------------------------------------------------------------------------------- /examples/editor/src/main.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | keyboard::{Key, NamedKey}, 3 | reactive::{RwSignal, SignalGet, SignalUpdate}, 4 | views::{ 5 | button, 6 | editor::{ 7 | command::{Command, CommandExecuted}, 8 | core::{command::EditCommand, editor::EditType, selection::Selection}, 9 | text::{default_dark_color, SimpleStyling}, 10 | }, 11 | stack, text_editor, Decorators, 12 | }, 13 | IntoView, View, 14 | }; 15 | 16 | fn app_view() -> impl IntoView { 17 | let text = std::env::args() 18 | .nth(1) 19 | .map(|s| std::fs::read_to_string(s).unwrap()); 20 | let text = text.as_deref().unwrap_or("Hello world"); 21 | 22 | let hide_gutter_a = RwSignal::new(false); 23 | let hide_gutter_b = RwSignal::new(true); 24 | 25 | let editor_a = text_editor(text) 26 | .styling(SimpleStyling::new()) 27 | .style(|s| s.size_full()) 28 | .editor_style(default_dark_color) 29 | .editor_style(move |s| s.hide_gutter(hide_gutter_a.get())); 30 | let editor_b = editor_a 31 | .shared_editor() 32 | .editor_style(default_dark_color) 33 | .editor_style(move |s| s.hide_gutter(hide_gutter_b.get())) 34 | .style(|s| s.size_full()) 35 | .pre_command(|ev| { 36 | if matches!(ev.cmd, Command::Edit(EditCommand::Undo)) { 37 | println!("Undo command executed on editor B, ignoring!"); 38 | return CommandExecuted::Yes; 39 | } 40 | CommandExecuted::No 41 | }) 42 | .update(|_| { 43 | // This hooks up to both editors! 44 | println!("Editor changed"); 45 | }) 46 | .placeholder("Some placeholder text"); 47 | let doc = editor_a.doc(); 48 | 49 | let view = stack(( 50 | editor_a, 51 | editor_b, 52 | stack(( 53 | button("Clear").action(move || { 54 | doc.edit_single( 55 | Selection::region(0, doc.text().len()), 56 | "", 57 | EditType::DeleteSelection, 58 | ); 59 | }), 60 | button("Flip Gutter").action(move || { 61 | hide_gutter_a.update(|hide| *hide = !*hide); 62 | hide_gutter_b.update(|hide| *hide = !*hide); 63 | }), 64 | )) 65 | .style(|s| s.width_full().flex_row().items_center().justify_center()), 66 | )) 67 | .style(|s| s.size_full().flex_col().items_center().justify_center()); 68 | 69 | let id = view.id(); 70 | view.on_key_up( 71 | Key::Named(NamedKey::F11), 72 | |m| m.is_empty(), 73 | move |_| id.inspect(), 74 | ) 75 | } 76 | 77 | fn main() { 78 | floem::launch(app_view) 79 | } 80 | -------------------------------------------------------------------------------- /examples/files/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "files" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | im.workspace = true 8 | floem = { path = "../..", features = ["rfd-async-std"] } 9 | -------------------------------------------------------------------------------- /examples/files/src/files.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | action::{open_file, save_as}, 3 | file::{FileDialogOptions, FileInfo, FileSpec}, 4 | reactive::{create_rw_signal, SignalGet, SignalUpdate}, 5 | text::Weight, 6 | views::{button, h_stack, label, v_stack, Decorators}, 7 | IntoView, 8 | }; 9 | 10 | pub fn files_view() -> impl IntoView { 11 | let files = create_rw_signal("".to_string()); 12 | let view = h_stack(( 13 | button("Select file").on_click_cont(move |_| { 14 | open_file( 15 | FileDialogOptions::new() 16 | .force_starting_directory("/") 17 | .title("Select file") 18 | .allowed_types(vec![FileSpec { 19 | name: "text", 20 | extensions: &["txt", "rs", "md"], 21 | }]), 22 | move |file_info| { 23 | if let Some(file) = file_info { 24 | println!("Selected file: {:?}", file.path); 25 | files.set(display_files(file)); 26 | } 27 | }, 28 | ); 29 | }), 30 | button("Select multiple files").on_click_cont(move |_| { 31 | open_file( 32 | FileDialogOptions::new() 33 | .multi_selection() 34 | .title("Select file") 35 | .allowed_types(vec![FileSpec { 36 | name: "text", 37 | extensions: &["txt", "rs", "md"], 38 | }]), 39 | move |file_info| { 40 | if let Some(file) = file_info { 41 | println!("Selected file: {:?}", file.path); 42 | files.set(display_files(file)); 43 | } 44 | }, 45 | ); 46 | }), 47 | button("Select folder").on_click_cont(move |_| { 48 | open_file( 49 | FileDialogOptions::new() 50 | .select_directories() 51 | .title("Select Folder"), 52 | move |file_info| { 53 | if let Some(file) = file_info { 54 | println!("Selected folder: {:?}", file.path); 55 | files.set(display_files(file)); 56 | } 57 | }, 58 | ); 59 | }), 60 | button("Select multiple folder").on_click_cont(move |_| { 61 | open_file( 62 | FileDialogOptions::new() 63 | .select_directories() 64 | .multi_selection() 65 | .title("Select multiple Folder"), 66 | move |file_info| { 67 | if let Some(file) = file_info { 68 | println!("Selected folder: {:?}", file.path); 69 | files.set(display_files(file)); 70 | } 71 | }, 72 | ); 73 | }), 74 | button("Save file").on_click_cont(move |_| { 75 | save_as( 76 | FileDialogOptions::new() 77 | .default_name("floem.file") 78 | .title("Save file"), 79 | move |file_info| { 80 | if let Some(file) = file_info { 81 | println!("Save file to: {:?}", file.path); 82 | files.set(display_files(file)); 83 | } 84 | }, 85 | ); 86 | }), 87 | )) 88 | .style(|s| s.justify_center()); 89 | 90 | v_stack(( 91 | view, 92 | h_stack(( 93 | "Path(s): ".style(|s| s.font_weight(Weight::BOLD)), 94 | label(move || files.get()), 95 | )), 96 | )) 97 | .style(|s| { 98 | s.col_gap(5) 99 | .width_full() 100 | .height_full() 101 | .items_center() 102 | .justify_center() 103 | }) 104 | } 105 | 106 | fn display_files(file: FileInfo) -> String { 107 | let paths: Vec<&str> = file.path.iter().filter_map(|p| p.to_str()).collect(); 108 | paths.join("\n") 109 | } 110 | -------------------------------------------------------------------------------- /examples/files/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod files; 2 | pub use files::*; 3 | -------------------------------------------------------------------------------- /examples/files/src/main.rs: -------------------------------------------------------------------------------- 1 | pub mod files; 2 | 3 | fn main() { 4 | floem::launch(files::files_view); 5 | } 6 | -------------------------------------------------------------------------------- /examples/flight_booker/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flight_booker" 3 | edition = "2021" 4 | license.workspace = true 5 | version.workspace = true 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | floem = { path = "../.." } 11 | time = { version = "0.3.31", features = ["parsing", "macros"] } 12 | strum = { version = "0.25.0", features = ["derive"] } 13 | -------------------------------------------------------------------------------- /examples/flight_booker/README.md: -------------------------------------------------------------------------------- 1 | # Flight Booker 2 | 3 | This is an example that emulates an application that books flights, as 4 | described in [task 3][task3] of [7gui tasks][7gui]. 5 | 6 | > The focus of Flight Booker lies on modelling constraints between 7 | > widgets on the one hand and modelling constraints within a widget 8 | > on the other hand. Such constraints are very common in everyday 9 | > interactions with GUI applications. A good solution for Flight Booker 10 | > will make the constraints clear, succinct and explicit in the source 11 | > code and not hidden behind a lot of scaffolding. 12 | 13 | | Initial state | Invalid date format | Return date before start date | 14 | | ------- | ------- | ------- | 15 | | ![valid] | ![invalid] | ![return-disabled] | 16 | 17 | [task3]: https://eugenkiss.github.io/7guis/tasks/#flight 18 | [7gui]: https://eugenkiss.github.io/7guis/ 19 | 20 | [valid]: https://github.com/lapce/floem/assets/23398472/fe2758d3-7161-43a3-b059-8a4a1ce0c02e 21 | [invalid]: https://github.com/lapce/floem/assets/23398472/aeb843aa-520b-48f3-a39d-6acb414dba57 22 | [return-disabled]: https://github.com/lapce/floem/assets/23398472/8f1268f9-efbd-4a4d-9a47-7a50425e3e39 -------------------------------------------------------------------------------- /examples/flight_booker/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use floem::views::StackExt; 4 | use floem::{ 5 | peniko::color::palette, 6 | reactive::{create_rw_signal, RwSignal, SignalGet, SignalUpdate}, 7 | unit::UnitExt, 8 | views::{button, dyn_container, empty, text, text_input, v_stack, Decorators, RadioButton}, 9 | IntoView, 10 | }; 11 | use strum::IntoEnumIterator; 12 | use time::Date; 13 | 14 | fn oneway_message(start_text: String) -> String { 15 | format!("You have booked a one-way flight on {start_text}") 16 | } 17 | 18 | fn return_message(start_text: String, return_text: String) -> String { 19 | format!("You have booked a flight on {start_text} and a return flight on {return_text}",) 20 | } 21 | 22 | #[derive(Eq, PartialEq, Clone, Copy, strum::EnumIter)] 23 | enum FlightMode { 24 | OneWay, 25 | Return, 26 | } 27 | impl Display for FlightMode { 28 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 | match self { 30 | FlightMode::OneWay => f.write_str("One Way Flight"), 31 | FlightMode::Return => f.write_str("Return Flight"), 32 | } 33 | } 34 | } 35 | 36 | static DATE_FORMAT: &[time::format_description::FormatItem<'_>] = 37 | time::macros::format_description!("[day]-[month]-[year]"); 38 | 39 | pub fn app_view() -> impl IntoView { 40 | let flight_mode = RwSignal::new(FlightMode::OneWay); 41 | 42 | let start_text = create_rw_signal("24-02-2024".to_string()); 43 | let start_date = move || Date::parse(&start_text.get(), &DATE_FORMAT).ok(); 44 | let start_date_is_valid = move || start_date().is_some(); 45 | 46 | let return_text = create_rw_signal("24-02-2024".to_string()); 47 | let return_date = move || Date::parse(&return_text.get(), &DATE_FORMAT).ok(); 48 | let return_text_is_enabled = move || flight_mode.get() == FlightMode::Return; 49 | let return_date_is_valid = move || { 50 | if return_text_is_enabled() { 51 | return_date().is_some() 52 | } else { 53 | true 54 | } 55 | }; 56 | 57 | let dates_are_chronological = move || match flight_mode.get() { 58 | FlightMode::OneWay => true, 59 | FlightMode::Return => match (return_date(), start_date()) { 60 | (Some(ret), Some(start)) => ret >= start, 61 | _ => false, 62 | }, 63 | }; 64 | 65 | let did_booking = create_rw_signal(false); 66 | 67 | let mode_picker = FlightMode::iter() 68 | .map(move |fm| RadioButton::new_labeled_rw(fm, flight_mode, move || fm)) 69 | .h_stack(); 70 | 71 | let start_date_input = text_input(start_text) 72 | .placeholder("Start date") 73 | .style(move |s| s.apply_if(!start_date_is_valid(), |s| s.background(palette::css::RED))); 74 | let return_date_input = text_input(return_text) 75 | .placeholder("Return date") 76 | .style(move |s| s.apply_if(!return_date_is_valid(), |s| s.background(palette::css::RED))) 77 | .disabled(move || !return_text_is_enabled()); 78 | 79 | let book_button = button("Book") 80 | .disabled(move || { 81 | !(dates_are_chronological() && start_date_is_valid() && return_date_is_valid()) 82 | }) 83 | .action(move || did_booking.set(true)); 84 | 85 | let success_message = dyn_container( 86 | move || (did_booking.get(), flight_mode.get()), 87 | move |value| match value { 88 | (true, FlightMode::OneWay) => text(oneway_message(start_text.get())).into_any(), 89 | (true, FlightMode::Return) => { 90 | text(return_message(start_text.get(), return_text.get())).into_any() 91 | } 92 | (false, _) => empty().into_any(), 93 | }, 94 | ); 95 | 96 | v_stack(( 97 | mode_picker, 98 | start_date_input, 99 | return_date_input, 100 | book_button, 101 | success_message, 102 | )) 103 | .style(|s| s.row_gap(5)) 104 | .style(|s| { 105 | s.size(100.pct(), 100.pct()) 106 | .flex_col() 107 | .items_center() 108 | .justify_center() 109 | }) 110 | } 111 | 112 | fn main() { 113 | floem::launch(app_view); 114 | } 115 | -------------------------------------------------------------------------------- /examples/keyboard_handler/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "keyboard_handler" 3 | edition = "2021" 4 | license.workspace = true 5 | version.workspace = true 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | floem = { path = "../.." } 11 | -------------------------------------------------------------------------------- /examples/keyboard_handler/src/main.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | event::{Event, EventListener}, 3 | keyboard::Key, 4 | unit::UnitExt, 5 | views::{stack, text, Decorators}, 6 | IntoView, 7 | }; 8 | 9 | fn app_view() -> impl IntoView { 10 | let view = 11 | stack((text("Example: Keyboard event handler").style(|s| s.padding(10.0)),)).style(|s| { 12 | s.size(100.pct(), 100.pct()) 13 | .flex_col() 14 | .items_center() 15 | .justify_center() 16 | }); 17 | view.keyboard_navigable() 18 | .on_event_stop(EventListener::KeyDown, move |e| { 19 | if let Event::KeyDown(e) = e { 20 | if e.key.logical_key == Key::Character("q".into()) { 21 | println!("Goodbye :)"); 22 | std::process::exit(0) 23 | } 24 | println!("Key pressed in KeyCode: {:?}", e.key.physical_key); 25 | } 26 | }) 27 | } 28 | 29 | fn main() { 30 | floem::launch(app_view); 31 | } 32 | -------------------------------------------------------------------------------- /examples/layout/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "layout" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | im.workspace = true 8 | floem = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/layout/src/draggable_sidebar.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | event::EventListener, 3 | prelude::*, 4 | style::{CustomStylable, CustomStyle}, 5 | }; 6 | 7 | pub fn draggable_sidebar_view() -> impl IntoView { 8 | let side_bar = VirtualStack::with_view( 9 | || 0..100, 10 | move |item| { 11 | label(move || format!("Item {} with long lines", item)).style(move |s| { 12 | s.text_ellipsis() 13 | .height(22) 14 | .padding(10.0) 15 | .padding_top(3.0) 16 | .padding_bottom(3.0) 17 | .width_full() 18 | .items_start() 19 | .border_bottom(1.0) 20 | .border_color(Color::from_rgb8(205, 205, 205)) 21 | }) 22 | }, 23 | ) 24 | .style(move |s| s.flex_col().width_full()) 25 | .scroll() 26 | .style(move |s| { 27 | s.border_right(1.0) 28 | .border_top(1.0) 29 | .border_color(Color::from_rgb8(205, 205, 205)) 30 | }); 31 | 32 | let main_window = scroll( 33 | container( 34 | label(move || String::from("<-- drag me!\n \n(double click to return to default)")) 35 | .style(|s| s.padding(10.0)), 36 | ) 37 | .style(|s| s.flex_col().items_start().padding_bottom(10.0)), 38 | ) 39 | .style(|s| { 40 | s.flex_col() 41 | .flex_basis(0) 42 | .min_width(0) 43 | .flex_grow(1.0) 44 | .border_top(1.0) 45 | .border_color(Color::from_rgb8(205, 205, 205)) 46 | }); 47 | 48 | let dragger_color = Color::from_rgb8(205, 205, 205); 49 | let active_dragger_color = Color::from_rgb8(41, 98, 218); 50 | 51 | let view = resizable::resizable((side_bar, main_window)) 52 | .style(|s| s.width_full().height_full()) 53 | .custom_style(move |s| { 54 | s.handle_color(dragger_color) 55 | .active(|s| s.handle_color(active_dragger_color)) 56 | }); 57 | 58 | let id = view.id(); 59 | view.on_event_stop(EventListener::KeyUp, move |e| { 60 | if let floem::event::Event::KeyUp(e) = e { 61 | if e.key.logical_key == floem::keyboard::Key::Named(floem::keyboard::NamedKey::F11) { 62 | id.inspect(); 63 | } 64 | } 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /examples/layout/src/holy_grail.rs: -------------------------------------------------------------------------------- 1 | use floem::{event::EventListener, prelude::*, taffy::Position}; 2 | 3 | const SIDEBAR_WIDTH: f64 = 140.0; 4 | const TOPBAR_HEIGHT: f64 = 30.0; 5 | const SIDEBAR_ITEM_HEIGHT: f64 = 21.0; 6 | 7 | pub fn holy_grail_view() -> impl IntoView { 8 | let top_bar = label(|| String::from("Top bar")) 9 | .style(|s| s.padding(10.0).width_full().height(TOPBAR_HEIGHT)); 10 | 11 | let side_bar_right = VirtualStack::with_view( 12 | || 0..100, 13 | |item| { 14 | label(move || item.to_string()).style(move |s| { 15 | s.padding(10.0) 16 | .padding_top(3.0) 17 | .padding_bottom(3.0) 18 | .width(SIDEBAR_WIDTH) 19 | .height(SIDEBAR_ITEM_HEIGHT) 20 | .items_start() 21 | .border_bottom(1.0) 22 | .border_color(Color::from_rgb8(205, 205, 205)) 23 | }) 24 | }, 25 | ) 26 | .style(|s| s.flex_col().width(SIDEBAR_WIDTH - 1.0)) 27 | .scroll() 28 | .style(|s| { 29 | s.width(SIDEBAR_WIDTH) 30 | .border_left(1.0) 31 | .border_top(1.0) 32 | .border_color(Color::from_rgb8(205, 205, 205)) 33 | }); 34 | 35 | let side_bar_left = VirtualStack::with_view( 36 | || 0..100, 37 | move |item| { 38 | label(move || item.to_string()).style(move |s| { 39 | s.padding(10.0) 40 | .padding_top(3.0) 41 | .padding_bottom(3.0) 42 | .width(SIDEBAR_WIDTH) 43 | .height(SIDEBAR_ITEM_HEIGHT) 44 | .items_start() 45 | .border_bottom(1.0) 46 | .border_color(Color::from_rgb8(205, 205, 205)) 47 | }) 48 | }, 49 | ) 50 | .style(|s| s.flex_col().width(SIDEBAR_WIDTH - 1.0)) 51 | .scroll() 52 | .style(|s| { 53 | s.width(SIDEBAR_WIDTH) 54 | .border_right(1.0) 55 | .border_top(1.0) 56 | .border_color(Color::from_rgb8(205, 205, 205)) 57 | }); 58 | 59 | let main_window = "Hello world" 60 | .style(|s| s.padding(10.0)) 61 | .container() 62 | .style(|s| s.flex_col().items_start().padding_bottom(10.0)) 63 | .scroll() 64 | .style(|s| s.flex_col().flex_basis(0).min_width(0).flex_grow(1.0)) 65 | .style(|s| { 66 | s.border_top(1.0) 67 | .border_color(Color::from_rgb8(205, 205, 205)) 68 | .width_full() 69 | .min_width(150.0) 70 | }); 71 | 72 | let content = (side_bar_left, main_window, side_bar_right) 73 | .h_stack() 74 | .style(|s| { 75 | s.position(Position::Absolute) 76 | .inset_top(TOPBAR_HEIGHT) 77 | .inset_bottom(0.0) 78 | .width_full() 79 | }); 80 | 81 | (top_bar, content) 82 | .v_stack() 83 | .style(|s| s.width_full().height_full()) 84 | .on_event_stop(EventListener::KeyUp, move |e| { 85 | if let floem::event::Event::KeyUp(e) = e { 86 | if e.key.logical_key == floem::keyboard::Key::Named(floem::keyboard::NamedKey::F11) 87 | { 88 | floem::action::inspect(); 89 | } 90 | } 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /examples/layout/src/left_sidebar.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | event::EventListener, 3 | peniko::Color, 4 | reactive::{create_rw_signal, SignalGet}, 5 | style::Position, 6 | views::{container, h_stack, label, scroll, v_stack, virtual_stack, Decorators}, 7 | IntoView, View, 8 | }; 9 | 10 | const SIDEBAR_WIDTH: f64 = 140.0; 11 | const TOPBAR_HEIGHT: f64 = 30.0; 12 | const SIDEBAR_ITEM_HEIGHT: f64 = 21.0; 13 | 14 | pub fn left_sidebar_view() -> impl IntoView { 15 | let long_list: im::Vector = (0..100).collect(); 16 | let long_list = create_rw_signal(long_list); 17 | 18 | let top_bar = label(|| String::from("Top bar")) 19 | .style(|s| s.padding(10.0).width_full().height(TOPBAR_HEIGHT)); 20 | 21 | let side_bar = scroll({ 22 | virtual_stack( 23 | move || long_list.get(), 24 | move |item| *item, 25 | move |item| { 26 | label(move || item.to_string()).style(move |s| { 27 | s.padding(10.0) 28 | .padding_top(3.0) 29 | .padding_bottom(3.0) 30 | .width(SIDEBAR_WIDTH) 31 | .height(SIDEBAR_ITEM_HEIGHT) 32 | .items_start() 33 | .border_bottom(1.0) 34 | .border_color(Color::from_rgb8(205, 205, 205)) 35 | }) 36 | }, 37 | ) 38 | .style(|s| s.flex_col().width(SIDEBAR_WIDTH - 1.0)) 39 | }) 40 | .style(|s| { 41 | s.width(SIDEBAR_WIDTH) 42 | .border_right(1.0) 43 | .border_top(1.0) 44 | .border_color(Color::from_rgb8(205, 205, 205)) 45 | }); 46 | 47 | let main_window = scroll( 48 | container(label(move || String::from("Hello world")).style(|s| s.padding(10.0))) 49 | .style(|s| s.flex_col().items_start().padding_bottom(10.0)), 50 | ) 51 | .style(|s| { 52 | s.flex_col() 53 | .flex_basis(0) 54 | .min_width(0) 55 | .flex_grow(1.0) 56 | .border_top(1.0) 57 | .border_color(Color::from_rgb8(205, 205, 205)) 58 | }); 59 | 60 | let content = h_stack((side_bar, main_window)).style(|s| { 61 | s.position(Position::Absolute) 62 | .inset_top(TOPBAR_HEIGHT) 63 | .inset_bottom(0.0) 64 | .width_full() 65 | }); 66 | 67 | let view = v_stack((top_bar, content)).style(|s| s.width_full().height_full()); 68 | 69 | let id = view.id(); 70 | view.on_event_stop(EventListener::KeyUp, move |e| { 71 | if let floem::event::Event::KeyUp(e) = e { 72 | if e.key.logical_key == floem::keyboard::Key::Named(floem::keyboard::NamedKey::F11) { 73 | id.inspect(); 74 | } 75 | } 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /examples/layout/src/main.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | event::{Event, EventListener}, 3 | keyboard::{Key, NamedKey}, 4 | kurbo::Size, 5 | style::AlignContent, 6 | views::{button, container, h_stack, label, v_stack, Decorators}, 7 | window::{new_window, WindowConfig}, 8 | IntoView, View, 9 | }; 10 | 11 | pub mod draggable_sidebar; 12 | pub mod holy_grail; 13 | pub mod left_sidebar; 14 | pub mod right_sidebar; 15 | pub mod tab_navigation; 16 | 17 | fn list_item(name: String, view_fn: impl Fn() -> V) -> impl IntoView { 18 | h_stack(( 19 | label(move || name.clone()).style(|s| s), 20 | container(view_fn()).style(|s| s.width_full().justify_content(AlignContent::End)), 21 | )) 22 | .style(|s| s.width(200)) 23 | } 24 | 25 | fn app_view() -> impl IntoView { 26 | let view = v_stack(( 27 | label(move || String::from("Static layouts")) 28 | .style(|s| s.font_size(30.0).margin_bottom(15.0)), 29 | list_item(String::from("Left sidebar"), move || { 30 | button("Open").action(|| { 31 | new_window( 32 | |_| left_sidebar::left_sidebar_view(), 33 | Some( 34 | WindowConfig::default() 35 | .size(Size::new(700.0, 400.0)) 36 | .title("Left sidebar"), 37 | ), 38 | ); 39 | }) 40 | }), 41 | list_item(String::from("Right sidebar"), move || { 42 | button("Open").action(|| { 43 | new_window( 44 | |_| right_sidebar::right_sidebar_view(), 45 | Some( 46 | WindowConfig::default() 47 | .size(Size::new(700.0, 400.0)) 48 | .title("Right sidebar"), 49 | ), 50 | ); 51 | }) 52 | }), 53 | list_item(String::from("Holy grail"), move || { 54 | button("Open").action(|| { 55 | new_window( 56 | |_| holy_grail::holy_grail_view(), 57 | Some( 58 | WindowConfig::default() 59 | .size(Size::new(700.0, 400.0)) 60 | .title("Holy Grail"), 61 | ), 62 | ); 63 | }) 64 | }), 65 | label(move || String::from("Interactive layouts")) 66 | .style(|s| s.font_size(30.0).margin_top(15.0).margin_bottom(15.0)), 67 | list_item(String::from("Tab navigation"), move || { 68 | button("Open").action(|| { 69 | new_window( 70 | |_| tab_navigation::tab_navigation_view(), 71 | Some( 72 | WindowConfig::default() 73 | .size(Size::new(400.0, 250.0)) 74 | .title("Tab navigation"), 75 | ), 76 | ); 77 | }) 78 | }), 79 | list_item(String::from("Draggable sidebar"), move || { 80 | button("Open").action(|| { 81 | new_window( 82 | |_| draggable_sidebar::draggable_sidebar_view(), 83 | Some( 84 | WindowConfig::default() 85 | .size(Size::new(700.0, 400.0)) 86 | .title("Draggable sidebar"), 87 | ), 88 | ); 89 | }) 90 | }), 91 | )) 92 | .style(|s| { 93 | s.flex_col() 94 | .width_full() 95 | .height_full() 96 | .padding(10.0) 97 | .row_gap(10.0) 98 | }); 99 | 100 | let id = view.id(); 101 | view.on_event_stop(EventListener::KeyUp, move |e| { 102 | if let Event::KeyUp(e) = e { 103 | if e.key.logical_key == Key::Named(NamedKey::F11) { 104 | id.inspect(); 105 | } 106 | } 107 | }) 108 | .window_title(|| String::from("Layout examples")) 109 | } 110 | 111 | fn main() { 112 | floem::launch(app_view); 113 | } 114 | -------------------------------------------------------------------------------- /examples/layout/src/right_sidebar.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | event::EventListener, 3 | peniko::Color, 4 | reactive::{create_rw_signal, SignalGet}, 5 | style::Position, 6 | views::{container, h_stack, label, scroll, v_stack, virtual_stack, Decorators}, 7 | IntoView, View, 8 | }; 9 | 10 | const SIDEBAR_WIDTH: f64 = 140.0; 11 | const TOPBAR_HEIGHT: f64 = 30.0; 12 | const SIDEBAR_ITEM_HEIGHT: f64 = 21.0; 13 | 14 | pub fn right_sidebar_view() -> impl IntoView { 15 | let long_list: im::Vector = (0..100).collect(); 16 | let long_list = create_rw_signal(long_list); 17 | 18 | let top_bar = label(|| String::from("Top bar")) 19 | .style(|s| s.padding(10.0).width_full().height(TOPBAR_HEIGHT)); 20 | 21 | let side_bar = scroll({ 22 | virtual_stack( 23 | move || long_list.get(), 24 | move |item| *item, 25 | move |item| { 26 | label(move || item.to_string()).style(move |s| { 27 | s.padding(10.0) 28 | .padding_top(3.0) 29 | .padding_bottom(3.0) 30 | .width(SIDEBAR_WIDTH) 31 | .height(SIDEBAR_ITEM_HEIGHT) 32 | .items_start() 33 | .border_bottom(1.0) 34 | .border_color(Color::from_rgb8(205, 205, 205)) 35 | }) 36 | }, 37 | ) 38 | .style(|s| s.flex_col().width(SIDEBAR_WIDTH - 1.0)) 39 | }) 40 | .style(|s| { 41 | s.width(SIDEBAR_WIDTH) 42 | .border_left(1.0) 43 | .border_top(1.0) 44 | .border_color(Color::from_rgb8(205, 205, 205)) 45 | }); 46 | 47 | let main_window = scroll( 48 | container(label(move || String::from("Hello world")).style(|s| s.padding(10.0))) 49 | .style(|s| s.flex_col().items_start().padding_bottom(10.0)), 50 | ) 51 | .style(|s| { 52 | s.flex_col() 53 | .flex_basis(0) 54 | .min_width(0) 55 | .flex_grow(1.0) 56 | .border_top(1.0) 57 | .border_color(Color::from_rgb8(205, 205, 205)) 58 | }); 59 | 60 | let content = h_stack((main_window, side_bar)).style(|s| { 61 | s.position(Position::Absolute) 62 | .inset_top(TOPBAR_HEIGHT) 63 | .inset_bottom(0.0) 64 | .width_full() 65 | }); 66 | 67 | let view = v_stack((top_bar, content)).style(|s| s.width_full().height_full()); 68 | 69 | let id = view.id(); 70 | view.on_event_stop(EventListener::KeyUp, move |e| { 71 | if let floem::event::Event::KeyUp(e) = e { 72 | if e.key.logical_key == floem::keyboard::Key::Named(floem::keyboard::NamedKey::F11) { 73 | id.inspect(); 74 | } 75 | } 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /examples/layout/src/tab_navigation.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | event::EventListener, 3 | peniko::Color, 4 | reactive::{create_signal, ReadSignal, SignalGet, SignalUpdate, WriteSignal}, 5 | style::{CursorStyle, Position}, 6 | text::Weight, 7 | views::{container, h_stack, label, scroll, tab, v_stack, Decorators}, 8 | IntoView, View, 9 | }; 10 | 11 | #[derive(Clone, Copy, Eq, Hash, PartialEq)] 12 | enum Tab { 13 | General, 14 | Settings, 15 | Feedback, 16 | } 17 | 18 | impl std::fmt::Display for Tab { 19 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 20 | match *self { 21 | Tab::General => write!(f, "General"), 22 | Tab::Settings => write!(f, "Settings"), 23 | Tab::Feedback => write!(f, "Feedback"), 24 | } 25 | } 26 | } 27 | 28 | fn tab_button( 29 | this_tab: Tab, 30 | tabs: ReadSignal>, 31 | set_active_tab: WriteSignal, 32 | active_tab: ReadSignal, 33 | ) -> impl IntoView { 34 | label(move || this_tab) 35 | .keyboard_navigable() 36 | .on_click_stop(move |_| { 37 | set_active_tab.update(|v: &mut usize| { 38 | *v = tabs 39 | .get_untracked() 40 | .iter() 41 | .position(|it| *it == this_tab) 42 | .unwrap(); 43 | }); 44 | }) 45 | .style(move |s| { 46 | s.width(70) 47 | .hover(|s| s.font_weight(Weight::BOLD).cursor(CursorStyle::Pointer)) 48 | .apply_if( 49 | active_tab.get() 50 | == tabs 51 | .get_untracked() 52 | .iter() 53 | .position(|it| *it == this_tab) 54 | .unwrap(), 55 | |s| s.font_weight(Weight::BOLD), 56 | ) 57 | }) 58 | } 59 | 60 | const TABBAR_HEIGHT: f64 = 37.0; 61 | const CONTENT_PADDING: f64 = 10.0; 62 | 63 | pub fn tab_navigation_view() -> impl IntoView { 64 | let tabs = vec![Tab::General, Tab::Settings, Tab::Feedback] 65 | .into_iter() 66 | .collect::>(); 67 | let (tabs, _set_tabs) = create_signal(tabs); 68 | let (active_tab, set_active_tab) = create_signal(0); 69 | 70 | let tabs_bar = h_stack(( 71 | tab_button(Tab::General, tabs, set_active_tab, active_tab), 72 | tab_button(Tab::Settings, tabs, set_active_tab, active_tab), 73 | tab_button(Tab::Feedback, tabs, set_active_tab, active_tab), 74 | )) 75 | .style(|s| { 76 | s.flex_row() 77 | .width_full() 78 | .height(TABBAR_HEIGHT) 79 | .col_gap(5) 80 | .padding(CONTENT_PADDING) 81 | .border_bottom(1) 82 | .border_color(Color::from_rgb8(205, 205, 205)) 83 | }); 84 | 85 | let main_content = container( 86 | scroll( 87 | tab( 88 | move || active_tab.get(), 89 | move || tabs.get(), 90 | |it| *it, 91 | |it| container(label(move || format!("{}", it))), 92 | ) 93 | .style(|s| s.padding(CONTENT_PADDING).padding_bottom(10.0)), 94 | ) 95 | .style(|s| s.flex_col().flex_basis(0).min_width(0).flex_grow(1.0)), 96 | ) 97 | .style(|s| { 98 | s.position(Position::Absolute) 99 | .inset_top(TABBAR_HEIGHT) 100 | .inset_bottom(0.0) 101 | .width_full() 102 | }); 103 | 104 | let settings_view = v_stack((tabs_bar, main_content)).style(|s| s.width_full().height_full()); 105 | 106 | let id = settings_view.id(); 107 | settings_view.on_event_stop(EventListener::KeyUp, move |e| { 108 | if let floem::event::Event::KeyUp(e) = e { 109 | if e.key.logical_key == floem::keyboard::Key::Named(floem::keyboard::NamedKey::F11) { 110 | id.inspect(); 111 | } 112 | } 113 | }) 114 | } 115 | -------------------------------------------------------------------------------- /examples/responsive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "responsive" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | im.workspace = true 8 | floem = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/responsive/src/main.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | peniko::color::palette, 3 | reactive::{create_signal, SignalGet, SignalUpdate}, 4 | responsive::{range, ScreenSize}, 5 | style::TextOverflow, 6 | unit::UnitExt, 7 | views::{h_stack, label, stack, text, Decorators}, 8 | IntoView, 9 | }; 10 | 11 | fn app_view() -> impl IntoView { 12 | let (is_text_overflown, set_is_text_overflown) = create_signal(false); 13 | 14 | stack({ 15 | ( 16 | label(|| "Resize the window to see the magic").style(|s| { 17 | s.border(1.0) 18 | .border_radius(10.0) 19 | .padding(10.0) 20 | .margin_horiz(10.0) 21 | .responsive(ScreenSize::XS, |s| s.background(palette::css::CYAN)) 22 | .responsive(ScreenSize::SM, |s| s.background(palette::css::PURPLE)) 23 | .responsive(ScreenSize::MD, |s| s.background(palette::css::ORANGE)) 24 | .responsive(ScreenSize::LG, |s| s.background(palette::css::GREEN)) 25 | .responsive(ScreenSize::XL, |s| s.background(palette::css::PINK)) 26 | .responsive(ScreenSize::XXL, |s| s.background(palette::css::RED)) 27 | .responsive(range(ScreenSize::XS..ScreenSize::LG), |s| { 28 | s.width(90.0.pct()).max_width(500.0) 29 | }) 30 | .responsive( 31 | // equivalent to: range(ScreenSize::LG..) 32 | ScreenSize::LG | ScreenSize::XL | ScreenSize::XXL, 33 | |s| s.width(300.0), 34 | ) 35 | }), 36 | text( 37 | "Long text that will overflow on smaller screens since the available width is less", 38 | ) 39 | .on_text_overflow(move |is_overflown| { 40 | set_is_text_overflown.update(|overflown| *overflown = is_overflown); 41 | }) 42 | .style(move |s| { 43 | s.background(palette::css::DIM_GRAY) 44 | .padding(10.0) 45 | .color(palette::css::WHITE_SMOKE) 46 | .margin_top(30.) 47 | .width_pct(70.0) 48 | .font_size(20.0) 49 | .max_width(800.) 50 | .text_overflow(TextOverflow::Ellipsis) 51 | }), 52 | h_stack(( 53 | text("The text fits in the available width?:"), 54 | label(move || if is_text_overflown.get() { "No" } else { "Yes" }.to_string()) 55 | .style(move |s| { 56 | s.color(if is_text_overflown.get() { 57 | palette::css::RED 58 | } else { 59 | palette::css::GREEN 60 | }) 61 | .font_bold() 62 | }), 63 | )), 64 | ) 65 | }) 66 | .style(|s| { 67 | s.size(100.pct(), 100.pct()) 68 | .flex_col() 69 | .justify_center() 70 | .items_center() 71 | }) 72 | } 73 | 74 | fn main() { 75 | floem::launch(app_view); 76 | } 77 | -------------------------------------------------------------------------------- /examples/stacks/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stacks" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | im.workspace = true 8 | floem = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/stacks/README.md: -------------------------------------------------------------------------------- 1 | In this example you can see the four different types of stacks floem has and 2 | their differences. 3 | -------------------------------------------------------------------------------- /examples/stacks/src/dyn_stack.rs: -------------------------------------------------------------------------------- 1 | use floem::prelude::*; 2 | 3 | pub fn dyn_stack_view() -> impl IntoView { 4 | // With the dyn_stack you can change the stack at runtime by controlling 5 | // your stack with a signal. 6 | 7 | let long_list: im::Vector = (0..10).collect(); 8 | let long_list = RwSignal::new(long_list); 9 | 10 | let button = button("Add an item") 11 | .action(move || long_list.update(|list| list.push_back(list.len() as i32 + 1))); 12 | 13 | let stack = dyn_stack( 14 | move || long_list.get(), 15 | move |item| *item, 16 | move |item| item.style(|s| s.height(20).justify_center()), 17 | ) 18 | .style(|s| s.flex_col().width_full()) 19 | .scroll() 20 | .style(|s| s.width(100).height(200).border(1)); 21 | 22 | (button, stack) 23 | .h_stack() 24 | .style(|s| s.flex_col().row_gap(5).margin_top(10)) 25 | } 26 | -------------------------------------------------------------------------------- /examples/stacks/src/main.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | keyboard::{Key, NamedKey}, 3 | views::Decorators, 4 | IntoView, View, 5 | }; 6 | 7 | mod dyn_stack; 8 | mod stack; 9 | mod stack_from_iter; 10 | mod virtual_stack; 11 | 12 | fn app_view() -> impl IntoView { 13 | let view = ( 14 | ( 15 | "stack".style(|s| s.font_size(16.0)), 16 | "From signal: false", 17 | "From iter: false", 18 | "Renders off-screen: true", 19 | stack::stack_view(), 20 | ) 21 | .style(|s| s.flex_col().row_gap(5).width_pct(25.0)), 22 | ( 23 | "stack_from_iter".style(|s| s.font_size(16.0)), 24 | "From signal: false", 25 | "From iter: true", 26 | "Renders off-screen: true", 27 | stack_from_iter::stack_from_iter_view(), 28 | ) 29 | .style(|s| s.flex_col().row_gap(5).width_pct(25.0)), 30 | ( 31 | "dyn_stack".style(|s| s.font_size(16.0)), 32 | "From signal: true", 33 | "From iter: true", 34 | "Renders off-screen: true", 35 | dyn_stack::dyn_stack_view(), 36 | ) 37 | .style(|s| s.flex_col().row_gap(5).width_pct(25.0)), 38 | ( 39 | "virtual_stack".style(|s| s.font_size(16.0)), 40 | "From signal: true", 41 | "From iter: true", 42 | "Renders off-screen: false", 43 | virtual_stack::virtual_stack_view(), 44 | ) 45 | .style(|s| s.flex_col().row_gap(5).width_pct(25.0)), 46 | ) 47 | .style(|s| s.flex().margin(20).width_full().height_full().col_gap(10)) 48 | .into_view(); 49 | 50 | let id = view.id(); 51 | view.on_key_up( 52 | Key::Named(NamedKey::F11), 53 | |m| m.is_empty(), 54 | move |_| id.inspect(), 55 | ) 56 | } 57 | 58 | fn main() { 59 | floem::launch(app_view); 60 | } 61 | -------------------------------------------------------------------------------- /examples/stacks/src/stack.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | views::{stack, v_stack, Decorators}, 3 | IntoView, 4 | }; 5 | 6 | #[rustfmt::skip] 7 | pub fn stack_view() -> impl IntoView { 8 | // An example of the three different ways you can do a vertical stack 9 | 10 | // A stack just with a tuple as syntax sugar 11 | ( 12 | "Item 1", 13 | "Item 2", 14 | 15 | // The stack view which takes a tuple as an argument 16 | stack(( 17 | "Item 3", 18 | "Item 4", 19 | )).style(|s| s.flex_col().row_gap(5)), 20 | 21 | // The vertical stack view which has flex_col() built in 22 | v_stack(( 23 | "Item 5", 24 | "Item 6", 25 | )).style(|s| s.row_gap(5)), 26 | 27 | ) 28 | .style(|s| s.flex_col().gap( 5).margin_top(10)) 29 | } 30 | -------------------------------------------------------------------------------- /examples/stacks/src/stack_from_iter.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | views::{stack_from_iter, Decorators}, 3 | IntoView, 4 | }; 5 | 6 | pub fn stack_from_iter_view() -> impl IntoView { 7 | // You can also use v_stack_from_iter and h_stack_from_iter for built in 8 | // flex direction. 9 | 10 | let collection: Vec = (0..10).collect(); 11 | 12 | stack_from_iter(collection.iter().map(|val| format!("Item {}", val))) 13 | .style(|s| s.flex_col().row_gap(5).margin_top(10)) 14 | } 15 | -------------------------------------------------------------------------------- /examples/stacks/src/virtual_stack.rs: -------------------------------------------------------------------------------- 1 | use floem::prelude::*; 2 | 3 | pub fn virtual_stack_view() -> impl IntoView { 4 | // A virtual list is optimized to only render the views that are visible 5 | // making it ideal for large lists with a lot of views. 6 | 7 | let long_list: im::Vector = (0..1000000).collect(); 8 | let long_list = RwSignal::new(long_list); 9 | 10 | let button = button("Add an item") 11 | .action(move || long_list.update(|list| list.push_back(list.len() as i32 + 1))); 12 | 13 | let virtual_stack = VirtualStack::new(move || long_list.get()) 14 | .style(|s| s.flex_col().width_full()) 15 | .scroll() 16 | .style(|s| s.width(100).height(200).border(1)); 17 | 18 | (button, virtual_stack) 19 | .h_stack() 20 | .style(|s| s.flex_col().row_gap(5).margin_top(10)) 21 | } 22 | -------------------------------------------------------------------------------- /examples/syntax-editor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "syntax-editor" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | im.workspace = true 8 | floem = { path = "../..", features = ["editor"] } 9 | syntect = "5.2.0" 10 | lazy_static = "1.4.0" 11 | -------------------------------------------------------------------------------- /examples/themes/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "themes" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | floem = { path = "../.." } 8 | -------------------------------------------------------------------------------- /examples/timer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "timer" 3 | edition = "2021" 4 | license.workspace = true 5 | version.workspace = true 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | floem = { path = "../.." } 11 | -------------------------------------------------------------------------------- /examples/timer/README.md: -------------------------------------------------------------------------------- 1 | # Timer 2 | 3 | This is an example timer app, as described in 4 | [task 4][task4] of [7gui tasks][7gui]. 5 | 6 | > Timer deals with concurrency in the sense that a timer process 7 | > that updates the elapsed time runs concurrently to the user’s 8 | > interactions with the GUI application. This also means that the 9 | > solution to competing user and signal interactions is tested. The 10 | > fact that slider adjustments must be reflected immediately moreover 11 | > tests the responsiveness of the solution. A good solution will make 12 | > it clear that the signal is a timer tick and, as always, has not 13 | > much scaffolding. 14 | 15 | ![timer](https://github.com/lapce/floem/assets/23398472/b55dae4f-56fe-4e9f-a0ee-1898db048588) 16 | 17 | [task4]: https://eugenkiss.github.io/7guis/tasks/#timer 18 | [7gui]: https://eugenkiss.github.io/7guis/ 19 | -------------------------------------------------------------------------------- /examples/timer/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | use floem::{ 4 | action::exec_after, 5 | reactive::{ 6 | create_effect, create_rw_signal, DerivedRwSignal, SignalGet, SignalTrack, SignalUpdate, 7 | }, 8 | unit::{Pct, UnitExt}, 9 | views::{button, container, label, slider, stack, text, v_stack, Decorators}, 10 | IntoView, 11 | }; 12 | 13 | fn main() { 14 | floem::launch(app_view); 15 | } 16 | 17 | fn app_view() -> impl IntoView { 18 | // We take maximum duration as 100s for convenience so that 19 | // one percent represents one second. 20 | let target_duration = create_rw_signal(100.pct()); 21 | let duration_slider = thin_slider(target_duration); 22 | 23 | let elapsed_time = create_rw_signal(Duration::ZERO); 24 | let is_active = move || elapsed_time.get().as_secs_f64() < target_duration.get().0; 25 | 26 | let elapsed_time_label = label(move || { 27 | format!( 28 | "{:.1}s", 29 | if is_active() { 30 | elapsed_time.get().as_secs_f64() 31 | } else { 32 | target_duration.get().0 33 | } 34 | ) 35 | }); 36 | 37 | let tick = create_rw_signal(()); 38 | create_effect(move |_| { 39 | tick.track(); 40 | let before_exec = Instant::now(); 41 | 42 | exec_after(Duration::from_millis(100), move |_| { 43 | if is_active() { 44 | elapsed_time.update(|d| *d += before_exec.elapsed()); 45 | } 46 | tick.set(()); 47 | }); 48 | }); 49 | 50 | let progress = DerivedRwSignal::new( 51 | target_duration, 52 | move |val| Pct(elapsed_time.get().as_secs_f64() / val.0 * 100.), 53 | |val| *val, 54 | ); 55 | let elapsed_time_bar = gauge(progress); 56 | 57 | let reset_button = button("Reset").action(move || elapsed_time.set(Duration::ZERO)); 58 | 59 | let view = v_stack(( 60 | stack((text("Elapsed Time: "), elapsed_time_bar)).style(|s| s.justify_between()), 61 | elapsed_time_label, 62 | stack((text("Duration: "), duration_slider)).style(|s| s.justify_between()), 63 | reset_button, 64 | )) 65 | .style(|s| s.gap(5)); 66 | 67 | container(view).style(|s| { 68 | s.size(100.pct(), 100.pct()) 69 | .flex_col() 70 | .items_center() 71 | .justify_center() 72 | }) 73 | } 74 | 75 | /// A slider with a thin bar instead of the default thick bar. 76 | fn thin_slider( 77 | fill_percent: impl SignalGet + SignalUpdate + Copy + 'static, 78 | ) -> slider::Slider { 79 | slider::Slider::new_rw(fill_percent) 80 | .slider_style(|s| s.accent_bar_height(30.pct()).bar_height(30.pct())) 81 | .style(|s| s.width(200)) 82 | } 83 | 84 | /// A non-interactive slider that has been repurposed into a progress bar. 85 | fn gauge(fill_percent: impl SignalGet + 'static) -> slider::Slider { 86 | slider::Slider::new(move || fill_percent.get()) 87 | .disabled(|| true) 88 | .slider_style(|s| { 89 | s.handle_radius(0) 90 | .bar_radius(25.pct()) 91 | .accent_bar_radius(25.pct()) 92 | }) 93 | .style(|s| s.width(200)) 94 | } 95 | -------------------------------------------------------------------------------- /examples/todo-complex/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "todo-complex" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | confy = "0.6.1" 8 | floem = { path = "../..", features = [ 9 | "serde", 10 | "vello", 11 | ], default-features = false } 12 | im.workspace = true 13 | rusqlite = { version = "0.32.1", features = ["bundled"] } 14 | serde = { workspace = true } 15 | # serde = { version = "1.0.210", features = ["derive"] } 16 | -------------------------------------------------------------------------------- /examples/tokio-timer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tokio-timer" 3 | edition = "2021" 4 | license.workspace = true 5 | version.workspace = true 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | floem = { path = "../..", features = ["futures"] } 11 | tokio = { version = "1.39.2", features = ["time", "rt-multi-thread"] } 12 | tokio-stream = "0.1.15" 13 | -------------------------------------------------------------------------------- /examples/tokio-timer/README.md: -------------------------------------------------------------------------------- 1 | # Timer 2 | 3 | This is an example timer app, as described in 4 | [task 4][task4] of [7gui tasks][7gui]. 5 | 6 | > Timer deals with concurrency in the sense that a timer process 7 | > that updates the elapsed time runs concurrently to the user’s 8 | > interactions with the GUI application. This also means that the 9 | > solution to competing user and signal interactions is tested. The 10 | > fact that slider adjustments must be reflected immediately moreover 11 | > tests the responsiveness of the solution. A good solution will make 12 | > it clear that the signal is a timer tick and, as always, has not 13 | > much scaffolding. 14 | 15 | This examples shows how to integrate tokio streams with a Floem application 16 | 17 | ![timer](https://github.com/lapce/floem/assets/23398472/b55dae4f-56fe-4e9f-a0ee-1898db048588) 18 | 19 | [task4]: https://eugenkiss.github.io/7guis/tasks/#timer 20 | [7gui]: https://eugenkiss.github.io/7guis/ 21 | -------------------------------------------------------------------------------- /examples/tokio-timer/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use floem::ext_event::create_signal_from_stream; 4 | use floem::prelude::*; 5 | use floem::reactive::DerivedRwSignal; 6 | use floem::unit::Pct; 7 | use tokio::runtime::Runtime; 8 | use tokio::time::Instant; 9 | use tokio_stream::wrappers::IntervalStream; 10 | 11 | fn main() { 12 | // Multi threaded runtime is required because the main thread is not a real tokio task 13 | let runtime = Runtime::new().expect("Could not start tokio runtime"); 14 | 15 | // We must make it so that the main task is under the tokio runtime so that APIs like 16 | // tokio::spawn work 17 | runtime.block_on(async { tokio::task::block_in_place(|| floem::launch(app_view)) }) 18 | } 19 | 20 | fn app_view() -> impl IntoView { 21 | // We take maximum duration as 100s for convenience so that 22 | // one percent represents one second. 23 | let target_duration = create_rw_signal(100.pct()); 24 | let duration_slider = thin_slider(target_duration); 25 | 26 | let stream = IntervalStream::new(tokio::time::interval(Duration::from_millis(100))); 27 | let now = Instant::now(); 28 | let started_at = create_rw_signal(now); 29 | let current_instant = create_signal_from_stream(now, stream); 30 | let elapsed_time = move || current_instant.get().duration_since(started_at.get()); 31 | let is_active = move || elapsed_time().as_secs_f64() < target_duration.get().0; 32 | 33 | let elapsed_time_label = label(move || { 34 | format!( 35 | "{:.1}s", 36 | if is_active() { 37 | elapsed_time().as_secs_f64() 38 | } else { 39 | target_duration.get().0 40 | } 41 | ) 42 | }); 43 | 44 | let progress = DerivedRwSignal::new( 45 | target_duration, 46 | move |val| Pct(elapsed_time().as_secs_f64() / val.0 * 100.), 47 | |val| *val, 48 | ); 49 | let elapsed_time_bar = gauge(progress); 50 | 51 | let reset_button = button("Reset").action(move || started_at.set(Instant::now())); 52 | 53 | let view = v_stack(( 54 | stack((text("Elapsed Time: "), elapsed_time_bar)).style(|s| s.justify_between()), 55 | elapsed_time_label, 56 | stack((text("Duration: "), duration_slider)).style(|s| s.justify_between()), 57 | reset_button, 58 | )) 59 | .style(|s| s.gap(5)); 60 | 61 | container(view).style(|s| { 62 | s.size(100.pct(), 100.pct()) 63 | .flex_col() 64 | .items_center() 65 | .justify_center() 66 | }) 67 | } 68 | 69 | /// A slider with a thin bar instead of the default thick bar. 70 | fn thin_slider( 71 | fill_percent: impl SignalGet + SignalUpdate + Copy + 'static, 72 | ) -> slider::Slider { 73 | slider::Slider::new_rw(fill_percent) 74 | .slider_style(|s| s.accent_bar_height(30.pct()).bar_height(30.pct())) 75 | .style(|s| s.width(200)) 76 | } 77 | 78 | /// A non-interactive slider that has been repurposed into a progress bar. 79 | fn gauge(fill_percent: impl SignalGet + 'static) -> slider::Slider { 80 | slider::Slider::new(move || fill_percent.get()) 81 | .disabled(|| true) 82 | .slider_style(|s| { 83 | s.handle_radius(0) 84 | .bar_radius(25.pct()) 85 | .accent_bar_radius(25.pct()) 86 | }) 87 | .style(|s| s.width(200)) 88 | } 89 | -------------------------------------------------------------------------------- /examples/view-transition/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "view-transition" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | im.workspace = true 8 | floem = { path = "../..", features = ["vello"], default-features = false } 9 | -------------------------------------------------------------------------------- /examples/view-transition/src/main.rs: -------------------------------------------------------------------------------- 1 | use floem::{prelude::*, style::Style, taffy::FlexWrap, IntoView}; 2 | mod music_player; 3 | 4 | #[derive(Clone, Copy, PartialEq)] 5 | enum ViewSwitcher { 6 | One, 7 | Two, 8 | } 9 | impl ViewSwitcher { 10 | fn toggle(&mut self) { 11 | *self = match self { 12 | Self::One => Self::Two, 13 | Self::Two => Self::One, 14 | }; 15 | } 16 | 17 | fn view(self, state: RwSignal) -> impl IntoView { 18 | match self { 19 | Self::One => music_player::music_player().into_any(), 20 | Self::Two => view_two(state).into_any(), 21 | } 22 | .style(|s| s.scale(100.pct())) 23 | .animation(|a| a.scale_effect().keyframe(0, |s| s.style(|s| s.size(0, 0)))) 24 | .clip() 25 | .style(|s| s.padding(20)) 26 | .animation(|a| { 27 | a.view_transition() 28 | .keyframe(0, |f| f.style(|s| s.padding(0))) 29 | }) 30 | } 31 | } 32 | 33 | fn main() { 34 | floem::launch(app_view); 35 | } 36 | 37 | fn app_view() -> impl IntoView { 38 | let state = RwSignal::new(ViewSwitcher::One); 39 | 40 | v_stack(( 41 | button("Switch views").action(move || state.update(ViewSwitcher::toggle)), 42 | h_stack(( 43 | dyn_container(move || state.get(), move |which| which.view(state)), 44 | empty() 45 | .animation(move |a| { 46 | a.scale_effect() 47 | .with_duration(|a, d| a.delay(d)) 48 | .keyframe(0, |s| s.style(|s| s.size(0, 0))) 49 | }) 50 | .style(move |s| { 51 | s.size(100, 100) 52 | .scale(100.pct()) 53 | .border_radius(5) 54 | .background(palette::css::RED) 55 | .apply_if(state.get() == ViewSwitcher::Two, |s| s.hide()) 56 | .apply(box_shadow()) 57 | }), 58 | )) 59 | .style(|s| s.items_center().justify_center().flex_wrap(FlexWrap::Wrap)), 60 | )) 61 | .style(|s| { 62 | s.width_full() 63 | .height_full() 64 | .items_center() 65 | .justify_center() 66 | .gap(20) 67 | }) 68 | } 69 | 70 | fn view_two(view: RwSignal) -> impl IntoView { 71 | v_stack(( 72 | "Another view", 73 | button("Switch back").action(move || view.set(ViewSwitcher::One)), 74 | )) 75 | .style(|s| { 76 | s.row_gap(10.0) 77 | .size(150, 100) 78 | .items_center() 79 | .justify_center() 80 | .border(1.) 81 | .border_radius(5) 82 | }) 83 | } 84 | 85 | fn box_shadow() -> Style { 86 | Style::new() 87 | .box_shadow_color(palette::css::BLACK.with_alpha(0.5)) 88 | .box_shadow_h_offset(5.) 89 | .box_shadow_v_offset(10.) 90 | // .box_shadow_spread(1) 91 | .box_shadow_blur(1.5) 92 | } 93 | -------------------------------------------------------------------------------- /examples/view-transition/src/music_player/svg.rs: -------------------------------------------------------------------------------- 1 | // hero icons 2 | // MIT License 3 | 4 | // Copyright (c) Tailwind Labs, Inc. 5 | 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | // SOFTWARE. 23 | pub const PLAY: &str = r#" 24 | 25 | 26 | "#; 27 | pub const PAUSE: &str = r#" 28 | 29 | 30 | "#; 31 | pub const FORWARD: &str = r#" 32 | 33 | 34 | "#; 35 | pub const BACKWARD: &str = r#" 36 | 37 | 38 | "#; 39 | pub const MUSIC: &str = r#" 40 | 41 | 42 | "#; 43 | -------------------------------------------------------------------------------- /examples/virtual_list/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "virtual_list" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | im.workspace = true 8 | floem = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/virtual_list/README.md: -------------------------------------------------------------------------------- 1 | This example showcases Floem's ability to have 1 million fixed height items in a list. 2 | 3 | Behind the scenes, it is keeping track of the list items that are visible and keeping or adding the items that are in view and removing the view items that are out of view. 4 | 5 | The Floem `VirtualList` gives you a way to deal with extremely long lists in a performant way without manually doing the adding and removing of views from the view tree. 6 | -------------------------------------------------------------------------------- /examples/virtual_list/src/main.rs: -------------------------------------------------------------------------------- 1 | use floem::prelude::*; 2 | 3 | fn app_view() -> impl IntoView { 4 | VirtualStack::list_new(move || 1..=1000000) 5 | .style(|s| { 6 | s.flex_col().items_center().class(LabelClass, |s| { 7 | s.padding_vert(2.5).width_full().justify_center() 8 | }) 9 | }) 10 | .scroll() 11 | .style(|s| s.size_pct(50., 75.).border(1.0)) 12 | .container() 13 | .style(|s| { 14 | s.size(100.pct(), 100.pct()) 15 | .padding_vert(20.0) 16 | .flex_col() 17 | .items_center() 18 | .justify_center() 19 | }) 20 | } 21 | 22 | fn main() { 23 | floem::launch(app_view); 24 | } 25 | -------------------------------------------------------------------------------- /examples/webgpu/.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | -------------------------------------------------------------------------------- /examples/webgpu/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webgpu" 3 | edition = "2021" 4 | license.workspace = true 5 | version.workspace = true 6 | 7 | [dependencies] 8 | im.workspace = true 9 | floem = { path = "../.." } 10 | cosmic-text = { version = "0.12.1", features = ["shape-run-cache"] } 11 | 12 | [target.'cfg(target_arch = "wasm32")'.dependencies] 13 | console_error_panic_hook = "0.1.6" 14 | console_log = "1.0" 15 | wgpu = { version = "23.0.1" } 16 | wasm-bindgen = "0.2" 17 | wasm-bindgen-futures = "0.4.30" 18 | web-sys = { version = "0.3.69", features = ["Document", "Window", "Element"] } 19 | -------------------------------------------------------------------------------- /examples/webgpu/README.md: -------------------------------------------------------------------------------- 1 | # Floem on WebGPU 2 | 3 | **WARNING**: WebGPU support is highly experimental right now. Expect missing features, bugs, or performance issues. 4 | 5 | ## Requirements 6 | 7 | * [Trunk](https://trunkrs.dev/) 8 | * [Browser with WebGPU support](https://caniuse.com/webgpu) 9 | 10 | ## Run 11 | 12 | From this directory, run: 13 | 14 | ```sh 15 | trunk serve --open 16 | ``` 17 | 18 | ## Specifying the canvas element 19 | 20 | You must specify the ID of the HTML canvas element in the `WindowConfig` struct. 21 | 22 | ```rust 23 | let window_config = WindowConfig::default() 24 | .with_web_config(|w| w.canvas_id("the-canvas")); 25 | ``` 26 | 27 | The application will otherwise panic with the following message: 28 | 29 | ``` 30 | Specify an id for the canvas. 31 | ``` 32 | 33 | ## Resizing the canvas 34 | 35 | By default, the floem window should automatically resize to fit the HTML canvas. 36 | This is usually the desired behavior, as the canvas will thereby integrate with the rest of the web application. 37 | You can change this behavior by specifying an explicit `size` in the `WindowConfig`. 38 | 39 | ```rust 40 | let window_config = WindowConfig::default() 41 | .size(Size::new(800.0, 600.0)); 42 | ``` 43 | 44 | Then, the canvas will have a fixed size and will not resize automatically based on the HTML canvas size. 45 | 46 | ## Fonts 47 | 48 | This example comes with a selection of fonts in the `fonts` directory. 49 | For simplicity, these are embedded in the binary in this example. 50 | Without manually configuring fonts, cosmic-text won't find any fonts and will panic. 51 | At the time of this writing, the default fonts (precisely those in the `fonts` directory) are hardcoded in cosmic-text. 52 | -------------------------------------------------------------------------------- /examples/webgpu/fonts/DejaVuSerif.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapce/floem/fab1fe93c9405edaaf0b931ceec72a99248f2852/examples/webgpu/fonts/DejaVuSerif.ttf -------------------------------------------------------------------------------- /examples/webgpu/fonts/FiraMono-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapce/floem/fab1fe93c9405edaaf0b931ceec72a99248f2852/examples/webgpu/fonts/FiraMono-Medium.ttf -------------------------------------------------------------------------------- /examples/webgpu/fonts/FiraSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapce/floem/fab1fe93c9405edaaf0b931ceec72a99248f2852/examples/webgpu/fonts/FiraSans-Medium.ttf -------------------------------------------------------------------------------- /examples/webgpu/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | WebGPU Demo 9 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /examples/webgpu/src/lib.rs: -------------------------------------------------------------------------------- 1 | use floem::text::FONT_SYSTEM; 2 | use floem::window::WindowConfig; 3 | use floem::Application; 4 | use floem::{ 5 | reactive::{create_signal, SignalGet, SignalUpdate}, 6 | views::{label, ButtonClass, Decorators}, 7 | IntoView, 8 | }; 9 | 10 | #[cfg(target_arch = "wasm32")] 11 | use wasm_bindgen::prelude::*; 12 | 13 | const FIRA_MONO: &[u8] = include_bytes!("../fonts/FiraMono-Medium.ttf"); 14 | const FIRA_SANS: &[u8] = include_bytes!("../fonts/FiraSans-Medium.ttf"); 15 | const DEJAVU_SERIF: &[u8] = include_bytes!("../fonts/DejaVuSerif.ttf"); 16 | 17 | pub fn app_view() -> impl IntoView { 18 | // Create a reactive signal with a counter value, defaulting to 0 19 | let (counter, set_counter) = create_signal(0); 20 | 21 | // Create a vertical layout 22 | ( 23 | // The counter value updates automatically, thanks to reactivity 24 | label(move || format!("Value: {}", counter.get())), 25 | // Create a horizontal layout 26 | ( 27 | "Increment".class(ButtonClass).on_click_stop(move |_| { 28 | set_counter.update(|value| *value += 1); 29 | }), 30 | "Decrement".class(ButtonClass).on_click_stop(move |_| { 31 | set_counter.update(|value| *value -= 1); 32 | }), 33 | ), 34 | ) 35 | .style(|s| s.flex_col()) 36 | } 37 | 38 | #[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))] 39 | pub fn run() { 40 | #[cfg(target_family = "wasm")] 41 | console_error_panic_hook::set_once(); 42 | 43 | { 44 | let mut font_system = FONT_SYSTEM.lock(); 45 | let font_db = font_system.db_mut(); 46 | font_db.load_font_data(Vec::from(FIRA_MONO)); 47 | font_db.load_font_data(Vec::from(FIRA_SANS)); 48 | font_db.load_font_data(Vec::from(DEJAVU_SERIF)); 49 | } 50 | 51 | let window_config = WindowConfig::default().with_web_config(|w| w.canvas_id("the-canvas")); 52 | 53 | Application::new() 54 | .window(move |_| app_view(), Some(window_config)) 55 | .run() 56 | } 57 | -------------------------------------------------------------------------------- /examples/webgpu/src/main.rs: -------------------------------------------------------------------------------- 1 | use webgpu::run; 2 | 3 | fn main() { 4 | run(); 5 | } 6 | -------------------------------------------------------------------------------- /examples/widget-gallery/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "widget-gallery" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | im.workspace = true 8 | floem = { path = "../..", features = ["rfd-async-std"] } 9 | strum = { version = "0.25.0", features = ["derive"] } 10 | files = { path = "../files/" } 11 | 12 | [features] 13 | vello = ["floem/vello"] 14 | -------------------------------------------------------------------------------- /examples/widget-gallery/assets/ferris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapce/floem/fab1fe93c9405edaaf0b931ceec72a99248f2852/examples/widget-gallery/assets/ferris.png -------------------------------------------------------------------------------- /examples/widget-gallery/assets/sunflower.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapce/floem/fab1fe93c9405edaaf0b931ceec72a99248f2852/examples/widget-gallery/assets/sunflower.jpg -------------------------------------------------------------------------------- /examples/widget-gallery/src/animation.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | animate::Animation, 3 | event::EventListener as EL, 4 | peniko::color::palette, 5 | reactive::{RwSignal, SignalGet, Trigger}, 6 | unit::DurationUnitExt, 7 | views::{empty, h_stack, Decorators}, 8 | IntoView, 9 | }; 10 | 11 | pub fn animation_view() -> impl IntoView { 12 | let animation = RwSignal::new( 13 | Animation::new() 14 | .duration(5.seconds()) 15 | .keyframe(0, |f| f.computed_style()) 16 | .keyframe(50, |f| { 17 | f.style(|s| s.background(palette::css::BLACK).size(30, 30)) 18 | .ease_in() 19 | }) 20 | .keyframe(100, |f| { 21 | f.style(|s| s.background(palette::css::AQUAMARINE).size(10, 300)) 22 | .ease_out() 23 | }) 24 | .repeat(true) 25 | .auto_reverse(true), 26 | ); 27 | 28 | let pause = Trigger::new(); 29 | let resume = Trigger::new(); 30 | 31 | h_stack(( 32 | empty() 33 | .style(|s| s.background(palette::css::RED).size(500, 100)) 34 | .animation(move |_| animation.get().duration(10.seconds())), 35 | empty() 36 | .style(|s| { 37 | s.background(palette::css::BLUE) 38 | .size(50, 100) 39 | .border(5.) 40 | .border_color(palette::css::GREEN) 41 | }) 42 | .animation(move |_| animation.get()) 43 | .animation(move |a| { 44 | a.keyframe(0, |f| f.computed_style()) 45 | .keyframe(100, |f| { 46 | f.style(|s| s.border(5.).border_color(palette::css::PURPLE)) 47 | }) 48 | .duration(5.seconds()) 49 | .repeat(true) 50 | .auto_reverse(true) 51 | }), 52 | empty() 53 | .style(|s| s.background(palette::css::GREEN).size(100, 300)) 54 | .animation(move |_| { 55 | animation 56 | .get() 57 | .pause(move || pause.track()) 58 | .resume(move || resume.track()) 59 | .delay(3.seconds()) 60 | }) 61 | .on_event_stop(EL::PointerEnter, move |_| { 62 | pause.notify(); 63 | }) 64 | .on_event_stop(EL::PointerLeave, move |_| { 65 | resume.notify(); 66 | }), 67 | )) 68 | .style(|s| s.size_full().gap(10).items_center().justify_center()) 69 | } 70 | -------------------------------------------------------------------------------- /examples/widget-gallery/src/buttons.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | peniko::color::palette, 3 | peniko::Color, 4 | style::CursorStyle, 5 | views::{button, toggle_button, Decorators, ToggleHandleBehavior}, 6 | IntoView, 7 | }; 8 | 9 | use crate::form::{form, form_item}; 10 | 11 | pub fn button_view() -> impl IntoView { 12 | form(( 13 | form_item( 14 | "Basic Button:", 15 | button("Click me").action(|| println!("Button clicked")), 16 | ), 17 | form_item( 18 | "Styled Button:", 19 | button("Click me") 20 | .action(|| println!("Button clicked")) 21 | .style(|s| { 22 | s.border(1.0) 23 | .border_radius(10.0) 24 | .padding(10.0) 25 | .background(palette::css::YELLOW_GREEN) 26 | .color(palette::css::DARK_GREEN) 27 | .cursor(CursorStyle::Pointer) 28 | .active(|s| s.color(palette::css::WHITE).background(palette::css::RED)) 29 | .hover(|s| s.background(Color::from_rgb8(244, 67, 54))) 30 | .focus_visible(|s| s.border(2.).border_color(palette::css::BLUE)) 31 | }), 32 | ), 33 | form_item( 34 | "Disabled Button:", 35 | button("Click me") 36 | .disabled(|| true) 37 | .action(|| println!("Button clicked")), 38 | ), 39 | form_item( 40 | "Secondary click button:", 41 | button("Right click me").on_secondary_click_stop(|_| { 42 | println!("Secondary mouse button click."); 43 | }), 44 | ), 45 | form_item( 46 | "Toggle button - Snap:", 47 | toggle_button(|| true) 48 | .on_toggle(|_| { 49 | println!("Button Toggled"); 50 | }) 51 | .toggle_style(|s| s.behavior(ToggleHandleBehavior::Snap)), 52 | ), 53 | form_item( 54 | "Toggle button - Follow:", 55 | toggle_button(|| true) 56 | .on_toggle(|_| { 57 | println!("Button Toggled"); 58 | }) 59 | .toggle_style(|s| s.behavior(ToggleHandleBehavior::Follow)), 60 | ), 61 | )) 62 | } 63 | -------------------------------------------------------------------------------- /examples/widget-gallery/src/checkbox.rs: -------------------------------------------------------------------------------- 1 | use floem::prelude::*; 2 | 3 | use crate::form::{form, form_item}; 4 | 5 | // Source: https://www.svgrepo.com/svg/509804/check | License: MIT 6 | const CUSTOM_CHECK_SVG: &str = r##" 7 | 8 | 9 | 10 | "##; 11 | 12 | // Source: https://www.svgrepo.com/svg/505349/cross | License: MIT 13 | pub const CROSS_SVG: &str = r##" 14 | 15 | 16 | 17 | "##; 18 | 19 | pub fn checkbox_view() -> impl IntoView { 20 | // let width = 160.0; 21 | let is_checked = RwSignal::new(true); 22 | form(( 23 | form_item("Checkbox:", Checkbox::new_rw(is_checked)), 24 | form_item( 25 | "Custom Checkbox 1:", 26 | Checkbox::new_rw_custom(is_checked, CUSTOM_CHECK_SVG) 27 | .style(|s| s.color(palette::css::GREEN)), 28 | ), 29 | form_item( 30 | "Disabled Checkbox:", 31 | checkbox(move || is_checked.get()).disabled(|| true), 32 | ), 33 | form_item( 34 | "Labeled Checkbox:", 35 | Checkbox::labeled_rw(is_checked, || "Check me!"), 36 | ), 37 | form_item( 38 | "Custom Checkbox 2:", 39 | Checkbox::custom_labeled_rw(is_checked, move || "Custom Check Mark", CROSS_SVG) 40 | .style(|s| s.class(CheckboxClass, |s| s.color(palette::css::RED))), 41 | ), 42 | form_item( 43 | "Disabled Labeled Checkbox:", 44 | labeled_checkbox(move || is_checked.get(), || "Check me!").disabled(|| true), 45 | ), 46 | )) 47 | } 48 | -------------------------------------------------------------------------------- /examples/widget-gallery/src/clipboard.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | reactive::{create_rw_signal, SignalGet, SignalUpdate}, 3 | views::{button, h_stack, label, text_input, v_stack, Decorators}, 4 | Clipboard, IntoView, 5 | }; 6 | 7 | use crate::form::{form, form_item}; 8 | 9 | pub fn clipboard_view() -> impl IntoView { 10 | let text1 = create_rw_signal("".to_string()); 11 | let text2 = create_rw_signal("-".to_string()); 12 | 13 | form(( 14 | form_item( 15 | "Simple copy", 16 | button("Copy the answer").action(move || { 17 | let _ = Clipboard::set_contents("42".to_string()); 18 | }), 19 | ), 20 | form_item( 21 | "Copy from input", 22 | h_stack(( 23 | text_input(text1).keyboard_navigable(), 24 | button("Copy").action(move || { 25 | let _ = Clipboard::set_contents(text1.get()); 26 | }), 27 | )), 28 | ), 29 | form_item( 30 | "Get clipboard", 31 | v_stack(( 32 | button("Get clipboard").action(move || { 33 | if let Ok(content) = Clipboard::get_contents() { 34 | text2.set(content); 35 | } 36 | }), 37 | label(move || text2.get()), 38 | )), 39 | ), 40 | )) 41 | } 42 | -------------------------------------------------------------------------------- /examples/widget-gallery/src/context_menu.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | menu::{Menu, MenuItem}, 3 | views::{label, stack, Decorators}, 4 | IntoView, 5 | }; 6 | 7 | pub fn menu_view() -> impl IntoView { 8 | stack({ 9 | ( 10 | label(|| "Click me (Popout menu)") 11 | .style(|s| s.padding(10.0).margin_bottom(10.0).border(1.0)) 12 | .popout_menu(|| { 13 | Menu::new("") 14 | .entry(MenuItem::new("I am a menu item!")) 15 | .separator() 16 | .entry(MenuItem::new("I am another menu item")) 17 | }), 18 | label(|| "Right click me (Context menu)") 19 | .style(|s| s.padding(10.0).border(1.0)) 20 | .context_menu(|| { 21 | Menu::new("") 22 | .entry( 23 | Menu::new("Sub Menu").entry(MenuItem::new("item 2").action(|| { 24 | println!("sub menu item 2"); 25 | })), 26 | ) 27 | .entry( 28 | MenuItem::new("Menu item with something on the\tright").action(|| { 29 | println!("menu item with something on the right"); 30 | }), 31 | ) 32 | }), 33 | ) 34 | }) 35 | .style(|s| s.flex_col()) 36 | } 37 | -------------------------------------------------------------------------------- /examples/widget-gallery/src/draggable.rs: -------------------------------------------------------------------------------- 1 | use floem::{prelude::*, style::CursorStyle}; 2 | 3 | fn sortable_item( 4 | name: &str, 5 | sortable_items: RwSignal>, 6 | dragger_id: RwSignal, 7 | item_id: usize, 8 | ) -> impl IntoView { 9 | let name = String::from(name); 10 | let colors = [ 11 | palette::css::WHITE, 12 | palette::css::BEIGE, 13 | palette::css::REBECCA_PURPLE, 14 | palette::css::TEAL, 15 | palette::css::PALE_GREEN, 16 | palette::css::YELLOW, 17 | palette::css::DODGER_BLUE, 18 | palette::css::KHAKI, 19 | palette::css::WHEAT, 20 | palette::css::DARK_SALMON, 21 | palette::css::HOT_PINK, 22 | ]; 23 | 24 | ( 25 | label(move || format!("Selectable item {name}")) 26 | .style(|s| s.padding(5).width_full()) 27 | .on_event_stop( 28 | floem::event::EventListener::PointerDown, 29 | |_| { /* Disable dragging for this view */ }, 30 | ), 31 | "drag me".style(|s| { 32 | s.selectable(false) 33 | .padding(2) 34 | .cursor(CursorStyle::RowResize) 35 | }), 36 | ) 37 | .draggable() 38 | .on_event(floem::event::EventListener::DragStart, move |_| { 39 | dragger_id.set(item_id); 40 | floem::event::EventPropagation::Continue 41 | }) 42 | .on_event(floem::event::EventListener::DragOver, move |_| { 43 | if dragger_id.get_untracked() != item_id { 44 | let dragger_pos = sortable_items 45 | .get() 46 | .iter() 47 | .position(|id| *id == dragger_id.get_untracked()) 48 | .unwrap(); 49 | let hover_pos = sortable_items 50 | .get() 51 | .iter() 52 | .position(|id| *id == item_id) 53 | .unwrap(); 54 | 55 | sortable_items.update(|items| { 56 | items.remove(dragger_pos); 57 | items.insert(hover_pos, dragger_id.get_untracked()); 58 | }); 59 | } 60 | floem::event::EventPropagation::Continue 61 | }) 62 | .dragging_style(|s| { 63 | s.box_shadow_blur(3) 64 | .box_shadow_color(Color::from_rgb8(100, 100, 100)) 65 | .box_shadow_spread(2) 66 | }) 67 | .style(move |s| { 68 | s.background(colors[item_id]) 69 | .selectable(false) 70 | .col_gap(5) 71 | .items_center() 72 | .border(2) 73 | .border_color(palette::css::RED) 74 | }) 75 | } 76 | 77 | pub fn draggable_view() -> impl IntoView { 78 | let items = [ 79 | "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", 80 | ]; 81 | let sortable_items = RwSignal::new((0..items.len()).collect::>()); 82 | let dragger_id = RwSignal::new(0); 83 | 84 | dyn_stack( 85 | move || sortable_items.get(), 86 | move |item_id| *item_id, 87 | move |item_id| sortable_item(items[item_id], sortable_items, dragger_id, item_id), 88 | ) 89 | .style(|s| s.flex_col().row_gap(5).padding(10)) 90 | .into_view() 91 | } 92 | -------------------------------------------------------------------------------- /examples/widget-gallery/src/dropdown.rs: -------------------------------------------------------------------------------- 1 | use dropdown::Dropdown; 2 | use strum::IntoEnumIterator; 3 | 4 | use floem::{prelude::*, reactive::create_effect}; 5 | 6 | use crate::form::{self, form_item}; 7 | 8 | #[derive(strum::EnumIter, Debug, PartialEq, Clone, Copy)] 9 | enum Values { 10 | One, 11 | Two, 12 | Three, 13 | Four, 14 | Five, 15 | } 16 | impl std::fmt::Display for Values { 17 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 18 | f.write_fmt(format_args!("{:?}", self)) 19 | } 20 | } 21 | 22 | pub fn dropdown_view() -> impl IntoView { 23 | let dropdown_active_item = RwSignal::new(Values::Three); 24 | 25 | create_effect(move |_| { 26 | let active_item = dropdown_active_item.get(); 27 | println!("Selected: {active_item}"); 28 | }); 29 | 30 | form::form((form_item( 31 | "Dropdown", 32 | Dropdown::new_rw(dropdown_active_item, Values::iter()), 33 | ),)) 34 | } 35 | -------------------------------------------------------------------------------- /examples/widget-gallery/src/dropped_file.rs: -------------------------------------------------------------------------------- 1 | use crate::form::{form, form_item}; 2 | use floem::{ 3 | event::{Event, EventListener}, 4 | keyboard::{Key, NamedKey}, 5 | prelude::*, 6 | }; 7 | 8 | pub fn dropped_file_view() -> impl IntoView { 9 | let filename = RwSignal::new("".to_string()); 10 | 11 | let dropped_view = dyn_view(move || "dropped file".to_string()) 12 | .style(|s| { 13 | s.size(200.0, 50.0) 14 | .background(palette::css::GRAY) 15 | .border(5.) 16 | .border_color(palette::css::BLACK) 17 | .flex_col() 18 | .items_center() 19 | .justify_center() 20 | }) 21 | .on_key_up( 22 | Key::Named(NamedKey::F11), 23 | |m| m.is_empty(), 24 | move |_| floem::action::inspect(), 25 | ) 26 | .on_event_stop(EventListener::DroppedFile, move |e| { 27 | if let Event::DroppedFile(e) = e { 28 | println!("DroppedFile {:?}", e); 29 | filename.set(format!("{:?}", e.path)); 30 | } 31 | }); 32 | 33 | form(( 34 | form_item("File:", label(move || filename.get())), 35 | form_item("", dropped_view), 36 | )) 37 | } 38 | -------------------------------------------------------------------------------- /examples/widget-gallery/src/form.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | prelude::*, 3 | taffy::prelude::{auto, fr}, 4 | text::Weight, 5 | view_tuple::ViewTupleFlat, 6 | }; 7 | 8 | pub fn form(children: VTF) -> impl IntoView { 9 | children 10 | .flatten() 11 | .style(|s| { 12 | s.grid() 13 | .grid_template_columns([auto(), fr(1.)]) 14 | .justify_center() 15 | .items_center() 16 | .row_gap(20) 17 | .col_gap(10) 18 | .padding(30) 19 | }) 20 | .debug_name("Form") 21 | } 22 | 23 | pub fn form_item(item_label: impl IntoView, view: V) -> Vec> { 24 | let label_view = item_label.style(|s| s.font_weight(Weight::BOLD)); 25 | 26 | (label_view, view).into_views() 27 | } 28 | -------------------------------------------------------------------------------- /examples/widget-gallery/src/images.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | prelude::*, 3 | style::{StyleValue, TextColor}, 4 | }; 5 | 6 | use crate::form::{form, form_item}; 7 | 8 | pub fn img_view() -> impl IntoView { 9 | let ferris_png = include_bytes!("./../assets/ferris.png"); 10 | let ferris_svg = include_str!("./../assets/ferris.svg"); 11 | let svg_str = r##" 12 | 13 | "##; 14 | let sunflower = include_bytes!("./../assets/sunflower.jpg"); 15 | 16 | form(( 17 | form_item( 18 | "PNG:", 19 | img(move || ferris_png.to_vec()).style(|s| s.aspect_ratio(1.5)), 20 | ), 21 | form_item( 22 | "PNG(resized):", 23 | img(move || ferris_png.to_vec()).style(|s| s.width(230.px()).height(153.px())), 24 | ), 25 | form_item( 26 | "SVG(from file):", 27 | svg(ferris_svg).style(|s| { 28 | s.set_style_value(TextColor, StyleValue::Unset) 29 | .width(230.px()) 30 | .height(153.px()) 31 | }), 32 | ), 33 | form_item( 34 | "SVG(from string):", 35 | svg(svg_str).style(|s| s.width(100.px()).height(100.px())), 36 | ), 37 | form_item("JPG:", img(move || sunflower.to_vec())), 38 | form_item( 39 | "JPG(resized):", 40 | img(move || sunflower.to_vec()).style(|s| s.width(320.px()).height(490.px())), 41 | ), 42 | //TODO: support percentages for width/height 43 | // img(move || ferris_png.to_vec()).style(|s| s.width(90.pct()).height(90.pct())) 44 | // 45 | //TODO: object fit and object position 46 | // img(move || ferris_png.to_vec()) 47 | // .object_fit(ObjectFit::Contain).object_position(VertPosition::Top, HorizPosition::Left)) 48 | // 49 | )) 50 | } 51 | -------------------------------------------------------------------------------- /examples/widget-gallery/src/inputs.rs: -------------------------------------------------------------------------------- 1 | use floem::{prelude::*, style::SelectionCornerRadius, text::Weight}; 2 | 3 | use crate::form::{form, form_item}; 4 | 5 | pub fn text_input_view() -> impl IntoView { 6 | let text = RwSignal::new("".to_string()); 7 | 8 | const LIGHT_GRAY_224: Color = Color::from_rgb8(224, 224, 224); 9 | const MEDIUM_GRAY_189: Color = Color::from_rgb8(189, 189, 189); 10 | const DARK_GRAY_66: Color = Color::from_rgb8(66, 66, 66); 11 | const SKY_BLUE: Color = palette::css::LIGHT_SKY_BLUE; 12 | 13 | const LIGHT_GRAY_BG: Color = LIGHT_GRAY_224.with_alpha(0.1); 14 | const LIGHT_GRAY_BG_HOVER: Color = LIGHT_GRAY_224.with_alpha(0.2); 15 | const SKY_BLUE_FOCUS: Color = SKY_BLUE.with_alpha(0.8); 16 | 17 | form(( 18 | form_item( 19 | "Simple Input:", 20 | text_input(text) 21 | .placeholder("Placeholder text") 22 | .style(|s| s.width(250.)) 23 | .keyboard_navigable(), 24 | ), 25 | form_item( 26 | "Styled Input:", 27 | text_input(text) 28 | .placeholder("Placeholder text") 29 | .style(|s| { 30 | s.border(1.5) 31 | .width(250.0) 32 | .background(LIGHT_GRAY_BG) 33 | .border_radius(15.0) 34 | .border_color(MEDIUM_GRAY_189) 35 | .padding(10.0) 36 | .hover(|s| s.background(LIGHT_GRAY_BG_HOVER).border_color(DARK_GRAY_66)) 37 | .set(SelectionCornerRadius, 4.0) 38 | .focus(|s| { 39 | s.border_color(SKY_BLUE_FOCUS) 40 | .hover(|s| s.border_color(SKY_BLUE)) 41 | }) 42 | .class(PlaceholderTextClass, |s| { 43 | s.color(SKY_BLUE) 44 | .font_style(floem::text::Style::Italic) 45 | .font_weight(Weight::BOLD) 46 | }) 47 | .font_family("monospace".to_owned()) 48 | }) 49 | .keyboard_navigable(), 50 | ), 51 | form_item("Disabled Input:", text_input(text).disabled(|| true)), 52 | )) 53 | } 54 | -------------------------------------------------------------------------------- /examples/widget-gallery/src/labels.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | prelude::*, 3 | text::{Style as FontStyle, Weight}, 4 | }; 5 | 6 | use crate::form::{form, form_item}; 7 | 8 | pub fn label_view() -> impl IntoView { 9 | form(( 10 | form_item( 11 | "Simple Label:", 12 | "This is a simple label with a tooltip.\n(hover over me)" 13 | .tooltip(|| "This is a tooltip for the label."), 14 | ), 15 | form_item( 16 | "Styled Label:", 17 | "This is a styled label".style(|s| { 18 | s.background(palette::css::YELLOW) 19 | .padding(10.0) 20 | .color(palette::css::GREEN) 21 | .font_weight(Weight::BOLD) 22 | .font_style(FontStyle::Italic) 23 | .font_size(24.0) 24 | }), 25 | ), 26 | )) 27 | } 28 | -------------------------------------------------------------------------------- /examples/widget-gallery/src/radio_buttons.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use floem::{prelude::*, style_class}; 4 | use strum::IntoEnumIterator; 5 | 6 | use crate::form::{form, form_item}; 7 | 8 | #[derive(PartialEq, Eq, Clone, Copy, strum::EnumIter)] 9 | enum OperatingSystem { 10 | Windows, 11 | MacOS, 12 | Linux, 13 | } 14 | 15 | impl Display for OperatingSystem { 16 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 17 | match *self { 18 | OperatingSystem::Windows => write!(f, "Windows"), 19 | OperatingSystem::MacOS => write!(f, "macOS"), 20 | OperatingSystem::Linux => write!(f, "Linux"), 21 | } 22 | } 23 | } 24 | 25 | style_class!(RadioButtonGroupClass); 26 | 27 | pub fn radio_buttons_view() -> impl IntoView { 28 | // let width = 160.0; 29 | let operating_system = RwSignal::new(OperatingSystem::Windows); 30 | form(( 31 | form_item( 32 | "Radio Buttons:", 33 | OperatingSystem::iter() 34 | .map(move |os| RadioButton::new_rw(os, operating_system)) 35 | .v_stack() 36 | .class(RadioButtonGroupClass), 37 | ), 38 | form_item( 39 | "Disabled Radio Buttons:", 40 | OperatingSystem::iter() 41 | .map(move |os| RadioButton::new_get(os, operating_system).disabled(|| true)) 42 | .v_stack() 43 | .class(RadioButtonGroupClass), 44 | ), 45 | form_item( 46 | "Labelled Radio Buttons:", 47 | OperatingSystem::iter() 48 | .map(move |os| RadioButton::new_labeled_rw(os, operating_system, move || os)) 49 | .v_stack() 50 | .class(RadioButtonGroupClass), 51 | ), 52 | form_item( 53 | "Disabled Labelled Radio Buttons:", 54 | OperatingSystem::iter() 55 | .map(move |os| { 56 | RadioButton::new_labeled_get(os, operating_system, move || os).disabled(|| true) 57 | }) 58 | .v_stack() 59 | .class(RadioButtonGroupClass), 60 | ), 61 | )) 62 | .style(|s| s.class(RadioButtonGroupClass, |s| s.gap(10.).margin_left(5.))) 63 | } 64 | -------------------------------------------------------------------------------- /examples/widget-gallery/src/rich_text.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | use floem::{ 4 | peniko::color::palette, 5 | text::{Attrs, AttrsList, Style, TextLayout}, 6 | views::{rich_text, scroll, v_stack, Decorators, RichTextExt}, 7 | IntoView, 8 | }; 9 | 10 | pub fn rich_text_view() -> impl IntoView { 11 | let builder = 12 | "This".red().italic() + " is rich text".blue() + "\nTest value: " + 5.to_string().green(); 13 | 14 | let text = " 15 | // floem is a ui lib, homepage https://github.com/lapce/floem 16 | fn main() { 17 | println(\"Hello World!\"); 18 | }"; 19 | scroll({ 20 | v_stack(( 21 | rich_text(move || { 22 | let attrs = Attrs::new().color(palette::css::BLACK); 23 | 24 | let mut attrs_list = AttrsList::new(attrs); 25 | 26 | attrs_list.add_span( 27 | Range { start: 5, end: 66 }, 28 | Attrs::new().color(palette::css::GRAY).style(Style::Italic), 29 | ); 30 | 31 | attrs_list.add_span( 32 | Range { start: 36, end: 66 }, 33 | Attrs::new().color(palette::css::BLUE), 34 | ); 35 | 36 | attrs_list.add_span( 37 | Range { start: 71, end: 73 }, 38 | Attrs::new().color(palette::css::PURPLE), 39 | ); 40 | 41 | attrs_list.add_span( 42 | Range { start: 74, end: 78 }, 43 | Attrs::new().color(palette::css::SKY_BLUE), 44 | ); 45 | 46 | attrs_list.add_span( 47 | Range { start: 78, end: 80 }, 48 | Attrs::new().color(palette::css::GOLDENROD), 49 | ); 50 | 51 | attrs_list.add_span( 52 | Range { start: 91, end: 98 }, 53 | Attrs::new().color(palette::css::GOLD), 54 | ); 55 | 56 | attrs_list.add_span( 57 | Range { start: 98, end: 99 }, 58 | Attrs::new().color(palette::css::PURPLE), 59 | ); 60 | 61 | attrs_list.add_span( 62 | Range { 63 | start: 100, 64 | end: 113, 65 | }, 66 | Attrs::new().color(palette::css::DARK_GREEN), 67 | ); 68 | 69 | attrs_list.add_span( 70 | Range { 71 | start: 113, 72 | end: 114, 73 | }, 74 | Attrs::new().color(palette::css::PURPLE), 75 | ); 76 | 77 | attrs_list.add_span( 78 | Range { 79 | start: 114, 80 | end: 115, 81 | }, 82 | Attrs::new().color(palette::css::GRAY), 83 | ); 84 | 85 | let mut text_layout = TextLayout::new(); 86 | text_layout.set_text(text, attrs_list); 87 | text_layout 88 | }), 89 | builder.style(|s| s.padding_left(15)), 90 | )) 91 | .style(|s| s.gap(20)) 92 | }) 93 | } 94 | -------------------------------------------------------------------------------- /examples/widget-gallery/src/slider.rs: -------------------------------------------------------------------------------- 1 | use floem::{prelude::*, reactive::DerivedRwSignal}; 2 | 3 | use crate::form::{self, form_item}; 4 | 5 | pub fn slider_view() -> impl IntoView { 6 | let input = RwSignal::new(String::from("50")); 7 | let slider_state = DerivedRwSignal::new( 8 | input, 9 | |val| val.parse::().unwrap_or_default().pct(), 10 | |val| val.0.to_string(), 11 | ); 12 | form::form(( 13 | form_item("Input Control:", text_input(input)), 14 | form_item( 15 | "Default Slider:", 16 | stack(( 17 | slider::Slider::new_rw(slider_state).style(|s| s.width(200)), 18 | label(move || format!("{:.1}%", slider_state.get().0)), 19 | )) 20 | .style(|s| s.gap(10)), 21 | ), 22 | form_item( 23 | "Unaligned Slider:", 24 | stack(( 25 | slider::Slider::new_rw(slider_state) 26 | .slider_style(|s| { 27 | s.accent_bar_height(30.pct()) 28 | .bar_height(30.pct()) 29 | .edge_align(false) 30 | }) 31 | .style(|s| s.width(200)), 32 | label(move || format!("{:.1}%", slider_state.get().0)), 33 | )) 34 | .style(|s| s.gap(10)), 35 | ), 36 | form_item( 37 | "Progress bar:", 38 | stack(( 39 | slider::Slider::new(move || slider_state.get()) 40 | .slider_style(|s| s.handle_radius(0).edge_align(true)) 41 | .style(|s| s.width(200)) 42 | .disabled(|| true), 43 | label(move || format!("{:.1}%", slider_state.get().0)), 44 | )) 45 | .style(|s| s.gap(10)), 46 | ), 47 | )) 48 | } 49 | -------------------------------------------------------------------------------- /examples/window-icon/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "window-icon" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | im.workspace = true 8 | floem = { path = "../.." } 9 | image = "0.25" 10 | nsvg = "0.5" 11 | -------------------------------------------------------------------------------- /examples/window-icon/assets/ferris.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapce/floem/fab1fe93c9405edaaf0b931ceec72a99248f2852/examples/window-icon/assets/ferris.png -------------------------------------------------------------------------------- /examples/window-icon/src/main.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | close_window, 3 | event::{Event, EventListener}, 4 | keyboard::{Key, NamedKey}, 5 | kurbo::Size, 6 | new_window, 7 | views::{button, label, v_stack, Decorators}, 8 | window::{Icon, WindowConfig, WindowId}, 9 | Application, IntoView, View, 10 | }; 11 | use std::path::Path; 12 | 13 | fn sub_window_view(id: WindowId) -> impl IntoView { 14 | v_stack(( 15 | label(move || String::from("This window has an icon from an SVG file.")) 16 | .style(|s| s.font_size(30.0)), 17 | button("Close this window").action(move || close_window(id)), 18 | )) 19 | .style(|s| { 20 | s.flex_col() 21 | .items_center() 22 | .justify_center() 23 | .width_full() 24 | .height_full() 25 | .row_gap(10.0) 26 | }) 27 | } 28 | 29 | fn app_view() -> impl IntoView { 30 | let view = v_stack(( 31 | label(move || String::from("This window has an icon from a PNG file")) 32 | .style(|s| s.font_size(30.0)), 33 | button("Open another window").action(|| { 34 | let svg_icon = load_svg_icon(include_str!("../assets/ferris.svg")); 35 | new_window( 36 | sub_window_view, 37 | Some( 38 | WindowConfig::default() 39 | .size(Size::new(600.0, 150.0)) 40 | .title("Window Icon Sub Example") 41 | .window_icon(svg_icon), 42 | ), 43 | ); 44 | }), 45 | )) 46 | .style(|s| { 47 | s.flex_col() 48 | .items_center() 49 | .justify_center() 50 | .width_full() 51 | .height_full() 52 | .row_gap(10.0) 53 | }); 54 | 55 | let id = view.id(); 56 | view.on_event_stop(EventListener::KeyUp, move |e| { 57 | if let Event::KeyUp(e) = e { 58 | if e.key.logical_key == Key::Named(NamedKey::F11) { 59 | id.inspect(); 60 | } 61 | } 62 | }) 63 | } 64 | 65 | fn main() { 66 | let png_icon_path = concat!(env!("CARGO_MANIFEST_DIR"), "/assets/ferris.png"); 67 | let png_icon = load_png_icon(Path::new(png_icon_path)); 68 | 69 | Application::new() 70 | .window( 71 | |_| app_view(), 72 | Some( 73 | WindowConfig::default() 74 | .size(Size::new(800.0, 250.0)) 75 | .title("Window Icon Example") 76 | .window_icon(png_icon), 77 | ), 78 | ) 79 | .run(); 80 | } 81 | 82 | fn load_png_icon(path: &Path) -> Icon { 83 | let (icon_rgba, icon_width, icon_height) = { 84 | let image = image::open(path) 85 | .expect("Failed to open icon path") 86 | .into_rgba8(); 87 | let (width, height) = image.dimensions(); 88 | let rgba = image.into_raw(); 89 | (rgba, width, height) 90 | }; 91 | Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon") 92 | } 93 | 94 | fn load_svg_icon(svg: &str) -> Icon { 95 | let svg = nsvg::parse_str(svg, nsvg::Units::Pixel, 96.0).unwrap(); 96 | let (icon_width, icon_height, icon_rgba) = svg.rasterize_to_raw_rgba(1.0).unwrap(); 97 | Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon") 98 | } 99 | -------------------------------------------------------------------------------- /examples/window-scale/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "window-scale" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | im.workspace = true 8 | floem = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/window-size/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "window-size" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | im.workspace = true 8 | floem = { path = "../.." } 9 | -------------------------------------------------------------------------------- /examples/window-size/src/main.rs: -------------------------------------------------------------------------------- 1 | use floem::{ 2 | event::{Event, EventListener}, 3 | keyboard::{Key, NamedKey}, 4 | kurbo::Size, 5 | prelude::{create_signal, SignalGet, SignalUpdate}, 6 | views::{label, v_stack, Decorators}, 7 | window::WindowConfig, 8 | Application, IntoView, View, 9 | }; 10 | 11 | fn app_view() -> impl IntoView { 12 | let (size, set_size) = create_signal(Size::default()); 13 | 14 | let view = v_stack((label(move || format!("{}", size.get())).style(|s| s.font_size(30.0)),)) 15 | .style(|s| { 16 | s.flex_col() 17 | .items_center() 18 | .justify_center() 19 | .width_full() 20 | .height_full() 21 | .row_gap(10.0) 22 | }); 23 | 24 | let id = view.id(); 25 | view.on_event_stop(EventListener::KeyUp, move |e| { 26 | if let Event::KeyUp(e) = e { 27 | if e.key.logical_key == Key::Named(NamedKey::F11) { 28 | id.inspect(); 29 | } 30 | } 31 | }) 32 | .on_resize(move |r| set_size.update(|value| *value = r.size())) 33 | } 34 | 35 | fn main() { 36 | let app = Application::new().window( 37 | |_| app_view(), 38 | Some( 39 | WindowConfig::default() 40 | .size(Size::new(800.0, 600.0)) 41 | .min_size(Size::new(400.0, 300.0)) 42 | .max_size(Size::new(1200.0, 900.0)) 43 | .title("Window Size Example"), 44 | ), 45 | ); 46 | app.run(); 47 | } 48 | -------------------------------------------------------------------------------- /reactive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floem_reactive" 3 | version.workspace = true 4 | edition = "2021" 5 | repository = "https://github.com/lapce/floem" 6 | description = "A native Rust UI library with fine-grained reactivity" 7 | license.workspace = true 8 | 9 | [dependencies] 10 | smallvec = "1.10.0" 11 | -------------------------------------------------------------------------------- /reactive/src/base.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::{ 4 | id::Id, 5 | signal::{NotThreadSafe, Signal}, 6 | ReadSignal, RwSignal, SignalGet, SignalUpdate, SignalWith, WriteSignal, 7 | }; 8 | 9 | /// BaseSignal gives you another way to control the lifetime of a Signal 10 | /// apart from Scope. 11 | /// 12 | /// When BaseSignal is dropped, it will dispose the underlying Signal as well. 13 | /// The signal isn't put in any Scope when a BaseSignal is created, so that 14 | /// the lifetime of the signal can only be determined by BaseSignal rather than 15 | /// Scope dependencies 16 | pub struct BaseSignal { 17 | id: Id, 18 | ty: PhantomData, 19 | pub(crate) ts: PhantomData, 20 | } 21 | 22 | impl Eq for BaseSignal {} 23 | 24 | impl PartialEq for BaseSignal { 25 | fn eq(&self, other: &Self) -> bool { 26 | self.id == other.id 27 | } 28 | } 29 | 30 | impl Drop for BaseSignal { 31 | fn drop(&mut self) { 32 | self.id.dispose(); 33 | } 34 | } 35 | 36 | pub fn create_base_signal(value: T) -> BaseSignal { 37 | let id = Signal::create(value); 38 | BaseSignal { 39 | id, 40 | ty: PhantomData, 41 | ts: PhantomData, 42 | } 43 | } 44 | 45 | impl BaseSignal { 46 | /// Create a RwSignal of this Signal 47 | pub fn rw(&self) -> RwSignal { 48 | RwSignal { 49 | id: self.id, 50 | ty: PhantomData, 51 | ts: PhantomData, 52 | } 53 | } 54 | 55 | /// Create a Getter of this Signal 56 | pub fn read_only(&self) -> ReadSignal { 57 | ReadSignal { 58 | id: self.id, 59 | ty: PhantomData, 60 | ts: PhantomData, 61 | } 62 | } 63 | 64 | /// Create a Setter of this Signal 65 | pub fn write_only(&self) -> WriteSignal { 66 | WriteSignal { 67 | id: self.id, 68 | ty: PhantomData, 69 | ts: PhantomData, 70 | } 71 | } 72 | } 73 | 74 | impl SignalGet for BaseSignal { 75 | fn id(&self) -> Id { 76 | self.id 77 | } 78 | } 79 | 80 | impl SignalWith for BaseSignal { 81 | fn id(&self) -> Id { 82 | self.id 83 | } 84 | } 85 | 86 | impl SignalUpdate for BaseSignal { 87 | fn id(&self) -> Id { 88 | self.id 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /reactive/src/context.rs: -------------------------------------------------------------------------------- 1 | use std::any::{Any, TypeId}; 2 | 3 | use crate::runtime::RUNTIME; 4 | 5 | /// Try to retrieve a stored Context value in the reactive system. 6 | /// You can store a Context value anywhere, and retrieve it from anywhere afterwards. 7 | /// 8 | /// # Example 9 | /// In a parent component: 10 | /// ```rust 11 | /// # use floem_reactive::provide_context; 12 | /// provide_context(42); 13 | /// provide_context(String::from("Hello world")); 14 | /// ``` 15 | /// 16 | /// And so in a child component you can retrieve each context data by specifying the type: 17 | /// ```rust 18 | /// # use floem_reactive::use_context; 19 | /// let foo: Option = use_context(); 20 | /// let bar: Option = use_context(); 21 | /// ``` 22 | pub fn use_context() -> Option 23 | where 24 | T: Clone + 'static, 25 | { 26 | let ty = TypeId::of::(); 27 | RUNTIME.with(|runtime| { 28 | let contexts = runtime.contexts.borrow(); 29 | let context = contexts 30 | .get(&ty) 31 | .and_then(|val| val.downcast_ref::()) 32 | .cloned(); 33 | context 34 | }) 35 | } 36 | 37 | /// Sets a context value to be stored in the reactive system. 38 | /// The stored context value can be retrieved from anywhere by using [use_context](use_context) 39 | /// 40 | /// # Example 41 | /// In a parent component: 42 | /// ```rust 43 | /// # use floem_reactive::provide_context; 44 | /// provide_context(42); 45 | /// provide_context(String::from("Hello world")); 46 | /// ``` 47 | /// 48 | /// And so in a child component you can retrieve each context data by specifying the type: 49 | /// ```rust 50 | /// # use floem_reactive::use_context; 51 | /// let foo: Option = use_context(); 52 | /// let bar: Option = use_context(); 53 | /// ``` 54 | pub fn provide_context(value: T) 55 | where 56 | T: Clone + 'static, 57 | { 58 | let id = value.type_id(); 59 | 60 | RUNTIME.with(|runtime| { 61 | let mut contexts = runtime.contexts.borrow_mut(); 62 | contexts.insert(id, Box::new(value) as Box); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /reactive/src/id.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::AtomicU64; 2 | 3 | use crate::{effect::observer_clean_up, runtime::RUNTIME, signal::Signal}; 4 | 5 | /// An internal id which can reference a Signal/Effect/Scope. 6 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Hash)] 7 | pub struct Id(u64); 8 | 9 | impl Id { 10 | /// Create a new Id that's next in order 11 | pub(crate) fn next() -> Id { 12 | static COUNTER: AtomicU64 = AtomicU64::new(0); 13 | Id(COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)) 14 | } 15 | 16 | /// Try to get the Signal that links with this Id 17 | pub(crate) fn signal(&self) -> Option { 18 | RUNTIME.with(|runtime| runtime.signals.borrow().get(self).cloned()) 19 | } 20 | 21 | /// Try to set the Signal to be linking with this Id 22 | pub(crate) fn add_signal(&self, signal: Signal) { 23 | RUNTIME.with(|runtime| runtime.signals.borrow_mut().insert(*self, signal)); 24 | } 25 | 26 | /// Make this Id a child of the current Scope 27 | pub(crate) fn set_scope(&self) { 28 | RUNTIME.with(|runtime| { 29 | let scope = runtime.current_scope.borrow(); 30 | let mut children = runtime.children.borrow_mut(); 31 | let children = children.entry(*scope).or_default(); 32 | children.insert(*self); 33 | }); 34 | } 35 | 36 | /// Dispose the relevant resources that's linking to this Id, and the all the children 37 | /// and grandchildren. 38 | pub(crate) fn dispose(&self) { 39 | if let Ok((children, signal)) = RUNTIME.try_with(|runtime| { 40 | ( 41 | runtime.children.borrow_mut().remove(self), 42 | runtime.signals.borrow_mut().remove(self), 43 | ) 44 | }) { 45 | if let Some(children) = children { 46 | for child in children { 47 | child.dispose(); 48 | } 49 | } 50 | 51 | if let Some(signal) = signal { 52 | for (_, effect) in signal.subscribers() { 53 | observer_clean_up(&effect); 54 | } 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /reactive/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # Floem Reactive 2 | //! 3 | //! [`RwSignal::new_split`](RwSignal::new_split) returns a separated [`ReadSignal`] and [`WriteSignal`] for a variable. 4 | //! An existing `RwSignal` may be converted using [`RwSignal::read_only`](RwSignal::read_only) 5 | //! and [`RwSignal::write_only`](RwSignal::write_only) where necessary, but the reverse is not possible. 6 | 7 | mod base; 8 | mod context; 9 | mod derived; 10 | mod effect; 11 | mod id; 12 | mod impls; 13 | mod memo; 14 | mod read; 15 | mod runtime; 16 | mod scope; 17 | mod signal; 18 | mod trigger; 19 | mod write; 20 | 21 | pub use base::{create_base_signal, BaseSignal}; 22 | pub use context::{provide_context, use_context}; 23 | pub use derived::{create_derived_rw_signal, DerivedRwSignal}; 24 | pub use effect::{ 25 | batch, create_effect, create_stateful_updater, create_tracker, create_updater, untrack, 26 | SignalTracker, 27 | }; 28 | pub use memo::{create_memo, Memo}; 29 | pub use read::{ReadSignalValue, SignalGet, SignalRead, SignalTrack, SignalWith}; 30 | pub use scope::{as_child_of_current_scope, with_scope, Scope}; 31 | pub use signal::{create_rw_signal, create_signal, ReadSignal, RwSignal, WriteSignal}; 32 | pub use trigger::{create_trigger, Trigger}; 33 | pub use write::{SignalUpdate, SignalWrite, WriteSignalValue}; 34 | -------------------------------------------------------------------------------- /reactive/src/memo.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::{ 4 | effect::create_effect, 5 | read::{SignalRead, SignalTrack}, 6 | scope::Scope, 7 | signal::{create_signal, NotThreadSafe, ReadSignal}, 8 | SignalGet, SignalUpdate, SignalWith, 9 | }; 10 | 11 | /// Memo computes the value from the closure on creation, and stores the value. 12 | /// 13 | /// It will act like a Signal when the value is different with the computed value 14 | /// from last run, i.e., it will trigger a effect run when you Get() it whenever the 15 | /// computed value changes to a different value. 16 | pub struct Memo { 17 | getter: ReadSignal, 18 | ty: PhantomData, 19 | pub(crate) ts: PhantomData, 20 | } 21 | 22 | impl Copy for Memo {} 23 | 24 | impl Clone for Memo { 25 | fn clone(&self) -> Self { 26 | *self 27 | } 28 | } 29 | 30 | impl SignalGet for Memo { 31 | fn id(&self) -> crate::id::Id { 32 | self.getter.id 33 | } 34 | } 35 | 36 | impl SignalWith for Memo { 37 | fn id(&self) -> crate::id::Id { 38 | self.getter.id 39 | } 40 | } 41 | impl SignalTrack for Memo { 42 | fn id(&self) -> crate::id::Id { 43 | self.getter.id 44 | } 45 | } 46 | 47 | /// Create a Memo which takes the computed value of the given function, and triggers 48 | /// the reactive system when the computed value is different with the last computed value. 49 | pub fn create_memo(f: impl Fn(Option<&T>) -> T + 'static) -> Memo 50 | where 51 | T: PartialEq + 'static, 52 | { 53 | let cx = Scope::current(); 54 | let initial = f(None); 55 | let (getter, setter) = create_signal(initial); 56 | let reader = getter.read_untracked(); 57 | 58 | create_effect(move |_| { 59 | cx.track(); 60 | let (is_different, new_value) = { 61 | let last_value = reader.borrow(); 62 | let new_value = f(Some(&last_value)); 63 | (new_value != *last_value, new_value) 64 | }; 65 | if is_different { 66 | setter.set(new_value); 67 | } 68 | }); 69 | 70 | Memo { 71 | getter, 72 | ty: PhantomData, 73 | ts: PhantomData, 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /reactive/src/runtime.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | any::{Any, TypeId}, 3 | cell::{Cell, RefCell}, 4 | collections::{HashMap, HashSet}, 5 | rc::Rc, 6 | }; 7 | 8 | use smallvec::SmallVec; 9 | 10 | use crate::{ 11 | effect::{run_effect, EffectTrait}, 12 | id::Id, 13 | signal::Signal, 14 | }; 15 | 16 | thread_local! { 17 | pub(crate) static RUNTIME: Runtime = Runtime::new(); 18 | } 19 | 20 | /// The internal reactive Runtime which stores all the reactive system states in a 21 | /// thread local 22 | pub(crate) struct Runtime { 23 | pub(crate) current_effect: RefCell>>, 24 | pub(crate) current_scope: RefCell, 25 | pub(crate) children: RefCell>>, 26 | pub(crate) signals: RefCell>, 27 | pub(crate) contexts: RefCell>>, 28 | pub(crate) batching: Cell, 29 | pub(crate) pending_effects: RefCell; 10]>>, 30 | } 31 | 32 | impl Default for Runtime { 33 | fn default() -> Self { 34 | Self::new() 35 | } 36 | } 37 | 38 | impl Runtime { 39 | pub(crate) fn new() -> Self { 40 | Self { 41 | current_effect: RefCell::new(None), 42 | current_scope: RefCell::new(Id::next()), 43 | children: RefCell::new(HashMap::new()), 44 | signals: Default::default(), 45 | contexts: Default::default(), 46 | batching: Cell::new(false), 47 | pending_effects: RefCell::new(SmallVec::new()), 48 | } 49 | } 50 | 51 | pub(crate) fn add_pending_effect(&self, effect: Rc) { 52 | let has_effect = self 53 | .pending_effects 54 | .borrow() 55 | .iter() 56 | .any(|e| e.id() == effect.id()); 57 | if !has_effect { 58 | self.pending_effects.borrow_mut().push(effect); 59 | } 60 | } 61 | 62 | pub(crate) fn run_pending_effects(&self) { 63 | let pending_effects = self.pending_effects.take(); 64 | for effect in pending_effects { 65 | run_effect(effect); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /reactive/src/trigger.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | signal::{create_rw_signal, RwSignal}, 3 | SignalUpdate, SignalWith, 4 | }; 5 | 6 | #[derive(Debug)] 7 | pub struct Trigger { 8 | signal: RwSignal<()>, 9 | } 10 | 11 | impl Copy for Trigger {} 12 | 13 | impl Clone for Trigger { 14 | fn clone(&self) -> Self { 15 | *self 16 | } 17 | } 18 | 19 | impl Trigger { 20 | pub fn notify(&self) { 21 | self.signal.set(()); 22 | } 23 | 24 | pub fn track(&self) { 25 | self.signal.with(|_| {}); 26 | } 27 | 28 | #[allow(clippy::new_without_default)] 29 | pub fn new() -> Self { 30 | create_trigger() 31 | } 32 | } 33 | 34 | pub fn create_trigger() -> Trigger { 35 | Trigger { 36 | signal: create_rw_signal(()), 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /reactive/src/write.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::{RefCell, RefMut}, 3 | marker::PhantomData, 4 | rc::Rc, 5 | }; 6 | 7 | use crate::{id::Id, signal::NotThreadSafe}; 8 | 9 | #[derive(Clone)] 10 | pub struct WriteSignalValue { 11 | pub(crate) id: Id, 12 | pub(crate) value: Rc>, 13 | pub(crate) ts: PhantomData, 14 | } 15 | 16 | impl Drop for WriteSignalValue { 17 | fn drop(&mut self) { 18 | if let Some(signal) = self.id.signal() { 19 | signal.run_effects(); 20 | } 21 | } 22 | } 23 | 24 | impl WriteSignalValue { 25 | /// Mutably borrows the current value stored in the Signal 26 | pub fn borrow_mut(&self) -> RefMut<'_, T> { 27 | self.value.borrow_mut() 28 | } 29 | } 30 | 31 | pub trait SignalUpdate { 32 | /// get the Signal Id 33 | fn id(&self) -> Id; 34 | 35 | /// Sets the new_value to the Signal and triggers effect run 36 | fn set(&self, new_value: T) 37 | where 38 | T: 'static, 39 | { 40 | if let Some(signal) = self.id().signal() { 41 | signal.update_value(|v| *v = new_value); 42 | } 43 | } 44 | 45 | /// Update the stored value with the given function and triggers effect run 46 | fn update(&self, f: impl FnOnce(&mut T)) 47 | where 48 | T: 'static, 49 | { 50 | if let Some(signal) = self.id().signal() { 51 | signal.update_value(f); 52 | } 53 | } 54 | 55 | /// Update the stored value with the given function, triggers effect run, 56 | /// and returns the value returned by the function 57 | fn try_update(&self, f: impl FnOnce(&mut T) -> O) -> Option 58 | where 59 | T: 'static, 60 | { 61 | self.id().signal().map(|signal| signal.update_value(f)) 62 | } 63 | } 64 | 65 | pub trait SignalWrite { 66 | /// get the Signal Id 67 | fn id(&self) -> Id; 68 | /// Convert the Signal to `WriteSignalValue` where it holds a RefCell wrapped 69 | /// original data of the signal, so that you can `borrow_mut()` to update the data. 70 | /// 71 | /// When `WriteSignalValue` drops, it triggers effect run 72 | fn write(&self) -> WriteSignalValue 73 | where 74 | T: 'static, 75 | { 76 | self.try_write().unwrap() 77 | } 78 | 79 | /// If the Signal isn't disposed, 80 | /// convert the Signal to `WriteSignalValue` where it holds a RefCell wrapped 81 | /// original data of the signal, so that you can `borrow_mut()` to update the data. 82 | /// 83 | /// When `WriteSignalValue` drops, it triggers effect run 84 | fn try_write(&self) -> Option> 85 | where 86 | T: 'static, 87 | { 88 | if let Some(signal) = self.id().signal() { 89 | Some(WriteSignalValue { 90 | id: signal.id, 91 | value: signal 92 | .value 93 | .clone() 94 | .downcast::>() 95 | .expect("to downcast signal type"), 96 | ts: PhantomData, 97 | }) 98 | } else { 99 | None 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /reactive/tests/effect.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::Cell, rc::Rc}; 2 | 3 | use floem_reactive::{batch, create_effect, create_rw_signal, SignalTrack, SignalUpdate}; 4 | 5 | #[test] 6 | fn batch_simple() { 7 | let name = create_rw_signal("John"); 8 | let age = create_rw_signal(20); 9 | 10 | let count = Rc::new(Cell::new(0)); 11 | 12 | create_effect({ 13 | let count = count.clone(); 14 | move |_| { 15 | name.track(); 16 | age.track(); 17 | 18 | count.set(count.get() + 1); 19 | } 20 | }); 21 | 22 | // The effect runs once immediately 23 | assert_eq!(count.get(), 1); 24 | 25 | // Setting each signal once will trigger the effect 26 | name.set("Mary"); 27 | assert_eq!(count.get(), 2); 28 | 29 | age.set(21); 30 | assert_eq!(count.get(), 3); 31 | 32 | // Batching will only update once 33 | batch(|| { 34 | name.set("John"); 35 | age.set(20); 36 | }); 37 | assert_eq!(count.get(), 4); 38 | } 39 | 40 | #[test] 41 | fn batch_batch() { 42 | let name = create_rw_signal("John"); 43 | let age = create_rw_signal(20); 44 | 45 | let count = Rc::new(Cell::new(0)); 46 | 47 | create_effect({ 48 | let count = count.clone(); 49 | move |_| { 50 | name.track(); 51 | age.track(); 52 | 53 | count.set(count.get() + 1); 54 | } 55 | }); 56 | 57 | assert_eq!(count.get(), 1); 58 | 59 | // Batching within another batch should be equivalent to batching them all together 60 | batch(|| { 61 | name.set("Mary"); 62 | age.set(21); 63 | batch(|| { 64 | name.set("John"); 65 | age.set(20); 66 | }); 67 | }); 68 | 69 | assert_eq!(count.get(), 2); 70 | } 71 | -------------------------------------------------------------------------------- /renderer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floem_renderer" 3 | version.workspace = true 4 | edition = "2021" 5 | repository = "https://github.com/lapce/floem" 6 | description = "A native Rust UI library with fine-grained reactivity" 7 | license.workspace = true 8 | 9 | [dependencies] 10 | parking_lot = { workspace = true } 11 | peniko = { workspace = true } 12 | resvg = { workspace = true } 13 | swash = { workspace = true } 14 | 15 | cosmic-text = { version = "0.12.1", features = ["shape-run-cache"] } 16 | 17 | winit = { workspace = true } 18 | wgpu = { workspace = true } 19 | crossbeam = { version = "0.8", optional = true } 20 | futures = "0.3.26" 21 | 22 | [target.'cfg(target_arch = "wasm32")'.dependencies] 23 | wasm-bindgen-futures = { version = "0.4" } 24 | 25 | [features] 26 | crossbeam = [ "dep:crossbeam" ] 27 | -------------------------------------------------------------------------------- /renderer/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod swash; 2 | pub mod text; 3 | 4 | use crate::text::LayoutRun; 5 | use peniko::{ 6 | kurbo::{Affine, Point, Rect, Shape, Stroke}, 7 | BlendMode, BrushRef, 8 | }; 9 | pub use resvg::tiny_skia; 10 | pub use resvg::usvg; 11 | use text::TextLayout; 12 | 13 | pub mod gpu_resources; 14 | 15 | pub struct Svg<'a> { 16 | pub tree: &'a usvg::Tree, 17 | pub hash: &'a [u8], 18 | } 19 | 20 | pub struct Img<'a> { 21 | pub img: peniko::Image, 22 | pub hash: &'a [u8], 23 | } 24 | 25 | pub trait Renderer { 26 | fn begin(&mut self, capture: bool); 27 | 28 | fn set_transform(&mut self, transform: Affine); 29 | 30 | fn set_z_index(&mut self, z_index: i32); 31 | 32 | /// Clip to a [`Shape`]. 33 | fn clip(&mut self, shape: &impl Shape); 34 | 35 | fn clear_clip(&mut self); 36 | 37 | /// Stroke a [`Shape`]. 38 | fn stroke<'b, 's>( 39 | &mut self, 40 | shape: &impl Shape, 41 | brush: impl Into>, 42 | stroke: &'s Stroke, 43 | ); 44 | 45 | /// Fill a [`Shape`], using the [non-zero fill rule]. 46 | /// 47 | /// [non-zero fill rule]: https://en.wikipedia.org/wiki/Nonzero-rule 48 | fn fill<'b>(&mut self, path: &impl Shape, brush: impl Into>, blur_radius: f64); 49 | 50 | /// Push a layer (This is not supported with Vger) 51 | fn push_layer( 52 | &mut self, 53 | blend: impl Into, 54 | alpha: f32, 55 | transform: Affine, 56 | clip: &impl Shape, 57 | ); 58 | 59 | /// Pop a layer (This is not supported with Vger) 60 | fn pop_layer(&mut self); 61 | 62 | /// Draw a [`TextLayout`]. 63 | /// 64 | /// The `pos` parameter specifies the upper-left corner of the layout object 65 | /// (even for right-to-left text). 66 | fn draw_text(&mut self, layout: &TextLayout, pos: impl Into) { 67 | self.draw_text_with_layout(layout.layout_runs(), pos); 68 | } 69 | 70 | fn draw_text_with_layout<'b>( 71 | &mut self, 72 | layout: impl Iterator>, 73 | pos: impl Into, 74 | ); 75 | 76 | fn draw_svg<'b>(&mut self, svg: Svg<'b>, rect: Rect, brush: Option>>); 77 | 78 | fn draw_img(&mut self, img: Img<'_>, rect: Rect); 79 | 80 | fn finish(&mut self) -> Option; 81 | 82 | fn debug_info(&self) -> String { 83 | "Unknown".into() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /renderer/src/swash.rs: -------------------------------------------------------------------------------- 1 | use cosmic_text::{CacheKey, CacheKeyFlags, SwashImage}; 2 | use swash::{ 3 | scale::{Render, ScaleContext, Source, StrikeWith}, 4 | zeno::{Angle, Format, Transform, Vector}, 5 | }; 6 | 7 | use crate::text::FONT_SYSTEM; 8 | 9 | const IS_MACOS: bool = cfg!(target_os = "macos"); 10 | 11 | pub struct SwashScaler { 12 | context: ScaleContext, 13 | pub font_embolden: f32, 14 | } 15 | 16 | impl Default for SwashScaler { 17 | fn default() -> Self { 18 | Self { 19 | context: ScaleContext::new(), 20 | font_embolden: 0., 21 | } 22 | } 23 | } 24 | 25 | impl SwashScaler { 26 | pub fn new(font_embolden: f32) -> Self { 27 | Self { 28 | context: ScaleContext::new(), 29 | font_embolden, 30 | } 31 | } 32 | 33 | pub fn get_image(&mut self, cache_key: CacheKey) -> Option { 34 | let font = match FONT_SYSTEM.lock().get_font(cache_key.font_id) { 35 | Some(some) => some, 36 | None => { 37 | return None; 38 | } 39 | }; 40 | 41 | // Build the scaler 42 | let mut scaler = self 43 | .context 44 | .builder(font.as_swash()) 45 | .size(f32::from_bits(cache_key.font_size_bits)) 46 | .hint(!IS_MACOS) 47 | .build(); 48 | 49 | let offset = Vector::new(cache_key.x_bin.as_float(), cache_key.y_bin.as_float()); 50 | 51 | Render::new(&[ 52 | Source::ColorOutline(0), 53 | Source::ColorBitmap(StrikeWith::BestFit), 54 | Source::Outline, 55 | ]) 56 | .format(Format::Alpha) 57 | .offset(offset) 58 | .embolden(self.font_embolden) 59 | .transform(if cache_key.flags.contains(CacheKeyFlags::FAKE_ITALIC) { 60 | Some(Transform::skew( 61 | Angle::from_degrees(14.0), 62 | Angle::from_degrees(0.0), 63 | )) 64 | } else { 65 | None 66 | }) 67 | .render(&mut scaler, cache_key.glyph_id) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /renderer/src/text/mod.rs: -------------------------------------------------------------------------------- 1 | mod attrs; 2 | mod layout; 3 | 4 | pub use attrs::{Attrs, AttrsList, AttrsOwned, FamilyOwned, LineHeightValue}; 5 | pub use cosmic_text::{ 6 | fontdb, CacheKey, Cursor, Family, LayoutGlyph, LayoutLine, LineEnding, Stretch, Style, 7 | SubpixelBin, SwashCache, SwashContent, Weight, Wrap, 8 | }; 9 | pub use layout::{HitPoint, HitPosition, LayoutRun, TextLayout, FONT_SYSTEM}; 10 | -------------------------------------------------------------------------------- /src/app_delegate.rs: -------------------------------------------------------------------------------- 1 | use objc2::rc::Retained; 2 | use objc2::runtime::{AnyObject, ProtocolObject}; 3 | use objc2::{define_class, msg_send, MainThreadMarker, MainThreadOnly}; 4 | use objc2_app_kit::{NSApplication, NSApplicationDelegate}; 5 | use objc2_foundation::{NSObject, NSObjectProtocol}; 6 | 7 | use crate::app::UserEvent; 8 | 9 | define_class!( 10 | #[unsafe(super(NSObject))] 11 | #[thread_kind = MainThreadOnly] 12 | #[name = "MyAppDelegate"] 13 | struct AppDelegate; 14 | 15 | unsafe impl NSObjectProtocol for AppDelegate {} 16 | 17 | unsafe impl NSApplicationDelegate for AppDelegate { 18 | #[unsafe(method(applicationShouldHandleReopen:hasVisibleWindows:))] 19 | fn should_handle_reopen( 20 | &self, 21 | _sender: &Option<&AnyObject>, 22 | has_visible_windows: bool, 23 | ) -> bool { 24 | crate::Application::send_proxy_event(UserEvent::Reopen { 25 | has_visible_windows, 26 | }); 27 | // return true to preserve the default behavior, such as showing the minimized window. 28 | true 29 | } 30 | } 31 | ); 32 | 33 | impl AppDelegate { 34 | fn new(mtm: MainThreadMarker) -> Retained { 35 | unsafe { msg_send![super(mtm.alloc().set_ivars(())), init] } 36 | } 37 | } 38 | 39 | pub(crate) fn set_app_delegate() { 40 | let mtm = MainThreadMarker::new().unwrap(); 41 | let delegate = AppDelegate::new(mtm); 42 | // Important: Call `sharedApplication` after `EventLoop::new`, 43 | // doing it before is not yet supported. 44 | let app = NSApplication::sharedApplication(mtm); 45 | app.setDelegate(Some(ProtocolObject::from_ref(&*delegate))); 46 | } 47 | -------------------------------------------------------------------------------- /src/clipboard.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::Mutex; 2 | use raw_window_handle::RawDisplayHandle; 3 | 4 | use copypasta::{ClipboardContext, ClipboardProvider}; 5 | 6 | static CLIPBOARD: Mutex> = Mutex::new(None); 7 | 8 | pub struct Clipboard { 9 | clipboard: Box, 10 | #[allow(dead_code)] 11 | selection: Option>, 12 | } 13 | 14 | #[derive(Clone, Debug)] 15 | pub enum ClipboardError { 16 | NotAvailable, 17 | ProviderError(String), 18 | } 19 | 20 | impl Clipboard { 21 | pub fn get_contents() -> Result { 22 | CLIPBOARD 23 | .lock() 24 | .as_mut() 25 | .ok_or(ClipboardError::NotAvailable)? 26 | .clipboard 27 | .get_contents() 28 | .map_err(|e| ClipboardError::ProviderError(e.to_string())) 29 | } 30 | 31 | pub fn set_contents(s: String) -> Result<(), ClipboardError> { 32 | if s.is_empty() { 33 | return Err(ClipboardError::ProviderError( 34 | "content is empty".to_string(), 35 | )); 36 | } 37 | CLIPBOARD 38 | .lock() 39 | .as_mut() 40 | .ok_or(ClipboardError::NotAvailable)? 41 | .clipboard 42 | .set_contents(s) 43 | .map_err(|e| ClipboardError::ProviderError(e.to_string())) 44 | } 45 | 46 | #[cfg(windows)] 47 | pub fn get_file_list() -> Result, ClipboardError> { 48 | clipboard_win::Clipboard::new_attempts(10) 49 | .and_then(|x| x.get_file_list()) 50 | .map_err(|e| ClipboardError::ProviderError(e.to_string())) 51 | } 52 | 53 | pub(crate) unsafe fn init(display: RawDisplayHandle) { 54 | *CLIPBOARD.lock() = Some(Self::new(display)); 55 | } 56 | 57 | /// # Safety 58 | /// The `display` must be valid as long as the returned Clipboard exists. 59 | unsafe fn new( 60 | #[allow(unused_variables)] /* on some platforms */ display: RawDisplayHandle, 61 | ) -> Self { 62 | #[cfg(not(any( 63 | target_os = "macos", 64 | target_os = "windows", 65 | target_os = "ios", 66 | target_os = "android", 67 | target_arch = "wasm32" 68 | )))] 69 | { 70 | if let RawDisplayHandle::Wayland(display) = display { 71 | use copypasta::wayland_clipboard; 72 | let (selection, clipboard) = 73 | wayland_clipboard::create_clipboards_from_external(display.display.as_ptr()); 74 | return Self { 75 | clipboard: Box::new(clipboard), 76 | selection: Some(Box::new(selection)), 77 | }; 78 | } 79 | 80 | use copypasta::x11_clipboard::{Primary, X11ClipboardContext}; 81 | Self { 82 | clipboard: Box::new(ClipboardContext::new().unwrap()), 83 | selection: Some(Box::new(X11ClipboardContext::::new().unwrap())), 84 | } 85 | } 86 | 87 | // TODO: Implement clipboard support for the web, ios, and android 88 | #[cfg(any( 89 | target_os = "macos", 90 | target_os = "windows", 91 | target_os = "ios", 92 | target_os = "android", 93 | target_arch = "wasm32" 94 | ))] 95 | return Self { 96 | clipboard: Box::new(ClipboardContext::new().unwrap()), 97 | selection: None, 98 | }; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/dropped_file.rs: -------------------------------------------------------------------------------- 1 | use peniko::kurbo::Point; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct DroppedFileEvent { 6 | pub path: PathBuf, 7 | pub pos: Point, 8 | } 9 | -------------------------------------------------------------------------------- /src/file_action.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use floem_reactive::Scope; 4 | 5 | use crate::{ 6 | ext_event::create_ext_action, 7 | file::{FileDialogOptions, FileInfo}, 8 | }; 9 | 10 | /// Open a file using the system file dialog 11 | pub fn open_file( 12 | options: FileDialogOptions, 13 | file_info_action: impl Fn(Option) + 'static, 14 | ) { 15 | let send = create_ext_action( 16 | Scope::new(), 17 | move |(path, paths): (Option, Option>)| { 18 | if paths.is_some() { 19 | file_info_action(paths.map(|paths| FileInfo { 20 | path: paths, 21 | format: None, 22 | })) 23 | } else { 24 | file_info_action(path.map(|path| FileInfo { 25 | path: vec![path], 26 | format: None, 27 | })) 28 | } 29 | }, 30 | ); 31 | std::thread::spawn(move || { 32 | let mut dialog = rfd::FileDialog::new(); 33 | if let Some(path) = options.starting_directory.as_ref() { 34 | dialog = dialog.set_directory(path); 35 | } 36 | if let Some(title) = options.title.as_ref() { 37 | dialog = dialog.set_title(title); 38 | } 39 | if let Some(allowed_types) = options.allowed_types.as_ref() { 40 | dialog = allowed_types.iter().fold(dialog, |dialog, filter| { 41 | dialog.add_filter(filter.name, filter.extensions) 42 | }); 43 | } 44 | 45 | if options.select_directories && options.multi_selection { 46 | send((None, dialog.pick_folders())); 47 | } else if options.select_directories && !options.multi_selection { 48 | send((dialog.pick_folder(), None)); 49 | } else if !options.select_directories && options.multi_selection { 50 | send((None, dialog.pick_files())); 51 | } else { 52 | send((dialog.pick_file(), None)); 53 | } 54 | }); 55 | } 56 | 57 | /// Open a system file save dialog 58 | pub fn save_as(options: FileDialogOptions, file_info_action: impl Fn(Option) + 'static) { 59 | let send = create_ext_action(Scope::new(), move |path: Option| { 60 | file_info_action(path.map(|path| FileInfo { 61 | path: vec![path], 62 | format: None, 63 | })) 64 | }); 65 | std::thread::spawn(move || { 66 | let mut dialog = rfd::FileDialog::new(); 67 | if let Some(path) = options.starting_directory.as_ref() { 68 | dialog = dialog.set_directory(path); 69 | } 70 | if let Some(name) = options.default_name.as_ref() { 71 | dialog = dialog.set_file_name(name); 72 | } 73 | if let Some(title) = options.title.as_ref() { 74 | dialog = dialog.set_title(title); 75 | } 76 | let path = dialog.save_file(); 77 | send(path); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /src/keyboard.rs: -------------------------------------------------------------------------------- 1 | use bitflags::bitflags; 2 | pub use winit::keyboard::{ 3 | Key, KeyCode, KeyLocation, ModifiersState, NamedKey, NativeKey, PhysicalKey, 4 | }; 5 | #[cfg(not(any(target_arch = "wasm32", target_os = "ios", target_os = "android")))] 6 | pub use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; 7 | 8 | #[derive(Debug, Clone, Eq, PartialEq, Hash)] 9 | pub struct KeyEvent { 10 | pub key: winit::event::KeyEvent, 11 | pub modifiers: Modifiers, 12 | } 13 | 14 | bitflags! { 15 | /// Represents the current state of the keyboard modifiers 16 | /// 17 | /// Each flag represents a modifier and is set if this modifier is active. 18 | #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 19 | pub struct Modifiers: u32 { 20 | /// The "shift" key. 21 | const SHIFT = 0b100; 22 | /// The "control" key. 23 | const CONTROL = 0b100 << 3; 24 | /// The "alt" key. 25 | const ALT = 0b100 << 6; 26 | /// This is the "windows" key on PC and "command" key on Mac. 27 | const META = 0b100 << 9; 28 | /// The "altgr" key. 29 | const ALTGR = 0b100 << 12; 30 | } 31 | } 32 | 33 | impl Modifiers { 34 | /// Returns `true` if the shift key is pressed. 35 | pub fn shift(&self) -> bool { 36 | self.intersects(Self::SHIFT) 37 | } 38 | /// Returns `true` if the control key is pressed. 39 | pub fn control(&self) -> bool { 40 | self.intersects(Self::CONTROL) 41 | } 42 | /// Returns `true` if the alt key is pressed. 43 | pub fn alt(&self) -> bool { 44 | self.intersects(Self::ALT) 45 | } 46 | /// Returns `true` if the meta key is pressed. 47 | pub fn meta(&self) -> bool { 48 | self.intersects(Self::META) 49 | } 50 | /// Returns `true` if the altgr key is pressed. 51 | pub fn altgr(&self) -> bool { 52 | self.intersects(Self::ALTGR) 53 | } 54 | } 55 | 56 | impl From for Modifiers { 57 | fn from(value: ModifiersState) -> Self { 58 | let mut modifiers = Modifiers::empty(); 59 | if value.shift_key() { 60 | modifiers.set(Modifiers::SHIFT, true); 61 | } 62 | if value.alt_key() { 63 | modifiers.set(Modifiers::ALT, true); 64 | } 65 | if value.control_key() { 66 | modifiers.set(Modifiers::CONTROL, true); 67 | } 68 | if value.super_key() { 69 | modifiers.set(Modifiers::META, true); 70 | } 71 | modifiers 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/menu.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::AtomicU64; 2 | 3 | /// An entry in a menu. 4 | /// 5 | /// An entry is either a [`MenuItem`], a submenu (i.e. [`Menu`]). 6 | pub enum MenuEntry { 7 | Separator, 8 | Item(MenuItem), 9 | SubMenu(Menu), 10 | } 11 | 12 | pub struct Menu { 13 | pub(crate) popup: bool, 14 | pub(crate) item: MenuItem, 15 | pub(crate) children: Vec, 16 | } 17 | 18 | impl From for MenuEntry { 19 | fn from(m: Menu) -> MenuEntry { 20 | MenuEntry::SubMenu(m) 21 | } 22 | } 23 | 24 | impl Menu { 25 | pub fn new(title: impl Into) -> Self { 26 | Self { 27 | popup: false, 28 | item: MenuItem::new(title), 29 | children: Vec::new(), 30 | } 31 | } 32 | 33 | pub(crate) fn popup(mut self) -> Self { 34 | self.popup = true; 35 | self 36 | } 37 | 38 | /// Append a menu entry to this menu, returning the modified menu. 39 | pub fn entry(mut self, entry: impl Into) -> Self { 40 | self.children.push(entry.into()); 41 | self 42 | } 43 | 44 | /// Append a separator to this menu, returning the modified menu. 45 | pub fn separator(self) -> Self { 46 | self.entry(MenuEntry::Separator) 47 | } 48 | 49 | #[cfg(any(target_os = "windows", target_os = "macos"))] 50 | pub(crate) fn platform_menu(&self) -> muda::Menu { 51 | let menu = muda::Menu::new(); 52 | for entry in &self.children { 53 | match entry { 54 | MenuEntry::Separator => { 55 | let _ = menu.append(&muda::PredefinedMenuItem::separator()); 56 | } 57 | MenuEntry::Item(item) => { 58 | let _ = menu.append(&muda::MenuItem::with_id( 59 | item.id.clone(), 60 | item.title.clone(), 61 | item.enabled, 62 | None, 63 | )); 64 | } 65 | MenuEntry::SubMenu(floem_menu) => { 66 | let _ = menu.append(&floem_menu.platform_submenu()); 67 | } 68 | } 69 | } 70 | menu 71 | } 72 | 73 | #[cfg(any(target_os = "windows", target_os = "macos"))] 74 | pub(crate) fn platform_submenu(&self) -> muda::Submenu { 75 | let menu = muda::Submenu::new(self.item.title.clone(), self.item.enabled); 76 | for entry in &self.children { 77 | match entry { 78 | MenuEntry::Separator => { 79 | let _ = menu.append(&muda::PredefinedMenuItem::separator()); 80 | } 81 | MenuEntry::Item(item) => { 82 | let _ = menu.append(&muda::MenuItem::with_id( 83 | item.id.clone(), 84 | item.title.clone(), 85 | item.enabled, 86 | None, 87 | )); 88 | } 89 | MenuEntry::SubMenu(floem_menu) => { 90 | let _ = menu.append(&floem_menu.platform_submenu()); 91 | } 92 | } 93 | } 94 | menu 95 | } 96 | } 97 | 98 | pub struct MenuItem { 99 | pub(crate) id: String, 100 | pub(crate) title: String, 101 | pub(crate) enabled: bool, 102 | pub(crate) action: Option>, 103 | } 104 | 105 | impl From for MenuEntry { 106 | fn from(i: MenuItem) -> MenuEntry { 107 | MenuEntry::Item(i) 108 | } 109 | } 110 | 111 | impl MenuItem { 112 | pub fn new(title: impl Into) -> Self { 113 | static COUNTER: AtomicU64 = AtomicU64::new(0); 114 | let id = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); 115 | Self { 116 | id: id.to_string(), 117 | title: title.into(), 118 | enabled: true, 119 | action: None, 120 | } 121 | } 122 | 123 | pub fn action(mut self, action: impl Fn() + 'static) -> Self { 124 | self.action = Some(Box::new(action)); 125 | self 126 | } 127 | 128 | pub fn enabled(mut self, enabled: bool) -> Self { 129 | self.enabled = enabled; 130 | self 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/nav.rs: -------------------------------------------------------------------------------- 1 | use peniko::kurbo::{Point, Rect}; 2 | use winit::keyboard::NamedKey; 3 | 4 | use crate::{app_state::AppState, id::ViewId, view::view_tab_navigation}; 5 | 6 | pub(crate) fn view_arrow_navigation(key: NamedKey, app_state: &mut AppState, view: ViewId) { 7 | let focused = match app_state.focus { 8 | Some(id) => id, 9 | None => { 10 | view_tab_navigation( 11 | view, 12 | app_state, 13 | matches!(key, NamedKey::ArrowUp | NamedKey::ArrowLeft), 14 | ); 15 | return; 16 | } 17 | }; 18 | let rect = focused.layout_rect().inflate(10.0, 10.0); 19 | let center = rect.center(); 20 | let intersect_target = match key { 21 | NamedKey::ArrowUp => Rect::new(rect.x0, f64::NEG_INFINITY, rect.x1, center.y), 22 | NamedKey::ArrowDown => Rect::new(rect.x0, center.y, rect.x1, f64::INFINITY), 23 | NamedKey::ArrowLeft => Rect::new(f64::NEG_INFINITY, rect.y0, center.x, rect.y1), 24 | NamedKey::ArrowRight => Rect::new(center.x, rect.y0, f64::INFINITY, rect.y1), 25 | _ => panic!(), 26 | }; 27 | let center_target = match key { 28 | NamedKey::ArrowUp => { 29 | Rect::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::INFINITY, rect.y0) 30 | } 31 | NamedKey::ArrowDown => Rect::new(f64::NEG_INFINITY, rect.y1, f64::INFINITY, f64::INFINITY), 32 | NamedKey::ArrowLeft => { 33 | Rect::new(f64::NEG_INFINITY, f64::NEG_INFINITY, rect.x0, f64::INFINITY) 34 | } 35 | NamedKey::ArrowRight => Rect::new(rect.x1, f64::NEG_INFINITY, f64::INFINITY, f64::INFINITY), 36 | _ => panic!(), 37 | }; 38 | let mut keyboard_navigable: Vec = 39 | app_state.keyboard_navigable.iter().copied().collect(); 40 | keyboard_navigable.retain(|id| { 41 | let layout = id.layout_rect(); 42 | 43 | !layout.intersect(intersect_target).is_zero_area() 44 | && center_target.contains(layout.center()) 45 | && app_state.can_focus(*id) 46 | && *id != focused 47 | }); 48 | 49 | let mut new_focus = None; 50 | for id in keyboard_navigable { 51 | let id_rect = id.layout_rect(); 52 | let id_center = id_rect.center(); 53 | let id_edge = match key { 54 | NamedKey::ArrowUp => Point::new(id_center.x, id_rect.y1), 55 | NamedKey::ArrowDown => Point::new(id_center.x, id_rect.y0), 56 | NamedKey::ArrowLeft => Point::new(id_rect.x1, id_center.y), 57 | NamedKey::ArrowRight => Point::new(id_rect.x0, id_center.y), 58 | _ => panic!(), 59 | }; 60 | let id_distance = center.distance_squared(id_edge); 61 | if let Some((_, distance)) = new_focus { 62 | if id_distance < distance { 63 | new_focus = Some((id, id_distance)); 64 | } 65 | } else { 66 | new_focus = Some((id, id_distance)); 67 | } 68 | } 69 | 70 | if let Some((id, _)) = new_focus { 71 | app_state.clear_focus(); 72 | app_state.update_focus(id, true); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/pointer.rs: -------------------------------------------------------------------------------- 1 | use std::hash::{Hash, Hasher}; 2 | 3 | use winit::event::ButtonSource; 4 | pub use winit::event::{FingerId, Force}; 5 | 6 | use peniko::kurbo::{Point, Vec2}; 7 | 8 | use crate::keyboard::Modifiers; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct PointerWheelEvent { 12 | pub pos: Point, 13 | pub delta: Vec2, 14 | pub modifiers: Modifiers, 15 | } 16 | 17 | #[derive(Debug, Clone, PartialEq, Copy)] 18 | pub enum PointerButton { 19 | Mouse(MouseButton), 20 | Touch { 21 | finger_id: FingerId, 22 | force: Option, 23 | }, 24 | Unknown(u16), 25 | } 26 | 27 | impl Eq for PointerButton {} 28 | 29 | impl Hash for PointerButton { 30 | fn hash(&self, state: &mut H) { 31 | match self { 32 | PointerButton::Mouse(mouse_button) => mouse_button.hash(state), 33 | PointerButton::Touch { finger_id, .. } => finger_id.hash(state), 34 | PointerButton::Unknown(n) => n.hash(state), 35 | } 36 | } 37 | } 38 | 39 | impl From for PointerButton { 40 | fn from(value: ButtonSource) -> Self { 41 | match value { 42 | ButtonSource::Mouse(mouse_button) => PointerButton::Mouse(mouse_button.into()), 43 | ButtonSource::Touch { finger_id, force } => PointerButton::Touch { finger_id, force }, 44 | ButtonSource::Unknown(n) => PointerButton::Unknown(n), 45 | } 46 | } 47 | } 48 | 49 | impl PointerButton { 50 | pub fn is_primary(&self) -> bool { 51 | self.mouse_button() == MouseButton::Primary 52 | } 53 | 54 | pub fn is_secondary(&self) -> bool { 55 | self.mouse_button() == MouseButton::Secondary 56 | } 57 | 58 | pub fn is_auxiliary(&self) -> bool { 59 | self.mouse_button() == MouseButton::Auxiliary 60 | } 61 | 62 | pub fn mouse_button(self) -> MouseButton { 63 | match self { 64 | PointerButton::Mouse(mouse) => mouse, 65 | PointerButton::Touch { .. } => MouseButton::Primary, 66 | PointerButton::Unknown(button) => match button { 67 | 0 => MouseButton::Primary, 68 | 1 => MouseButton::Auxiliary, 69 | 2 => MouseButton::Secondary, 70 | 3 => MouseButton::X1, 71 | 4 => MouseButton::X2, 72 | _ => MouseButton::None, 73 | }, 74 | } 75 | } 76 | } 77 | 78 | #[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, Ord, PartialOrd)] 79 | pub enum MouseButton { 80 | Primary, 81 | Secondary, 82 | Auxiliary, 83 | X1, 84 | X2, 85 | None, 86 | } 87 | 88 | impl From for MouseButton { 89 | fn from(value: winit::event::MouseButton) -> Self { 90 | match value { 91 | winit::event::MouseButton::Left => Self::Primary, 92 | winit::event::MouseButton::Right => Self::Secondary, 93 | winit::event::MouseButton::Middle => Self::Auxiliary, 94 | winit::event::MouseButton::Back => Self::X1, 95 | winit::event::MouseButton::Forward => Self::X2, 96 | winit::event::MouseButton::Other(_) => Self::None, 97 | } 98 | } 99 | } 100 | 101 | impl MouseButton { 102 | pub fn is_primary(&self) -> bool { 103 | self == &MouseButton::Primary 104 | } 105 | 106 | pub fn is_secondary(&self) -> bool { 107 | self == &MouseButton::Secondary 108 | } 109 | 110 | pub fn is_auxiliary(&self) -> bool { 111 | self == &MouseButton::Auxiliary 112 | } 113 | 114 | pub fn is_x1(&self) -> bool { 115 | self == &MouseButton::X1 116 | } 117 | 118 | pub fn is_x2(&self) -> bool { 119 | self == &MouseButton::X2 120 | } 121 | } 122 | 123 | #[derive(Debug, Clone)] 124 | pub struct PointerInputEvent { 125 | pub pos: Point, 126 | pub button: PointerButton, 127 | pub modifiers: Modifiers, 128 | pub count: u8, 129 | } 130 | 131 | #[derive(Debug, Clone)] 132 | pub struct PointerMoveEvent { 133 | pub pos: Point, 134 | pub modifiers: Modifiers, 135 | } 136 | -------------------------------------------------------------------------------- /src/touchpad.rs: -------------------------------------------------------------------------------- 1 | use winit::event::TouchPhase; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct PinchGestureEvent { 5 | pub delta: f64, 6 | pub phase: TouchPhase, 7 | } 8 | -------------------------------------------------------------------------------- /src/unit.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use taffy::style::{Dimension, LengthPercentage, LengthPercentageAuto}; 4 | 5 | /// A pixel value 6 | #[derive(Debug, Clone, Copy, PartialEq)] 7 | pub struct Px(pub f64); 8 | 9 | /// A percent value 10 | #[derive(Debug, Clone, Copy, PartialEq)] 11 | pub struct Pct(pub f64); 12 | impl From for Pct { 13 | fn from(value: f32) -> Self { 14 | Pct(value as f64) 15 | } 16 | } 17 | 18 | impl From for Pct { 19 | fn from(value: i32) -> Self { 20 | Pct(value as f64) 21 | } 22 | } 23 | 24 | /// Used for automatically computed values 25 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 26 | pub struct Auto; 27 | 28 | impl From for Px { 29 | fn from(value: f64) -> Self { 30 | Px(value) 31 | } 32 | } 33 | 34 | impl From for Px { 35 | fn from(value: f32) -> Self { 36 | Px(value as f64) 37 | } 38 | } 39 | 40 | impl From for Px { 41 | fn from(value: i32) -> Self { 42 | Px(value as f64) 43 | } 44 | } 45 | 46 | #[derive(Debug, Clone, Copy, PartialEq)] 47 | pub enum PxPct { 48 | Px(f64), 49 | Pct(f64), 50 | } 51 | 52 | impl From for PxPct { 53 | fn from(value: Pct) -> Self { 54 | PxPct::Pct(value.0) 55 | } 56 | } 57 | 58 | impl From for PxPct 59 | where 60 | T: Into, 61 | { 62 | fn from(value: T) -> Self { 63 | PxPct::Px(value.into().0) 64 | } 65 | } 66 | 67 | #[derive(Debug, Clone, Copy, PartialEq)] 68 | pub enum PxPctAuto { 69 | Px(f64), 70 | Pct(f64), 71 | Auto, 72 | } 73 | 74 | impl From for PxPctAuto { 75 | fn from(value: Pct) -> Self { 76 | PxPctAuto::Pct(value.0) 77 | } 78 | } 79 | 80 | impl From for PxPctAuto { 81 | fn from(_: Auto) -> Self { 82 | PxPctAuto::Auto 83 | } 84 | } 85 | 86 | impl From for PxPctAuto 87 | where 88 | T: Into, 89 | { 90 | fn from(value: T) -> Self { 91 | PxPctAuto::Px(value.into().0) 92 | } 93 | } 94 | 95 | impl From for PxPctAuto { 96 | fn from(value: PxPct) -> Self { 97 | match value { 98 | PxPct::Pct(pct) => PxPctAuto::Pct(pct), 99 | PxPct::Px(px) => PxPctAuto::Px(px), 100 | } 101 | } 102 | } 103 | 104 | pub trait DurationUnitExt { 105 | fn minutes(self) -> Duration; 106 | fn seconds(self) -> Duration; 107 | fn millis(self) -> Duration; 108 | } 109 | impl DurationUnitExt for u64 { 110 | fn minutes(self) -> Duration { 111 | Duration::from_secs(self) 112 | } 113 | 114 | fn seconds(self) -> Duration { 115 | Duration::from_secs(self) 116 | } 117 | 118 | fn millis(self) -> Duration { 119 | Duration::from_millis(self) 120 | } 121 | } 122 | 123 | pub trait UnitExt { 124 | fn pct(self) -> Pct; 125 | fn px(self) -> Px; 126 | } 127 | 128 | impl UnitExt for f64 { 129 | fn pct(self) -> Pct { 130 | Pct(self) 131 | } 132 | 133 | fn px(self) -> Px { 134 | Px(self) 135 | } 136 | } 137 | 138 | impl UnitExt for i32 { 139 | fn pct(self) -> Pct { 140 | Pct(self as f64) 141 | } 142 | 143 | fn px(self) -> Px { 144 | Px(self as f64) 145 | } 146 | } 147 | 148 | impl From for Dimension { 149 | fn from(value: PxPctAuto) -> Self { 150 | match value { 151 | PxPctAuto::Px(v) => Dimension::Length(v as f32), 152 | PxPctAuto::Pct(v) => Dimension::Percent(v as f32 / 100.0), 153 | PxPctAuto::Auto => Dimension::Auto, 154 | } 155 | } 156 | } 157 | 158 | impl From for LengthPercentage { 159 | fn from(value: PxPct) -> Self { 160 | match value { 161 | PxPct::Px(v) => LengthPercentage::Length(v as f32), 162 | PxPct::Pct(v) => LengthPercentage::Percent(v as f32 / 100.0), 163 | } 164 | } 165 | } 166 | 167 | impl From for LengthPercentageAuto { 168 | fn from(value: PxPctAuto) -> Self { 169 | match value { 170 | PxPctAuto::Px(v) => LengthPercentageAuto::Length(v as f32), 171 | PxPctAuto::Pct(v) => LengthPercentageAuto::Percent(v as f32 / 100.0), 172 | PxPctAuto::Auto => LengthPercentageAuto::Auto, 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/update.rs: -------------------------------------------------------------------------------- 1 | use std::{any::Any, cell::RefCell, collections::HashMap}; 2 | 3 | use peniko::kurbo::{Point, Rect, Size, Vec2}; 4 | use winit::window::ResizeDirection; 5 | 6 | use crate::{id::ViewId, menu::Menu, view::View}; 7 | 8 | thread_local! { 9 | /// Stores all the update message with their original `ViewId` 10 | /// When a view sends a update message, we need to store them in `CENTRAL_UPDATE_MESSAGES`, 11 | /// because when the view was built, it probably hasn't got a parent yet, 12 | /// so we didn't know which window root view it belonged to. 13 | /// In `process_update_messages`, it will parse all the entries in `CENTRAL_UPDATE_MESSAGES`, 14 | /// and put the messages to `UPDATE_MESSAGES` according to their root `ViewId`. 15 | pub(crate) static CENTRAL_UPDATE_MESSAGES: RefCell> = Default::default(); 16 | /// Stores a queue of update messages for each view. This is a list of build in messages, including a built-in State message 17 | /// that you can use to send a state update to a view. 18 | pub(crate) static UPDATE_MESSAGES: RefCell>> = Default::default(); 19 | /// Similar to `CENTRAL_UPDATE_MESSAGES` but for `DEFERRED_UPDATE_MESSAGES` 20 | pub(crate) static CENTRAL_DEFERRED_UPDATE_MESSAGES: RefCell)>> = Default::default(); 21 | pub(crate) static DEFERRED_UPDATE_MESSAGES: RefCell = Default::default(); 22 | /// It stores the active view handle, so that when you dispatch an action, it knows 23 | /// which view handle it submitted to 24 | pub(crate) static CURRENT_RUNNING_VIEW_HANDLE: RefCell = RefCell::new(ViewId::new()); 25 | } 26 | 27 | type DeferredUpdateMessages = HashMap)>>; 28 | 29 | pub(crate) enum UpdateMessage { 30 | Focus(ViewId), 31 | ClearFocus(ViewId), 32 | ClearAppFocus, 33 | Active(ViewId), 34 | ClearActive(ViewId), 35 | WindowScale(f64), 36 | Disabled { 37 | id: ViewId, 38 | is_disabled: bool, 39 | }, 40 | RequestPaint, 41 | State { 42 | id: ViewId, 43 | state: Box, 44 | }, 45 | KeyboardNavigable { 46 | id: ViewId, 47 | }, 48 | RemoveKeyboardNavigable { 49 | id: ViewId, 50 | }, 51 | Draggable { 52 | id: ViewId, 53 | }, 54 | ToggleWindowMaximized, 55 | SetWindowMaximized(bool), 56 | MinimizeWindow, 57 | DragWindow, 58 | DragResizeWindow(ResizeDirection), 59 | SetWindowDelta(Vec2), 60 | ShowContextMenu { 61 | menu: Menu, 62 | pos: Option, 63 | }, 64 | WindowMenu { 65 | menu: Menu, 66 | }, 67 | SetWindowTitle { 68 | title: String, 69 | }, 70 | AddOverlay { 71 | id: ViewId, 72 | position: Point, 73 | view: Box Box>, 74 | }, 75 | RemoveOverlay { 76 | id: ViewId, 77 | }, 78 | Inspect, 79 | ScrollTo { 80 | id: ViewId, 81 | rect: Option, 82 | }, 83 | FocusWindow, 84 | SetImeAllowed { 85 | allowed: bool, 86 | }, 87 | SetImeCursorArea { 88 | position: Point, 89 | size: Size, 90 | }, 91 | WindowVisible(bool), 92 | ViewTransitionAnimComplete(ViewId), 93 | } 94 | -------------------------------------------------------------------------------- /src/view_storage.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | use slotmap::{SecondaryMap, SlotMap}; 4 | 5 | use crate::{id::ViewId, view::AnyView, view_state::ViewState, IntoView}; 6 | 7 | thread_local! { 8 | pub(crate) static VIEW_STORAGE: RefCell = Default::default(); 9 | } 10 | 11 | pub(crate) struct ViewStorage { 12 | pub(crate) taffy: Rc>, 13 | pub(crate) view_ids: SlotMap, 14 | pub(crate) views: SecondaryMap>>, 15 | pub(crate) children: SecondaryMap>, 16 | // the parent of a View 17 | pub(crate) parent: SecondaryMap>, 18 | /// Cache the root [`ViewId`] for a view 19 | pub(crate) root: SecondaryMap>, 20 | pub(crate) states: SecondaryMap>>, 21 | pub(crate) stale_view_state: Rc>, 22 | pub(crate) stale_view: Rc>, 23 | } 24 | 25 | impl Default for ViewStorage { 26 | fn default() -> Self { 27 | Self::new() 28 | } 29 | } 30 | 31 | impl ViewStorage { 32 | pub fn new() -> Self { 33 | let mut taffy = taffy::TaffyTree::default(); 34 | taffy.disable_rounding(); 35 | let state_view_state = ViewState::new(&mut taffy); 36 | 37 | Self { 38 | taffy: Rc::new(RefCell::new(taffy)), 39 | view_ids: Default::default(), 40 | views: Default::default(), 41 | children: Default::default(), 42 | parent: Default::default(), 43 | root: Default::default(), 44 | states: Default::default(), 45 | stale_view_state: Rc::new(RefCell::new(state_view_state)), 46 | stale_view: Rc::new(RefCell::new( 47 | crate::views::Empty { 48 | id: ViewId::default(), 49 | } 50 | .into_any(), 51 | )), 52 | } 53 | } 54 | 55 | /// Returns the deepest view ID encountered traversing parents. It does *not* guarantee 56 | /// that it is a real window root; any caller should perform the same test 57 | /// of `window_tracking::is_known_root()` that `ViewId.root()` does before 58 | /// assuming the returned value is really a window root. 59 | pub(crate) fn root_view_id(&self, id: ViewId) -> Option { 60 | if let Some(p) = self.parent.get(id).unwrap_or(&None) { 61 | self.root_view_id(*p) 62 | } else { 63 | Some(id) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/view_tuple.rs: -------------------------------------------------------------------------------- 1 | use taffy::FlexDirection; 2 | 3 | use crate::{ 4 | view::{IntoView, View}, 5 | views::{create_stack, Stack}, 6 | }; 7 | 8 | pub trait ViewTupleFlat { 9 | fn flatten(self) -> Vec>; 10 | } 11 | 12 | // Macro to implement ViewTupleFlat for tuples of Vec> 13 | macro_rules! impl_view_tuple_flat { 14 | ($capacity:expr, $($t:ident),+) => { 15 | impl<$($t: IntoIterator>),+> ViewTupleFlat for ($($t,)+) { 16 | fn flatten(self) -> Vec> { 17 | #[allow(non_snake_case)] 18 | let ($($t,)+) = self; 19 | let mut views = Vec::new(); 20 | $( 21 | views.extend($t); 22 | )+ 23 | views 24 | } 25 | } 26 | }; 27 | } 28 | impl_view_tuple_flat!(1, A); 29 | impl_view_tuple_flat!(2, A, B); 30 | impl_view_tuple_flat!(3, A, B, C); 31 | impl_view_tuple_flat!(4, A, B, C, D); 32 | impl_view_tuple_flat!(5, A, B, C, D, E); 33 | impl_view_tuple_flat!(6, A, B, C, D, E, F); 34 | impl_view_tuple_flat!(7, A, B, C, D, E, F, G); 35 | impl_view_tuple_flat!(8, A, B, C, D, E, F, G, H); 36 | impl_view_tuple_flat!(9, A, B, C, D, E, F, G, H, I); 37 | impl_view_tuple_flat!(10, A, B, C, D, E, F, G, H, I, J); 38 | impl_view_tuple_flat!(11, A, B, C, D, E, F, G, H, I, J, K); 39 | impl_view_tuple_flat!(12, A, B, C, D, E, F, G, H, I, J, K, L); 40 | impl_view_tuple_flat!(13, A, B, C, D, E, F, G, H, I, J, K, L, M); 41 | impl_view_tuple_flat!(14, A, B, C, D, E, F, G, H, I, J, K, L, M, N); 42 | impl_view_tuple_flat!(15, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O); 43 | impl_view_tuple_flat!(16, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); 44 | 45 | pub trait ViewTuple { 46 | fn into_views(self) -> Vec>; 47 | fn stack(self, direction: FlexDirection) -> Stack; 48 | fn v_stack(self) -> Stack 49 | where 50 | Self: Sized, 51 | { 52 | ViewTuple::stack(self, FlexDirection::Column) 53 | } 54 | fn h_stack(self) -> Stack 55 | where 56 | Self: Sized, 57 | { 58 | ViewTuple::stack(self, FlexDirection::Row) 59 | } 60 | } 61 | 62 | // Macro to implement ViewTuple for tuples of Views and Vec> 63 | macro_rules! impl_view_tuple { 64 | ($capacity:expr, $($t:ident),+) => { 65 | impl<$($t: IntoView + 'static),+> ViewTuple for ($($t,)+) { 66 | fn into_views(self) -> Vec> { 67 | #[allow(non_snake_case)] 68 | let ($($t,)+) = self; 69 | vec![ 70 | $($t.into_any(),)+ 71 | ] 72 | } 73 | fn stack(self, direction: FlexDirection) -> Stack { 74 | create_stack(self.into_views(), Some(direction)) 75 | } 76 | } 77 | 78 | impl<$($t: IntoView + 'static),+> IntoView for ($($t,)+) { 79 | type V = crate::views::Stack; 80 | 81 | fn into_view(self) -> Self::V { 82 | #[allow(non_snake_case)] 83 | let ($($t,)+) = self; 84 | let views = vec![ $($t.into_any(),)+ ]; 85 | crate::views::create_stack(views, None) 86 | } 87 | } 88 | }; 89 | } 90 | 91 | impl_view_tuple!(1, A); 92 | impl_view_tuple!(2, A, B); 93 | impl_view_tuple!(3, A, B, C); 94 | impl_view_tuple!(4, A, B, C, D); 95 | impl_view_tuple!(5, A, B, C, D, E); 96 | impl_view_tuple!(6, A, B, C, D, E, F); 97 | impl_view_tuple!(7, A, B, C, D, E, F, G); 98 | impl_view_tuple!(8, A, B, C, D, E, F, G, H); 99 | impl_view_tuple!(9, A, B, C, D, E, F, G, H, I); 100 | impl_view_tuple!(10, A, B, C, D, E, F, G, H, I, J); 101 | impl_view_tuple!(11, A, B, C, D, E, F, G, H, I, J, K); 102 | impl_view_tuple!(12, A, B, C, D, E, F, G, H, I, J, K, L); 103 | impl_view_tuple!(13, A, B, C, D, E, F, G, H, I, J, K, L, M); 104 | impl_view_tuple!(14, A, B, C, D, E, F, G, H, I, J, K, L, M, N); 105 | impl_view_tuple!(15, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O); 106 | impl_view_tuple!(16, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); 107 | -------------------------------------------------------------------------------- /src/views/button.rs: -------------------------------------------------------------------------------- 1 | use crate::{style_class, views::Decorators, IntoView, View, ViewId}; 2 | use core::ops::FnMut; 3 | 4 | style_class!(pub ButtonClass); 5 | 6 | pub fn button(child: V) -> Button { 7 | Button::new(child) 8 | } 9 | 10 | pub struct Button { 11 | id: ViewId, 12 | } 13 | impl View for Button { 14 | fn id(&self) -> ViewId { 15 | self.id 16 | } 17 | } 18 | impl Button { 19 | pub fn new(child: impl IntoView) -> Self { 20 | let id = ViewId::new(); 21 | id.add_child(Box::new(child.into_view())); 22 | Button { id }.keyboard_navigable().class(ButtonClass) 23 | } 24 | 25 | pub fn action(self, mut on_press: impl FnMut() + 'static) -> Self { 26 | self.on_click_stop(move |_| { 27 | on_press(); 28 | }) 29 | } 30 | } 31 | 32 | pub trait ButtonExt { 33 | fn button(self) -> Button; 34 | } 35 | impl ButtonExt for T { 36 | fn button(self) -> Button { 37 | button(self) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/views/canvas.rs: -------------------------------------------------------------------------------- 1 | use floem_reactive::{create_tracker, SignalTracker}; 2 | use peniko::kurbo::Size; 3 | 4 | use crate::{context::PaintCx, id::ViewId, view::View}; 5 | 6 | /// A canvas view 7 | #[allow(clippy::type_complexity)] 8 | pub struct Canvas { 9 | id: ViewId, 10 | paint_fn: Box, 11 | size: Size, 12 | tracker: Option, 13 | } 14 | 15 | /// Creates a new Canvas view that can be used for custom painting 16 | /// 17 | /// A [`Canvas`] provides a low-level interface for custom drawing operations. The supplied 18 | /// paint function will be called whenever the view needs to be rendered, and any signals accessed 19 | /// within the paint function will automatically trigger repaints when they change. 20 | /// 21 | /// 22 | /// # Example 23 | /// ```rust 24 | /// use floem::prelude::*; 25 | /// use palette::css; 26 | /// use peniko::kurbo::Rect; 27 | /// canvas(move |cx, size| { 28 | /// cx.fill( 29 | /// &Rect::ZERO 30 | /// .with_size(size) 31 | /// .to_rounded_rect(8.), 32 | /// css::PURPLE, 33 | /// 0., 34 | /// ); 35 | /// }) 36 | /// .style(|s| s.size(100, 300)); 37 | /// ``` 38 | pub fn canvas(paint: impl Fn(&mut PaintCx, Size) + 'static) -> Canvas { 39 | let id = ViewId::new(); 40 | 41 | Canvas { 42 | id, 43 | paint_fn: Box::new(paint), 44 | size: Default::default(), 45 | tracker: None, 46 | } 47 | } 48 | 49 | impl View for Canvas { 50 | fn id(&self) -> ViewId { 51 | self.id 52 | } 53 | 54 | fn debug_name(&self) -> std::borrow::Cow<'static, str> { 55 | "Canvas".into() 56 | } 57 | 58 | fn compute_layout( 59 | &mut self, 60 | _cx: &mut crate::context::ComputeLayoutCx, 61 | ) -> Option { 62 | self.size = self.id.get_size().unwrap_or_default(); 63 | None 64 | } 65 | 66 | fn paint(&mut self, cx: &mut PaintCx) { 67 | let id = self.id; 68 | let paint = &self.paint_fn; 69 | 70 | if self.tracker.is_none() { 71 | self.tracker = Some(create_tracker(move || { 72 | id.request_paint(); 73 | })); 74 | } 75 | 76 | let tracker = self.tracker.as_ref().unwrap(); 77 | tracker.track(|| { 78 | paint(cx, self.size); 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/views/clip.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | 3 | use peniko::kurbo::Size; 4 | 5 | use crate::{ 6 | id::ViewId, 7 | view::{IntoView, View}, 8 | }; 9 | 10 | /// A wrapper around a child View to clip painting. See [`clip`]. 11 | pub struct Clip { 12 | id: ViewId, 13 | } 14 | 15 | /// A clip is a wrapper around a child View that will clip the painting of the child so that it does not show outside of the viewport of the [`Clip`]. 16 | /// 17 | /// This can be useful for limiting child painting, including for rounded borders using border radius. 18 | pub fn clip(child: V) -> Clip { 19 | let child = child.into_view(); 20 | let id = ViewId::new(); 21 | id.set_children([child]); 22 | Clip { id } 23 | } 24 | 25 | impl View for Clip { 26 | fn id(&self) -> ViewId { 27 | self.id 28 | } 29 | 30 | fn debug_name(&self) -> std::borrow::Cow<'static, str> { 31 | "Clip".into() 32 | } 33 | 34 | fn paint(&mut self, cx: &mut crate::context::PaintCx) { 35 | cx.save(); 36 | let view_state = self.id.state(); 37 | let border_radius = view_state.borrow().combined_style.builtin().border_radius(); 38 | let size = self 39 | .id 40 | .get_layout() 41 | .map(|layout| Size::new(layout.size.width as f64, layout.size.height as f64)) 42 | .unwrap_or_default(); 43 | 44 | let radius = match border_radius { 45 | crate::unit::PxPct::Px(px) => px, 46 | crate::unit::PxPct::Pct(pct) => size.min_side() * (pct / 100.), 47 | }; 48 | if radius > 0.0 { 49 | let rect = size.to_rect().to_rounded_rect(radius); 50 | cx.clip(&rect); 51 | } else { 52 | cx.clip(&size.to_rect()); 53 | } 54 | cx.paint_children(self.id); 55 | cx.restore(); 56 | } 57 | } 58 | 59 | /// A trait that adds a `clip` method to any type that implements `IntoView`. 60 | pub trait ClipExt { 61 | /// Wrap the view in a clip view. 62 | fn clip(self) -> Clip; 63 | } 64 | 65 | impl ClipExt for T { 66 | fn clip(self) -> Clip { 67 | clip(self) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/views/container.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | 3 | use crate::{ 4 | id::ViewId, 5 | view::{IntoView, View}, 6 | }; 7 | 8 | /// A simple wrapper around another View. See [`container`]. 9 | pub struct Container { 10 | id: ViewId, 11 | } 12 | 13 | /// A simple wrapper around another View 14 | /// 15 | /// A [`Container`] is useful for wrapping another [View](crate::view::View). This is often useful for allowing another 16 | /// set of styles completely separate from the child View that is being wrapped. 17 | pub fn container(child: V) -> Container { 18 | let id = ViewId::new(); 19 | id.set_children([child.into_view()]); 20 | 21 | Container { id } 22 | } 23 | 24 | impl View for Container { 25 | fn id(&self) -> ViewId { 26 | self.id 27 | } 28 | 29 | fn debug_name(&self) -> std::borrow::Cow<'static, str> { 30 | "Container".into() 31 | } 32 | } 33 | 34 | /// A trait that adds a `container` method to any type that implements `IntoView`. 35 | pub trait ContainerExt { 36 | /// Wrap the view in a container. 37 | fn container(self) -> Container; 38 | } 39 | 40 | impl ContainerExt for T { 41 | fn container(self) -> Container { 42 | container(self) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/views/drag_resize_window_area.rs: -------------------------------------------------------------------------------- 1 | use winit::window::ResizeDirection; 2 | 3 | use crate::{ 4 | action::drag_resize_window, 5 | event::EventListener, 6 | id::ViewId, 7 | style::CursorStyle, 8 | view::{IntoView, View}, 9 | }; 10 | 11 | use super::Decorators; 12 | 13 | /// A view that will resize the window when the mouse is dragged. See [`drag_resize_window_area`]. 14 | /// 15 | /// ## Platform-specific 16 | /// 17 | /// - **macOS:** Not supported. 18 | /// - **iOS / Android / Web / Orbital:** Not supported. 19 | pub struct DragResizeWindowArea { 20 | id: ViewId, 21 | } 22 | 23 | /// A view that will resize the window when the mouse is dragged. 24 | /// 25 | /// ## Platform-specific 26 | /// 27 | /// - **macOS:** Not supported. 28 | /// - **iOS / Android / Web / Orbital:** Not supported. 29 | pub fn drag_resize_window_area( 30 | direction: ResizeDirection, 31 | child: V, 32 | ) -> DragResizeWindowArea { 33 | let id = ViewId::new(); 34 | id.set_children([child.into_view()]); 35 | DragResizeWindowArea { id } 36 | .on_event_stop(EventListener::PointerDown, move |_| { 37 | drag_resize_window(direction) 38 | }) 39 | .style(move |s| { 40 | let cursor = match direction { 41 | ResizeDirection::East => CursorStyle::ColResize, 42 | ResizeDirection::West => CursorStyle::ColResize, 43 | ResizeDirection::North => CursorStyle::RowResize, 44 | ResizeDirection::South => CursorStyle::RowResize, 45 | ResizeDirection::NorthEast => CursorStyle::NeswResize, 46 | ResizeDirection::SouthWest => CursorStyle::NeswResize, 47 | ResizeDirection::SouthEast => CursorStyle::NwseResize, 48 | ResizeDirection::NorthWest => CursorStyle::NwseResize, 49 | }; 50 | s.cursor(cursor) 51 | }) 52 | } 53 | 54 | impl View for DragResizeWindowArea { 55 | fn id(&self) -> ViewId { 56 | self.id 57 | } 58 | 59 | fn debug_name(&self) -> std::borrow::Cow<'static, str> { 60 | "Drag-Resize Window Area".into() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/views/drag_window_area.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | action::{drag_window, toggle_window_maximized}, 3 | event::{Event, EventListener}, 4 | id::ViewId, 5 | view::{IntoView, View}, 6 | }; 7 | 8 | use super::Decorators; 9 | 10 | /// A view that will move the window when the mouse is dragged. See [`drag_window_area`]. 11 | pub struct DragWindowArea { 12 | id: ViewId, 13 | } 14 | 15 | /// A view that will move the window when the mouse is dragged. 16 | /// 17 | /// This can be useful when the window has the title bar turned off and you want to be able to still drag the window. 18 | pub fn drag_window_area(child: V) -> DragWindowArea { 19 | let id = ViewId::new(); 20 | id.set_children([child]); 21 | DragWindowArea { id } 22 | .on_event_stop(EventListener::PointerDown, |e| { 23 | if let Event::PointerDown(input_event) = e { 24 | if input_event.button.is_primary() { 25 | drag_window(); 26 | } 27 | } 28 | }) 29 | .on_double_click_stop(|_| toggle_window_maximized()) 30 | } 31 | impl View for DragWindowArea { 32 | fn id(&self) -> ViewId { 33 | self.id 34 | } 35 | 36 | fn debug_name(&self) -> std::borrow::Cow<'static, str> { 37 | "Drag Window Area".into() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/views/dyn_view.rs: -------------------------------------------------------------------------------- 1 | use floem_reactive::{as_child_of_current_scope, create_updater, Scope}; 2 | 3 | use crate::{ 4 | id::ViewId, 5 | view::{IntoView, View}, 6 | }; 7 | 8 | /// A container for a dynamically updating View. See [`dyn_view`] 9 | pub struct DynamicView { 10 | id: ViewId, 11 | child_scope: Scope, 12 | } 13 | 14 | /// A container for a dynamically updating View 15 | pub fn dyn_view(view_fn: VF) -> DynamicView 16 | where 17 | VF: Fn() -> IV + 'static, 18 | IV: IntoView, 19 | { 20 | let id = ViewId::new(); 21 | let view_fn = Box::new(as_child_of_current_scope(move |_| view_fn().into_any())); 22 | 23 | let (child, child_scope) = create_updater( 24 | move || view_fn(()), 25 | move |(new_view, new_scope)| { 26 | let current_children = id.children(); 27 | id.set_children([new_view]); 28 | id.update_state((current_children, new_scope)); 29 | }, 30 | ); 31 | 32 | id.set_children([child]); 33 | DynamicView { id, child_scope } 34 | } 35 | 36 | impl View for DynamicView { 37 | fn id(&self) -> ViewId { 38 | self.id 39 | } 40 | 41 | fn update(&mut self, cx: &mut crate::context::UpdateCx, state: Box) { 42 | if let Ok(val) = state.downcast::<(Vec, Scope)>() { 43 | let old_child_scope = self.child_scope; 44 | let (old_children, child_scope) = *val; 45 | self.child_scope = child_scope; 46 | for child in old_children { 47 | cx.app_state_mut().remove_view(child); 48 | } 49 | old_child_scope.dispose(); 50 | self.id.request_all(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/views/editor/color.rs: -------------------------------------------------------------------------------- 1 | use strum_macros::{Display, EnumIter, EnumString, IntoStaticStr}; 2 | 3 | #[derive(Display, EnumString, EnumIter, IntoStaticStr, Debug, Clone, Copy, PartialEq, Eq)] 4 | pub enum EditorColor { 5 | #[strum(serialize = "editor.background")] 6 | Background, 7 | #[strum(serialize = "editor.scroll_bar")] 8 | Scrollbar, 9 | #[strum(serialize = "editor.dropdown_shadow")] 10 | DropdownShadow, 11 | #[strum(serialize = "editor.foreground")] 12 | Foreground, 13 | #[strum(serialize = "editor.dim")] 14 | Dim, 15 | #[strum(serialize = "editor.focus")] 16 | Focus, 17 | #[strum(serialize = "editor.caret")] 18 | Caret, 19 | #[strum(serialize = "editor.selection")] 20 | Selection, 21 | #[strum(serialize = "editor.current_line")] 22 | CurrentLine, 23 | #[strum(serialize = "editor.link")] 24 | Link, 25 | #[strum(serialize = "editor.visible_whitespace")] 26 | VisibleWhitespace, 27 | #[strum(serialize = "editor.indent_guide")] 28 | IndentGuide, 29 | #[strum(serialize = "editor.sticky_header_background")] 30 | StickyHeaderBackground, 31 | #[strum(serialize = "editor.preedit.underline")] 32 | PreeditUnderline, 33 | } 34 | -------------------------------------------------------------------------------- /src/views/editor/command.rs: -------------------------------------------------------------------------------- 1 | use floem_editor_core::command::{ 2 | EditCommand, MotionModeCommand, MoveCommand, MultiSelectionCommand, ScrollCommand, 3 | }; 4 | use strum::EnumMessage; 5 | 6 | #[derive(Clone, Debug, PartialEq, Eq)] 7 | pub enum Command { 8 | Edit(EditCommand), 9 | Move(MoveCommand), 10 | Scroll(ScrollCommand), 11 | MotionMode(MotionModeCommand), 12 | MultiSelection(MultiSelectionCommand), 13 | } 14 | 15 | impl Command { 16 | pub fn desc(&self) -> Option<&'static str> { 17 | match &self { 18 | Command::Edit(cmd) => cmd.get_message(), 19 | Command::Move(cmd) => cmd.get_message(), 20 | Command::Scroll(cmd) => cmd.get_message(), 21 | Command::MotionMode(cmd) => cmd.get_message(), 22 | Command::MultiSelection(cmd) => cmd.get_message(), 23 | } 24 | } 25 | 26 | pub fn str(&self) -> &'static str { 27 | match &self { 28 | Command::Edit(cmd) => cmd.into(), 29 | Command::Move(cmd) => cmd.into(), 30 | Command::Scroll(cmd) => cmd.into(), 31 | Command::MotionMode(cmd) => cmd.into(), 32 | Command::MultiSelection(cmd) => cmd.into(), 33 | } 34 | } 35 | } 36 | 37 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 38 | pub enum CommandExecuted { 39 | Yes, 40 | No, 41 | } 42 | -------------------------------------------------------------------------------- /src/views/editor/id.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::AtomicU64; 2 | 3 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 4 | pub struct Id(u64); 5 | 6 | impl Id { 7 | /// Allocate a new, unique `Id`. 8 | pub fn next() -> Id { 9 | static TIMER_COUNTER: AtomicU64 = AtomicU64::new(0); 10 | Id(TIMER_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)) 11 | } 12 | 13 | pub fn to_raw(self) -> u64 { 14 | self.0 15 | } 16 | } 17 | 18 | pub type EditorId = Id; 19 | -------------------------------------------------------------------------------- /src/views/editor/listener.rs: -------------------------------------------------------------------------------- 1 | use floem_reactive::{RwSignal, Scope, SignalGet, SignalUpdate}; 2 | 3 | /// A signal listener that receives 'events' from the outside and runs the callback. 4 | /// 5 | /// This is implemented using effects and normal rw signals. This should be used when it doesn't 6 | /// make sense to think of it as 'storing' a value, like an `RwSignal` would typically be used for. 7 | /// 8 | /// Copied/Cloned listeners refer to the same listener. 9 | #[derive(Debug)] 10 | pub struct Listener { 11 | cx: Scope, 12 | val: RwSignal>, 13 | } 14 | 15 | impl Listener { 16 | pub fn new(cx: Scope, on_val: impl Fn(T) + 'static) -> Listener { 17 | let val = cx.create_rw_signal(None); 18 | 19 | let listener = Listener { val, cx }; 20 | listener.listen(on_val); 21 | 22 | listener 23 | } 24 | 25 | /// Construct a listener when you can't yet give it a callback. 26 | /// 27 | /// Call `listen` to set a callback. 28 | pub fn new_empty(cx: Scope) -> Listener { 29 | let val = cx.create_rw_signal(None); 30 | Listener { val, cx } 31 | } 32 | 33 | pub fn scope(&self) -> Scope { 34 | self.cx 35 | } 36 | 37 | /// Listen for values sent to this listener. 38 | pub fn listen(self, on_val: impl Fn(T) + 'static) { 39 | self.listen_with(self.cx, on_val) 40 | } 41 | 42 | /// Listen for values sent to this listener. 43 | /// 44 | /// Allows creating the effect with a custom scope, letting it be disposed of. 45 | pub fn listen_with(self, cx: Scope, on_val: impl Fn(T) + 'static) { 46 | let val = self.val; 47 | 48 | cx.create_effect(move |_| { 49 | // TODO(minor): Signals could have a `take` method to avoid cloning. 50 | if let Some(cmd) = val.get() { 51 | on_val(cmd); 52 | } 53 | }); 54 | } 55 | 56 | /// Send a value to the listener. 57 | pub fn send(&self, v: T) { 58 | self.val.set(Some(v)); 59 | } 60 | } 61 | 62 | impl Copy for Listener {} 63 | 64 | impl Clone for Listener { 65 | fn clone(&self) -> Self { 66 | *self 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/views/empty.rs: -------------------------------------------------------------------------------- 1 | use crate::{id::ViewId, view::View}; 2 | 3 | /// An empty View. See [`empty`]. 4 | pub struct Empty { 5 | pub(crate) id: ViewId, 6 | } 7 | 8 | /// An empty View. This view can still have a size, background, border radius, and outline. 9 | /// 10 | /// This view can also be useful if you have another view that requires a child element but there is not a meaningful child element that needs to be provided. 11 | pub fn empty() -> Empty { 12 | Empty { id: ViewId::new() } 13 | } 14 | 15 | impl View for Empty { 16 | fn id(&self) -> ViewId { 17 | self.id 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/views/value_container.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | use floem_reactive::{ 4 | create_effect, create_rw_signal, create_updater, RwSignal, SignalGet, SignalUpdate, 5 | }; 6 | 7 | use crate::{ 8 | context::UpdateCx, 9 | id::ViewId, 10 | view::{IntoView, View}, 11 | }; 12 | 13 | /// A wrapper around another View that has value updates. See [`value_container`] 14 | pub struct ValueContainer { 15 | id: ViewId, 16 | on_update: Option>, 17 | } 18 | 19 | /// A convenience function that creates two signals for use in a [`value_container`] 20 | /// - The outbound signal enables a widget's internal input event handlers 21 | /// to publish state changes via `ValueContainer::on_update`. 22 | /// - The inbound signal propagates value changes in the producer function 23 | /// into a widget's internals. 24 | pub fn create_value_container_signals( 25 | producer: impl Fn() -> T + 'static, 26 | ) -> (RwSignal, RwSignal) 27 | where 28 | T: Clone + 'static, 29 | { 30 | let initial_value = producer(); 31 | 32 | let inbound_signal = create_rw_signal(initial_value.clone()); 33 | create_effect(move |_| { 34 | let checked = producer(); 35 | inbound_signal.set(checked); 36 | }); 37 | 38 | let outbound_signal = create_rw_signal(initial_value.clone()); 39 | create_effect(move |_| { 40 | let checked = outbound_signal.get(); 41 | inbound_signal.set(checked); 42 | }); 43 | 44 | (inbound_signal, outbound_signal) 45 | } 46 | 47 | /// A wrapper around another View that has value updates. 48 | /// 49 | /// A [`ValueContainer`] is useful for wrapping another [View](crate::view::View). 50 | /// This is to provide the `on_update` method which can notify when the view's 51 | /// internal value was get changed 52 | pub fn value_container( 53 | child: V, 54 | value_update: impl Fn() -> T + 'static, 55 | ) -> ValueContainer { 56 | let id = ViewId::new(); 57 | let child = child.into_view(); 58 | id.set_children([child]); 59 | create_updater(value_update, move |new_value| id.update_state(new_value)); 60 | ValueContainer { 61 | id, 62 | on_update: None, 63 | } 64 | } 65 | 66 | impl ValueContainer { 67 | pub fn on_update(mut self, action: impl Fn(T) + 'static) -> Self { 68 | self.on_update = Some(Box::new(action)); 69 | self 70 | } 71 | } 72 | 73 | impl View for ValueContainer { 74 | fn id(&self) -> ViewId { 75 | self.id 76 | } 77 | 78 | fn update(&mut self, _cx: &mut UpdateCx, state: Box) { 79 | if let Ok(state) = state.downcast::() { 80 | if let Some(on_update) = self.on_update.as_ref() { 81 | on_update(*state); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tiny_skia/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floem_tiny_skia_renderer" 3 | version.workspace = true 4 | edition = "2021" 5 | repository = "https://github.com/lapce/floem" 6 | description = "A native Rust UI library with fine-grained reactivity" 7 | license.workspace = true 8 | 9 | [dependencies] 10 | peniko = { workspace = true } 11 | resvg = { workspace = true } 12 | raw-window-handle = { workspace = true } 13 | 14 | anyhow = "1.0.69" 15 | floem_renderer = { path = "../renderer", version = "0.2.0" } 16 | softbuffer = "0.4.1" 17 | -------------------------------------------------------------------------------- /vello/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floem_vello_renderer" 3 | version.workspace = true 4 | edition = "2021" 5 | repository = "https://github.com/lapce/floem" 6 | description = "A native Rust UI library with fine-grained reactivity" 7 | license.workspace = true 8 | 9 | [dependencies] 10 | peniko = { workspace = true } 11 | wgpu = { workspace = true } 12 | 13 | anyhow = "1.0.69" 14 | vello = "0.4.0" 15 | vello_svg = "0.6.0" 16 | floem_renderer = { path = "../renderer", version = "0.2.0" } 17 | -------------------------------------------------------------------------------- /vger/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "floem_vger_renderer" 3 | version.workspace = true 4 | edition = "2021" 5 | repository = "https://github.com/lapce/floem" 6 | description = "A native Rust UI library with fine-grained reactivity" 7 | license.workspace = true 8 | 9 | [dependencies] 10 | image = { workspace = true } 11 | resvg = { workspace = true } 12 | peniko = { workspace = true } 13 | wgpu = { workspace = true } 14 | 15 | anyhow = "1.0.69" 16 | floem-vger-rs = { git = "https://github.com/lapce/vger-rs.git", rev = "b2441cc85b1d555a3c74e2cdced08382460759df", version = "0.3.1", package = "floem-vger" } 17 | floem_renderer = { path = "../renderer", version = "0.2.0" } 18 | --------------------------------------------------------------------------------