Compare commits
30 Commits
main
...
v0.143.4-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66f9457bff | ||
|
|
e9714f7a74 | ||
|
|
477dc1481b | ||
|
|
8af43984d8 | ||
|
|
5b271b01ad | ||
|
|
b4c61e5ba5 | ||
|
|
ec270e344b | ||
|
|
f837bc1963 | ||
|
|
41fdc44d75 | ||
|
|
61f22219ed | ||
|
|
92aae1cec1 | ||
|
|
5e9139f4a6 | ||
|
|
8cae7eb6af | ||
|
|
8a71bc161e | ||
|
|
618915f5f1 | ||
|
|
7e6329eabf | ||
|
|
ec94ec9f2b | ||
|
|
e281abcc5f | ||
|
|
1e52998493 | ||
|
|
d6d75d2a54 | ||
|
|
ae7d49a06b | ||
|
|
daee260300 | ||
|
|
01902eeeda | ||
|
|
ae696c8582 | ||
|
|
9b84af1af4 | ||
|
|
29a5111ed9 | ||
|
|
dbf29656b8 | ||
|
|
3eec525351 | ||
|
|
c59098f07e | ||
|
|
443f423096 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -402,7 +402,7 @@ jobs:
|
||||
|
||||
- name: Upload app bundle to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
|
||||
58
Cargo.lock
generated
58
Cargo.lock
generated
@@ -341,9 +341,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093"
|
||||
version = "0.9.0"
|
||||
source = "git+https://github.com/bilelmoussaoui/ashpd?rev=29f2e1a#29f2e1a6f4b0911f504658f5f4630c02e01b13f2"
|
||||
dependencies = [
|
||||
"async-fs 2.1.1",
|
||||
"async-net 2.0.0",
|
||||
@@ -4889,11 +4888,9 @@ dependencies = [
|
||||
"log",
|
||||
"media",
|
||||
"metal",
|
||||
"mio 1.0.0",
|
||||
"num_cpus",
|
||||
"objc",
|
||||
"oo7",
|
||||
"open",
|
||||
"parking",
|
||||
"parking_lot",
|
||||
"pathfinder_geometry",
|
||||
@@ -5691,7 +5688,7 @@ dependencies = [
|
||||
"fnv",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"mio",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"tempfile",
|
||||
@@ -5705,25 +5702,6 @@ version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
|
||||
|
||||
[[package]]
|
||||
name = "is-docker"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-wsl"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||
dependencies = [
|
||||
"is-docker",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isahc"
|
||||
version = "1.7.2"
|
||||
@@ -6640,19 +6618,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4929e1f84c5e54c3ec6141cd5d8b5a5c055f031f80cf78f2072920173cb4d880"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.9",
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miow"
|
||||
version = "0.6.0"
|
||||
@@ -6891,7 +6856,7 @@ dependencies = [
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 0.8.11",
|
||||
"mio",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
@@ -7225,17 +7190,6 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "5.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "449f0ff855d85ddbf1edd5b646d65249ead3f5e422aaa86b7d2d0b049b103e32"
|
||||
dependencies = [
|
||||
"is-wsl",
|
||||
"libc",
|
||||
"pathdiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "open_ai"
|
||||
version = "0.1.0"
|
||||
@@ -11112,7 +11066,7 @@ dependencies = [
|
||||
"backtrace",
|
||||
"bytes 1.5.0",
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
@@ -13607,7 +13561,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.143.0"
|
||||
version = "0.143.4"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
12
Cargo.toml
12
Cargo.toml
@@ -272,9 +272,9 @@ zed_actions = { path = "crates/zed_actions" }
|
||||
alacritty_terminal = "0.23"
|
||||
any_vec = "0.13"
|
||||
anyhow = "1.0.57"
|
||||
ashpd = "0.8.0"
|
||||
ashpd = { git = "https://github.com/bilelmoussaoui/ashpd", rev = "29f2e1a" }
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-dispatcher = { version = "0.1"}
|
||||
async-dispatcher = { version = "0.1" }
|
||||
async-fs = "1.6"
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.4.2"
|
||||
@@ -315,7 +315,9 @@ image = "0.25.1"
|
||||
indexmap = { version = "1.6.2", features = ["serde"] }
|
||||
indoc = "1"
|
||||
# We explicitly disable http2 support in isahc.
|
||||
isahc = { version = "1.7.2", default-features = false, features = [ "text-decoding" ] }
|
||||
isahc = { version = "1.7.2", default-features = false, features = [
|
||||
"text-decoding",
|
||||
] }
|
||||
itertools = "0.11.0"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2"
|
||||
@@ -341,7 +343,9 @@ rand = "0.8.5"
|
||||
refineable = { path = "./crates/refineable" }
|
||||
regex = "1.5"
|
||||
repair_json = "0.1.0"
|
||||
runtimelib = { version="0.12", default-features = false, features = ["async-dispatcher-runtime"] }
|
||||
runtimelib = { version = "0.12", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
schemars = "0.8"
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
{
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"shift-tab": "menu::SelectPrev",
|
||||
"home": "menu::SelectFirst",
|
||||
"pageup": "menu::SelectFirst",
|
||||
"shift-pageup": "menu::SelectFirst",
|
||||
"ctrl-p": "menu::SelectPrev",
|
||||
"down": "menu::SelectNext",
|
||||
"tab": "menu::SelectNext",
|
||||
"end": "menu::SelectLast",
|
||||
"pagedown": "menu::SelectLast",
|
||||
"shift-pagedown": "menu::SelectFirst",
|
||||
"ctrl-n": "menu::SelectNext",
|
||||
@@ -16,8 +20,18 @@
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"shift-enter": "picker::UseSelectedQuery",
|
||||
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
|
||||
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
|
||||
"alt-enter": [
|
||||
"picker::ConfirmInput",
|
||||
{
|
||||
"secondary": false
|
||||
}
|
||||
],
|
||||
"ctrl-alt-enter": [
|
||||
"picker::ConfirmInput",
|
||||
{
|
||||
"secondary": true
|
||||
}
|
||||
],
|
||||
"ctrl-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"ctrl-o": "workspace::Open",
|
||||
@@ -148,12 +162,7 @@
|
||||
"replace_enabled": true
|
||||
}
|
||||
],
|
||||
// "cmd-e": [
|
||||
// "buffer_search::Deploy",
|
||||
// {
|
||||
// "focus": false
|
||||
// }
|
||||
// ],
|
||||
// "cmd-e": ["buffer_search::Deploy", { "focus": false }],
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"ctrl-alt-e": "editor::SelectEnclosingSymbol"
|
||||
@@ -268,6 +277,7 @@
|
||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||
"ctrl-w": "pane::CloseActiveItem",
|
||||
"ctrl-f4": "pane::CloseActiveItem",
|
||||
"alt-ctrl-t": "pane::CloseInactiveItems",
|
||||
"alt-ctrl-shift-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-k u": "pane::CloseCleanItems",
|
||||
@@ -349,16 +359,23 @@
|
||||
"f2": "editor::Rename",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"alt-f12": "editor::GoToDefinitionSplit",
|
||||
"ctrl-shift-f10": "editor::GoToDefinitionSplit",
|
||||
"ctrl-f12": "editor::GoToTypeDefinition",
|
||||
"shift-f12": "editor::GoToImplementation",
|
||||
"alt-ctrl-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-shift-\\": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-shift-[": "editor::Fold",
|
||||
"ctrl-shift-]": "editor::UnfoldLines",
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
"ctrl-.": "editor::ToggleCodeActions",
|
||||
"alt-ctrl-r": "editor::RevealInFinder",
|
||||
"ctrl-k r": "editor::RevealInFinder",
|
||||
"ctrl-k p": "editor::CopyPath",
|
||||
"ctrl-\\": "pane::SplitRight",
|
||||
"ctrl-k v": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-alt-shift-c": "editor::DisplayCursorNames"
|
||||
}
|
||||
},
|
||||
@@ -385,6 +402,8 @@
|
||||
"ctrl-alt--": "pane::GoBack",
|
||||
"ctrl-alt-_": "pane::GoForward",
|
||||
"ctrl-shift-t": "pane::ReopenClosedItem",
|
||||
"f3": "search::SelectNextMatch",
|
||||
"shift-f3": "search::SelectPrevMatch",
|
||||
"ctrl-shift-f": "project_search::ToggleFocus"
|
||||
}
|
||||
},
|
||||
@@ -392,12 +411,7 @@
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
// Change the default action on `menu::Confirm` by setting the parameter
|
||||
// "alt-cmd-o": [
|
||||
// "projects::OpenRecent",
|
||||
// {
|
||||
// "create_new_window": true
|
||||
// }
|
||||
// ]
|
||||
// "alt-ctrl-o": ["projects::OpenRecent", { "create_new_window": true }],
|
||||
"alt-ctrl-o": "projects::OpenRecent",
|
||||
"alt-ctrl-shift-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
@@ -432,9 +446,15 @@
|
||||
"ctrl-shift-t": "project_symbols::Toggle",
|
||||
"ctrl-p": "file_finder::Toggle",
|
||||
"ctrl-tab": "tab_switcher::Toggle",
|
||||
"ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
|
||||
"ctrl-shift-tab": [
|
||||
"tab_switcher::Toggle",
|
||||
{
|
||||
"select_last": true
|
||||
}
|
||||
],
|
||||
"ctrl-e": "file_finder::Toggle",
|
||||
"ctrl-shift-p": "command_palette::Toggle",
|
||||
"f1": "command_palette::Toggle",
|
||||
"ctrl-shift-m": "diagnostics::Deploy",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-b": "outline_panel::ToggleFocus",
|
||||
@@ -450,6 +470,7 @@
|
||||
"ctrl-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"ctrl-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"ctrl-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"ctrl-shift-x": "zed::Extensions",
|
||||
"alt-t": "task::Rerun",
|
||||
"alt-shift-t": "task::Spawn"
|
||||
}
|
||||
@@ -466,7 +487,7 @@
|
||||
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
|
||||
"ctrl-alt-d": "editor::DeleteToNextSubwordEnd",
|
||||
"ctrl-alt-left": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
|
||||
// "ctrl-alt-b": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-alt-right": "editor::MoveToNextSubwordEnd",
|
||||
"ctrl-alt-f": "editor::MoveToNextSubwordEnd",
|
||||
"ctrl-alt-shift-left": "editor::SelectToPreviousSubwordStart",
|
||||
@@ -591,11 +612,36 @@
|
||||
"alt-ctrl-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
"enter": "project_panel::Rename",
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"backspace": [
|
||||
"project_panel::Trash",
|
||||
{
|
||||
"skip_prompt": false
|
||||
}
|
||||
],
|
||||
"shift-delete": [
|
||||
"project_panel::Delete",
|
||||
{
|
||||
"skip_prompt": false
|
||||
}
|
||||
],
|
||||
"delete": [
|
||||
"project_panel::Trash",
|
||||
{
|
||||
"skip_prompt": false
|
||||
}
|
||||
],
|
||||
"ctrl-backspace": [
|
||||
"project_panel::Delete",
|
||||
{
|
||||
"skip_prompt": false
|
||||
}
|
||||
],
|
||||
"ctrl-delete": [
|
||||
"project_panel::Delete",
|
||||
{
|
||||
"skip_prompt": false
|
||||
}
|
||||
],
|
||||
"alt-ctrl-r": "project_panel::RevealInFinder",
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
@@ -636,7 +682,9 @@
|
||||
},
|
||||
{
|
||||
"context": "FileFinder",
|
||||
"bindings": { "ctrl-shift-p": "file_finder::SelectPrev" }
|
||||
"bindings": {
|
||||
"ctrl-shift-p": "file_finder::SelectPrev"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "TabSwitcher",
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
{
|
||||
"bindings": {
|
||||
"up": "menu::SelectPrev",
|
||||
"shift-tab": "menu::SelectPrev",
|
||||
"home": "menu::SelectFirst",
|
||||
"pageup": "menu::SelectFirst",
|
||||
"shift-pageup": "menu::SelectFirst",
|
||||
"ctrl-p": "menu::SelectPrev",
|
||||
"down": "menu::SelectNext",
|
||||
"tab": "menu::SelectNext",
|
||||
"end": "menu::SelectLast",
|
||||
"pagedown": "menu::SelectLast",
|
||||
"shift-pagedown": "menu::SelectFirst",
|
||||
"ctrl-n": "menu::SelectNext",
|
||||
@@ -285,6 +289,7 @@
|
||||
"context": "ProjectSearchBar",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"cmd-shift-j": "project_search::ToggleFilters",
|
||||
"cmd-shift-f": "search::FocusSearch",
|
||||
"cmd-shift-h": "search::ToggleReplace",
|
||||
"alt-cmd-g": "search::ToggleRegex",
|
||||
@@ -309,6 +314,7 @@
|
||||
"context": "ProjectSearchView",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"cmd-shift-j": "project_search::ToggleFilters",
|
||||
"cmd-shift-h": "search::ToggleReplace",
|
||||
"alt-cmd-g": "search::ToggleRegex",
|
||||
"alt-cmd-x": "search::ToggleRegex"
|
||||
@@ -400,11 +406,17 @@
|
||||
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
"cmd-shift-\\": "editor::MoveToEnclosingBracket",
|
||||
"alt-cmd-[": "editor::Fold",
|
||||
"alt-cmd-]": "editor::UnfoldLines",
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
"cmd-.": "editor::ToggleCodeActions",
|
||||
"alt-cmd-r": "editor::RevealInFinder",
|
||||
"cmd-k r": "editor::RevealInFinder",
|
||||
"cmd-k p": "editor::CopyPath",
|
||||
"cmd-\\": "pane::SplitRight",
|
||||
"cmd-k v": "markdown::OpenPreviewToTheSide",
|
||||
"cmd-shift-v": "markdown::OpenPreview",
|
||||
"ctrl-cmd-c": "editor::DisplayCursorNames"
|
||||
}
|
||||
},
|
||||
@@ -495,6 +507,7 @@
|
||||
"cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
|
||||
"cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
|
||||
"cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"],
|
||||
"cmd-shift-x": "zed::Extensions",
|
||||
"alt-t": "task::Rerun",
|
||||
"alt-shift-t": "task::Spawn"
|
||||
}
|
||||
@@ -619,6 +632,7 @@
|
||||
"cmd-alt-c": "project_panel::CopyPath",
|
||||
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
|
||||
"enter": "project_panel::Rename",
|
||||
"f2": "project_panel::Rename",
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
|
||||
@@ -707,10 +721,14 @@
|
||||
"escape": ["terminal::SendKeystroke", "escape"],
|
||||
"enter": ["terminal::SendKeystroke", "enter"],
|
||||
"ctrl-c": ["terminal::SendKeystroke", "ctrl-c"],
|
||||
"cmd-up": "terminal::ScrollPageUp",
|
||||
"cmd-down": "terminal::ScrollPageDown",
|
||||
"shift-pageup": "terminal::ScrollPageUp",
|
||||
"shift-pagedown": "terminal::ScrollPageDown",
|
||||
"shift-up": "terminal::ScrollLineUp",
|
||||
"shift-down": "terminal::ScrollLineDown",
|
||||
"cmd-home": "terminal::ScrollToTop",
|
||||
"cmd-end": "terminal::ScrollToBottom",
|
||||
"shift-home": "terminal::ScrollToTop",
|
||||
"shift-end": "terminal::ScrollToBottom"
|
||||
}
|
||||
|
||||
93
assets/keymaps/linux/atom.json
Normal file
93
assets/keymaps/linux/atom.json
Normal file
@@ -0,0 +1,93 @@
|
||||
// Default Keymap (Atom) for Zed on Linux
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-shift-f5": "workspace::Reload", // window:reload
|
||||
"ctrl-k ctrl-n": "workspace::ActivatePreviousPane", // window:focus-next-pane
|
||||
"ctrl-k ctrl-p": "workspace::ActivateNextPane" // window:focus-previous-pane
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-l": "language_selector::Toggle", // grammar-selector:show
|
||||
"ctrl-|": "pane::RevealInProjectPanel", // tree-view:reveal-active-file
|
||||
"ctrl-b": "editor::GoToDefinition", // fuzzy-finder:toggle-buffer-finder
|
||||
"ctrl-alt-b": "editor::GoToDefinitionSplit", // N/A: From JetBrains
|
||||
"ctrl-<": "editor::ScrollCursorCenter", // editor:scroll-to-cursor
|
||||
"f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next
|
||||
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
|
||||
"alt-shift-down": "editor::AddSelectionBelow", // editor:add-selection-below
|
||||
"alt-shift-up": "editor::AddSelectionAbove", // editor:add-selection-above
|
||||
"ctrl-k ctrl-u": "editor::ConvertToUpperCase", // editor:upper-case
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase", // editor:lower-case
|
||||
"ctrl-j": "editor::JoinLines", // editor:join-lines
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown", // editor:duplicate-lines
|
||||
"ctrl-up": "editor::MoveLineUp", // editor:move-line-up
|
||||
"ctrl-down": "editor::MoveLineDown", // editor:move-line-down
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide" // markdown-preview:toggle
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-r": "outline::Toggle" // symbols-view:toggle-project-symbols
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"ctrl-f3": "search::SelectNextMatch", // find-and-replace:find-next-selected
|
||||
"ctrl-shift-f3": "search::SelectPrevMatch" // find-and-replace:find-previous-selected
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-\\": "workspace::ToggleLeftDock", // tree-view:toggle
|
||||
"ctrl-k ctrl-b": "workspace::ToggleLeftDock", // tree-view:toggle
|
||||
"ctrl-t": "file_finder::Toggle", // fuzzy-finder:toggle-file-finder
|
||||
"ctrl-r": "project_symbols::Toggle" // symbols-view:toggle-project-symbols
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
// "ctrl-0": "project_panel::ToggleFocus", // tree-view:toggle-focus
|
||||
"ctrl-1": ["pane::ActivateItem", 0], // tree-view:open-selected-entry-in-pane-1
|
||||
"ctrl-2": ["pane::ActivateItem", 1], // tree-view:open-selected-entry-in-pane-2
|
||||
"ctrl-3": ["pane::ActivateItem", 2], // tree-view:open-selected-entry-in-pane-3
|
||||
"ctrl-4": ["pane::ActivateItem", 3], // tree-view:open-selected-entry-in-pane-4
|
||||
"ctrl-5": ["pane::ActivateItem", 4], // tree-view:open-selected-entry-in-pane-5
|
||||
"ctrl-6": ["pane::ActivateItem", 5], // tree-view:open-selected-entry-in-pane-6
|
||||
"ctrl-7": ["pane::ActivateItem", 6], // tree-view:open-selected-entry-in-pane-7
|
||||
"ctrl-8": ["pane::ActivateItem", 7], // tree-view:open-selected-entry-in-pane-8
|
||||
"ctrl-9": ["pane::ActivateItem", 8] // tree-view:open-selected-entry-in-pane-9
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"f2": "project_panel::Rename", // tree-view:rename
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"ctrl-x": "project_panel::Cut", // tree-view:cut
|
||||
"ctrl-c": "project_panel::Copy", // tree-view:copy
|
||||
"ctrl-v": "project_panel::Paste" // tree-view:paste
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel && not_editing",
|
||||
"bindings": {
|
||||
"ctrl-shift-c": "project_panel::CopyPath", // tree-view:copy-full-path
|
||||
"ctrl-[": "project_panel::CollapseSelectedEntry", // tree-view:collapse-directory
|
||||
"ctrl-b": "project_panel::CollapseSelectedEntry", // tree-view:collapse-directory
|
||||
"ctrl-]": "project_panel::ExpandSelectedEntry", // tree-view:expand-item
|
||||
"ctrl-f": "project_panel::ExpandSelectedEntry", // tree-view:expand-item
|
||||
"a": "project_panel::NewFile", // tree-view:add-file
|
||||
"d": "project_panel::Duplicate", // tree-view:duplicate
|
||||
"home": "menu::SelectFirst", // core:move-to-top
|
||||
"end": "menu::SelectLast", // core:move-to-bottom
|
||||
"shift-a": "project_panel::NewDirectory" // tree-view:add-folder
|
||||
}
|
||||
}
|
||||
]
|
||||
90
assets/keymaps/linux/jetbrains.json
Normal file
90
assets/keymaps/linux/jetbrains.json
Normal file
@@ -0,0 +1,90 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-shift-[": "pane::ActivatePrevItem",
|
||||
"ctrl-shift-]": "pane::ActivateNextItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl->": "zed::IncreaseBufferFontSize",
|
||||
"ctrl-<": "zed::DecreaseBufferFontSize",
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"ctrl-d": "editor::DuplicateLineDown",
|
||||
"ctrl-y": "editor::DeleteLine",
|
||||
"ctrl-pagedown": "editor::MovePageDown",
|
||||
"ctrl-pageup": "editor::MovePageUp",
|
||||
// "ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
|
||||
"ctrl-alt-enter": "editor::NewlineAbove",
|
||||
"shift-enter": "editor::NewlineBelow",
|
||||
// "ctrl--": "editor::Fold", // TODO: `ctrl-numpad--` (numpad not implemented)
|
||||
// "ctrl-+": "editor::UnfoldLines", // TODO: `ctrl-numpad+` (numpad not implemented)
|
||||
"alt-shift-g": "editor::SplitSelectionIntoLines",
|
||||
"alt-j": ["editor::SelectNext", { "replace_newest": false }],
|
||||
"alt-shift-j": ["editor::SelectPrevious", { "replace_newest": false }],
|
||||
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": true }],
|
||||
"alt-up": "editor::SelectLargerSyntaxNode",
|
||||
"alt-down": "editor::SelectSmallerSyntaxNode",
|
||||
"shift-alt-up": "editor::MoveLineUp",
|
||||
"shift-alt-down": "editor::MoveLineDown",
|
||||
"ctrl-alt-l": "editor::Format",
|
||||
"shift-f6": "editor::Rename",
|
||||
"ctrl-alt-left": "pane::GoBack",
|
||||
"ctrl-alt-right": "pane::GoForward",
|
||||
"alt-f7": "editor::FindAllReferences",
|
||||
"ctrl-alt-f7": "editor::FindAllReferences",
|
||||
// "ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
|
||||
// "ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleLeftDock
|
||||
"ctrl-shift-b": "editor::GoToTypeDefinition",
|
||||
"ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit",
|
||||
"f2": "editor::GoToDiagnostic",
|
||||
"shift-f2": "editor::GoToPrevDiagnostic",
|
||||
"ctrl-alt-shift-down": "editor::GoToHunk",
|
||||
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
|
||||
"ctrl-home": "editor::MoveToBeginning",
|
||||
"ctrl-end": "editor::MoveToEnd",
|
||||
"ctrl-shift-home": "editor::SelectToBeginning",
|
||||
"ctrl-shift-end": "editor::SelectToEnd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-f12": "outline::Toggle",
|
||||
"alt-7": "outline::Toggle",
|
||||
"ctrl-shift-n": "file_finder::Toggle",
|
||||
"ctrl-g": "go_to_line::Toggle",
|
||||
"alt-enter": "editor::ToggleCodeActions"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-shift-n": "file_finder::Toggle",
|
||||
"ctrl-shift-a": "command_palette::Toggle",
|
||||
"shift shift": "command_palette::Toggle",
|
||||
"ctrl-alt-shift-n": "project_symbols::Toggle",
|
||||
"alt-1": "workspace::ToggleLeftDock",
|
||||
"ctrl-e": "tab_switcher::Toggle",
|
||||
"alt-6": "diagnostics::Deploy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"ctrl-alt-left": "pane::GoBack",
|
||||
"ctrl-alt-right": "pane::GoForward"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"enter": "project_panel::Open",
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"shift-f6": "project_panel::Rename"
|
||||
}
|
||||
}
|
||||
]
|
||||
53
assets/keymaps/linux/sublime_text.json
Normal file
53
assets/keymaps/linux/sublime_text.json
Normal file
@@ -0,0 +1,53 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-shift-[": "pane::ActivatePrevItem",
|
||||
"ctrl-shift-]": "pane::ActivateNextItem",
|
||||
"ctrl-pagedown": "pane::ActivatePrevItem",
|
||||
"ctrl-pageup": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivateNextItem",
|
||||
"ctrl-tab": "pane::ActivatePrevItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
|
||||
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"ctrl-f12": "editor::GoToDefinitionSplit",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPrevHunk",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"ctrl-r": "outline::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"f4": "search::SelectNextMatch",
|
||||
"shift-f4": "search::SelectPrevMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-k ctrl-b": "workspace::ToggleLeftDock",
|
||||
// "ctrl-0": "project_panel::ToggleFocus", // normally resets zoom
|
||||
"shift-ctrl-r": "project_symbols::Toggle"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,8 @@
|
||||
// Default Keymap (Atom) for Zed on MacOS
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-alt-cmd-l": "workspace::Reload",
|
||||
"cmd-k cmd-p": "workspace::ActivatePreviousPane",
|
||||
"cmd-k cmd-n": "workspace::ActivateNextPane"
|
||||
}
|
||||
@@ -8,24 +10,23 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-l": "language_selector::Toggle",
|
||||
"cmd-|": "pane::RevealInProjectPanel",
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"alt-cmd-b": "editor::GoToDefinitionSplit",
|
||||
"cmd-<": "editor::ScrollCursorCenter",
|
||||
"cmd-g": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"ctrl-cmd-g": [
|
||||
"editor::SelectPrevious",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"cmd-g": ["editor::SelectNext", { "replace_newest": true }],
|
||||
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
|
||||
"cmd-k cmd-u": "editor::ConvertToUpperCase",
|
||||
"cmd-k cmd-l": "editor::ConvertToLowerCase",
|
||||
"alt-enter": "editor::Newline",
|
||||
"cmd-j": "editor::JoinLines",
|
||||
"cmd-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-cmd-up": "editor::MoveLineUp",
|
||||
"ctrl-cmd-down": "editor::MoveLineDown",
|
||||
"ctrl-shift-m": "markdown::OpenPreviewToTheSide"
|
||||
}
|
||||
},
|
||||
@@ -74,21 +75,22 @@
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"cmd-x": "project_panel::Cut",
|
||||
"cmd-c": "project_panel::Copy",
|
||||
"cmd-v": "project_panel::Paste",
|
||||
"ctrl-[": "project_panel::CollapseSelectedEntry",
|
||||
"ctrl-b": "project_panel::CollapseSelectedEntry",
|
||||
"alt-b": "project_panel::CollapseSelectedEntry",
|
||||
"ctrl-]": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-f": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-shift-c": "project_panel::CopyPath"
|
||||
"cmd-v": "project_panel::Paste"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel && not_editing",
|
||||
"bindings": {
|
||||
"ctrl-shift-c": "project_panel::CopyPath",
|
||||
"ctrl-[": "project_panel::CollapseSelectedEntry",
|
||||
"ctrl-b": "project_panel::CollapseSelectedEntry",
|
||||
"ctrl-]": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-f": "project_panel::ExpandSelectedEntry",
|
||||
"a": "project_panel::NewFile",
|
||||
"shift-a": "project_panel::NewDirectory",
|
||||
"shift-d": "project_panel::Duplicate"
|
||||
"d": "project_panel::Duplicate",
|
||||
"home": "menu::SelectFirst",
|
||||
"end": "menu::SelectLast",
|
||||
"shift-a": "project_panel::NewDirectory"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -54,7 +54,7 @@
|
||||
"cmd-shift-b": "editor::GoToTypeDefinition",
|
||||
"cmd-alt-shift-b": "editor::GoToTypeDefinitionSplit",
|
||||
"f2": "editor::GoToDiagnostic",
|
||||
"cmd-f2": "editor::GoToPrevDiagnostic",
|
||||
"shift-f2": "editor::GoToPrevDiagnostic",
|
||||
"ctrl-alt-shift-down": "editor::GoToHunk",
|
||||
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
|
||||
"cmd-home": "editor::MoveToBeginning",
|
||||
@@ -6,8 +6,7 @@
|
||||
"ctrl-pagedown": "pane::ActivatePrevItem",
|
||||
"ctrl-pageup": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivateNextItem",
|
||||
"ctrl-tab": "pane::ActivatePrevItem",
|
||||
"cmd-+": "zed::IncreaseBufferFontSize"
|
||||
"ctrl-tab": "pane::ActivatePrevItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -26,6 +25,9 @@
|
||||
"alt-shift-cmd-down": "editor::FindAllReferences",
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPrevHunk",
|
||||
"cmd-k cmd-u": "editor::ConvertToUpperCase",
|
||||
"cmd-k cmd-l": "editor::ConvertToLowerCase",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
}
|
||||
@@ -775,7 +775,8 @@
|
||||
"PHP": {
|
||||
"prettier": {
|
||||
"allowed": true,
|
||||
"plugins": ["@prettier/plugin-php"]
|
||||
"plugins": ["@prettier/plugin-php"],
|
||||
"parser": "php"
|
||||
}
|
||||
},
|
||||
"Ruby": {
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::{rc::Rc, sync::Arc};
|
||||
pub use collab_panel::CollabPanel;
|
||||
use gpui::{
|
||||
point, AppContext, Pixels, PlatformDisplay, Size, WindowBackgroundAppearance, WindowBounds,
|
||||
WindowKind, WindowOptions,
|
||||
WindowDecorations, WindowKind, WindowOptions,
|
||||
};
|
||||
use panel_settings::MessageEditorSettings;
|
||||
pub use panel_settings::{
|
||||
@@ -63,8 +63,9 @@ fn notification_window_options(
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
display_id: Some(screen.id()),
|
||||
window_background: WindowBackgroundAppearance::default(),
|
||||
window_background: WindowBackgroundAppearance::Transparent,
|
||||
app_id: Some(app_id.to_owned()),
|
||||
window_min_size: None,
|
||||
window_decorations: Some(WindowDecorations::Client),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,6 @@ wayland-protocols = { version = "0.31.2", features = [
|
||||
] }
|
||||
wayland-protocols-plasma = { version = "0.2.0", features = ["client"] }
|
||||
oo7 = "0.3.0"
|
||||
open = "5.1.2"
|
||||
filedescriptor = "0.8.2"
|
||||
x11rb = { version = "0.13.0", features = [
|
||||
"allow-unsafe-code",
|
||||
@@ -133,6 +132,7 @@ x11rb = { version = "0.13.0", features = [
|
||||
"xinput",
|
||||
"cursor",
|
||||
"resource_manager",
|
||||
"sync",
|
||||
] }
|
||||
xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
|
||||
xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca4afad184ab6e7c16af", features = [
|
||||
@@ -141,7 +141,6 @@ xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca
|
||||
] }
|
||||
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4", features = ["source-fontconfig-dlopen"] }
|
||||
x11-clipboard = "0.9.2"
|
||||
mio = { version = "1.0.0", features = ["os-poll", "os-ext"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
@@ -160,6 +159,10 @@ path = "examples/image/image.rs"
|
||||
name = "set_menus"
|
||||
path = "examples/set_menus.rs"
|
||||
|
||||
[[example]]
|
||||
name = "window_shadow"
|
||||
path = "examples/window_shadow.rs"
|
||||
|
||||
[[example]]
|
||||
name = "input"
|
||||
path = "examples/input.rs"
|
||||
|
||||
@@ -23,7 +23,7 @@ impl Render for HelloWorld {
|
||||
|
||||
fn main() {
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
|
||||
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
|
||||
@@ -52,6 +52,7 @@ fn main() {
|
||||
is_movable: false,
|
||||
app_id: None,
|
||||
window_min_size: None,
|
||||
window_decorations: None,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
222
crates/gpui/examples/window_shadow.rs
Normal file
222
crates/gpui/examples/window_shadow.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use gpui::*;
|
||||
use prelude::FluentBuilder;
|
||||
|
||||
struct WindowShadow {}
|
||||
|
||||
/*
|
||||
Things to do:
|
||||
1. We need a way of calculating which edge or corner the mouse is on,
|
||||
and then dispatch on that
|
||||
2. We need to improve the shadow rendering significantly
|
||||
3. We need to implement the techniques in here in Zed
|
||||
*/
|
||||
|
||||
impl Render for WindowShadow {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let decorations = cx.window_decorations();
|
||||
let rounding = px(10.0);
|
||||
let shadow_size = px(10.0);
|
||||
let border_size = px(1.0);
|
||||
let grey = rgb(0x808080);
|
||||
cx.set_client_inset(shadow_size);
|
||||
|
||||
div()
|
||||
.id("window-backdrop")
|
||||
.bg(transparent_black())
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { tiling, .. } => div
|
||||
.bg(gpui::transparent_black())
|
||||
.child(
|
||||
canvas(
|
||||
|_bounds, cx| {
|
||||
cx.insert_hitbox(
|
||||
Bounds::new(
|
||||
point(px(0.0), px(0.0)),
|
||||
cx.window_bounds().get_bounds().size,
|
||||
),
|
||||
false,
|
||||
)
|
||||
},
|
||||
move |_bounds, hitbox, cx| {
|
||||
let mouse = cx.mouse_position();
|
||||
let size = cx.window_bounds().get_bounds().size;
|
||||
let Some(edge) = resize_edge(mouse, shadow_size, size) else {
|
||||
return;
|
||||
};
|
||||
cx.set_cursor_style(
|
||||
match edge {
|
||||
ResizeEdge::Top | ResizeEdge::Bottom => {
|
||||
CursorStyle::ResizeUpDown
|
||||
}
|
||||
ResizeEdge::Left | ResizeEdge::Right => {
|
||||
CursorStyle::ResizeLeftRight
|
||||
}
|
||||
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
|
||||
CursorStyle::ResizeUpLeftDownRight
|
||||
}
|
||||
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
|
||||
CursorStyle::ResizeUpRightDownLeft
|
||||
}
|
||||
},
|
||||
&hitbox,
|
||||
);
|
||||
},
|
||||
)
|
||||
.size_full()
|
||||
.absolute(),
|
||||
)
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_tr(rounding)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
|
||||
.when(!tiling.top, |div| div.pt(shadow_size))
|
||||
.when(!tiling.bottom, |div| div.pb(shadow_size))
|
||||
.when(!tiling.left, |div| div.pl(shadow_size))
|
||||
.when(!tiling.right, |div| div.pr(shadow_size))
|
||||
.on_mouse_move(|_e, cx| cx.refresh())
|
||||
.on_mouse_down(MouseButton::Left, move |e, cx| {
|
||||
let size = cx.window_bounds().get_bounds().size;
|
||||
let pos = e.position;
|
||||
|
||||
match resize_edge(pos, shadow_size, size) {
|
||||
Some(edge) => cx.start_window_resize(edge),
|
||||
None => cx.start_window_move(),
|
||||
};
|
||||
}),
|
||||
})
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.cursor(CursorStyle::Arrow)
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { tiling } => div
|
||||
.border_color(grey)
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_tr(rounding)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |div| div.rounded_tl(rounding))
|
||||
.when(!tiling.top, |div| div.border_t(border_size))
|
||||
.when(!tiling.bottom, |div| div.border_b(border_size))
|
||||
.when(!tiling.left, |div| div.border_l(border_size))
|
||||
.when(!tiling.right, |div| div.border_r(border_size))
|
||||
.when(!tiling.is_tiled(), |div| {
|
||||
div.shadow(smallvec::smallvec![gpui::BoxShadow {
|
||||
color: Hsla {
|
||||
h: 0.,
|
||||
s: 0.,
|
||||
l: 0.,
|
||||
a: 0.4,
|
||||
},
|
||||
blur_radius: shadow_size / 2.,
|
||||
spread_radius: px(0.),
|
||||
offset: point(px(0.0), px(0.0)),
|
||||
}])
|
||||
}),
|
||||
})
|
||||
.on_mouse_move(|_e, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.bg(gpui::rgb(0xCCCCFF))
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_around()
|
||||
.child(
|
||||
div().w_full().flex().flex_row().justify_around().child(
|
||||
div()
|
||||
.flex()
|
||||
.bg(white())
|
||||
.size(Length::Definite(Pixels(300.0).into()))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.shadow_lg()
|
||||
.border_1()
|
||||
.border_color(rgb(0x0000ff))
|
||||
.text_xl()
|
||||
.text_color(rgb(0xffffff))
|
||||
.child(
|
||||
div()
|
||||
.id("hello")
|
||||
.w(px(200.0))
|
||||
.h(px(100.0))
|
||||
.bg(green())
|
||||
.shadow(smallvec::smallvec![gpui::BoxShadow {
|
||||
color: Hsla {
|
||||
h: 0.,
|
||||
s: 0.,
|
||||
l: 0.,
|
||||
a: 1.0,
|
||||
},
|
||||
blur_radius: px(20.0),
|
||||
spread_radius: px(0.0),
|
||||
offset: point(px(0.0), px(0.0)),
|
||||
}])
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { .. } => div
|
||||
.on_mouse_down(MouseButton::Left, |_e, cx| {
|
||||
cx.start_window_move();
|
||||
})
|
||||
.on_click(|e, cx| {
|
||||
if e.down.button == MouseButton::Right {
|
||||
cx.show_window_menu(e.up.position);
|
||||
}
|
||||
})
|
||||
.text_color(black())
|
||||
.child("this is the custom titlebar"),
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_edge(pos: Point<Pixels>, shadow_size: Pixels, size: Size<Pixels>) -> Option<ResizeEdge> {
|
||||
let edge = if pos.y < shadow_size && pos.x < shadow_size {
|
||||
ResizeEdge::TopLeft
|
||||
} else if pos.y < shadow_size && pos.x > size.width - shadow_size {
|
||||
ResizeEdge::TopRight
|
||||
} else if pos.y < shadow_size {
|
||||
ResizeEdge::Top
|
||||
} else if pos.y > size.height - shadow_size && pos.x < shadow_size {
|
||||
ResizeEdge::BottomLeft
|
||||
} else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size {
|
||||
ResizeEdge::BottomRight
|
||||
} else if pos.y > size.height - shadow_size {
|
||||
ResizeEdge::Bottom
|
||||
} else if pos.x < shadow_size {
|
||||
ResizeEdge::Left
|
||||
} else if pos.x > size.width - shadow_size {
|
||||
ResizeEdge::Right
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
Some(edge)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
let bounds = Bounds::centered(None, size(px(600.0), px(600.0)), cx);
|
||||
cx.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
window_background: WindowBackgroundAppearance::Opaque,
|
||||
window_decorations: Some(WindowDecorations::Client),
|
||||
..Default::default()
|
||||
},
|
||||
|cx| {
|
||||
cx.new_view(|cx| {
|
||||
cx.observe_window_appearance(|_, cx| {
|
||||
cx.refresh();
|
||||
})
|
||||
.detach();
|
||||
WindowShadow {}
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
@@ -309,6 +309,16 @@ pub fn transparent_black() -> Hsla {
|
||||
}
|
||||
}
|
||||
|
||||
/// Transparent black in [`Hsla`]
|
||||
pub fn transparent_white() -> Hsla {
|
||||
Hsla {
|
||||
h: 0.,
|
||||
s: 0.,
|
||||
l: 1.,
|
||||
a: 0.,
|
||||
}
|
||||
}
|
||||
|
||||
/// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1]
|
||||
pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
|
||||
Hsla {
|
||||
|
||||
@@ -883,6 +883,14 @@ where
|
||||
self.size.height = self.size.height.clone() + double_amount;
|
||||
}
|
||||
|
||||
/// inset the bounds by a specified amount
|
||||
/// Note that this may panic if T does not support negative values
|
||||
pub fn inset(&self, amount: T) -> Self {
|
||||
let mut result = self.clone();
|
||||
result.dilate(T::default() - amount);
|
||||
result
|
||||
}
|
||||
|
||||
/// Returns the center point of the bounds.
|
||||
///
|
||||
/// Calculates the center by taking the origin's x and y coordinates and adding half the width and height
|
||||
@@ -1266,12 +1274,36 @@ where
|
||||
/// size: Size { width: 10.0, height: 20.0 },
|
||||
/// });
|
||||
/// ```
|
||||
pub fn map_origin(self, f: impl Fn(Point<T>) -> Point<T>) -> Bounds<T> {
|
||||
pub fn map_origin(self, f: impl Fn(T) -> T) -> Bounds<T> {
|
||||
Bounds {
|
||||
origin: f(self.origin),
|
||||
origin: self.origin.map(f),
|
||||
size: self.size,
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies a function to the origin of the bounds, producing a new `Bounds` with the new origin
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use zed::{Bounds, Point, Size};
|
||||
/// let bounds = Bounds {
|
||||
/// origin: Point { x: 10.0, y: 10.0 },
|
||||
/// size: Size { width: 10.0, height: 20.0 },
|
||||
/// };
|
||||
/// let new_bounds = bounds.map_size(|value| value * 1.5);
|
||||
///
|
||||
/// assert_eq!(new_bounds, Bounds {
|
||||
/// origin: Point { x: 10.0, y: 10.0 },
|
||||
/// size: Size { width: 15.0, height: 30.0 },
|
||||
/// });
|
||||
/// ```
|
||||
pub fn map_size(self, f: impl Fn(T) -> T) -> Bounds<T> {
|
||||
Bounds {
|
||||
origin: self.origin,
|
||||
size: self.size.map(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the bounds represent an empty area.
|
||||
@@ -2288,6 +2320,18 @@ impl Pixels {
|
||||
Self(self.0.abs())
|
||||
}
|
||||
|
||||
/// Returns the sign of the `Pixels` value.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns:
|
||||
/// * `1.0` if the value is positive
|
||||
/// * `-1.0` if the value is negative
|
||||
/// * `0.0` if the value is zero
|
||||
pub fn signum(&self) -> f32 {
|
||||
self.0.signum()
|
||||
}
|
||||
|
||||
/// Returns the f64 value of `Pixels`.
|
||||
///
|
||||
/// # Returns
|
||||
|
||||
@@ -291,14 +291,41 @@ impl ScrollDelta {
|
||||
}
|
||||
|
||||
/// Combines two scroll deltas into one.
|
||||
/// If the signs of the deltas are the same (both positive or both negative),
|
||||
/// the deltas are added together. If the signs are opposite, the second delta
|
||||
/// (other) is used, effectively overriding the first delta.
|
||||
pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta {
|
||||
match (self, other) {
|
||||
(ScrollDelta::Pixels(px_a), ScrollDelta::Pixels(px_b)) => {
|
||||
ScrollDelta::Pixels(px_a + px_b)
|
||||
(ScrollDelta::Pixels(a), ScrollDelta::Pixels(b)) => {
|
||||
let x = if a.x.signum() * b.x.signum() >= 0. {
|
||||
a.x + b.x
|
||||
} else {
|
||||
b.x
|
||||
};
|
||||
|
||||
let y = if a.y.signum() * b.y.signum() >= 0. {
|
||||
a.y + b.y
|
||||
} else {
|
||||
b.y
|
||||
};
|
||||
|
||||
ScrollDelta::Pixels(point(x, y))
|
||||
}
|
||||
|
||||
(ScrollDelta::Lines(lines_a), ScrollDelta::Lines(lines_b)) => {
|
||||
ScrollDelta::Lines(lines_a + lines_b)
|
||||
(ScrollDelta::Lines(a), ScrollDelta::Lines(b)) => {
|
||||
let x = if a.x.signum() * b.x.signum() >= 0. {
|
||||
a.x + b.x
|
||||
} else {
|
||||
b.x
|
||||
};
|
||||
|
||||
let y = if a.y.signum() * b.y.signum() >= 0. {
|
||||
a.y + b.y
|
||||
} else {
|
||||
b.y
|
||||
};
|
||||
|
||||
ScrollDelta::Lines(point(x, y))
|
||||
}
|
||||
|
||||
_ => other,
|
||||
|
||||
@@ -210,6 +210,93 @@ impl Debug for DisplayId {
|
||||
|
||||
unsafe impl Send for DisplayId {}
|
||||
|
||||
/// Which part of the window to resize
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ResizeEdge {
|
||||
/// The top edge
|
||||
Top,
|
||||
/// The top right corner
|
||||
TopRight,
|
||||
/// The right edge
|
||||
Right,
|
||||
/// The bottom right corner
|
||||
BottomRight,
|
||||
/// The bottom edge
|
||||
Bottom,
|
||||
/// The bottom left corner
|
||||
BottomLeft,
|
||||
/// The left edge
|
||||
Left,
|
||||
/// The top left corner
|
||||
TopLeft,
|
||||
}
|
||||
|
||||
/// A type to describe the appearance of a window
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub enum WindowDecorations {
|
||||
#[default]
|
||||
/// Server side decorations
|
||||
Server,
|
||||
/// Client side decorations
|
||||
Client,
|
||||
}
|
||||
|
||||
/// A type to describe how this window is currently configured
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub enum Decorations {
|
||||
/// The window is configured to use server side decorations
|
||||
#[default]
|
||||
Server,
|
||||
/// The window is configured to use client side decorations
|
||||
Client {
|
||||
/// The edge tiling state
|
||||
tiling: Tiling,
|
||||
},
|
||||
}
|
||||
|
||||
/// What window controls this platform supports
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub struct WindowControls {
|
||||
/// Whether this platform supports fullscreen
|
||||
pub fullscreen: bool,
|
||||
/// Whether this platform supports maximize
|
||||
pub maximize: bool,
|
||||
/// Whether this platform supports minimize
|
||||
pub minimize: bool,
|
||||
/// Whether this platform supports a window menu
|
||||
pub window_menu: bool,
|
||||
}
|
||||
|
||||
/// A type to describe which sides of the window are currently tiled in some way
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub struct Tiling {
|
||||
/// Whether the top edge is tiled
|
||||
pub top: bool,
|
||||
/// Whether the left edge is tiled
|
||||
pub left: bool,
|
||||
/// Whether the right edge is tiled
|
||||
pub right: bool,
|
||||
/// Whether the bottom edge is tiled
|
||||
pub bottom: bool,
|
||||
}
|
||||
|
||||
impl Tiling {
|
||||
/// Initializes a [`Tiling`] type with all sides tiled
|
||||
pub fn tiled() -> Self {
|
||||
Self {
|
||||
top: true,
|
||||
left: true,
|
||||
right: true,
|
||||
bottom: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether any edge is tiled
|
||||
pub fn is_tiled(&self) -> bool {
|
||||
self.top || self.left || self.right || self.bottom
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn bounds(&self) -> Bounds<Pixels>;
|
||||
fn is_maximized(&self) -> bool;
|
||||
@@ -232,10 +319,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn activate(&self);
|
||||
fn is_active(&self) -> bool;
|
||||
fn set_title(&mut self, title: &str);
|
||||
fn set_app_id(&mut self, app_id: &str);
|
||||
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance);
|
||||
fn set_edited(&mut self, edited: bool);
|
||||
fn show_character_palette(&self);
|
||||
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
|
||||
fn minimize(&self);
|
||||
fn zoom(&self);
|
||||
fn toggle_fullscreen(&self);
|
||||
@@ -252,12 +336,31 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn completed_frame(&self) {}
|
||||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
|
||||
|
||||
// macOS specific methods
|
||||
fn set_edited(&mut self, _edited: bool) {}
|
||||
fn show_character_palette(&self) {}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_raw_handle(&self) -> windows::HWND;
|
||||
|
||||
fn show_window_menu(&self, position: Point<Pixels>);
|
||||
fn start_system_move(&self);
|
||||
fn should_render_window_controls(&self) -> bool;
|
||||
// Linux specific methods
|
||||
fn request_decorations(&self, _decorations: WindowDecorations) {}
|
||||
fn show_window_menu(&self, _position: Point<Pixels>) {}
|
||||
fn start_window_move(&self) {}
|
||||
fn start_window_resize(&self, _edge: ResizeEdge) {}
|
||||
fn window_decorations(&self) -> Decorations {
|
||||
Decorations::Server
|
||||
}
|
||||
fn set_app_id(&mut self, _app_id: &str) {}
|
||||
fn window_controls(&self) -> WindowControls {
|
||||
WindowControls {
|
||||
fullscreen: true,
|
||||
maximize: true,
|
||||
minimize: true,
|
||||
window_menu: false,
|
||||
}
|
||||
}
|
||||
fn set_client_inset(&self, _inset: Pixels) {}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn as_test(&mut self) -> Option<&mut TestWindow> {
|
||||
@@ -570,6 +673,10 @@ pub struct WindowOptions {
|
||||
|
||||
/// Window minimum size
|
||||
pub window_min_size: Option<Size<Pixels>>,
|
||||
|
||||
/// Whether to use client or server side decorations. Wayland only
|
||||
/// Note that this may be ignored.
|
||||
pub window_decorations: Option<WindowDecorations>,
|
||||
}
|
||||
|
||||
/// The variables that can be configured when creating a new window
|
||||
@@ -596,8 +703,6 @@ pub(crate) struct WindowParams {
|
||||
|
||||
pub display_id: Option<DisplayId>,
|
||||
|
||||
pub window_background: WindowBackgroundAppearance,
|
||||
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub window_min_size: Option<Size<Pixels>>,
|
||||
}
|
||||
@@ -649,6 +754,7 @@ impl Default for WindowOptions {
|
||||
window_background: WindowBackgroundAppearance::default(),
|
||||
app_id: None,
|
||||
window_min_size: None,
|
||||
window_decorations: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -659,7 +765,7 @@ pub struct TitlebarOptions {
|
||||
/// The initial title of the window
|
||||
pub title: Option<SharedString>,
|
||||
|
||||
/// Whether the titlebar should appear transparent
|
||||
/// Whether the titlebar should appear transparent (macOS only)
|
||||
pub appears_transparent: bool,
|
||||
|
||||
/// The position of the macOS traffic light buttons
|
||||
@@ -805,6 +911,14 @@ pub enum CursorStyle {
|
||||
/// corresponds to the CSS cursor value `ns-resize`
|
||||
ResizeUpDown,
|
||||
|
||||
/// A resize cursor directing up-left and down-right
|
||||
/// corresponds to the CSS cursor value `nesw-resize`
|
||||
ResizeUpLeftDownRight,
|
||||
|
||||
/// A resize cursor directing up-right and down-left
|
||||
/// corresponds to the CSS cursor value `nwse-resize`
|
||||
ResizeUpRightDownLeft,
|
||||
|
||||
/// A cursor indicating that the item/column can be resized horizontally.
|
||||
/// corresponds to the CSS curosr value `col-resize`
|
||||
ResizeColumn,
|
||||
|
||||
@@ -5,10 +5,12 @@ use calloop::{
|
||||
timer::TimeoutAction,
|
||||
EventLoop,
|
||||
};
|
||||
use mio::Waker;
|
||||
use parking::{Parker, Unparker};
|
||||
use parking_lot::Mutex;
|
||||
use std::{sync::Arc, thread, time::Duration};
|
||||
use std::{
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
struct TimerAfter {
|
||||
@@ -19,7 +21,6 @@ struct TimerAfter {
|
||||
pub(crate) struct LinuxDispatcher {
|
||||
parker: Mutex<Parker>,
|
||||
main_sender: Sender<Runnable>,
|
||||
main_waker: Option<Arc<Waker>>,
|
||||
timer_sender: Sender<TimerAfter>,
|
||||
background_sender: flume::Sender<Runnable>,
|
||||
_background_threads: Vec<thread::JoinHandle<()>>,
|
||||
@@ -27,18 +28,26 @@ pub(crate) struct LinuxDispatcher {
|
||||
}
|
||||
|
||||
impl LinuxDispatcher {
|
||||
pub fn new(main_sender: Sender<Runnable>, main_waker: Option<Arc<Waker>>) -> Self {
|
||||
pub fn new(main_sender: Sender<Runnable>) -> Self {
|
||||
let (background_sender, background_receiver) = flume::unbounded::<Runnable>();
|
||||
let thread_count = std::thread::available_parallelism()
|
||||
.map(|i| i.get())
|
||||
.unwrap_or(1);
|
||||
|
||||
let mut background_threads = (0..thread_count)
|
||||
.map(|_| {
|
||||
.map(|i| {
|
||||
let receiver = background_receiver.clone();
|
||||
std::thread::spawn(move || {
|
||||
for runnable in receiver {
|
||||
let start = Instant::now();
|
||||
|
||||
runnable.run();
|
||||
|
||||
log::trace!(
|
||||
"background thread {}: ran runnable. took: {:?}",
|
||||
i,
|
||||
start.elapsed()
|
||||
);
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -79,7 +88,6 @@ impl LinuxDispatcher {
|
||||
Self {
|
||||
parker: Mutex::new(Parker::new()),
|
||||
main_sender,
|
||||
main_waker,
|
||||
timer_sender,
|
||||
background_sender,
|
||||
_background_threads: background_threads,
|
||||
@@ -99,9 +107,6 @@ impl PlatformDispatcher for LinuxDispatcher {
|
||||
|
||||
fn dispatch_on_main_thread(&self, runnable: Runnable) {
|
||||
self.main_sender.send(runnable).ok();
|
||||
if let Some(main_waker) = self.main_waker.as_ref() {
|
||||
main_waker.wake().ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_after(&self, duration: Duration, runnable: Runnable) {
|
||||
|
||||
@@ -22,7 +22,7 @@ impl HeadlessClient {
|
||||
pub(crate) fn new() -> Self {
|
||||
let event_loop = EventLoop::try_new().unwrap();
|
||||
|
||||
let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()), None);
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
|
||||
let handle = event_loop.handle();
|
||||
|
||||
@@ -81,6 +81,8 @@ impl LinuxClient for HeadlessClient {
|
||||
|
||||
fn open_uri(&self, _uri: &str) {}
|
||||
|
||||
fn reveal_path(&self, _path: std::path::PathBuf) {}
|
||||
|
||||
fn write_to_primary(&self, _item: crate::ClipboardItem) {}
|
||||
|
||||
fn write_to_clipboard(&self, _item: crate::ClipboardItem) {}
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::ffi::OsString;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::os::fd::{AsRawFd, FromRawFd};
|
||||
use std::os::fd::{AsFd, AsRawFd, FromRawFd};
|
||||
use std::panic::Location;
|
||||
use std::rc::Weak;
|
||||
use std::{
|
||||
@@ -20,13 +20,14 @@ use std::{
|
||||
|
||||
use anyhow::anyhow;
|
||||
use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
|
||||
use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest as OpenUriRequest};
|
||||
use ashpd::{url, ActivationToken};
|
||||
use async_task::Runnable;
|
||||
use calloop::channel::Channel;
|
||||
use calloop::{EventLoop, LoopHandle, LoopSignal};
|
||||
use filedescriptor::FileDescriptor;
|
||||
use flume::{Receiver, Sender};
|
||||
use futures::channel::oneshot;
|
||||
use mio::Waker;
|
||||
use parking_lot::Mutex;
|
||||
use time::UtcOffset;
|
||||
use util::ResultExt;
|
||||
@@ -67,6 +68,7 @@ pub trait LinuxClient {
|
||||
) -> anyhow::Result<Box<dyn PlatformWindow>>;
|
||||
fn set_cursor_style(&self, style: CursorStyle);
|
||||
fn open_uri(&self, uri: &str);
|
||||
fn reveal_path(&self, path: PathBuf);
|
||||
fn write_to_primary(&self, item: ClipboardItem);
|
||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||
fn read_from_primary(&self) -> Option<ClipboardItem>;
|
||||
@@ -85,16 +87,6 @@ pub(crate) struct PlatformHandlers {
|
||||
pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
|
||||
}
|
||||
|
||||
pub trait QuitSignal {
|
||||
fn quit(&mut self);
|
||||
}
|
||||
|
||||
impl QuitSignal for LoopSignal {
|
||||
fn quit(&mut self) {
|
||||
self.stop();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct LinuxCommon {
|
||||
pub(crate) background_executor: BackgroundExecutor,
|
||||
pub(crate) foreground_executor: ForegroundExecutor,
|
||||
@@ -102,20 +94,17 @@ pub(crate) struct LinuxCommon {
|
||||
pub(crate) appearance: WindowAppearance,
|
||||
pub(crate) auto_hide_scrollbars: bool,
|
||||
pub(crate) callbacks: PlatformHandlers,
|
||||
pub(crate) quit_signal: Box<dyn QuitSignal>,
|
||||
pub(crate) signal: LoopSignal,
|
||||
pub(crate) menus: Vec<OwnedMenu>,
|
||||
}
|
||||
|
||||
impl LinuxCommon {
|
||||
pub fn new(
|
||||
quit_signal: Box<dyn QuitSignal>,
|
||||
main_waker: Option<Arc<Waker>>,
|
||||
) -> (Self, Channel<Runnable>) {
|
||||
pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
|
||||
let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
|
||||
let text_system = Arc::new(CosmicTextSystem::new());
|
||||
let callbacks = PlatformHandlers::default();
|
||||
|
||||
let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone(), main_waker));
|
||||
let dispatcher = Arc::new(LinuxDispatcher::new(main_sender.clone()));
|
||||
|
||||
let background_executor = BackgroundExecutor::new(dispatcher.clone());
|
||||
|
||||
@@ -126,7 +115,7 @@ impl LinuxCommon {
|
||||
appearance: WindowAppearance::Light,
|
||||
auto_hide_scrollbars: false,
|
||||
callbacks,
|
||||
quit_signal,
|
||||
signal,
|
||||
menus: Vec::new(),
|
||||
};
|
||||
|
||||
@@ -160,7 +149,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
}
|
||||
|
||||
fn quit(&self) {
|
||||
self.with_common(|common| common.quit_signal.quit());
|
||||
self.with_common(|common| common.signal.stop());
|
||||
}
|
||||
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
@@ -314,20 +303,27 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
let directory = directory.to_owned();
|
||||
self.foreground_executor()
|
||||
.spawn(async move {
|
||||
let result = SaveFileRequest::default()
|
||||
let request = SaveFileRequest::default()
|
||||
.modal(true)
|
||||
.title("Select new path")
|
||||
.accept_label("Accept")
|
||||
.send()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|request| request.response().ok())
|
||||
.and_then(|response| {
|
||||
response
|
||||
.uris()
|
||||
.first()
|
||||
.and_then(|uri| uri.to_file_path().ok())
|
||||
});
|
||||
.current_folder(directory);
|
||||
|
||||
let result = if let Ok(request) = request {
|
||||
request
|
||||
.send()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|request| request.response().ok())
|
||||
.and_then(|response| {
|
||||
response
|
||||
.uris()
|
||||
.first()
|
||||
.and_then(|uri| uri.to_file_path().ok())
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
done_tx.send(result);
|
||||
})
|
||||
@@ -337,13 +333,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
}
|
||||
|
||||
fn reveal_path(&self, path: &Path) {
|
||||
if path.is_dir() {
|
||||
open::that_detached(path);
|
||||
return;
|
||||
}
|
||||
// If `path` is a file, the system may try to open it in a text editor
|
||||
let dir = path.parent().unwrap_or(Path::new(""));
|
||||
open::that_detached(dir);
|
||||
self.reveal_path(path.to_owned());
|
||||
}
|
||||
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>) {
|
||||
@@ -504,18 +494,40 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
fn add_recent_document(&self, _path: &Path) {}
|
||||
}
|
||||
|
||||
pub(super) fn open_uri_internal(uri: &str, activation_token: Option<&str>) {
|
||||
let mut last_err = None;
|
||||
for mut command in open::commands(uri) {
|
||||
if let Some(token) = activation_token {
|
||||
command.env("XDG_ACTIVATION_TOKEN", token);
|
||||
}
|
||||
match command.spawn() {
|
||||
Ok(_) => return,
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
pub(super) fn open_uri_internal(
|
||||
executor: BackgroundExecutor,
|
||||
uri: &str,
|
||||
activation_token: Option<String>,
|
||||
) {
|
||||
if let Some(uri) = url::Url::parse(uri).log_err() {
|
||||
executor
|
||||
.spawn(async move {
|
||||
OpenUriRequest::default()
|
||||
.activation_token(activation_token.map(ActivationToken::from))
|
||||
.send_uri(&uri)
|
||||
.await
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
log::error!("failed to open uri: {uri:?}, last error: {last_err:?}");
|
||||
}
|
||||
|
||||
pub(super) fn reveal_path_internal(
|
||||
executor: BackgroundExecutor,
|
||||
path: PathBuf,
|
||||
activation_token: Option<String>,
|
||||
) {
|
||||
executor
|
||||
.spawn(async move {
|
||||
if let Some(dir) = File::open(path).log_err() {
|
||||
OpenDirectoryRequest::default()
|
||||
.activation_token(activation_token.map(ActivationToken::from))
|
||||
.send(&dir.as_fd())
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bool {
|
||||
@@ -572,6 +584,8 @@ impl CursorStyle {
|
||||
CursorStyle::ResizeUp => Shape::NResize,
|
||||
CursorStyle::ResizeDown => Shape::SResize,
|
||||
CursorStyle::ResizeUpDown => Shape::NsResize,
|
||||
CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize,
|
||||
CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize,
|
||||
CursorStyle::ResizeColumn => Shape::ColResize,
|
||||
CursorStyle::ResizeRow => Shape::RowResize,
|
||||
CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
|
||||
@@ -599,6 +613,8 @@ impl CursorStyle {
|
||||
CursorStyle::ResizeUp => "n-resize",
|
||||
CursorStyle::ResizeDown => "s-resize",
|
||||
CursorStyle::ResizeUpDown => "ns-resize",
|
||||
CursorStyle::ResizeUpLeftDownRight => "nwse-resize",
|
||||
CursorStyle::ResizeUpRightDownLeft => "nesw-resize",
|
||||
CursorStyle::ResizeColumn => "col-resize",
|
||||
CursorStyle::ResizeRow => "row-resize",
|
||||
CursorStyle::IBeamCursorForVerticalLayout => "vertical-text",
|
||||
|
||||
@@ -61,7 +61,6 @@ use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blu
|
||||
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
|
||||
use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
|
||||
|
||||
use super::super::{open_uri_internal, read_fd, DOUBLE_CLICK_INTERVAL};
|
||||
use super::display::WaylandDisplay;
|
||||
use super::window::{ImeInput, WaylandWindowStatePtr};
|
||||
use crate::platform::linux::wayland::clipboard::{
|
||||
@@ -72,11 +71,14 @@ use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
|
||||
use crate::platform::linux::wayland::window::WaylandWindow;
|
||||
use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::linux::{get_xkb_compose_state, is_within_click_distance};
|
||||
use crate::platform::linux::{
|
||||
get_xkb_compose_state, is_within_click_distance, open_uri_internal, read_fd,
|
||||
reveal_path_internal,
|
||||
};
|
||||
use crate::platform::PlatformWindow;
|
||||
use crate::{
|
||||
point, px, size, Bounds, DevicePixels, FileDropEvent, ForegroundExecutor, MouseExitEvent, Size,
|
||||
SCROLL_LINES,
|
||||
DOUBLE_CLICK_INTERVAL, SCROLL_LINES,
|
||||
};
|
||||
use crate::{
|
||||
AnyWindowHandle, CursorStyle, DisplayId, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
|
||||
@@ -138,7 +140,7 @@ impl Globals {
|
||||
primary_selection_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
shm: globals.bind(&qh, 1..=1, ()).unwrap(),
|
||||
seat,
|
||||
wm_base: globals.bind(&qh, 1..=1, ()).unwrap(),
|
||||
wm_base: globals.bind(&qh, 2..=5, ()).unwrap(),
|
||||
viewporter: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
decoration_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
@@ -220,7 +222,7 @@ pub(crate) struct WaylandClientState {
|
||||
data_offers: Vec<DataOffer<WlDataOffer>>,
|
||||
primary_data_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
|
||||
cursor: Cursor,
|
||||
pending_open_uri: Option<String>,
|
||||
pending_activation: Option<PendingActivation>,
|
||||
event_loop: Option<EventLoop<'static, WaylandClientStatePtr>>,
|
||||
common: LinuxCommon,
|
||||
}
|
||||
@@ -244,6 +246,15 @@ pub(crate) struct KeyRepeat {
|
||||
current_keycode: Option<xkb::Keycode>,
|
||||
}
|
||||
|
||||
pub(crate) enum PendingActivation {
|
||||
/// URI to open in the web browser.
|
||||
Uri(String),
|
||||
/// Path to open in the file explorer.
|
||||
Path(PathBuf),
|
||||
/// A window from ourselves to raise.
|
||||
Window(ObjectId),
|
||||
}
|
||||
|
||||
/// This struct is required to conform to Rust's orphan rules, so we can dispatch on the state but hand the
|
||||
/// window to GPUI.
|
||||
#[derive(Clone)]
|
||||
@@ -260,6 +271,11 @@ impl WaylandClientStatePtr {
|
||||
self.0.upgrade().unwrap().borrow().serial_tracker.get(kind)
|
||||
}
|
||||
|
||||
pub fn set_pending_activation(&self, window: ObjectId) {
|
||||
self.0.upgrade().unwrap().borrow_mut().pending_activation =
|
||||
Some(PendingActivation::Window(window));
|
||||
}
|
||||
|
||||
pub fn enable_ime(&self) {
|
||||
let client = self.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
@@ -310,7 +326,7 @@ impl WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
if state.windows.is_empty() {
|
||||
state.common.quit_signal.quit();
|
||||
state.common.signal.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -406,7 +422,7 @@ impl WaylandClient {
|
||||
|
||||
let event_loop = EventLoop::<WaylandClientStatePtr>::try_new().unwrap();
|
||||
|
||||
let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()), None);
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
|
||||
let handle = event_loop.handle();
|
||||
handle
|
||||
@@ -443,7 +459,7 @@ impl WaylandClient {
|
||||
let mut cursor = Cursor::new(&conn, &globals, 24);
|
||||
|
||||
handle
|
||||
.insert_source(XDPEventSource::new(&common.background_executor, None), {
|
||||
.insert_source(XDPEventSource::new(&common.background_executor), {
|
||||
move |event, _, client| match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
if let Some(client) = client.0.upgrade() {
|
||||
@@ -530,7 +546,7 @@ impl WaylandClient {
|
||||
data_offers: Vec::new(),
|
||||
primary_data_offer: None,
|
||||
cursor,
|
||||
pending_open_uri: None,
|
||||
pending_activation: None,
|
||||
event_loop: Some(event_loop),
|
||||
}));
|
||||
|
||||
@@ -629,14 +645,33 @@ impl LinuxClient for WaylandClient {
|
||||
state.globals.activation.clone(),
|
||||
state.mouse_focused_window.clone(),
|
||||
) {
|
||||
state.pending_open_uri = Some(uri.to_owned());
|
||||
state.pending_activation = Some(PendingActivation::Uri(uri.to_string()));
|
||||
let token = activation.get_activation_token(&state.globals.qh, ());
|
||||
let serial = state.serial_tracker.get(SerialKind::MousePress);
|
||||
token.set_serial(serial, &state.wl_seat);
|
||||
token.set_surface(&window.surface());
|
||||
token.commit();
|
||||
} else {
|
||||
open_uri_internal(uri, None);
|
||||
let executor = state.common.background_executor.clone();
|
||||
open_uri_internal(executor, uri, None);
|
||||
}
|
||||
}
|
||||
|
||||
fn reveal_path(&self, path: PathBuf) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
if let (Some(activation), Some(window)) = (
|
||||
state.globals.activation.clone(),
|
||||
state.mouse_focused_window.clone(),
|
||||
) {
|
||||
state.pending_activation = Some(PendingActivation::Path(path));
|
||||
let token = activation.get_activation_token(&state.globals.qh, ());
|
||||
let serial = state.serial_tracker.get(SerialKind::MousePress);
|
||||
token.set_serial(serial, &state.wl_seat);
|
||||
token.set_surface(&window.surface());
|
||||
token.commit();
|
||||
} else {
|
||||
let executor = state.common.background_executor.clone();
|
||||
reveal_path_internal(executor, path, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -809,7 +844,7 @@ impl Dispatch<WlCallback, ObjectId> for WaylandClientStatePtr {
|
||||
|
||||
match event {
|
||||
wl_callback::Event::Done { .. } => {
|
||||
window.frame(true);
|
||||
window.frame();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -954,13 +989,25 @@ impl Dispatch<xdg_activation_token_v1::XdgActivationTokenV1, ()> for WaylandClie
|
||||
) {
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
|
||||
if let xdg_activation_token_v1::Event::Done { token } = event {
|
||||
if let Some(uri) = state.pending_open_uri.take() {
|
||||
open_uri_internal(&uri, Some(&token));
|
||||
} else {
|
||||
log::error!("called while pending_open_uri is None");
|
||||
let executor = state.common.background_executor.clone();
|
||||
match state.pending_activation.take() {
|
||||
Some(PendingActivation::Uri(uri)) => open_uri_internal(executor, &uri, Some(token)),
|
||||
Some(PendingActivation::Path(path)) => {
|
||||
reveal_path_internal(executor, path, Some(token))
|
||||
}
|
||||
Some(PendingActivation::Window(window)) => {
|
||||
let Some(window) = get_window(&mut state, &window) else {
|
||||
return;
|
||||
};
|
||||
let activation = state.globals.activation.as_ref().unwrap();
|
||||
activation.activate(token, &window.surface());
|
||||
}
|
||||
None => log::error!("activation token received with no pending activation"),
|
||||
}
|
||||
}
|
||||
|
||||
token.destroy();
|
||||
}
|
||||
}
|
||||
@@ -1194,7 +1241,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
&& state.repeat.current_keycode.is_some()
|
||||
&& state.keyboard_focused_window.is_some();
|
||||
|
||||
if !is_repeating {
|
||||
if !is_repeating || rate == 0 {
|
||||
return TimeoutAction::Drop;
|
||||
}
|
||||
|
||||
@@ -1508,6 +1555,11 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
if state.axis_source == AxisSource::Wheel {
|
||||
return;
|
||||
}
|
||||
let axis = if state.modifiers.shift {
|
||||
wl_pointer::Axis::HorizontalScroll
|
||||
} else {
|
||||
axis
|
||||
};
|
||||
let axis_modifier = match axis {
|
||||
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
|
||||
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
|
||||
@@ -1533,6 +1585,11 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
discrete,
|
||||
} => {
|
||||
state.scroll_event_received = true;
|
||||
let axis = if state.modifiers.shift {
|
||||
wl_pointer::Axis::HorizontalScroll
|
||||
} else {
|
||||
axis
|
||||
};
|
||||
let axis_modifier = match axis {
|
||||
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
|
||||
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
|
||||
@@ -1555,6 +1612,11 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
value120,
|
||||
} => {
|
||||
state.scroll_event_received = true;
|
||||
let axis = if state.modifiers.shift {
|
||||
wl_pointer::Axis::HorizontalScroll
|
||||
} else {
|
||||
axis
|
||||
};
|
||||
let axis_modifier = match axis {
|
||||
wl_pointer::Axis::VerticalScroll => state.vertical_modifier,
|
||||
wl_pointer::Axis::HorizontalScroll => state.horizontal_modifier,
|
||||
|
||||
@@ -25,9 +25,10 @@ use crate::platform::linux::wayland::serial::SerialKind;
|
||||
use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow};
|
||||
use crate::scene::Scene;
|
||||
use crate::{
|
||||
px, size, AnyWindowHandle, Bounds, Globals, Modifiers, Output, Pixels, PlatformDisplay,
|
||||
PlatformInput, Point, PromptLevel, Size, WaylandClientStatePtr, WindowAppearance,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowParams,
|
||||
px, size, AnyWindowHandle, Bounds, Decorations, Globals, Modifiers, Output, Pixels,
|
||||
PlatformDisplay, PlatformInput, Point, PromptLevel, ResizeEdge, Size, Tiling,
|
||||
WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
|
||||
WindowControls, WindowDecorations, WindowParams,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -62,10 +63,12 @@ impl rwh::HasDisplayHandle for RawWindow {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InProgressConfigure {
|
||||
size: Option<Size<Pixels>>,
|
||||
fullscreen: bool,
|
||||
maximized: bool,
|
||||
tiling: Tiling,
|
||||
}
|
||||
|
||||
pub struct WaylandWindowState {
|
||||
@@ -73,6 +76,7 @@ pub struct WaylandWindowState {
|
||||
acknowledged_first_configure: bool,
|
||||
pub surface: wl_surface::WlSurface,
|
||||
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||
app_id: Option<String>,
|
||||
appearance: WindowAppearance,
|
||||
blur: Option<org_kde_kwin_blur::OrgKdeKwinBlur>,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
@@ -84,14 +88,19 @@ pub struct WaylandWindowState {
|
||||
bounds: Bounds<Pixels>,
|
||||
scale: f32,
|
||||
input_handler: Option<PlatformInputHandler>,
|
||||
decoration_state: WaylandDecorationState,
|
||||
decorations: WindowDecorations,
|
||||
background_appearance: WindowBackgroundAppearance,
|
||||
fullscreen: bool,
|
||||
maximized: bool,
|
||||
windowed_bounds: Bounds<Pixels>,
|
||||
tiling: Tiling,
|
||||
window_bounds: Bounds<Pixels>,
|
||||
client: WaylandClientStatePtr,
|
||||
handle: AnyWindowHandle,
|
||||
active: bool,
|
||||
in_progress_configure: Option<InProgressConfigure>,
|
||||
in_progress_window_controls: Option<WindowControls>,
|
||||
window_controls: WindowControls,
|
||||
inset: Option<Pixels>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -142,7 +151,7 @@ impl WaylandWindowState {
|
||||
height: options.bounds.size.height.0 as u32,
|
||||
depth: 1,
|
||||
},
|
||||
transparent: options.window_background != WindowBackgroundAppearance::Opaque,
|
||||
transparent: true,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
@@ -150,6 +159,7 @@ impl WaylandWindowState {
|
||||
acknowledged_first_configure: false,
|
||||
surface,
|
||||
decoration,
|
||||
app_id: None,
|
||||
blur: None,
|
||||
toplevel,
|
||||
viewport,
|
||||
@@ -160,17 +170,33 @@ impl WaylandWindowState {
|
||||
bounds: options.bounds,
|
||||
scale: 1.0,
|
||||
input_handler: None,
|
||||
decoration_state: WaylandDecorationState::Client,
|
||||
decorations: WindowDecorations::Client,
|
||||
background_appearance: WindowBackgroundAppearance::Opaque,
|
||||
fullscreen: false,
|
||||
maximized: false,
|
||||
windowed_bounds: options.bounds,
|
||||
tiling: Tiling::default(),
|
||||
window_bounds: options.bounds,
|
||||
in_progress_configure: None,
|
||||
client,
|
||||
appearance,
|
||||
handle,
|
||||
active: false,
|
||||
in_progress_window_controls: None,
|
||||
// Assume that we can do anything, unless told otherwise
|
||||
window_controls: WindowControls {
|
||||
fullscreen: true,
|
||||
maximize: true,
|
||||
minimize: true,
|
||||
window_menu: true,
|
||||
},
|
||||
inset: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_transparent(&self) -> bool {
|
||||
self.decorations == WindowDecorations::Client
|
||||
|| self.background_appearance != WindowBackgroundAppearance::Opaque
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct WaylandWindow(pub WaylandWindowStatePtr);
|
||||
@@ -235,7 +261,7 @@ impl WaylandWindow {
|
||||
.wm_base
|
||||
.get_xdg_surface(&surface, &globals.qh, surface.id());
|
||||
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
|
||||
toplevel.set_min_size(200, 200);
|
||||
toplevel.set_min_size(50, 50);
|
||||
|
||||
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
|
||||
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
|
||||
@@ -246,13 +272,7 @@ impl WaylandWindow {
|
||||
.decoration_manager
|
||||
.as_ref()
|
||||
.map(|decoration_manager| {
|
||||
let decoration = decoration_manager.get_toplevel_decoration(
|
||||
&toplevel,
|
||||
&globals.qh,
|
||||
surface.id(),
|
||||
);
|
||||
decoration.set_mode(zxdg_toplevel_decoration_v1::Mode::ClientSide);
|
||||
decoration
|
||||
decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id())
|
||||
});
|
||||
|
||||
let viewport = globals
|
||||
@@ -296,12 +316,11 @@ impl WaylandWindowStatePtr {
|
||||
Rc::ptr_eq(&self.state, &other.state)
|
||||
}
|
||||
|
||||
pub fn frame(&self, request_frame_callback: bool) {
|
||||
if request_frame_callback {
|
||||
let state = self.state.borrow_mut();
|
||||
state.surface.frame(&state.globals.qh, state.surface.id());
|
||||
drop(state);
|
||||
}
|
||||
pub fn frame(&self) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.surface.frame(&state.globals.qh, state.surface.id());
|
||||
drop(state);
|
||||
|
||||
let mut cb = self.callbacks.borrow_mut();
|
||||
if let Some(fun) = cb.request_frame.as_mut() {
|
||||
fun();
|
||||
@@ -311,6 +330,18 @@ impl WaylandWindowStatePtr {
|
||||
pub fn handle_xdg_surface_event(&self, event: xdg_surface::Event) {
|
||||
match event {
|
||||
xdg_surface::Event::Configure { serial } => {
|
||||
{
|
||||
let mut state = self.state.borrow_mut();
|
||||
if let Some(window_controls) = state.in_progress_window_controls.take() {
|
||||
state.window_controls = window_controls;
|
||||
|
||||
drop(state);
|
||||
let mut callbacks = self.callbacks.borrow_mut();
|
||||
if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() {
|
||||
appearance_changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut state = self.state.borrow_mut();
|
||||
|
||||
@@ -318,18 +349,20 @@ impl WaylandWindowStatePtr {
|
||||
let got_unmaximized = state.maximized && !configure.maximized;
|
||||
state.fullscreen = configure.fullscreen;
|
||||
state.maximized = configure.maximized;
|
||||
|
||||
if got_unmaximized {
|
||||
configure.size = Some(state.windowed_bounds.size);
|
||||
} else if !configure.fullscreen && !configure.maximized {
|
||||
state.tiling = configure.tiling;
|
||||
if !configure.fullscreen && !configure.maximized {
|
||||
configure.size = if got_unmaximized {
|
||||
Some(state.window_bounds.size)
|
||||
} else {
|
||||
compute_outer_size(state.inset, configure.size, state.tiling)
|
||||
};
|
||||
if let Some(size) = configure.size {
|
||||
state.windowed_bounds = Bounds {
|
||||
state.window_bounds = Bounds {
|
||||
origin: Point::default(),
|
||||
size,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
drop(state);
|
||||
if let Some(size) = configure.size {
|
||||
self.resize(size);
|
||||
@@ -338,10 +371,28 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.xdg_surface.ack_configure(serial);
|
||||
|
||||
let window_geometry = inset_by_tiling(
|
||||
state.bounds.map_origin(|_| px(0.0)),
|
||||
state.inset.unwrap_or(px(0.0)),
|
||||
state.tiling,
|
||||
)
|
||||
.map(|v| v.0 as i32)
|
||||
.map_size(|v| if v <= 0 { 1 } else { v });
|
||||
|
||||
state.xdg_surface.set_window_geometry(
|
||||
window_geometry.origin.x,
|
||||
window_geometry.origin.y,
|
||||
window_geometry.size.width,
|
||||
window_geometry.size.height,
|
||||
);
|
||||
|
||||
let request_frame_callback = !state.acknowledged_first_configure;
|
||||
state.acknowledged_first_configure = true;
|
||||
drop(state);
|
||||
self.frame(request_frame_callback);
|
||||
if request_frame_callback {
|
||||
state.acknowledged_first_configure = true;
|
||||
drop(state);
|
||||
self.frame();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -351,10 +402,21 @@ impl WaylandWindowStatePtr {
|
||||
match event {
|
||||
zxdg_toplevel_decoration_v1::Event::Configure { mode } => match mode {
|
||||
WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ServerSide) => {
|
||||
self.set_decoration_state(WaylandDecorationState::Server)
|
||||
self.state.borrow_mut().decorations = WindowDecorations::Server;
|
||||
if let Some(mut appearance_changed) =
|
||||
self.callbacks.borrow_mut().appearance_changed.as_mut()
|
||||
{
|
||||
appearance_changed();
|
||||
}
|
||||
}
|
||||
WEnum::Value(zxdg_toplevel_decoration_v1::Mode::ClientSide) => {
|
||||
self.set_decoration_state(WaylandDecorationState::Client)
|
||||
self.state.borrow_mut().decorations = WindowDecorations::Client;
|
||||
// Update background to be transparent
|
||||
if let Some(mut appearance_changed) =
|
||||
self.callbacks.borrow_mut().appearance_changed.as_mut()
|
||||
{
|
||||
appearance_changed();
|
||||
}
|
||||
}
|
||||
WEnum::Value(_) => {
|
||||
log::warn!("Unknown decoration mode");
|
||||
@@ -389,14 +451,48 @@ impl WaylandWindowStatePtr {
|
||||
Some(size(px(width as f32), px(height as f32)))
|
||||
};
|
||||
|
||||
let fullscreen = states.contains(&(xdg_toplevel::State::Fullscreen as u8));
|
||||
let maximized = states.contains(&(xdg_toplevel::State::Maximized as u8));
|
||||
let states = extract_states::<xdg_toplevel::State>(&states);
|
||||
|
||||
let mut tiling = Tiling::default();
|
||||
let mut fullscreen = false;
|
||||
let mut maximized = false;
|
||||
|
||||
for state in states {
|
||||
match state {
|
||||
xdg_toplevel::State::Maximized => {
|
||||
maximized = true;
|
||||
}
|
||||
xdg_toplevel::State::Fullscreen => {
|
||||
fullscreen = true;
|
||||
}
|
||||
xdg_toplevel::State::TiledTop => {
|
||||
tiling.top = true;
|
||||
}
|
||||
xdg_toplevel::State::TiledLeft => {
|
||||
tiling.left = true;
|
||||
}
|
||||
xdg_toplevel::State::TiledRight => {
|
||||
tiling.right = true;
|
||||
}
|
||||
xdg_toplevel::State::TiledBottom => {
|
||||
tiling.bottom = true;
|
||||
}
|
||||
_ => {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fullscreen || maximized {
|
||||
tiling = Tiling::tiled();
|
||||
}
|
||||
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.in_progress_configure = Some(InProgressConfigure {
|
||||
size,
|
||||
fullscreen,
|
||||
maximized,
|
||||
tiling,
|
||||
});
|
||||
|
||||
false
|
||||
@@ -415,6 +511,33 @@ impl WaylandWindowStatePtr {
|
||||
true
|
||||
}
|
||||
}
|
||||
xdg_toplevel::Event::WmCapabilities { capabilities } => {
|
||||
let mut window_controls = WindowControls::default();
|
||||
|
||||
let states = extract_states::<xdg_toplevel::WmCapabilities>(&capabilities);
|
||||
|
||||
for state in states {
|
||||
match state {
|
||||
xdg_toplevel::WmCapabilities::Maximize => {
|
||||
window_controls.maximize = true;
|
||||
}
|
||||
xdg_toplevel::WmCapabilities::Minimize => {
|
||||
window_controls.minimize = true;
|
||||
}
|
||||
xdg_toplevel::WmCapabilities::Fullscreen => {
|
||||
window_controls.fullscreen = true;
|
||||
}
|
||||
xdg_toplevel::WmCapabilities::WindowMenu => {
|
||||
window_controls.window_menu = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.in_progress_window_controls = Some(window_controls);
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -545,18 +668,6 @@ impl WaylandWindowStatePtr {
|
||||
self.set_size_and_scale(None, Some(scale));
|
||||
}
|
||||
|
||||
/// Notifies the window of the state of the decorations.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This API is indirectly called by the wayland compositor and
|
||||
/// not meant to be called by a user who wishes to change the state
|
||||
/// of the decorations. This is because the state of the decorations
|
||||
/// is managed by the compositor and not the client.
|
||||
pub fn set_decoration_state(&self, state: WaylandDecorationState) {
|
||||
self.state.borrow_mut().decoration_state = state;
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
let mut callbacks = self.callbacks.borrow_mut();
|
||||
if let Some(fun) = callbacks.close.take() {
|
||||
@@ -599,6 +710,17 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_states<'a, S: TryFrom<u32> + 'a>(states: &'a [u8]) -> impl Iterator<Item = S> + 'a
|
||||
where
|
||||
<S as TryFrom<u32>>::Error: 'a,
|
||||
{
|
||||
states
|
||||
.chunks_exact(4)
|
||||
.flat_map(TryInto::<[u8; 4]>::try_into)
|
||||
.map(u32::from_ne_bytes)
|
||||
.flat_map(S::try_from)
|
||||
}
|
||||
|
||||
fn primary_output_scale(state: &mut RefMut<WaylandWindowState>) -> i32 {
|
||||
let mut scale = 1;
|
||||
let mut current_output = state.display.take();
|
||||
@@ -639,9 +761,9 @@ impl PlatformWindow for WaylandWindow {
|
||||
fn window_bounds(&self) -> WindowBounds {
|
||||
let state = self.borrow();
|
||||
if state.fullscreen {
|
||||
WindowBounds::Fullscreen(state.windowed_bounds)
|
||||
WindowBounds::Fullscreen(state.window_bounds)
|
||||
} else if state.maximized {
|
||||
WindowBounds::Maximized(state.windowed_bounds)
|
||||
WindowBounds::Maximized(state.window_bounds)
|
||||
} else {
|
||||
drop(state);
|
||||
WindowBounds::Windowed(self.bounds())
|
||||
@@ -703,7 +825,20 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
|
||||
fn activate(&self) {
|
||||
log::info!("Wayland does not support this API");
|
||||
// Try to request an activation token. Even though the activation is likely going to be rejected,
|
||||
// KWin and Mutter can use the app_id to visually indicate we're requesting attention.
|
||||
let state = self.borrow();
|
||||
if let (Some(activation), Some(app_id)) = (&state.globals.activation, state.app_id.clone())
|
||||
{
|
||||
state.client.set_pending_activation(state.surface.id());
|
||||
let token = activation.get_activation_token(&state.globals.qh, ());
|
||||
// The serial isn't exactly important here, since the activation is probably going to be rejected anyway.
|
||||
let serial = state.client.get_serial(SerialKind::MousePress);
|
||||
token.set_app_id(app_id);
|
||||
token.set_serial(serial, &state.globals.seat);
|
||||
token.set_surface(&state.surface);
|
||||
token.commit();
|
||||
}
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
@@ -715,55 +850,15 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, app_id: &str) {
|
||||
self.borrow().toplevel.set_app_id(app_id.to_owned());
|
||||
}
|
||||
|
||||
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
|
||||
let opaque = background_appearance == WindowBackgroundAppearance::Opaque;
|
||||
let mut state = self.borrow_mut();
|
||||
state.renderer.update_transparency(!opaque);
|
||||
|
||||
let region = state
|
||||
.globals
|
||||
.compositor
|
||||
.create_region(&state.globals.qh, ());
|
||||
region.add(0, 0, i32::MAX, i32::MAX);
|
||||
|
||||
if opaque {
|
||||
// Promise the compositor that this region of the window surface
|
||||
// contains no transparent pixels. This allows the compositor to
|
||||
// do skip whatever is behind the surface for better performance.
|
||||
state.surface.set_opaque_region(Some(®ion));
|
||||
} else {
|
||||
state.surface.set_opaque_region(None);
|
||||
}
|
||||
|
||||
if let Some(ref blur_manager) = state.globals.blur_manager {
|
||||
if background_appearance == WindowBackgroundAppearance::Blurred {
|
||||
if state.blur.is_none() {
|
||||
let blur = blur_manager.create(&state.surface, &state.globals.qh, ());
|
||||
blur.set_region(Some(®ion));
|
||||
state.blur = Some(blur);
|
||||
}
|
||||
state.blur.as_ref().unwrap().commit();
|
||||
} else {
|
||||
// It probably doesn't hurt to clear the blur for opaque windows
|
||||
blur_manager.unset(&state.surface);
|
||||
if let Some(b) = state.blur.take() {
|
||||
b.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
region.destroy();
|
||||
state.toplevel.set_app_id(app_id.to_owned());
|
||||
state.app_id = Some(app_id.to_owned());
|
||||
}
|
||||
|
||||
fn set_edited(&mut self, _edited: bool) {
|
||||
log::info!("ignoring macOS specific set_edited");
|
||||
}
|
||||
|
||||
fn show_character_palette(&self) {
|
||||
log::info!("ignoring macOS specific show_character_palette");
|
||||
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
|
||||
let mut state = self.borrow_mut();
|
||||
state.background_appearance = background_appearance;
|
||||
update_window(state);
|
||||
}
|
||||
|
||||
fn minimize(&self) {
|
||||
@@ -830,7 +925,7 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
|
||||
fn completed_frame(&self) {
|
||||
let mut state = self.borrow_mut();
|
||||
let state = self.borrow();
|
||||
state.surface.commit();
|
||||
}
|
||||
|
||||
@@ -850,22 +945,173 @@ impl PlatformWindow for WaylandWindow {
|
||||
);
|
||||
}
|
||||
|
||||
fn start_system_move(&self) {
|
||||
fn start_window_move(&self) {
|
||||
let state = self.borrow();
|
||||
let serial = state.client.get_serial(SerialKind::MousePress);
|
||||
state.toplevel._move(&state.globals.seat, serial);
|
||||
}
|
||||
|
||||
fn should_render_window_controls(&self) -> bool {
|
||||
self.borrow().decoration_state == WaylandDecorationState::Client
|
||||
fn start_window_resize(&self, edge: crate::ResizeEdge) {
|
||||
let state = self.borrow();
|
||||
state.toplevel.resize(
|
||||
&state.globals.seat,
|
||||
state.client.get_serial(SerialKind::MousePress),
|
||||
edge.to_xdg(),
|
||||
)
|
||||
}
|
||||
|
||||
fn window_decorations(&self) -> Decorations {
|
||||
let state = self.borrow();
|
||||
match state.decorations {
|
||||
WindowDecorations::Server => Decorations::Server,
|
||||
WindowDecorations::Client => Decorations::Client {
|
||||
tiling: state.tiling,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn request_decorations(&self, decorations: WindowDecorations) {
|
||||
let mut state = self.borrow_mut();
|
||||
state.decorations = decorations;
|
||||
if let Some(decoration) = state.decoration.as_ref() {
|
||||
decoration.set_mode(decorations.to_xdg());
|
||||
update_window(state);
|
||||
}
|
||||
}
|
||||
|
||||
fn window_controls(&self) -> WindowControls {
|
||||
self.borrow().window_controls
|
||||
}
|
||||
|
||||
fn set_client_inset(&self, inset: Pixels) {
|
||||
let mut state = self.borrow_mut();
|
||||
if Some(inset) != state.inset {
|
||||
state.inset = Some(inset);
|
||||
update_window(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum WaylandDecorationState {
|
||||
/// Decorations are to be provided by the client
|
||||
Client,
|
||||
fn update_window(mut state: RefMut<WaylandWindowState>) {
|
||||
let opaque = !state.is_transparent();
|
||||
|
||||
/// Decorations are provided by the server
|
||||
Server,
|
||||
state.renderer.update_transparency(!opaque);
|
||||
let mut opaque_area = state.window_bounds.map(|v| v.0 as i32);
|
||||
if let Some(inset) = state.inset {
|
||||
opaque_area.inset(inset.0 as i32);
|
||||
}
|
||||
|
||||
let region = state
|
||||
.globals
|
||||
.compositor
|
||||
.create_region(&state.globals.qh, ());
|
||||
region.add(
|
||||
opaque_area.origin.x,
|
||||
opaque_area.origin.y,
|
||||
opaque_area.size.width,
|
||||
opaque_area.size.height,
|
||||
);
|
||||
|
||||
// Note that rounded corners make this rectangle API hard to work with.
|
||||
// As this is common when using CSD, let's just disable this API.
|
||||
if state.background_appearance == WindowBackgroundAppearance::Opaque
|
||||
&& state.decorations == WindowDecorations::Server
|
||||
{
|
||||
// Promise the compositor that this region of the window surface
|
||||
// contains no transparent pixels. This allows the compositor to
|
||||
// do skip whatever is behind the surface for better performance.
|
||||
state.surface.set_opaque_region(Some(®ion));
|
||||
} else {
|
||||
state.surface.set_opaque_region(None);
|
||||
}
|
||||
|
||||
if let Some(ref blur_manager) = state.globals.blur_manager {
|
||||
if state.background_appearance == WindowBackgroundAppearance::Blurred {
|
||||
if state.blur.is_none() {
|
||||
let blur = blur_manager.create(&state.surface, &state.globals.qh, ());
|
||||
blur.set_region(Some(®ion));
|
||||
state.blur = Some(blur);
|
||||
}
|
||||
state.blur.as_ref().unwrap().commit();
|
||||
} else {
|
||||
// It probably doesn't hurt to clear the blur for opaque windows
|
||||
blur_manager.unset(&state.surface);
|
||||
if let Some(b) = state.blur.take() {
|
||||
b.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
region.destroy();
|
||||
}
|
||||
|
||||
impl WindowDecorations {
|
||||
fn to_xdg(&self) -> zxdg_toplevel_decoration_v1::Mode {
|
||||
match self {
|
||||
WindowDecorations::Client => zxdg_toplevel_decoration_v1::Mode::ClientSide,
|
||||
WindowDecorations::Server => zxdg_toplevel_decoration_v1::Mode::ServerSide,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResizeEdge {
|
||||
fn to_xdg(&self) -> xdg_toplevel::ResizeEdge {
|
||||
match self {
|
||||
ResizeEdge::Top => xdg_toplevel::ResizeEdge::Top,
|
||||
ResizeEdge::TopRight => xdg_toplevel::ResizeEdge::TopRight,
|
||||
ResizeEdge::Right => xdg_toplevel::ResizeEdge::Right,
|
||||
ResizeEdge::BottomRight => xdg_toplevel::ResizeEdge::BottomRight,
|
||||
ResizeEdge::Bottom => xdg_toplevel::ResizeEdge::Bottom,
|
||||
ResizeEdge::BottomLeft => xdg_toplevel::ResizeEdge::BottomLeft,
|
||||
ResizeEdge::Left => xdg_toplevel::ResizeEdge::Left,
|
||||
ResizeEdge::TopLeft => xdg_toplevel::ResizeEdge::TopLeft,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The configuration event is in terms of the window geometry, which we are constantly
|
||||
/// updating to account for the client decorations. But that's not the area we want to render
|
||||
/// to, due to our intrusize CSD. So, here we calculate the 'actual' size, by adding back in the insets
|
||||
fn compute_outer_size(
|
||||
inset: Option<Pixels>,
|
||||
new_size: Option<Size<Pixels>>,
|
||||
tiling: Tiling,
|
||||
) -> Option<Size<Pixels>> {
|
||||
let Some(inset) = inset else { return new_size };
|
||||
|
||||
new_size.map(|mut new_size| {
|
||||
if !tiling.top {
|
||||
new_size.height += inset;
|
||||
}
|
||||
if !tiling.bottom {
|
||||
new_size.height += inset;
|
||||
}
|
||||
if !tiling.left {
|
||||
new_size.width += inset;
|
||||
}
|
||||
if !tiling.right {
|
||||
new_size.width += inset;
|
||||
}
|
||||
|
||||
new_size
|
||||
})
|
||||
}
|
||||
|
||||
fn inset_by_tiling(mut bounds: Bounds<Pixels>, inset: Pixels, tiling: Tiling) -> Bounds<Pixels> {
|
||||
if !tiling.top {
|
||||
bounds.origin.y += inset;
|
||||
bounds.size.height -= inset;
|
||||
}
|
||||
if !tiling.bottom {
|
||||
bounds.size.height -= inset;
|
||||
}
|
||||
if !tiling.left {
|
||||
bounds.origin.x += inset;
|
||||
bounds.size.width -= inset;
|
||||
}
|
||||
if !tiling.right {
|
||||
bounds.size.width -= inset;
|
||||
}
|
||||
|
||||
bounds
|
||||
}
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashSet;
|
||||
use std::ops::Deref;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use anyhow::Context;
|
||||
use async_task::Runnable;
|
||||
use calloop::channel::Channel;
|
||||
use calloop::generic::{FdWrapper, Generic};
|
||||
use calloop::{EventLoop, LoopHandle, RegistrationToken};
|
||||
|
||||
use collections::HashMap;
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use mio::{Interest, Token, Waker};
|
||||
use util::ResultExt;
|
||||
|
||||
use x11rb::connection::{Connection, RequestConnection};
|
||||
use x11rb::cursor;
|
||||
use x11rb::errors::ConnectionError;
|
||||
use x11rb::protocol::randr::ConnectionExt as _;
|
||||
use x11rb::protocol::xinput::ConnectionExt;
|
||||
use x11rb::protocol::xkb::ConnectionExt as _;
|
||||
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _};
|
||||
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _, KeyPressEvent};
|
||||
use x11rb::protocol::{randr, render, xinput, xkb, xproto, Event};
|
||||
use x11rb::resource_manager::Database;
|
||||
use x11rb::xcb_ffi::XCBConnection;
|
||||
@@ -33,24 +30,24 @@ use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||
use crate::{
|
||||
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle,
|
||||
DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput,
|
||||
Point, QuitSignal, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
|
||||
DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, Platform, PlatformDisplay,
|
||||
PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
|
||||
};
|
||||
|
||||
use super::{
|
||||
super::{get_xkb_compose_state, open_uri_internal, SCROLL_LINES},
|
||||
X11Display, X11WindowStatePtr, XcbAtoms,
|
||||
};
|
||||
use super::{button_of_key, modifiers_from_state, pressed_button_from_mask};
|
||||
use super::{X11Display, X11WindowStatePtr, XcbAtoms};
|
||||
use super::{XimCallbackEvent, XimHandler};
|
||||
use crate::platform::linux::is_within_click_distance;
|
||||
use crate::platform::linux::platform::DOUBLE_CLICK_INTERVAL;
|
||||
use crate::platform::linux::platform::{DOUBLE_CLICK_INTERVAL, SCROLL_LINES};
|
||||
use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
|
||||
use crate::platform::linux::{
|
||||
get_xkb_compose_state, is_within_click_distance, open_uri_internal, reveal_path_internal,
|
||||
};
|
||||
|
||||
pub(super) const XINPUT_MASTER_DEVICE: u16 = 1;
|
||||
|
||||
pub(crate) struct WindowRef {
|
||||
window: X11WindowStatePtr,
|
||||
refresh_event_token: RegistrationToken,
|
||||
}
|
||||
|
||||
impl WindowRef {
|
||||
@@ -98,19 +95,17 @@ impl From<xim::ClientError> for EventHandlerError {
|
||||
}
|
||||
|
||||
pub struct X11ClientState {
|
||||
/// poll is in an Option so we can take it out in `run()` without
|
||||
/// mutating self.
|
||||
poll: Option<mio::Poll>,
|
||||
quit_signal_rx: oneshot::Receiver<()>,
|
||||
runnables: Channel<Runnable>,
|
||||
xdp_event_source: XDPEventSource,
|
||||
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
|
||||
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
|
||||
|
||||
pub(crate) last_click: Instant,
|
||||
pub(crate) last_location: Point<Pixels>,
|
||||
pub(crate) current_count: usize,
|
||||
|
||||
pub(crate) scale_factor: f32,
|
||||
|
||||
pub(crate) xcb_connection: Rc<XCBConnection>,
|
||||
client_side_decorations_supported: bool,
|
||||
pub(crate) x_root_index: usize,
|
||||
pub(crate) _resource_database: Database,
|
||||
pub(crate) atoms: XcbAtoms,
|
||||
@@ -124,6 +119,7 @@ pub struct X11ClientState {
|
||||
pub(crate) compose_state: Option<xkbc::compose::State>,
|
||||
pub(crate) pre_edit_text: Option<String>,
|
||||
pub(crate) composing: bool,
|
||||
pub(crate) pre_ime_key_down: Option<Keystroke>,
|
||||
pub(crate) cursor_handle: cursor::Handle,
|
||||
pub(crate) cursor_styles: HashMap<xproto::Window, CursorStyle>,
|
||||
pub(crate) cursor_cache: HashMap<CursorStyle, xproto::Cursor>,
|
||||
@@ -145,46 +141,14 @@ impl X11ClientStatePtr {
|
||||
let client = X11Client(self.0.upgrade().expect("client already dropped"));
|
||||
let mut state = client.0.borrow_mut();
|
||||
|
||||
if state.windows.remove(&x_window).is_none() {
|
||||
log::warn!(
|
||||
"failed to remove X window {} from client state, does not exist",
|
||||
x_window
|
||||
);
|
||||
if let Some(window_ref) = state.windows.remove(&x_window) {
|
||||
state.loop_handle.remove(window_ref.refresh_event_token);
|
||||
}
|
||||
|
||||
state.cursor_styles.remove(&x_window);
|
||||
|
||||
if state.windows.is_empty() {
|
||||
state.common.quit_signal.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelQuitSignal {
|
||||
tx: Option<oneshot::Sender<()>>,
|
||||
waker: Option<Arc<Waker>>,
|
||||
}
|
||||
|
||||
impl ChannelQuitSignal {
|
||||
fn new(waker: Option<Arc<Waker>>) -> (Self, oneshot::Receiver<()>) {
|
||||
let (tx, rx) = oneshot::channel::<()>();
|
||||
|
||||
let quit_signal = ChannelQuitSignal {
|
||||
tx: Some(tx),
|
||||
waker,
|
||||
};
|
||||
|
||||
(quit_signal, rx)
|
||||
}
|
||||
}
|
||||
|
||||
impl QuitSignal for ChannelQuitSignal {
|
||||
fn quit(&mut self) {
|
||||
if let Some(tx) = self.tx.take() {
|
||||
tx.send(()).log_err();
|
||||
if let Some(waker) = self.waker.as_ref() {
|
||||
waker.wake().ok();
|
||||
}
|
||||
state.common.signal.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,12 +158,27 @@ pub(crate) struct X11Client(Rc<RefCell<X11ClientState>>);
|
||||
|
||||
impl X11Client {
|
||||
pub(crate) fn new() -> Self {
|
||||
let mut poll = mio::Poll::new().unwrap();
|
||||
let event_loop = EventLoop::try_new().unwrap();
|
||||
|
||||
let waker = Arc::new(Waker::new(poll.registry(), WAKER_TOKEN).unwrap());
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
|
||||
let (quit_signal, quit_signal_rx) = ChannelQuitSignal::new(Some(waker.clone()));
|
||||
let (common, runnables) = LinuxCommon::new(Box::new(quit_signal), Some(waker.clone()));
|
||||
let handle = event_loop.handle();
|
||||
|
||||
handle
|
||||
.insert_source(main_receiver, {
|
||||
let handle = handle.clone();
|
||||
move |event, _, _: &mut X11Client| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
// Insert the runnables as idle callbacks, so we make sure that user-input and X11
|
||||
// events have higher priority and runnables are only worked off after the event
|
||||
// callbacks.
|
||||
handle.insert_idle(|_| {
|
||||
runnable.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap();
|
||||
xcb_connection
|
||||
@@ -241,13 +220,25 @@ impl X11Client {
|
||||
.map(|class| *class)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let atoms = XcbAtoms::new(&xcb_connection).unwrap();
|
||||
let atoms = XcbAtoms::new(&xcb_connection).unwrap().reply().unwrap();
|
||||
|
||||
let root = xcb_connection.setup().roots[0].root;
|
||||
let compositor_present = check_compositor_present(&xcb_connection, root);
|
||||
let gtk_frame_extents_supported =
|
||||
check_gtk_frame_extents_supported(&xcb_connection, &atoms, root);
|
||||
let client_side_decorations_supported = compositor_present && gtk_frame_extents_supported;
|
||||
log::info!(
|
||||
"x11: compositor present: {}, gtk_frame_extents_supported: {}",
|
||||
compositor_present,
|
||||
gtk_frame_extents_supported
|
||||
);
|
||||
|
||||
let xkb = xcb_connection
|
||||
.xkb_use_extension(XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
|
||||
let atoms = atoms.reply().unwrap();
|
||||
let xkb = xkb.reply().unwrap();
|
||||
let events = xkb::EventType::STATE_NOTIFY;
|
||||
xcb_connection
|
||||
.xkb_select_events(
|
||||
@@ -298,24 +289,54 @@ impl X11Client {
|
||||
None
|
||||
};
|
||||
|
||||
let xdp_event_source =
|
||||
XDPEventSource::new(&common.background_executor, Some(waker.clone()));
|
||||
// Safety: Safe if xcb::Connection always returns a valid fd
|
||||
let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) };
|
||||
|
||||
handle
|
||||
.insert_source(
|
||||
Generic::new_with_error::<EventHandlerError>(
|
||||
fd,
|
||||
calloop::Interest::READ,
|
||||
calloop::Mode::Level,
|
||||
),
|
||||
{
|
||||
let xcb_connection = xcb_connection.clone();
|
||||
move |_readiness, _, client| {
|
||||
client.process_x11_events(&xcb_connection)?;
|
||||
Ok(calloop::PostAction::Continue)
|
||||
}
|
||||
},
|
||||
)
|
||||
.expect("Failed to initialize x11 event source");
|
||||
|
||||
handle
|
||||
.insert_source(XDPEventSource::new(&common.background_executor), {
|
||||
move |event, _, client| match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
client.with_common(|common| common.appearance = appearance);
|
||||
for (_, window) in &mut client.0.borrow_mut().windows {
|
||||
window.window.set_appearance(appearance);
|
||||
}
|
||||
}
|
||||
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
|
||||
// noop, X11 manages this for us.
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
X11Client(Rc::new(RefCell::new(X11ClientState {
|
||||
poll: Some(poll),
|
||||
runnables,
|
||||
|
||||
xdp_event_source,
|
||||
quit_signal_rx,
|
||||
common,
|
||||
|
||||
modifiers: Modifiers::default(),
|
||||
event_loop: Some(event_loop),
|
||||
loop_handle: handle,
|
||||
common,
|
||||
last_click: Instant::now(),
|
||||
last_location: Point::new(px(0.0), px(0.0)),
|
||||
current_count: 0,
|
||||
scale_factor,
|
||||
|
||||
xcb_connection,
|
||||
client_side_decorations_supported,
|
||||
x_root_index,
|
||||
_resource_database: resource_database,
|
||||
atoms,
|
||||
@@ -327,6 +348,7 @@ impl X11Client {
|
||||
|
||||
compose_state,
|
||||
pre_edit_text: None,
|
||||
pre_ime_key_down: None,
|
||||
composing: false,
|
||||
|
||||
cursor_handle,
|
||||
@@ -342,6 +364,125 @@ impl X11Client {
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn process_x11_events(
|
||||
&self,
|
||||
xcb_connection: &XCBConnection,
|
||||
) -> Result<(), EventHandlerError> {
|
||||
loop {
|
||||
let mut events = Vec::new();
|
||||
let mut windows_to_refresh = HashSet::new();
|
||||
|
||||
let mut last_key_release = None;
|
||||
let mut last_key_press: Option<KeyPressEvent> = None;
|
||||
|
||||
loop {
|
||||
match xcb_connection.poll_for_event() {
|
||||
Ok(Some(event)) => {
|
||||
match event {
|
||||
Event::Expose(expose_event) => {
|
||||
windows_to_refresh.insert(expose_event.window);
|
||||
}
|
||||
Event::KeyRelease(_) => {
|
||||
last_key_release = Some(event);
|
||||
}
|
||||
Event::KeyPress(key_press) => {
|
||||
if let Some(last_press) = last_key_press.as_ref() {
|
||||
if last_press.detail == key_press.detail {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Event::KeyRelease(key_release)) =
|
||||
last_key_release.take()
|
||||
{
|
||||
// We ignore that last KeyRelease if it's too close to this KeyPress,
|
||||
// suggesting that it's auto-generated by X11 as a key-repeat event.
|
||||
if key_release.detail != key_press.detail
|
||||
|| key_press.time.saturating_sub(key_release.time) > 20
|
||||
{
|
||||
events.push(Event::KeyRelease(key_release));
|
||||
}
|
||||
}
|
||||
events.push(Event::KeyPress(key_press));
|
||||
last_key_press = Some(key_press);
|
||||
}
|
||||
_ => {
|
||||
if let Some(release_event) = last_key_release.take() {
|
||||
events.push(release_event);
|
||||
}
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// Add any remaining stored KeyRelease event
|
||||
if let Some(release_event) = last_key_release.take() {
|
||||
events.push(release_event);
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("error polling for X11 events: {e:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if events.is_empty() && windows_to_refresh.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for window in windows_to_refresh.into_iter() {
|
||||
if let Some(window) = self.get_window(window) {
|
||||
window.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
for event in events.into_iter() {
|
||||
let mut state = self.0.borrow_mut();
|
||||
if state.ximc.is_none() || state.xim_handler.is_none() {
|
||||
drop(state);
|
||||
self.handle_event(event);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut ximc = state.ximc.take().unwrap();
|
||||
let mut xim_handler = state.xim_handler.take().unwrap();
|
||||
let xim_connected = xim_handler.connected;
|
||||
drop(state);
|
||||
|
||||
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
|
||||
Ok(handled) => handled,
|
||||
Err(err) => {
|
||||
log::error!("XIMClientError: {}", err);
|
||||
false
|
||||
}
|
||||
};
|
||||
let xim_callback_event = xim_handler.last_callback_event.take();
|
||||
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.ximc = Some(ximc);
|
||||
state.xim_handler = Some(xim_handler);
|
||||
drop(state);
|
||||
|
||||
if let Some(event) = xim_callback_event {
|
||||
self.handle_xim_callback_event(event);
|
||||
}
|
||||
|
||||
if xim_filtered {
|
||||
continue;
|
||||
}
|
||||
|
||||
if xim_connected {
|
||||
self.xim_handle_event(event);
|
||||
} else {
|
||||
self.handle_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn enable_ime(&self) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
if state.ximc.is_none() {
|
||||
@@ -404,115 +545,11 @@ impl X11Client {
|
||||
.map(|window_reference| window_reference.window.clone())
|
||||
}
|
||||
|
||||
fn read_x11_events(&self) -> (HashSet<u32>, Vec<Event>) {
|
||||
let mut events = Vec::new();
|
||||
let mut windows_to_refresh = HashSet::new();
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let mut last_key_release: Option<Event> = None;
|
||||
|
||||
loop {
|
||||
match state.xcb_connection.poll_for_event() {
|
||||
Ok(Some(event)) => {
|
||||
if let Event::Expose(expose_event) = event {
|
||||
windows_to_refresh.insert(expose_event.window);
|
||||
} else {
|
||||
match event {
|
||||
Event::KeyRelease(_) => {
|
||||
last_key_release = Some(event);
|
||||
}
|
||||
Event::KeyPress(key_press) => {
|
||||
if let Some(Event::KeyRelease(key_release)) =
|
||||
last_key_release.take()
|
||||
{
|
||||
// We ignore that last KeyRelease if it's too close to this KeyPress,
|
||||
// suggesting that it's auto-generated by X11 as a key-repeat event.
|
||||
if key_release.detail != key_press.detail
|
||||
|| key_press.time.wrapping_sub(key_release.time) > 20
|
||||
{
|
||||
events.push(Event::KeyRelease(key_release));
|
||||
}
|
||||
}
|
||||
events.push(Event::KeyPress(key_press));
|
||||
}
|
||||
_ => {
|
||||
if let Some(release_event) = last_key_release.take() {
|
||||
events.push(release_event);
|
||||
}
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// Add any remaining stored KeyRelease event
|
||||
if let Some(release_event) = last_key_release.take() {
|
||||
events.push(release_event);
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("error polling for X11 events: {e:?}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(windows_to_refresh, events)
|
||||
}
|
||||
|
||||
fn process_x11_events(&self, events: Vec<Event>) {
|
||||
for event in events.into_iter() {
|
||||
let mut state = self.0.borrow_mut();
|
||||
if state.ximc.is_none() || state.xim_handler.is_none() {
|
||||
drop(state);
|
||||
self.handle_event(event);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut ximc = state.ximc.take().unwrap();
|
||||
let mut xim_handler = state.xim_handler.take().unwrap();
|
||||
let xim_connected = xim_handler.connected;
|
||||
drop(state);
|
||||
|
||||
// let xim_filtered = false;
|
||||
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
|
||||
Ok(handled) => handled,
|
||||
Err(err) => {
|
||||
log::error!("XIMClientError: {}", err);
|
||||
false
|
||||
}
|
||||
};
|
||||
let xim_callback_event = xim_handler.last_callback_event.take();
|
||||
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.ximc = Some(ximc);
|
||||
state.xim_handler = Some(xim_handler);
|
||||
|
||||
if let Some(event) = xim_callback_event {
|
||||
drop(state);
|
||||
self.handle_xim_callback_event(event);
|
||||
} else {
|
||||
drop(state);
|
||||
}
|
||||
|
||||
if xim_filtered {
|
||||
continue;
|
||||
}
|
||||
|
||||
if xim_connected {
|
||||
self.xim_handle_event(event);
|
||||
} else {
|
||||
self.handle_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event(&self, event: Event) -> Option<()> {
|
||||
match event {
|
||||
Event::ClientMessage(event) => {
|
||||
let window = self.get_window(event.window)?;
|
||||
let [atom, ..] = event.data.as_data32();
|
||||
let [atom, _arg1, arg2, arg3, _arg4] = event.data.as_data32();
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
if atom == state.atoms.WM_DELETE_WINDOW {
|
||||
@@ -521,6 +558,12 @@ impl X11Client {
|
||||
// Rest of the close logic is handled in drop_window()
|
||||
window.close();
|
||||
}
|
||||
} else if atom == state.atoms._NET_WM_SYNC_REQUEST {
|
||||
window.state.borrow_mut().last_sync_counter =
|
||||
Some(x11rb::protocol::sync::Int64 {
|
||||
lo: arg2,
|
||||
hi: arg3 as i32,
|
||||
})
|
||||
}
|
||||
}
|
||||
Event::ConfigureNotify(event) => {
|
||||
@@ -537,9 +580,9 @@ impl X11Client {
|
||||
let window = self.get_window(event.window)?;
|
||||
window.configure(bounds);
|
||||
}
|
||||
Event::Expose(event) => {
|
||||
Event::PropertyNotify(event) => {
|
||||
let window = self.get_window(event.window)?;
|
||||
window.refresh();
|
||||
window.property_notify(event);
|
||||
}
|
||||
Event::FocusIn(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
@@ -568,8 +611,8 @@ impl X11Client {
|
||||
event.base_mods.into(),
|
||||
event.latched_mods.into(),
|
||||
event.locked_mods.into(),
|
||||
0,
|
||||
0,
|
||||
event.base_group as u32,
|
||||
event.latched_group as u32,
|
||||
event.locked_group.into(),
|
||||
);
|
||||
|
||||
@@ -593,6 +636,7 @@ impl X11Client {
|
||||
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
state.modifiers = modifiers;
|
||||
state.pre_ime_key_down.take();
|
||||
|
||||
let keystroke = {
|
||||
let code = event.detail.into();
|
||||
@@ -807,10 +851,15 @@ impl X11Client {
|
||||
|
||||
if let Some(old_scroll) = old_scroll {
|
||||
let delta_scroll = old_scroll - new_scroll;
|
||||
let (x, y) = if !modifiers.shift {
|
||||
(0.0, delta_scroll)
|
||||
} else {
|
||||
(delta_scroll, 0.0)
|
||||
};
|
||||
window.handle_input(PlatformInput::ScrollWheel(
|
||||
crate::ScrollWheelEvent {
|
||||
position,
|
||||
delta: ScrollDelta::Lines(Point::new(0.0, delta_scroll)),
|
||||
delta: ScrollDelta::Lines(Point::new(x, y)),
|
||||
modifiers,
|
||||
touch_phase: TouchPhase::default(),
|
||||
},
|
||||
@@ -867,6 +916,11 @@ impl X11Client {
|
||||
match event {
|
||||
Event::KeyPress(event) | Event::KeyRelease(event) => {
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.pre_ime_key_down = Some(Keystroke::from_xkb(
|
||||
&state.xkb,
|
||||
state.modifiers,
|
||||
event.detail.into(),
|
||||
));
|
||||
let mut ximc = state.ximc.take().unwrap();
|
||||
let mut xim_handler = state.xim_handler.take().unwrap();
|
||||
drop(state);
|
||||
@@ -893,6 +947,16 @@ impl X11Client {
|
||||
fn xim_handle_commit(&self, window: xproto::Window, text: String) -> Option<()> {
|
||||
let window = self.get_window(window).unwrap();
|
||||
let mut state = self.0.borrow_mut();
|
||||
if !state.composing {
|
||||
if let Some(keystroke) = state.pre_ime_key_down.take() {
|
||||
drop(state);
|
||||
window.handle_input(PlatformInput::KeyDown(crate::KeyDownEvent {
|
||||
keystroke,
|
||||
is_held: false,
|
||||
}));
|
||||
return Some(());
|
||||
}
|
||||
}
|
||||
state.composing = false;
|
||||
drop(state);
|
||||
|
||||
@@ -942,13 +1006,11 @@ impl X11Client {
|
||||
}
|
||||
}
|
||||
|
||||
const XCB_CONNECTION_TOKEN: Token = Token(0);
|
||||
const WAKER_TOKEN: Token = Token(1);
|
||||
|
||||
impl LinuxClient for X11Client {
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
"X11"
|
||||
}
|
||||
|
||||
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
|
||||
f(&mut self.0.borrow_mut().common)
|
||||
}
|
||||
@@ -1007,6 +1069,7 @@ impl LinuxClient for X11Client {
|
||||
state.common.foreground_executor.clone(),
|
||||
params,
|
||||
&state.xcb_connection,
|
||||
state.client_side_decorations_supported,
|
||||
state.x_root_index,
|
||||
x_window,
|
||||
&state.atoms,
|
||||
@@ -1014,8 +1077,61 @@ impl LinuxClient for X11Client {
|
||||
state.common.appearance,
|
||||
)?;
|
||||
|
||||
let screen_resources = state
|
||||
.xcb_connection
|
||||
.randr_get_screen_resources(x_window)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.expect("Could not find available screens");
|
||||
|
||||
let mode = screen_resources
|
||||
.crtcs
|
||||
.iter()
|
||||
.find_map(|crtc| {
|
||||
let crtc_info = state
|
||||
.xcb_connection
|
||||
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
|
||||
.ok()?
|
||||
.reply()
|
||||
.ok()?;
|
||||
|
||||
screen_resources
|
||||
.modes
|
||||
.iter()
|
||||
.find(|m| m.id == crtc_info.mode)
|
||||
})
|
||||
.expect("Unable to find screen refresh rate");
|
||||
|
||||
let refresh_event_token = state
|
||||
.loop_handle
|
||||
.insert_source(calloop::timer::Timer::immediate(), {
|
||||
let refresh_duration = mode_refresh_rate(mode);
|
||||
move |mut instant, (), client| {
|
||||
let xcb_connection = {
|
||||
let state = client.0.borrow_mut();
|
||||
let xcb_connection = state.xcb_connection.clone();
|
||||
if let Some(window) = state.windows.get(&x_window) {
|
||||
let window = window.window.clone();
|
||||
drop(state);
|
||||
window.refresh();
|
||||
}
|
||||
xcb_connection
|
||||
};
|
||||
client.process_x11_events(&xcb_connection).log_err();
|
||||
|
||||
// Take into account that some frames have been skipped
|
||||
let now = Instant::now();
|
||||
while instant < now {
|
||||
instant += refresh_duration;
|
||||
}
|
||||
calloop::timer::TimeoutAction::ToInstant(instant)
|
||||
}
|
||||
})
|
||||
.expect("Failed to initialize refresh timer");
|
||||
|
||||
let window_ref = WindowRef {
|
||||
window: window.0.clone(),
|
||||
refresh_event_token,
|
||||
};
|
||||
|
||||
state.windows.insert(x_window, window_ref);
|
||||
@@ -1057,11 +1173,17 @@ impl LinuxClient for X11Client {
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("failed to change window cursor");
|
||||
.expect("failed to change window cursor")
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn open_uri(&self, uri: &str) {
|
||||
open_uri_internal(uri, None);
|
||||
open_uri_internal(self.background_executor(), uri, None);
|
||||
}
|
||||
|
||||
fn reveal_path(&self, path: PathBuf) {
|
||||
reveal_path_internal(self.background_executor(), path, None);
|
||||
}
|
||||
|
||||
fn write_to_primary(&self, item: crate::ClipboardItem) {
|
||||
@@ -1138,123 +1260,14 @@ impl LinuxClient for X11Client {
|
||||
}
|
||||
|
||||
fn run(&self) {
|
||||
let mut poll = self
|
||||
let mut event_loop = self
|
||||
.0
|
||||
.borrow_mut()
|
||||
.poll
|
||||
.event_loop
|
||||
.take()
|
||||
.context("no poll set on X11Client. calling run more than once is not possible")
|
||||
.unwrap();
|
||||
.expect("App is already running");
|
||||
|
||||
let xcb_fd = self.0.borrow().xcb_connection.as_raw_fd();
|
||||
let mut xcb_source = mio::unix::SourceFd(&xcb_fd);
|
||||
poll.registry()
|
||||
.register(&mut xcb_source, XCB_CONNECTION_TOKEN, Interest::READABLE)
|
||||
.unwrap();
|
||||
|
||||
let mut events = mio::Events::with_capacity(1024);
|
||||
let mut next_refresh_needed = Instant::now();
|
||||
|
||||
'run_loop: loop {
|
||||
let poll_timeout = next_refresh_needed - Instant::now();
|
||||
// We rounding the poll_timeout down so `mio` doesn't round it up to the next higher milliseconds
|
||||
let poll_timeout = Duration::from_millis(poll_timeout.as_millis() as u64);
|
||||
|
||||
if poll_timeout >= Duration::from_millis(1) {
|
||||
let _ = poll.poll(&mut events, Some(poll_timeout));
|
||||
};
|
||||
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
// Check if we need to quit
|
||||
if let Ok(Some(())) = state.quit_signal_rx.try_recv() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Redraw windows
|
||||
let now = Instant::now();
|
||||
if now > next_refresh_needed {
|
||||
// This will be pulled down to 16ms (or less) if a window is open
|
||||
let mut frame_length = Duration::from_millis(100);
|
||||
|
||||
let mut windows = vec![];
|
||||
for (_, window_ref) in state.windows.iter() {
|
||||
if !window_ref.window.state.borrow().destroyed {
|
||||
frame_length = frame_length.min(window_ref.window.refresh_rate());
|
||||
windows.push(window_ref.window.clone());
|
||||
}
|
||||
}
|
||||
|
||||
drop(state);
|
||||
|
||||
for window in windows {
|
||||
window.refresh();
|
||||
}
|
||||
|
||||
state = self.0.borrow_mut();
|
||||
|
||||
// In the case that we're looping a bit too fast, slow down
|
||||
next_refresh_needed = now.max(next_refresh_needed) + frame_length;
|
||||
}
|
||||
|
||||
// X11 events
|
||||
drop(state);
|
||||
|
||||
loop {
|
||||
let (x_windows, events) = self.read_x11_events();
|
||||
for x_window in x_windows {
|
||||
if let Some(window) = self.get_window(x_window) {
|
||||
window.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
if events.len() == 0 {
|
||||
break;
|
||||
}
|
||||
self.process_x11_events(events);
|
||||
|
||||
// When X11 is sending us events faster than we can handle we'll
|
||||
// let the frame rate drop to 10fps to try and avoid getting too behind.
|
||||
if Instant::now() > next_refresh_needed + Duration::from_millis(80) {
|
||||
continue 'run_loop;
|
||||
}
|
||||
}
|
||||
|
||||
state = self.0.borrow_mut();
|
||||
|
||||
// Runnables
|
||||
while let Ok(runnable) = state.runnables.try_recv() {
|
||||
drop(state);
|
||||
runnable.run();
|
||||
state = self.0.borrow_mut();
|
||||
|
||||
if Instant::now() + Duration::from_millis(1) >= next_refresh_needed {
|
||||
continue 'run_loop;
|
||||
}
|
||||
}
|
||||
|
||||
// XDG events
|
||||
if let Ok(event) = state.xdp_event_source.try_recv() {
|
||||
match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
let mut windows = state
|
||||
.windows
|
||||
.values()
|
||||
.map(|window| window.window.clone())
|
||||
.collect::<Vec<_>>();
|
||||
drop(state);
|
||||
|
||||
self.with_common(|common| common.appearance = appearance);
|
||||
for mut window in windows {
|
||||
window.set_appearance(appearance);
|
||||
}
|
||||
}
|
||||
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
|
||||
// noop, X11 manages this for us.
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
@@ -1268,6 +1281,120 @@ impl LinuxClient for X11Client {
|
||||
}
|
||||
}
|
||||
|
||||
// Adatpted from:
|
||||
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
|
||||
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
|
||||
if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 {
|
||||
return Duration::from_millis(16);
|
||||
}
|
||||
|
||||
let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
|
||||
let micros = 1_000_000_000 / millihertz;
|
||||
log::info!("Refreshing at {} micros", micros);
|
||||
Duration::from_micros(micros)
|
||||
}
|
||||
|
||||
fn fp3232_to_f32(value: xinput::Fp3232) -> f32 {
|
||||
value.integral as f32 + value.frac as f32 / u32::MAX as f32
|
||||
}
|
||||
|
||||
fn check_compositor_present(xcb_connection: &XCBConnection, root: u32) -> bool {
|
||||
// Method 1: Check for _NET_WM_CM_S{root}
|
||||
let atom_name = format!("_NET_WM_CM_S{}", root);
|
||||
let atom = xcb_connection
|
||||
.intern_atom(false, atom_name.as_bytes())
|
||||
.unwrap()
|
||||
.reply()
|
||||
.map(|reply| reply.atom)
|
||||
.unwrap_or(0);
|
||||
|
||||
let method1 = if atom != 0 {
|
||||
xcb_connection
|
||||
.get_selection_owner(atom)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.map(|reply| reply.owner != 0)
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Method 2: Check for _NET_WM_CM_OWNER
|
||||
let atom_name = "_NET_WM_CM_OWNER";
|
||||
let atom = xcb_connection
|
||||
.intern_atom(false, atom_name.as_bytes())
|
||||
.unwrap()
|
||||
.reply()
|
||||
.map(|reply| reply.atom)
|
||||
.unwrap_or(0);
|
||||
|
||||
let method2 = if atom != 0 {
|
||||
xcb_connection
|
||||
.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.map(|reply| reply.value_len > 0)
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Method 3: Check for _NET_SUPPORTING_WM_CHECK
|
||||
let atom_name = "_NET_SUPPORTING_WM_CHECK";
|
||||
let atom = xcb_connection
|
||||
.intern_atom(false, atom_name.as_bytes())
|
||||
.unwrap()
|
||||
.reply()
|
||||
.map(|reply| reply.atom)
|
||||
.unwrap_or(0);
|
||||
|
||||
let method3 = if atom != 0 {
|
||||
xcb_connection
|
||||
.get_property(false, root, atom, xproto::AtomEnum::WINDOW, 0, 1)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.map(|reply| reply.value_len > 0)
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// TODO: Remove this
|
||||
log::info!(
|
||||
"Compositor detection: _NET_WM_CM_S?={}, _NET_WM_CM_OWNER={}, _NET_SUPPORTING_WM_CHECK={}",
|
||||
method1,
|
||||
method2,
|
||||
method3
|
||||
);
|
||||
|
||||
method1 || method2 || method3
|
||||
}
|
||||
|
||||
fn check_gtk_frame_extents_supported(
|
||||
xcb_connection: &XCBConnection,
|
||||
atoms: &XcbAtoms,
|
||||
root: xproto::Window,
|
||||
) -> bool {
|
||||
let supported_atoms = xcb_connection
|
||||
.get_property(
|
||||
false,
|
||||
root,
|
||||
atoms._NET_SUPPORTED,
|
||||
xproto::AtomEnum::ATOM,
|
||||
0,
|
||||
1024,
|
||||
)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.map(|reply| {
|
||||
// Convert Vec<u8> to Vec<u32>
|
||||
reply
|
||||
.value
|
||||
.chunks_exact(4)
|
||||
.map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
|
||||
.collect::<Vec<u32>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
supported_atoms.contains(&atoms._GTK_FRAME_EXTENTS)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ use anyhow::Context;
|
||||
|
||||
use crate::{
|
||||
platform::blade::{BladeRenderer, BladeSurfaceConfig},
|
||||
px, size, AnyWindowHandle, Bounds, DevicePixels, ForegroundExecutor, Modifiers, Pixels,
|
||||
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
|
||||
PromptLevel, Scene, Size, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
|
||||
WindowKind, WindowParams, X11ClientStatePtr,
|
||||
px, size, AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, Modifiers,
|
||||
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
|
||||
Point, PromptLevel, ResizeEdge, Scene, Size, Tiling, WindowAppearance,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowParams,
|
||||
X11ClientStatePtr,
|
||||
};
|
||||
|
||||
use blade_graphics as gpu;
|
||||
@@ -14,25 +15,17 @@ use util::{maybe, ResultExt};
|
||||
use x11rb::{
|
||||
connection::Connection,
|
||||
protocol::{
|
||||
randr::{self, ConnectionExt as _},
|
||||
sync,
|
||||
xinput::{self, ConnectionExt as _},
|
||||
xproto::{
|
||||
self, ClientMessageEvent, ConnectionExt as _, EventMask, TranslateCoordinatesReply,
|
||||
},
|
||||
xproto::{self, ClientMessageEvent, ConnectionExt, EventMask, TranslateCoordinatesReply},
|
||||
},
|
||||
wrapper::ConnectionExt as _,
|
||||
xcb_ffi::XCBConnection,
|
||||
};
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
ffi::c_void,
|
||||
num::NonZeroU32,
|
||||
ops::Div,
|
||||
ptr::NonNull,
|
||||
rc::Rc,
|
||||
sync::{self, Arc},
|
||||
time::Duration,
|
||||
cell::RefCell, ffi::c_void, mem::size_of, num::NonZeroU32, ops::Div, ptr::NonNull, rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use super::{X11Display, XINPUT_MASTER_DEVICE};
|
||||
@@ -50,10 +43,18 @@ x11rb::atom_manager! {
|
||||
_NET_WM_STATE_HIDDEN,
|
||||
_NET_WM_STATE_FOCUSED,
|
||||
_NET_ACTIVE_WINDOW,
|
||||
_NET_WM_SYNC_REQUEST,
|
||||
_NET_WM_SYNC_REQUEST_COUNTER,
|
||||
_NET_WM_BYPASS_COMPOSITOR,
|
||||
_NET_WM_MOVERESIZE,
|
||||
_NET_WM_WINDOW_TYPE,
|
||||
_NET_WM_WINDOW_TYPE_NOTIFICATION,
|
||||
_NET_WM_SYNC,
|
||||
_NET_SUPPORTED,
|
||||
_MOTIF_WM_HINTS,
|
||||
_GTK_SHOW_WINDOW_MENU,
|
||||
_GTK_FRAME_EXTENTS,
|
||||
_GTK_EDGE_CONSTRAINTS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +71,64 @@ fn query_render_extent(xcb_connection: &XCBConnection, x_window: xproto::Window)
|
||||
}
|
||||
}
|
||||
|
||||
impl ResizeEdge {
|
||||
fn to_moveresize(&self) -> u32 {
|
||||
match self {
|
||||
ResizeEdge::TopLeft => 0,
|
||||
ResizeEdge::Top => 1,
|
||||
ResizeEdge::TopRight => 2,
|
||||
ResizeEdge::Right => 3,
|
||||
ResizeEdge::BottomRight => 4,
|
||||
ResizeEdge::Bottom => 5,
|
||||
ResizeEdge::BottomLeft => 6,
|
||||
ResizeEdge::Left => 7,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct EdgeConstraints {
|
||||
top_tiled: bool,
|
||||
#[allow(dead_code)]
|
||||
top_resizable: bool,
|
||||
|
||||
right_tiled: bool,
|
||||
#[allow(dead_code)]
|
||||
right_resizable: bool,
|
||||
|
||||
bottom_tiled: bool,
|
||||
#[allow(dead_code)]
|
||||
bottom_resizable: bool,
|
||||
|
||||
left_tiled: bool,
|
||||
#[allow(dead_code)]
|
||||
left_resizable: bool,
|
||||
}
|
||||
|
||||
impl EdgeConstraints {
|
||||
fn from_atom(atom: u32) -> Self {
|
||||
EdgeConstraints {
|
||||
top_tiled: (atom & (1 << 0)) != 0,
|
||||
top_resizable: (atom & (1 << 1)) != 0,
|
||||
right_tiled: (atom & (1 << 2)) != 0,
|
||||
right_resizable: (atom & (1 << 3)) != 0,
|
||||
bottom_tiled: (atom & (1 << 4)) != 0,
|
||||
bottom_resizable: (atom & (1 << 5)) != 0,
|
||||
left_tiled: (atom & (1 << 6)) != 0,
|
||||
left_resizable: (atom & (1 << 7)) != 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_tiling(&self) -> Tiling {
|
||||
Tiling {
|
||||
top: self.top_tiled,
|
||||
right: self.right_tiled,
|
||||
bottom: self.bottom_tiled,
|
||||
left: self.left_tiled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Visual {
|
||||
id: xproto::Visualid,
|
||||
@@ -161,11 +220,12 @@ pub struct Callbacks {
|
||||
|
||||
pub struct X11WindowState {
|
||||
pub destroyed: bool,
|
||||
refresh_rate: Duration,
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
atoms: XcbAtoms,
|
||||
x_root_window: xproto::Window,
|
||||
pub(crate) counter_id: sync::Counter,
|
||||
pub(crate) last_sync_counter: Option<sync::Int64>,
|
||||
_raw: RawWindow,
|
||||
bounds: Bounds<Pixels>,
|
||||
scale_factor: f32,
|
||||
@@ -173,7 +233,23 @@ pub struct X11WindowState {
|
||||
display: Rc<dyn PlatformDisplay>,
|
||||
input_handler: Option<PlatformInputHandler>,
|
||||
appearance: WindowAppearance,
|
||||
background_appearance: WindowBackgroundAppearance,
|
||||
maximized_vertical: bool,
|
||||
maximized_horizontal: bool,
|
||||
hidden: bool,
|
||||
active: bool,
|
||||
fullscreen: bool,
|
||||
client_side_decorations_supported: bool,
|
||||
decorations: WindowDecorations,
|
||||
edge_constraints: Option<EdgeConstraints>,
|
||||
pub handle: AnyWindowHandle,
|
||||
last_insets: [u32; 4],
|
||||
}
|
||||
|
||||
impl X11WindowState {
|
||||
fn is_transparent(&self) -> bool {
|
||||
self.background_appearance != WindowBackgroundAppearance::Opaque
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -181,7 +257,7 @@ pub(crate) struct X11WindowStatePtr {
|
||||
pub state: Rc<RefCell<X11WindowState>>,
|
||||
pub(crate) callbacks: Rc<RefCell<Callbacks>>,
|
||||
xcb_connection: Rc<XCBConnection>,
|
||||
pub x_window: xproto::Window,
|
||||
x_window: xproto::Window,
|
||||
}
|
||||
|
||||
impl rwh::HasWindowHandle for RawWindow {
|
||||
@@ -219,6 +295,7 @@ impl X11WindowState {
|
||||
executor: ForegroundExecutor,
|
||||
params: WindowParams,
|
||||
xcb_connection: &Rc<XCBConnection>,
|
||||
client_side_decorations_supported: bool,
|
||||
x_main_screen_index: usize,
|
||||
x_window: xproto::Window,
|
||||
atoms: &XcbAtoms,
|
||||
@@ -230,19 +307,11 @@ impl X11WindowState {
|
||||
.map_or(x_main_screen_index, |did| did.0 as usize);
|
||||
|
||||
let visual_set = find_visuals(&xcb_connection, x_screen_index);
|
||||
let visual_maybe = match params.window_background {
|
||||
WindowBackgroundAppearance::Opaque => visual_set.opaque,
|
||||
WindowBackgroundAppearance::Transparent | WindowBackgroundAppearance::Blurred => {
|
||||
visual_set.transparent
|
||||
}
|
||||
};
|
||||
let visual = match visual_maybe {
|
||||
|
||||
let visual = match visual_set.transparent {
|
||||
Some(visual) => visual,
|
||||
None => {
|
||||
log::warn!(
|
||||
"Unable to find a matching visual for {:?}",
|
||||
params.window_background
|
||||
);
|
||||
log::warn!("Unable to find a transparent visual",);
|
||||
visual_set.inherit
|
||||
}
|
||||
};
|
||||
@@ -269,7 +338,8 @@ impl X11WindowState {
|
||||
| xproto::EventMask::STRUCTURE_NOTIFY
|
||||
| xproto::EventMask::FOCUS_CHANGE
|
||||
| xproto::EventMask::KEY_PRESS
|
||||
| xproto::EventMask::KEY_RELEASE,
|
||||
| xproto::EventMask::KEY_RELEASE
|
||||
| EventMask::PROPERTY_CHANGE,
|
||||
);
|
||||
|
||||
let mut bounds = params.bounds.to_device_pixels(scale_factor);
|
||||
@@ -349,7 +419,26 @@ impl X11WindowState {
|
||||
x_window,
|
||||
atoms.WM_PROTOCOLS,
|
||||
xproto::AtomEnum::ATOM,
|
||||
&[atoms.WM_DELETE_WINDOW],
|
||||
&[atoms.WM_DELETE_WINDOW, atoms._NET_WM_SYNC_REQUEST],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
sync::initialize(xcb_connection, 3, 1).unwrap();
|
||||
let sync_request_counter = xcb_connection.generate_id().unwrap();
|
||||
sync::create_counter(
|
||||
xcb_connection,
|
||||
sync_request_counter,
|
||||
sync::Int64 { lo: 0, hi: 0 },
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
xcb_connection
|
||||
.change_property32(
|
||||
xproto::PropMode::REPLACE,
|
||||
x_window,
|
||||
atoms._NET_WM_SYNC_REQUEST_COUNTER,
|
||||
xproto::AtomEnum::CARDINAL,
|
||||
&[sync_request_counter],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -396,35 +485,14 @@ impl X11WindowState {
|
||||
// Note: this has to be done after the GPU init, or otherwise
|
||||
// the sizes are immediately invalidated.
|
||||
size: query_render_extent(xcb_connection, x_window),
|
||||
transparent: params.window_background != WindowBackgroundAppearance::Opaque,
|
||||
// We set it to transparent by default, even if we have client-side
|
||||
// decorations, since those seem to work on X11 even without `true` here.
|
||||
// If the window appearance changes, then the renderer will get updated
|
||||
// too
|
||||
transparent: false,
|
||||
};
|
||||
xcb_connection.map_window(x_window).unwrap();
|
||||
|
||||
let screen_resources = xcb_connection
|
||||
.randr_get_screen_resources(x_window)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.expect("Could not find available screens");
|
||||
|
||||
let mode = screen_resources
|
||||
.crtcs
|
||||
.iter()
|
||||
.find_map(|crtc| {
|
||||
let crtc_info = xcb_connection
|
||||
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
|
||||
.ok()?
|
||||
.reply()
|
||||
.ok()?;
|
||||
|
||||
screen_resources
|
||||
.modes
|
||||
.iter()
|
||||
.find(|m| m.id == crtc_info.mode)
|
||||
})
|
||||
.expect("Unable to find screen refresh rate");
|
||||
|
||||
let refresh_rate = mode_refresh_rate(&mode);
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
executor,
|
||||
@@ -438,10 +506,21 @@ impl X11WindowState {
|
||||
renderer: BladeRenderer::new(gpu, config),
|
||||
atoms: *atoms,
|
||||
input_handler: None,
|
||||
active: false,
|
||||
fullscreen: false,
|
||||
maximized_vertical: false,
|
||||
maximized_horizontal: false,
|
||||
hidden: false,
|
||||
appearance,
|
||||
handle,
|
||||
background_appearance: WindowBackgroundAppearance::Opaque,
|
||||
destroyed: false,
|
||||
refresh_rate,
|
||||
client_side_decorations_supported,
|
||||
decorations: WindowDecorations::Server,
|
||||
last_insets: [0, 0, 0, 0],
|
||||
edge_constraints: None,
|
||||
counter_id: sync_request_counter,
|
||||
last_sync_counter: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -505,19 +584,21 @@ impl X11Window {
|
||||
executor: ForegroundExecutor,
|
||||
params: WindowParams,
|
||||
xcb_connection: &Rc<XCBConnection>,
|
||||
client_side_decorations_supported: bool,
|
||||
x_main_screen_index: usize,
|
||||
x_window: xproto::Window,
|
||||
atoms: &XcbAtoms,
|
||||
scale_factor: f32,
|
||||
appearance: WindowAppearance,
|
||||
) -> anyhow::Result<Self> {
|
||||
Ok(Self(X11WindowStatePtr {
|
||||
let ptr = X11WindowStatePtr {
|
||||
state: Rc::new(RefCell::new(X11WindowState::new(
|
||||
handle,
|
||||
client,
|
||||
executor,
|
||||
params,
|
||||
xcb_connection,
|
||||
client_side_decorations_supported,
|
||||
x_main_screen_index,
|
||||
x_window,
|
||||
atoms,
|
||||
@@ -527,7 +608,12 @@ impl X11Window {
|
||||
callbacks: Rc::new(RefCell::new(Callbacks::default())),
|
||||
xcb_connection: xcb_connection.clone(),
|
||||
x_window,
|
||||
}))
|
||||
};
|
||||
|
||||
let state = ptr.state.borrow_mut();
|
||||
ptr.set_wm_properties(state);
|
||||
|
||||
Ok(Self(ptr))
|
||||
}
|
||||
|
||||
fn set_wm_hints(&self, wm_hint_property_state: WmHintPropertyState, prop1: u32, prop2: u32) {
|
||||
@@ -546,30 +632,9 @@ impl X11Window {
|
||||
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
||||
message,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn get_wm_hints(&self) -> Vec<u32> {
|
||||
let reply = self
|
||||
.0
|
||||
.xcb_connection
|
||||
.get_property(
|
||||
false,
|
||||
self.0.x_window,
|
||||
self.0.state.borrow().atoms._NET_WM_STATE,
|
||||
xproto::AtomEnum::ATOM,
|
||||
0,
|
||||
u32::MAX,
|
||||
)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.check()
|
||||
.unwrap();
|
||||
// Reply is in u8 but atoms are represented as u32
|
||||
reply
|
||||
.value
|
||||
.chunks_exact(4)
|
||||
.map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_root_position(&self, position: Point<Pixels>) -> TranslateCoordinatesReply {
|
||||
@@ -586,6 +651,48 @@ impl X11Window {
|
||||
.reply()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn send_moveresize(&self, flag: u32) {
|
||||
let state = self.0.state.borrow();
|
||||
|
||||
self.0
|
||||
.xcb_connection
|
||||
.ungrab_pointer(x11rb::CURRENT_TIME)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
|
||||
let pointer = self
|
||||
.0
|
||||
.xcb_connection
|
||||
.query_pointer(self.0.x_window)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
let message = ClientMessageEvent::new(
|
||||
32,
|
||||
self.0.x_window,
|
||||
state.atoms._NET_WM_MOVERESIZE,
|
||||
[
|
||||
pointer.root_x as u32,
|
||||
pointer.root_y as u32,
|
||||
flag,
|
||||
0, // Left mouse button
|
||||
0,
|
||||
],
|
||||
);
|
||||
self.0
|
||||
.xcb_connection
|
||||
.send_event(
|
||||
false,
|
||||
state.x_root_window,
|
||||
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
||||
message,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.0.xcb_connection.flush().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl X11WindowStatePtr {
|
||||
@@ -600,6 +707,78 @@ impl X11WindowStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn property_notify(&self, event: xproto::PropertyNotifyEvent) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
if event.atom == state.atoms._NET_WM_STATE {
|
||||
self.set_wm_properties(state);
|
||||
} else if event.atom == state.atoms._GTK_EDGE_CONSTRAINTS {
|
||||
self.set_edge_constraints(state);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_edge_constraints(&self, mut state: std::cell::RefMut<X11WindowState>) {
|
||||
let reply = self
|
||||
.xcb_connection
|
||||
.get_property(
|
||||
false,
|
||||
self.x_window,
|
||||
state.atoms._GTK_EDGE_CONSTRAINTS,
|
||||
xproto::AtomEnum::CARDINAL,
|
||||
0,
|
||||
4,
|
||||
)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
|
||||
if reply.value_len != 0 {
|
||||
let atom = u32::from_ne_bytes(reply.value[0..4].try_into().unwrap());
|
||||
let edge_constraints = EdgeConstraints::from_atom(atom);
|
||||
state.edge_constraints.replace(edge_constraints);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_wm_properties(&self, mut state: std::cell::RefMut<X11WindowState>) {
|
||||
let reply = self
|
||||
.xcb_connection
|
||||
.get_property(
|
||||
false,
|
||||
self.x_window,
|
||||
state.atoms._NET_WM_STATE,
|
||||
xproto::AtomEnum::ATOM,
|
||||
0,
|
||||
u32::MAX,
|
||||
)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
|
||||
let atoms = reply
|
||||
.value
|
||||
.chunks_exact(4)
|
||||
.map(|chunk| u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]));
|
||||
|
||||
state.active = false;
|
||||
state.fullscreen = false;
|
||||
state.maximized_vertical = false;
|
||||
state.maximized_horizontal = false;
|
||||
state.hidden = true;
|
||||
|
||||
for atom in atoms {
|
||||
if atom == state.atoms._NET_WM_STATE_FOCUSED {
|
||||
state.active = true;
|
||||
} else if atom == state.atoms._NET_WM_STATE_FULLSCREEN {
|
||||
state.fullscreen = true;
|
||||
} else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_VERT {
|
||||
state.maximized_vertical = true;
|
||||
} else if atom == state.atoms._NET_WM_STATE_MAXIMIZED_HORZ {
|
||||
state.maximized_horizontal = true;
|
||||
} else if atom == state.atoms._NET_WM_STATE_HIDDEN {
|
||||
state.hidden = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
let mut callbacks = self.callbacks.borrow_mut();
|
||||
if let Some(fun) = callbacks.close.take() {
|
||||
@@ -715,6 +894,9 @@ impl X11WindowStatePtr {
|
||||
));
|
||||
resize_args = Some((state.content_size(), state.scale_factor));
|
||||
}
|
||||
if let Some(value) = state.last_sync_counter.take() {
|
||||
sync::set_counter(&self.xcb_connection, state.counter_id, value).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let mut callbacks = self.callbacks.borrow_mut();
|
||||
@@ -737,17 +919,17 @@ impl X11WindowStatePtr {
|
||||
}
|
||||
|
||||
pub fn set_appearance(&mut self, appearance: WindowAppearance) {
|
||||
self.state.borrow_mut().appearance = appearance;
|
||||
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.appearance = appearance;
|
||||
let is_transparent = state.is_transparent();
|
||||
state.renderer.update_transparency(is_transparent);
|
||||
state.appearance = appearance;
|
||||
drop(state);
|
||||
let mut callbacks = self.callbacks.borrow_mut();
|
||||
if let Some(ref mut fun) = callbacks.appearance_changed {
|
||||
(fun)()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_rate(&self) -> Duration {
|
||||
self.state.borrow().refresh_rate
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformWindow for X11Window {
|
||||
@@ -757,11 +939,9 @@ impl PlatformWindow for X11Window {
|
||||
|
||||
fn is_maximized(&self) -> bool {
|
||||
let state = self.0.state.borrow();
|
||||
let wm_hints = self.get_wm_hints();
|
||||
|
||||
// A maximized window that gets minimized will still retain its maximized state.
|
||||
!wm_hints.contains(&state.atoms._NET_WM_STATE_HIDDEN)
|
||||
&& wm_hints.contains(&state.atoms._NET_WM_STATE_MAXIMIZED_VERT)
|
||||
&& wm_hints.contains(&state.atoms._NET_WM_STATE_MAXIMIZED_HORZ)
|
||||
!state.hidden && state.maximized_vertical && state.maximized_horizontal
|
||||
}
|
||||
|
||||
fn window_bounds(&self) -> WindowBounds {
|
||||
@@ -859,12 +1039,11 @@ impl PlatformWindow for X11Window {
|
||||
xproto::Time::CURRENT_TIME,
|
||||
)
|
||||
.log_err();
|
||||
self.0.xcb_connection.flush().unwrap();
|
||||
}
|
||||
|
||||
fn is_active(&self) -> bool {
|
||||
let state = self.0.state.borrow();
|
||||
self.get_wm_hints()
|
||||
.contains(&state.atoms._NET_WM_STATE_FOCUSED)
|
||||
self.0.state.borrow().active
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
@@ -889,6 +1068,7 @@ impl PlatformWindow for X11Window {
|
||||
title.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
self.0.xcb_connection.flush().unwrap();
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, app_id: &str) {
|
||||
@@ -906,6 +1086,8 @@ impl PlatformWindow for X11Window {
|
||||
xproto::AtomEnum::STRING,
|
||||
&data,
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -913,10 +1095,11 @@ impl PlatformWindow for X11Window {
|
||||
log::info!("ignoring macOS specific set_edited");
|
||||
}
|
||||
|
||||
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
|
||||
let mut inner = self.0.state.borrow_mut();
|
||||
let transparent = background_appearance != WindowBackgroundAppearance::Opaque;
|
||||
inner.renderer.update_transparency(transparent);
|
||||
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
|
||||
let mut state = self.0.state.borrow_mut();
|
||||
state.background_appearance = background_appearance;
|
||||
let transparent = state.is_transparent();
|
||||
state.renderer.update_transparency(transparent);
|
||||
}
|
||||
|
||||
fn show_character_palette(&self) {
|
||||
@@ -940,6 +1123,8 @@ impl PlatformWindow for X11Window {
|
||||
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
||||
message,
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -962,9 +1147,7 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
|
||||
fn is_fullscreen(&self) -> bool {
|
||||
let state = self.0.state.borrow();
|
||||
self.get_wm_hints()
|
||||
.contains(&state.atoms._NET_WM_STATE_FULLSCREEN)
|
||||
self.0.state.borrow().fullscreen
|
||||
}
|
||||
|
||||
fn on_request_frame(&self, callback: Box<dyn FnMut()>) {
|
||||
@@ -1004,7 +1187,7 @@ impl PlatformWindow for X11Window {
|
||||
inner.renderer.draw(scene);
|
||||
}
|
||||
|
||||
fn sprite_atlas(&self) -> sync::Arc<dyn PlatformAtlas> {
|
||||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
|
||||
let inner = self.0.state.borrow();
|
||||
inner.renderer.sprite_atlas().clone()
|
||||
}
|
||||
@@ -1032,56 +1215,144 @@ impl PlatformWindow for X11Window {
|
||||
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
||||
message,
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn start_system_move(&self) {
|
||||
let state = self.0.state.borrow();
|
||||
let pointer = self
|
||||
.0
|
||||
.xcb_connection
|
||||
.query_pointer(self.0.x_window)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap();
|
||||
fn start_window_move(&self) {
|
||||
const MOVERESIZE_MOVE: u32 = 8;
|
||||
let message = ClientMessageEvent::new(
|
||||
32,
|
||||
self.0.x_window,
|
||||
state.atoms._NET_WM_MOVERESIZE,
|
||||
[
|
||||
pointer.root_x as u32,
|
||||
pointer.root_y as u32,
|
||||
MOVERESIZE_MOVE,
|
||||
1, // Left mouse button
|
||||
1,
|
||||
],
|
||||
);
|
||||
self.send_moveresize(MOVERESIZE_MOVE);
|
||||
}
|
||||
|
||||
fn start_window_resize(&self, edge: ResizeEdge) {
|
||||
self.send_moveresize(edge.to_moveresize());
|
||||
}
|
||||
|
||||
fn window_decorations(&self) -> crate::Decorations {
|
||||
let state = self.0.state.borrow();
|
||||
|
||||
// Client window decorations require compositor support
|
||||
if !state.client_side_decorations_supported {
|
||||
return Decorations::Server;
|
||||
}
|
||||
|
||||
match state.decorations {
|
||||
WindowDecorations::Server => Decorations::Server,
|
||||
WindowDecorations::Client => {
|
||||
let tiling = if let Some(edge_constraints) = &state.edge_constraints {
|
||||
edge_constraints.to_tiling()
|
||||
} else {
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:ui/ozone/platform/x11/x11_window.cc;l=2519;drc=1f14cc876cc5bf899d13284a12c451498219bb2d
|
||||
Tiling {
|
||||
top: state.maximized_vertical,
|
||||
bottom: state.maximized_vertical,
|
||||
left: state.maximized_horizontal,
|
||||
right: state.maximized_horizontal,
|
||||
}
|
||||
};
|
||||
|
||||
Decorations::Client { tiling }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_client_inset(&self, inset: Pixels) {
|
||||
let mut state = self.0.state.borrow_mut();
|
||||
|
||||
let dp = (inset.0 * state.scale_factor) as u32;
|
||||
|
||||
let insets = if let Some(edge_constraints) = &state.edge_constraints {
|
||||
let left = if edge_constraints.left_tiled { 0 } else { dp };
|
||||
let top = if edge_constraints.top_tiled { 0 } else { dp };
|
||||
let right = if edge_constraints.right_tiled { 0 } else { dp };
|
||||
let bottom = if edge_constraints.bottom_tiled { 0 } else { dp };
|
||||
|
||||
[left, right, top, bottom]
|
||||
} else {
|
||||
let (left, right) = if state.maximized_horizontal {
|
||||
(0, 0)
|
||||
} else {
|
||||
(dp, dp)
|
||||
};
|
||||
let (top, bottom) = if state.maximized_vertical {
|
||||
(0, 0)
|
||||
} else {
|
||||
(dp, dp)
|
||||
};
|
||||
[left, right, top, bottom]
|
||||
};
|
||||
|
||||
if state.last_insets != insets {
|
||||
state.last_insets = insets;
|
||||
|
||||
self.0
|
||||
.xcb_connection
|
||||
.change_property(
|
||||
xproto::PropMode::REPLACE,
|
||||
self.0.x_window,
|
||||
state.atoms._GTK_FRAME_EXTENTS,
|
||||
xproto::AtomEnum::CARDINAL,
|
||||
size_of::<u32>() as u8 * 8,
|
||||
4,
|
||||
bytemuck::cast_slice::<u32, u8>(&insets),
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn request_decorations(&self, mut decorations: crate::WindowDecorations) {
|
||||
let mut state = self.0.state.borrow_mut();
|
||||
|
||||
if matches!(decorations, crate::WindowDecorations::Client)
|
||||
&& !state.client_side_decorations_supported
|
||||
{
|
||||
log::info!(
|
||||
"x11: no compositor present, falling back to server-side window decorations"
|
||||
);
|
||||
decorations = crate::WindowDecorations::Server;
|
||||
}
|
||||
|
||||
// https://github.com/rust-windowing/winit/blob/master/src/platform_impl/linux/x11/util/hint.rs#L53-L87
|
||||
let hints_data: [u32; 5] = match decorations {
|
||||
WindowDecorations::Server => [1 << 1, 0, 1, 0, 0],
|
||||
WindowDecorations::Client => [1 << 1, 0, 0, 0, 0],
|
||||
};
|
||||
|
||||
self.0
|
||||
.xcb_connection
|
||||
.send_event(
|
||||
false,
|
||||
state.x_root_window,
|
||||
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
||||
message,
|
||||
.change_property(
|
||||
xproto::PropMode::REPLACE,
|
||||
self.0.x_window,
|
||||
state.atoms._MOTIF_WM_HINTS,
|
||||
state.atoms._MOTIF_WM_HINTS,
|
||||
std::mem::size_of::<u32>() as u8 * 8,
|
||||
5,
|
||||
bytemuck::cast_slice::<u32, u8>(&hints_data),
|
||||
)
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn should_render_window_controls(&self) -> bool {
|
||||
false
|
||||
match decorations {
|
||||
WindowDecorations::Server => {
|
||||
state.decorations = WindowDecorations::Server;
|
||||
let is_transparent = state.is_transparent();
|
||||
state.renderer.update_transparency(is_transparent);
|
||||
}
|
||||
WindowDecorations::Client => {
|
||||
state.decorations = WindowDecorations::Client;
|
||||
let is_transparent = state.is_transparent();
|
||||
state.renderer.update_transparency(is_transparent);
|
||||
}
|
||||
}
|
||||
|
||||
drop(state);
|
||||
let mut callbacks = self.0.callbacks.borrow_mut();
|
||||
if let Some(appearance_changed) = callbacks.appearance_changed.as_mut() {
|
||||
appearance_changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from:
|
||||
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
|
||||
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
|
||||
if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 {
|
||||
return Duration::from_millis(16);
|
||||
}
|
||||
|
||||
let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
|
||||
let micros = 1_000_000_000 / millihertz;
|
||||
log::info!("Refreshing at {} micros", micros);
|
||||
Duration::from_micros(micros)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,9 @@
|
||||
//!
|
||||
//! This module uses the [ashpd] crate
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use ashpd::desktop::settings::{ColorScheme, Settings};
|
||||
use calloop::channel::{Channel, Sender};
|
||||
use calloop::channel::Channel;
|
||||
use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory};
|
||||
use mio::Waker;
|
||||
use smol::stream::StreamExt;
|
||||
|
||||
use crate::{BackgroundExecutor, WindowAppearance};
|
||||
@@ -24,45 +20,31 @@ pub struct XDPEventSource {
|
||||
}
|
||||
|
||||
impl XDPEventSource {
|
||||
pub fn new(executor: &BackgroundExecutor, waker: Option<Arc<Waker>>) -> Self {
|
||||
pub fn new(executor: &BackgroundExecutor) -> Self {
|
||||
let (sender, channel) = calloop::channel::channel();
|
||||
|
||||
let background = executor.clone();
|
||||
|
||||
executor
|
||||
.spawn(async move {
|
||||
fn send_event<T>(
|
||||
sender: &Sender<T>,
|
||||
waker: &Option<Arc<Waker>>,
|
||||
event: T,
|
||||
) -> Result<(), std::sync::mpsc::SendError<T>> {
|
||||
sender.send(event)?;
|
||||
if let Some(waker) = waker {
|
||||
waker.wake().ok();
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let settings = Settings::new().await?;
|
||||
|
||||
if let Ok(initial_appearance) = settings.color_scheme().await {
|
||||
send_event(
|
||||
&sender,
|
||||
&waker,
|
||||
Event::WindowAppearance(WindowAppearance::from_native(initial_appearance)),
|
||||
)?;
|
||||
sender.send(Event::WindowAppearance(WindowAppearance::from_native(
|
||||
initial_appearance,
|
||||
)))?;
|
||||
}
|
||||
if let Ok(initial_theme) = settings
|
||||
.read::<String>("org.gnome.desktop.interface", "cursor-theme")
|
||||
.await
|
||||
{
|
||||
send_event(&sender, &waker, Event::CursorTheme(initial_theme))?;
|
||||
sender.send(Event::CursorTheme(initial_theme))?;
|
||||
}
|
||||
if let Ok(initial_size) = settings
|
||||
.read::<u32>("org.gnome.desktop.interface", "cursor-size")
|
||||
.await
|
||||
{
|
||||
send_event(&sender, &waker, Event::CursorSize(initial_size))?;
|
||||
sender.send(Event::CursorSize(initial_size))?;
|
||||
}
|
||||
|
||||
if let Ok(mut cursor_theme_changed) = settings
|
||||
@@ -73,12 +55,11 @@ impl XDPEventSource {
|
||||
.await
|
||||
{
|
||||
let sender = sender.clone();
|
||||
let waker = waker.clone();
|
||||
background
|
||||
.spawn(async move {
|
||||
while let Some(theme) = cursor_theme_changed.next().await {
|
||||
let theme = theme?;
|
||||
send_event(&sender, &waker, Event::CursorTheme(theme))?;
|
||||
sender.send(Event::CursorTheme(theme))?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
@@ -93,12 +74,11 @@ impl XDPEventSource {
|
||||
.await
|
||||
{
|
||||
let sender = sender.clone();
|
||||
let waker = waker.clone();
|
||||
background
|
||||
.spawn(async move {
|
||||
while let Some(size) = cursor_size_changed.next().await {
|
||||
let size = size?;
|
||||
send_event(&sender, &waker, Event::CursorSize(size))?;
|
||||
sender.send(Event::CursorSize(size))?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
@@ -107,11 +87,9 @@ impl XDPEventSource {
|
||||
|
||||
let mut appearance_changed = settings.receive_color_scheme_changed().await?;
|
||||
while let Some(scheme) = appearance_changed.next().await {
|
||||
send_event(
|
||||
&sender,
|
||||
&waker,
|
||||
Event::WindowAppearance(WindowAppearance::from_native(scheme)),
|
||||
)?;
|
||||
sender.send(Event::WindowAppearance(WindowAppearance::from_native(
|
||||
scheme,
|
||||
)))?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -120,12 +98,6 @@ impl XDPEventSource {
|
||||
|
||||
Self { channel }
|
||||
}
|
||||
|
||||
pub fn try_recv(&self) -> anyhow::Result<Event> {
|
||||
self.channel
|
||||
.try_recv()
|
||||
.map_err(|error| anyhow!("{}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventSource for XDPEventSource {
|
||||
|
||||
@@ -796,14 +796,24 @@ impl Platform for MacPlatform {
|
||||
CursorStyle::ClosedHand => msg_send![class!(NSCursor), closedHandCursor],
|
||||
CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor],
|
||||
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
|
||||
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
|
||||
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), verticalResizeCursor],
|
||||
CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor],
|
||||
CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor],
|
||||
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
|
||||
CursorStyle::ResizeColumn => msg_send![class!(NSCursor), resizeLeftRightCursor],
|
||||
CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor],
|
||||
CursorStyle::ResizeUp => msg_send![class!(NSCursor), resizeUpCursor],
|
||||
CursorStyle::ResizeDown => msg_send![class!(NSCursor), resizeDownCursor],
|
||||
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
|
||||
CursorStyle::ResizeRow => msg_send![class!(NSCursor), resizeUpDownCursor],
|
||||
|
||||
// Undocumented, private class methods:
|
||||
// https://stackoverflow.com/questions/27242353/cocoa-predefined-resize-mouse-cursor
|
||||
CursorStyle::ResizeUpLeftDownRight => {
|
||||
msg_send![class!(NSCursor), _windowResizeNorthWestSouthEastCursor]
|
||||
}
|
||||
CursorStyle::ResizeUpRightDownLeft => {
|
||||
msg_send![class!(NSCursor), _windowResizeNorthEastSouthWestCursor]
|
||||
}
|
||||
|
||||
CursorStyle::IBeamCursorForVerticalLayout => {
|
||||
msg_send![class!(NSCursor), IBeamCursorForVerticalLayout]
|
||||
}
|
||||
|
||||
@@ -497,7 +497,6 @@ impl MacWindow {
|
||||
pub fn open(
|
||||
handle: AnyWindowHandle,
|
||||
WindowParams {
|
||||
window_background,
|
||||
bounds,
|
||||
titlebar,
|
||||
kind,
|
||||
@@ -603,7 +602,7 @@ impl MacWindow {
|
||||
native_window as *mut _,
|
||||
native_view as *mut _,
|
||||
bounds.size.map(|pixels| pixels.0),
|
||||
window_background != WindowBackgroundAppearance::Opaque,
|
||||
false,
|
||||
),
|
||||
request_frame_callback: None,
|
||||
event_callback: None,
|
||||
@@ -676,8 +675,6 @@ impl MacWindow {
|
||||
native_window.setContentView_(native_view.autorelease());
|
||||
native_window.makeFirstResponder_(native_view);
|
||||
|
||||
window.set_background_appearance(window_background);
|
||||
|
||||
match kind {
|
||||
WindowKind::Normal => {
|
||||
native_window.setLevel_(NSNormalWindowLevel);
|
||||
@@ -956,7 +953,7 @@ impl PlatformWindow for MacWindow {
|
||||
|
||||
fn set_app_id(&mut self, _app_id: &str) {}
|
||||
|
||||
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
|
||||
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
|
||||
let mut this = self.0.as_ref().lock();
|
||||
this.renderer
|
||||
.update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
|
||||
@@ -1092,14 +1089,6 @@ impl PlatformWindow for MacWindow {
|
||||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
|
||||
self.0.lock().renderer.sprite_atlas().clone()
|
||||
}
|
||||
|
||||
fn show_window_menu(&self, _position: Point<Pixels>) {}
|
||||
|
||||
fn start_system_move(&self) {}
|
||||
|
||||
fn should_render_window_controls(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl rwh::HasWindowHandle for MacWindow {
|
||||
|
||||
@@ -188,9 +188,7 @@ impl PlatformWindow for TestWindow {
|
||||
|
||||
fn set_app_id(&mut self, _app_id: &str) {}
|
||||
|
||||
fn set_background_appearance(&mut self, _background: WindowBackgroundAppearance) {
|
||||
unimplemented!()
|
||||
}
|
||||
fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {}
|
||||
|
||||
fn set_edited(&mut self, edited: bool) {
|
||||
self.0.lock().edited = edited;
|
||||
@@ -262,13 +260,9 @@ impl PlatformWindow for TestWindow {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn start_system_move(&self) {
|
||||
fn start_window_move(&self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn should_render_window_controls(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TestAtlasState {
|
||||
|
||||
@@ -274,7 +274,7 @@ impl WindowsWindow {
|
||||
handle,
|
||||
hide_title_bar,
|
||||
display,
|
||||
transparent: params.window_background != WindowBackgroundAppearance::Opaque,
|
||||
transparent: true,
|
||||
executor,
|
||||
current_cursor,
|
||||
};
|
||||
@@ -511,9 +511,7 @@ impl PlatformWindow for WindowsWindow {
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, _app_id: &str) {}
|
||||
|
||||
fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
|
||||
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
|
||||
self.0
|
||||
.state
|
||||
.borrow_mut()
|
||||
@@ -521,12 +519,6 @@ impl PlatformWindow for WindowsWindow {
|
||||
.update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
|
||||
}
|
||||
|
||||
// todo(windows)
|
||||
fn set_edited(&mut self, _edited: bool) {}
|
||||
|
||||
// todo(windows)
|
||||
fn show_character_palette(&self) {}
|
||||
|
||||
fn minimize(&self) {
|
||||
unsafe { ShowWindowAsync(self.0.hwnd, SW_MINIMIZE).ok().log_err() };
|
||||
}
|
||||
@@ -645,14 +637,6 @@ impl PlatformWindow for WindowsWindow {
|
||||
fn get_raw_handle(&self) -> HWND {
|
||||
self.0.hwnd
|
||||
}
|
||||
|
||||
fn show_window_menu(&self, _position: Point<Pixels>) {}
|
||||
|
||||
fn start_system_move(&self) {}
|
||||
|
||||
fn should_render_window_controls(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[implement(IDropTarget)]
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use crate::{
|
||||
hash, point, prelude::*, px, size, transparent_black, Action, AnyDrag, AnyElement, AnyTooltip,
|
||||
AnyView, AppContext, Arena, Asset, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow,
|
||||
Context, Corners, CursorStyle, DevicePixels, DispatchActionListener, DispatchNodeId,
|
||||
DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten,
|
||||
FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyBinding,
|
||||
KeyContext, KeyDownEvent, KeyEvent, KeyMatch, KeymatchResult, Keystroke, KeystrokeEvent,
|
||||
LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers, ModifiersChangedEvent,
|
||||
MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels,
|
||||
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
|
||||
PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams,
|
||||
RenderSvgParams, ScaledPixels, Scene, Shadow, SharedString, Size, StrikethroughStyle, Style,
|
||||
SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
|
||||
TransformationMatrix, Underline, UnderlineStyle, View, VisualContext, WeakView,
|
||||
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowOptions, WindowParams,
|
||||
WindowTextSystem, SUBPIXEL_VARIANTS,
|
||||
Context, Corners, CursorStyle, Decorations, DevicePixels, DispatchActionListener,
|
||||
DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter,
|
||||
FileDropEvent, Flatten, FontId, Global, GlobalElementId, GlyphId, Hsla, ImageData,
|
||||
InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, KeyMatch, KeymatchResult,
|
||||
Keystroke, KeystrokeEvent, LayoutId, LineLayoutIndex, Model, ModelContext, Modifiers,
|
||||
ModifiersChangedEvent, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent,
|
||||
Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
|
||||
PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams,
|
||||
RenderImageParams, RenderSvgParams, ResizeEdge, ScaledPixels, Scene, Shadow, SharedString,
|
||||
Size, StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task,
|
||||
TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, View,
|
||||
VisualContext, WeakView, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
|
||||
WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
|
||||
SUBPIXEL_VARIANTS,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::{FxHashMap, FxHashSet};
|
||||
@@ -610,7 +611,10 @@ fn default_bounds(display_id: Option<DisplayId>, cx: &mut AppContext) -> Bounds<
|
||||
|
||||
cx.active_window()
|
||||
.and_then(|w| w.update(cx, |_, cx| cx.bounds()).ok())
|
||||
.map(|bounds| bounds.map_origin(|origin| origin + DEFAULT_WINDOW_OFFSET))
|
||||
.map(|mut bounds| {
|
||||
bounds.origin += DEFAULT_WINDOW_OFFSET;
|
||||
bounds
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let display = display_id
|
||||
.map(|id| cx.find_display(id))
|
||||
@@ -639,6 +643,7 @@ impl Window {
|
||||
window_background,
|
||||
app_id,
|
||||
window_min_size,
|
||||
window_decorations,
|
||||
} = options;
|
||||
|
||||
let bounds = window_bounds
|
||||
@@ -654,7 +659,6 @@ impl Window {
|
||||
focus,
|
||||
show,
|
||||
display_id,
|
||||
window_background,
|
||||
window_min_size,
|
||||
},
|
||||
)?;
|
||||
@@ -672,6 +676,10 @@ impl Window {
|
||||
let next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>> = Default::default();
|
||||
let last_input_timestamp = Rc::new(Cell::new(Instant::now()));
|
||||
|
||||
platform_window
|
||||
.request_decorations(window_decorations.unwrap_or(WindowDecorations::Server));
|
||||
platform_window.set_background_appearance(window_background);
|
||||
|
||||
if let Some(ref window_open_state) = window_bounds {
|
||||
match window_open_state {
|
||||
WindowBounds::Fullscreen(_) => platform_window.toggle_fullscreen(),
|
||||
@@ -990,6 +998,16 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.platform_window.is_maximized()
|
||||
}
|
||||
|
||||
/// request a certain window decoration (Wayland)
|
||||
pub fn request_decorations(&self, decorations: WindowDecorations) {
|
||||
self.window.platform_window.request_decorations(decorations);
|
||||
}
|
||||
|
||||
/// Start a window resize operation (Wayland)
|
||||
pub fn start_window_resize(&self, edge: ResizeEdge) {
|
||||
self.window.platform_window.start_window_resize(edge);
|
||||
}
|
||||
|
||||
/// Return the `WindowBounds` to indicate that how a window should be opened
|
||||
/// after it has been closed
|
||||
pub fn window_bounds(&self) -> WindowBounds {
|
||||
@@ -1217,13 +1235,23 @@ impl<'a> WindowContext<'a> {
|
||||
/// Tells the compositor to take control of window movement (Wayland and X11)
|
||||
///
|
||||
/// Events may not be received during a move operation.
|
||||
pub fn start_system_move(&self) {
|
||||
self.window.platform_window.start_system_move()
|
||||
pub fn start_window_move(&self) {
|
||||
self.window.platform_window.start_window_move()
|
||||
}
|
||||
|
||||
/// When using client side decorations, set this to the width of the invisible decorations (Wayland and X11)
|
||||
pub fn set_client_inset(&self, inset: Pixels) {
|
||||
self.window.platform_window.set_client_inset(inset);
|
||||
}
|
||||
|
||||
/// Returns whether the title bar window controls need to be rendered by the application (Wayland and X11)
|
||||
pub fn should_render_window_controls(&self) -> bool {
|
||||
self.window.platform_window.should_render_window_controls()
|
||||
pub fn window_decorations(&self) -> Decorations {
|
||||
self.window.platform_window.window_decorations()
|
||||
}
|
||||
|
||||
/// Returns which window controls are currently visible (Wayland)
|
||||
pub fn window_controls(&self) -> WindowControls {
|
||||
self.window.platform_window.window_controls()
|
||||
}
|
||||
|
||||
/// Updates the window's title at the platform level.
|
||||
@@ -1237,7 +1265,7 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
|
||||
/// Sets the window background appearance.
|
||||
pub fn set_background_appearance(&mut self, background_appearance: WindowBackgroundAppearance) {
|
||||
pub fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
|
||||
self.window
|
||||
.platform_window
|
||||
.set_background_appearance(background_appearance);
|
||||
|
||||
@@ -28,7 +28,8 @@ pub use settings::*;
|
||||
pub use styles::*;
|
||||
|
||||
use gpui::{
|
||||
AppContext, AssetSource, Hsla, SharedString, WindowAppearance, WindowBackgroundAppearance,
|
||||
px, AppContext, AssetSource, Hsla, Pixels, SharedString, WindowAppearance,
|
||||
WindowBackgroundAppearance,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -38,6 +39,9 @@ pub enum Appearance {
|
||||
Dark,
|
||||
}
|
||||
|
||||
pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0);
|
||||
pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0);
|
||||
|
||||
impl Appearance {
|
||||
pub fn is_light(&self) -> bool {
|
||||
match self {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
pub mod platform_generic;
|
||||
pub mod platform_linux;
|
||||
pub mod platform_mac;
|
||||
pub mod platform_windows;
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
use gpui::{prelude::*, Action};
|
||||
|
||||
use ui::prelude::*;
|
||||
|
||||
use crate::window_controls::{WindowControl, WindowControlType};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct GenericWindowControls {
|
||||
close_window_action: Box<dyn Action>,
|
||||
}
|
||||
|
||||
impl GenericWindowControls {
|
||||
pub fn new(close_action: Box<dyn Action>) -> Self {
|
||||
Self {
|
||||
close_window_action: close_action,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for GenericWindowControls {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id("generic-window-controls")
|
||||
.px_3()
|
||||
.gap_1p5()
|
||||
.child(WindowControl::new(
|
||||
"minimize",
|
||||
WindowControlType::Minimize,
|
||||
cx,
|
||||
))
|
||||
.child(WindowControl::new(
|
||||
"maximize-or-restore",
|
||||
if cx.is_maximized() {
|
||||
WindowControlType::Restore
|
||||
} else {
|
||||
WindowControlType::Maximize
|
||||
},
|
||||
cx,
|
||||
))
|
||||
.child(WindowControl::new_close(
|
||||
"close",
|
||||
WindowControlType::Close,
|
||||
self.close_window_action,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
use gpui::{prelude::*, Action};
|
||||
use gpui::{prelude::*, Action, MouseButton};
|
||||
|
||||
use ui::prelude::*;
|
||||
|
||||
use super::platform_generic::GenericWindowControls;
|
||||
use crate::window_controls::{WindowControl, WindowControlType};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct LinuxWindowControls {
|
||||
@@ -18,7 +18,31 @@ impl LinuxWindowControls {
|
||||
}
|
||||
|
||||
impl RenderOnce for LinuxWindowControls {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
GenericWindowControls::new(self.close_window_action.boxed_clone()).into_any_element()
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id("generic-window-controls")
|
||||
.px_3()
|
||||
.gap_3()
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.child(WindowControl::new(
|
||||
"minimize",
|
||||
WindowControlType::Minimize,
|
||||
cx,
|
||||
))
|
||||
.child(WindowControl::new(
|
||||
"maximize-or-restore",
|
||||
if cx.is_maximized() {
|
||||
WindowControlType::Restore
|
||||
} else {
|
||||
WindowControlType::Maximize
|
||||
},
|
||||
cx,
|
||||
))
|
||||
.child(WindowControl::new_close(
|
||||
"close",
|
||||
WindowControlType::Close,
|
||||
self.close_window_action,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ use call::{ActiveCall, ParticipantLocation};
|
||||
use client::{Client, UserStore};
|
||||
use collab::render_color_ribbon;
|
||||
use gpui::{
|
||||
actions, div, px, Action, AnyElement, AppContext, Element, InteractiveElement, Interactivity,
|
||||
IntoElement, Model, ParentElement, Render, Stateful, StatefulInteractiveElement, Styled,
|
||||
Subscription, ViewContext, VisualContext, WeakView,
|
||||
actions, div, px, Action, AnyElement, AppContext, Decorations, Element, InteractiveElement,
|
||||
Interactivity, IntoElement, Model, MouseButton, ParentElement, Render, Stateful,
|
||||
StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use project::{Project, RepositoryEntry};
|
||||
use recent_projects::RecentProjects;
|
||||
@@ -58,6 +58,7 @@ pub struct TitleBar {
|
||||
user_store: Model<UserStore>,
|
||||
client: Arc<Client>,
|
||||
workspace: WeakView<Workspace>,
|
||||
should_move: bool,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -73,8 +74,10 @@ impl Render for TitleBar {
|
||||
let platform_supported = cfg!(target_os = "macos");
|
||||
|
||||
let height = Self::height(cx);
|
||||
let supported_controls = cx.window_controls();
|
||||
let decorations = cx.window_decorations();
|
||||
|
||||
let mut title_bar = h_flex()
|
||||
h_flex()
|
||||
.id("titlebar")
|
||||
.w_full()
|
||||
.pt(Self::top_padding(cx))
|
||||
@@ -88,6 +91,16 @@ impl Render for TitleBar {
|
||||
this.pl_2()
|
||||
}
|
||||
})
|
||||
.map(|el| {
|
||||
match decorations {
|
||||
Decorations::Server => el,
|
||||
Decorations::Client { tiling, .. } => el
|
||||
.when(!(tiling.top || tiling.right), |el| {
|
||||
el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING))
|
||||
}
|
||||
})
|
||||
.bg(cx.theme().colors().title_bar_background)
|
||||
.content_stretch()
|
||||
.child(
|
||||
@@ -113,7 +126,7 @@ impl Render for TitleBar {
|
||||
.children(self.render_project_host(cx))
|
||||
.child(self.render_project_name(cx))
|
||||
.children(self.render_project_branch(cx))
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation()),
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -145,7 +158,7 @@ impl Render for TitleBar {
|
||||
|
||||
this.children(current_user_face_pile.map(|face_pile| {
|
||||
v_flex()
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.child(face_pile)
|
||||
.child(render_color_ribbon(player_colors.local().cursor))
|
||||
}))
|
||||
@@ -208,7 +221,7 @@ impl Render for TitleBar {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.pr_1()
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.when_some(room, |this, room| {
|
||||
let room = room.read(cx);
|
||||
let project = self.project.read(cx);
|
||||
@@ -373,34 +386,38 @@ impl Render for TitleBar {
|
||||
}
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
// Windows Window Controls
|
||||
title_bar = title_bar.when(
|
||||
).when(
|
||||
self.platform_style == PlatformStyle::Windows && !cx.is_fullscreen(),
|
||||
|title_bar| title_bar.child(platform_windows::WindowsWindowControls::new(height)),
|
||||
);
|
||||
|
||||
// Linux Window Controls
|
||||
title_bar = title_bar.when(
|
||||
).when(
|
||||
self.platform_style == PlatformStyle::Linux
|
||||
&& !cx.is_fullscreen()
|
||||
&& cx.should_render_window_controls(),
|
||||
&& matches!(decorations, Decorations::Client { .. }),
|
||||
|title_bar| {
|
||||
title_bar
|
||||
.child(platform_linux::LinuxWindowControls::new(close_action))
|
||||
.on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
|
||||
cx.show_window_menu(ev.position)
|
||||
.when(supported_controls.window_menu, |titlebar| {
|
||||
titlebar.on_mouse_down(gpui::MouseButton::Right, move |ev, cx| {
|
||||
cx.show_window_menu(ev.position)
|
||||
})
|
||||
})
|
||||
.on_mouse_move(move |ev, cx| {
|
||||
if ev.dragging() {
|
||||
cx.start_system_move();
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
title_bar
|
||||
.on_mouse_move(cx.listener(move |this, _ev, cx| {
|
||||
if this.should_move {
|
||||
this.should_move = false;
|
||||
cx.start_window_move();
|
||||
}
|
||||
}))
|
||||
.on_mouse_down_out(cx.listener(move |this, _ev, _cx| {
|
||||
this.should_move = false;
|
||||
}))
|
||||
.on_mouse_down(gpui::MouseButton::Left, cx.listener(move |this, _ev, _cx| {
|
||||
this.should_move = true;
|
||||
}))
|
||||
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,6 +447,7 @@ impl TitleBar {
|
||||
content: div().id(id.into()),
|
||||
children: SmallVec::new(),
|
||||
workspace: workspace.weak_handle(),
|
||||
should_move: false,
|
||||
project,
|
||||
user_store,
|
||||
client,
|
||||
|
||||
@@ -38,7 +38,7 @@ impl WindowControlStyle {
|
||||
|
||||
Self {
|
||||
background: colors.ghost_element_background,
|
||||
background_hover: colors.ghost_element_background,
|
||||
background_hover: colors.ghost_element_hover,
|
||||
icon: colors.icon,
|
||||
icon_hover: colors.icon_muted,
|
||||
}
|
||||
@@ -127,7 +127,7 @@ impl WindowControl {
|
||||
impl RenderOnce for WindowControl {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let icon = svg()
|
||||
.size_5()
|
||||
.size_4()
|
||||
.flex_none()
|
||||
.path(self.icon.icon().path())
|
||||
.text_color(self.style.icon)
|
||||
@@ -139,7 +139,7 @@ impl RenderOnce for WindowControl {
|
||||
.cursor_pointer()
|
||||
.justify_center()
|
||||
.content_center()
|
||||
.rounded_md()
|
||||
.rounded_2xl()
|
||||
.w_5()
|
||||
.h_5()
|
||||
.hover(|this| this.bg(self.style.background_hover))
|
||||
|
||||
@@ -32,6 +32,7 @@ impl Display for BaseKeymap {
|
||||
}
|
||||
|
||||
impl BaseKeymap {
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const OPTIONS: [(&'static str, Self); 5] = [
|
||||
("VSCode (Default)", Self::VSCode),
|
||||
("Atom", Self::Atom),
|
||||
@@ -40,12 +41,31 @@ impl BaseKeymap {
|
||||
("TextMate", Self::TextMate),
|
||||
];
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub const OPTIONS: [(&'static str, Self); 4] = [
|
||||
("VSCode (Default)", Self::VSCode),
|
||||
("Atom", Self::Atom),
|
||||
("JetBrains", Self::JetBrains),
|
||||
("Sublime Text", Self::SublimeText),
|
||||
];
|
||||
|
||||
pub fn asset_path(&self) -> Option<&'static str> {
|
||||
#[cfg(target_os = "macos")]
|
||||
match self {
|
||||
BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
|
||||
BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
|
||||
BaseKeymap::Atom => Some("keymaps/atom.json"),
|
||||
BaseKeymap::TextMate => Some("keymaps/textmate.json"),
|
||||
BaseKeymap::JetBrains => Some("keymaps/macos/jetbrains.json"),
|
||||
BaseKeymap::SublimeText => Some("keymaps/macos/sublime_text.json"),
|
||||
BaseKeymap::Atom => Some("keymaps/macos/atom.json"),
|
||||
BaseKeymap::TextMate => Some("keymaps/macos/textmate.json"),
|
||||
BaseKeymap::VSCode => None,
|
||||
BaseKeymap::None => None,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
match self {
|
||||
BaseKeymap::JetBrains => Some("keymaps/linux/jetbrains.json"),
|
||||
BaseKeymap::SublimeText => Some("keymaps/linux/sublime_text.json"),
|
||||
BaseKeymap::Atom => Some("keymaps/linux/atom.json"),
|
||||
BaseKeymap::TextMate => None,
|
||||
BaseKeymap::VSCode => None,
|
||||
BaseKeymap::None => None,
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::{ItemHandle, Pane};
|
||||
use gpui::{
|
||||
AnyView, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
|
||||
WindowContext,
|
||||
AnyView, Decorations, IntoElement, ParentElement, Render, Styled, Subscription, View,
|
||||
ViewContext, WindowContext,
|
||||
};
|
||||
use std::any::TypeId;
|
||||
use theme::CLIENT_SIDE_DECORATION_ROUNDING;
|
||||
use ui::{h_flex, prelude::*};
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -40,8 +41,17 @@ impl Render for StatusBar {
|
||||
.gap(Spacing::Large.rems(cx))
|
||||
.py(Spacing::Small.rems(cx))
|
||||
.px(Spacing::Large.rems(cx))
|
||||
// .h_8()
|
||||
.bg(cx.theme().colors().status_bar_background)
|
||||
.map(|el| match cx.window_decorations() {
|
||||
Decorations::Server => el,
|
||||
Decorations::Client { tiling, .. } => el
|
||||
.when(!(tiling.bottom || tiling.right), |el| {
|
||||
el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.left), |el| {
|
||||
el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
}),
|
||||
})
|
||||
.child(self.render_left_tools(cx))
|
||||
.child(self.render_right_tools(cx))
|
||||
}
|
||||
|
||||
@@ -27,11 +27,13 @@ use futures::{
|
||||
Future, FutureExt, StreamExt,
|
||||
};
|
||||
use gpui::{
|
||||
action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size, Action,
|
||||
AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds,
|
||||
DragMoveEvent, Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global,
|
||||
KeyContext, Keystroke, ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel,
|
||||
Render, Size, Subscription, Task, View, WeakView, WindowBounds, WindowHandle, WindowOptions,
|
||||
action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
|
||||
transparent_black, Action, AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext,
|
||||
AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId,
|
||||
EventEmitter, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, ManagedView,
|
||||
Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge,
|
||||
Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds, WindowHandle,
|
||||
WindowOptions,
|
||||
};
|
||||
use item::{
|
||||
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
|
||||
@@ -4165,156 +4167,162 @@ impl Render for Workspace {
|
||||
let theme = cx.theme().clone();
|
||||
let colors = theme.colors();
|
||||
|
||||
self.actions(div(), cx)
|
||||
.key_context(context)
|
||||
.relative()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.font(ui_font)
|
||||
.gap_0()
|
||||
.justify_start()
|
||||
.items_start()
|
||||
.text_color(colors.text)
|
||||
.bg(colors.background)
|
||||
.children(self.titlebar_item.clone())
|
||||
.child(
|
||||
div()
|
||||
.id("workspace")
|
||||
.relative()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.overflow_hidden()
|
||||
.border_t_1()
|
||||
.border_b_1()
|
||||
.border_color(colors.border)
|
||||
.child({
|
||||
let this = cx.view().clone();
|
||||
canvas(
|
||||
move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
|
||||
|_, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.size_full()
|
||||
})
|
||||
.when(self.zoomed.is_none(), |this| {
|
||||
this.on_drag_move(cx.listener(
|
||||
|workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
|
||||
DockPosition::Left => {
|
||||
let size = workspace.bounds.left() + e.event.position.x;
|
||||
workspace.left_dock.update(cx, |left_dock, cx| {
|
||||
left_dock.resize_active_panel(Some(size), cx);
|
||||
});
|
||||
}
|
||||
DockPosition::Right => {
|
||||
let size = workspace.bounds.right() - e.event.position.x;
|
||||
workspace.right_dock.update(cx, |right_dock, cx| {
|
||||
right_dock.resize_active_panel(Some(size), cx);
|
||||
});
|
||||
}
|
||||
DockPosition::Bottom => {
|
||||
let size = workspace.bounds.bottom() - e.event.position.y;
|
||||
workspace.bottom_dock.update(cx, |bottom_dock, cx| {
|
||||
bottom_dock.resize_active_panel(Some(size), cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
))
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.h_full()
|
||||
// Left Dock
|
||||
.children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
|
||||
|| {
|
||||
div()
|
||||
.flex()
|
||||
.flex_none()
|
||||
.overflow_hidden()
|
||||
.child(self.left_dock.clone())
|
||||
client_side_decorations(
|
||||
self.actions(div(), cx)
|
||||
.key_context(context)
|
||||
.relative()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.font(ui_font)
|
||||
.gap_0()
|
||||
.justify_start()
|
||||
.items_start()
|
||||
.text_color(colors.text)
|
||||
.overflow_hidden()
|
||||
.children(self.titlebar_item.clone())
|
||||
.child(
|
||||
div()
|
||||
.id("workspace")
|
||||
.bg(colors.background)
|
||||
.relative()
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.overflow_hidden()
|
||||
.border_t_1()
|
||||
.border_b_1()
|
||||
.border_color(colors.border)
|
||||
.child({
|
||||
let this = cx.view().clone();
|
||||
canvas(
|
||||
move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
|
||||
|_, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.size_full()
|
||||
})
|
||||
.when(self.zoomed.is_none(), |this| {
|
||||
this.on_drag_move(cx.listener(
|
||||
|workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
|
||||
DockPosition::Left => {
|
||||
let size = e.event.position.x - workspace.bounds.left();
|
||||
workspace.left_dock.update(cx, |left_dock, cx| {
|
||||
left_dock.resize_active_panel(Some(size), cx);
|
||||
});
|
||||
}
|
||||
DockPosition::Right => {
|
||||
let size = workspace.bounds.right() - e.event.position.x;
|
||||
workspace.right_dock.update(cx, |right_dock, cx| {
|
||||
right_dock.resize_active_panel(Some(size), cx);
|
||||
});
|
||||
}
|
||||
DockPosition::Bottom => {
|
||||
let size = workspace.bounds.bottom() - e.event.position.y;
|
||||
workspace.bottom_dock.update(cx, |bottom_dock, cx| {
|
||||
bottom_dock.resize_active_panel(Some(size), cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
))
|
||||
// Panes
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.flex_1()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.when_some(paddings.0, |this, p| {
|
||||
this.child(p.border_r_1())
|
||||
})
|
||||
.child(self.center.render(
|
||||
&self.project,
|
||||
&self.follower_states,
|
||||
self.active_call(),
|
||||
&self.active_pane,
|
||||
self.zoomed.as_ref(),
|
||||
&self.app_state,
|
||||
cx,
|
||||
))
|
||||
.when_some(paddings.1, |this, p| {
|
||||
this.child(p.border_l_1())
|
||||
}),
|
||||
)
|
||||
.children(
|
||||
self.zoomed_position
|
||||
.ne(&Some(DockPosition::Bottom))
|
||||
.then(|| self.bottom_dock.clone()),
|
||||
),
|
||||
)
|
||||
// Right Dock
|
||||
.children(self.zoomed_position.ne(&Some(DockPosition::Right)).then(
|
||||
|| {
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.h_full()
|
||||
// Left Dock
|
||||
.children(self.zoomed_position.ne(&Some(DockPosition::Left)).then(
|
||||
|| {
|
||||
div()
|
||||
.flex()
|
||||
.flex_none()
|
||||
.overflow_hidden()
|
||||
.child(self.left_dock.clone())
|
||||
},
|
||||
))
|
||||
// Panes
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_none()
|
||||
.flex_col()
|
||||
.flex_1()
|
||||
.overflow_hidden()
|
||||
.child(self.right_dock.clone())
|
||||
},
|
||||
)),
|
||||
)
|
||||
.children(self.zoomed.as_ref().and_then(|view| {
|
||||
let zoomed_view = view.upgrade()?;
|
||||
let div = div()
|
||||
.occlude()
|
||||
.absolute()
|
||||
.overflow_hidden()
|
||||
.border_color(colors.border)
|
||||
.bg(colors.background)
|
||||
.child(zoomed_view)
|
||||
.inset_0()
|
||||
.shadow_lg();
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.when_some(paddings.0, |this, p| {
|
||||
this.child(p.border_r_1())
|
||||
})
|
||||
.child(self.center.render(
|
||||
&self.project,
|
||||
&self.follower_states,
|
||||
self.active_call(),
|
||||
&self.active_pane,
|
||||
self.zoomed.as_ref(),
|
||||
&self.app_state,
|
||||
cx,
|
||||
))
|
||||
.when_some(paddings.1, |this, p| {
|
||||
this.child(p.border_l_1())
|
||||
}),
|
||||
)
|
||||
.children(
|
||||
self.zoomed_position
|
||||
.ne(&Some(DockPosition::Bottom))
|
||||
.then(|| self.bottom_dock.clone()),
|
||||
),
|
||||
)
|
||||
// Right Dock
|
||||
.children(
|
||||
self.zoomed_position.ne(&Some(DockPosition::Right)).then(
|
||||
|| {
|
||||
div()
|
||||
.flex()
|
||||
.flex_none()
|
||||
.overflow_hidden()
|
||||
.child(self.right_dock.clone())
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.children(self.zoomed.as_ref().and_then(|view| {
|
||||
let zoomed_view = view.upgrade()?;
|
||||
let div = div()
|
||||
.occlude()
|
||||
.absolute()
|
||||
.overflow_hidden()
|
||||
.border_color(colors.border)
|
||||
.bg(colors.background)
|
||||
.child(zoomed_view)
|
||||
.inset_0()
|
||||
.shadow_lg();
|
||||
|
||||
Some(match self.zoomed_position {
|
||||
Some(DockPosition::Left) => div.right_2().border_r_1(),
|
||||
Some(DockPosition::Right) => div.left_2().border_l_1(),
|
||||
Some(DockPosition::Bottom) => div.top_2().border_t_1(),
|
||||
None => div.top_2().bottom_2().left_2().right_2().border_1(),
|
||||
})
|
||||
}))
|
||||
.child(self.modal_layer.clone())
|
||||
.children(self.render_notifications(cx)),
|
||||
)
|
||||
.child(self.status_bar.clone())
|
||||
.children(if self.project.read(cx).is_disconnected() {
|
||||
if let Some(render) = self.render_disconnected_overlay.take() {
|
||||
let result = render(self, cx);
|
||||
self.render_disconnected_overlay = Some(render);
|
||||
Some(result)
|
||||
Some(match self.zoomed_position {
|
||||
Some(DockPosition::Left) => div.right_2().border_r_1(),
|
||||
Some(DockPosition::Right) => div.left_2().border_l_1(),
|
||||
Some(DockPosition::Bottom) => div.top_2().border_t_1(),
|
||||
None => div.top_2().bottom_2().left_2().right_2().border_1(),
|
||||
})
|
||||
}))
|
||||
.child(self.modal_layer.clone())
|
||||
.children(self.render_notifications(cx)),
|
||||
)
|
||||
.child(self.status_bar.clone())
|
||||
.children(if self.project.read(cx).is_disconnected() {
|
||||
if let Some(render) = self.render_disconnected_overlay.take() {
|
||||
let result = render(self, cx);
|
||||
self.render_disconnected_overlay = Some(render);
|
||||
Some(result)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6474,3 +6482,222 @@ mod tests {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client_side_decorations(element: impl IntoElement, cx: &mut WindowContext) -> Stateful<Div> {
|
||||
const BORDER_SIZE: Pixels = px(1.0);
|
||||
let decorations = cx.window_decorations();
|
||||
|
||||
if matches!(decorations, Decorations::Client { .. }) {
|
||||
cx.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW);
|
||||
}
|
||||
|
||||
struct GlobalResizeEdge(ResizeEdge);
|
||||
impl Global for GlobalResizeEdge {}
|
||||
|
||||
div()
|
||||
.id("window-backdrop")
|
||||
.bg(transparent_black())
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { tiling, .. } => div
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |div| {
|
||||
div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.right), |div| {
|
||||
div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.left), |div| {
|
||||
div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!tiling.top, |div| {
|
||||
div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW)
|
||||
})
|
||||
.when(!tiling.bottom, |div| {
|
||||
div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW)
|
||||
})
|
||||
.when(!tiling.left, |div| {
|
||||
div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW)
|
||||
})
|
||||
.when(!tiling.right, |div| {
|
||||
div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW)
|
||||
})
|
||||
.on_mouse_move(move |e, cx| {
|
||||
let size = cx.window_bounds().get_bounds().size;
|
||||
let pos = e.position;
|
||||
|
||||
let new_edge =
|
||||
resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling);
|
||||
|
||||
let edge = cx.try_global::<GlobalResizeEdge>();
|
||||
if new_edge != edge.map(|edge| edge.0) {
|
||||
cx.window_handle()
|
||||
.update(cx, |workspace, cx| cx.notify(workspace.entity_id()))
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, move |e, cx| {
|
||||
let size = cx.window_bounds().get_bounds().size;
|
||||
let pos = e.position;
|
||||
|
||||
let edge = match resize_edge(
|
||||
pos,
|
||||
theme::CLIENT_SIDE_DECORATION_SHADOW,
|
||||
size,
|
||||
tiling,
|
||||
) {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
|
||||
cx.start_window_resize(edge);
|
||||
}),
|
||||
})
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.cursor(CursorStyle::Arrow)
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { tiling } => div
|
||||
.border_color(cx.theme().colors().border)
|
||||
.when(!(tiling.top || tiling.right), |div| {
|
||||
div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.top || tiling.left), |div| {
|
||||
div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.right), |div| {
|
||||
div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!(tiling.bottom || tiling.left), |div| {
|
||||
div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(!tiling.top, |div| div.border_t(BORDER_SIZE))
|
||||
.when(!tiling.bottom, |div| div.border_b(BORDER_SIZE))
|
||||
.when(!tiling.left, |div| div.border_l(BORDER_SIZE))
|
||||
.when(!tiling.right, |div| div.border_r(BORDER_SIZE))
|
||||
.when(!tiling.is_tiled(), |div| {
|
||||
div.shadow(smallvec::smallvec![gpui::BoxShadow {
|
||||
color: Hsla {
|
||||
h: 0.,
|
||||
s: 0.,
|
||||
l: 0.,
|
||||
a: 0.4,
|
||||
},
|
||||
blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2.,
|
||||
spread_radius: px(0.),
|
||||
offset: point(px(0.0), px(0.0)),
|
||||
}])
|
||||
}),
|
||||
})
|
||||
.on_mouse_move(|_e, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.bg(cx.theme().colors().border)
|
||||
.size_full()
|
||||
.child(element),
|
||||
)
|
||||
.map(|div| match decorations {
|
||||
Decorations::Server => div,
|
||||
Decorations::Client { tiling, .. } => div.child(
|
||||
canvas(
|
||||
|_bounds, cx| {
|
||||
cx.insert_hitbox(
|
||||
Bounds::new(
|
||||
point(px(0.0), px(0.0)),
|
||||
cx.window_bounds().get_bounds().size,
|
||||
),
|
||||
false,
|
||||
)
|
||||
},
|
||||
move |_bounds, hitbox, cx| {
|
||||
let mouse = cx.mouse_position();
|
||||
let size = cx.window_bounds().get_bounds().size;
|
||||
let Some(edge) =
|
||||
resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
cx.set_global(GlobalResizeEdge(edge));
|
||||
cx.set_cursor_style(
|
||||
match edge {
|
||||
ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown,
|
||||
ResizeEdge::Left | ResizeEdge::Right => {
|
||||
CursorStyle::ResizeLeftRight
|
||||
}
|
||||
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
|
||||
CursorStyle::ResizeUpLeftDownRight
|
||||
}
|
||||
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
|
||||
CursorStyle::ResizeUpRightDownLeft
|
||||
}
|
||||
},
|
||||
&hitbox,
|
||||
);
|
||||
},
|
||||
)
|
||||
.size_full()
|
||||
.absolute(),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn resize_edge(
|
||||
pos: Point<Pixels>,
|
||||
shadow_size: Pixels,
|
||||
window_size: Size<Pixels>,
|
||||
tiling: Tiling,
|
||||
) -> Option<ResizeEdge> {
|
||||
let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
|
||||
if bounds.contains(&pos) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
|
||||
let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
|
||||
if top_left_bounds.contains(&pos) {
|
||||
return Some(ResizeEdge::TopLeft);
|
||||
}
|
||||
|
||||
let top_right_bounds = Bounds::new(
|
||||
Point::new(window_size.width - corner_size.width, px(0.)),
|
||||
corner_size,
|
||||
);
|
||||
if top_right_bounds.contains(&pos) {
|
||||
return Some(ResizeEdge::TopRight);
|
||||
}
|
||||
|
||||
let bottom_left_bounds = Bounds::new(
|
||||
Point::new(px(0.), window_size.height - corner_size.height),
|
||||
corner_size,
|
||||
);
|
||||
if bottom_left_bounds.contains(&pos) {
|
||||
return Some(ResizeEdge::BottomLeft);
|
||||
}
|
||||
|
||||
let bottom_right_bounds = Bounds::new(
|
||||
Point::new(
|
||||
window_size.width - corner_size.width,
|
||||
window_size.height - corner_size.height,
|
||||
),
|
||||
corner_size,
|
||||
);
|
||||
if bottom_right_bounds.contains(&pos) {
|
||||
return Some(ResizeEdge::BottomRight);
|
||||
}
|
||||
|
||||
if !tiling.top && pos.y < shadow_size {
|
||||
Some(ResizeEdge::Top)
|
||||
} else if !tiling.bottom && pos.y > window_size.height - shadow_size {
|
||||
Some(ResizeEdge::Bottom)
|
||||
} else if !tiling.left && pos.x < shadow_size {
|
||||
Some(ResizeEdge::Left)
|
||||
} else if !tiling.right && pos.x > window_size.width - shadow_size {
|
||||
Some(ResizeEdge::Right)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.143.0"
|
||||
version = "0.143.4"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
preview
|
||||
@@ -6,9 +6,12 @@ GenericName=Text Editor
|
||||
Comment=A high-performance, multiplayer code editor.
|
||||
TryExec=$APP_CLI
|
||||
StartupNotify=$DO_STARTUP_NOTIFY
|
||||
StartupWMClass=$APP_ID
|
||||
Exec=$APP_CLI $APP_ARGS
|
||||
Icon=$APP_ICON
|
||||
Categories=Utility;TextEditor;Development;IDE;
|
||||
Keywords=zed;
|
||||
MimeType=text/plain;inode/directory;
|
||||
|
||||
[Desktop Action NewWorkspace]
|
||||
Exec=$APP_CLI --new $APP_ARGS
|
||||
Name=Open a new workspace
|
||||
|
||||
@@ -90,6 +90,11 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut AppContext) ->
|
||||
.find(|display| display.uuid().ok() == Some(uuid))
|
||||
});
|
||||
let app_id = ReleaseChannel::global(cx).app_id();
|
||||
let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") {
|
||||
Ok(val) if val == "server" => gpui::WindowDecorations::Server,
|
||||
Ok(val) if val == "client" => gpui::WindowDecorations::Client,
|
||||
_ => gpui::WindowDecorations::Client,
|
||||
};
|
||||
|
||||
WindowOptions {
|
||||
titlebar: Some(TitlebarOptions {
|
||||
@@ -105,6 +110,7 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut AppContext) ->
|
||||
display_id: display.map(|display| display.id()),
|
||||
window_background: cx.theme().window_background_appearance(),
|
||||
app_id: Some(app_id.to_owned()),
|
||||
window_decorations: Some(window_decorations),
|
||||
window_min_size: Some(gpui::Size {
|
||||
width: px(360.0),
|
||||
height: px(240.0),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::{
|
||||
div, opaque_grey, AppContext, EventEmitter, FocusHandle, FocusableView, FontWeight,
|
||||
InteractiveElement, IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse,
|
||||
Render, RenderablePromptHandle, Styled, ViewContext, VisualContext, WindowContext,
|
||||
div, AppContext, EventEmitter, FocusHandle, FocusableView, FontWeight, InteractiveElement,
|
||||
IntoElement, ParentElement, PromptHandle, PromptLevel, PromptResponse, Render,
|
||||
RenderablePromptHandle, Styled, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
@@ -31,6 +31,7 @@ pub fn fallback_prompt_renderer(
|
||||
detail: detail.map(ToString::to_string),
|
||||
actions: actions.iter().map(ToString::to_string).collect(),
|
||||
focus: cx.focus_handle(),
|
||||
active_action_id: actions.len() - 1,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -44,10 +45,12 @@ pub struct FallbackPromptRenderer {
|
||||
detail: Option<String>,
|
||||
actions: Vec<String>,
|
||||
focus: FocusHandle,
|
||||
active_action_id: usize,
|
||||
}
|
||||
|
||||
impl FallbackPromptRenderer {
|
||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(PromptResponse(0));
|
||||
cx.emit(PromptResponse(self.active_action_id));
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
@@ -55,7 +58,32 @@ impl FallbackPromptRenderer {
|
||||
cx.emit(PromptResponse(ix));
|
||||
}
|
||||
}
|
||||
|
||||
fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
|
||||
self.active_action_id = 0;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
|
||||
self.active_action_id = self.actions.len().saturating_sub(1);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
|
||||
self.active_action_id = (self.active_action_id + 1) % self.actions.len();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
if self.active_action_id > 0 {
|
||||
self.active_action_id -= 1;
|
||||
} else {
|
||||
self.active_action_id = self.actions.len().saturating_sub(1);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for FallbackPromptRenderer {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
@@ -66,6 +94,10 @@ impl Render for FallbackPromptRenderer {
|
||||
.track_focus(&self.focus)
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_prev))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.elevation_3(cx)
|
||||
.w_72()
|
||||
.overflow_hidden()
|
||||
@@ -87,11 +119,11 @@ impl Render for FallbackPromptRenderer {
|
||||
.child(detail)
|
||||
}))
|
||||
.child(h_flex().justify_end().gap_2().children(
|
||||
self.actions.iter().enumerate().rev().map(|(ix, action)| {
|
||||
self.actions.iter().rev().enumerate().map(|(ix, action)| {
|
||||
ui::Button::new(ix, action.clone())
|
||||
.label_size(LabelSize::Large)
|
||||
.style(ButtonStyle::Filled)
|
||||
.when(ix == 0, |el| {
|
||||
.when(ix == self.active_action_id, |el| {
|
||||
el.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
})
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
@@ -101,35 +133,24 @@ impl Render for FallbackPromptRenderer {
|
||||
}),
|
||||
));
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.occlude()
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.bg(opaque_grey(0.5, 0.6))
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0(),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_around()
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_around()
|
||||
.child(prompt),
|
||||
),
|
||||
)
|
||||
div().size_full().occlude().child(
|
||||
div()
|
||||
.size_full()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_around()
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_around()
|
||||
.child(prompt),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ cp "crates/zed/resources/app-icon$suffix@2x.png" "${zed_dir}/share/icons/hicolor
|
||||
export DO_STARTUP_NOTIFY="true"
|
||||
export APP_CLI="zed"
|
||||
export APP_ICON="zed"
|
||||
export APP_ARGS="%U"
|
||||
if [[ "$channel" == "preview" ]]; then
|
||||
export APP_NAME="Zed Preview"
|
||||
elif [[ "$channel" == "nightly" ]]; then
|
||||
|
||||
Reference in New Issue
Block a user