├── .envrc ├── .github ├── README.md └── workflows │ └── deploy.yml ├── .gitignore ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── build.rs ├── docs ├── .vitepress │ ├── config.mts │ └── theme │ │ ├── index.ts │ │ └── tabs.css ├── assets │ └── software-buttons-visualized.svg ├── configuration │ ├── animations.md │ ├── cursor.md │ ├── decorations.md │ ├── general.md │ ├── input.md │ ├── introduction.md │ ├── keybindings.md │ ├── layer-rules.md │ ├── mousebindings.md │ ├── outputs.md │ └── window-rules.md ├── getting-started │ ├── example-nix-setup.md │ ├── guided-tour.md │ ├── important-software.md │ ├── installing.md │ └── introduction.md ├── index.md ├── package-lock.json ├── package.json ├── public │ └── assets │ │ ├── default-output-arrangement.svg │ │ ├── elastic.png │ │ ├── master-slave-stacks.png │ │ ├── mwfact.svg │ │ ├── preview.png │ │ ├── proportion-changes.svg │ │ └── xwayland.png └── usage │ ├── ipc.md │ ├── layouts.md │ ├── nix.md │ ├── portals.md │ ├── workspaces.md │ └── xwayland.md ├── fht-compositor-config ├── Cargo.toml └── src │ └── lib.rs ├── flake.lock ├── flake.nix ├── res ├── compositor.toml ├── cursor.rgba ├── fht-compositor-portals.conf ├── fht-compositor-uwsm.desktop ├── fht-compositor.desktop ├── fht-compositor.portal └── preview.png ├── rust-toolchain.toml ├── rustfmt.toml └── src ├── backend ├── mod.rs ├── udev.rs └── winit.rs ├── cli.rs ├── config ├── mod.rs └── ui.rs ├── cursor.rs ├── egui.rs ├── focus_target.rs ├── frame_clock.rs ├── handlers ├── alpha_modifiers.rs ├── buffer.rs ├── compositor.rs ├── content_type.rs ├── cursor_shape.rs ├── data_control.rs ├── data_device.rs ├── dmabuf.rs ├── dnd.rs ├── drm_lease.rs ├── drm_syncobj.rs ├── foreign_toplevel_list.rs ├── fractional_scale.rs ├── idle_inhibit.rs ├── input_method.rs ├── keyboard_shortcuts_inhibit.rs ├── layer_shell.rs ├── mod.rs ├── output.rs ├── output_management.rs ├── pointer_constraints.rs ├── pointer_gestures.rs ├── presentation.rs ├── primary_selection.rs ├── relative_pointer.rs ├── screencopy.rs ├── seat.rs ├── security_context.rs ├── selection.rs ├── session_lock.rs ├── shm.rs ├── single_pixel_buffer.rs ├── viewporter.rs ├── virtual_keyboard.rs ├── xdg_activation.rs ├── xdg_decoration.rs ├── xdg_dialog.rs ├── xdg_foreign.rs └── xdg_shell.rs ├── input ├── actions.rs ├── mod.rs ├── resize_tile_grab.rs └── swap_tile_grab.rs ├── layer.rs ├── main.rs ├── output.rs ├── portals ├── mod.rs ├── screencast.rs └── shared.rs ├── profiling.rs ├── protocols ├── mod.rs ├── output_management.rs └── screencopy.rs ├── renderer ├── blur │ ├── element.rs │ ├── mod.rs │ └── shader.rs ├── data.rs ├── extra_damage.rs ├── mod.rs ├── pixel_shader_element.rs ├── render_elements.rs ├── rounded_element │ └── shader_.frag ├── rounded_window.rs ├── shaders │ ├── blur-down.frag │ ├── blur-finish.frag │ ├── blur-up.frag │ ├── border.frag │ ├── box-shadow.frag │ ├── mod.rs │ ├── resizing-texture.frag │ ├── rounded-window.frag │ └── texture.vert ├── texture_element.rs └── texture_shader_element.rs ├── space ├── closing_tile.rs ├── decorations.rs ├── mod.rs ├── monitor.rs ├── tile.rs └── workspace.rs ├── state.rs ├── utils ├── mod.rs └── pipewire.rs └── window.rs /.envrc: -------------------------------------------------------------------------------- 1 | strict_env 2 | use flake . 3 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 |

fht-compositor

2 |

A dynamic tiling Wayland compositor.

3 |

4 | Matrix 5 | GitHub License 6 | GitHub Release 7 |

8 | 9 | ![preview](https://github.com/user-attachments/assets/31e53789-d2a8-4c82-af4d-1c5352df01ab) 10 | 11 | 12 | ## About 13 | 14 | `fht-compositor` is a dynamic tiling Wayland compositor that implements a window management model 15 | inspired by X11 window managers such as [dwm](https://dwm.suckless.org) and [xmonad](https://xmonad.org) 16 | 17 | 18 | Each output gets assigned 9 independent workspaces, each one holding windows that get automatically 19 | arranged on the screen space using layouts, minimizing lost screen real estate, and providing a 20 | keyboard-focused workflow 21 | 22 | 23 | In addition, the compositor also provides some nice-to-have features that elevate the experience 24 | from a visual and practical standpoint, see features for more information. 25 | 26 | ## Video demo 27 | 28 | https://github.com/user-attachments/assets/4ea9b294-85a8-49ab-9f42-2f76111f063b 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy documentation 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - docs/** 8 | # Also allows me to rebuild manually from the actionss tab 9 | workflow_dispatch: 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 18 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 19 | concurrency: 20 | group: pages 21 | cancel-in-progress: false 22 | 23 | jobs: 24 | # Build job 25 | build: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 # Not needed if lastUpdated is not enabled 32 | # - uses: pnpm/action-setup@v3 # Uncomment this block if you're using pnpm 33 | # with: 34 | # version: 9 # Not needed if you've set "packageManager" in package.json 35 | # - uses: oven-sh/setup-bun@v1 # Uncomment this if you're using Bun 36 | - name: Setup Node 37 | uses: actions/setup-node@v4 38 | with: 39 | node-version: 20 40 | cache: npm # or pnpm / yarn 41 | cache-dependency-path: docs/package-lock.json 42 | - name: Setup Pages 43 | uses: actions/configure-pages@v4 44 | - name: Install dependencies 45 | run: cd docs && npm ci # or pnpm install / yarn install / bun install 46 | - name: Build with VitePress 47 | run: cd docs && npm run docs:build # or pnpm docs:build / yarn docs:build / bun run docs:build 48 | - name: Upload artifact 49 | uses: actions/upload-pages-artifact@v3 50 | with: 51 | path: docs/.vitepress/dist 52 | 53 | # Deployment job 54 | deploy: 55 | environment: 56 | name: github-pages 57 | url: ${{ steps.deployment.outputs.page_url }} 58 | needs: build 59 | runs-on: ubuntu-latest 60 | name: Deploy 61 | steps: 62 | - name: Deploy to GitHub Pages 63 | id: deployment 64 | uses: actions/deploy-pages@v4 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | # Useless log files in the repo 3 | std* 4 | # when building with nix 5 | /result 6 | # direnv cache 7 | /.direnv 8 | 9 | # Documentation/wiki, generated with vitepress 10 | # cache and dist are used when building/previewing the site 11 | docs/node_modules 12 | docs/.vitepress/cache 13 | docs/.vitepress/dist 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guide 2 | 3 | Some insight and guidelines for contribution to the compositor. 4 | 5 | - Formatting is done using `cargo +nightly fmt` 6 | - Current MSRV is `nightly`, due to [this](https://github.com/rust-lang/rust/issues/95439) 7 | - Keep your git commit titles short, expand in their descriptions (your editor should have settings for this) 8 | 9 | ## Logging 10 | 11 | There exist four kind of log levels: 12 | - `info!`: For information message, if some *important enough* action succeeded 13 | - `warn!`: For error/unexpected behaviour, but ***not** important enough* to alter compositor activity 14 | - `error!`: For error/unexpected behaviour, that is *important enough* to alter compositor activity 15 | - `debug!`: For keeping track of events and actions that matter for developers, not end users 16 | 17 | Additional directives are 18 | - Avoid punctuation when logging messages 19 | - use tracing's `?value` to specify arguments, unless it hurts user readability, for example `warn!(?err, "msg")` 20 | 21 | ## Code organization 22 | 23 | - `backend::*`: Backend-only interaction 24 | - `config::*`: Config types 25 | - `handlers::*`: Custom `*Handler` trait types or `delegate_*` macros, required by smithay. 26 | - `portals::*`: [XDG desktop portals](https://flatpak.github.io/xdg-desktop-portal/) 27 | - `renderer::*`: Rendering and custom render elements 28 | - `shell::*`: Modules related to the desktop shell with `xdg-shell`, `wlr-layer-shell`, workspaces, etc. 29 | - `utils::*`: General enough utilities (optimally I'd get rid of this) 30 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # {{{ Workspace 2 | 3 | [workspace] 4 | members = ["fht-compositor-config"] 5 | resolver = "2" 6 | 7 | [workspace.package] 8 | version = "25.3.1" 9 | description = "A dynamic tiling Wayland compositor" 10 | authors = ["Nadjib Ferhat "] 11 | license = "GPL-3.0" 12 | edition = "2021" 13 | repository = "https://github.com/nferhat/fht-compositor" 14 | 15 | [workspace.dependencies] 16 | anyhow = "1.0.79" 17 | # Basic logging setup 18 | tracing = "0.1.40" 19 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } 20 | # Config file support 21 | 22 | [workspace.dependencies.smithay] 23 | git = "https://github.com/smithay/Smithay" 24 | rev = "99cd2507255f62c46e67467db2fc378451d58aa7" 25 | default-features = false 26 | 27 | [workspace.dependencies.smithay-drm-extras] 28 | git = "https://github.com/Smithay/Smithay" 29 | rev = "99cd2507255f62c46e67467db2fc378451d58aa7" 30 | 31 | [profile.opt] 32 | inherits = "release" 33 | codegen-units = 1 34 | lto = "fat" 35 | opt-level = 3 36 | 37 | [profile.dev] 38 | opt-level = 3 39 | 40 | 41 | # }}} 42 | 43 | # {{{ fht-compositor 44 | 45 | [package] 46 | name = "fht-compositor" 47 | version.workspace = true 48 | description.workspace = true 49 | authors.workspace = true 50 | license.workspace = true 51 | edition.workspace = true 52 | repository.workspace = true 53 | readme = "README.md" 54 | 55 | [dependencies] 56 | anyhow.workspace = true 57 | tracing.workspace = true 58 | tracing-subscriber.workspace = true 59 | smithay = { workspace = true, features = [ 60 | "desktop", "wayland_frontend", # Provide abstractions for wayland stuff 61 | "renderer_glow", # Default renderer used everywhere 62 | "use_system_lib", "backend_egl", # EGL support for wayland 63 | "backend_libinput", # Input handling 64 | ] } 65 | smithay-drm-extras = { workspace = true, optional = true } 66 | bitflags = "2.6.0" 67 | thiserror = "1.0.61" 68 | libc = "0.2.155" 69 | zbus = { version = "5.5.0", optional = true } 70 | zvariant = { version = "5.4.0", features = ["option-as-array"], optional = true } 71 | pipewire = { git = "https://gitlab.freedesktop.org/pipewire/pipewire-rs.git", optional = true, features = ["v0_3_33"] } 72 | xcursor = "0.3.5" 73 | fht-compositor-config.path = "./fht-compositor-config" 74 | glam = "0.28.0" 75 | egui = "0.31" 76 | egui_glow = "0.31" 77 | fht-animation = { git = "https://github.com/nferhat/fht-animation", version = "0.1.0" } 78 | # FIXME: We use this instead of std::sync::MappedMutexGuard 79 | # SEE: tracking issue for nightly flag: https://github.com/rust-lang/rust/issues/117108 80 | owning_ref = "0.4.1" 81 | clap = { version = "4.5.18", features = ["derive", "string"] } 82 | async-channel = "2.3.1" 83 | async-io = "2.3.4" 84 | clap_complete = "4.5.38" 85 | serde_json = "1.0.134" 86 | xdg = "2.5.2" 87 | serde = { version = "1.0.217", features = ["derive"] } 88 | tracy-client = { version = "0.18.0", default-features = false } 89 | serde_repr = "0.1.20" 90 | libdisplay-info = "0.2.2" 91 | drm-ffi = "0.9.0" 92 | 93 | [features] 94 | default = ["winit-backend", "udev-backend", "all-portals"] 95 | 96 | # Marker feature to enable D-Bus connectivity. 97 | # 98 | # You should not enable this yourself, this is meant to be used by compositor 99 | # feature dependencies. 100 | dbus = ["zbus", "zvariant"] 101 | 102 | # Enable the winit backend. 103 | # 104 | # This allows the compositor to run under a winit window. 105 | # 106 | # NOTE: Although this backend works, it's not explicity supported, since its only here for 107 | # developement. For all regular usage, please use the udev backend. 108 | winit-backend = [ 109 | "smithay/backend_winit", 110 | "smithay/renderer_glow", 111 | "smithay/backend_drm", 112 | ] 113 | 114 | # Enable the winit backend. 115 | # 116 | # This allows the compositor to run under a libseat session. 117 | udev-backend = [ 118 | "smithay-drm-extras", 119 | "smithay/backend_libinput", 120 | "smithay/backend_udev", 121 | "smithay/backend_drm", 122 | "smithay/backend_gbm", 123 | "smithay/backend_vulkan", 124 | "smithay/backend_egl", 125 | "smithay/backend_session_libseat", 126 | "smithay/renderer_multi", 127 | ] 128 | 129 | # Enable profiling with tracy 130 | # 131 | # You should **NOT** enable this unless you want to profile compositor performance. 132 | # This will automatically stream compositor data to localhost:DEFAULT_PORT. 133 | profile-with-tracy = ["tracy-client/default"] 134 | profile-with-tracy-allocations = ["profile-with-tracy"] 135 | 136 | # Marker feature to enable all supported portals. 137 | all-portals = ["xdg-screencast-portal"] 138 | 139 | # Enable xdg-screencast portal support 140 | xdg-screencast-portal = ["pipewire", "dbus", "smithay/backend_gbm"] 141 | 142 | # UWSM support. https://github.com/Vladimir-csp/uwsm 143 | # Recommended if you are under systemd. 144 | uwsm = [] 145 | 146 | # }}} 147 | 148 | # vim: foldmethod=marker 149 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-only 2 | use std::process::Command; 3 | 4 | fn main() { 5 | if let Ok(output) = Command::new("git") 6 | .args(["rev-parse", "--short=8", "HEAD"]) 7 | .output() 8 | { 9 | let git_hash = String::from_utf8(output.stdout).unwrap(); 10 | println!("cargo:rustc-env=GIT_HASH={}", git_hash); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress"; 2 | import { tabsMarkdownPlugin } from "vitepress-plugin-tabs"; 3 | 4 | // https://vitepress.dev/reference/site-config 5 | export default defineConfig({ 6 | title: "fht-compositor", 7 | description: "A dynamic tiling Wayland compositor.", 8 | themeConfig: { 9 | // https://vitepress.dev/reference/default-theme-config 10 | nav: [ 11 | { text: "Home", link: "/" }, 12 | { text: "Getting started", link: "/getting-started/introduction" }, 13 | { text: "Configuration", link: "/configuration/introduction" }, 14 | ], 15 | 16 | sidebar: [ 17 | { 18 | text: "Getting started", 19 | items: [ 20 | { text: "Introduction", link: "/getting-started/introduction" }, 21 | { text: "Installing", link: "/getting-started/installing" }, 22 | { text: "Guided tour", link: "/getting-started/guided-tour" }, 23 | { 24 | text: "Important software", 25 | link: "/getting-started/important-software", 26 | }, 27 | { 28 | text: "Example setup with Nix flakes", 29 | link: "/getting-started/example-nix-setup", 30 | }, 31 | ], 32 | }, 33 | 34 | { 35 | text: "Usage", 36 | items: [ 37 | { text: "Workspaces", link: "/usage/workspaces" }, 38 | { text: "Dynamic layouts", link: "/usage/layouts" }, 39 | { text: "XWayland", link: "/usage/xwayland" }, 40 | { text: "Nix modules", link: "/usage/nix" }, 41 | { text: "Portals", link: "/usage/portals" }, 42 | { text: "IPC", link: "/usage/ipc" }, 43 | ], 44 | }, 45 | 46 | { 47 | text: "Configuration", 48 | items: [ 49 | { text: "Introduction", link: "/configuration/introduction" }, 50 | { text: "General", link: "/configuration/general" }, 51 | { text: "Input", link: "/configuration/input" }, 52 | { text: "Keybindings", link: "/configuration/keybindings" }, 53 | { text: "Mousebindings", link: "/configuration/Mousebindings" }, 54 | { text: "Window rules", link: "/configuration/window-rules" }, 55 | { text: "Layer rules", link: "/configuration/layer-rules" }, 56 | { text: "Outputs", link: "/configuration/outputs" }, 57 | { text: "Cursor theme", link: "/configuration/cursor" }, 58 | { text: "Decorations", link: "/configuration/decorations" }, 59 | { text: "Animations", link: "/configuration/animations" }, 60 | ], 61 | }, 62 | ], 63 | 64 | socialLinks: [ 65 | { icon: "github", link: "https://github.com/nferhat/fht-compositor" }, 66 | { 67 | icon: "matrix", 68 | link: "https://matrix.to/#/#fht-compositor:matrix.org", 69 | }, 70 | ], 71 | }, 72 | markdown: { 73 | config(md) { 74 | md.use(tabsMarkdownPlugin); 75 | }, 76 | }, 77 | base: "/fht-compositor/", 78 | }); 79 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import type { Theme } from "vitepress"; 2 | import DefaultTheme from "vitepress/theme"; 3 | import { enhanceAppWithTabs } from "vitepress-plugin-tabs/client"; 4 | import "./tabs.css"; 5 | 6 | export default { 7 | extends: DefaultTheme, 8 | enhanceApp({ app }) { 9 | enhanceAppWithTabs(app); 10 | }, 11 | } satisfies Theme; 12 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/tabs.css: -------------------------------------------------------------------------------- 1 | /* this is miserable */ 2 | 3 | .plugin-tabs--content div[class*="pad-content"] { 4 | padding: 16px !important; 5 | } 6 | 7 | .plugin-tabs--content > div[class*="pad-content"] > :first-child { 8 | margin-top: 0px !important; 9 | } 10 | 11 | .plugin-tabs--content 12 | > :first-child 13 | ~ div[class*="pad-content"] 14 | > :first-child { 15 | margin-top: -16px !important; 16 | } 17 | 18 | .plugin-tabs--content .pad-content > :last-child { 19 | margin-bottom: 0px !important; 20 | } 21 | 22 | .plugin-tabs--content .pad-content:has(+ *) > :last-child { 23 | margin-bottom: -16px !important; 24 | } 25 | 26 | .plugin-tabs--content:has(> div[class*="language-"]:first-child) { 27 | padding: 0px !important; 28 | } 29 | 30 | .plugin-tabs--content:has(> div[class*="language-"]:first-child) { 31 | background-color: var(--vp-code-block-bg) !important; 32 | border-radius: 0px 0px 8px 8px !important; 33 | } 34 | 35 | .plugin-tabs--tab-list { 36 | background-color: var(--vp-code-tab-bg) !important; 37 | border-radius: 8px 8px 0px 0px !important; 38 | } 39 | 40 | .plugin-tabs--tab-list::after { 41 | height: 1px !important; 42 | background-color: var(--vp-code-tab-divider) !important; 43 | } 44 | -------------------------------------------------------------------------------- /docs/configuration/animations.md: -------------------------------------------------------------------------------- 1 | # Animations 2 | 3 | `fht-compositor` has many animations to make different interactions fluid. You can fine-tune the animation curves to you liking 4 | for snappy animations or get nice buttery smooth transitions. 5 | 6 | ## Animation curves 7 | 8 | An animation is simply a function of time `t` that interpolates between two values: `start` and `end`. You can configure 9 | said function to your liking. 10 | 11 | > [!TIP] 12 | > When deserializing the configuration, we make use of tagged unions. Depending on the value that you assign to the `curve` field, we automatically 13 | > detect which kind of curve you want. Note however that you must specify **all fields** of for the configuration to be (re)loaded successfully 14 | 15 | ### Pre-defined easings 16 | 17 | This is by far the simplest option. All easings on on [easings.net](https://easings.net/) are available, but rename them from 18 | `camelCase` to `kebab-case`, for example `easeInQuint` becomes `ease-in-quint` 19 | 20 | You must give a duration to the animation with a pre-defined easing. 21 | 22 | ```toml 23 | [animations.window-geometry] 24 | curve = "ease-out-quint" 25 | duration = 450 # in milliseconds 26 | ``` 27 | 28 | ### Custom cubic curve 29 | 30 | You can also use your own easing curves in the form of a custom cubic Bezier curve. There are four control points 31 | 1. `x=0`, `y=0` (this point is forced to keep the values in bound) 32 | 2. Custom point 1, `p1` 33 | 2. Custom point 2, `p2` 34 | 4. `x=1`, `y=1` (this point is forced to keep the values in bound) 35 | 36 | ### Springs 37 | 38 | The last kind of curves we support are spring curves. They use a physical model of a spring that is *identical* to 39 | [libadwaita's `SpringAnimation`](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.3/class.SpringAnimation.html). 40 | 41 | Since they are much more tweakable, you'd rather use something like [Elastic](https://apps.gnome.org/Elastic/) to tweak 42 | the parameters yourself, and get a preview of what the curve will look like, aswell as the total animation time. 43 | 44 | ![Elastic window](/assets/elastic.png) 45 | 46 | Note however that you should be conservative with the valuess you pass into this animation, as it can *really quickly* cause 47 | values to overshoot/undershoot to infinity, and potentially causing crashes (integer overflows). 48 | 49 | ```toml 50 | [animations.workspace-switch] 51 | curve = { clamp = true, damping-ratio = 1, initial-velocity = 5, mass = 1, stiffness = 700 } 52 | # NOTE: The duration given here is not taken into account at all! 53 | # The spring's simulation duration will be used instead 54 | duration = 1000000000000 55 | ``` 56 | 57 | ## Configuration options 58 | 59 | #### `disable` 60 | 61 | Disable all animations. Useful if you want the snappiest most responsive experience, of if animations 62 | take a toll on your device performance. 63 | 64 | > [!TIP] 65 | > Each animation has an individual `disable` toggle! 66 | 67 | --- 68 | 69 | #### `window-geometry` 70 | 71 | Animation settings used for window geometry changes: both *location* and *size* are animated. 72 | 73 | Default curve: 74 | 75 | ```toml 76 | [animations.window-geomtry.curve] 77 | initial-velocity = 1.0 78 | clamp = false 79 | mass = 1.0 80 | damping-radio = 1.2 81 | stiffness = 800.0 82 | epsilon = 0.0001 83 | ``` 84 | 85 | --- 86 | 87 | #### `window-open-close` 88 | 89 | Animation settings used for opening and closing of windows. The window open-close animation makes windows 90 | pop in/out from their center, and fades them in/out. 91 | 92 | Default curve: 93 | 94 | ```toml 95 | [animations.window-open-close.curve] 96 | initial-velocity = 1.0 97 | clamp = false 98 | mass = 1.0 99 | damping-radio = 1.2 100 | stiffness = 800.0 101 | epsilon = 0.0001 102 | ``` 103 | 104 | --- 105 | 106 | #### `workspace-switch` 107 | 108 | Animations settings for switching workspaces. The animation slides workspaces in/out the output's edges depending 109 | on the workspace index relative to the one you're switching to. 110 | 111 | `workspace-switch.direction`: Can either be `horizontal` or `vertical` 112 | 113 | Default curve: 114 | 115 | ```toml 116 | [animations.workspace-switch.curve] 117 | initial-velocity = 1.0 118 | clamp = false 119 | mass = 0.85 120 | damping-radio = 1.0 121 | stiffness = 600.0 122 | epsilon = 0.0001 123 | ``` 124 | -------------------------------------------------------------------------------- /docs/configuration/cursor.md: -------------------------------------------------------------------------------- 1 | # Cursor theme. 2 | 3 | `fht-compositor` supports XCursor themes. You can configured the used theme and the cursor: 4 | 5 | ```toml 6 | [cursor] 7 | name = "Vimix-cursors" 8 | size = 32 9 | ``` 10 | 11 | If these are not specified, the compositor will try to load these values from the `XCURSOR_NAME` 12 | and `XCURSOR_SIZE` variables. 13 | 14 | When loading and applying the cursor theme, the compositor will set these variables. 15 | -------------------------------------------------------------------------------- /docs/configuration/decorations.md: -------------------------------------------------------------------------------- 1 | # Decorations 2 | 3 | `fht-compositor` provides decorations: optional visual effects that can give off a nice looking effect. While they are not strictly required, they 4 | still enhance the looks of the desktop session 5 | 6 | > [!TIP] Window rules 7 | > All the decorations values can be overriden for specific windows using [window rules](/configuration/window-rules)! 8 | 9 | ## Borders 10 | 11 | Borders are an outline drawn around windows in a workspace. The focused window will get a different border color to indicate that it is focused and that 12 | it has active keyboard focus. Borders can also apply rounded corner radius aroud windows. 13 | 14 | #### `border.focused-color`, `border.normal-color` 15 | 16 | The border color for the focused and unfocused windows. The compositor optionally supports gradient borders, akin to CSS' `linear-gradient`, taking a 17 | start color, end color, and an angle (in degrees). 18 | 19 | Color values itself are strings, and can make use of CSS color declarations (let that be color functions, named colors, etc.) 20 | 21 | ::: tabs 22 | == Simple solid border 23 | ```toml 24 | [decorations.border] 25 | focused-color = "#6791c9" 26 | normal-color = "transparent" 27 | ``` 28 | == Gradient border 29 | ```toml 30 | [decorations.border] 31 | focused-color = { start = "#87c7a1", end = "#96d6b0", angle = 0 } 32 | normal-color = "#101112" 33 | ``` 34 | ::: 35 | 36 | > [!TIP] 37 | > When deserializing the configuration, we make use of tagged unions. Depending on the value that you assign to the color value, we automatically 38 | > detect which kind of border color you want. Note however that you must specify **all fields** of gradient color for the configuration to be 39 | > (re)loaded successfully 40 | 41 | #### `border.thickness`, `border.radius` 42 | 43 | Controls the size and corner radius of the border. Having a thickness of `0` will disable all border logic. 44 | 45 | ## Shadows 46 | 47 | Drop shadows can be rendered behind windows. With floating windows, this becomes requires to distinguish the stacking order of windows. 48 | 49 | #### `shadow.disable`, `shadow.floating-only` 50 | 51 | Toggles to disable completely shadows, or only for non-floating/tiled windows. Both are `false` by default 52 | 53 | #### `shadow.color` 54 | 55 | Color of the shadow. You can also make use of CSS color functions to specify this. Default is fully black with opacity of `0.75` 56 | 57 | #### `shadow.sigma` 58 | 59 | The blur sigma of the shadow. This controls how much the shadow will "spread" below the window. Default is `10.0` 60 | 61 | ## Blur 62 | 63 | Blur is a nice-looking effect behind semi-transparent windows. The actual implementation in the compositor is 64 | [Dual Kawase](https://www.intel.com/content/www/us/en/developer/articles/technical/an-investigation-of-fast-real-time-gpu-based-image-blur-algorithms.html), 65 | a fast approximation of Gaussian blur. 66 | 67 | #### `blur.radius` 68 | 69 | How much we should offset when sampling the blurred texture. In layman's terms, the higher the number the blurrier the result. Most values 70 | above `20` will just make everything blend together 71 | 72 | --- 73 | 74 | #### `blur.passes` 75 | 76 | The number of passes for the blur. More blur passes are required to get correct sampling for higher radius values. They more or less correlate together, 77 | though nothing stops you from using high number of passes with low blur values, if you care about the accuracy of the results. 78 | 79 | > [!CAUTION] 80 | > Higher blur passes will cause more rendering to happen, and thus put more strain on your GPU. It is recommended to keep this value below 81 | > 3 or 4, because otherwise it will *kill* your performance and framerates 82 | 83 | --- 84 | 85 | #### `blur.noise` 86 | 87 | Additional noise effect to add when rendering blur. It just looks nice and can give off the "glassy blur" effect, similar to Windows 11 Acrylic 88 | blur look. 89 | -------------------------------------------------------------------------------- /docs/configuration/general.md: -------------------------------------------------------------------------------- 1 | # General behaviour configuration 2 | 3 | #### `cursor-warps` 4 | 5 | Whether to warp the pointer cursor whenever based on select events/actions. This currently includes 6 | 7 | - When opening a new window, warp the pointer to the center 8 | - When connecting a new output, warp the pointer to its center 9 | - When using `focus-next/previous-window/output` key actions 10 | 11 | Default is `true` 12 | 13 | --- 14 | 15 | #### `focus-new-windows` 16 | 17 | Whether to focus newly opened windows. Default is `true` 18 | 19 | --- 20 | 21 | #### `focus-follows-mouse` 22 | 23 | Whether to automatically update the keyboard focus to the window under the pointer cursor. Default is `false` 24 | 25 | --- 26 | 27 | #### `insert-window-strategy` 28 | 29 | How to insert a newly opened window in a workspace. 30 | 31 | - `end-of-slave-stack`: Insert it at the end of the slave stack, IE. push it on the window list 32 | - `replace-master`: Replace the first master window 33 | - `after-focused`: Insert after the currently focused window 34 | 35 | Default is `end-of-slave-stack` 36 | 37 | --- 38 | 39 | #### `layouts` 40 | 41 | The [dynamic layouts](/usage/layouts) to enable. You can cycle through them using the `select-next/previous-layout` 42 | key actions, and workspaces can have different layouts active at the same time. 43 | 44 | Available layouts are 45 | - `tile`: A classic master-slave layout, with the master stack on the left. 46 | - `bottom-stack`: A variant of the `tile` layout with the master stack on the upper half of the screen. 47 | - `centered-master`: A three column layout where the master stack is centered 48 | 49 | Default is `["tile"]` 50 | 51 | --- 52 | 53 | #### `nmaster`, `mwfact` 54 | 55 | The number of master clients in the workspace. Refer to the [dynamic layouts](/usage/layouts) page to understand 56 | how they affect the layout system. 57 | 58 | Default is `nmaster=1`, `mwfact=0.5` 59 | 60 | > [!NOTE] Transient properties 61 | > These layout properties are *transient* and kept around across config reloads, IE. if you changed them during 62 | > runtime they will not be reloaded with the configuration! For example: 63 | > 64 | > 1. Start the compositor with `nmaster=1` and `mwfact=0.5` 65 | > 2. On workspace 1, open some windows, change the number of master clients, make the master stack a bit wider 66 | > and change these values around to fit your current workflow 67 | > 3. On workspace 2, just open two windows 68 | > 4. Change these values in your configuration 69 | > 70 | > When the compositor reloads the configuration, workspace 1 `nmaster` and `mwfact` will not be reset to the 71 | > new configuration value, since your current workflow on workspace 1 *depends* on the correct values. However, 72 | > workspace 2 will apply the new settings, since you have not changed them when using it. 73 | > 74 | > This is done to avoid destroying the workflows you have set in place when reloading the configuration 75 | 76 | --- 77 | 78 | #### `outer-gaps`, `inner-gaps` 79 | 80 | Gaps to add around the screen edge and in between windows respectively. 81 | 82 | Default is `8` for both. 83 | -------------------------------------------------------------------------------- /docs/configuration/input.md: -------------------------------------------------------------------------------- 1 | # Input device configuration 2 | 3 | You can configure various input device functionality under `fht-compositor` thanks to [libinput](https://www.freedesktop.org/wiki/Software/libinput/). 4 | 5 | ## Keyboard configuration 6 | 7 | > [!NOTE] 8 | > As of writing this, keyboard configuration is **only global**, IE. you can't set it per-device. This is a limitation of the 9 | > wl-seat protocol, allowing for only one configuration at a time. 10 | 11 | #### `rules`, `model`, `layout`, `variant`, `options` 12 | 13 | These are all [XKB](https://www.x.org/wiki/XKB/) settings. 14 | 15 | - You can find out available keyboard rules, variants and options from the `/usr/share/X11/xkb/rules/base.lst` file, or using `localectl` (see `man 5 localectl`) 16 | - You can find the correct keyboard layout for yourself [on this page](https://xkeyboard-config.pages.freedesktop.org/website/layouts/gallery/) 17 | 18 | By default, all these are empty strings (using system defaults), and layout is `us` 19 | 20 | --- 21 | 22 | #### `repeat-rate`, `repeat-delay` 23 | 24 | These two options control key repeating. `repeat-delay` is the delay in milliseconds that you should hown a key for key repeating to 25 | start. `repeat-rate` is the frequency at which the key is repeated. 26 | 27 | Default settings are `repeat-rate=25`, `repeat-delay=250` 28 | 29 | ## Mouse settings 30 | 31 | Mouse settings are appliewd for regular mice, touchpads, trackballs, etc. The compositor (and libinput) will figure out automatically 32 | which setting should be applied or not, and whether the connected mouse type supports a given feature. 33 | 34 | > [!NOTE] Default mouse settings 35 | > If an option does not have a default specified, it is up to the device driver (IE. libinput) to choose one. 36 | 37 | --- 38 | 39 | #### `acceleration-profile` 40 | 41 | How should the pointer cursor accelerate with mouse movement. Available values are: 42 | - `adaptive`: Takes the current speed of the device into account when deciding on acceleration. 43 | - `linear`: Constant factor `acceleration-speed` applied to all deltas, regardless of the speed of motion. 44 | 45 | --- 46 | 47 | #### `acceleration-speed` 48 | 49 | A factor to multiply mouse movement delta with. Must be in the range `[-1.0, 1.0]` 50 | 51 | Default is `1.0` 52 | 53 | --- 54 | 55 | #### `left-handed` 56 | 57 | Whether to enable left handed mode for the device. This will swap the left and right clicks. 58 | 59 | --- 60 | 61 | #### `scroll-method` 62 | 63 | For touchpads, determines how to emulate a scroll wheel using only your fingers (and no dedicated button). Available values are: 64 | - `no-scroll`: Disable scrolling emulation. 65 | - `two-finger`: Scrolling is triggered by two fingers being placed on the surface of the touchpad. 66 | - `edge`: Scrolling is triggered by moving a single finger along the right edge (vertical scroll) or bottom edge (horizontal scroll). 67 | - `on-button-down`: Converts the motion of a device into scroll events while a designated button is held down. This is common in ThinkPad trackpoints 68 | 69 | --- 70 | 71 | #### `scroll-button`, `scroll-button-lock` 72 | 73 | The button used to enable `on-button-down` scroll method. When `scroll-button-lock` is enabled, the button does not need to be held 74 | down, and insteads turns the button into a toggle switch. 75 | 76 | --- 77 | 78 | #### `click-method` 79 | 80 | Determines how button events are triggered on a touchpad/[clickpad](https://wayland.freedesktop.org/libinput/doc/latest/clickpad-softbuttons.html#clickpad-softbuttons)s. 81 | Available values are: 82 | 83 | - `button-areas`: The bottom area is divided into three thirds, like the following: 84 |

85 | 86 | - `click-finger`: Emulate clicks based on the number of fingers used, 1 is left, 2 is right, 3 is middle. 87 | 88 | --- 89 | 90 | #### `natural-scrolling` 91 | 92 | Whether to enable natural scrolling. 93 | 94 | Natural scrolling matches the motion of the scroll device with the motion of the **content** 95 | 96 | --- 97 | 98 | #### `middle-button-emulation` 99 | 100 | Whether to emulate a left+right click at the same time as a middle click. The middle click is the one you have when you 101 | click on your mouse's scroll wheel. 102 | 103 | --- 104 | 105 | #### `disable-while-typing` 106 | 107 | The name is clear enough. 108 | 109 | --- 110 | 111 | #### `tap-to-click`, `tap-button-map` 112 | 113 | Whether to emulate clicking on touchpads/clickpads by tapping the surface. 114 | 115 | `tap-button-map` changes how tap-to-click behaves. Available maps/modes are: `left-right-middle`, `left-middle-right` (for 116 | 1 finger, 2 finger and 3 finger taps respectively) 117 | 118 | --- 119 | 120 | #### `tap-and-drag`, `drag-lock` 121 | 122 | Whether to enable Tap-and-drag. If a tap is shortly followed by the finger being held down, moving the finger around will 123 | *drag around* the selected item from the tap. 124 | 125 | Having `drag-lock` enabled will make the dragging process persist even when lifting the finger from the touchpad, and instead 126 | will require a final tap to let go of the grabbed item. 127 | 128 | ## Per-device configuration 129 | 130 | You can configure each registered input device individually. Per-device configuration is a table, which keys can be: 131 | - The device pretty name (AKA. the readable name, which you would see in a device manager) 132 | - A raw device path, `/dev/input/eventX` 133 | 134 | To find out which devices you have connected, you can execute in a shell
135 | ```sh 136 | # You might need root privileges to run this 137 | libinput list-devices | grep Device: 138 | ``` 139 | 140 | --- 141 | 142 | - `disable`: Whether to completely disable this device. 143 | - `mouse`: Same as `input.mouse`, but for this device only. 144 | -------------------------------------------------------------------------------- /docs/configuration/introduction.md: -------------------------------------------------------------------------------- 1 | # Configuration in `fht-compositor` 2 | 3 | `fht-compositor` is configured using a [TOML](https://toml.io), a very simple and obvious key-value language. The configuration 4 | contents itself is broken down into multiple sub-sections: 5 | 6 | - [Top-level section](#top-level-section) 7 | - [General behaviour](./general) 8 | - [Input configuration](./input) 9 | - [Key-bindings](./keybindings) 10 | - [Mouse-bindings](./mousebindings) 11 | - [Window rules](./window-rules) 12 | - [Layer-shell rules](./layer-rules) 13 | - [Outputs](./outputs) 14 | - [Cursor theme](./cursor) 15 | - [Decorations](./decorations) 16 | - [Animations](./animations) 17 | 18 | ## Loading 19 | 20 | The compositor will try to load your configuration from the following paths, with decreasing order of precedence: 21 | 22 | 1. `--config-path`/`-c` command line argument. 23 | 2. `$XDG_CONFIG_HOME/fht/compositor.toml` 24 | 3. `~/.config/fht/compositor.toml` 25 | 26 | If there's no configuration in second/third paths, the compositor will generate a 27 | [template configuration file](https://github.com/nferhat/fht-compositor/blob/main/res/compositor.toml). You should 28 | use it as a base for further modifications. 29 | 30 | ## Configuration reloading 31 | 32 | The configuration is live-reloaded. You can edit and save the file and `fht-compositor` will automatically pick up and 33 | apply changes. 34 | 35 | If you made a mistake when writing your configuration (let that be syntax, invalid values, unknown enum variant, etc.), the 36 | compositor will warn you with a popup window sliding from the top of your screen. You can run `fht-comopsitor check-configuration` 37 | to get that error in your terminal. 38 | 39 | 40 | ## Top-level section 41 | 42 | ##### `autostart` 43 | 44 | Command lines to run whenever the compositor starts up. Each line is evaluated using `/bin/sh -c ""`, meaning you have 45 | access to shell-expansions like using variables, or exapding of `~` to `$HOME` 46 | 47 | > [!TIP] Autostart with UWSM 48 | > While using this approach for autostart can work, using systemd user services are a much better! You benefit from having 49 | > logs using `journalctl -xeu --user {user-service}`, restart-on-failure, and bind them to specific targets. 50 | 51 | > [!NOTE] XDG autostart on non-systemd distros 52 | > Most desktop programs under Linux that have a "Run on computer startup" or programs that make use of XDG autostart will *not* 53 | > work by default! You will need a program like [dex](https://github.com/jceb/dex) for it. 54 | > 55 | > ```toml 56 | > autostart = ["dex -a"] # autostart XDG desktop entries/programs 57 | > ``` 58 | > 59 | > A notable example is PipeWire that makes use of this to startup with the desktop session. 60 | 61 | --- 62 | 63 | ##### `env` 64 | 65 | Environment variables to set. They are set before anything else starts up in the compositor, and before `autostart` is exxecuted 66 | 67 | ```toml 68 | [env] 69 | DISPLAY = ":727" 70 | _JAVA_AWT_NONREPARENTING = "1" 71 | ``` 72 | -------------------------------------------------------------------------------- /docs/configuration/layer-rules.md: -------------------------------------------------------------------------------- 1 | # Layer-shell rules 2 | 3 | Layer rules, similar to [window rules](./window-rules), are a way to apply custom settings on layer-shells. 4 | 5 | Just like window rules, they have two parts: the match part, and the properties part. Refer to the [window rules] 6 | page for information about what that is and `match-all` property. 7 | 8 | For now, layer-shell rules are used to enable various effects on matched layer-shells 9 | 10 | ## The match part 11 | 12 | #### `match-namespace` 13 | 14 | A list of [Regular Expression](https://en.wikipedia.org/wiki/Regular_expression)s. They match onto the layer-shell's namespace. 15 | The namespace per protocol definition defines the purpose of a layer-shell, for example, `notification`, or `volume-osd` 16 | 17 | Requires that the namespace has a match on only *one* regex from the given regexes. 18 | 19 | #### `on-output` 20 | 21 | Match on the output the layer-shell is opened on. Nothing fancy. 22 | 23 | The following rule matches all layer-shells opened on a laptop's internal display 24 | 25 | ```toml 26 | [[layer-rules]] 27 | on-output = "eDP-1" 28 | opacity = 0.5 29 | ``` 30 | 31 | ## Layer-shell properties 32 | 33 | #### `border`, `blur`, `shadow` 34 | 35 | These values take the same fields as their versions in the [decorations configuration](/configuration/decorations), 36 | however, they will *override* the decorations configuration with whatever fields you have provided. 37 | 38 | By default, layer-shells have all of these disabled. Set `disable=false` to enable these effectss. 39 | 40 | --- 41 | 42 | #### `opacity` 43 | 44 | The opacity of the layer-shell, `0.0` is fully transparent, `1.0` is fully opaque. 45 | -------------------------------------------------------------------------------- /docs/configuration/mousebindings.md: -------------------------------------------------------------------------------- 1 | # Mousebindings 2 | 3 | These are exactly the same as [keybindings](/configuration/keybindings), expect the available mouse buttonss (instead of 4 | key names) are: `left`, `right`, `middle`, `right`, `forward`, `back` 5 | 6 | ## Available mouse actions 7 | 8 | - `swap-tile`: Initiates an interactive tile swap. Allows you to grab a window and put it elsewhere in the window stack, 9 | move it across outputs or workspaces. For floating windows, it allows you to move them around with the mouse 10 | - `resize-tile`: Initiates an interactive resize. Only affects floating windows so far. 11 | -------------------------------------------------------------------------------- /docs/configuration/outputs.md: -------------------------------------------------------------------------------- 1 | # Output configuration 2 | 3 | When starting up, `fht-compositor` will scan all connected outputs and turn them on. They will get arranged 4 | in a straight horizontal line, like the following: 5 | 6 | ![Default output arrangement](/assets/default-output-arrangement.svg) 7 | 8 | You refer by outputs using their connector names, for example `eDP-1` is your laptop builtin display, 9 | `DP-{number}` are display port connectors, `HDMI-A-{number}` are builtin HDMI ports, etc. 10 | 11 | You configure outputs by using the `outputs.{connector-name}` table. 12 | 13 | --- 14 | 15 | #### `disable` 16 | 17 | Whether to completely disable an output. You will not be able to accesss it using the mouse. 18 | 19 | > [!NOTE] Disabling an already enabled output 20 | > When you disable a output that has opened windows in its workspaces, these windows will get "merged" or into the same workspaces 21 | > of the *newly active* output instead. 22 | 23 | --- 24 | 25 | #### `mode` 26 | 27 | A string representation of a mode that takes the form of `{width}x{height}` or `{width}x{height}@{refresh-hz}`. Optionally, there's 28 | custom mode support using [CVT timing calculation](http://www.uruk.org/~erich/projects/cvt/) 29 | 30 | When picking the mode for the output, the compositor will first filter out modes with matching width and height, then 31 | - If there's a given refresh rate, find the mode which refresh rate is the closest to what you have given 32 | - If there's no refresh rate, pick the highest one available. 33 | 34 | --- 35 | 36 | #### `scale` 37 | 38 | The *integer scale* of the output. There's currently no support for fractional scaling in `fht-compositor`. 39 | 40 | --- 41 | 42 | #### `position` 43 | 44 | The position of the *top-left corner*. The output space is absolute. 45 | 46 | > [!NOTE] Overlapping output geometries 47 | > If your configuration contains two overlapping outputs, `fht-compositor` will resort to the default output arragement seen 48 | > at the top of this page. It will also print out a warning message in the logs 49 | -------------------------------------------------------------------------------- /docs/configuration/window-rules.md: -------------------------------------------------------------------------------- 1 | # Window rules 2 | 3 | Window rules are a simple but powerful way to streamline and organize your workflow. They pair really well with 4 | the [static workspaces](/usage/workspaces) system that `fht-compositor` has. You can make a window rule target 5 | a specific set of windows by filtering (or *matching*) properties that you want, like app identifier, title, 6 | etc. 7 | 8 | A window rule has two parts: the match part, and the properties part. 9 | 10 | ## The match part 11 | 12 | A window rule can have multiple criteria to scope its requirements. For example, the following rule will 13 | only be applied to Alacritty windows, and will make it open maximized. 14 | 15 | ```toml 16 | [[rules]] 17 | match-app-id = ["Alacritty"] 18 | maximized = true 19 | ``` 20 | 21 | You can have different matching requirements, the following rule matches windows Alacritty windows *or* 22 | LibreWolf windows 23 | 24 | ```toml 25 | [[rules]] 26 | match-app-id = ["Alacritty", "LibreWolf"] 27 | maximized = true 28 | ``` 29 | 30 | 33 | 34 | --- 35 | 36 | #### `match-all` 37 | 38 | When a window rule is marked as `match-all`, all the given match requirements *must* match. For example, 39 | this window rule will only match Fractal windows that are on workspace with the index 2 40 | 41 | ```toml 42 | [[rules]] 43 | match-all = true 44 | match-app-id = ["org.gnome.Fractal"] 45 | on-workspace = 2 46 | ``` 47 | 48 | However, this window rule matches Fractal windows *or* all windows opened on workspace with index 2 49 | 50 | ```toml 51 | [[rules]] 52 | match-app-id = ["org.gnome.Fractal"] 53 | on-workspace = 2 54 | ``` 55 | 56 | This allows you to make very precise window rules. 57 | 58 | > [!TIP] Different matching rules 59 | > If different rules match onto the same window, they will "merge" together, based on their declaration order. 60 | > For example, having these two window rules 61 | > 62 | > ```toml 63 | > [[rules]] 64 | > match-app-id = ["Alacritty"] 65 | > floating = true 66 | > 67 | > [[rules]] 68 | > match-app-id = ["Alacritty"] # etc, imagine its just another rule 69 | > floating = false 70 | > ``` 71 | > 72 | > Will result in a window rule with `floating` equal to **false** (since it was declared *later*) in your config. 73 | > This is why you should try to make your matching conditions precise to avoid unexpected things to happen! 74 | 75 | --- 76 | 77 | #### `match-title`, `match-app-id` 78 | 79 | Both are a list of [Regular Expression](https://en.wikipedia.org/wiki/Regular_expression)s. This match conditions 80 | requires that the window matches only *one* regex from the given regexes. 81 | 82 | The following window rule matches WebCord, Telegram, and Fractal windows 83 | 84 | ```toml 85 | [[rules]] 86 | match-app-id = ["WebCord", "Telegram", "org.gnome.Fractal"] 87 | open-on-workspace = 2 88 | 89 | ``` 90 | 91 | This window rule, matches all Steam games that are opened with Proton 92 | 93 | ```toml 94 | [[rules]] 95 | match-app-id = ["steam_app_*"] 96 | floating = true 97 | centered = true 98 | on-workspace = 5 99 | ``` 100 | 101 | --- 102 | 103 | #### `on-output` 104 | 105 | Match on the output the window is opened on. Nothing fancy. 106 | 107 | The following rule matches all windows opened on a laptop's internal display 108 | 109 | ```toml 110 | [[rules]] 111 | on-output = "eDP-1" 112 | floating = true 113 | ``` 114 | 115 | --- 116 | 117 | #### `on-workspace` 118 | 119 | Match on the workspace *index* (not number!) the window is opened on. 120 | 121 | --- 122 | 123 | #### `is-focused` 124 | 125 | Match on the currently focused window. A focused window is the one that should receive keyboard focus. 126 | There can be *multiple* focused window, one per workspace. 127 | 128 | The following rule matches all *un*focused windows 129 | 130 | ```toml 131 | [[rules]] 132 | is-focused = false 133 | opacity = 0.95 # lesser visible non-focused windows 134 | ``` 135 | 136 | ## Window properties 137 | 138 | #### `open-on-output`, `open-on-workspace` 139 | 140 | These properties control *where* the window should open. They take an output name and a workspace *index* 141 | 142 | If the given output/workspace index is invalid, the compositor will fallback to the active one. (This is the 143 | default when no window rule is applied) 144 | 145 | The following rule opens several game launchers/games on workspace with the index 5 146 | 147 | ```toml 148 | [[rules]] 149 | match-app-id = [ 150 | "Celeste.bin.x86_64", 151 | "steam_app_*", 152 | "Etterna", 153 | "Quaver", 154 | "Steam", 155 | "org.prismlauncher.PrismLauncher" 156 | ] 157 | open-on-workspace = 5 158 | centered = true 159 | floating = true 160 | ``` 161 | 162 | --- 163 | 164 | #### `border`, `blur`, `shadow` 165 | 166 | These values take the same fields as their versions in the [decorations configuration](/configuration/decorations), 167 | however, they will *override* the decorations configuration with whatever fields you have provided. 168 | 169 | For example, this window rule will disable all blur in workspace with index 5 170 | 171 | ```toml 172 | [[rules]] 173 | on-workspace = 5 174 | blur.disable = true 175 | ``` 176 | 177 | --- 178 | 179 | #### `proportion` 180 | 181 | Change the initial window proportion. See [dynamic layouts](/usage/layouts) for information about how window 182 | proportions affect layouts. 183 | 184 | ---- 185 | 186 | #### `opacity` 187 | 188 | The opacity of a window, `0.0` is fully transparent, `1.0` is fully opaque. 189 | 190 | ---- 191 | 192 | #### `decoration-mode` 193 | 194 | The decoration mode for this window. See [`decorations.decorations-mode`](/configuration/decorations#decorations-mode) 195 | for more information about differences between these values. 196 | 197 | Useful when a client misbehaves when using specifically SSD/CSD. 198 | 199 | ---- 200 | 201 | #### `maximized`, `fullscreen`, `floating`, `centered` 202 | 203 | State to toggle on when opening the window. This only gets applied *once*! 204 | 205 | They are self-explainatory. Example window rule making all GNOME apps open floating centered. 206 | 207 | ```toml 208 | [[rules]] 209 | centered = true 210 | floating = true 211 | match-app-id = ["^(org.gnome.*)$"] 212 | ``` 213 | -------------------------------------------------------------------------------- /docs/getting-started/example-nix-setup.md: -------------------------------------------------------------------------------- 1 | # Example setup with Nix Flakes 2 | 3 | The following page will describe to you how you can setup a good Wayland desktop session with Nix 4 | flakes, NixOS, and home-manager. You can obviously adapt the following to suit your needs. 5 | 6 | The setup includes `fht-compositor` itself, PipeWire for sound and screencast portal, GDM, and 7 | shows you how to create user services that startup with the session using home-manager. 8 | 9 | 1. Add the [flake](../usage/nix.md) to your inputs. 10 | 11 | 2. NixOS part: You want to include the provided module `inputs.fht-compositor.nixosModules.default` 12 | and enable the following: 13 | 14 | ```nix 15 | { inputs, ... }: 16 | 17 | { 18 | imports = [inputs.fht-compositor.nixosModules.default]; 19 | 20 | programs = { 21 | dconf.enable = true; # required for dbus and GTK theming 22 | # Enabling the compositor here will ensure that the .desktop files are correctly created 23 | # and a UWSM session is instanciated. You'll find it with the name 'fht-compositor (UWSM)' 24 | fht-compositor = { enable = true; withUWSM = true; }; 25 | }; 26 | 27 | services = { 28 | # You probably want sound, right? 29 | # This is also required for the screencast portal. 30 | pipewire = { 31 | enable = true; 32 | alsa = { enable = true; support32Bit = true; }; 33 | jack.enable = true; 34 | pulse.enable = true; 35 | }; 36 | 37 | # Enable whatever display/session manager you like. 38 | # Or do without one, and run `uwsm start fht-compositor-uwsm.desktop` from a TTY. 39 | xserver.displayManager.gdm.enable = true; 40 | }; 41 | 42 | # The compositor itself will start its own portal. 43 | # Otherwise enable GTK portal as a fallback. 44 | xdg.portal = { 45 | enable = true; 46 | xdgOpenUsePortal = true; 47 | config.common.default = ["gtk"]; 48 | extraPortals = [pkgs.xdg-desktop-portal-gtk]; 49 | }; 50 | } 51 | ``` 52 | 53 | 3. The home-manager part: Configure the compositor with Nix and setup services. The following 54 | examples are from my [dotfiles](https://github.com/nferhat/dotfiles) 55 | 56 | ```nix 57 | { config, inputs, ... }: 58 | 59 | { 60 | imports = [inputs.fht-compositor.homeModules.default]; 61 | 62 | # Enable configuration. 63 | # NOTE: The final configuration is checked before being applied! 64 | programs.fht-compositor = { 65 | enable = true; 66 | settings = { 67 | # Include cursor configuration from home environment 68 | cursor = {inherit (config.home.pointerCursor) name size;}; 69 | 70 | # I mean, its really up to you... 71 | # You can also just do `builtins.fromTOML` if you have an existing config 72 | 73 | }; 74 | }; 75 | 76 | # Services that we setup as part of the desktop/graphical session. 77 | # They get all triggered when fht-compositor reaches the graphical.target 78 | # --- 79 | # You are **REALLY** recommended to use systemd services/units for your 80 | # autostart instead of configuring them with the autostart section, since you also get restart 81 | # on failure, logs, and all nice stuff. 82 | systemd.user.services = let 83 | start-with-graphical-session = Description: { 84 | Unit = { 85 | inherit Description; 86 | After = ["graphical-session.target"]; 87 | PartOf = ["graphical-session.target"]; 88 | BindsTo = ["graphical-session.target"]; 89 | Requisite = ["graphical-session.target"]; 90 | }; 91 | Install.WantedBy = ["graphical-session.target" "fht-compositor.service"]; 92 | }; 93 | in { 94 | wallpaper = 95 | start-with-graphical-session "Wallpaper service" 96 | // { 97 | Service = { 98 | Type = "simple"; 99 | ExecStart = "${pkgs.swaybg}/bin/swaybg -i ${/path/to/wallpaper-file}"; 100 | Restart = "on-failure"; 101 | }; 102 | }; 103 | 104 | # For my personally, I like having xwayland satellite to play games. 105 | # It works really fine, I already play non-native stuff fine. Though for other programs it may 106 | # not work as good, for example windows that need positionning 107 | xwayland-sattelite = 108 | start-with-graphical-session "Xwayland-satellite" 109 | // { 110 | Service = { 111 | Type = "notify"; 112 | NotifyAccess = "all"; 113 | ExecStart = "${pkgs.xwayland-satellite}/bin/xwayland-satellite"; 114 | StandardOutput = "jounral"; 115 | }; 116 | }; 117 | }; 118 | } 119 | ``` 120 | 121 | 4. Enjoy? Rebuild your system configuration and have fun I guess. 122 | -------------------------------------------------------------------------------- /docs/getting-started/guided-tour.md: -------------------------------------------------------------------------------- 1 | # Guided tour of `fht-compositor` 2 | 3 | Before launching the compositor, you are recommended to install [Alacritty](https://alacritty.org) and 4 | [wofi](https://hg.sr.ht/~scoopta/wofi) since the default configuration makes use of them. 5 | 6 | ::: tabs 7 | == systemd (with UWSM) 8 | - **Login managers**: You should use the `fht-compositor (UWSM)` 9 | - **From a TTY**: Run `uwsm start fht-compositor-uwsm.desktop` 10 | 11 | == non-systemd (or without UWSM) 12 | ```sh 13 | # While you can run without D-Bus, many things like the ScreenCast portal will not work! 14 | dbus-run-session fht-compositor 15 | ``` 16 | ::: 17 | 18 | On startup, the compositor will try to generate a default configuration file inside `~/.config/fht/compositor.toml`. 19 | You should use it as a base to build your configuration, with this Wiki. 20 | 21 | Some important key-binds to know are: 22 | 23 | | Binding | Keyaction | 24 | | ------- | --------- | 25 | | Super+Enter | Spawn alacritty | 26 | | Super+Shift+C | Close focused window | 27 | | Super+P | Spawn wofi | 28 | | Super+J/K | Focus the next/previous window | 29 | | Super+Shift+J/K | Swap current window with the next/previous window | 30 | | Super+[1-9] | Focus the nth workspace | 31 | | Super+Shift+[1-9] | Send the focused window to the nth workspace | 32 | | Super+Ctrl+Space | Toggle floating on focused window | 33 | | Super+F | Toggle fullscreen on focused window | 34 | | Super+M | Toggle maximized on focused window | 35 | 36 | Start to open some windows and get a feel for the compositor. You will immediatly notice the [dynamic layouts](/usage/layouts) 37 | arranging the opened windows in a very special way: In a master and slave stack. The default layout, the `master/tile` tile, 38 | arranges windows as follows: 39 | 40 | ![Master slave stack](/assets/master-slave-stacks.png) 41 | -------------------------------------------------------------------------------- /docs/getting-started/important-software.md: -------------------------------------------------------------------------------- 1 | # Important software 2 | 3 | To get the best experience with the compositor, you should install additional services and tools to 4 | get a more complete desktop session. Most importantly, you should have a terminal (of your choice 5 | though the default configuration has [Alacritty](https://github.com/Alacritty/Alacritty)), 6 | a text editor of your choice, and an app launcher. 7 | 8 | Desktops environments like GNOME/KDE have all of these bundled together, `fht-compositor` does not 9 | do that, so you are **very strongly** recommended to read this page. 10 | 11 | 12 | ## Must-have services 13 | 14 | - **Sound**: You most likely want to have sound working on your session. Install 15 | [PipeWire](https://www.pipewire.org/) otherwise your desktop will be mute. PipeWire should 16 | be autostarted if you are under a systemd setup, otherwise, add to `autostart` section.
17 | If you want to use the XDG screencast portal, PipeWire is required! 18 | 19 | - **Notification daemon**: Many apps require one and might freeze (for example Discord) if no one 20 | is found. [mako](https://github.com/emersion/mako) is simple and works fine, otherwise, use 21 | whatever suits you. 22 | 23 | - **Policy-kit daemon**: (abbreviated to polkit): Required to give system(root) access to regular 24 | applications in a safe and controlled manner. Refer to the 25 | [Arch Linux wiki page](https://wiki.archlinux.org/title/Polkit#Authentication_agents) 26 | on the topic and install the one you prefer.
27 | 28 | > [!TIP] NixOS module 29 | > If you use the NixOS module provided by the Nix flake, setting `programs.fht-compositor.enable` 30 | > will automatically enable `polkit-gnome`! 31 | 32 | - **XDG desktop portal**: The compositor binary itself will start a session d-bus connection and 33 | expose the `ScreenCast` interfaces. However, other interfaces are **NOT** implemented, this is why 34 | you should fall back to 35 | [xdg-desktop-portal-gtk](https://github.com/flatpak/xdg-desktop-portal-gtk) 36 | 37 | ## Desktop Shell 38 | 39 | Most desktop interface utilities like shells/panels use `wlr-layer-shell` under the hood to create 40 | the surfaces to draw onto. `fht-compositor` implements said protocol so all the utilities/programs 41 | you are used to will work fine! 42 | 43 | - **Wallpaper**: [swww](https://github.com/LGFae/swww), [swaybg](https://github.com/swaywm/swaybg) 44 | or [wbg](https://codeberg.org/dnkl/wbg) will work fine. 45 | 46 | - **App launcher**: [wofi](https://hg.sr.ht/~scoopta/wofi), 47 | [bemenu](https://github.com/Cloudef/bemenu), [fuzzel](https://codeberg.org/dnkl/fuzzel), 48 | [Anyrun](https://github.com/anyrun-org/anyrun), are all fine. 49 | 50 | - **Desktop shell**: 51 | - [**Elkowar's Wacky Widgets**](https://github.com/elkowar/eww): provides you with a DSL to write 52 | GTK3 based panels, with built-in support for JSON, listeners, SCSS/SASS. 53 | - [**Astal**](https://github.com/aylur/astal): A framework to build desktop shells with GTK using 54 | Typescript or Lua (or any language that has GObject-Introspection).
55 | Provides a lot of batteries to get you started (watchers for bluetooth, battery, notifications, 56 | etc.) 57 | - [**Quickshell**](https://git.outfoxxed.me/outfoxxed/quickshell): Qt-based building blocks for 58 | your desktop with QML. 59 | - [**Waybar**](https://github.com/Alexays/Waybar): Good'ol waybar, nothing fancy, but gets the 60 | job done fast and easy. 61 | 62 | ## Nice to have 63 | 64 | While these are not required, the following tools are compatible with `fht-compositor` and provide 65 | extra functionality that is *nice to have*. 66 | 67 | - [**swayidle**](https://github.com/swaywm/swayidle): An idle management daemon, IE. run commands 68 | after idling for a while, for example turning off your screen after 10 minutes. 69 | - [**cliphist**](https://github.com/sentriz/cliphist): A very simple clipboard manager for Wayland 70 | sessions. 71 | - [**wl-clip-persist**](https://github.com/Linus789/wl-clip-persist): Make selections from an 72 | application persist even after closing it. 73 | -------------------------------------------------------------------------------- /docs/getting-started/installing.md: -------------------------------------------------------------------------------- 1 | # Install guide 2 | 3 | ## Using package managers 4 | 5 | ::: tabs 6 | == Nix (flake) 7 | 8 | If you are using Nix(OS) with [Nix Flakes](https://nixos.wiki/wiki/flakes) enabled, refer to the [Nix](/usage/nix) page, 9 | and the [example Nix flakes setup](./example-nix-setup) 10 | 11 | == Arch (AUR) 12 | ```sh 13 | paru -S fht-compositor-git 14 | # Needed for screencast to work 15 | paru -S fht-share-picker-git 16 | # Recommended 17 | paru -S uwsm 18 | ``` 19 | ::: 20 | 21 | 22 | ## Manual install 23 | 24 | First, get the latest Rust available through your preferred means. 25 | 26 | > [!NOTE] Minimum supported rust version 27 | > The MSRV of `fht-compositor` is mostly tied to [Smithay](https://github.com/smithay)'s MSRV.
28 | > As of writing this it's currently `1.84` 29 | 30 | You will need the following system dependencies. Depending on your distribution, you might want to install the 31 | `-dev` or `-devel` or `-headers` variant to get developement headers that are required to build. 32 | 33 | - `libwayland` and dependencies 34 | - `libxkbcommon` and dependencies 35 | - `mesa` with the appropriate OpenGL driver for you GPU. 36 | - In order to run the compositor from a TTY: `libudev`, `libinput`, `libgbm`, [`libseat`](https://git.sr.ht/~kennylevinsen/seatd), `libdrm` and `lib-displayinfo` 37 | - To use [XDG screencast portal](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.ScreenCast.html): `pipewire`, `dbus` 38 | 39 | Then you can proceed with compiling. 40 | 41 | ```sh 42 | # Clone and build. 43 | git clone https://github.com/nferhat/fht-compositor/ && cd fht-compositor 44 | 45 | # If you are not under systemd 46 | cargo build --profile opt 47 | # If you are under systemd, highly recommended 48 | # See below the note on UWSM 49 | cargo build --profile opt --features uwsm 50 | # You can copy it to /usr/local/bin or ~/.local/bin, make sure its in $PATH though! 51 | cp target/opt/fht-compositor /somewhere/inside/PATH 52 | 53 | # Wayland session desktop files 54 | install -Dm644 res/fht-compositor.desktop -t /usr/share/wayland-sessions # generic 55 | # See below the note on UWSM, highly recommended 56 | install -Dm644 res/fht-compositor-uwsm.desktop -t /usr/share/wayland-sessions 57 | ``` 58 | 59 | > [!CAUTION] Build features 60 | > Do **not** compile the compositor with `--all-features` as some of these are reserved for dev/testing purposes (for exxample 61 | > enabling profiling). Always refer to the `Cargo.toml` file before enabling features 62 | 63 | > [!TIP] Using Universal Wayland Session Manager 64 | > If you are using a systemd distribution, you are *highlighy* recommended to install [UWSM](https://github.com/Vladimir-csp/uwsm) 65 | > to launch the compositor session as it will bind many systemd targets to make the overall compositor experience better. 66 | > 67 | > To do so, install UWSM with your favourite package manager and enable the `uwsm` feature at build time. 68 | 69 | > [!NOTE] Portals 70 | > Refer to the [portals](/usage/portals) page if you want the included portal (you most likely want to) 71 | -------------------------------------------------------------------------------- /docs/getting-started/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | `fht-compositor` is a dynamic tiling Wayland compositor based on the [Smithay](https://github.com/smithay) 4 | compositor library. It has a layout model inspired by popular X11 window managers such as [DWM](https://dwm.suckless.org), 5 | [AwesomeWM](https://awesomewm.org) and [XMonad](https://xmonad.org). Each connected output has 9 workspaces, each 6 | workspace managing a bunch of windows. 7 | 8 | Windows in the workspace are arranged using a *dynamic layout* in order to maximized the used area of your screen 9 | in two stacks: The master and slave stack. Different parameters can be adjusted at runtime to adapt the different 10 | dynamic layouts to your needs. 11 | 12 | ![Preview image](/assets/preview.png) 13 | 14 | --- 15 | 16 | If this is your first time with the compositor, please head to the [install guide](./installing) to take 17 | the [guided tour](/getting-started/guided-tour.md) in order to get up and running with the compositor! 18 | 19 | > [!WARNING] 20 | > 21 | > `fht-compositor` is a bare-bones compositor, it does not include a bar, notifications, and other nice-to-have 22 | > components that a full desktop environment like GNOME or KDE will provide. You are expected to tinker and work 23 | > your way through error messages and configuration files! 24 | > 25 | > --- 26 | > 27 | > The compositor is still not *mature* yet, any feedback and reports would be greatly appreciated. If you have 28 | > something not working right, you can file an [new issue](https://github.com/nferhat/fht-compositor/issues/new) 29 | > or get in touch through the [matrix channel](https://matrix.to/#/#fht-compositor:matrix.org)! 30 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | 4 | hero: 5 | name: fht-compositor 6 | tagline: The documentation for the dynamic tiling Wayland compositor. 7 | --- 8 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "docs:dev": "vitepress dev", 4 | "docs:build": "vitepress build", 5 | "docs:preview": "vitepress preview" 6 | }, 7 | "devDependencies": { 8 | "vitepress": "^1.6.3" 9 | }, 10 | "dependencies": { 11 | "vitepress-plugin-tabs": "^0.6.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /docs/public/assets/elastic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nferhat/fht-compositor/579731eb437d709607790efe1d493e9457a2fb55/docs/public/assets/elastic.png -------------------------------------------------------------------------------- /docs/public/assets/master-slave-stacks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nferhat/fht-compositor/579731eb437d709607790efe1d493e9457a2fb55/docs/public/assets/master-slave-stacks.png -------------------------------------------------------------------------------- /docs/public/assets/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nferhat/fht-compositor/579731eb437d709607790efe1d493e9457a2fb55/docs/public/assets/preview.png -------------------------------------------------------------------------------- /docs/public/assets/xwayland.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nferhat/fht-compositor/579731eb437d709607790efe1d493e9457a2fb55/docs/public/assets/xwayland.png -------------------------------------------------------------------------------- /docs/usage/ipc.md: -------------------------------------------------------------------------------- 1 | # IPC 2 | 3 | > [!INFO] Work in progress 4 | > :construction: The IPC system is still a work-in-progress! Nothing stable and working is out yet!
5 | > Check out the `feat-ipc` branch for status updates! 6 | -------------------------------------------------------------------------------- /docs/usage/layouts.md: -------------------------------------------------------------------------------- 1 | # Layouts 2 | 3 | `fht-compositor` has dynamic layouts, IE. the workspace arranges windows for you, *automatically*. 4 | 5 | Layouts divide windows in two stacks 6 | 7 | - **Master stack**: Contains the most important window(s) that need the most attention. This is for example your opened 8 | text editor, or a mail client. The master window(s) take up a majority of the screen area. 9 | - **Slave stack**: Contains the lesser important windows. This can be a video you have opened on the side, some 10 | reference material, library documentation, work terminals, etc. These windows share the remainder of the screen space.
11 | It can be divided in more ways, for example the `centered-master` layout, that divides it in left and right stacks. 12 | 13 | Different variables control the layout at runtime. 14 | 15 | - **Number of master clients** (`nmaster`): The number of master clients in the master stack. Must always be >= 1 16 | - **Master width factor** (`mwfact`): The proportion of screen space the master stack takes up relative to the slave stack. It 17 | in `[0.01, 0.99]` 18 | 19 | ![Master width factor changes diagram](/assets/mwfact.svg) 20 | 21 | - **Per-window proportion**: (`proportion`) The proportion control how much space a window takes relative to other windows in its 22 | stack. 23 | 24 | ![Proportion changes example diagram](/assets/proportion-changes.svg) 25 | 26 | By default, when you open a window in a workspace, it gets inserted as tiled at the *end* of the slave stack. The layout will 27 | dynamically resize the other windows to share the screen space with the newly opened window. 28 | 29 | ## Floating windows 30 | 31 | The layout system only affects windows that are *tiled*. Floating windows, on the other hand, don't get managed. 32 | 33 | There's no such thing as a floating layer in a workspace. Floating windows live in the same layer the tiled windows, and thus 34 | can be displayed *below* tiled windows. 35 | 36 | You can make any window floating using the `float-focused-window` key action, or by making use of [window rules](/configuration/window-rules) 37 | 38 | ```toml 39 | [[rules]] 40 | # -> your matching rules here... 41 | floating = true 42 | centered = true # preference 43 | ``` 44 | 45 | The rendering order of windows is decided by their position in the workspace window list. 46 | 47 | ## Why dynamic layouts? 48 | 49 | Dynamic layouts are extremely flexible and can be molded to adapt for any situation/workflow. You, *the end user*, don't have to 50 | fuss creating a specialized layout for your current job in other window management models like Sway's or River's, instead, you 51 | pick a layout that suits your need, resize and move some window around, and start working. 52 | 53 | You can create workflows and make them work with what you have to do quickly, and adapt for some unforseen cases or patterns 54 | the main task has. You can build from the provided layouts and get started working. 55 | 56 | Overall, the experience is very natural and you'll get the hang of it really fast. 57 | -------------------------------------------------------------------------------- /docs/usage/nix.md: -------------------------------------------------------------------------------- 1 | # Nix modules 2 | 3 | To ease setups with [NixOS](https://nixos.org) and [home-manager](https://github.com/nix-community/home-manager/), the 4 | [`fht-compositor` repository](https://github.com/nferhat/fht-compositor) is a [Nix Flake](https://nixos.wiki/wiki/flakes). 5 | 6 | You can add it to your configuration like the following. 7 | 8 | ```nix 9 | { 10 | inputs = { 11 | # Currently only tested against unstable, but in theory should work fine with latest 12 | # stable release. If anything goes wrong, report an issue! 13 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 14 | 15 | fht-compositor = { 16 | url = "github:nferhat/fht-compositor"; 17 | inputs.nixpkgs.follows = "nixpkgs"; 18 | 19 | # If you make use of flake-parts yourself, override here 20 | # inputs.flake-parts.follows = "flake-parts"; 21 | 22 | # Disable rust-overlay since it's only meant to be here for the devShell provided 23 | # (IE. only for developement purposes, end users don't care) 24 | inputs.rust-overlay.follows = ""; 25 | }; 26 | } 27 | } 28 | ``` 29 | 30 | ## NixOS module 31 | 32 | This module lets you enable `fht-compositor` and expose it to display managers like GDM, and enable required configuration 33 | (mesa, hardware acceleration, etc.) It also setups nice-to-have features for a fuller desktop session: 34 | 35 | - A [polkit agent](https://wiki.archlinux.org/title/Polkit#Authentication_agents): `polkit-gnome` to be exact 36 | - [GNOME keyring](https://wiki.gnome.org/Projects/GnomeKeyring): Authentification agent 37 | - [xdg-desktop-portal-gtk](https://github.com/flatpak/xdg-desktop-portal-gtk): Fallback portal 38 | - [UWSM](https://github.com/Vladimir-csp/uwsm) session script. 39 | 40 | To enable it, include it `inputs.fht-compositor.nixosModules.default` 41 | 42 | --- 43 | 44 | #### `programs.fht-compositor.enable` 45 | 46 | Whether to enable `fht-compositor` 47 | 48 | --- 49 | 50 | #### `programs.fht-compositor.package` 51 | 52 | The `fht-compositor` package to use. 53 | 54 | Default: `.packages.${pkgs.system}.fht-compositor` 55 | 56 | --- 57 | 58 | #### `programs.fht-compositor.withUWSM` 59 | 60 | Launch the fht-compositor session with UWSM (Universal Wayland Session Manager). Using this is highly recommended since it 61 | improves fht-compositor's systemd support by binding appropriate targets like `graphical-session.target`, 62 | `wayland-session@fht-compositor.target`, etc. for a regular desktop session. 63 | 64 | ## home-manager module 65 | 66 | This module lets you easily configure `fht-compositor` through home-manager module system. 67 | 68 | To enable it, include it `inputs.fht-compositor.homeModules.default` 69 | 70 | --- 71 | 72 | #### `programs.fht-compositor.enable` 73 | 74 | Whether to enable `fht-compositor` 75 | 76 | --- 77 | 78 | #### `programs.fht-compositor.package` 79 | 80 | The `fht-compositor` package to use. 81 | 82 | Default: `.packages.${pkgs.system}.fht-compositor` 83 | 84 | --- 85 | 86 | #### `programs.fht-compositor.settings` 87 | 88 | Configuration table written directly to `$XDG_CONFIG_HOME/fht/compositor.toml`. Since Nix and TOML have a one-to-one mapping, all 89 | the data types and structures you have in TOML can be easily re-written in Nix. 90 | 91 | > [!TIP] Configuration check 92 | > `programs.fht-compositor.settings` is checked against `programs.fht-compositor.package`! Using the following command line 93 | > ```sh 94 | > fht-compositor check-configuration /path/to/generated/compositor.toml 95 | > ``` 96 | > If your configuration have any issues, home-manager **will not** rebuild your configuration! 97 | 98 | A possible alternative is to use `builtins.fromTOML`: 99 | 100 | ```nix 101 | { 102 | programs.fht-compositor.settings = builtins.fromTOML ./path/to/compositor.toml; 103 | } 104 | ``` 105 | -------------------------------------------------------------------------------- /docs/usage/portals.md: -------------------------------------------------------------------------------- 1 | # Portals in `fht-compositor` 2 | 3 | [XDG desktop portals](https://flatpak.github.io/xdg-desktop-portal/) are a core component of any Wayland desktop session. 4 | They allow user programs and applications to interact with other components of the system (like the compositor) in a 5 | safe and secure way through D-Bus. 6 | 7 | The default recommended portal for `fht-compositor` desktop sessions is [xdg-desktop-portal-gtk](http://github.com/flatpak/xdg-desktop-portal-gtk), 8 | it provides all the basics needed (file picker, accounts, settings, etc.). `gnome-keyring` can be added to have `Secrets` portal 9 | support (needed for programs like Fractal or Secrets) 10 | 11 | In addition, `fht-compositor` provides additional portals that need a session-specific implementation. 12 | 13 | ## Setting up `fht-compositor` XDG desktop portal. 14 | 15 | > [!TIP] AUR package/Nix module 16 | > - If you are using `fht-compositor` through the NixOS/home-manager modules or the `fht-compositor` package, this should 17 | > have already been sorted out for you! 18 | > - If you are using the AUR package, this should also be sorted out for you! 19 | 20 | Install the following files in the relevant places 21 | 22 | | File | Target path | 23 | | ---------------------------------------- | ---------------------------------------- | 24 | | `res/fht-compositor.portal` | `/usr/share/xdg-desktop-portal/portals/` | 25 | | `res/fht-compositor-portals.conf` | `/usr/share/xdg-desktop-portal/` | 26 | 27 | The compositor itself should start up the portal after initializing graphics (IE starting up outputs). 28 | 29 | ## XDG ScreenCast portal 30 | 31 | The XDG screencast portal is used by applications that request casting/recording a screen or part of a screen. Example 32 | programs include [OBS](https://obsproject.com/download) and web browsers (through WebRTC). 33 | 34 | You can chose between three options: 35 | 36 | - Screencast an entire monitor 37 | - Screencast a workspace: It will include only the workspace windows, not any layer shells 38 | - Screencast a window: The window itself with additional popups will be screencasted 39 | 40 | The screencasts are **damage-tracked**, IE. new frames will be pushed and drawed only when *something* changes. Moreover, 41 | only DMABUF-based screencasting is supported, as SHM is way too slow. If that is needed, use the `wlr-screencopy` protocol. 42 | 43 | Using this portal will require an additional dependency, [`fht-share-picker`](https://github.com/nferhat/fht-share-picker), 44 | it is **required** to select which source (from the above) to screen cast. 45 | 46 | ::: tabs 47 | === Nix 48 | If you are using the [Nix module](/usage/nix), this should be installed if you compile `fht-compositor` with default attrs. 49 | Otherwise, make sure to use a package with `withXdgScreenCast` set to true, like so: 50 | 51 | ```nix 52 | fht-compositor.override { withXdgScreenCast = true; /* other attrs */ } 53 | ``` 54 | === Arch (AUR) 55 | ```bash 56 | # Or use your AUR helper of choice. 57 | paru -S fht-share-picker-git 58 | ``` 59 | === From source 60 | ```bash 61 | git clone https://github.com/nferhat/fht-share-picker && cd fht-share-picker 62 | cargo build --release 63 | # You can copy it to /usr/local/bin or ~/.local/bin 64 | cp target/release/fht-share-picker /somewhere/in/PATH 65 | ``` 66 | ::: 67 | -------------------------------------------------------------------------------- /docs/usage/workspaces.md: -------------------------------------------------------------------------------- 1 | # Workspace system 2 | 3 | `fht-compositor` has static workspaces assigned to each output/monitor. 4 | 5 | There's no shared space/floating layer across outputs/workspaces. Each workspace is responsible to manage its own windows. 6 | Windows in a workspace can not "overflow" to adjacent workspaces/monitors. This is very similar to what the [DWM](https://dwm.suckless.org) 7 | and [AwesomeWM](https://awesomewm.org) window managers propose. Effectively, you can think of each output as a sliding 8 | carousel of 9 workspaces, displaying only one at a time. 9 | 10 | When connecting outputs, the compositor will create a fresh set of workspaces for each one of them. When 11 | disconnecting an output, all its windows will get inserted into the respective workspaces of the *primary output* 12 | (most likely the first output you inserted, but this can be [configured](/configuration/outputs)) 13 | 14 | Compared to other wayland compositor/tiling window managers, workspaces **can not** be moved across outputs, 15 | instead you move individual windows from/to workspaces or outputs. 16 | 17 | ## Advantages of this system 18 | 19 | Windows are organized in a predictible fashion, and you have full knowledge and control on where they are and where they go. 20 | Workspaces are always available for window rules and custom scripts (for example to create scratchpad workspaces). 21 | 22 | ## Example workflow 23 | 24 | When I make use of the compositor, through muscle memory and [window rules](/configuration/window-rules), I 25 | assign different workspaces different purposes: first workspace is the *work* one (that contains the project 26 | I am currently *working* on), second is for the web browser, third is for chat clients (think Discord, Telegram, 27 | Fractal, etc.) sixth is for video games, etc. 28 | 29 | This streamlined workflow allows me to switch working context **very** fast using keybinds, since I have keybinds 30 | engrained in my mind, I can switly get to my browser to search up documentation, quickly respond to a notification from 31 | a chat client, and more. 32 | 33 | If you want, you can even disable [animations](/configuration/animations) to make this even more immediate. 34 | -------------------------------------------------------------------------------- /docs/usage/xwayland.md: -------------------------------------------------------------------------------- 1 | # XWayland support 2 | 3 | `fht-compositor` does **not** have native XWayland support, since X11 is very quirky and weird to 4 | implement, and while it would play nice with the layout system (allowing for freely moving windows), 5 | I do not have planned XWayland support. 6 | 7 | Don't fret, however, there are still solutions if you rely on X11-only programs (which I do) 8 | 9 | ## xwayland-satellite 10 | 11 | > [!TIP] Example setup 12 | > The [nix example setup](/getting-started/example-nix-setup) has xwayland-satellite enabled as a user service. 13 | 14 | [xwayland-satellite](https://github.com/Supreeeme/xwayland-satellite) is an external XWayland 15 | implementation that grants any compositor rootless XWayland support. X11 windows opened through 16 | XWayland appear as normal windows and they will automatically share clipboard and render fine. 17 | 18 | Install it through your package manager or build it from source and either add it to your `autostart` section, 19 | or integrate it in your desktop session using systemd user services (recommended). You should find in the logs 20 | the display number that it connected to (most likely `:0`, but you can give it a specific one to connect to, like 21 | `xwayland-satellite :727`) 22 | 23 | You can now use X11 programs by setting the `DISPLAY` environment variable 24 | 25 | ::: tabs 26 | == Per-program 27 | ```sh 28 | # For example, run steam. Flag is needed otherwise you get a black screen. 29 | env DISPLAY=:0 steam -system-composer 30 | ``` 31 | 32 | == Setting in config 33 | ```toml 34 | # You should force it to use a display number for this to work properly! 35 | env.DISPLAY = ":727" 36 | ``` 37 | 38 | ::: 39 | 40 | And voila! 41 | 42 | ![X11 programs under fht-compositor](/assets/xwayland.png) 43 | 44 | --- 45 | 46 | Other tips and information to keep in mind are: 47 | 48 | - Video games and simple clients open and work 100% fine! No performance issue is noticed, a seamless experience. 49 | - You should use the `floating` layouts if you plan on using X11 applications since they sometimes don't play nice with dyanmic 50 | layouts. 51 | - If you use self-resizing/self-moving windows, they will not play very nice too! 52 | 53 | ## Rootful XWayland 54 | 55 | Sometimes, programs just don't play well with xwayland-satellite, so you must resort to using rootful XWayland, in order words: 56 | run XWayland inside a window. 57 | 58 | 1. Install `xwayland` using your system package manager 59 | 2. Install a simple X11 window manager of your choice, recommended is [i3wm](https://i3wm.org). 60 | 3. Run XWayland and your window manager inside, and open a terminal. 61 | 62 | This approach comes with many downsides, notably the fact your X11 windows live *inside* another window, which can hurt your 63 | workflow a little bit. You can however fullscreen the XWayland window and enable keyboard grabbing to get a seamless X11 session, 64 | in case you need it. 65 | 66 | The other downside is that the clipboard is **not** shared. You can use X11 clipboard tools like `xset` to pull/push values 67 | from/to the X11 clipboard. 68 | 69 | ```sh 70 | env DISPLAY=:0 xsel -ob | wl-copy # Get a value from XWayland 71 | wl-paste -n | env DISPLAY=:0 xsel -ib # Push a value to XWayland 72 | ``` 73 | -------------------------------------------------------------------------------- /fht-compositor-config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fht-compositor-config" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | csscolorparser = { version = "0.7.0", features = ["serde"] } 10 | fht-animation = { git = "https://github.com/nferhat/fht-animation", version = "0.1.0", features = ["serde"] } 11 | regex = "1.10.6" 12 | serde = { version = "1.0.203", features = ["derive"] } 13 | smithay = { workspace = true, features = ["backend_libinput"] } 14 | sscanf = "0.4.2" 15 | thiserror = "1.0.56" 16 | toml = { version = "0.8.19", default-features = false, features = ["parse", "display"] } 17 | tracing.workspace = true 18 | xdg = "2.5.2" 19 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "fht-share-picker": { 4 | "inputs": { 5 | "flake-parts": [ 6 | "flake-parts" 7 | ], 8 | "nixpkgs": [ 9 | "nixpkgs" 10 | ], 11 | "rust-overlay": [] 12 | }, 13 | "locked": { 14 | "lastModified": 1735832883, 15 | "narHash": "sha256-JX6btqPN6FA/nGYh0W9VnWePwWI3fZTlPLRXF2mALMA=", 16 | "owner": "nferhat", 17 | "repo": "fht-share-picker", 18 | "rev": "999ca73503c4e86cd22876a9a7e2eb2e04c65c67", 19 | "type": "github" 20 | }, 21 | "original": { 22 | "owner": "nferhat", 23 | "ref": "gtk-rewrite", 24 | "repo": "fht-share-picker", 25 | "type": "github" 26 | } 27 | }, 28 | "flake-parts": { 29 | "inputs": { 30 | "nixpkgs-lib": [ 31 | "nixpkgs" 32 | ] 33 | }, 34 | "locked": { 35 | "lastModified": 1733312601, 36 | "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", 37 | "owner": "hercules-ci", 38 | "repo": "flake-parts", 39 | "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", 40 | "type": "github" 41 | }, 42 | "original": { 43 | "owner": "hercules-ci", 44 | "repo": "flake-parts", 45 | "type": "github" 46 | } 47 | }, 48 | "nixpkgs": { 49 | "locked": { 50 | "lastModified": 1742669843, 51 | "narHash": "sha256-G5n+FOXLXcRx+3hCJ6Rt6ZQyF1zqQ0DL0sWAMn2Nk0w=", 52 | "owner": "nixos", 53 | "repo": "nixpkgs", 54 | "rev": "1e5b653dff12029333a6546c11e108ede13052eb", 55 | "type": "github" 56 | }, 57 | "original": { 58 | "owner": "nixos", 59 | "ref": "nixos-unstable", 60 | "repo": "nixpkgs", 61 | "type": "github" 62 | } 63 | }, 64 | "root": { 65 | "inputs": { 66 | "fht-share-picker": "fht-share-picker", 67 | "flake-parts": "flake-parts", 68 | "nixpkgs": "nixpkgs", 69 | "rust-overlay": "rust-overlay" 70 | } 71 | }, 72 | "rust-overlay": { 73 | "inputs": { 74 | "nixpkgs": [ 75 | "nixpkgs" 76 | ] 77 | }, 78 | "locked": { 79 | "lastModified": 1742870002, 80 | "narHash": "sha256-eQnw8ufyLmrboODU8RKVNh2Mv7SACzdoFrRUV5zdNNE=", 81 | "owner": "oxalica", 82 | "repo": "rust-overlay", 83 | "rev": "b4c18f262dbebecb855136c1ed8047b99a9c75b6", 84 | "type": "github" 85 | }, 86 | "original": { 87 | "owner": "oxalica", 88 | "repo": "rust-overlay", 89 | "type": "github" 90 | } 91 | } 92 | }, 93 | "root": "root", 94 | "version": 7 95 | } 96 | -------------------------------------------------------------------------------- /res/cursor.rgba: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nferhat/fht-compositor/579731eb437d709607790efe1d493e9457a2fb55/res/cursor.rgba -------------------------------------------------------------------------------- /res/fht-compositor-portals.conf: -------------------------------------------------------------------------------- 1 | [preferred] 2 | default=gtk; 3 | org.freedesktop.impl.portal.Access=gtk; 4 | org.freedesktop.impl.portal.Notification=gtk; 5 | org.freedesktop.impl.portal.ScreenCast=fht-compositor; 6 | -------------------------------------------------------------------------------- /res/fht-compositor-uwsm.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=fht-compositor (UWSM) 3 | Comment=A dynamic tiling Wayland compositor 4 | Exec=uwsm start -- fht-compositor --uwsm 5 | DesktopNames=fht-compositor 6 | Type=Application 7 | -------------------------------------------------------------------------------- /res/fht-compositor.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=fht-compositor 3 | Comment=A dynamic tiling Wayland compositor 4 | Exec=fht-compositor 5 | Type=Application 6 | DesktopNames=fht-compositor 7 | -------------------------------------------------------------------------------- /res/fht-compositor.portal: -------------------------------------------------------------------------------- 1 | [portal] 2 | DBusName=fht.desktop.Compositor 3 | Interfaces=org.freedesktop.impl.portal.ScreenCast; 4 | UseIn=fht-compositor 5 | -------------------------------------------------------------------------------- /res/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nferhat/fht-compositor/579731eb437d709607790efe1d493e9457a2fb55/res/preview.png -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.84" 3 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Module" 2 | group_imports = "StdExternalCrate" 3 | wrap_comments = true 4 | comment_width = 100 5 | -------------------------------------------------------------------------------- /src/backend/mod.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use smithay::backend::renderer::glow::GlowRenderer; 4 | use smithay::output::Output; 5 | 6 | use crate::state::Fht; 7 | 8 | #[cfg(feature = "udev-backend")] 9 | pub mod udev; 10 | #[cfg(feature = "winit-backend")] 11 | pub mod winit; 12 | 13 | pub enum Backend { 14 | #[cfg(feature = "winit-backend")] 15 | Winit(winit::WinitData), 16 | #[cfg(feature = "udev-backend")] 17 | Udev(udev::UdevData), 18 | } 19 | 20 | #[cfg(feature = "winit-backend")] 21 | impl From for Backend { 22 | fn from(value: winit::WinitData) -> Self { 23 | Self::Winit(value) 24 | } 25 | } 26 | 27 | #[cfg(feature = "udev-backend")] 28 | impl From for Backend { 29 | fn from(value: udev::UdevData) -> Self { 30 | Self::Udev(value) 31 | } 32 | } 33 | 34 | impl Backend { 35 | #[cfg(feature = "winit-backend")] 36 | pub fn winit(&mut self) -> &mut winit::WinitData { 37 | #[allow(irrefutable_let_patterns)] 38 | if let Self::Winit(data) = self { 39 | return data; 40 | } 41 | unreachable!("Tried to get winit backend data on non-winit backend") 42 | } 43 | 44 | #[cfg(feature = "udev-backend")] 45 | pub fn udev(&mut self) -> &mut udev::UdevData { 46 | #[allow(irrefutable_let_patterns)] 47 | if let Self::Udev(data) = self { 48 | return data; 49 | } 50 | unreachable!("Tried to get udev backend data on non-udev backend") 51 | } 52 | 53 | pub fn render( 54 | &mut self, 55 | #[allow(unused)] fht: &mut Fht, 56 | #[allow(unused)] output: &Output, 57 | #[allow(unused)] target_presentation_time: Duration, 58 | ) -> anyhow::Result { 59 | match self { 60 | #[cfg(feature = "winit-backend")] 61 | #[allow(irrefutable_let_patterns)] 62 | Self::Winit(data) => data.render(fht), 63 | #[cfg(feature = "udev-backend")] 64 | #[allow(irrefutable_let_patterns)] 65 | Self::Udev(data) => data.render(fht, output, target_presentation_time), 66 | #[allow(unreachable_patterns)] 67 | _ => unreachable!(), 68 | } 69 | } 70 | 71 | pub fn with_renderer( 72 | &mut self, 73 | #[allow(unused)] f: impl FnOnce(&mut GlowRenderer) -> T, 74 | ) -> T { 75 | match self { 76 | #[cfg(feature = "winit-backend")] 77 | #[allow(irrefutable_let_patterns)] 78 | Self::Winit(ref mut data) => f(data.renderer()), 79 | #[cfg(feature = "udev-backend")] 80 | #[allow(irrefutable_let_patterns)] 81 | Self::Udev(data) => { 82 | let mut renderer = data 83 | .gpu_manager 84 | .single_renderer(&data.primary_gpu) 85 | .expect("No primary gpu"); 86 | use crate::renderer::AsGlowRenderer; 87 | f(renderer.glow_renderer_mut()) 88 | } 89 | #[allow(unreachable_patterns)] 90 | _ => unreachable!(), 91 | } 92 | } 93 | 94 | pub fn set_output_mode( 95 | &mut self, 96 | fht: &mut Fht, 97 | output: &Output, 98 | mode: smithay::output::Mode, 99 | ) -> anyhow::Result<()> { 100 | match self { 101 | #[cfg(feature = "winit-backend")] 102 | #[allow(irrefutable_let_patterns)] 103 | // winit who cares 104 | Self::Winit(_) => Ok(()), 105 | #[cfg(feature = "udev-backend")] 106 | #[allow(irrefutable_let_patterns)] 107 | Self::Udev(data) => data.set_output_mode(fht, output, mode), 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::builder::styling::{AnsiColor, Effects}; 4 | use clap::builder::Styles; 5 | 6 | pub const CLAP_STYLING: Styles = Styles::styled() 7 | .header(AnsiColor::Green.on_default().effects(Effects::BOLD)) 8 | .usage(AnsiColor::Green.on_default().effects(Effects::BOLD)) 9 | .literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD)) 10 | .placeholder(AnsiColor::Cyan.on_default()) 11 | .error(AnsiColor::Red.on_default().effects(Effects::BOLD)) 12 | .valid(AnsiColor::Cyan.on_default().effects(Effects::BOLD)) 13 | .invalid(AnsiColor::Yellow.on_default().effects(Effects::BOLD)); 14 | 15 | #[derive(Debug, clap::Parser)] 16 | #[command(author, version = get_version_string(), about, long_about = None, styles = CLAP_STYLING)] 17 | pub struct Cli { 18 | /// What backend should the compositor start with? 19 | #[arg(short, long, value_name = "BACKEND")] 20 | pub backend: Option, 21 | /// The configuration path to use. 22 | #[arg(short, long, value_name = "PATH")] 23 | pub config_path: Option, 24 | /// Whether to run `uwsm` to finalize the compositor environment. 25 | #[arg(long)] 26 | #[cfg(feature = "uwsm")] 27 | pub uwsm: bool, 28 | #[command(subcommand)] 29 | pub command: Option, 30 | } 31 | 32 | #[derive(Debug, Clone, Copy, clap::Subcommand)] 33 | pub enum Command { 34 | /// Check the compositor configuration for any errors. 35 | CheckConfiguration, 36 | /// Generate shell completions for shell 37 | GenerateCompletions { shell: clap_complete::Shell }, 38 | } 39 | 40 | #[derive(Debug, Clone, Copy, clap::ValueEnum)] 41 | pub enum BackendType { 42 | #[cfg(feature = "winit-backend")] 43 | /// Use the Winit backend, inside an Winit window. 44 | Winit, 45 | #[cfg(feature = "udev-backend")] 46 | /// Use the Udev backend, using a libseat session. 47 | Udev, 48 | } 49 | 50 | fn get_version_string() -> String { 51 | let major = env!("CARGO_PKG_VERSION_MAJOR"); 52 | let minor = env!("CARGO_PKG_VERSION_MINOR"); 53 | let patch = env!("CARGO_PKG_VERSION_PATCH"); 54 | let commit = option_env!("GIT_HASH").unwrap_or("unknown"); 55 | 56 | // Since cargo forces us to "follow" semantic versionning, we must work around it. 57 | // Release 25.03 will be marked as 25.3.0 in Cargo.toml 58 | if patch == "0" { 59 | format!("{major}.{minor:0>2} ({commit})") 60 | } else { 61 | format!("{major}.{minor:0>2}.{patch} ({commit})") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | use std::thread::JoinHandle; 3 | use std::time::Duration; 4 | 5 | use smithay::reexports::calloop::{self, LoopHandle, RegistrationToken}; 6 | use smithay::reexports::rustix::path::Arg; 7 | 8 | use crate::state::State; 9 | 10 | pub mod ui; 11 | 12 | pub struct Watcher { 13 | // This token is a handle to the calloop channel that drives the reload_config messages 14 | token: RegistrationToken, 15 | _join_handles: Vec>, 16 | } 17 | 18 | impl Watcher { 19 | pub fn stop(self, loop_handle: &LoopHandle<'static, State>) { 20 | loop_handle.remove(self.token); 21 | } 22 | } 23 | 24 | pub fn init_watcher( 25 | paths: Vec, 26 | loop_handle: &LoopHandle<'static, State>, 27 | ) -> anyhow::Result { 28 | // We use a () as a dummy message to notify that the configuration file changed 29 | let (tx, channel) = calloop::channel::channel::<()>(); 30 | let token = loop_handle 31 | .insert_source(channel, |event, _, state| { 32 | if let calloop::channel::Event::Msg(()) = event { 33 | state.reload_config() 34 | } 35 | }) 36 | .map_err(|err| { 37 | anyhow::anyhow!("Failed to insert config file watcher source into event loop! {err}") 38 | })?; 39 | 40 | let mut handles = vec![]; 41 | for path in paths { 42 | let tx = tx.clone(); 43 | let path_2 = path.clone(); 44 | match std::thread::Builder::new() 45 | .name(format!( 46 | "Config file watcher for: {}", 47 | path.as_str().unwrap() 48 | )) 49 | .spawn(move || { 50 | let path: &Path = path.as_ref(); 51 | let mut last_mtime = path.metadata().and_then(|md| md.modified()).ok(); 52 | loop { 53 | std::thread::sleep(Duration::from_secs(1)); 54 | if let Some(new_mtime) = path 55 | .metadata() 56 | .and_then(|md| md.modified()) 57 | .ok() 58 | .filter(|mtime| Some(mtime) != last_mtime.as_ref()) 59 | { 60 | debug!(?new_mtime, "Config file change detected"); 61 | last_mtime = Some(new_mtime); 62 | if tx.send(()).is_err() { 63 | // Silently error as this is a way to stop this thread. 64 | // The only possible error here is that the channel got dropped, in this 65 | // case a new config file watcher will be created 66 | break; 67 | } 68 | } 69 | } 70 | }) { 71 | Ok(handle) => handles.push(handle), 72 | Err(err) => { 73 | error!( 74 | ?err, 75 | path = ?path_2, 76 | "Failed to start config file watcher for path" 77 | ); 78 | } 79 | } 80 | } 81 | 82 | Ok(Watcher { 83 | token, 84 | _join_handles: handles, 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /src/frame_clock.rs: -------------------------------------------------------------------------------- 1 | //! A frameclock for outputs. 2 | //! 3 | //! This implemenetation logic is inspired by Mutter's frame clock, `ClutterFrameClock`, with some 4 | //! cases and checks removed since we are a much simpler compositor overall. 5 | use std::num::NonZero; 6 | use std::time::Duration; 7 | 8 | use crate::utils::get_monotonic_time; 9 | 10 | #[derive(Debug)] 11 | pub struct FrameClock { 12 | // The number of nanoseconds between each presentation time. 13 | // This can be None for winit, since it does not have a set refresh time. 14 | refresh_interval_ns: Option>, 15 | last_presentation_time: Option, 16 | } 17 | 18 | impl FrameClock { 19 | pub fn new(refresh_interval: Option) -> Self { 20 | Self { 21 | refresh_interval_ns: refresh_interval.map(|interval| { 22 | NonZero::new(interval.subsec_nanos().into()) 23 | .expect("refresh internal should never be zero") 24 | }), 25 | last_presentation_time: None, 26 | } 27 | } 28 | 29 | pub fn refresh_interval(&self) -> Option { 30 | self.refresh_interval_ns 31 | .map(|r| Duration::from_nanos(r.get())) 32 | } 33 | 34 | /// Mark the latest presentation time `now` in the [`FrameClock`]. 35 | pub fn present(&mut self, now: Duration) { 36 | self.last_presentation_time = Some(now); 37 | } 38 | 39 | /// Get the next presentation time of this clock 40 | pub fn next_presentation_time(&self) -> Duration { 41 | let now = get_monotonic_time(); 42 | let Some(refresh_interval) = self 43 | .refresh_interval_ns 44 | .map(NonZero::get) 45 | .map(Duration::from_nanos) 46 | else { 47 | // Winit backend presents as soon as a redraw is done, since we don't have to wait for 48 | // a VBlank and instead just swap buffers 49 | return now; 50 | }; 51 | let Some(last_presentation_time) = self.last_presentation_time else { 52 | // We did not present anything yet. 53 | return now; 54 | }; 55 | 56 | // The common case is that the next presentation happens 1 refresh interval after the 57 | // last/previous presentation. 58 | // | 60 | // now>| | possible presentation times 82 | // \_/ \_____/ 83 | // / \ 84 | // current_phase_us refresh_interval_us 85 | // 86 | 87 | let current_phase = Duration::from_nanos( 88 | ((now.as_nanos() - last_presentation_time.as_nanos()) % refresh_interval.as_nanos()) 89 | as u64, // FIXME: can overflow, but we dont care about it 90 | ); 91 | next_presentation_time = now - current_phase + refresh_interval; 92 | } 93 | 94 | // time_since_last_next_presentation_time_us = 95 | // next_presentation_time_us - last_presentation->next_presentation_time_us; 96 | // if (time_since_last_next_presentation_time_us > 0 && 97 | // time_since_last_next_presentation_time_us < (refresh_interval_us / 2)) 98 | // { 99 | // next_presentation_time_us = 100 | // frame_clock->next_presentation_time_us + refresh_interval_us; 101 | // } 102 | 103 | next_presentation_time 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/handlers/alpha_modifiers.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_alpha_modifier; 2 | 3 | use crate::state::State; 4 | 5 | delegate_alpha_modifier!(State); 6 | -------------------------------------------------------------------------------- /src/handlers/buffer.rs: -------------------------------------------------------------------------------- 1 | use smithay::wayland::buffer::BufferHandler; 2 | 3 | use crate::state::State; 4 | 5 | impl BufferHandler for State { 6 | fn buffer_destroyed( 7 | &mut self, 8 | _buffer: &smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer, 9 | ) { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/handlers/content_type.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_content_type; 2 | 3 | use crate::state::State; 4 | 5 | delegate_content_type!(State); 6 | -------------------------------------------------------------------------------- /src/handlers/cursor_shape.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_cursor_shape; 2 | 3 | use crate::state::State; 4 | 5 | delegate_cursor_shape!(State); 6 | -------------------------------------------------------------------------------- /src/handlers/data_control.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_data_control; 2 | use smithay::wayland::selection::wlr_data_control::DataControlHandler; 3 | 4 | use crate::state::State; 5 | 6 | impl DataControlHandler for State { 7 | fn data_control_state( 8 | &self, 9 | ) -> &smithay::wayland::selection::wlr_data_control::DataControlState { 10 | &self.fht.data_control_state 11 | } 12 | } 13 | 14 | delegate_data_control!(State); 15 | -------------------------------------------------------------------------------- /src/handlers/data_device.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_data_device; 2 | use smithay::wayland::selection::data_device::DataDeviceHandler; 3 | 4 | use crate::state::State; 5 | 6 | impl DataDeviceHandler for State { 7 | fn data_device_state(&self) -> &smithay::wayland::selection::data_device::DataDeviceState { 8 | &self.fht.data_device_state 9 | } 10 | } 11 | 12 | delegate_data_device!(State); 13 | -------------------------------------------------------------------------------- /src/handlers/dmabuf.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_dmabuf; 2 | use smithay::wayland::dmabuf::DmabufHandler; 3 | 4 | use crate::state::State; 5 | 6 | impl DmabufHandler for State { 7 | fn dmabuf_state(&mut self) -> &mut smithay::wayland::dmabuf::DmabufState { 8 | &mut self.fht.dmabuf_state 9 | } 10 | 11 | #[allow(unused_mut, unreachable_code, unreachable_patterns)] 12 | fn dmabuf_imported( 13 | &mut self, 14 | _global: &smithay::wayland::dmabuf::DmabufGlobal, 15 | #[allow(unused)] dmabuf: smithay::backend::allocator::dmabuf::Dmabuf, 16 | #[allow(unused)] notifier: smithay::wayland::dmabuf::ImportNotifier, 17 | ) { 18 | match self.backend { 19 | #[cfg(feature = "winit-backend")] 20 | #[allow(irrefutable_let_patterns)] 21 | crate::backend::Backend::Winit(ref mut data) => data.dmabuf_imported(&dmabuf, notifier), 22 | #[cfg(feature = "udev-backend")] 23 | #[allow(irrefutable_let_patterns)] 24 | crate::backend::Backend::Udev(ref mut data) => data.dmabuf_imported(dmabuf, notifier), 25 | _ => unreachable!(), 26 | }; 27 | } 28 | } 29 | 30 | delegate_dmabuf!(State); 31 | -------------------------------------------------------------------------------- /src/handlers/dnd.rs: -------------------------------------------------------------------------------- 1 | use smithay::reexports::wayland_server::protocol::wl_data_source::WlDataSource; 2 | use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; 3 | use smithay::wayland::selection::data_device::{ClientDndGrabHandler, ServerDndGrabHandler}; 4 | 5 | use crate::state::State; 6 | 7 | impl ClientDndGrabHandler for State { 8 | fn started( 9 | &mut self, 10 | _source: Option, 11 | icon: Option, 12 | _seat: smithay::input::Seat, 13 | ) { 14 | self.fht.dnd_icon = icon; 15 | } 16 | 17 | fn dropped( 18 | &mut self, 19 | _target: Option, 20 | _validated: bool, 21 | _seat: smithay::input::Seat, 22 | ) { 23 | self.fht.dnd_icon = None; 24 | } 25 | } 26 | 27 | impl ServerDndGrabHandler for State { 28 | fn send( 29 | &mut self, 30 | _mime_type: String, 31 | _fd: std::os::unix::prelude::OwnedFd, 32 | _seat: smithay::input::Seat, 33 | ) { 34 | unreachable!("We don't support server-side grabs"); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/handlers/drm_lease.rs: -------------------------------------------------------------------------------- 1 | use smithay::backend::drm::DrmNode; 2 | use smithay::delegate_drm_lease; 3 | use smithay::wayland::drm_lease::{ 4 | DrmLease, DrmLeaseBuilder, DrmLeaseHandler, DrmLeaseRequest, DrmLeaseState, LeaseRejected, 5 | }; 6 | 7 | use crate::state::State; 8 | 9 | impl DrmLeaseHandler for State { 10 | fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState { 11 | self.backend 12 | .udev() 13 | .devices 14 | .get_mut(&node) 15 | .unwrap() 16 | .lease_state 17 | .as_mut() 18 | .unwrap() 19 | } 20 | 21 | fn lease_request( 22 | &mut self, 23 | node: DrmNode, 24 | request: DrmLeaseRequest, 25 | ) -> Result { 26 | let device = self 27 | .backend 28 | .udev() 29 | .devices 30 | .get(&node) 31 | .ok_or(LeaseRejected::default())?; 32 | 33 | let drm_device = device.drm_output_manager.device(); 34 | let mut builder = DrmLeaseBuilder::new(drm_device); 35 | for conn in request.connectors { 36 | if let Some((_, crtc)) = device 37 | .non_desktop_connectors 38 | .iter() 39 | .find(|(handle, _)| *handle == conn) 40 | { 41 | builder.add_connector(conn); 42 | builder.add_crtc(*crtc); 43 | let planes = drm_device.planes(crtc).map_err(LeaseRejected::with_cause)?; 44 | 45 | let (primary_plane, primary_plane_claim) = planes 46 | .primary 47 | .iter() 48 | .find_map(|plane| { 49 | drm_device 50 | .claim_plane(plane.handle, *crtc) 51 | .map(|claim| (plane, claim)) 52 | }) 53 | .ok_or_else(LeaseRejected::default)?; 54 | builder.add_plane(primary_plane.handle, primary_plane_claim); 55 | if let Some((cursor, claim)) = planes.cursor.iter().find_map(|plane| { 56 | drm_device 57 | .claim_plane(plane.handle, *crtc) 58 | .map(|claim| (plane, claim)) 59 | }) { 60 | builder.add_plane(cursor.handle, claim); 61 | } 62 | } else { 63 | warn!( 64 | ?conn, 65 | "Lease requested for desktop connector, denying request" 66 | ); 67 | return Err(LeaseRejected::default()); 68 | } 69 | } 70 | 71 | Ok(builder) 72 | } 73 | 74 | fn new_active_lease(&mut self, node: DrmNode, lease: DrmLease) { 75 | let backend = self.backend.udev().devices.get_mut(&node).unwrap(); 76 | backend.active_leases.push(lease); 77 | } 78 | 79 | fn lease_destroyed(&mut self, node: DrmNode, lease_id: u32) { 80 | let backend = self.backend.udev().devices.get_mut(&node).unwrap(); 81 | backend.active_leases.retain(|l| l.id() != lease_id); 82 | } 83 | } 84 | 85 | delegate_drm_lease!(State); 86 | -------------------------------------------------------------------------------- /src/handlers/drm_syncobj.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_drm_syncobj; 2 | use smithay::wayland::drm_syncobj::{DrmSyncobjHandler, DrmSyncobjState}; 3 | 4 | use crate::state::State; 5 | 6 | impl DrmSyncobjHandler for State { 7 | fn drm_syncobj_state(&mut self) -> &mut DrmSyncobjState { 8 | let backend = self.backend.udev(); 9 | backend.syncobj_state.as_mut().expect( 10 | "drm syncobj request should only happen when Syncobj state has been initialized", 11 | ) 12 | } 13 | } 14 | 15 | delegate_drm_syncobj!(State); 16 | -------------------------------------------------------------------------------- /src/handlers/foreign_toplevel_list.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_foreign_toplevel_list; 2 | use smithay::wayland::foreign_toplevel_list::{ 3 | ForeignToplevelListHandler, ForeignToplevelListState, 4 | }; 5 | 6 | use crate::state::{Fht, State}; 7 | use crate::window::Window; 8 | 9 | impl ForeignToplevelListHandler for State { 10 | fn foreign_toplevel_list_state(&mut self) -> &mut ForeignToplevelListState { 11 | &mut self.fht.foreign_toplevel_list_state 12 | } 13 | } 14 | 15 | impl Fht { 16 | /// Adversite a new [`Window`] with the ext-foreignt-toplevel-v1 protocol. 17 | /// 18 | /// This creates the toplevel handle and stores it inside the [`Window`]. 19 | pub fn adversite_new_foreign_window(&mut self, window: &Window) { 20 | if window.foreign_toplevel_handle().is_some() { 21 | warn!(window = ?window.id(), "Tried to adversite window to ext-foreign-toplevel-v1 twice"); 22 | return; 23 | } 24 | 25 | // FIXME: This can result in empty title/app-id, but that's how cosmic-comp handles it, 26 | // apparently. Protocol spec does not say anything about this 27 | let app_id = window.app_id().unwrap_or_else(|| { 28 | warn!(window = ?window.id(), "Window without app_id"); 29 | Default::default() 30 | }); 31 | let title = window.title().unwrap_or_else(|| app_id.clone()); 32 | let handle = self 33 | .foreign_toplevel_list_state 34 | .new_toplevel::(title.clone(), app_id.clone()); 35 | 36 | // send all initial data. 37 | 38 | window.set_foreign_toplevel_handle(handle); 39 | } 40 | 41 | /// De-adversite a [`Window`] with the ext-foreignt-toplevel-v1 protocol. 42 | pub fn close_foreign_handle(&mut self, window: &Window) { 43 | let Some(handle) = window.take_foreign_toplevel_handle() else { 44 | // this can happen, for example unmapped window gets removed. 45 | return; 46 | }; 47 | 48 | self.foreign_toplevel_list_state.remove_toplevel(&handle); 49 | } 50 | 51 | /// Send new window details for all ext-toplevel-foreign-list instances. 52 | pub fn send_foreign_window_details(&mut self, window: &Window) { 53 | if let Some(handle) = window.foreign_toplevel_handle() { 54 | match window.title() { 55 | Some(title) => handle.send_title(&title), 56 | None => error!(window = ?window.id(), "Window changed title to None?"), 57 | } 58 | 59 | match window.app_id() { 60 | Some(app_id) => handle.send_app_id(&app_id), 61 | None => error!(window = ?window.id(), "Window changed app_id to None?"), 62 | } 63 | 64 | handle.send_done(); 65 | } else { 66 | // it was not adversited before, this should be done on-map 67 | // this shoud not happen though. 68 | warn!(window = ?window.id(), "Tried updating foreign toplevel handle details for window without one"); 69 | self.adversite_new_foreign_window(window); 70 | } 71 | } 72 | } 73 | 74 | delegate_foreign_toplevel_list!(State); 75 | -------------------------------------------------------------------------------- /src/handlers/fractional_scale.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use smithay::delegate_fractional_scale; 4 | use smithay::desktop::utils::surface_primary_scanout_output; 5 | use smithay::output::Output; 6 | use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; 7 | use smithay::wayland::compositor::{get_parent, with_states, SurfaceData}; 8 | use smithay::wayland::fractional_scale::{with_fractional_scale, FractionalScaleHandler}; 9 | 10 | use crate::state::State; 11 | 12 | impl FractionalScaleHandler for State { 13 | fn new_fractional_scale(&mut self, surface: WlSurface) { 14 | // A surface has asked to get a fractional scale matching its output. 15 | // 16 | // First check: The surface has a primary scanout output: then use that output's scale. 17 | // Second check: The surface is a subsurface, try to use the root's scanout output scale 18 | // Third check: The surface is root == a toplevel, use the toplevel's workspace output 19 | // scale. 20 | #[allow(clippy::redundant_clone)] 21 | let mut root = surface.clone(); 22 | while let Some(parent) = get_parent(&root) { 23 | root = parent; 24 | } 25 | 26 | let get_scanout_output = |surface: &WlSurface, states: &SurfaceData| { 27 | surface_primary_scanout_output(surface, states).or_else(|| { 28 | // Our custom send frames throlling state. 29 | let last_callback_output: &RefCell> = 30 | states.data_map.get_or_insert(RefCell::default); 31 | let last_callback_output = last_callback_output.borrow_mut(); 32 | last_callback_output.as_ref().map(|(o, _)| o).cloned() 33 | }) 34 | }; 35 | 36 | let primary_scanout_output = if root != surface { 37 | // We are the root surface. 38 | with_states(&root, |states| get_scanout_output(&root, states)).or_else(|| { 39 | // Use window workspace output. 40 | self.fht 41 | .space 42 | .workspace_for_window_surface(&root) 43 | .map(|ws| ws.output().clone()) 44 | }) 45 | } else { 46 | // We are not the root surface, try from surface state. 47 | with_states(&surface, |states| get_scanout_output(&surface, states)).or_else(|| { 48 | // Try the root of the surface, if possible 49 | self.fht 50 | .space 51 | .workspace_for_window_surface(&root) 52 | .map(|ws| ws.output().clone()) 53 | }) 54 | } 55 | .unwrap_or_else(|| self.fht.space.primary_output().clone()); 56 | 57 | with_states(&surface, |states| { 58 | with_fractional_scale(states, |fractional_scale| { 59 | fractional_scale 60 | .set_preferred_scale(primary_scanout_output.current_scale().fractional_scale()); 61 | }); 62 | }); 63 | } 64 | } 65 | 66 | delegate_fractional_scale!(State); 67 | -------------------------------------------------------------------------------- /src/handlers/idle_inhibit.rs: -------------------------------------------------------------------------------- 1 | use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; 2 | use smithay::wayland::idle_inhibit::IdleInhibitHandler; 3 | use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState}; 4 | use smithay::{delegate_idle_inhibit, delegate_idle_notify}; 5 | 6 | use crate::state::State; 7 | 8 | impl IdleInhibitHandler for State { 9 | fn inhibit(&mut self, surface: WlSurface) { 10 | self.fht.idle_inhibiting_surfaces.push(surface); 11 | } 12 | 13 | fn uninhibit(&mut self, surface: WlSurface) { 14 | self.fht.idle_inhibiting_surfaces.retain(|s| *s != surface); 15 | } 16 | } 17 | 18 | delegate_idle_inhibit!(State); 19 | 20 | impl IdleNotifierHandler for State { 21 | fn idle_notifier_state(&mut self) -> &mut IdleNotifierState { 22 | &mut self.fht.idle_notifier_state 23 | } 24 | } 25 | 26 | delegate_idle_notify!(State); 27 | -------------------------------------------------------------------------------- /src/handlers/input_method.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_input_method_manager; 2 | use smithay::desktop::{PopupKind, PopupManager}; 3 | use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; 4 | use smithay::utils::Rectangle; 5 | use smithay::wayland::input_method::{InputMethodHandler, PopupSurface}; 6 | 7 | use crate::state::State; 8 | 9 | impl InputMethodHandler for State { 10 | fn new_popup(&mut self, surface: PopupSurface) { 11 | if let Err(err) = self.fht.popups.track_popup(PopupKind::from(surface)) { 12 | warn!("Failed to track popup: {}", err); 13 | } 14 | } 15 | 16 | fn popup_repositioned(&mut self, _: PopupSurface) {} 17 | 18 | fn dismiss_popup(&mut self, surface: PopupSurface) { 19 | if let Some(parent) = surface.get_parent().map(|parent| parent.surface.clone()) { 20 | let _ = PopupManager::dismiss_popup(&parent, &PopupKind::from(surface)); 21 | } 22 | } 23 | 24 | fn parent_geometry(&self, parent: &WlSurface) -> Rectangle { 25 | self.fht 26 | .space 27 | .find_window(parent) 28 | .map(|w| Rectangle::new(w.render_offset(), w.size())) 29 | .unwrap_or_default() 30 | } 31 | } 32 | 33 | delegate_input_method_manager!(State); 34 | -------------------------------------------------------------------------------- /src/handlers/keyboard_shortcuts_inhibit.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_keyboard_shortcuts_inhibit; 2 | use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitHandler; 3 | 4 | use crate::state::State; 5 | 6 | impl KeyboardShortcutsInhibitHandler for State { 7 | fn keyboard_shortcuts_inhibit_state( 8 | &mut self, 9 | ) -> &mut smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitState { 10 | &mut self.fht.keyboard_shortcuts_inhibit_state 11 | } 12 | 13 | fn new_inhibitor( 14 | &mut self, 15 | inhibitor: smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitor, 16 | ) { 17 | // Just allow it 18 | inhibitor.activate(); 19 | } 20 | } 21 | 22 | delegate_keyboard_shortcuts_inhibit!(State); 23 | -------------------------------------------------------------------------------- /src/handlers/layer_shell.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_layer_shell; 2 | use smithay::desktop::{layer_map_for_output, LayerSurface, PopupKind, WindowSurfaceType}; 3 | use smithay::output::Output; 4 | use smithay::reexports::wayland_server::protocol::wl_output; 5 | use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; 6 | use smithay::wayland::compositor::with_states; 7 | use smithay::wayland::shell::wlr_layer::{ 8 | self, Layer, LayerSurfaceData, WlrLayerShellHandler, WlrLayerShellState, 9 | }; 10 | use smithay::wayland::shell::xdg::PopupSurface; 11 | 12 | use crate::layer::ResolvedLayerRules; 13 | use crate::renderer::blur::EffectsFramebuffers; 14 | use crate::state::{Fht, State}; 15 | 16 | impl WlrLayerShellHandler for State { 17 | fn shell_state(&mut self) -> &mut WlrLayerShellState { 18 | &mut self.fht.layer_shell_state 19 | } 20 | 21 | fn new_layer_surface( 22 | &mut self, 23 | surface: wlr_layer::LayerSurface, 24 | output: Option, 25 | wlr_layer: wlr_layer::Layer, 26 | namespace: String, 27 | ) { 28 | // We don't map layer surfaces immediatly, rather, they get pushed to `pending_layers` 29 | // before mapping. The compositors waits for the initial configure of the layer surface 30 | // before mapping so we are sure it have dimensions and a render buffer 31 | let output = output 32 | .as_ref() 33 | .and_then(Output::from_resource) 34 | .unwrap_or_else(|| self.fht.space.active_output().clone()); 35 | let layer_surface = LayerSurface::new(surface, namespace); 36 | 37 | // Initially resolve layer rules. 38 | // --- 39 | // The layer surface might not have sent its namespace yet, but still initialize state with 40 | // whatever we have here. 41 | ResolvedLayerRules::resolve(&layer_surface, &self.fht.config.layer_rules, &output); 42 | 43 | if matches!(wlr_layer, Layer::Background | Layer::Bottom) { 44 | // the optimized blur buffer has been dirtied, re-render on next State::dispatch 45 | EffectsFramebuffers::get(&output).optimized_blur_dirty = true; 46 | } 47 | 48 | let mut map = layer_map_for_output(&output); 49 | if let Err(err) = map.map_layer(&layer_surface) { 50 | error!(?err, "Failed to map layer-shell"); 51 | } 52 | } 53 | 54 | fn layer_destroyed(&mut self, surface: wlr_layer::LayerSurface) { 55 | let mut layer_output = None; 56 | if let Some((mut layer_map, layer, output)) = self.fht.space.outputs().find_map(|o| { 57 | let layer_map = layer_map_for_output(o); 58 | let layer = layer_map 59 | .layers() 60 | .find(|&layer| layer.layer_surface() == &surface) 61 | .cloned(); 62 | layer.map(|l| (layer_map, l, o.clone())) 63 | }) { 64 | // Otherwise, it was already mapped, unmap it then close 65 | layer_map.unmap_layer(&layer); 66 | layer.layer_surface().send_close(); 67 | 68 | if matches!(layer.layer(), Layer::Background | Layer::Bottom) { 69 | // the optimized blur buffer has been dirtied, re-render on next State::dispatch 70 | EffectsFramebuffers::get(&output).optimized_blur_dirty = true; 71 | } 72 | 73 | layer_output = Some(output); 74 | } 75 | 76 | if let Some(output) = layer_output { 77 | self.fht.output_resized(&output); 78 | } 79 | } 80 | 81 | fn new_popup(&mut self, parent: wlr_layer::LayerSurface, popup: PopupSurface) { 82 | let desktop_layer = self.fht.space.outputs().find_map(|output| { 83 | let layer_map = layer_map_for_output(output); 84 | let layer = layer_map 85 | .layers() 86 | .find(|layer| layer.layer_surface() == &parent)?; 87 | Some((layer.clone(), output.clone())) 88 | }); 89 | 90 | if let Some((parent_layer, output)) = desktop_layer { 91 | self.fht 92 | .unconstrain_layer_popup(&popup, &parent_layer, &output); 93 | if let Err(err) = self.fht.popups.track_popup(PopupKind::from(popup)) { 94 | tracing::warn!(?err, "Failed to track layer shell popup!"); 95 | } 96 | } 97 | } 98 | } 99 | 100 | impl State { 101 | pub fn process_layer_shell_commit(surface: &WlSurface, state: &mut Fht) -> Option { 102 | let mut layer_output = None; 103 | if let Some(output) = state.space.outputs().find(|o| { 104 | let map = layer_map_for_output(o); 105 | map.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL) 106 | .is_some() 107 | }) { 108 | layer_output = Some(output.clone()); 109 | let initial_configure_sent = with_states(surface, |states| { 110 | states 111 | .data_map 112 | .get::() 113 | .unwrap() 114 | .lock() 115 | .unwrap() 116 | .initial_configure_sent 117 | }); 118 | 119 | let mut map = layer_map_for_output(output); 120 | 121 | // arrange the layers before sending the initial configure 122 | // to respect any size the client may have sent 123 | map.arrange(); 124 | let layer = map 125 | .layer_for_surface(surface, WindowSurfaceType::TOPLEVEL) 126 | .unwrap(); 127 | // send the initial configure if relevant 128 | if !initial_configure_sent { 129 | if matches!(layer.layer(), Layer::Background | Layer::Bottom) { 130 | // the optimized blur buffer has been dirtied, re-render on next State::dispatch 131 | EffectsFramebuffers::get(output).optimized_blur_dirty = true; 132 | } 133 | 134 | layer.layer_surface().send_configure(); 135 | } 136 | 137 | // FIXME: Maybe check if there were changes before commiting? 138 | ResolvedLayerRules::resolve(layer, &state.config.layer_rules, output); 139 | } 140 | if let Some(output) = layer_output.as_ref() { 141 | // fighting rust's borrow checker episode 32918731287 142 | state.output_resized(output); 143 | } 144 | 145 | layer_output 146 | } 147 | } 148 | 149 | delegate_layer_shell!(State); 150 | -------------------------------------------------------------------------------- /src/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | mod alpha_modifiers; 2 | mod buffer; 3 | mod compositor; 4 | mod content_type; 5 | mod cursor_shape; 6 | mod data_control; 7 | mod data_device; 8 | mod dmabuf; 9 | mod dnd; 10 | #[cfg(feature = "udev-backend")] 11 | mod drm_lease; 12 | #[cfg(feature = "udev-backend")] 13 | mod drm_syncobj; 14 | mod foreign_toplevel_list; 15 | mod fractional_scale; 16 | mod idle_inhibit; 17 | mod input_method; 18 | mod keyboard_shortcuts_inhibit; 19 | mod layer_shell; 20 | mod output; 21 | mod output_management; 22 | mod pointer_constraints; 23 | mod pointer_gestures; 24 | mod presentation; 25 | mod primary_selection; 26 | mod relative_pointer; 27 | mod screencopy; 28 | mod seat; 29 | mod security_context; 30 | mod selection; 31 | pub mod session_lock; 32 | mod shm; 33 | mod single_pixel_buffer; 34 | mod viewporter; 35 | mod virtual_keyboard; 36 | mod xdg_activation; 37 | mod xdg_decoration; 38 | mod xdg_dialog; 39 | mod xdg_foreign; 40 | mod xdg_shell; 41 | -------------------------------------------------------------------------------- /src/handlers/output.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_output; 2 | use smithay::wayland::output::OutputHandler; 3 | 4 | use crate::state::State; 5 | 6 | impl OutputHandler for State {} 7 | 8 | delegate_output!(State); 9 | -------------------------------------------------------------------------------- /src/handlers/output_management.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use smithay::output::{self, Mode, Output}; 4 | 5 | use crate::delegate_output_management; 6 | use crate::protocols::output_management::{ 7 | OutputConfiguration, OutputManagementHandler, OutputManagementManagerState, 8 | }; 9 | use crate::state::State; 10 | 11 | impl OutputManagementHandler for State { 12 | fn output_management_manager_state(&mut self) -> &mut OutputManagementManagerState { 13 | &mut self.fht.output_management_manager_state 14 | } 15 | 16 | fn apply_configuration(&mut self, config: HashMap) -> bool { 17 | // We filter by the outputs we know. 18 | let known_outputs = self.fht.space.outputs().cloned().collect::>(); 19 | let mut any_changed = false; 20 | 21 | for (output, config) in config 22 | .into_iter() 23 | .filter(|(output, _)| known_outputs.contains(output)) 24 | { 25 | let output_name = output.name(); 26 | debug!("Applying wlr-output-configuration for {output_name}"); 27 | 28 | // FIXME: Handle output powered state 29 | let OutputConfiguration::Enabled { 30 | mode, 31 | position, 32 | transform, 33 | scale, 34 | adaptive_sync: _, // FIXME: Handle VRR 35 | } = config 36 | else { 37 | continue; 38 | }; 39 | 40 | let changed = 41 | mode.is_some() || position.is_some() || transform.is_some() || scale.is_some(); 42 | if !changed { 43 | continue; 44 | } 45 | 46 | if let Some(mode) = mode.map(|(size, refresh)| Mode { 47 | size, 48 | refresh: refresh.map(|v| v.get() as i32).unwrap_or(60000), 49 | }) { 50 | // First try to switch in the backend 51 | if let Err(err) = self.backend.set_output_mode(&mut self.fht, &output, mode) { 52 | error!( 53 | ?err, 54 | "Failed to apply wlr-output-configuration mode for {output_name}" 55 | ); 56 | return false; 57 | } 58 | } 59 | 60 | output.change_current_state( 61 | None, 62 | transform, 63 | scale.map(|scale| output::Scale::Integer(scale.round() as i32)), 64 | position, 65 | ); 66 | 67 | if changed { 68 | self.fht.output_resized(&output); 69 | any_changed = true; 70 | } 71 | } 72 | 73 | if any_changed { 74 | self.fht.has_transient_output_changes = true; 75 | } 76 | 77 | true 78 | } 79 | 80 | fn test_configuration(&mut self, _config: HashMap) -> bool { 81 | // FIXME: Actually test the configuration 82 | true 83 | } 84 | } 85 | 86 | delegate_output_management!(State); 87 | -------------------------------------------------------------------------------- /src/handlers/pointer_constraints.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use smithay::delegate_pointer_constraints; 4 | use smithay::input::pointer::PointerHandle; 5 | use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; 6 | use smithay::utils::{Logical, Point}; 7 | use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler}; 8 | use smithay::wayland::seat::WaylandFocus; 9 | 10 | use crate::state::{Fht, State}; 11 | 12 | impl Fht { 13 | /// Activate the pointer constraint associated with the currently focused surface, if any. 14 | pub fn activate_pointer_constraint(&mut self) { 15 | let pointer = self.seat.get_pointer().unwrap(); 16 | let pointer_loc = pointer.current_location(); 17 | 18 | let Some((under, surface_loc)) = self.focus_target_under(pointer_loc) else { 19 | return; 20 | }; 21 | let Some(surface) = under.wl_surface() else { 22 | return; 23 | }; 24 | 25 | with_pointer_constraint(&surface, &pointer, |constraint| { 26 | let Some(constraint) = constraint else { return }; 27 | 28 | if constraint.is_active() { 29 | return; 30 | } 31 | 32 | // Constraint does not apply if not within region. 33 | if let Some(region) = constraint.region() { 34 | let pos_within_surface = pointer_loc - surface_loc.to_f64(); 35 | if !region.contains(pos_within_surface.to_i32_round()) { 36 | return; 37 | } 38 | } 39 | 40 | constraint.activate(); 41 | }); 42 | } 43 | } 44 | 45 | impl PointerConstraintsHandler for State { 46 | fn new_constraint(&mut self, _: &WlSurface, _: &PointerHandle) { 47 | self.update_pointer_focus(); 48 | self.fht.activate_pointer_constraint(); 49 | } 50 | 51 | fn cursor_position_hint( 52 | &mut self, 53 | surface: &WlSurface, 54 | pointer: &PointerHandle, 55 | location: Point, 56 | ) { 57 | // Implementation copied from anvil 58 | if with_pointer_constraint(surface, pointer, |constraint| { 59 | constraint.is_some_and(|c| c.is_active()) 60 | }) { 61 | let Some(focused_surface) = pointer 62 | .current_focus() 63 | .and_then(|f| f.wl_surface().map(Cow::into_owned)) 64 | else { 65 | return; 66 | }; 67 | 68 | if &focused_surface != surface { 69 | // only focused surfaec can give position hint 70 | // this is to avoid random cursor warps around the screen 71 | return; 72 | } 73 | 74 | let Some(window) = self.fht.space.find_window(surface) else { 75 | return; 76 | }; 77 | let window_loc = self.fht.space.window_location(&window).unwrap(); 78 | pointer.set_location(window_loc.to_f64() + window.render_offset().to_f64() + location); 79 | } 80 | } 81 | } 82 | 83 | delegate_pointer_constraints!(State); 84 | -------------------------------------------------------------------------------- /src/handlers/pointer_gestures.rs: -------------------------------------------------------------------------------- 1 | smithay::delegate_pointer_gestures!(crate::state::State); 2 | -------------------------------------------------------------------------------- /src/handlers/presentation.rs: -------------------------------------------------------------------------------- 1 | smithay::delegate_presentation!(crate::state::State); 2 | -------------------------------------------------------------------------------- /src/handlers/primary_selection.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_primary_selection; 2 | use smithay::wayland::selection::primary_selection::PrimarySelectionHandler; 3 | 4 | use crate::state::State; 5 | 6 | impl PrimarySelectionHandler for State { 7 | fn primary_selection_state( 8 | &self, 9 | ) -> &smithay::wayland::selection::primary_selection::PrimarySelectionState { 10 | &self.fht.primary_selection_state 11 | } 12 | } 13 | 14 | delegate_primary_selection!(State); 15 | -------------------------------------------------------------------------------- /src/handlers/relative_pointer.rs: -------------------------------------------------------------------------------- 1 | smithay::delegate_relative_pointer!(crate::state::State); 2 | -------------------------------------------------------------------------------- /src/handlers/screencopy.rs: -------------------------------------------------------------------------------- 1 | use crate::delegate_screencopy; 2 | use crate::protocols::screencopy::{ScreencopyFrame, ScreencopyHandler}; 3 | use crate::state::State; 4 | 5 | impl ScreencopyHandler for State { 6 | fn new_frame(&mut self, frame: ScreencopyFrame) { 7 | let Some(output_state) = self.fht.output_state.get_mut(frame.output()) else { 8 | warn!("wlr-screencopy frame with invalid output"); 9 | return; 10 | }; 11 | 12 | // A weird quirk with wlr-screencopy is that the clients decide of frame scheduling, not the 13 | // compositor, which causes some weird situations with our redraw loop. 14 | // 15 | // If the screencopy frame is requested with damage, we wait until the backend has damage 16 | // to submit. If the screencopy frame is requested without damage, we queue a redraw of the 17 | // output to satisfy the screencopy request on the next dispatch cycle. 18 | if !frame.with_damage() { 19 | // If we need damage, wait for the next render. 20 | output_state.redraw_state.queue(); 21 | } 22 | 23 | output_state.pending_screencopies.push(frame); 24 | } 25 | } 26 | 27 | delegate_screencopy!(State); 28 | -------------------------------------------------------------------------------- /src/handlers/seat.rs: -------------------------------------------------------------------------------- 1 | use smithay::input::keyboard::LedState; 2 | use smithay::input::pointer::CursorImageStatus; 3 | use smithay::input::{Seat, SeatHandler, SeatState}; 4 | use smithay::reexports::input::DeviceCapability; 5 | use smithay::reexports::wayland_server::Resource; 6 | use smithay::wayland::seat::WaylandFocus; 7 | use smithay::wayland::selection::data_device::set_data_device_focus; 8 | use smithay::wayland::selection::primary_selection::set_primary_focus; 9 | use smithay::wayland::tablet_manager::TabletSeatHandler; 10 | use smithay::{delegate_seat, delegate_tablet_manager, delegate_text_input_manager}; 11 | 12 | use crate::focus_target::{KeyboardFocusTarget, PointerFocusTarget}; 13 | use crate::state::State; 14 | 15 | impl TabletSeatHandler for State { 16 | fn tablet_tool_image( 17 | &mut self, 18 | _tool: &smithay::backend::input::TabletToolDescriptor, 19 | image_status: CursorImageStatus, 20 | ) { 21 | self.fht.cursor_theme_manager.set_image_status(image_status); 22 | } 23 | } 24 | 25 | impl SeatHandler for State { 26 | type KeyboardFocus = KeyboardFocusTarget; 27 | type PointerFocus = PointerFocusTarget; 28 | type TouchFocus = PointerFocusTarget; 29 | 30 | fn seat_state(&mut self) -> &mut SeatState { 31 | &mut self.fht.seat_state 32 | } 33 | 34 | fn focus_changed(&mut self, seat: &Seat, focused: Option<&KeyboardFocusTarget>) { 35 | let dh = &self.fht.display_handle; 36 | let wl_surface = focused.and_then(WaylandFocus::wl_surface); 37 | let client = wl_surface.and_then(|s| dh.get_client(s.id()).ok()); 38 | set_data_device_focus(dh, seat, client.clone()); 39 | set_primary_focus(dh, seat, client); 40 | } 41 | 42 | fn led_state_changed(&mut self, _seat: &Seat, led_state: LedState) { 43 | let keyboards = self 44 | .fht 45 | .devices 46 | .iter() 47 | .filter(|device| device.has_capability(DeviceCapability::Keyboard)) 48 | .cloned(); 49 | 50 | for mut keyboard in keyboards { 51 | keyboard.led_update(led_state.into()); 52 | } 53 | } 54 | 55 | fn cursor_image(&mut self, _seat: &Seat, image: CursorImageStatus) { 56 | self.fht.cursor_theme_manager.set_image_status(image); 57 | } 58 | } 59 | 60 | delegate_seat!(State); 61 | 62 | delegate_tablet_manager!(State); 63 | 64 | delegate_text_input_manager!(State); 65 | -------------------------------------------------------------------------------- /src/handlers/security_context.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use smithay::delegate_security_context; 4 | use smithay::wayland::security_context::{ 5 | SecurityContext, SecurityContextHandler, SecurityContextListenerSource, 6 | }; 7 | 8 | use crate::state::{ClientState, State}; 9 | 10 | impl SecurityContextHandler for State { 11 | fn context_created(&mut self, source: SecurityContextListenerSource, context: SecurityContext) { 12 | self.fht 13 | .loop_handle 14 | .insert_source(source, move |client_stream, _, state| { 15 | let client_state = ClientState { 16 | security_context: Some(context.clone()), 17 | ..ClientState::default() 18 | }; 19 | 20 | if let Err(err) = state 21 | .fht 22 | .display_handle 23 | .insert_client(client_stream, Arc::new(client_state)) 24 | { 25 | warn!(?err, "Failed to add wayland client to display") 26 | } 27 | }) 28 | .expect("Failed to init Wayland security context source!"); 29 | } 30 | } 31 | 32 | delegate_security_context!(State); 33 | -------------------------------------------------------------------------------- /src/handlers/selection.rs: -------------------------------------------------------------------------------- 1 | impl smithay::wayland::selection::SelectionHandler for crate::state::State { 2 | type SelectionUserData = (); 3 | } 4 | -------------------------------------------------------------------------------- /src/handlers/session_lock.rs: -------------------------------------------------------------------------------- 1 | use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; 2 | use smithay::backend::renderer::element::surface::{ 3 | render_elements_from_surface_tree, WaylandSurfaceRenderElement, 4 | }; 5 | use smithay::backend::renderer::element::Kind; 6 | use smithay::backend::renderer::Color32F; 7 | use smithay::delegate_session_lock; 8 | use smithay::output::Output; 9 | use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; 10 | use smithay::utils::Point; 11 | use smithay::wayland::compositor::{send_surface_state, with_states}; 12 | use smithay::wayland::fractional_scale::with_fractional_scale; 13 | use smithay::wayland::session_lock::{self, LockSurface, SessionLockHandler}; 14 | 15 | use crate::output::OutputExt; 16 | use crate::renderer::FhtRenderer; 17 | use crate::state::{Fht, State}; 18 | 19 | crate::fht_render_elements! { 20 | SessionLockRenderElement => { 21 | ClearBackdrop = SolidColorRenderElement, 22 | LockSurface = WaylandSurfaceRenderElement, 23 | } 24 | } 25 | 26 | const LOCKED_OUTPUT_BACKDROP_COLOR: Color32F = Color32F::new(0.05, 0.05, 0.05, 1.0); 27 | 28 | impl SessionLockHandler for State { 29 | fn lock_state(&mut self) -> &mut session_lock::SessionLockManagerState { 30 | &mut self.fht.session_lock_manager_state 31 | } 32 | 33 | fn lock(&mut self, locker: session_lock::SessionLocker) { 34 | self.fht.lock_state = LockState::Pending(locker); 35 | } 36 | 37 | fn unlock(&mut self) { 38 | self.fht.lock_state = LockState::Unlocked; 39 | // "Unlock" all the outputs 40 | let outputs = self.fht.space.outputs().cloned().collect::>(); 41 | for output in &outputs { 42 | let output_state = self.fht.output_state.get_mut(output).unwrap(); 43 | output_state.lock_backdrop = None; 44 | let _ = output_state.lock_surface.take(); 45 | } 46 | // Reset focus 47 | let active_window = self.fht.space.active_window(); 48 | self.set_keyboard_focus(active_window); 49 | } 50 | 51 | fn new_surface(&mut self, lock_surface: LockSurface, wl_output: WlOutput) { 52 | let Some(output) = Output::from_resource(&wl_output) else { 53 | return; 54 | }; 55 | 56 | // Configure our surface for the output 57 | let output_size = output.geometry().size; 58 | lock_surface.with_pending_state(|state| { 59 | state.size = Some((output_size.w as u32, output_size.h as u32).into()); 60 | }); 61 | let scale = output.current_scale(); 62 | let transform = output.current_transform(); 63 | let wl_surface = lock_surface.wl_surface(); 64 | with_states(wl_surface, |data| { 65 | send_surface_state(wl_surface, data, scale.integer_scale(), transform); 66 | with_fractional_scale(data, |fractional| { 67 | fractional.set_preferred_scale(scale.fractional_scale()); 68 | }); 69 | }); 70 | 71 | lock_surface.send_configure(); 72 | 73 | let output_state = self.fht.output_state.get_mut(&output).unwrap(); 74 | output_state.lock_surface = Some(lock_surface.clone()); 75 | output_state.redraw_state.queue(); 76 | 77 | if output == *self.fht.space.active_output() { 78 | // Focus the newly placed lock surface. 79 | self.set_keyboard_focus(Some(lock_surface)); 80 | } 81 | } 82 | } 83 | 84 | delegate_session_lock!(State); 85 | 86 | impl Fht { 87 | pub fn is_locked(&self) -> bool { 88 | matches!(&self.lock_state, LockState::Locked | LockState::Pending(_)) 89 | } 90 | 91 | pub fn session_lock_elements( 92 | &mut self, 93 | renderer: &mut R, 94 | output: &Output, 95 | ) -> Vec> { 96 | let scale = output.current_scale().integer_scale() as f64; 97 | let mut elements = vec![]; 98 | if !self.is_locked() { 99 | return elements; 100 | } 101 | 102 | let output_state = self.output_state.get_mut(output).unwrap(); 103 | 104 | if let Some(lock_surface) = output_state.lock_surface.as_ref() { 105 | elements.extend(render_elements_from_surface_tree( 106 | renderer, 107 | lock_surface.wl_surface(), 108 | Point::default(), 109 | scale, 110 | 1.0, 111 | Kind::Unspecified, 112 | )); 113 | } 114 | 115 | // We still render a black drop to not show desktop content 116 | let solid_buffer = output_state.lock_backdrop.get_or_insert_with(|| { 117 | SolidColorBuffer::new(output.geometry().size, LOCKED_OUTPUT_BACKDROP_COLOR) 118 | }); 119 | 120 | elements.push( 121 | SolidColorRenderElement::from_buffer( 122 | &*solid_buffer, 123 | Point::default(), 124 | scale, 125 | 1.0, 126 | Kind::Unspecified, 127 | ) 128 | .into(), 129 | ); 130 | 131 | elements 132 | } 133 | } 134 | 135 | /// The locking state of the compositor. 136 | /// 137 | /// Needed in order to notify the session lock confirmation that we drew a black backdrop over all 138 | /// the outputs of the compositor. 139 | #[derive(Default, Debug)] 140 | pub enum LockState { 141 | /// The compositor is unlocked and displays content as usual. 142 | #[default] 143 | Unlocked, 144 | /// The compositor has received a lock request and is in the process of drawing a black 145 | /// backdrop Over all the [`Output`]s 146 | Pending(session_lock::SessionLocker), 147 | /// The compositor is fully locked. 148 | Locked, 149 | } 150 | -------------------------------------------------------------------------------- /src/handlers/shm.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_shm; 2 | use smithay::wayland::shm::ShmHandler; 3 | 4 | use crate::state::State; 5 | 6 | impl ShmHandler for State { 7 | fn shm_state(&self) -> &smithay::wayland::shm::ShmState { 8 | &self.fht.shm_state 9 | } 10 | } 11 | 12 | delegate_shm!(State); 13 | -------------------------------------------------------------------------------- /src/handlers/single_pixel_buffer.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_single_pixel_buffer; 2 | 3 | use crate::state::State; 4 | 5 | delegate_single_pixel_buffer!(State); 6 | -------------------------------------------------------------------------------- /src/handlers/viewporter.rs: -------------------------------------------------------------------------------- 1 | smithay::delegate_viewporter!(crate::state::State); 2 | -------------------------------------------------------------------------------- /src/handlers/virtual_keyboard.rs: -------------------------------------------------------------------------------- 1 | smithay::delegate_virtual_keyboard_manager!(crate::state::State); 2 | -------------------------------------------------------------------------------- /src/handlers/xdg_activation.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_xdg_activation; 2 | use smithay::input::Seat; 3 | use smithay::reexports::wayland_server::protocol::wl_surface; 4 | use smithay::wayland::xdg_activation::{self, XdgActivationHandler}; 5 | 6 | use crate::state::State; 7 | 8 | pub const ACTIVATION_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); 9 | 10 | impl XdgActivationHandler for State { 11 | fn activation_state(&mut self) -> &mut xdg_activation::XdgActivationState { 12 | &mut self.fht.xdg_activation_state 13 | } 14 | 15 | fn token_created( 16 | &mut self, 17 | _token: xdg_activation::XdgActivationToken, 18 | data: xdg_activation::XdgActivationTokenData, 19 | ) -> bool { 20 | if let Some((serial, seat)) = data.serial { 21 | Seat::from_resource(&seat).as_ref() == Some(&self.fht.seat) 22 | && self 23 | .fht 24 | .keyboard 25 | .last_enter() 26 | .map(|le| serial.is_no_older_than(&le)) 27 | .unwrap_or(false) 28 | } else { 29 | false 30 | } 31 | } 32 | 33 | fn request_activation( 34 | &mut self, 35 | _token: xdg_activation::XdgActivationToken, 36 | token_data: xdg_activation::XdgActivationTokenData, 37 | surface: wl_surface::WlSurface, 38 | ) { 39 | if token_data.timestamp.elapsed() < ACTIVATION_TIMEOUT { 40 | let mut output = None; 41 | if let Some((window, workspace)) = self.fht.space.find_window_and_workspace(&surface) { 42 | output = Some(workspace.output().clone()); 43 | self.fht.space.activate_window(&window, true); 44 | } 45 | 46 | if let Some(ref output) = output { 47 | self.fht.queue_redraw(output); 48 | } 49 | } 50 | } 51 | } 52 | 53 | delegate_xdg_activation!(State); 54 | -------------------------------------------------------------------------------- /src/handlers/xdg_decoration.rs: -------------------------------------------------------------------------------- 1 | use smithay::{ 2 | delegate_xdg_decoration, 3 | reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode, wayland::shell::xdg::{ToplevelSurface, decoration::XdgDecorationHandler}, 4 | }; 5 | 6 | use crate::state::State; 7 | 8 | // NOTE: Based on CONFIG.decoration.allow_csd, this will only (and forcefully) set either server 9 | // side deco or client side deco 10 | // This file only exists to adversite the protocol to clients that support it. The decoration mode 11 | // is set when mapping windows (see [`Fht::prepare_map_window`](../shell/mod.rs)) 12 | 13 | impl XdgDecorationHandler for State { 14 | fn new_decoration(&mut self, _toplevel: ToplevelSurface) {} 15 | 16 | fn request_mode(&mut self, _toplevel: ToplevelSurface, _mode: DecorationMode) {} 17 | 18 | fn unset_mode(&mut self, _toplevel: ToplevelSurface) {} 19 | } 20 | 21 | delegate_xdg_decoration!(State); 22 | -------------------------------------------------------------------------------- /src/handlers/xdg_dialog.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_xdg_dialog; 2 | use smithay::utils::Rectangle; 3 | use smithay::wayland::shell::xdg::dialog::XdgDialogHandler; 4 | use smithay::wayland::shell::xdg::ToplevelSurface; 5 | 6 | use crate::output::OutputExt; 7 | use crate::state::State; 8 | use crate::utils::RectCenterExt; 9 | 10 | impl XdgDialogHandler for State { 11 | fn modal_changed(&mut self, toplevel: ToplevelSurface, is_modal: bool) { 12 | let Some(workspace) = self 13 | .fht 14 | .space 15 | .workspace_mut_for_window_surface(toplevel.wl_surface()) 16 | else { 17 | warn!("Received modal_changed for unmapped toplevel"); 18 | return; 19 | }; 20 | let output_rect = Rectangle::from_size(workspace.output().geometry().size); 21 | 22 | if !is_modal { 23 | // I mean, we kinda don't care if its not. 24 | return; 25 | } 26 | 27 | let tile = workspace 28 | .tiles_mut() 29 | .find(|tile| *tile.window().toplevel() == toplevel) 30 | .unwrap(); 31 | tile.window().request_tiled(false); 32 | // Ask the toplevel to set its own size according to whatever it likes. 33 | // For modals/dialogs it should set whatever needed size. 34 | tile.window().reset_size(); 35 | tile.window().send_configure(); 36 | 37 | // Now center the tile. 38 | let tile_size = tile.size(); 39 | let loc = output_rect.center() - tile_size.to_f64().downscale(2.0).to_i32_round(); 40 | tile.set_location(loc, !self.fht.config.animations.disable); 41 | 42 | // Now re-arrange in case the modal window was tiled. 43 | workspace.arrange_tiles(!self.fht.config.animations.disable); 44 | } 45 | } 46 | 47 | delegate_xdg_dialog!(State); 48 | -------------------------------------------------------------------------------- /src/handlers/xdg_foreign.rs: -------------------------------------------------------------------------------- 1 | use smithay::delegate_xdg_foreign; 2 | use smithay::wayland::xdg_foreign::XdgForeignHandler; 3 | 4 | use crate::state::State; 5 | 6 | impl XdgForeignHandler for State { 7 | fn xdg_foreign_state(&mut self) -> &mut smithay::wayland::xdg_foreign::XdgForeignState { 8 | &mut self.fht.xdg_foreign_state 9 | } 10 | } 11 | 12 | delegate_xdg_foreign!(State); 13 | -------------------------------------------------------------------------------- /src/input/swap_tile_grab.rs: -------------------------------------------------------------------------------- 1 | use smithay::input::pointer::{ 2 | AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, GestureHoldBeginEvent, 3 | GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, 4 | GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData, 5 | MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent, 6 | }; 7 | use smithay::utils::{Logical, Point}; 8 | 9 | use crate::focus_target::PointerFocusTarget; 10 | use crate::state::State; 11 | use crate::window::Window; 12 | 13 | // NOTE: It is named swap-tile grab, but this name is quite misleading. 14 | // 15 | // In case the window is floating, the window will only be moving, the swapping 16 | // process is only between tiled windows. 17 | pub struct SwapTileGrab { 18 | pub window: Window, 19 | pub start_data: GrabStartData, 20 | } 21 | 22 | impl PointerGrab for SwapTileGrab { 23 | fn motion( 24 | &mut self, 25 | data: &mut State, 26 | handle: &mut PointerInnerHandle<'_, State>, 27 | _focus: Option<(PointerFocusTarget, Point)>, 28 | event: &MotionEvent, 29 | ) { 30 | // No focus while motion is active 31 | handle.motion(data, None, event); 32 | 33 | if data 34 | .fht 35 | .space 36 | .handle_interactive_swap_motion(&self.window, event.location.to_i32_round()) 37 | { 38 | return; 39 | } 40 | 41 | handle.unset_grab(self, data, event.serial, event.time, true) 42 | } 43 | 44 | fn relative_motion( 45 | &mut self, 46 | data: &mut State, 47 | handle: &mut PointerInnerHandle<'_, State>, 48 | focus: Option<(PointerFocusTarget, Point)>, 49 | event: &RelativeMotionEvent, 50 | ) { 51 | handle.relative_motion(data, focus, event) 52 | } 53 | 54 | fn button( 55 | &mut self, 56 | data: &mut State, 57 | handle: &mut PointerInnerHandle<'_, State>, 58 | event: &ButtonEvent, 59 | ) { 60 | handle.button(data, event); 61 | if handle.current_pressed().is_empty() { 62 | data.fht 63 | .space 64 | .handle_interactive_swap_end(&self.window, handle.current_location()); 65 | handle.unset_grab(self, data, event.serial, event.time, true); 66 | } 67 | } 68 | 69 | fn axis( 70 | &mut self, 71 | data: &mut State, 72 | handle: &mut PointerInnerHandle<'_, State>, 73 | details: AxisFrame, 74 | ) { 75 | handle.axis(data, details) 76 | } 77 | 78 | fn frame(&mut self, data: &mut State, handle: &mut PointerInnerHandle<'_, State>) { 79 | handle.frame(data) 80 | } 81 | 82 | fn gesture_swipe_begin( 83 | &mut self, 84 | data: &mut State, 85 | handle: &mut PointerInnerHandle<'_, State>, 86 | event: &GestureSwipeBeginEvent, 87 | ) { 88 | handle.gesture_swipe_begin(data, event) 89 | } 90 | 91 | fn gesture_swipe_update( 92 | &mut self, 93 | data: &mut State, 94 | handle: &mut PointerInnerHandle<'_, State>, 95 | event: &GestureSwipeUpdateEvent, 96 | ) { 97 | handle.gesture_swipe_update(data, event) 98 | } 99 | 100 | fn gesture_swipe_end( 101 | &mut self, 102 | data: &mut State, 103 | handle: &mut PointerInnerHandle<'_, State>, 104 | event: &GestureSwipeEndEvent, 105 | ) { 106 | handle.gesture_swipe_end(data, event) 107 | } 108 | 109 | fn gesture_pinch_begin( 110 | &mut self, 111 | data: &mut State, 112 | handle: &mut PointerInnerHandle<'_, State>, 113 | event: &GesturePinchBeginEvent, 114 | ) { 115 | handle.gesture_pinch_begin(data, event) 116 | } 117 | 118 | fn gesture_pinch_update( 119 | &mut self, 120 | data: &mut State, 121 | handle: &mut PointerInnerHandle<'_, State>, 122 | event: &GesturePinchUpdateEvent, 123 | ) { 124 | handle.gesture_pinch_update(data, event) 125 | } 126 | 127 | fn gesture_pinch_end( 128 | &mut self, 129 | data: &mut State, 130 | handle: &mut PointerInnerHandle<'_, State>, 131 | event: &GesturePinchEndEvent, 132 | ) { 133 | handle.gesture_pinch_end(data, event) 134 | } 135 | 136 | fn gesture_hold_begin( 137 | &mut self, 138 | data: &mut State, 139 | handle: &mut PointerInnerHandle<'_, State>, 140 | event: &GestureHoldBeginEvent, 141 | ) { 142 | handle.gesture_hold_begin(data, event); 143 | } 144 | 145 | fn gesture_hold_end( 146 | &mut self, 147 | data: &mut State, 148 | handle: &mut PointerInnerHandle<'_, State>, 149 | event: &GestureHoldEndEvent, 150 | ) { 151 | handle.gesture_hold_end(data, event); 152 | } 153 | 154 | fn start_data(&self) -> &GrabStartData { 155 | &self.start_data 156 | } 157 | 158 | fn unset(&mut self, state: &mut State) { 159 | state 160 | .fht 161 | .cursor_theme_manager 162 | .set_image_status(CursorImageStatus::Named(CursorIcon::Default)); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/portals/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use smithay::reexports::calloop::{self, LoopHandle}; 3 | 4 | use crate::state::State; 5 | 6 | mod shared; 7 | 8 | #[cfg(feature = "xdg-screencast-portal")] 9 | pub mod screencast; 10 | 11 | pub fn start( 12 | dbus_connection: &zbus::blocking::Connection, 13 | loop_handle: &LoopHandle<'static, State>, 14 | ) -> anyhow::Result<()> { 15 | #[cfg(feature = "xdg-screencast-portal")] 16 | { 17 | info!("Starting XDG screencast portal"); 18 | let (to_compositor, from_screencast) = calloop::channel::channel::(); 19 | let portal = screencast::Portal { to_compositor }; 20 | loop_handle 21 | .insert_source(from_screencast, move |event, _, state| { 22 | let calloop::channel::Event::Msg(req) = event else { 23 | return; 24 | }; 25 | state.handle_screencast_request(req); 26 | }) 27 | .map_err(|err| { 28 | anyhow::anyhow!("Failed to insert XDG screencast portal source! {err}") 29 | })?; 30 | assert!(dbus_connection 31 | .object_server() 32 | .at("/org/freedesktop/portal/desktop", portal) 33 | .context("Failed to insert XDG screencast portal in dbus!")?); 34 | } 35 | 36 | Ok(()) 37 | } 38 | -------------------------------------------------------------------------------- /src/portals/shared.rs: -------------------------------------------------------------------------------- 1 | //! Shared logic for XDG desktop portal. 2 | //! 3 | //! Using an XDG desktop portal revolves around creating a [`Session`]. The client can then either 4 | //! call [`Session::close`] to end the portal session OR receive the `Session::closed` signal. 5 | //! 6 | //! When the client needs something, the application calls a portal request, receives back an 7 | //! object path to a [`Request`] object, and when the portal backend is done, it sends out a 8 | //! `Request::response` signal containing the data. 9 | 10 | use std::collections::HashMap; 11 | use std::sync::{Arc, Mutex}; 12 | 13 | use zbus::object_server::SignalEmitter; 14 | 15 | /// A long-lived XDG portal session. 16 | /// 17 | /// You can optionally associate some portal-specific data with this session. Useful to keep for 18 | /// example communication channels to the compositor or portal state. 19 | pub struct Session { 20 | data: Arc>, 21 | on_destroy: Option>, 22 | handle: zvariant::OwnedObjectPath, 23 | } 24 | 25 | impl Session { 26 | /// Create a new XDG desktop portal session. 27 | pub fn new(handle: P, data: T, on_destroy: Option) -> Self 28 | where 29 | P: Into, 30 | F: FnOnce(&T) + Send + Sync + 'static, 31 | { 32 | Self { 33 | data: Arc::new(Mutex::new(data)), 34 | handle: handle.into(), 35 | on_destroy: on_destroy.map(|cb| Box::new(cb) as Box<_>), 36 | } 37 | } 38 | 39 | pub fn with_data(&self, cb: F) -> R 40 | where 41 | F: FnOnce(&mut T) -> R, 42 | { 43 | let mut data = self.data.lock().unwrap(); 44 | cb(&mut *data) 45 | } 46 | } 47 | 48 | #[zbus::interface(name = "org.freedesktop.impl.portal.Session")] 49 | impl Session { 50 | /// Closes the portal session to which this object refers and ends all related user interaction 51 | /// (dialogs, etc). 52 | pub async fn close(&mut self, #[zbus(object_server)] object_server: &zbus::ObjectServer) { 53 | match object_server.remove::(&self.handle).await { 54 | Ok(true) => (), 55 | Ok(false) => warn!(handle = ?self.handle, "Could not destroy portal session"), 56 | Err(err) => error!(?err, "Failed to destroy portal session"), 57 | } 58 | 59 | if let Some(on_destroy) = self.on_destroy.take() { 60 | let data = self.data.lock().unwrap(); 61 | on_destroy(&*data); 62 | } 63 | } 64 | 65 | /// Emitted when a session is closed. 66 | /// 67 | /// The content of details is specified by the interface creating the session. 68 | #[zbus(signal)] 69 | pub async fn closed( 70 | &self, 71 | _emitter: &SignalEmitter<'_>, 72 | details: HashMap<&str, zvariant::Value<'_>>, 73 | ) -> zbus::Result<()>; 74 | } 75 | 76 | /// A single portal request. 77 | pub struct Request { 78 | handle: zvariant::OwnedObjectPath, 79 | } 80 | 81 | impl Request { 82 | /// Create a new XDG desktop portal request. 83 | pub fn new

(handle: P) -> Self 84 | where 85 | P: Into, 86 | { 87 | Self { 88 | handle: handle.into(), 89 | } 90 | } 91 | } 92 | 93 | #[zbus::interface(name = "org.freedesktop.impl.portal.Request")] 94 | impl Request { 95 | /// Closes the portal request to which this object refers and ends all related user interaction 96 | /// (dialogs, etc). 97 | pub async fn close(&mut self, #[zbus(object_server)] object_server: &zbus::ObjectServer) { 98 | match object_server.remove::(&self.handle).await { 99 | Ok(true) => (), 100 | Ok(false) => warn!(handle = ?self.handle, "Could not destroy portal request"), 101 | Err(err) => error!(?err, "Failed to destroy portal request"), 102 | } 103 | } 104 | } 105 | 106 | /// A result from a portal request. 107 | #[derive( 108 | Debug, 109 | Clone, 110 | Copy, 111 | PartialEq, 112 | Eq, 113 | serde_repr::Deserialize_repr, 114 | serde_repr::Serialize_repr, 115 | zvariant::Type, 116 | )] 117 | #[repr(u32)] 118 | pub enum PortalResponse { 119 | Success, 120 | Cancelled, 121 | Error, 122 | } 123 | -------------------------------------------------------------------------------- /src/profiling.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_macros)] 2 | #![allow(unused_imports)] 3 | 4 | macro_rules! profile_function { 5 | () => { 6 | let _tracy_span = tracy_client::span!(); 7 | }; 8 | ($data:expr) => { 9 | let _location = $crate::tracy_client::span_location!(); 10 | let _tracy_span = $crate::tracy_client::Client::running() 11 | .expect("function_scope! without a running tracy_client::Client") 12 | .span(_location, 0); 13 | _tracy_span.emit_text($data); 14 | }; 15 | } 16 | pub(crate) use profile_function; 17 | 18 | /// Profiling macro for feature "profile-with-puffin" 19 | macro_rules! profile_scope { 20 | // Note: literal patterns provided as an optimization since they can skip an allocation. 21 | ($name:literal) => { 22 | // Note: callstack_depth is 0 since this has significant overhead 23 | let _tracy_span = ::tracy_client::span!($name, 0); 24 | }; 25 | ($name:literal, $data:expr) => { 26 | // Note: callstack_depth is 0 since this has significant overhead 27 | let _tracy_span = ::tracy_client::span!($name, 0); 28 | _tracy_span.emit_text($data); 29 | }; 30 | ($name:expr) => { 31 | let _function_name = { 32 | struct S; 33 | let type_name = core::any::type_name::(); 34 | &type_name[..type_name.len() - 3] 35 | }; 36 | let _tracy_span = ::tracy_client::Client::running() 37 | .expect("scope! without a running tracy_client::Client") 38 | // Note: callstack_depth is 0 since this has significant overhead 39 | .span_alloc(Some($name), _function_name, file!(), line!(), 0); 40 | }; 41 | ($name:expr, $data:expr) => { 42 | let _function_name = { 43 | struct S; 44 | let type_name = core::any::type_name::(); 45 | &type_name[..type_name.len() - 3] 46 | }; 47 | let _tracy_span = ::tracy_client::Client::running() 48 | .expect("scope! without a running tracy_client::Client") 49 | // Note: callstack_depth is 0 since this has significant overhead 50 | .span_alloc(Some($name), _function_name, file!(), line!(), 0); 51 | _tracy_span.emit_text($data); 52 | }; 53 | } 54 | pub(crate) use profile_scope; 55 | -------------------------------------------------------------------------------- /src/protocols/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod output_management; 2 | pub mod screencopy; 3 | -------------------------------------------------------------------------------- /src/renderer/data.rs: -------------------------------------------------------------------------------- 1 | use smithay::backend::renderer::gles::{ffi, Capability, GlesFrame, GlesRenderer}; 2 | 3 | /// Extra renderer data used for custom drawing with gles FFI. 4 | /// 5 | /// [`GlesRenderer`] creates these, but keeps them private, so we create our own. 6 | pub struct RendererData { 7 | pub vbos: [u32; 2], 8 | } 9 | 10 | impl RendererData { 11 | pub fn init(renderer: &mut GlesRenderer) { 12 | let capabilities = renderer.capabilities(); 13 | let vertices: &[ffi::types::GLfloat] = if capabilities.contains(&Capability::Instancing) { 14 | &INSTANCED_VERTS 15 | } else { 16 | &TRIANGLE_VERTS 17 | }; 18 | 19 | let this = renderer 20 | .with_context(|gl| unsafe { 21 | let mut vbos = [0; 2]; 22 | gl.GenBuffers(vbos.len() as i32, vbos.as_mut_ptr()); 23 | gl.BindBuffer(ffi::ARRAY_BUFFER, vbos[0]); 24 | gl.BufferData( 25 | ffi::ARRAY_BUFFER, 26 | std::mem::size_of_val(vertices) as isize, 27 | vertices.as_ptr() as *const _, 28 | ffi::STATIC_DRAW, 29 | ); 30 | gl.BindBuffer(ffi::ARRAY_BUFFER, vbos[1]); 31 | gl.BufferData( 32 | ffi::ARRAY_BUFFER, 33 | (std::mem::size_of::() * OUTPUT_VERTS.len()) as isize, 34 | OUTPUT_VERTS.as_ptr() as *const _, 35 | ffi::STATIC_DRAW, 36 | ); 37 | gl.BindBuffer(ffi::ARRAY_BUFFER, 0); 38 | 39 | Self { vbos } 40 | }) 41 | .unwrap(); 42 | 43 | renderer 44 | .egl_context() 45 | .user_data() 46 | .insert_if_missing(|| this); 47 | } 48 | 49 | pub fn get(renderer: &mut GlesRenderer) -> &Self { 50 | renderer.egl_context().user_data().get().unwrap() 51 | } 52 | 53 | pub fn get_from_frame<'a>(frame: &'a mut GlesFrame<'_, '_>) -> &'a Self { 54 | frame.egl_context().user_data().get().unwrap() 55 | } 56 | } 57 | 58 | /// Vertices for instanced rendering. 59 | static INSTANCED_VERTS: [ffi::types::GLfloat; 8] = [ 60 | 1.0, 0.0, // top right 61 | 0.0, 0.0, // top left 62 | 1.0, 1.0, // bottom right 63 | 0.0, 1.0, // bottom left 64 | ]; 65 | 66 | /// Vertices for rendering individual triangles. 67 | const MAX_RECTS_PER_DRAW: usize = 10; 68 | const TRIANGLE_VERTS: [ffi::types::GLfloat; 12 * MAX_RECTS_PER_DRAW] = triangle_verts(); 69 | const fn triangle_verts() -> [ffi::types::GLfloat; 12 * MAX_RECTS_PER_DRAW] { 70 | let mut verts = [0.; 12 * MAX_RECTS_PER_DRAW]; 71 | let mut i = 0; 72 | loop { 73 | // Top Left. 74 | verts[i * 12] = 0.0; 75 | verts[i * 12 + 1] = 0.0; 76 | 77 | // Bottom left. 78 | verts[i * 12 + 2] = 0.0; 79 | verts[i * 12 + 3] = 1.0; 80 | 81 | // Bottom right. 82 | verts[i * 12 + 4] = 1.0; 83 | verts[i * 12 + 5] = 1.0; 84 | 85 | // Top left. 86 | verts[i * 12 + 6] = 0.0; 87 | verts[i * 12 + 7] = 0.0; 88 | 89 | // Bottom right. 90 | verts[i * 12 + 8] = 1.0; 91 | verts[i * 12 + 9] = 1.0; 92 | 93 | // Top right. 94 | verts[i * 12 + 10] = 1.0; 95 | verts[i * 12 + 11] = 0.0; 96 | 97 | i += 1; 98 | if i == MAX_RECTS_PER_DRAW { 99 | break; 100 | } 101 | } 102 | verts 103 | } 104 | 105 | /// Vertices for output rendering. 106 | static OUTPUT_VERTS: [ffi::types::GLfloat; 8] = [ 107 | -1.0, 1.0, // top right 108 | -1.0, -1.0, // top left 109 | 1.0, 1.0, // bottom right 110 | 1.0, -1.0, // bottom left 111 | ]; 112 | -------------------------------------------------------------------------------- /src/renderer/extra_damage.rs: -------------------------------------------------------------------------------- 1 | // From https://github.com/Yalter/Niri licensed under GPL-v3.0 2 | // Thank you very much. 3 | use smithay::backend::renderer::element::{Element, Id, RenderElement}; 4 | use smithay::backend::renderer::utils::CommitCounter; 5 | use smithay::backend::renderer::Renderer; 6 | use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Size}; 7 | 8 | #[derive(Debug, Clone)] 9 | pub struct ExtraDamage { 10 | id: Id, 11 | commit: CommitCounter, 12 | geometry: Rectangle, 13 | } 14 | 15 | impl ExtraDamage { 16 | pub fn new(size: Size) -> Self { 17 | Self { 18 | id: Id::new(), 19 | commit: Default::default(), 20 | geometry: Rectangle::from_size(size), 21 | } 22 | } 23 | 24 | pub fn set_size(&mut self, size: Size) { 25 | if self.geometry.size == size { 26 | return; 27 | } 28 | 29 | self.geometry.size = size; 30 | self.commit.increment(); 31 | } 32 | 33 | pub fn with_location(mut self, location: Point) -> Self { 34 | self.geometry.loc = location; 35 | self 36 | } 37 | } 38 | 39 | impl Default for ExtraDamage { 40 | fn default() -> Self { 41 | Self::new(Size::default()) 42 | } 43 | } 44 | 45 | impl Element for ExtraDamage { 46 | fn id(&self) -> &Id { 47 | &self.id 48 | } 49 | 50 | fn current_commit(&self) -> CommitCounter { 51 | self.commit 52 | } 53 | 54 | fn src(&self) -> Rectangle { 55 | Rectangle::new((0., 0.).into(), (1., 1.).into()) 56 | } 57 | 58 | fn geometry(&self, scale: Scale) -> Rectangle { 59 | self.geometry.to_physical_precise_round(scale) 60 | } 61 | } 62 | 63 | impl RenderElement for ExtraDamage { 64 | fn draw( 65 | &self, 66 | _frame: &mut R::Frame<'_, '_>, 67 | _src: Rectangle, 68 | _dst: Rectangle, 69 | _damage: &[Rectangle], 70 | _or: &[Rectangle], 71 | ) -> Result<(), R::Error> { 72 | Ok(()) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/renderer/pixel_shader_element.rs: -------------------------------------------------------------------------------- 1 | use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement}; 2 | use smithay::backend::renderer::gles::element::PixelShaderElement; 3 | use smithay::backend::renderer::gles::{GlesError, GlesPixelProgram, Uniform}; 4 | use smithay::backend::renderer::glow::{GlowFrame, GlowRenderer}; 5 | use smithay::backend::renderer::utils::{CommitCounter, DamageSet}; 6 | use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Transform}; 7 | 8 | #[cfg(feature = "udev-backend")] 9 | use crate::backend::udev::{UdevFrame, UdevRenderError, UdevRenderer}; 10 | 11 | #[derive(Debug)] 12 | pub struct FhtPixelShaderElement(PixelShaderElement, Option>); 13 | 14 | impl FhtPixelShaderElement { 15 | /// Create a new [`FhtPixelShaderElement`]. 16 | /// 17 | /// See [`PixelShaderElement::new`] 18 | pub fn new( 19 | program: GlesPixelProgram, 20 | geometry: Rectangle, 21 | alpha: f32, 22 | additional_uniforms: Vec>, 23 | damage: Option<&[Rectangle]>, 24 | kind: Kind, 25 | ) -> Self { 26 | let inner = 27 | PixelShaderElement::new(program, geometry, None, alpha, additional_uniforms, kind); 28 | Self(inner, damage.map(DamageSet::from_slice)) 29 | } 30 | } 31 | 32 | impl Element for FhtPixelShaderElement { 33 | fn id(&self) -> &Id { 34 | self.0.id() 35 | } 36 | 37 | fn current_commit(&self) -> CommitCounter { 38 | self.0.current_commit() 39 | } 40 | 41 | fn src(&self) -> Rectangle { 42 | self.0.src() 43 | } 44 | 45 | fn geometry(&self, scale: Scale) -> Rectangle { 46 | self.0.geometry(scale) 47 | } 48 | 49 | fn location(&self, scale: Scale) -> Point { 50 | self.geometry(scale).loc 51 | } 52 | 53 | fn transform(&self) -> Transform { 54 | Transform::Normal 55 | } 56 | 57 | fn damage_since( 58 | &self, 59 | scale: Scale, 60 | commit: Option, 61 | ) -> smithay::backend::renderer::utils::DamageSet { 62 | match &self.1 { 63 | // If we have custom damage, use that. Otherwise pixel shader element uses full 64 | // area. FIXME: Maybe avoid the allocation, but this should live on the stack 65 | Some(damage) => DamageSet::from_iter( 66 | damage 67 | .iter() 68 | .copied() 69 | .map(|rect| rect.to_physical_precise_round(scale)), 70 | ), 71 | None => self.0.damage_since(scale, commit), 72 | } 73 | } 74 | 75 | fn opaque_regions( 76 | &self, 77 | scale: Scale, 78 | ) -> smithay::backend::renderer::utils::OpaqueRegions { 79 | self.0.opaque_regions(scale) 80 | } 81 | 82 | fn alpha(&self) -> f32 { 83 | self.0.alpha() 84 | } 85 | 86 | fn kind(&self) -> Kind { 87 | self.0.kind() 88 | } 89 | } 90 | 91 | impl RenderElement for FhtPixelShaderElement { 92 | fn draw( 93 | &self, 94 | frame: &mut GlowFrame<'_, '_>, 95 | src: Rectangle, 96 | dst: Rectangle, 97 | damage: &[Rectangle], 98 | opaque_regions: &[Rectangle], 99 | ) -> Result<(), GlesError> { 100 | >::draw( 101 | &self.0, 102 | frame, 103 | src, 104 | dst, 105 | damage, 106 | opaque_regions, 107 | ) 108 | } 109 | 110 | fn underlying_storage( 111 | &self, 112 | _: &mut GlowRenderer, 113 | ) -> Option { 114 | None // pixel shader elements can't be scanned out. 115 | } 116 | } 117 | 118 | #[cfg(feature = "udev-backend")] 119 | impl<'a> RenderElement> for FhtPixelShaderElement { 120 | fn draw( 121 | &self, 122 | frame: &mut UdevFrame<'a, '_, '_>, 123 | src: Rectangle, 124 | dst: Rectangle, 125 | damage: &[Rectangle], 126 | opaque_regions: &[Rectangle], 127 | ) -> Result<(), UdevRenderError> { 128 | >::draw( 129 | &self.0, 130 | frame.as_mut(), 131 | src, 132 | dst, 133 | damage, 134 | opaque_regions, 135 | ) 136 | .map_err(UdevRenderError::Render) 137 | } 138 | 139 | fn underlying_storage( 140 | &self, 141 | _: &mut UdevRenderer<'a>, 142 | ) -> Option { 143 | None // pixel shader elements can't be scanned out. 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/renderer/rounded_element/shader_.frag: -------------------------------------------------------------------------------- 1 | //_DEFINES_ 2 | 3 | #if defined(EXTERNAL) 4 | #extension GL_OES_EGL_image_external : require 5 | #endif 6 | 7 | precision mediump float; 8 | #if defined(EXTERNAL) 9 | uniform samplerExternalOES tex; 10 | #else 11 | uniform sampler2D tex; 12 | #endif 13 | 14 | uniform float alpha; 15 | varying vec2 v_coords; 16 | 17 | #if defined(DEBUG_FLAGS) 18 | uniform float tint; 19 | #endif 20 | 21 | uniform vec2 size; 22 | uniform float radius; 23 | 24 | float rounded_box(vec2 center, vec2 size, float radius) { 25 | return length(max(abs(center) - size + radius, 0.0)) - radius; 26 | } 27 | 28 | void main() { 29 | vec2 center = size / 2.0; 30 | vec2 location = v_coords * size; 31 | 32 | float distance = rounded_box(location - center, size / 2.0, radius); 33 | vec4 mix_color; 34 | if (distance > 1.0) { 35 | mix_color = vec4(0); 36 | } else { 37 | mix_color = texture2D(tex, v_coords); 38 | } 39 | 40 | #if defined(NO_ALPHA) 41 | mix_color = vec4(mix_color.rgb, 1.0) * alpha; 42 | #else 43 | mix_color = mix_color * alpha; 44 | #endif 45 | 46 | #if defined(DEBUG_FLAGS) 47 | if (tint == 1.0) 48 | mix_color = vec4(0.0, 0.3, 0.0, 0.2) + mix_color * 0.8; 49 | #endif 50 | 51 | gl_FragColor = mix_color; 52 | } 53 | 54 | // vim: ft=glsl 55 | -------------------------------------------------------------------------------- /src/renderer/shaders/blur-down.frag: -------------------------------------------------------------------------------- 1 | #version 100 2 | 3 | //_DEFINES_ 4 | 5 | #if defined(EXTERNAL) 6 | #extension GL_OES_EGL_image_external : require 7 | #endif 8 | 9 | precision mediump float; 10 | #if defined(EXTERNAL) 11 | uniform samplerExternalOES tex; 12 | #else 13 | uniform sampler2D tex; 14 | #endif 15 | 16 | varying vec2 v_coords; 17 | uniform float radius; 18 | uniform vec2 half_pixel; 19 | 20 | void main() { 21 | vec2 uv = v_coords * 2.0; 22 | 23 | vec4 sum = texture2D(tex, uv) * 4.0; 24 | sum += texture2D(tex, uv - half_pixel.xy * radius); 25 | sum += texture2D(tex, uv + half_pixel.xy * radius); 26 | sum += texture2D(tex, uv + vec2(half_pixel.x, -half_pixel.y) * radius); 27 | sum += texture2D(tex, uv - vec2(half_pixel.x, -half_pixel.y) * radius); 28 | 29 | gl_FragColor = sum / 8.0; 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/renderer/shaders/blur-finish.frag: -------------------------------------------------------------------------------- 1 | // Implementation from pinnacle-comp/pinnacle (GPL-3.0) 2 | // Thank you very much! 3 | #version 100 4 | 5 | //_DEFINES_ 6 | 7 | #if defined(EXTERNAL) 8 | #extension GL_OES_EGL_image_external : require 9 | #endif 10 | 11 | precision mediump float; 12 | #if defined(EXTERNAL) 13 | uniform samplerExternalOES tex; 14 | #else 15 | uniform sampler2D tex; 16 | #endif 17 | 18 | uniform float alpha; 19 | varying vec2 v_coords; 20 | 21 | #if defined(DEBUG_FLAGS) 22 | uniform float tint; 23 | #endif 24 | 25 | uniform vec4 geo; 26 | uniform float corner_radius; 27 | uniform float noise; 28 | 29 | float rounding_alpha(vec2 coords, vec2 size, float radius) { 30 | vec2 center; 31 | 32 | if (coords.x < corner_radius && coords.y < corner_radius) { 33 | center = vec2(radius); 34 | } else if (size.x - corner_radius < coords.x && coords.y < corner_radius) { 35 | center = vec2(size.x - radius, radius); 36 | } else if (size.x - corner_radius < coords.x && size.y - corner_radius < coords.y) { 37 | center = size - vec2(radius); 38 | } else if (coords.x < corner_radius && size.y - corner_radius < coords.y) { 39 | center = vec2(radius, size.y - radius); 40 | } else { 41 | return 1.0; 42 | } 43 | 44 | float dist = distance(coords, center); 45 | return 1.0 - smoothstep(radius - 0.5, radius + 0.5, dist); 46 | } 47 | 48 | // Noise function copied from hyprland. 49 | // I like the effect it gave, can be tweaked further 50 | float hash(vec2 p) { 51 | vec3 p3 = fract(vec3(p.xyx) * 727.727); // wysi :wink: :wink: 52 | p3 += dot(p3, p3.xyz + 33.33); 53 | return fract((p3.x + p3.y) * p3.z); 54 | } 55 | 56 | void main() { 57 | 58 | // Sample the texture. 59 | vec4 color = texture2D(tex, v_coords); 60 | 61 | #if defined(NO_ALPHA) 62 | color = vec4(color.rgb, 1.0); 63 | #endif 64 | 65 | // This shader exists to make blur rounding correct. 66 | // 67 | // Since we are scr-ing a texture that is the size of the output, the v_coords are always 68 | // relative to the output. This corresponds to gl_FragCoord. 69 | vec2 size = geo.zw; 70 | vec2 loc = gl_FragCoord.xy - geo.xy; 71 | 72 | // Add noise fx 73 | // This can be used to achieve a glass look 74 | float noiseHash = hash(loc / size); 75 | float noiseAmount = (mod(noiseHash, 1.0) - 0.5); 76 | color.rgb += noiseAmount * noise; 77 | 78 | // Apply corner rounding inside geometry. 79 | color *= rounding_alpha(loc, size, corner_radius); 80 | 81 | 82 | // Apply final alpha and tint. 83 | color *= alpha; 84 | #if defined(DEBUG_FLAGS) 85 | if (tint == 1.0) 86 | color = vec4(0.0, 0.2, 0.0, 0.2) + color * 0.8; 87 | #endif 88 | 89 | gl_FragColor = color; 90 | } 91 | 92 | // vim: ft=glsl 93 | -------------------------------------------------------------------------------- /src/renderer/shaders/blur-up.frag: -------------------------------------------------------------------------------- 1 | #version 100 2 | 3 | //_DEFINES_ 4 | 5 | #if defined(EXTERNAL) 6 | #extension GL_OES_EGL_image_external : require 7 | #endif 8 | 9 | precision mediump float; 10 | #if defined(EXTERNAL) 11 | uniform samplerExternalOES tex; 12 | #else 13 | uniform sampler2D tex; 14 | #endif 15 | 16 | varying vec2 v_coords; 17 | uniform vec2 half_pixel; 18 | uniform float radius; 19 | 20 | void main() { 21 | vec2 uv = v_coords / 2.0; 22 | 23 | vec4 sum = texture2D(tex, uv + vec2(-half_pixel.x * 2.0, 0.0) * radius); 24 | sum += texture2D(tex, uv + vec2(-half_pixel.x, half_pixel.y) * radius) * 2.0; 25 | sum += texture2D(tex, uv + vec2(0.0, half_pixel.y * 2.0) * radius); 26 | sum += texture2D(tex, uv + vec2(half_pixel.x, half_pixel.y) * radius) * 2.0; 27 | sum += texture2D(tex, uv + vec2(half_pixel.x * 2.0, 0.0) * radius); 28 | sum += texture2D(tex, uv + vec2(half_pixel.x, -half_pixel.y) * radius) * 2.0; 29 | sum += texture2D(tex, uv + vec2(0.0, -half_pixel.y * 2.0) * radius); 30 | sum += texture2D(tex, uv + vec2(-half_pixel.x, -half_pixel.y) * radius) * 2.0; 31 | 32 | gl_FragColor = sum / 12.0; 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/renderer/shaders/border.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform vec4 v_start_color; 4 | uniform vec4 v_end_color; 5 | uniform float v_gradient_angle; 6 | uniform float corner_radius; 7 | uniform float thickness; 8 | 9 | uniform vec2 size; 10 | uniform float alpha; 11 | varying vec2 v_coords; 12 | 13 | float rounding_alpha(vec2 coords, vec2 size, float radius) 14 | { 15 | vec2 center; 16 | 17 | if (coords.x < corner_radius && coords.y < corner_radius) 18 | { 19 | center = vec2(radius); 20 | } 21 | else if (size.x - corner_radius < coords.x && coords.y < corner_radius) 22 | { 23 | center = vec2(size.x - radius, radius); 24 | } 25 | else if (size.x - corner_radius < coords.x && size.y - corner_radius < coords.y) 26 | { 27 | center = size - vec2(radius); 28 | } 29 | else if (coords.x < corner_radius && size.y - corner_radius < coords.y) 30 | { 31 | center = vec2(radius, size.y - radius); 32 | } 33 | else 34 | { 35 | return 1.0; 36 | } 37 | 38 | float dist = distance(coords, center); 39 | return 1.0 - smoothstep(radius - 0.5, radius + 0.5, dist); 40 | } 41 | 42 | // Gradient color calculation from here 43 | // https://www.shadertoy.com/view/Mt2XDK 44 | vec4 get_pixel_color() 45 | { 46 | vec2 origin = vec2(0.5); 47 | vec2 uv = v_coords - origin; 48 | 49 | float angle = radians(90.0) - radians(v_gradient_angle) + atan(uv.x, uv.y); 50 | 51 | float uv_len = length(uv); 52 | uv = vec2(cos(angle) * uv_len, sin(angle) * uv_len) + origin; 53 | 54 | return mix(v_start_color, v_end_color, smoothstep(0.0, 1.0, uv.x)); 55 | } 56 | 57 | void main() 58 | { 59 | vec2 loc = v_coords * size; 60 | // First rounding pass is for outside radius 61 | vec4 color = get_pixel_color(); 62 | color *= rounding_alpha(loc, size, corner_radius); 63 | 64 | if (thickness > 0.0) 65 | { 66 | // Second pass: inner rounding 67 | loc -= vec2(thickness); 68 | vec2 inner_size = size - vec2(thickness * 2.0); 69 | 70 | // Only apply rounding when we are inside 71 | if (0.0 <= loc.x && loc.x <= inner_size.x && 0.0 <= loc.y && loc.y <= inner_size.y) 72 | { 73 | float inner_radius = max(corner_radius - thickness, 0.0); 74 | color = color * (1.0 - rounding_alpha(loc, inner_size, inner_radius)); 75 | } 76 | } 77 | 78 | gl_FragColor = color * alpha; 79 | } 80 | 81 | // vim: ft=glsl 82 | -------------------------------------------------------------------------------- /src/renderer/shaders/box-shadow.frag: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | uniform vec4 shadow_color; 4 | uniform float blur_sigma; 5 | uniform float corner_radius; 6 | 7 | uniform vec2 size; 8 | uniform float alpha; 9 | varying vec2 v_coords; 10 | 11 | // The shader code is from here 12 | // https://madebyevan.com/shaders/fast-rounded-rectangle-shadows/ 13 | 14 | // A standard gaussian function, used for weighting samples 15 | float gaussian(float x, float sigma) 16 | { 17 | const float pi = 3.141592653589793; 18 | return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * pi) * sigma); 19 | } 20 | 21 | // This approximates the error function, needed for the gaussian integral 22 | vec2 erf(vec2 x) 23 | { 24 | vec2 s = sign(x), a = abs(x); 25 | x = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a; 26 | x *= x; 27 | return s - s / (x * x); 28 | } 29 | 30 | // Return the blurred mask along the x dimension 31 | float rounded_box_shadow_x(float x, float y, float sigma, float corner, vec2 halfSize) 32 | { 33 | float delta = min(halfSize.y - corner - abs(y), 0.0); 34 | float curved = halfSize.x - corner + sqrt(max(0.0, corner * corner - delta * delta)); 35 | vec2 integral = 0.5 + 0.5 * erf((x + vec2(-curved, curved)) * (sqrt(0.5) / sigma)); 36 | return integral.y - integral.x; 37 | } 38 | 39 | // Return the mask for the shadow of a box from lower to upper 40 | float rounded_box_shadow(vec2 lower, vec2 upper, vec2 point, float sigma, float corner) 41 | { 42 | // Center everything to make the math easier 43 | vec2 center = (lower + upper) * 0.5; 44 | vec2 halfSize = (upper - lower) * 0.5; 45 | point -= center; 46 | 47 | // The signal is only non-zero in a limited range, so don't waste samples 48 | float low = point.y - halfSize.y; 49 | float high = point.y + halfSize.y; 50 | float start = clamp(-3.0 * sigma, low, high); 51 | float end = clamp(3.0 * sigma, low, high); 52 | 53 | // Accumulate samples (we can get away with surprisingly few samples) 54 | float step = (end - start) / 4.0; 55 | float y = start + step * 0.5; 56 | float value = 0.0; 57 | for (int i = 0; i < 4; i++) 58 | { 59 | value += rounded_box_shadow_x(point.x, point.y - y, sigma, corner, halfSize) * gaussian(y, sigma) * step; 60 | y += step; 61 | } 62 | 63 | return value; 64 | } 65 | 66 | // per-pixel "random" number between 0 and 1 67 | float random() 68 | { 69 | return fract(sin(dot(vec2(12.9898, 78.233), gl_FragCoord.xy)) * 43758.5453); 70 | } 71 | 72 | // simple rounded box sdf to check that we are inside 73 | // https://iquilezles.org/articles/distfunctions2d/ 74 | float rounded_box_sdf(vec2 pos, vec4 rect, float corner_radius) 75 | { 76 | vec2 half_size = (rect.zw) * 0.5; 77 | vec2 q = abs(pos - rect.xy - half_size) - half_size + corner_radius; 78 | return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - corner_radius; 79 | } 80 | 81 | void main() 82 | { 83 | // the shader's element size will always fit the blur sigma / 2 84 | vec4 rect = vec4(vec2(blur_sigma), size.x - (2. * blur_sigma), size.y - (2. * blur_sigma)); 85 | vec2 pos = v_coords * size; 86 | if (rounded_box_sdf(pos, rect, corner_radius) < 0.0) 87 | discard; // we dont draw the shadow *inside* the rectangle 88 | 89 | // First rounding pass is for outside radius 90 | float frag_alpha = shadow_color.a; 91 | frag_alpha *= rounded_box_shadow(rect.xy, rect.xy + rect.zw, v_coords * size, blur_sigma / 2., corner_radius); 92 | frag_alpha += (random() - 0.5) / 128.0; 93 | 94 | gl_FragColor = vec4(shadow_color.xyz * frag_alpha, frag_alpha) * alpha; 95 | } 96 | 97 | // vim: ft=glsl 98 | -------------------------------------------------------------------------------- /src/renderer/shaders/mod.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::BorrowMut; 2 | 3 | use smithay::backend::renderer::gles::{ 4 | GlesFrame, GlesPixelProgram, GlesRenderer, GlesTexProgram, UniformName, UniformType, 5 | }; 6 | use smithay::backend::renderer::glow::GlowRenderer; 7 | 8 | use super::blur::shader::BlurShaders; 9 | 10 | const BORDER_SRC: &str = include_str!("./border.frag"); 11 | const BOX_SHADOW_SRC: &str = include_str!("./box-shadow.frag"); 12 | const ROUNDED_WINDOW_SRC: &str = include_str!("./rounded-window.frag"); 13 | const BLUR_FINISH_SRC: &str = include_str!("./blur-finish.frag"); 14 | const RESIZING_TEXTURE_SRC: &str = include_str!("./resizing-texture.frag"); 15 | pub(super) const BLUR_DOWN_SRC: &str = include_str!("./blur-down.frag"); 16 | pub(super) const BLUR_UP_SRC: &str = include_str!("./blur-up.frag"); 17 | pub(super) const VERTEX_SRC: &str = include_str!("./texture.vert"); 18 | 19 | pub struct Shaders { 20 | pub border: GlesPixelProgram, 21 | pub box_shadow: GlesPixelProgram, 22 | // rounded_window => complex shader that takes into account subsurface position through 23 | // matrices, only used in src/space/tile.rs 24 | pub rounded_window: GlesTexProgram, 25 | // blur_finish => apply rounded corners and additional effects 26 | pub blur_finish: GlesTexProgram, 27 | pub resizing_texture: GlesTexProgram, 28 | pub blur: BlurShaders, 29 | } 30 | 31 | impl Shaders { 32 | pub fn init(renderer: &mut GlowRenderer) { 33 | let renderer: &mut GlesRenderer = renderer.borrow_mut(); 34 | 35 | let rounded_window = renderer 36 | .compile_custom_texture_shader( 37 | ROUNDED_WINDOW_SRC, 38 | &[ 39 | UniformName::new("corner_radius", UniformType::_1f), 40 | UniformName::new("geo_size", UniformType::_2f), 41 | UniformName::new("input_to_geo", UniformType::Matrix3x3), 42 | ], 43 | ) 44 | .expect("Shader source should always compile!"); 45 | let blur_finish = renderer 46 | .compile_custom_texture_shader( 47 | BLUR_FINISH_SRC, 48 | &[ 49 | UniformName::new("corner_radius", UniformType::_1f), 50 | UniformName::new("noise", UniformType::_1f), 51 | UniformName::new("geo", UniformType::_4f), 52 | ], 53 | ) 54 | .expect("Shader source should always compile!"); 55 | 56 | let resizing_texture = renderer 57 | .compile_custom_texture_shader( 58 | RESIZING_TEXTURE_SRC, 59 | &[ 60 | UniformName::new("corner_radius", UniformType::_1f), 61 | // the size of the window texture we sampled from 62 | UniformName::new("win_size", UniformType::_2f), 63 | UniformName::new("curr_size", UniformType::_2f), 64 | ], 65 | ) 66 | .expect("Shader source should always compile!"); 67 | let border = renderer 68 | .compile_custom_pixel_shader( 69 | BORDER_SRC, 70 | &[ 71 | UniformName::new("v_start_color", UniformType::_4f), 72 | UniformName::new("v_end_color", UniformType::_4f), 73 | UniformName::new("v_gradient_angle", UniformType::_1f), 74 | UniformName::new("corner_radius", UniformType::_1f), 75 | UniformName::new("thickness", UniformType::_1f), 76 | ], 77 | ) 78 | .expect("Shader source should always compile!"); 79 | let box_shadow = renderer 80 | .compile_custom_pixel_shader( 81 | BOX_SHADOW_SRC, 82 | &[ 83 | UniformName::new("shadow_color", UniformType::_4f), 84 | UniformName::new("corner_radius", UniformType::_1f), 85 | UniformName::new("blur_sigma", UniformType::_1f), 86 | ], 87 | ) 88 | .expect("Shader source should always compile!"); 89 | let blur = BlurShaders::compile(renderer).expect("Shader source should always compile!"); 90 | 91 | let shaders = Self { 92 | border, 93 | box_shadow, 94 | rounded_window, 95 | blur_finish, 96 | resizing_texture, 97 | blur, 98 | }; 99 | 100 | renderer 101 | .egl_context() 102 | .user_data() 103 | .insert_if_missing(|| shaders); 104 | } 105 | 106 | pub fn get(renderer: &GlowRenderer) -> &Self { 107 | renderer 108 | .egl_context() 109 | .user_data() 110 | .get() 111 | .expect("Shaders are initialized at startup!") 112 | } 113 | 114 | pub fn get_from_frame<'a>(frame: &'a GlesFrame<'_, '_>) -> &'a Self { 115 | frame 116 | .egl_context() 117 | .user_data() 118 | .get() 119 | .expect("Shaders are initialized at startup!") 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/renderer/shaders/resizing-texture.frag: -------------------------------------------------------------------------------- 1 | #version 100 2 | 3 | //_DEFINES_ 4 | 5 | #if defined(EXTERNAL) 6 | #extension GL_OES_EGL_image_external : require 7 | #endif 8 | 9 | precision mediump float; 10 | #if defined(EXTERNAL) 11 | uniform samplerExternalOES tex; 12 | #else 13 | uniform sampler2D tex; 14 | #endif 15 | 16 | uniform float alpha; 17 | 18 | // the size of the window texture 19 | uniform vec2 win_size; 20 | // the size we should display with 21 | uniform vec2 curr_size; 22 | // sample coords inside curr_size 23 | varying vec2 v_coords; 24 | // The corner radius of the tile. 25 | uniform float corner_radius; 26 | 27 | #if defined(DEBUG_FLAGS) 28 | uniform float tint; 29 | #endif 30 | 31 | float rounding_alpha(vec2 coords, vec2 size, float radius) 32 | { 33 | vec2 center; 34 | 35 | if (coords.x < corner_radius && coords.y < corner_radius) 36 | { 37 | center = vec2(radius); 38 | } 39 | else if (size.x - corner_radius < coords.x && coords.y < corner_radius) 40 | { 41 | center = vec2(size.x - radius, radius); 42 | } 43 | else if (size.x - corner_radius < coords.x && size.y - corner_radius < coords.y) 44 | { 45 | center = size - vec2(radius); 46 | } 47 | else if (coords.x < corner_radius && size.y - corner_radius < coords.y) 48 | { 49 | center = vec2(radius, size.y - radius); 50 | } 51 | else 52 | { 53 | return 1.0; 54 | } 55 | 56 | float dist = distance(coords, center); 57 | return 1.0 - smoothstep(radius - 0.5, radius + 0.5, dist); 58 | } 59 | 60 | void main() 61 | { 62 | vec2 tex_coords = (v_coords * win_size) / curr_size; 63 | if (win_size.x > curr_size.x) 64 | tex_coords.x = v_coords.x; 65 | if (win_size.y > curr_size.y) 66 | tex_coords.y = v_coords.y; 67 | vec4 color = texture2D(tex, tex_coords); 68 | 69 | if (corner_radius > 0.0) 70 | color *= rounding_alpha(v_coords * curr_size, curr_size, corner_radius); 71 | 72 | #if defined(NO_ALPHA) 73 | color = vec4(color.rgb, 1.0); 74 | #endif 75 | 76 | #if defined(DEBUG_FLAGS) 77 | if (tint == 1.0) 78 | color = vec4(0.0, 0.2, 0.0, 0.2) + color * 0.8; 79 | #endif 80 | 81 | gl_FragColor = color; 82 | } 83 | 84 | // vim: ft=glsl 85 | -------------------------------------------------------------------------------- /src/renderer/shaders/rounded-window.frag: -------------------------------------------------------------------------------- 1 | // Implementation from pinnacle-comp/pinnacle (GPL-3.0) 2 | // Thank you very much! 3 | #version 100 4 | 5 | //_DEFINES_ 6 | 7 | #if defined(EXTERNAL) 8 | #extension GL_OES_EGL_image_external : require 9 | #endif 10 | 11 | precision mediump float; 12 | #if defined(EXTERNAL) 13 | uniform samplerExternalOES tex; 14 | #else 15 | uniform sampler2D tex; 16 | #endif 17 | 18 | uniform float alpha; 19 | varying vec2 v_coords; 20 | 21 | #if defined(DEBUG_FLAGS) 22 | uniform float tint; 23 | #endif 24 | 25 | uniform vec2 geo_size; 26 | uniform float corner_radius; 27 | uniform mat3 input_to_geo; 28 | 29 | float rounding_alpha(vec2 coords, vec2 size, float radius) { 30 | vec2 center; 31 | 32 | if (coords.x < corner_radius && coords.y < corner_radius) { 33 | center = vec2(radius); 34 | } else if (size.x - corner_radius < coords.x && coords.y < corner_radius) { 35 | center = vec2(size.x - radius, radius); 36 | } else if (size.x - corner_radius < coords.x && size.y - corner_radius < coords.y) { 37 | center = size - vec2(radius); 38 | } else if (coords.x < corner_radius && size.y - corner_radius < coords.y) { 39 | center = vec2(radius, size.y - radius); 40 | } else { 41 | return 1.0; 42 | } 43 | 44 | float dist = distance(coords, center); 45 | return 1.0 - smoothstep(radius - 0.5, radius + 0.5, dist); 46 | } 47 | 48 | void main() { 49 | vec3 coords_geo = input_to_geo * vec3(v_coords, 1.0); 50 | 51 | // Sample the texture. 52 | vec4 color = texture2D(tex, v_coords); 53 | #if defined(NO_ALPHA) 54 | color = vec4(color.rgb, 1.0); 55 | #endif 56 | 57 | if (coords_geo.x < 0.0 || 1.0 < coords_geo.x || coords_geo.y < 0.0 || 1.0 < coords_geo.y) { 58 | // Clip outside geometry. 59 | color = vec4(0.0); 60 | } else { 61 | // Apply corner rounding inside geometry. 62 | color = color * rounding_alpha(coords_geo.xy * geo_size, geo_size, corner_radius); 63 | } 64 | 65 | // Apply final alpha and tint. 66 | color = color * alpha; 67 | 68 | #if defined(DEBUG_FLAGS) 69 | if (tint == 1.0) 70 | color = vec4(0.0, 0.2, 0.0, 0.2) + color * 0.8; 71 | #endif 72 | 73 | gl_FragColor = color; 74 | } 75 | 76 | // vim: ft=glsl 77 | -------------------------------------------------------------------------------- /src/renderer/shaders/texture.vert: -------------------------------------------------------------------------------- 1 | 2 | #version 100 3 | 4 | uniform mat3 matrix; 5 | uniform mat3 tex_matrix; 6 | 7 | attribute vec2 vert; 8 | attribute vec4 vert_position; 9 | 10 | varying vec2 v_coords; 11 | 12 | mat2 scale(vec2 scale_vec){ 13 | return mat2( 14 | scale_vec.x, 0.0, 15 | 0.0, scale_vec.y 16 | ); 17 | } 18 | 19 | void main() { 20 | vec2 vert_transform_translation = vert_position.xy; 21 | vec2 vert_transform_scale = vert_position.zw; 22 | vec3 position = vec3(vert * scale(vert_transform_scale) + vert_transform_translation, 1.0); 23 | v_coords = (tex_matrix * position).xy; 24 | gl_Position = vec4(matrix * position, 1.0); 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/texture_element.rs: -------------------------------------------------------------------------------- 1 | use smithay::backend::renderer::element::texture::TextureRenderElement; 2 | use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement}; 3 | use smithay::backend::renderer::gles::{GlesError, GlesTexture}; 4 | use smithay::backend::renderer::glow::{GlowFrame, GlowRenderer}; 5 | #[cfg(feature = "udev-backend")] 6 | use smithay::backend::renderer::multigpu::MultiTexture; 7 | use smithay::backend::renderer::utils::CommitCounter; 8 | use smithay::backend::renderer::Texture; 9 | use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Transform}; 10 | 11 | #[cfg(feature = "udev-backend")] 12 | use crate::backend::udev::{UdevFrame, UdevRenderError, UdevRenderer}; 13 | 14 | #[derive(Debug)] 15 | pub struct FhtTextureElement(pub TextureRenderElement) 16 | where 17 | E: Texture + Clone + 'static; 18 | 19 | impl From> for FhtTextureElement { 20 | fn from(value: TextureRenderElement) -> Self { 21 | Self(value) 22 | } 23 | } 24 | 25 | impl Element for FhtTextureElement { 26 | fn id(&self) -> &Id { 27 | self.0.id() 28 | } 29 | 30 | fn current_commit(&self) -> CommitCounter { 31 | self.0.current_commit() 32 | } 33 | 34 | fn src(&self) -> Rectangle { 35 | self.0.src() 36 | } 37 | 38 | fn geometry(&self, scale: Scale) -> Rectangle { 39 | self.0.geometry(scale) 40 | } 41 | 42 | fn location(&self, scale: Scale) -> Point { 43 | self.geometry(scale).loc 44 | } 45 | 46 | fn transform(&self) -> Transform { 47 | Transform::Normal 48 | } 49 | 50 | fn damage_since( 51 | &self, 52 | scale: Scale, 53 | commit: Option, 54 | ) -> smithay::backend::renderer::utils::DamageSet { 55 | self.0.damage_since(scale, commit) 56 | } 57 | 58 | fn alpha(&self) -> f32 { 59 | self.0.alpha() 60 | } 61 | 62 | fn kind(&self) -> Kind { 63 | self.0.kind() 64 | } 65 | } 66 | 67 | impl RenderElement for FhtTextureElement { 68 | fn draw( 69 | &self, 70 | frame: &mut GlowFrame<'_, '_>, 71 | src: Rectangle, 72 | dst: Rectangle, 73 | damage: &[Rectangle], 74 | opaque_regions: &[Rectangle], 75 | ) -> Result<(), GlesError> { 76 | as RenderElement>::draw( 77 | &self.0, 78 | frame, 79 | src, 80 | dst, 81 | damage, 82 | opaque_regions, 83 | ) 84 | } 85 | 86 | fn underlying_storage( 87 | &self, 88 | renderer: &mut GlowRenderer, 89 | ) -> Option { 90 | self.0.underlying_storage(renderer) 91 | } 92 | } 93 | 94 | #[cfg(feature = "udev-backend")] 95 | impl<'a> RenderElement> for FhtTextureElement { 96 | fn draw( 97 | &self, 98 | frame: &mut UdevFrame<'a, '_, '_>, 99 | src: Rectangle, 100 | dst: Rectangle, 101 | damage: &[Rectangle], 102 | opaque_regions: &[Rectangle], 103 | ) -> Result<(), UdevRenderError> { 104 | as RenderElement>>::draw( 105 | &self.0, 106 | frame, 107 | src, 108 | dst, 109 | damage, 110 | opaque_regions, 111 | ) 112 | } 113 | 114 | fn underlying_storage( 115 | &self, 116 | _: &mut UdevRenderer<'a>, 117 | ) -> Option { 118 | None // pixel shader elements can't be scanned out. 119 | } 120 | } 121 | 122 | #[cfg(feature = "udev-backend")] 123 | impl<'a> RenderElement> for FhtTextureElement { 124 | fn draw( 125 | &self, 126 | frame: &mut UdevFrame<'a, '_, '_>, 127 | src: Rectangle, 128 | dst: Rectangle, 129 | damage: &[Rectangle], 130 | opaque_regions: &[Rectangle], 131 | ) -> Result<(), UdevRenderError> { 132 | as RenderElement>::draw( 133 | &self.0, 134 | frame.as_mut(), 135 | src, 136 | dst, 137 | damage, 138 | opaque_regions, 139 | ) 140 | .map_err(UdevRenderError::Render) 141 | } 142 | 143 | fn underlying_storage( 144 | &self, 145 | _: &mut UdevRenderer<'a>, 146 | ) -> Option { 147 | None // pixel shader elements can't be scanned out. 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/renderer/texture_shader_element.rs: -------------------------------------------------------------------------------- 1 | use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement}; 2 | use smithay::backend::renderer::gles::element::TextureShaderElement; 3 | use smithay::backend::renderer::gles::GlesError; 4 | use smithay::backend::renderer::glow::{GlowFrame, GlowRenderer}; 5 | use smithay::backend::renderer::utils::CommitCounter; 6 | use smithay::utils::{Buffer, Physical, Point, Rectangle, Scale, Transform}; 7 | 8 | #[cfg(feature = "udev-backend")] 9 | use crate::backend::udev::{UdevFrame, UdevRenderError, UdevRenderer}; 10 | 11 | /// NewType wrapper to impl [`TextureShaderElement`] for [`UdevRenderer`] 12 | #[derive(Debug)] 13 | pub struct FhtTextureShaderElement(pub TextureShaderElement); 14 | 15 | impl From for FhtTextureShaderElement { 16 | fn from(value: TextureShaderElement) -> Self { 17 | Self(value) 18 | } 19 | } 20 | 21 | impl Element for FhtTextureShaderElement { 22 | fn id(&self) -> &Id { 23 | self.0.id() 24 | } 25 | 26 | fn current_commit(&self) -> CommitCounter { 27 | self.0.current_commit() 28 | } 29 | 30 | fn src(&self) -> Rectangle { 31 | self.0.src() 32 | } 33 | 34 | fn geometry(&self, scale: Scale) -> Rectangle { 35 | self.0.geometry(scale) 36 | } 37 | 38 | fn location(&self, scale: Scale) -> Point { 39 | self.geometry(scale).loc 40 | } 41 | 42 | fn transform(&self) -> Transform { 43 | Transform::Normal 44 | } 45 | 46 | fn damage_since( 47 | &self, 48 | scale: Scale, 49 | commit: Option, 50 | ) -> smithay::backend::renderer::utils::DamageSet { 51 | self.0.damage_since(scale, commit) 52 | } 53 | 54 | fn alpha(&self) -> f32 { 55 | self.0.alpha() 56 | } 57 | 58 | fn kind(&self) -> Kind { 59 | self.0.kind() 60 | } 61 | } 62 | 63 | impl RenderElement for FhtTextureShaderElement { 64 | fn draw( 65 | &self, 66 | frame: &mut GlowFrame<'_, '_>, 67 | src: Rectangle, 68 | dst: Rectangle, 69 | damage: &[Rectangle], 70 | opaque_regions: &[Rectangle], 71 | ) -> Result<(), GlesError> { 72 | >::draw( 73 | &self.0, 74 | frame, 75 | src, 76 | dst, 77 | damage, 78 | opaque_regions, 79 | ) 80 | } 81 | 82 | fn underlying_storage( 83 | &self, 84 | renderer: &mut GlowRenderer, 85 | ) -> Option { 86 | self.0.underlying_storage(renderer) 87 | } 88 | } 89 | 90 | #[cfg(feature = "udev-backend")] 91 | impl<'a> RenderElement> for FhtTextureShaderElement { 92 | fn draw( 93 | &self, 94 | frame: &mut UdevFrame<'a, '_, '_>, 95 | src: Rectangle, 96 | dst: Rectangle, 97 | damage: &[Rectangle], 98 | opaque_regions: &[Rectangle], 99 | ) -> Result<(), UdevRenderError> { 100 | >::draw( 101 | &self.0, 102 | frame.as_mut(), 103 | src, 104 | dst, 105 | damage, 106 | opaque_regions, 107 | ) 108 | .map_err(UdevRenderError::Render) 109 | } 110 | 111 | fn underlying_storage( 112 | &self, 113 | _: &mut UdevRenderer<'a>, 114 | ) -> Option { 115 | None // pixel shader elements can't be scanned out. 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/space/closing_tile.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use fht_animation::Animation; 4 | use smithay::backend::allocator::Fourcc; 5 | use smithay::backend::renderer::element::solid::SolidColorRenderElement; 6 | use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement}; 7 | use smithay::backend::renderer::element::utils::{ 8 | Relocate, RelocateRenderElement, RescaleRenderElement, 9 | }; 10 | use smithay::backend::renderer::element::{Element as _, Id, Kind}; 11 | use smithay::backend::renderer::gles::GlesTexture; 12 | use smithay::backend::renderer::glow::GlowRenderer; 13 | use smithay::backend::renderer::utils::CommitCounter; 14 | use smithay::backend::renderer::Color32F; 15 | use smithay::utils::{Logical, Point, Rectangle, Scale, Transform}; 16 | 17 | use super::tile::TileRenderElement; 18 | use crate::fht_render_elements; 19 | use crate::renderer::render_to_texture; 20 | use crate::renderer::texture_element::FhtTextureElement; 21 | 22 | const CLOSE_SCALE_THRESHOLD: f64 = 0.8; 23 | const FALLBACK_BUFFER_COLOR: Color32F = Color32F::new(1.0, 0.0, 0.0, 1.0); 24 | 25 | /// A representation of a closing [`Tile`](super::tile::Tile). 26 | /// 27 | /// When the tile's [`Window`](crate::window::Window) gets unmapped, or dies, the tile will have 28 | /// prepared in advance a list of render elements that constitutes the "snapshot" or the last frame 29 | /// before unmapped, used to animate a closing effect where the tile pops out. 30 | #[derive(Debug)] 31 | pub struct ClosingTile { 32 | /// The texture with the tile's render elements. 33 | /// 34 | /// If this is [`None`], a [`SolidColorBuffer`] will get rendered as a fallback. 35 | texture: Option<(TextureBuffer, Point)>, 36 | /// The last registered tile geometry. 37 | geometry: Rectangle, 38 | /// The animation. 39 | progress: Animation, 40 | } 41 | 42 | fht_render_elements! { 43 | ClosingTileRenderElement => { 44 | Texture = RelocateRenderElement>, 45 | Solid = SolidColorRenderElement, 46 | } 47 | } 48 | 49 | impl ClosingTile { 50 | pub fn new( 51 | renderer: &mut GlowRenderer, 52 | render_elements: Vec>, 53 | geometry: Rectangle, 54 | scale: Scale, 55 | animation: &super::AnimationConfig, 56 | ) -> Self { 57 | let geo = render_elements 58 | .iter() 59 | .fold(Rectangle::default(), |acc, e| acc.merge(e.geometry(scale))); 60 | let render_elements = render_elements.into_iter().rev().map(|e| { 61 | RelocateRenderElement::from_element(e, (-geo.loc.x, -geo.loc.y), Relocate::Relative) 62 | }); 63 | 64 | let texture = match render_to_texture( 65 | renderer, 66 | geo.size, 67 | scale, 68 | Transform::Normal, 69 | Fourcc::Abgr8888, 70 | render_elements.into_iter(), 71 | ) { 72 | Ok((texture, _)) => { 73 | let texture = TextureBuffer::from_texture( 74 | renderer, 75 | texture, 76 | scale.x.max(scale.y) as i32, 77 | Transform::Normal, 78 | None, 79 | ); 80 | Some((texture, geo.loc.to_f64().to_logical(scale).to_i32_round())) 81 | } 82 | Err(err) => { 83 | warn!(?err, "Failed to render texture for ClosingTile"); 84 | None 85 | } 86 | }; 87 | 88 | Self { 89 | texture, 90 | geometry, 91 | progress: Animation::new(1.0, 0.0, animation.duration).with_curve(animation.curve), 92 | } 93 | } 94 | 95 | pub fn advance_animations(&mut self, target_presentation_time: Duration) { 96 | self.progress.tick(target_presentation_time); 97 | } 98 | 99 | /// Did we finish animating the closing animation. 100 | pub fn is_finished(&self) -> bool { 101 | self.progress.is_finished() 102 | } 103 | 104 | /// Render this [`ClosingTile`]. 105 | /// 106 | /// NOTE: It is up to YOU to assure that the rendered that will draw the 107 | /// [`ClosingTileRenderElement`] is the same one used to create the [`ClosingTile`]. 108 | pub fn render(&self, scale: i32, alpha: f32) -> ClosingTileRenderElement { 109 | let Some((texture, offset)) = &self.texture else { 110 | return SolidColorRenderElement::new( 111 | Id::new(), 112 | self.geometry.to_physical_precise_round(scale), 113 | CommitCounter::default(), 114 | FALLBACK_BUFFER_COLOR, 115 | Kind::Unspecified, 116 | ) 117 | .into(); 118 | }; 119 | let progress = self.progress.value(); 120 | 121 | let texture: FhtTextureElement = TextureRenderElement::from_texture_buffer( 122 | Point::from((0., 0.)), 123 | texture, 124 | Some(progress.clamp(0., 1.) as f32 * alpha), 125 | None, 126 | None, 127 | Kind::Unspecified, 128 | ) 129 | .into(); 130 | 131 | let center = self.geometry.size.to_point().downscale(2); 132 | let origin = (center + *offset).to_physical_precise_round(scale); 133 | let rescale = progress * (1.0 - CLOSE_SCALE_THRESHOLD) + CLOSE_SCALE_THRESHOLD; 134 | let rescale = RescaleRenderElement::from_element(texture, origin, rescale); 135 | 136 | let location = (self.geometry.loc + *offset).to_physical_precise_round(scale); 137 | let relocate = RelocateRenderElement::from_element(rescale, location, Relocate::Relative); 138 | ClosingTileRenderElement::Texture(relocate) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/space/decorations.rs: -------------------------------------------------------------------------------- 1 | //! Decorations rendering. 2 | //! 3 | //! This is achieved using a GlesPixelShader, nothing special otherwise. 4 | 5 | use smithay::backend::renderer::element::Kind; 6 | use smithay::backend::renderer::gles::Uniform; 7 | use smithay::utils::{Logical, Point, Rectangle, Size}; 8 | 9 | use crate::renderer::pixel_shader_element::FhtPixelShaderElement; 10 | use crate::renderer::shaders::Shaders; 11 | use crate::renderer::AsGlowRenderer; 12 | 13 | pub fn draw_border( 14 | renderer: &mut impl AsGlowRenderer, 15 | scale: i32, 16 | alpha: f32, 17 | geometry: Rectangle, 18 | thickness: f64, 19 | radius: f64, 20 | color: fht_compositor_config::Color, 21 | ) -> FhtPixelShaderElement { 22 | let scaled_thickness = thickness * scale as f64; 23 | let (start_color, end_color, angle) = match color { 24 | fht_compositor_config::Color::Solid(color) => (color, color, 0.0), 25 | fht_compositor_config::Color::Gradient { start, end, angle } => (start, end, angle), 26 | }; 27 | 28 | // Only draw whats needed for the border. 29 | // 30 | // XXXXXXXXXXXXXXXXXX 31 | // XXooooooooooooooXX 32 | // XooooooooooooooooX 33 | // XooooooooooooooooX 34 | // XooooooooooooooooX 35 | // XooooooooooooooooX 36 | // XXooooooooooooooXX 37 | // XXXXXXXXXXXXXXXXXX 38 | // 39 | // We leave the four patches of X at each border for corner radius 40 | // FIXME: const? 41 | let border_damage = Rectangle::from_size(geometry.size).subtract_rect(Rectangle::new( 42 | Point::from((scaled_thickness, scaled_thickness)).to_i32_round(), 43 | geometry.size 44 | - Size::from((scaled_thickness, scaled_thickness)) 45 | .upscale(2.0) 46 | .to_i32_round(), 47 | )); 48 | 49 | FhtPixelShaderElement::new( 50 | Shaders::get(renderer.glow_renderer()).border.clone(), 51 | geometry, 52 | alpha, 53 | vec![ 54 | Uniform::new("v_start_color", start_color), 55 | Uniform::new("v_end_color", end_color), 56 | Uniform::new("v_gradient_angle", angle), 57 | // NOTE: For some reasons we cant use f64s, we shall cast 58 | Uniform::new("thickness", scaled_thickness as f32), 59 | Uniform::new("corner_radius", radius as f32), 60 | ], 61 | Some(&border_damage), 62 | Kind::Unspecified, 63 | ) 64 | } 65 | 66 | // Shadow drawing shader using the following article code: 67 | // https://madebyevan.com/shaders/fast-rounded-rectangle-shadows/ 68 | pub fn draw_shadow( 69 | renderer: &mut impl AsGlowRenderer, 70 | alpha: f32, 71 | scale: i32, 72 | mut geometry: Rectangle, 73 | blur_sigma: f32, 74 | corner_radius: f32, 75 | color: [f32; 4], 76 | ) -> FhtPixelShaderElement { 77 | let scaled_blur_sigma = (blur_sigma / scale as f32).round() as i32; 78 | geometry.loc -= Point::from((scaled_blur_sigma, scaled_blur_sigma)); 79 | geometry.size += Size::from((2 * scaled_blur_sigma, 2 * scaled_blur_sigma)); 80 | 81 | // We only draw the shadow around the window, its pointless to damage everything, only causing 82 | // useless drawing from the GPU. 83 | // 84 | // XXXXXXXXXXXXXXXXXX 85 | // XXooooooooooooooXX 86 | // XXooooooooooooooXX 87 | // XXooooooooooooooXX 88 | // XXooooooooooooooXX 89 | // XXXXXXXXXXXXXXXXXX 90 | // 91 | // We generate the damage for the regions marked by X, relative to (0,0) 92 | // FIXME: const? 93 | let shadow_damage = Rectangle::from_size(geometry.size).subtract_rect(Rectangle::new( 94 | Point::from((scaled_blur_sigma, scaled_blur_sigma)), 95 | geometry.size - Size::from((scaled_blur_sigma, scaled_blur_sigma)).upscale(2), 96 | )); 97 | 98 | FhtPixelShaderElement::new( 99 | Shaders::get(renderer.glow_renderer()).box_shadow.clone(), 100 | geometry, 101 | alpha, 102 | vec![ 103 | // NOTE: For some reasons we cant use f64s, we shall cast 104 | Uniform::new("shadow_color", color), 105 | Uniform::new("blur_sigma", blur_sigma), 106 | Uniform::new("corner_radius", corner_radius), 107 | ], 108 | Some(&shadow_damage), 109 | Kind::Unspecified, 110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | use std::mem::MaybeUninit; 2 | use std::os::unix::process::CommandExt; 3 | use std::process::Stdio; 4 | use std::time::Duration; 5 | 6 | use smithay::reexports::rustix; 7 | use smithay::utils::{Coordinate, Point, Rectangle}; 8 | 9 | #[cfg(feature = "xdg-screencast-portal")] 10 | pub mod pipewire; 11 | 12 | pub fn get_monotonic_time() -> Duration { 13 | // This does the same job as a Clock provided by smithay. 14 | // I do not understand why they decided to put on an abstraction 15 | // 16 | // We also do not use the Time structure provided by smithay since its really 17 | // annoying to work with (addition, difference, etc...) 18 | let timespec = rustix::time::clock_gettime(rustix::time::ClockId::Monotonic); 19 | Duration::new(timespec.tv_sec as u64, timespec.tv_nsec as u32) 20 | } 21 | 22 | pub fn spawn(cmd: &str) { 23 | let cmd = cmd.to_string(); 24 | crate::profile_function!(); 25 | let res = std::thread::Builder::new() 26 | .name("Command spawner".to_string()) 27 | .spawn(move || { 28 | let mut command = std::process::Command::new("/bin/sh"); 29 | command.args(["-c", &cmd]); 30 | // Disable all IO. 31 | command 32 | .stdin(Stdio::null()) 33 | .stdout(Stdio::null()) 34 | .stderr(Stdio::null()); 35 | 36 | // Double for in order to avoid the command being a child of fht-compositor. 37 | // This will allow us to avoid creating zombie processes. 38 | // 39 | // This also lets us not waitpid from the child 40 | unsafe { 41 | command.pre_exec(|| { 42 | match libc::fork() { 43 | -1 => return Err(std::io::Error::last_os_error()), 44 | 0 => (), 45 | _ => libc::_exit(0), 46 | } 47 | 48 | if libc::setsid() == -1 { 49 | return Err(std::io::Error::last_os_error()); 50 | } 51 | 52 | // Reset signal handlers. 53 | let mut signal_set = MaybeUninit::uninit(); 54 | libc::sigemptyset(signal_set.as_mut_ptr()); 55 | libc::sigprocmask( 56 | libc::SIG_SETMASK, 57 | signal_set.as_mut_ptr(), 58 | std::ptr::null_mut(), 59 | ); 60 | 61 | Ok(()) 62 | }); 63 | } 64 | 65 | let mut child = match command.spawn() { 66 | Ok(child) => child, 67 | Err(err) => { 68 | warn!(?err, ?cmd, "Error spawning command"); 69 | return; 70 | } 71 | }; 72 | 73 | match child.wait() { 74 | Ok(status) => { 75 | if !status.success() { 76 | warn!(?status, "Child didn't exit sucessfully") 77 | } 78 | } 79 | Err(err) => { 80 | warn!(?err, "Failed to wait for child") 81 | } 82 | } 83 | }); 84 | 85 | if let Err(err) = res { 86 | warn!(?err, "Failed to create command spawner for command") 87 | } 88 | } 89 | 90 | pub trait RectCenterExt { 91 | fn center(self) -> Point; 92 | } 93 | 94 | impl RectCenterExt for Rectangle { 95 | fn center(self) -> Point { 96 | self.loc + self.size.downscale(2).to_point() 97 | } 98 | } 99 | 100 | impl RectCenterExt for Rectangle { 101 | fn center(self) -> Point { 102 | self.loc + self.size.downscale(2.0).to_point() 103 | } 104 | } 105 | --------------------------------------------------------------------------------