WIP: clap
This commit is contained in:
1
.cargo/baseview
Submodule
1
.cargo/baseview
Submodule
Submodule .cargo/baseview added at 237d323c72
1
.cargo/egui-baseview
Submodule
1
.cargo/egui-baseview
Submodule
Submodule .cargo/egui-baseview added at c5ad2f4bd0
65
.github/workflows/ci.yml
vendored
65
.github/workflows/ci.yml
vendored
@@ -81,6 +81,9 @@ jobs:
|
||||
if: runner.os != 'Windows'
|
||||
run: cargo bundle --release --features desktop --bin cagire-desktop --target ${{ matrix.target }}
|
||||
|
||||
- name: Bundle CLAP plugin
|
||||
run: cargo xtask bundle cagire-clap --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Zip macOS app bundle
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
@@ -122,6 +125,18 @@ jobs:
|
||||
name: ${{ matrix.artifact }}-desktop
|
||||
path: target/${{ matrix.target }}/release/cagire-desktop.exe
|
||||
|
||||
- name: Upload CLAP artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-clap
|
||||
path: target/bundled/cagire-clap.clap
|
||||
|
||||
- name: Upload VST3 artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact }}-vst3
|
||||
path: target/bundled/cagire-clap.vst3
|
||||
|
||||
universal-macos:
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
@@ -159,6 +174,34 @@ jobs:
|
||||
lipo -info Cagire.app/Contents/MacOS/cagire-desktop
|
||||
zip -r Cagire.app.zip Cagire.app
|
||||
|
||||
- name: Create universal CLAP plugin
|
||||
run: |
|
||||
mkdir -p cagire-clap.clap/Contents/MacOS
|
||||
cp artifacts/cagire-macos-aarch64-clap/cagire-clap.clap/Contents/Info.plist \
|
||||
cagire-clap.clap/Contents/ 2>/dev/null || true
|
||||
cp artifacts/cagire-macos-aarch64-clap/cagire-clap.clap/Contents/PkgInfo \
|
||||
cagire-clap.clap/Contents/ 2>/dev/null || true
|
||||
lipo -create \
|
||||
artifacts/cagire-macos-x86_64-clap/cagire-clap.clap/Contents/MacOS/cagire-clap \
|
||||
artifacts/cagire-macos-aarch64-clap/cagire-clap.clap/Contents/MacOS/cagire-clap \
|
||||
-output cagire-clap.clap/Contents/MacOS/cagire-clap
|
||||
lipo -info cagire-clap.clap/Contents/MacOS/cagire-clap
|
||||
|
||||
- name: Create universal VST3 plugin
|
||||
run: |
|
||||
mkdir -p cagire-clap.vst3/Contents/MacOS
|
||||
cp -R artifacts/cagire-macos-aarch64-vst3/cagire-clap.vst3/Contents/Info.plist \
|
||||
cagire-clap.vst3/Contents/ 2>/dev/null || true
|
||||
cp artifacts/cagire-macos-aarch64-vst3/cagire-clap.vst3/Contents/PkgInfo \
|
||||
cagire-clap.vst3/Contents/ 2>/dev/null || true
|
||||
cp -R artifacts/cagire-macos-aarch64-vst3/cagire-clap.vst3/Contents/Resources \
|
||||
cagire-clap.vst3/Contents/ 2>/dev/null || true
|
||||
lipo -create \
|
||||
artifacts/cagire-macos-x86_64-vst3/cagire-clap.vst3/Contents/MacOS/cagire-clap \
|
||||
artifacts/cagire-macos-aarch64-vst3/cagire-clap.vst3/Contents/MacOS/cagire-clap \
|
||||
-output cagire-clap.vst3/Contents/MacOS/cagire-clap
|
||||
lipo -info cagire-clap.vst3/Contents/MacOS/cagire-clap
|
||||
|
||||
- name: Build .pkg installer
|
||||
run: |
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
@@ -184,6 +227,18 @@ jobs:
|
||||
name: cagire-macos-universal-desktop
|
||||
path: Cagire.app.zip
|
||||
|
||||
- name: Upload universal CLAP plugin
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-macos-universal-clap
|
||||
path: cagire-clap.clap
|
||||
|
||||
- name: Upload universal VST3 plugin
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cagire-macos-universal-vst3
|
||||
path: cagire-clap.vst3
|
||||
|
||||
- name: Upload .pkg installer
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -215,6 +270,16 @@ jobs:
|
||||
cp "$dir/Cagire.app.zip" "release/cagire-macos-universal-desktop.app.zip"
|
||||
elif [[ "$name" == "cagire-macos-universal" ]]; then
|
||||
cp "$dir/cagire" "release/cagire-macos-universal"
|
||||
elif [[ "$name" == "cagire-macos-universal-clap" ]]; then
|
||||
cd "$dir" && zip -r "../../release/cagire-macos-universal-clap.zip" cagire-clap.clap && cd ../..
|
||||
elif [[ "$name" == "cagire-macos-universal-vst3" ]]; then
|
||||
cd "$dir" && zip -r "../../release/cagire-macos-universal-vst3.zip" cagire-clap.vst3 && cd ../..
|
||||
elif [[ "$name" == *-clap ]]; then
|
||||
base="${name%-clap}"
|
||||
cd "$dir" && zip -r "../../release/${base}-clap.zip" cagire-clap.clap && cd ../..
|
||||
elif [[ "$name" == *-vst3 ]]; then
|
||||
base="${name%-vst3}"
|
||||
cd "$dir" && zip -r "../../release/${base}-vst3.zip" cagire-clap.vst3 && cd ../..
|
||||
elif [[ "$name" == *-desktop ]]; then
|
||||
base="${name%-desktop}"
|
||||
if ls "$dir"/*.deb 1>/dev/null 2>&1; then
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,6 +3,9 @@ Cargo.lock
|
||||
*.prof
|
||||
.DS_Store
|
||||
|
||||
# Cargo config
|
||||
.cargo/config.toml
|
||||
|
||||
# Claude
|
||||
.claude/
|
||||
CLAUDE.md
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[workspace]
|
||||
members = ["crates/forth", "crates/markdown", "crates/project", "crates/ratatui"]
|
||||
members = ["crates/forth", "crates/markdown", "crates/project", "crates/ratatui", "crates/clap", "crates/baseview", "crates/egui-baseview", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.9"
|
||||
@@ -92,6 +92,12 @@ codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
|
||||
[patch."https://github.com/BillyDM/egui-baseview.git"]
|
||||
egui-baseview = { path = "crates/egui-baseview" }
|
||||
|
||||
[patch."https://github.com/RustAudio/baseview.git"]
|
||||
baseview = { path = "crates/baseview" }
|
||||
|
||||
[package.metadata.bundle.bin.cagire-desktop]
|
||||
name = "Cagire"
|
||||
identifier = "com.sova.cagire"
|
||||
|
||||
34
crates/baseview/Cargo.toml
Normal file
34
crates/baseview/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "baseview"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
opengl = ["uuid", "x11/glx"]
|
||||
|
||||
[dependencies]
|
||||
keyboard-types = { version = "0.6.1", default-features = false }
|
||||
raw-window-handle = "0.5"
|
||||
|
||||
[target.'cfg(target_os="linux")'.dependencies]
|
||||
x11rb = { version = "0.13.0", features = ["cursor", "resource_manager", "allow-unsafe-code"] }
|
||||
x11 = { version = "2.21", features = ["xlib", "xlib_xcb"] }
|
||||
nix = "0.22.0"
|
||||
|
||||
[target.'cfg(target_os="windows")'.dependencies]
|
||||
winapi = { version = "0.3.8", features = ["libloaderapi", "winuser", "windef", "minwindef", "guiddef", "combaseapi", "wingdi", "errhandlingapi", "ole2", "oleidl", "shellapi", "winerror"] }
|
||||
uuid = { version = "0.8", features = ["v4"], optional = true }
|
||||
|
||||
[target.'cfg(target_os="macos")'.dependencies]
|
||||
cocoa = "0.24.0"
|
||||
core-foundation = "0.9.1"
|
||||
objc = "0.2.7"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
|
||||
[lints.clippy]
|
||||
missing-safety-doc = "allow"
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = "allow"
|
||||
176
crates/baseview/LICENSE-APACHE
Normal file
176
crates/baseview/LICENSE-APACHE
Normal file
@@ -0,0 +1,176 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
23
crates/baseview/LICENSE-MIT
Normal file
23
crates/baseview/LICENSE-MIT
Normal file
@@ -0,0 +1,23 @@
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
10
crates/baseview/src/clipboard.rs
Normal file
10
crates/baseview/src/clipboard.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::macos as platform;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::win as platform;
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::x11 as platform;
|
||||
|
||||
pub fn copy_to_clipboard(data: &str) {
|
||||
platform::copy_to_clipboard(data)
|
||||
}
|
||||
162
crates/baseview/src/event.rs
Normal file
162
crates/baseview/src/event.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use keyboard_types::{KeyboardEvent, Modifiers};
|
||||
|
||||
use crate::{Point, WindowInfo};
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum MouseButton {
|
||||
Left,
|
||||
Middle,
|
||||
Right,
|
||||
Back,
|
||||
Forward,
|
||||
Other(u8),
|
||||
}
|
||||
|
||||
/// A scroll movement.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum ScrollDelta {
|
||||
/// A line-based scroll movement
|
||||
Lines {
|
||||
/// The number of horizontal lines scrolled
|
||||
x: f32,
|
||||
|
||||
/// The number of vertical lines scrolled
|
||||
y: f32,
|
||||
},
|
||||
/// A pixel-based scroll movement
|
||||
Pixels {
|
||||
/// The number of horizontal pixels scrolled
|
||||
x: f32,
|
||||
/// The number of vertical pixels scrolled
|
||||
y: f32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum MouseEvent {
|
||||
/// The mouse cursor was moved
|
||||
CursorMoved {
|
||||
/// The logical coordinates of the mouse position
|
||||
position: Point,
|
||||
/// The modifiers that were held down just before the event.
|
||||
modifiers: Modifiers,
|
||||
},
|
||||
|
||||
/// A mouse button was pressed.
|
||||
ButtonPressed {
|
||||
/// The button that was pressed.
|
||||
button: MouseButton,
|
||||
/// The modifiers that were held down just before the event.
|
||||
modifiers: Modifiers,
|
||||
},
|
||||
|
||||
/// A mouse button was released.
|
||||
ButtonReleased {
|
||||
/// The button that was released.
|
||||
button: MouseButton,
|
||||
/// The modifiers that were held down just before the event.
|
||||
modifiers: Modifiers,
|
||||
},
|
||||
|
||||
/// The mouse wheel was scrolled.
|
||||
WheelScrolled {
|
||||
/// How much was scrolled, in factional lines.
|
||||
delta: ScrollDelta,
|
||||
/// The modifiers that were held down just before the event.
|
||||
modifiers: Modifiers,
|
||||
},
|
||||
|
||||
/// The mouse cursor entered the window.
|
||||
///
|
||||
/// May not be available on all platforms.
|
||||
CursorEntered,
|
||||
|
||||
/// The mouse cursor left the window.
|
||||
///
|
||||
/// May not be available on all platforms.
|
||||
CursorLeft,
|
||||
|
||||
DragEntered {
|
||||
/// The logical coordinates of the mouse position
|
||||
position: Point,
|
||||
/// The modifiers that were held down just before the event.
|
||||
modifiers: Modifiers,
|
||||
/// Data being dragged
|
||||
data: DropData,
|
||||
},
|
||||
|
||||
DragMoved {
|
||||
/// The logical coordinates of the mouse position
|
||||
position: Point,
|
||||
/// The modifiers that were held down just before the event.
|
||||
modifiers: Modifiers,
|
||||
/// Data being dragged
|
||||
data: DropData,
|
||||
},
|
||||
|
||||
DragLeft,
|
||||
|
||||
DragDropped {
|
||||
/// The logical coordinates of the mouse position
|
||||
position: Point,
|
||||
/// The modifiers that were held down just before the event.
|
||||
modifiers: Modifiers,
|
||||
/// Data being dragged
|
||||
data: DropData,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum WindowEvent {
|
||||
Resized(WindowInfo),
|
||||
Focused,
|
||||
Unfocused,
|
||||
WillClose,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event {
|
||||
Mouse(MouseEvent),
|
||||
Keyboard(KeyboardEvent),
|
||||
Window(WindowEvent),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum DropEffect {
|
||||
Copy,
|
||||
Move,
|
||||
Link,
|
||||
Scroll,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum DropData {
|
||||
None,
|
||||
Files(Vec<PathBuf>),
|
||||
}
|
||||
|
||||
/// Return value for [WindowHandler::on_event](`crate::WindowHandler::on_event()`),
|
||||
/// indicating whether the event was handled by your window or should be passed
|
||||
/// back to the platform.
|
||||
///
|
||||
/// For most event types, this value won't have any effect. This is the case
|
||||
/// when there is no clear meaning of passing back the event to the platform,
|
||||
/// or it isn't obviously useful. Currently, only [`Event::Keyboard`] variants
|
||||
/// are supported.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum EventStatus {
|
||||
/// Event was handled by your window and will not be sent back to the
|
||||
/// platform for further processing.
|
||||
Captured,
|
||||
/// Event was **not** handled by your window, so pass it back to the
|
||||
/// platform. For parented windows, this usually means that the parent
|
||||
/// window will receive the event. This is useful for cases such as using
|
||||
/// DAW functionality for playing piano keys with the keyboard while a
|
||||
/// plugin window is in focus.
|
||||
Ignored,
|
||||
/// We are prepared to handle the data in the drag and dropping will
|
||||
/// result in [DropEffect]
|
||||
AcceptDrop(DropEffect),
|
||||
}
|
||||
154
crates/baseview/src/gl/macos.rs
Normal file
154
crates/baseview/src/gl/macos.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
// This is required because the objc crate is causing a lot of warnings: https://github.com/SSheldon/rust-objc/issues/125
|
||||
// Eventually we should migrate to the objc2 crate and remove this.
|
||||
#![allow(unexpected_cfgs)]
|
||||
|
||||
use std::ffi::c_void;
|
||||
use std::str::FromStr;
|
||||
|
||||
use raw_window_handle::RawWindowHandle;
|
||||
|
||||
use cocoa::appkit::{
|
||||
NSOpenGLContext, NSOpenGLContextParameter, NSOpenGLPFAAccelerated, NSOpenGLPFAAlphaSize,
|
||||
NSOpenGLPFAColorSize, NSOpenGLPFADepthSize, NSOpenGLPFADoubleBuffer, NSOpenGLPFAMultisample,
|
||||
NSOpenGLPFAOpenGLProfile, NSOpenGLPFASampleBuffers, NSOpenGLPFASamples, NSOpenGLPFAStencilSize,
|
||||
NSOpenGLPixelFormat, NSOpenGLProfileVersion3_2Core, NSOpenGLProfileVersion4_1Core,
|
||||
NSOpenGLProfileVersionLegacy, NSOpenGLView, NSView,
|
||||
};
|
||||
use cocoa::base::{id, nil, YES};
|
||||
use cocoa::foundation::NSSize;
|
||||
|
||||
use core_foundation::base::TCFType;
|
||||
use core_foundation::bundle::{CFBundleGetBundleWithIdentifier, CFBundleGetFunctionPointerForName};
|
||||
use core_foundation::string::CFString;
|
||||
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
|
||||
use super::{GlConfig, GlError, Profile};
|
||||
|
||||
pub type CreationFailedError = ();
|
||||
pub struct GlContext {
|
||||
view: id,
|
||||
context: id,
|
||||
}
|
||||
|
||||
impl GlContext {
|
||||
pub unsafe fn create(parent: &RawWindowHandle, config: GlConfig) -> Result<GlContext, GlError> {
|
||||
let handle = if let RawWindowHandle::AppKit(handle) = parent {
|
||||
handle
|
||||
} else {
|
||||
return Err(GlError::InvalidWindowHandle);
|
||||
};
|
||||
|
||||
if handle.ns_view.is_null() {
|
||||
return Err(GlError::InvalidWindowHandle);
|
||||
}
|
||||
|
||||
let parent_view = handle.ns_view as id;
|
||||
|
||||
let version = if config.version < (3, 2) && config.profile == Profile::Compatibility {
|
||||
NSOpenGLProfileVersionLegacy
|
||||
} else if config.version == (3, 2) && config.profile == Profile::Core {
|
||||
NSOpenGLProfileVersion3_2Core
|
||||
} else if config.version > (3, 2) && config.profile == Profile::Core {
|
||||
NSOpenGLProfileVersion4_1Core
|
||||
} else {
|
||||
return Err(GlError::VersionNotSupported);
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
let mut attrs = vec![
|
||||
NSOpenGLPFAOpenGLProfile as u32, version as u32,
|
||||
NSOpenGLPFAColorSize as u32, (config.red_bits + config.blue_bits + config.green_bits) as u32,
|
||||
NSOpenGLPFAAlphaSize as u32, config.alpha_bits as u32,
|
||||
NSOpenGLPFADepthSize as u32, config.depth_bits as u32,
|
||||
NSOpenGLPFAStencilSize as u32, config.stencil_bits as u32,
|
||||
NSOpenGLPFAAccelerated as u32,
|
||||
];
|
||||
|
||||
if let Some(samples) = config.samples {
|
||||
#[rustfmt::skip]
|
||||
attrs.extend_from_slice(&[
|
||||
NSOpenGLPFAMultisample as u32,
|
||||
NSOpenGLPFASampleBuffers as u32, 1,
|
||||
NSOpenGLPFASamples as u32, samples as u32,
|
||||
]);
|
||||
}
|
||||
|
||||
if config.double_buffer {
|
||||
attrs.push(NSOpenGLPFADoubleBuffer as u32);
|
||||
}
|
||||
|
||||
attrs.push(0);
|
||||
|
||||
let pixel_format = NSOpenGLPixelFormat::alloc(nil).initWithAttributes_(&attrs);
|
||||
|
||||
if pixel_format == nil {
|
||||
return Err(GlError::CreationFailed(()));
|
||||
}
|
||||
|
||||
let view =
|
||||
NSOpenGLView::alloc(nil).initWithFrame_pixelFormat_(parent_view.frame(), pixel_format);
|
||||
|
||||
if view == nil {
|
||||
return Err(GlError::CreationFailed(()));
|
||||
}
|
||||
|
||||
view.setWantsBestResolutionOpenGLSurface_(YES);
|
||||
|
||||
NSOpenGLView::display_(view);
|
||||
parent_view.addSubview_(view);
|
||||
|
||||
let context: id = msg_send![view, openGLContext];
|
||||
let () = msg_send![context, retain];
|
||||
|
||||
context.setValues_forParameter_(
|
||||
&(config.vsync as i32),
|
||||
NSOpenGLContextParameter::NSOpenGLCPSwapInterval,
|
||||
);
|
||||
|
||||
let () = msg_send![pixel_format, release];
|
||||
|
||||
Ok(GlContext { view, context })
|
||||
}
|
||||
|
||||
pub unsafe fn make_current(&self) {
|
||||
self.context.makeCurrentContext();
|
||||
}
|
||||
|
||||
pub unsafe fn make_not_current(&self) {
|
||||
NSOpenGLContext::clearCurrentContext(self.context);
|
||||
}
|
||||
|
||||
pub fn get_proc_address(&self, symbol: &str) -> *const c_void {
|
||||
let symbol_name = CFString::from_str(symbol).unwrap();
|
||||
let framework_name = CFString::from_str("com.apple.opengl").unwrap();
|
||||
let framework =
|
||||
unsafe { CFBundleGetBundleWithIdentifier(framework_name.as_concrete_TypeRef()) };
|
||||
|
||||
unsafe { CFBundleGetFunctionPointerForName(framework, symbol_name.as_concrete_TypeRef()) }
|
||||
}
|
||||
|
||||
pub fn swap_buffers(&self) {
|
||||
unsafe {
|
||||
self.context.flushBuffer();
|
||||
let () = msg_send![self.view, setNeedsDisplay: YES];
|
||||
}
|
||||
}
|
||||
|
||||
/// On macOS the `NSOpenGLView` needs to be resized separtely from our main view.
|
||||
pub(crate) fn resize(&self, size: NSSize) {
|
||||
unsafe { NSView::setFrameSize(self.view, size) };
|
||||
unsafe {
|
||||
let _: () = msg_send![self.view, setNeedsDisplay: YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GlContext {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let () = msg_send![self.context, release];
|
||||
let () = msg_send![self.view, release];
|
||||
}
|
||||
}
|
||||
}
|
||||
115
crates/baseview/src/gl/mod.rs
Normal file
115
crates/baseview/src/gl/mod.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use std::ffi::c_void;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
// On X11 creating the context is a two step process
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use raw_window_handle::RawWindowHandle;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win;
|
||||
#[cfg(target_os = "windows")]
|
||||
use win as platform;
|
||||
|
||||
// We need to use this directly within the X11 window creation to negotiate the correct visual
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) mod x11;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) use self::x11 as platform;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
#[cfg(target_os = "macos")]
|
||||
use macos as platform;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GlConfig {
|
||||
pub version: (u8, u8),
|
||||
pub profile: Profile,
|
||||
pub red_bits: u8,
|
||||
pub blue_bits: u8,
|
||||
pub green_bits: u8,
|
||||
pub alpha_bits: u8,
|
||||
pub depth_bits: u8,
|
||||
pub stencil_bits: u8,
|
||||
pub samples: Option<u8>,
|
||||
pub srgb: bool,
|
||||
pub double_buffer: bool,
|
||||
pub vsync: bool,
|
||||
}
|
||||
|
||||
impl Default for GlConfig {
|
||||
fn default() -> Self {
|
||||
GlConfig {
|
||||
version: (3, 2),
|
||||
profile: Profile::Core,
|
||||
red_bits: 8,
|
||||
blue_bits: 8,
|
||||
green_bits: 8,
|
||||
alpha_bits: 8,
|
||||
depth_bits: 24,
|
||||
stencil_bits: 8,
|
||||
samples: None,
|
||||
srgb: true,
|
||||
double_buffer: true,
|
||||
vsync: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Profile {
|
||||
Compatibility,
|
||||
Core,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GlError {
|
||||
InvalidWindowHandle,
|
||||
VersionNotSupported,
|
||||
CreationFailed(platform::CreationFailedError),
|
||||
}
|
||||
|
||||
pub struct GlContext {
|
||||
context: platform::GlContext,
|
||||
phantom: PhantomData<*mut ()>,
|
||||
}
|
||||
|
||||
impl GlContext {
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) unsafe fn create(
|
||||
parent: &RawWindowHandle, config: GlConfig,
|
||||
) -> Result<GlContext, GlError> {
|
||||
platform::GlContext::create(parent, config)
|
||||
.map(|context| GlContext { context, phantom: PhantomData })
|
||||
}
|
||||
|
||||
/// The X11 version needs to be set up in a different way compared to the Windows and macOS
|
||||
/// versions. So the platform-specific versions should be used to construct the context within
|
||||
/// baseview, and then this object can be passed to the user.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn new(context: platform::GlContext) -> GlContext {
|
||||
GlContext { context, phantom: PhantomData }
|
||||
}
|
||||
|
||||
pub unsafe fn make_current(&self) {
|
||||
self.context.make_current();
|
||||
}
|
||||
|
||||
pub unsafe fn make_not_current(&self) {
|
||||
self.context.make_not_current();
|
||||
}
|
||||
|
||||
pub fn get_proc_address(&self, symbol: &str) -> *const c_void {
|
||||
self.context.get_proc_address(symbol)
|
||||
}
|
||||
|
||||
pub fn swap_buffers(&self) {
|
||||
self.context.swap_buffers();
|
||||
}
|
||||
|
||||
/// On macOS the `NSOpenGLView` needs to be resized separtely from our main view.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) fn resize(&self, size: cocoa::foundation::NSSize) {
|
||||
self.context.resize(size);
|
||||
}
|
||||
}
|
||||
308
crates/baseview/src/gl/win.rs
Normal file
308
crates/baseview/src/gl/win.rs
Normal file
@@ -0,0 +1,308 @@
|
||||
use std::ffi::{c_void, CString, OsStr};
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
use raw_window_handle::RawWindowHandle;
|
||||
|
||||
use winapi::shared::minwindef::{HINSTANCE, HMODULE};
|
||||
use winapi::shared::ntdef::WCHAR;
|
||||
use winapi::shared::windef::{HDC, HGLRC, HWND};
|
||||
use winapi::um::libloaderapi::{FreeLibrary, GetProcAddress, LoadLibraryA};
|
||||
use winapi::um::wingdi::{
|
||||
wglCreateContext, wglDeleteContext, wglGetProcAddress, wglMakeCurrent, ChoosePixelFormat,
|
||||
DescribePixelFormat, SetPixelFormat, SwapBuffers, PFD_DOUBLEBUFFER, PFD_DRAW_TO_WINDOW,
|
||||
PFD_MAIN_PLANE, PFD_SUPPORT_OPENGL, PFD_TYPE_RGBA, PIXELFORMATDESCRIPTOR,
|
||||
};
|
||||
use winapi::um::winnt::IMAGE_DOS_HEADER;
|
||||
use winapi::um::winuser::{
|
||||
CreateWindowExW, DefWindowProcW, DestroyWindow, GetDC, RegisterClassW, ReleaseDC,
|
||||
UnregisterClassW, CS_OWNDC, CW_USEDEFAULT, WNDCLASSW,
|
||||
};
|
||||
|
||||
use super::{GlConfig, GlError, Profile};
|
||||
|
||||
// See https://www.khronos.org/registry/OpenGL/extensions/ARB/WGL_ARB_create_context.txt
|
||||
|
||||
type WglCreateContextAttribsARB = extern "system" fn(HDC, HGLRC, *const i32) -> HGLRC;
|
||||
|
||||
const WGL_CONTEXT_MAJOR_VERSION_ARB: i32 = 0x2091;
|
||||
const WGL_CONTEXT_MINOR_VERSION_ARB: i32 = 0x2092;
|
||||
const WGL_CONTEXT_PROFILE_MASK_ARB: i32 = 0x9126;
|
||||
|
||||
const WGL_CONTEXT_CORE_PROFILE_BIT_ARB: i32 = 0x00000001;
|
||||
const WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB: i32 = 0x00000002;
|
||||
|
||||
// See https://www.khronos.org/registry/OpenGL/extensions/ARB/WGL_ARB_pixel_format.txt
|
||||
|
||||
type WglChoosePixelFormatARB =
|
||||
extern "system" fn(HDC, *const i32, *const f32, u32, *mut i32, *mut u32) -> i32;
|
||||
|
||||
const WGL_DRAW_TO_WINDOW_ARB: i32 = 0x2001;
|
||||
const WGL_ACCELERATION_ARB: i32 = 0x2003;
|
||||
const WGL_SUPPORT_OPENGL_ARB: i32 = 0x2010;
|
||||
const WGL_DOUBLE_BUFFER_ARB: i32 = 0x2011;
|
||||
const WGL_PIXEL_TYPE_ARB: i32 = 0x2013;
|
||||
const WGL_RED_BITS_ARB: i32 = 0x2015;
|
||||
const WGL_GREEN_BITS_ARB: i32 = 0x2017;
|
||||
const WGL_BLUE_BITS_ARB: i32 = 0x2019;
|
||||
const WGL_ALPHA_BITS_ARB: i32 = 0x201B;
|
||||
const WGL_DEPTH_BITS_ARB: i32 = 0x2022;
|
||||
const WGL_STENCIL_BITS_ARB: i32 = 0x2023;
|
||||
|
||||
const WGL_FULL_ACCELERATION_ARB: i32 = 0x2027;
|
||||
const WGL_TYPE_RGBA_ARB: i32 = 0x202B;
|
||||
|
||||
// See https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_multisample.txt
|
||||
|
||||
const WGL_SAMPLE_BUFFERS_ARB: i32 = 0x2041;
|
||||
const WGL_SAMPLES_ARB: i32 = 0x2042;
|
||||
|
||||
// See https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_framebuffer_sRGB.txt
|
||||
|
||||
const WGL_FRAMEBUFFER_SRGB_CAPABLE_ARB: i32 = 0x20A9;
|
||||
|
||||
// See https://www.khronos.org/registry/OpenGL/extensions/EXT/WGL_EXT_swap_control.txt
|
||||
|
||||
type WglSwapIntervalEXT = extern "system" fn(i32) -> i32;
|
||||
|
||||
pub type CreationFailedError = ();
|
||||
pub struct GlContext {
|
||||
hwnd: HWND,
|
||||
hdc: HDC,
|
||||
hglrc: HGLRC,
|
||||
gl_library: HMODULE,
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
static __ImageBase: IMAGE_DOS_HEADER;
|
||||
}
|
||||
|
||||
impl GlContext {
|
||||
pub unsafe fn create(parent: &RawWindowHandle, config: GlConfig) -> Result<GlContext, GlError> {
|
||||
let handle = if let RawWindowHandle::Win32(handle) = parent {
|
||||
handle
|
||||
} else {
|
||||
return Err(GlError::InvalidWindowHandle);
|
||||
};
|
||||
|
||||
if handle.hwnd.is_null() {
|
||||
return Err(GlError::InvalidWindowHandle);
|
||||
}
|
||||
|
||||
// Create temporary window and context to load function pointers
|
||||
|
||||
let class_name_str = format!("raw-gl-context-window-{}", uuid::Uuid::new_v4().to_simple());
|
||||
let mut class_name: Vec<WCHAR> = OsStr::new(&class_name_str).encode_wide().collect();
|
||||
class_name.push(0);
|
||||
|
||||
let hinstance = &__ImageBase as *const IMAGE_DOS_HEADER as HINSTANCE;
|
||||
|
||||
let wnd_class = WNDCLASSW {
|
||||
style: CS_OWNDC,
|
||||
lpfnWndProc: Some(DefWindowProcW),
|
||||
hInstance: hinstance,
|
||||
lpszClassName: class_name.as_ptr(),
|
||||
..std::mem::zeroed()
|
||||
};
|
||||
|
||||
let class = RegisterClassW(&wnd_class);
|
||||
if class == 0 {
|
||||
return Err(GlError::CreationFailed(()));
|
||||
}
|
||||
|
||||
let hwnd_tmp = CreateWindowExW(
|
||||
0,
|
||||
class as *const WCHAR,
|
||||
[0].as_ptr(),
|
||||
0,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
CW_USEDEFAULT,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
hinstance,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
|
||||
if hwnd_tmp.is_null() {
|
||||
return Err(GlError::CreationFailed(()));
|
||||
}
|
||||
|
||||
let hdc_tmp = GetDC(hwnd_tmp);
|
||||
|
||||
let pfd_tmp = PIXELFORMATDESCRIPTOR {
|
||||
nSize: std::mem::size_of::<PIXELFORMATDESCRIPTOR>() as u16,
|
||||
nVersion: 1,
|
||||
dwFlags: PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
|
||||
iPixelType: PFD_TYPE_RGBA,
|
||||
cColorBits: 32,
|
||||
cAlphaBits: 8,
|
||||
cDepthBits: 24,
|
||||
cStencilBits: 8,
|
||||
iLayerType: PFD_MAIN_PLANE,
|
||||
..std::mem::zeroed()
|
||||
};
|
||||
|
||||
SetPixelFormat(hdc_tmp, ChoosePixelFormat(hdc_tmp, &pfd_tmp), &pfd_tmp);
|
||||
|
||||
let hglrc_tmp = wglCreateContext(hdc_tmp);
|
||||
if hglrc_tmp.is_null() {
|
||||
ReleaseDC(hwnd_tmp, hdc_tmp);
|
||||
UnregisterClassW(class as *const WCHAR, hinstance);
|
||||
DestroyWindow(hwnd_tmp);
|
||||
return Err(GlError::CreationFailed(()));
|
||||
}
|
||||
|
||||
wglMakeCurrent(hdc_tmp, hglrc_tmp);
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let wglCreateContextAttribsARB: Option<WglCreateContextAttribsARB> = {
|
||||
let symbol = CString::new("wglCreateContextAttribsARB").unwrap();
|
||||
let addr = wglGetProcAddress(symbol.as_ptr());
|
||||
if !addr.is_null() {
|
||||
#[allow(clippy::missing_transmute_annotations)]
|
||||
Some(std::mem::transmute(addr))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let wglChoosePixelFormatARB: Option<WglChoosePixelFormatARB> = {
|
||||
let symbol = CString::new("wglChoosePixelFormatARB").unwrap();
|
||||
let addr = wglGetProcAddress(symbol.as_ptr());
|
||||
if !addr.is_null() {
|
||||
#[allow(clippy::missing_transmute_annotations)]
|
||||
Some(std::mem::transmute(addr))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let wglSwapIntervalEXT: Option<WglSwapIntervalEXT> = {
|
||||
let symbol = CString::new("wglSwapIntervalEXT").unwrap();
|
||||
let addr = wglGetProcAddress(symbol.as_ptr());
|
||||
if !addr.is_null() {
|
||||
#[allow(clippy::missing_transmute_annotations)]
|
||||
Some(std::mem::transmute(addr))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
wglMakeCurrent(hdc_tmp, std::ptr::null_mut());
|
||||
wglDeleteContext(hglrc_tmp);
|
||||
ReleaseDC(hwnd_tmp, hdc_tmp);
|
||||
UnregisterClassW(class as *const WCHAR, hinstance);
|
||||
DestroyWindow(hwnd_tmp);
|
||||
|
||||
// Create actual context
|
||||
|
||||
let hwnd = handle.hwnd as HWND;
|
||||
|
||||
let hdc = GetDC(hwnd);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let pixel_format_attribs = [
|
||||
WGL_DRAW_TO_WINDOW_ARB, 1,
|
||||
WGL_ACCELERATION_ARB, WGL_FULL_ACCELERATION_ARB,
|
||||
WGL_SUPPORT_OPENGL_ARB, 1,
|
||||
WGL_DOUBLE_BUFFER_ARB, config.double_buffer as i32,
|
||||
WGL_PIXEL_TYPE_ARB, WGL_TYPE_RGBA_ARB,
|
||||
WGL_RED_BITS_ARB, config.red_bits as i32,
|
||||
WGL_GREEN_BITS_ARB, config.green_bits as i32,
|
||||
WGL_BLUE_BITS_ARB, config.blue_bits as i32,
|
||||
WGL_ALPHA_BITS_ARB, config.alpha_bits as i32,
|
||||
WGL_DEPTH_BITS_ARB, config.depth_bits as i32,
|
||||
WGL_STENCIL_BITS_ARB, config.stencil_bits as i32,
|
||||
WGL_SAMPLE_BUFFERS_ARB, config.samples.is_some() as i32,
|
||||
WGL_SAMPLES_ARB, config.samples.unwrap_or(0) as i32,
|
||||
WGL_FRAMEBUFFER_SRGB_CAPABLE_ARB, config.srgb as i32,
|
||||
0,
|
||||
];
|
||||
|
||||
let mut pixel_format = 0;
|
||||
let mut num_formats = 0;
|
||||
wglChoosePixelFormatARB.unwrap()(
|
||||
hdc,
|
||||
pixel_format_attribs.as_ptr(),
|
||||
std::ptr::null(),
|
||||
1,
|
||||
&mut pixel_format,
|
||||
&mut num_formats,
|
||||
);
|
||||
|
||||
let mut pfd: PIXELFORMATDESCRIPTOR = std::mem::zeroed();
|
||||
DescribePixelFormat(
|
||||
hdc,
|
||||
pixel_format,
|
||||
std::mem::size_of::<PIXELFORMATDESCRIPTOR>() as u32,
|
||||
&mut pfd,
|
||||
);
|
||||
SetPixelFormat(hdc, pixel_format, &pfd);
|
||||
|
||||
let profile_mask = match config.profile {
|
||||
Profile::Core => WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
|
||||
Profile::Compatibility => WGL_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB,
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
let ctx_attribs = [
|
||||
WGL_CONTEXT_MAJOR_VERSION_ARB, config.version.0 as i32,
|
||||
WGL_CONTEXT_MINOR_VERSION_ARB, config.version.1 as i32,
|
||||
WGL_CONTEXT_PROFILE_MASK_ARB, profile_mask,
|
||||
0
|
||||
];
|
||||
|
||||
let hglrc =
|
||||
wglCreateContextAttribsARB.unwrap()(hdc, std::ptr::null_mut(), ctx_attribs.as_ptr());
|
||||
if hglrc.is_null() {
|
||||
return Err(GlError::CreationFailed(()));
|
||||
}
|
||||
|
||||
let gl_library_name = CString::new("opengl32.dll").unwrap();
|
||||
let gl_library = LoadLibraryA(gl_library_name.as_ptr());
|
||||
|
||||
wglMakeCurrent(hdc, hglrc);
|
||||
wglSwapIntervalEXT.unwrap()(config.vsync as i32);
|
||||
wglMakeCurrent(hdc, std::ptr::null_mut());
|
||||
|
||||
Ok(GlContext { hwnd, hdc, hglrc, gl_library })
|
||||
}
|
||||
|
||||
pub unsafe fn make_current(&self) {
|
||||
wglMakeCurrent(self.hdc, self.hglrc);
|
||||
}
|
||||
|
||||
pub unsafe fn make_not_current(&self) {
|
||||
wglMakeCurrent(self.hdc, std::ptr::null_mut());
|
||||
}
|
||||
|
||||
pub fn get_proc_address(&self, symbol: &str) -> *const c_void {
|
||||
let symbol = CString::new(symbol).unwrap();
|
||||
let addr = unsafe { wglGetProcAddress(symbol.as_ptr()) as *const c_void };
|
||||
if !addr.is_null() {
|
||||
addr
|
||||
} else {
|
||||
unsafe { GetProcAddress(self.gl_library, symbol.as_ptr()) as *const c_void }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn swap_buffers(&self) {
|
||||
unsafe {
|
||||
SwapBuffers(self.hdc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GlContext {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
wglMakeCurrent(std::ptr::null_mut(), std::ptr::null_mut());
|
||||
wglDeleteContext(self.hglrc);
|
||||
ReleaseDC(self.hwnd, self.hdc);
|
||||
FreeLibrary(self.gl_library);
|
||||
}
|
||||
}
|
||||
}
|
||||
249
crates/baseview/src/gl/x11.rs
Normal file
249
crates/baseview/src/gl/x11.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
use std::ffi::{c_void, CString};
|
||||
use std::os::raw::{c_int, c_ulong};
|
||||
|
||||
use x11::glx;
|
||||
use x11::xlib;
|
||||
|
||||
use super::{GlConfig, GlError, Profile};
|
||||
|
||||
mod errors;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CreationFailedError {
|
||||
InvalidFBConfig,
|
||||
NoVisual,
|
||||
GetProcAddressFailed,
|
||||
MakeCurrentFailed,
|
||||
ContextCreationFailed,
|
||||
X11Error(errors::XLibError),
|
||||
}
|
||||
|
||||
impl From<errors::XLibError> for GlError {
|
||||
fn from(e: errors::XLibError) -> Self {
|
||||
GlError::CreationFailed(CreationFailedError::X11Error(e))
|
||||
}
|
||||
}
|
||||
|
||||
// See https://www.khronos.org/registry/OpenGL/extensions/ARB/GLX_ARB_create_context.txt
|
||||
|
||||
type GlXCreateContextAttribsARB = unsafe extern "C" fn(
|
||||
dpy: *mut xlib::Display,
|
||||
fbc: glx::GLXFBConfig,
|
||||
share_context: glx::GLXContext,
|
||||
direct: xlib::Bool,
|
||||
attribs: *const c_int,
|
||||
) -> glx::GLXContext;
|
||||
|
||||
// See https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_swap_control.txt
|
||||
|
||||
type GlXSwapIntervalEXT =
|
||||
unsafe extern "C" fn(dpy: *mut xlib::Display, drawable: glx::GLXDrawable, interval: i32);
|
||||
|
||||
// See https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_framebuffer_sRGB.txt
|
||||
|
||||
const GLX_FRAMEBUFFER_SRGB_CAPABLE_ARB: i32 = 0x20B2;
|
||||
|
||||
fn get_proc_address(symbol: &str) -> *const c_void {
|
||||
let symbol = CString::new(symbol).unwrap();
|
||||
unsafe { glx::glXGetProcAddress(symbol.as_ptr() as *const u8).unwrap() as *const c_void }
|
||||
}
|
||||
|
||||
pub struct GlContext {
|
||||
window: c_ulong,
|
||||
display: *mut xlib::_XDisplay,
|
||||
context: glx::GLXContext,
|
||||
}
|
||||
|
||||
/// The frame buffer configuration along with the general OpenGL configuration to somewhat minimize
|
||||
/// misuse.
|
||||
pub struct FbConfig {
|
||||
gl_config: GlConfig,
|
||||
fb_config: *mut glx::__GLXFBConfigRec,
|
||||
}
|
||||
|
||||
/// The configuration a window should be created with after calling
|
||||
/// [GlContext::get_fb_config_and_visual].
|
||||
pub struct WindowConfig {
|
||||
pub depth: u8,
|
||||
pub visual: u32,
|
||||
}
|
||||
|
||||
impl GlContext {
|
||||
/// Creating an OpenGL context under X11 works slightly different. Different OpenGL
|
||||
/// configurations require different framebuffer configurations, and to be able to use that
|
||||
/// context with a window the window needs to be created with a matching visual. This means that
|
||||
/// you need to decide on the framebuffer config before creating the window, ask the X11 server
|
||||
/// for a matching visual for that framebuffer config, crate the window with that visual, and
|
||||
/// only then create the OpenGL context.
|
||||
///
|
||||
/// Use [Self::get_fb_config_and_visual] to create both of these things.
|
||||
pub unsafe fn create(
|
||||
window: c_ulong, display: *mut xlib::_XDisplay, config: FbConfig,
|
||||
) -> Result<GlContext, GlError> {
|
||||
if display.is_null() {
|
||||
return Err(GlError::InvalidWindowHandle);
|
||||
}
|
||||
|
||||
errors::XErrorHandler::handle(display, |error_handler| {
|
||||
#[allow(non_snake_case)]
|
||||
let glXCreateContextAttribsARB: GlXCreateContextAttribsARB = {
|
||||
let addr = get_proc_address("glXCreateContextAttribsARB");
|
||||
if addr.is_null() {
|
||||
return Err(GlError::CreationFailed(CreationFailedError::GetProcAddressFailed));
|
||||
} else {
|
||||
#[allow(clippy::missing_transmute_annotations)]
|
||||
std::mem::transmute(addr)
|
||||
}
|
||||
};
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
let glXSwapIntervalEXT: GlXSwapIntervalEXT = {
|
||||
let addr = get_proc_address("glXSwapIntervalEXT");
|
||||
if addr.is_null() {
|
||||
return Err(GlError::CreationFailed(CreationFailedError::GetProcAddressFailed));
|
||||
} else {
|
||||
#[allow(clippy::missing_transmute_annotations)]
|
||||
std::mem::transmute(addr)
|
||||
}
|
||||
};
|
||||
|
||||
error_handler.check()?;
|
||||
|
||||
let profile_mask = match config.gl_config.profile {
|
||||
Profile::Core => glx::arb::GLX_CONTEXT_CORE_PROFILE_BIT_ARB,
|
||||
Profile::Compatibility => glx::arb::GLX_CONTEXT_COMPATIBILITY_PROFILE_BIT_ARB,
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
let ctx_attribs = [
|
||||
glx::arb::GLX_CONTEXT_MAJOR_VERSION_ARB, config.gl_config.version.0 as i32,
|
||||
glx::arb::GLX_CONTEXT_MINOR_VERSION_ARB, config.gl_config.version.1 as i32,
|
||||
glx::arb::GLX_CONTEXT_PROFILE_MASK_ARB, profile_mask,
|
||||
0,
|
||||
];
|
||||
|
||||
let context = glXCreateContextAttribsARB(
|
||||
display,
|
||||
config.fb_config,
|
||||
std::ptr::null_mut(),
|
||||
1,
|
||||
ctx_attribs.as_ptr(),
|
||||
);
|
||||
|
||||
error_handler.check()?;
|
||||
|
||||
if context.is_null() {
|
||||
return Err(GlError::CreationFailed(CreationFailedError::ContextCreationFailed));
|
||||
}
|
||||
|
||||
let res = glx::glXMakeCurrent(display, window, context);
|
||||
error_handler.check()?;
|
||||
if res == 0 {
|
||||
return Err(GlError::CreationFailed(CreationFailedError::MakeCurrentFailed));
|
||||
}
|
||||
|
||||
glXSwapIntervalEXT(display, window, config.gl_config.vsync as i32);
|
||||
error_handler.check()?;
|
||||
|
||||
if glx::glXMakeCurrent(display, 0, std::ptr::null_mut()) == 0 {
|
||||
error_handler.check()?;
|
||||
return Err(GlError::CreationFailed(CreationFailedError::MakeCurrentFailed));
|
||||
}
|
||||
|
||||
Ok(GlContext { window, display, context })
|
||||
})
|
||||
}
|
||||
|
||||
/// Find a matching framebuffer config and window visual for the given OpenGL configuration.
|
||||
/// This needs to be passed to [Self::create] along with a handle to a window that was created
|
||||
/// using the visual also returned from this function.
|
||||
pub unsafe fn get_fb_config_and_visual(
|
||||
display: *mut xlib::_XDisplay, config: GlConfig,
|
||||
) -> Result<(FbConfig, WindowConfig), GlError> {
|
||||
errors::XErrorHandler::handle(display, |error_handler| {
|
||||
let screen = xlib::XDefaultScreen(display);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let fb_attribs = [
|
||||
glx::GLX_X_RENDERABLE, 1,
|
||||
glx::GLX_X_VISUAL_TYPE, glx::GLX_TRUE_COLOR,
|
||||
glx::GLX_DRAWABLE_TYPE, glx::GLX_WINDOW_BIT,
|
||||
glx::GLX_RENDER_TYPE, glx::GLX_RGBA_BIT,
|
||||
glx::GLX_RED_SIZE, config.red_bits as i32,
|
||||
glx::GLX_GREEN_SIZE, config.green_bits as i32,
|
||||
glx::GLX_BLUE_SIZE, config.blue_bits as i32,
|
||||
glx::GLX_ALPHA_SIZE, config.alpha_bits as i32,
|
||||
glx::GLX_DEPTH_SIZE, config.depth_bits as i32,
|
||||
glx::GLX_STENCIL_SIZE, config.stencil_bits as i32,
|
||||
glx::GLX_DOUBLEBUFFER, config.double_buffer as i32,
|
||||
glx::GLX_SAMPLE_BUFFERS, config.samples.is_some() as i32,
|
||||
glx::GLX_SAMPLES, config.samples.unwrap_or(0) as i32,
|
||||
GLX_FRAMEBUFFER_SRGB_CAPABLE_ARB, config.srgb as i32,
|
||||
0,
|
||||
];
|
||||
|
||||
let mut n_configs = 0;
|
||||
let fb_config =
|
||||
glx::glXChooseFBConfig(display, screen, fb_attribs.as_ptr(), &mut n_configs);
|
||||
|
||||
error_handler.check()?;
|
||||
if n_configs <= 0 || fb_config.is_null() {
|
||||
return Err(GlError::CreationFailed(CreationFailedError::InvalidFBConfig));
|
||||
}
|
||||
|
||||
// Now that we have a matching framebuffer config, we need to know which visual matches
|
||||
// thsi config so the window is compatible with the OpenGL context we're about to create
|
||||
let fb_config = *fb_config;
|
||||
let visual = glx::glXGetVisualFromFBConfig(display, fb_config);
|
||||
if visual.is_null() {
|
||||
return Err(GlError::CreationFailed(CreationFailedError::NoVisual));
|
||||
}
|
||||
|
||||
Ok((
|
||||
FbConfig { fb_config, gl_config: config },
|
||||
WindowConfig { depth: (*visual).depth as u8, visual: (*visual).visualid as u32 },
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub unsafe fn make_current(&self) {
|
||||
errors::XErrorHandler::handle(self.display, |error_handler| {
|
||||
let res = glx::glXMakeCurrent(self.display, self.window, self.context);
|
||||
error_handler.check().unwrap();
|
||||
if res == 0 {
|
||||
panic!("make_current failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub unsafe fn make_not_current(&self) {
|
||||
errors::XErrorHandler::handle(self.display, |error_handler| {
|
||||
let res = glx::glXMakeCurrent(self.display, 0, std::ptr::null_mut());
|
||||
error_handler.check().unwrap();
|
||||
if res == 0 {
|
||||
panic!("make_not_current failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_proc_address(&self, symbol: &str) -> *const c_void {
|
||||
get_proc_address(symbol)
|
||||
}
|
||||
|
||||
pub fn swap_buffers(&self) {
|
||||
unsafe {
|
||||
errors::XErrorHandler::handle(self.display, |error_handler| {
|
||||
glx::glXSwapBuffers(self.display, self.window);
|
||||
error_handler.check().unwrap();
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GlContext {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
glx::glXDestroyContext(self.display, self.context);
|
||||
}
|
||||
}
|
||||
}
|
||||
166
crates/baseview/src/gl/x11/errors.rs
Normal file
166
crates/baseview/src/gl/x11/errors.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
use std::ffi::CStr;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use x11::xlib;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::error::Error;
|
||||
use std::os::raw::{c_int, c_uchar, c_ulong};
|
||||
use std::panic::AssertUnwindSafe;
|
||||
|
||||
thread_local! {
|
||||
/// Used as part of [`XErrorHandler::handle()`]. When an X11 error occurs during this function,
|
||||
/// the error gets copied to this RefCell after which the program is allowed to resume. The
|
||||
/// error can then be converted to a regular Rust Result value afterward.
|
||||
static CURRENT_X11_ERROR: RefCell<Option<XLibError>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// A helper struct for safe X11 error handling
|
||||
pub struct XErrorHandler<'a> {
|
||||
display: *mut xlib::Display,
|
||||
error: &'a RefCell<Option<XLibError>>,
|
||||
}
|
||||
|
||||
impl<'a> XErrorHandler<'a> {
|
||||
/// Syncs and checks if any previous X11 calls from the given display returned an error
|
||||
pub fn check(&mut self) -> Result<(), XLibError> {
|
||||
// Flush all possible previous errors
|
||||
unsafe {
|
||||
xlib::XSync(self.display, 0);
|
||||
}
|
||||
let error = self.error.borrow_mut().take();
|
||||
|
||||
match error {
|
||||
None => Ok(()),
|
||||
Some(inner) => Err(inner),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets up a temporary X11 error handler for the duration of the given closure, and allows
|
||||
/// that closure to check on the latest X11 error at any time.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The given display pointer *must* be and remain valid for the duration of this function, as
|
||||
/// well as for the duration of the given `handler` closure.
|
||||
pub unsafe fn handle<T, F: FnOnce(&mut XErrorHandler) -> T>(
|
||||
display: *mut xlib::Display, handler: F,
|
||||
) -> T {
|
||||
/// # Safety
|
||||
/// The given display and error pointers *must* be valid for the duration of this function.
|
||||
unsafe extern "C" fn error_handler(
|
||||
_dpy: *mut xlib::Display, err: *mut xlib::XErrorEvent,
|
||||
) -> i32 {
|
||||
// SAFETY: the error pointer should be safe to access
|
||||
let err = &*err;
|
||||
|
||||
CURRENT_X11_ERROR.with(|error| {
|
||||
let mut error = error.borrow_mut();
|
||||
match error.as_mut() {
|
||||
// If multiple errors occur, keep the first one since that's likely going to be the
|
||||
// cause of the other errors
|
||||
Some(_) => 1,
|
||||
None => {
|
||||
*error = Some(XLibError::from_event(err));
|
||||
0
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Flush all possible previous errors
|
||||
unsafe {
|
||||
xlib::XSync(display, 0);
|
||||
}
|
||||
|
||||
CURRENT_X11_ERROR.with(|error| {
|
||||
// Make sure to clear any errors from the last call to this function
|
||||
{
|
||||
*error.borrow_mut() = None;
|
||||
}
|
||||
|
||||
let old_handler = unsafe { xlib::XSetErrorHandler(Some(error_handler)) };
|
||||
let panic_result = std::panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
let mut h = XErrorHandler { display, error };
|
||||
handler(&mut h)
|
||||
}));
|
||||
// Whatever happened, restore old error handler
|
||||
unsafe { xlib::XSetErrorHandler(old_handler) };
|
||||
|
||||
match panic_result {
|
||||
Ok(v) => v,
|
||||
Err(e) => std::panic::resume_unwind(e),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct XLibError {
|
||||
type_: c_int,
|
||||
resourceid: xlib::XID,
|
||||
serial: c_ulong,
|
||||
error_code: c_uchar,
|
||||
request_code: c_uchar,
|
||||
minor_code: c_uchar,
|
||||
|
||||
display_name: Box<str>,
|
||||
}
|
||||
|
||||
impl XLibError {
|
||||
/// # Safety
|
||||
/// The display pointer inside error must be valid for the duration of this call
|
||||
unsafe fn from_event(error: &xlib::XErrorEvent) -> Self {
|
||||
Self {
|
||||
type_: error.type_,
|
||||
resourceid: error.resourceid,
|
||||
serial: error.serial,
|
||||
|
||||
error_code: error.error_code,
|
||||
request_code: error.request_code,
|
||||
minor_code: error.minor_code,
|
||||
|
||||
display_name: Self::get_display_name(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// The display pointer inside error must be valid for the duration of this call
|
||||
unsafe fn get_display_name(error: &xlib::XErrorEvent) -> Box<str> {
|
||||
let mut buf = [0; 255];
|
||||
unsafe {
|
||||
xlib::XGetErrorText(
|
||||
error.display,
|
||||
error.error_code.into(),
|
||||
buf.as_mut_ptr().cast(),
|
||||
(buf.len() - 1) as i32,
|
||||
);
|
||||
}
|
||||
|
||||
*buf.last_mut().unwrap() = 0;
|
||||
// SAFETY: whatever XGetErrorText did or not, we guaranteed there is a nul byte at the end of the buffer
|
||||
let cstr = unsafe { CStr::from_ptr(buf.as_mut_ptr().cast()) };
|
||||
|
||||
cstr.to_string_lossy().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for XLibError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("XLibError")
|
||||
.field("error_code", &self.error_code)
|
||||
.field("error_message", &self.display_name)
|
||||
.field("minor_code", &self.minor_code)
|
||||
.field("request_code", &self.request_code)
|
||||
.field("type", &self.type_)
|
||||
.field("resource_id", &self.resourceid)
|
||||
.field("serial", &self.serial)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for XLibError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "XLib error: {} (error code {})", &self.display_name, self.error_code)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for XLibError {}
|
||||
55
crates/baseview/src/keyboard.rs
Normal file
55
crates/baseview/src/keyboard.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright 2020 The Druid Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Baseview modifications to druid code:
|
||||
// - only keep code_to_location function
|
||||
|
||||
//! Keyboard types.
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
use keyboard_types::{Code, Location};
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
/// Map key code to location.
|
||||
///
|
||||
/// The logic for this is adapted from InitKeyEvent in TextInputHandler (in the Mozilla
|
||||
/// mac port).
|
||||
///
|
||||
/// Note: in the original, this is based on kVK constants, but since we don't have those
|
||||
/// readily available, we use the mapping to code (which should be effectively lossless).
|
||||
pub fn code_to_location(code: Code) -> Location {
|
||||
match code {
|
||||
Code::MetaLeft | Code::ShiftLeft | Code::AltLeft | Code::ControlLeft => Location::Left,
|
||||
Code::MetaRight | Code::ShiftRight | Code::AltRight | Code::ControlRight => Location::Right,
|
||||
Code::Numpad0
|
||||
| Code::Numpad1
|
||||
| Code::Numpad2
|
||||
| Code::Numpad3
|
||||
| Code::Numpad4
|
||||
| Code::Numpad5
|
||||
| Code::Numpad6
|
||||
| Code::Numpad7
|
||||
| Code::Numpad8
|
||||
| Code::Numpad9
|
||||
| Code::NumpadAdd
|
||||
| Code::NumpadComma
|
||||
| Code::NumpadDecimal
|
||||
| Code::NumpadDivide
|
||||
| Code::NumpadEnter
|
||||
| Code::NumpadEqual
|
||||
| Code::NumpadMultiply
|
||||
| Code::NumpadSubtract => Location::Numpad,
|
||||
_ => Location::Standard,
|
||||
}
|
||||
}
|
||||
24
crates/baseview/src/lib.rs
Normal file
24
crates/baseview/src/lib.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod x11;
|
||||
|
||||
mod clipboard;
|
||||
mod event;
|
||||
mod keyboard;
|
||||
mod mouse_cursor;
|
||||
mod window;
|
||||
mod window_info;
|
||||
mod window_open_options;
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
pub mod gl;
|
||||
|
||||
pub use clipboard::*;
|
||||
pub use event::*;
|
||||
pub use mouse_cursor::MouseCursor;
|
||||
pub use window::*;
|
||||
pub use window_info::*;
|
||||
pub use window_open_options::*;
|
||||
357
crates/baseview/src/macos/keyboard.rs
Normal file
357
crates/baseview/src/macos/keyboard.rs
Normal file
@@ -0,0 +1,357 @@
|
||||
// Copyright 2020 The Druid Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Baseview modifications to druid code:
|
||||
// - move from_nsstring function to this file
|
||||
// - update imports, paths etc
|
||||
|
||||
//! Conversion of platform keyboard event into cross-platform event.
|
||||
|
||||
use std::cell::Cell;
|
||||
|
||||
use cocoa::appkit::{NSEvent, NSEventModifierFlags, NSEventType};
|
||||
use cocoa::base::id;
|
||||
use cocoa::foundation::NSString;
|
||||
use keyboard_types::{Code, Key, KeyState, KeyboardEvent, Modifiers};
|
||||
use objc::{msg_send, sel, sel_impl};
|
||||
|
||||
use crate::keyboard::code_to_location;
|
||||
|
||||
pub(crate) fn from_nsstring(s: id) -> String {
|
||||
unsafe {
|
||||
let slice = std::slice::from_raw_parts(s.UTF8String() as *const _, s.len());
|
||||
let result = std::str::from_utf8_unchecked(slice);
|
||||
result.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// State for processing of keyboard events.
|
||||
///
|
||||
/// This needs to be stateful for proper processing of dead keys. The current
|
||||
/// implementation is somewhat primitive and is not based on IME; in the future
|
||||
/// when IME is implemented, it will need to be redone somewhat, letting the IME
|
||||
/// be the authoritative source of truth for Unicode string values of keys.
|
||||
///
|
||||
/// Most of the logic in this module is adapted from Mozilla, and in particular
|
||||
/// TextInputHandler.mm.
|
||||
pub(crate) struct KeyboardState {
|
||||
last_mods: Cell<NSEventModifierFlags>,
|
||||
}
|
||||
|
||||
/// Convert a macOS platform key code (keyCode field of NSEvent).
|
||||
///
|
||||
/// The primary source for this mapping is:
|
||||
/// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values
|
||||
///
|
||||
/// It should also match up with CODE_MAP_MAC bindings in
|
||||
/// NativeKeyToDOMCodeName.h.
|
||||
fn key_code_to_code(key_code: u16) -> Code {
|
||||
match key_code {
|
||||
0x00 => Code::KeyA,
|
||||
0x01 => Code::KeyS,
|
||||
0x02 => Code::KeyD,
|
||||
0x03 => Code::KeyF,
|
||||
0x04 => Code::KeyH,
|
||||
0x05 => Code::KeyG,
|
||||
0x06 => Code::KeyZ,
|
||||
0x07 => Code::KeyX,
|
||||
0x08 => Code::KeyC,
|
||||
0x09 => Code::KeyV,
|
||||
0x0a => Code::IntlBackslash,
|
||||
0x0b => Code::KeyB,
|
||||
0x0c => Code::KeyQ,
|
||||
0x0d => Code::KeyW,
|
||||
0x0e => Code::KeyE,
|
||||
0x0f => Code::KeyR,
|
||||
0x10 => Code::KeyY,
|
||||
0x11 => Code::KeyT,
|
||||
0x12 => Code::Digit1,
|
||||
0x13 => Code::Digit2,
|
||||
0x14 => Code::Digit3,
|
||||
0x15 => Code::Digit4,
|
||||
0x16 => Code::Digit6,
|
||||
0x17 => Code::Digit5,
|
||||
0x18 => Code::Equal,
|
||||
0x19 => Code::Digit9,
|
||||
0x1a => Code::Digit7,
|
||||
0x1b => Code::Minus,
|
||||
0x1c => Code::Digit8,
|
||||
0x1d => Code::Digit0,
|
||||
0x1e => Code::BracketRight,
|
||||
0x1f => Code::KeyO,
|
||||
0x20 => Code::KeyU,
|
||||
0x21 => Code::BracketLeft,
|
||||
0x22 => Code::KeyI,
|
||||
0x23 => Code::KeyP,
|
||||
0x24 => Code::Enter,
|
||||
0x25 => Code::KeyL,
|
||||
0x26 => Code::KeyJ,
|
||||
0x27 => Code::Quote,
|
||||
0x28 => Code::KeyK,
|
||||
0x29 => Code::Semicolon,
|
||||
0x2a => Code::Backslash,
|
||||
0x2b => Code::Comma,
|
||||
0x2c => Code::Slash,
|
||||
0x2d => Code::KeyN,
|
||||
0x2e => Code::KeyM,
|
||||
0x2f => Code::Period,
|
||||
0x30 => Code::Tab,
|
||||
0x31 => Code::Space,
|
||||
0x32 => Code::Backquote,
|
||||
0x33 => Code::Backspace,
|
||||
0x34 => Code::NumpadEnter,
|
||||
0x35 => Code::Escape,
|
||||
0x36 => Code::MetaRight,
|
||||
0x37 => Code::MetaLeft,
|
||||
0x38 => Code::ShiftLeft,
|
||||
0x39 => Code::CapsLock,
|
||||
// Note: in the linked source doc, this is "OSLeft"
|
||||
0x3a => Code::AltLeft,
|
||||
0x3b => Code::ControlLeft,
|
||||
0x3c => Code::ShiftRight,
|
||||
// Note: in the linked source doc, this is "OSRight"
|
||||
0x3d => Code::AltRight,
|
||||
0x3e => Code::ControlRight,
|
||||
0x3f => Code::Fn, // No events fired
|
||||
//0x40 => Code::F17,
|
||||
0x41 => Code::NumpadDecimal,
|
||||
0x43 => Code::NumpadMultiply,
|
||||
0x45 => Code::NumpadAdd,
|
||||
0x47 => Code::NumLock,
|
||||
0x48 => Code::AudioVolumeUp,
|
||||
0x49 => Code::AudioVolumeDown,
|
||||
0x4a => Code::AudioVolumeMute,
|
||||
0x4b => Code::NumpadDivide,
|
||||
0x4c => Code::NumpadEnter,
|
||||
0x4e => Code::NumpadSubtract,
|
||||
//0x4f => Code::F18,
|
||||
//0x50 => Code::F19,
|
||||
0x51 => Code::NumpadEqual,
|
||||
0x52 => Code::Numpad0,
|
||||
0x53 => Code::Numpad1,
|
||||
0x54 => Code::Numpad2,
|
||||
0x55 => Code::Numpad3,
|
||||
0x56 => Code::Numpad4,
|
||||
0x57 => Code::Numpad5,
|
||||
0x58 => Code::Numpad6,
|
||||
0x59 => Code::Numpad7,
|
||||
//0x5a => Code::F20,
|
||||
0x5b => Code::Numpad8,
|
||||
0x5c => Code::Numpad9,
|
||||
0x5d => Code::IntlYen,
|
||||
0x5e => Code::IntlRo,
|
||||
0x5f => Code::NumpadComma,
|
||||
0x60 => Code::F5,
|
||||
0x61 => Code::F6,
|
||||
0x62 => Code::F7,
|
||||
0x63 => Code::F3,
|
||||
0x64 => Code::F8,
|
||||
0x65 => Code::F9,
|
||||
0x66 => Code::Lang2,
|
||||
0x67 => Code::F11,
|
||||
0x68 => Code::Lang1,
|
||||
// Note: this is listed as F13, but in testing with a standard
|
||||
// USB kb, this the code produced by PrtSc.
|
||||
0x69 => Code::PrintScreen,
|
||||
//0x6a => Code::F16,
|
||||
//0x6b => Code::F14,
|
||||
0x6d => Code::F10,
|
||||
0x6e => Code::ContextMenu,
|
||||
0x6f => Code::F12,
|
||||
//0x71 => Code::F15,
|
||||
0x72 => Code::Help,
|
||||
0x73 => Code::Home,
|
||||
0x74 => Code::PageUp,
|
||||
0x75 => Code::Delete,
|
||||
0x76 => Code::F4,
|
||||
0x77 => Code::End,
|
||||
0x78 => Code::F2,
|
||||
0x79 => Code::PageDown,
|
||||
0x7a => Code::F1,
|
||||
0x7b => Code::ArrowLeft,
|
||||
0x7c => Code::ArrowRight,
|
||||
0x7d => Code::ArrowDown,
|
||||
0x7e => Code::ArrowUp,
|
||||
_ => Code::Unidentified,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert code to key.
|
||||
///
|
||||
/// On macOS, for non-printable keys, the keyCode we get from the event serves is
|
||||
/// really more of a key than a physical scan code.
|
||||
///
|
||||
/// When this function returns None, the code can be considered printable.
|
||||
///
|
||||
/// The logic for this function is derived from KEY_MAP_COCOA bindings in
|
||||
/// NativeKeyToDOMKeyName.h.
|
||||
fn code_to_key(code: Code) -> Option<Key> {
|
||||
Some(match code {
|
||||
Code::Escape => Key::Escape,
|
||||
Code::ShiftLeft | Code::ShiftRight => Key::Shift,
|
||||
Code::AltLeft | Code::AltRight => Key::Alt,
|
||||
Code::MetaLeft | Code::MetaRight => Key::Meta,
|
||||
Code::ControlLeft | Code::ControlRight => Key::Control,
|
||||
Code::CapsLock => Key::CapsLock,
|
||||
// kVK_ANSI_KeypadClear
|
||||
Code::NumLock => Key::Clear,
|
||||
Code::Fn => Key::Fn,
|
||||
Code::F1 => Key::F1,
|
||||
Code::F2 => Key::F2,
|
||||
Code::F3 => Key::F3,
|
||||
Code::F4 => Key::F4,
|
||||
Code::F5 => Key::F5,
|
||||
Code::F6 => Key::F6,
|
||||
Code::F7 => Key::F7,
|
||||
Code::F8 => Key::F8,
|
||||
Code::F9 => Key::F9,
|
||||
Code::F10 => Key::F10,
|
||||
Code::F11 => Key::F11,
|
||||
Code::F12 => Key::F12,
|
||||
Code::Pause => Key::Pause,
|
||||
Code::ScrollLock => Key::ScrollLock,
|
||||
Code::PrintScreen => Key::PrintScreen,
|
||||
Code::Insert => Key::Insert,
|
||||
Code::Delete => Key::Delete,
|
||||
Code::Tab => Key::Tab,
|
||||
Code::Backspace => Key::Backspace,
|
||||
Code::ContextMenu => Key::ContextMenu,
|
||||
// kVK_JIS_Kana
|
||||
Code::Lang1 => Key::KanjiMode,
|
||||
// kVK_JIS_Eisu
|
||||
Code::Lang2 => Key::Eisu,
|
||||
Code::Home => Key::Home,
|
||||
Code::End => Key::End,
|
||||
Code::PageUp => Key::PageUp,
|
||||
Code::PageDown => Key::PageDown,
|
||||
Code::ArrowLeft => Key::ArrowLeft,
|
||||
Code::ArrowRight => Key::ArrowRight,
|
||||
Code::ArrowUp => Key::ArrowUp,
|
||||
Code::ArrowDown => Key::ArrowDown,
|
||||
Code::Enter => Key::Enter,
|
||||
Code::NumpadEnter => Key::Enter,
|
||||
Code::Help => Key::Help,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn is_valid_key(s: &str) -> bool {
|
||||
match s.chars().next() {
|
||||
None => false,
|
||||
Some(c) => c >= ' ' && c != '\x7f' && !('\u{e000}'..'\u{f900}').contains(&c),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_modifier_code(code: Code) -> bool {
|
||||
matches!(
|
||||
code,
|
||||
Code::ShiftLeft
|
||||
| Code::ShiftRight
|
||||
| Code::AltLeft
|
||||
| Code::AltRight
|
||||
| Code::ControlLeft
|
||||
| Code::ControlRight
|
||||
| Code::MetaLeft
|
||||
| Code::MetaRight
|
||||
| Code::CapsLock
|
||||
| Code::Help
|
||||
)
|
||||
}
|
||||
|
||||
impl KeyboardState {
|
||||
pub(crate) fn new() -> KeyboardState {
|
||||
let last_mods = Cell::new(NSEventModifierFlags::empty());
|
||||
KeyboardState { last_mods }
|
||||
}
|
||||
|
||||
pub(crate) fn last_mods(&self) -> NSEventModifierFlags {
|
||||
self.last_mods.get()
|
||||
}
|
||||
|
||||
pub(crate) fn process_native_event(&self, event: id) -> Option<KeyboardEvent> {
|
||||
unsafe {
|
||||
let event_type = event.eventType();
|
||||
let key_code = event.keyCode();
|
||||
let code = key_code_to_code(key_code);
|
||||
let location = code_to_location(code);
|
||||
let raw_mods = event.modifierFlags();
|
||||
let modifiers = make_modifiers(raw_mods);
|
||||
let state = match event_type {
|
||||
NSEventType::NSKeyDown => KeyState::Down,
|
||||
NSEventType::NSKeyUp => KeyState::Up,
|
||||
NSEventType::NSFlagsChanged => {
|
||||
// We use `bits` here because we want to distinguish the
|
||||
// device dependent bits (when both left and right keys
|
||||
// may be pressed, for example).
|
||||
let any_down = raw_mods.bits() & !self.last_mods.get().bits();
|
||||
self.last_mods.set(raw_mods);
|
||||
if is_modifier_code(code) {
|
||||
if any_down == 0 {
|
||||
KeyState::Up
|
||||
} else {
|
||||
KeyState::Down
|
||||
}
|
||||
} else {
|
||||
// HandleFlagsChanged has some logic for this; it might
|
||||
// happen when an app is deactivated by Command-Tab. In
|
||||
// that case, the best thing to do is synthesize the event
|
||||
// from the modifiers. But a challenge there is that we
|
||||
// might get multiple events.
|
||||
return None;
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let is_composing = false;
|
||||
let repeat: bool = event_type == NSEventType::NSKeyDown && msg_send![event, isARepeat];
|
||||
let key = if let Some(key) = code_to_key(code) {
|
||||
key
|
||||
} else {
|
||||
let characters = from_nsstring(event.characters());
|
||||
if is_valid_key(&characters) {
|
||||
Key::Character(characters)
|
||||
} else {
|
||||
let chars_ignoring = from_nsstring(event.charactersIgnoringModifiers());
|
||||
if is_valid_key(&chars_ignoring) {
|
||||
Key::Character(chars_ignoring)
|
||||
} else {
|
||||
// There may be more heroic things we can do here.
|
||||
Key::Unidentified
|
||||
}
|
||||
}
|
||||
};
|
||||
let event =
|
||||
KeyboardEvent { code, key, location, modifiers, state, is_composing, repeat };
|
||||
Some(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MODIFIER_MAP: &[(NSEventModifierFlags, Modifiers)] = &[
|
||||
(NSEventModifierFlags::NSShiftKeyMask, Modifiers::SHIFT),
|
||||
(NSEventModifierFlags::NSAlternateKeyMask, Modifiers::ALT),
|
||||
(NSEventModifierFlags::NSControlKeyMask, Modifiers::CONTROL),
|
||||
(NSEventModifierFlags::NSCommandKeyMask, Modifiers::META),
|
||||
(NSEventModifierFlags::NSAlphaShiftKeyMask, Modifiers::CAPS_LOCK),
|
||||
];
|
||||
|
||||
pub(crate) fn make_modifiers(raw: NSEventModifierFlags) -> Modifiers {
|
||||
let mut modifiers = Modifiers::empty();
|
||||
for &(flags, mods) in MODIFIER_MAP {
|
||||
if raw.contains(flags) {
|
||||
modifiers |= mods;
|
||||
}
|
||||
}
|
||||
modifiers
|
||||
}
|
||||
21
crates/baseview/src/macos/mod.rs
Normal file
21
crates/baseview/src/macos/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
// This is required because the objc crate is causing a lot of warnings: https://github.com/SSheldon/rust-objc/issues/125
|
||||
// Eventually we should migrate to the objc2 crate and remove this.
|
||||
#![allow(unexpected_cfgs)]
|
||||
|
||||
mod keyboard;
|
||||
mod view;
|
||||
mod window;
|
||||
|
||||
pub use window::*;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
mod consts {
|
||||
use cocoa::foundation::NSUInteger;
|
||||
|
||||
pub const NSDragOperationNone: NSUInteger = 0;
|
||||
pub const NSDragOperationCopy: NSUInteger = 1;
|
||||
pub const NSDragOperationLink: NSUInteger = 2;
|
||||
pub const NSDragOperationGeneric: NSUInteger = 4;
|
||||
pub const NSDragOperationMove: NSUInteger = 16;
|
||||
}
|
||||
use consts::*;
|
||||
556
crates/baseview/src/macos/view.rs
Normal file
556
crates/baseview/src/macos/view.rs
Normal file
@@ -0,0 +1,556 @@
|
||||
use std::ffi::c_void;
|
||||
|
||||
use cocoa::appkit::{NSEvent, NSFilenamesPboardType, NSView, NSWindow};
|
||||
use cocoa::base::{id, nil, BOOL, NO, YES};
|
||||
use cocoa::foundation::{NSArray, NSPoint, NSRect, NSSize, NSUInteger};
|
||||
|
||||
use objc::{
|
||||
class,
|
||||
declare::ClassDecl,
|
||||
msg_send,
|
||||
runtime::{Class, Object, Sel},
|
||||
sel, sel_impl,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::MouseEvent::{ButtonPressed, ButtonReleased};
|
||||
use crate::{
|
||||
DropData, DropEffect, Event, EventStatus, MouseButton, MouseEvent, Point, ScrollDelta, Size,
|
||||
WindowEvent, WindowInfo, WindowOpenOptions,
|
||||
};
|
||||
|
||||
use super::keyboard::{from_nsstring, make_modifiers};
|
||||
use super::window::WindowState;
|
||||
use super::{
|
||||
NSDragOperationCopy, NSDragOperationGeneric, NSDragOperationLink, NSDragOperationMove,
|
||||
NSDragOperationNone,
|
||||
};
|
||||
|
||||
/// Name of the field used to store the `WindowState` pointer.
|
||||
pub(super) const BASEVIEW_STATE_IVAR: &str = "baseview_state";
|
||||
|
||||
#[link(name = "AppKit", kind = "framework")]
|
||||
extern "C" {
|
||||
static NSWindowDidBecomeKeyNotification: id;
|
||||
static NSWindowDidResignKeyNotification: id;
|
||||
}
|
||||
|
||||
macro_rules! add_simple_mouse_class_method {
|
||||
($class:ident, $sel:ident, $event:expr) => {
|
||||
#[allow(non_snake_case)]
|
||||
extern "C" fn $sel(this: &Object, _: Sel, _: id){
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
|
||||
state.trigger_event(Event::Mouse($event));
|
||||
}
|
||||
|
||||
$class.add_method(
|
||||
sel!($sel:),
|
||||
$sel as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/// Similar to [add_simple_mouse_class_method!], but this creates its own event object for the
|
||||
/// press/release event and adds the active modifier keys to that event.
|
||||
macro_rules! add_mouse_button_class_method {
|
||||
($class:ident, $sel:ident, $event_ty:ident, $button:expr) => {
|
||||
#[allow(non_snake_case)]
|
||||
extern "C" fn $sel(this: &Object, _: Sel, event: id){
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
|
||||
let modifiers = unsafe { NSEvent::modifierFlags(event) };
|
||||
|
||||
state.trigger_event(Event::Mouse($event_ty {
|
||||
button: $button,
|
||||
modifiers: make_modifiers(modifiers),
|
||||
}));
|
||||
}
|
||||
|
||||
$class.add_method(
|
||||
sel!($sel:),
|
||||
$sel as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! add_simple_keyboard_class_method {
|
||||
($class:ident, $sel:ident) => {
|
||||
#[allow(non_snake_case)]
|
||||
extern "C" fn $sel(this: &Object, _: Sel, event: id){
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
|
||||
if let Some(key_event) = state.process_native_key_event(event){
|
||||
let status = state.trigger_event(Event::Keyboard(key_event));
|
||||
|
||||
if let EventStatus::Ignored = status {
|
||||
unsafe {
|
||||
let superclass = msg_send![this, superclass];
|
||||
|
||||
let () = msg_send![super(this, superclass), $sel:event];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$class.add_method(
|
||||
sel!($sel:),
|
||||
$sel as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
unsafe fn register_notification(observer: id, notification_name: id, object: id) {
|
||||
let notification_center: id = msg_send![class!(NSNotificationCenter), defaultCenter];
|
||||
|
||||
let _: () = msg_send![
|
||||
notification_center,
|
||||
addObserver:observer
|
||||
selector:sel!(handleNotification:)
|
||||
name:notification_name
|
||||
object:object
|
||||
];
|
||||
}
|
||||
|
||||
pub(super) unsafe fn create_view(window_options: &WindowOpenOptions) -> id {
|
||||
let class = create_view_class();
|
||||
|
||||
let view: id = msg_send![class, alloc];
|
||||
|
||||
let size = window_options.size;
|
||||
|
||||
view.initWithFrame_(NSRect::new(NSPoint::new(0., 0.), NSSize::new(size.width, size.height)));
|
||||
|
||||
register_notification(view, NSWindowDidBecomeKeyNotification, nil);
|
||||
register_notification(view, NSWindowDidResignKeyNotification, nil);
|
||||
|
||||
let _: id = msg_send![
|
||||
view,
|
||||
registerForDraggedTypes: NSArray::arrayWithObjects(nil, &[NSFilenamesPboardType])
|
||||
];
|
||||
|
||||
view
|
||||
}
|
||||
|
||||
unsafe fn create_view_class() -> &'static Class {
|
||||
// Use unique class names so that there are no conflicts between different
|
||||
// instances. The class is deleted when the view is released. Previously,
|
||||
// the class was stored in a OnceCell after creation. This way, we didn't
|
||||
// have to recreate it each time a view was opened, but now we don't leave
|
||||
// any class definitions lying around when the plugin is closed.
|
||||
let class_name = format!("BaseviewNSView_{}", Uuid::new_v4().to_simple());
|
||||
let mut class = ClassDecl::new(&class_name, class!(NSView)).unwrap();
|
||||
|
||||
class.add_method(
|
||||
sel!(acceptsFirstResponder),
|
||||
property_yes as extern "C" fn(&Object, Sel) -> BOOL,
|
||||
);
|
||||
class.add_method(
|
||||
sel!(becomeFirstResponder),
|
||||
become_first_responder as extern "C" fn(&Object, Sel) -> BOOL,
|
||||
);
|
||||
class.add_method(
|
||||
sel!(resignFirstResponder),
|
||||
resign_first_responder as extern "C" fn(&Object, Sel) -> BOOL,
|
||||
);
|
||||
class.add_method(sel!(isFlipped), property_yes as extern "C" fn(&Object, Sel) -> BOOL);
|
||||
class.add_method(
|
||||
sel!(preservesContentInLiveResize),
|
||||
property_no as extern "C" fn(&Object, Sel) -> BOOL,
|
||||
);
|
||||
class.add_method(
|
||||
sel!(acceptsFirstMouse:),
|
||||
accepts_first_mouse as extern "C" fn(&Object, Sel, id) -> BOOL,
|
||||
);
|
||||
|
||||
class.add_method(
|
||||
sel!(windowShouldClose:),
|
||||
window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
|
||||
);
|
||||
class.add_method(sel!(dealloc), dealloc as extern "C" fn(&mut Object, Sel));
|
||||
class.add_method(
|
||||
sel!(viewWillMoveToWindow:),
|
||||
view_will_move_to_window as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
class.add_method(
|
||||
sel!(updateTrackingAreas:),
|
||||
update_tracking_areas as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
|
||||
class.add_method(sel!(mouseMoved:), mouse_moved as extern "C" fn(&Object, Sel, id));
|
||||
class.add_method(sel!(mouseDragged:), mouse_moved as extern "C" fn(&Object, Sel, id));
|
||||
class.add_method(sel!(rightMouseDragged:), mouse_moved as extern "C" fn(&Object, Sel, id));
|
||||
class.add_method(sel!(otherMouseDragged:), mouse_moved as extern "C" fn(&Object, Sel, id));
|
||||
|
||||
class.add_method(sel!(scrollWheel:), scroll_wheel as extern "C" fn(&Object, Sel, id));
|
||||
|
||||
class.add_method(
|
||||
sel!(viewDidChangeBackingProperties:),
|
||||
view_did_change_backing_properties as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
|
||||
class.add_method(
|
||||
sel!(draggingEntered:),
|
||||
dragging_entered as extern "C" fn(&Object, Sel, id) -> NSUInteger,
|
||||
);
|
||||
class.add_method(
|
||||
sel!(prepareForDragOperation:),
|
||||
prepare_for_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
|
||||
);
|
||||
class.add_method(
|
||||
sel!(performDragOperation:),
|
||||
perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
|
||||
);
|
||||
class.add_method(
|
||||
sel!(draggingUpdated:),
|
||||
dragging_updated as extern "C" fn(&Object, Sel, id) -> NSUInteger,
|
||||
);
|
||||
class.add_method(sel!(draggingExited:), dragging_exited as extern "C" fn(&Object, Sel, id));
|
||||
class.add_method(
|
||||
sel!(handleNotification:),
|
||||
handle_notification as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
|
||||
add_mouse_button_class_method!(class, mouseDown, ButtonPressed, MouseButton::Left);
|
||||
add_mouse_button_class_method!(class, mouseUp, ButtonReleased, MouseButton::Left);
|
||||
add_mouse_button_class_method!(class, rightMouseDown, ButtonPressed, MouseButton::Right);
|
||||
add_mouse_button_class_method!(class, rightMouseUp, ButtonReleased, MouseButton::Right);
|
||||
add_mouse_button_class_method!(class, otherMouseDown, ButtonPressed, MouseButton::Middle);
|
||||
add_mouse_button_class_method!(class, otherMouseUp, ButtonReleased, MouseButton::Middle);
|
||||
add_simple_mouse_class_method!(class, mouseEntered, MouseEvent::CursorEntered);
|
||||
add_simple_mouse_class_method!(class, mouseExited, MouseEvent::CursorLeft);
|
||||
|
||||
add_simple_keyboard_class_method!(class, keyDown);
|
||||
add_simple_keyboard_class_method!(class, keyUp);
|
||||
add_simple_keyboard_class_method!(class, flagsChanged);
|
||||
|
||||
class.add_ivar::<*mut c_void>(BASEVIEW_STATE_IVAR);
|
||||
|
||||
class.register()
|
||||
}
|
||||
|
||||
extern "C" fn property_yes(_this: &Object, _sel: Sel) -> BOOL {
|
||||
YES
|
||||
}
|
||||
|
||||
extern "C" fn property_no(_this: &Object, _sel: Sel) -> BOOL {
|
||||
NO
|
||||
}
|
||||
|
||||
extern "C" fn accepts_first_mouse(_this: &Object, _sel: Sel, _event: id) -> BOOL {
|
||||
YES
|
||||
}
|
||||
|
||||
extern "C" fn become_first_responder(this: &Object, _sel: Sel) -> BOOL {
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
let is_key_window = unsafe {
|
||||
let window: id = msg_send![this, window];
|
||||
if window != nil {
|
||||
let is_key_window: BOOL = msg_send![window, isKeyWindow];
|
||||
is_key_window == YES
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
if is_key_window {
|
||||
state.trigger_deferrable_event(Event::Window(WindowEvent::Focused));
|
||||
}
|
||||
YES
|
||||
}
|
||||
|
||||
extern "C" fn resign_first_responder(this: &Object, _sel: Sel) -> BOOL {
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
state.trigger_deferrable_event(Event::Window(WindowEvent::Unfocused));
|
||||
YES
|
||||
}
|
||||
|
||||
extern "C" fn window_should_close(this: &Object, _: Sel, _sender: id) -> BOOL {
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
|
||||
state.trigger_event(Event::Window(WindowEvent::WillClose));
|
||||
|
||||
state.window_inner.close();
|
||||
|
||||
NO
|
||||
}
|
||||
|
||||
extern "C" fn dealloc(this: &mut Object, _sel: Sel) {
|
||||
unsafe {
|
||||
let class = msg_send![this, class];
|
||||
|
||||
let superclass = msg_send![this, superclass];
|
||||
let () = msg_send![super(this, superclass), dealloc];
|
||||
|
||||
// Delete class
|
||||
::objc::runtime::objc_disposeClassPair(class);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel, _: id) {
|
||||
unsafe {
|
||||
let ns_window: *mut Object = msg_send![this, window];
|
||||
|
||||
let scale_factor: f64 =
|
||||
if ns_window.is_null() { 1.0 } else { NSWindow::backingScaleFactor(ns_window) };
|
||||
|
||||
let state = WindowState::from_view(this);
|
||||
|
||||
let bounds: NSRect = msg_send![this, bounds];
|
||||
|
||||
let new_window_info = WindowInfo::from_logical_size(
|
||||
Size::new(bounds.size.width, bounds.size.height),
|
||||
scale_factor,
|
||||
);
|
||||
|
||||
let window_info = state.window_info.get();
|
||||
|
||||
// Only send the event when the window's size has actually changed to be in line with the
|
||||
// other platform implementations
|
||||
if new_window_info.physical_size() != window_info.physical_size() {
|
||||
state.window_info.set(new_window_info);
|
||||
state.trigger_event(Event::Window(WindowEvent::Resized(new_window_info)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Init/reinit tracking area
|
||||
///
|
||||
/// Info:
|
||||
/// https://developer.apple.com/documentation/appkit/nstrackingarea
|
||||
/// https://developer.apple.com/documentation/appkit/nstrackingarea/options
|
||||
/// https://developer.apple.com/documentation/appkit/nstrackingareaoptions
|
||||
unsafe fn reinit_tracking_area(this: &Object, tracking_area: *mut Object) {
|
||||
let options: usize = {
|
||||
let mouse_entered_and_exited = 0x01;
|
||||
let tracking_mouse_moved = 0x02;
|
||||
let tracking_cursor_update = 0x04;
|
||||
let tracking_active_in_active_app = 0x40;
|
||||
let tracking_in_visible_rect = 0x200;
|
||||
let tracking_enabled_during_mouse_drag = 0x400;
|
||||
|
||||
mouse_entered_and_exited
|
||||
| tracking_mouse_moved
|
||||
| tracking_cursor_update
|
||||
| tracking_active_in_active_app
|
||||
| tracking_in_visible_rect
|
||||
| tracking_enabled_during_mouse_drag
|
||||
};
|
||||
|
||||
let bounds: NSRect = msg_send![this, bounds];
|
||||
|
||||
*tracking_area = msg_send![tracking_area,
|
||||
initWithRect:bounds
|
||||
options:options
|
||||
owner:this
|
||||
userInfo:nil
|
||||
];
|
||||
}
|
||||
|
||||
extern "C" fn view_will_move_to_window(this: &Object, _self: Sel, new_window: id) {
|
||||
unsafe {
|
||||
let tracking_areas: *mut Object = msg_send![this, trackingAreas];
|
||||
let tracking_area_count = NSArray::count(tracking_areas);
|
||||
|
||||
if new_window == nil {
|
||||
if tracking_area_count != 0 {
|
||||
let tracking_area = NSArray::objectAtIndex(tracking_areas, 0);
|
||||
|
||||
let _: () = msg_send![this, removeTrackingArea: tracking_area];
|
||||
let _: () = msg_send![tracking_area, release];
|
||||
}
|
||||
} else {
|
||||
if tracking_area_count == 0 {
|
||||
let class = Class::get("NSTrackingArea").unwrap();
|
||||
|
||||
let tracking_area: *mut Object = msg_send![class, alloc];
|
||||
|
||||
reinit_tracking_area(this, tracking_area);
|
||||
|
||||
let _: () = msg_send![this, addTrackingArea: tracking_area];
|
||||
}
|
||||
|
||||
let _: () = msg_send![new_window, setAcceptsMouseMovedEvents: YES];
|
||||
let _: () = msg_send![new_window, makeFirstResponder: this];
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let superclass = msg_send![this, superclass];
|
||||
|
||||
let () = msg_send![super(this, superclass), viewWillMoveToWindow: new_window];
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn update_tracking_areas(this: &Object, _self: Sel, _: id) {
|
||||
unsafe {
|
||||
let tracking_areas: *mut Object = msg_send![this, trackingAreas];
|
||||
let tracking_area = NSArray::objectAtIndex(tracking_areas, 0);
|
||||
|
||||
reinit_tracking_area(this, tracking_area);
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn mouse_moved(this: &Object, _sel: Sel, event: id) {
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
|
||||
let point: NSPoint = unsafe {
|
||||
let point = NSEvent::locationInWindow(event);
|
||||
|
||||
msg_send![this, convertPoint:point fromView:nil]
|
||||
};
|
||||
let modifiers = unsafe { NSEvent::modifierFlags(event) };
|
||||
|
||||
let position = Point { x: point.x, y: point.y };
|
||||
|
||||
state.trigger_event(Event::Mouse(MouseEvent::CursorMoved {
|
||||
position,
|
||||
modifiers: make_modifiers(modifiers),
|
||||
}));
|
||||
}
|
||||
|
||||
extern "C" fn scroll_wheel(this: &Object, _: Sel, event: id) {
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
|
||||
let delta = unsafe {
|
||||
let x = NSEvent::scrollingDeltaX(event) as f32;
|
||||
let y = NSEvent::scrollingDeltaY(event) as f32;
|
||||
|
||||
if NSEvent::hasPreciseScrollingDeltas(event) != NO {
|
||||
ScrollDelta::Pixels { x, y }
|
||||
} else {
|
||||
ScrollDelta::Lines { x, y }
|
||||
}
|
||||
};
|
||||
|
||||
let modifiers = unsafe { NSEvent::modifierFlags(event) };
|
||||
|
||||
state.trigger_event(Event::Mouse(MouseEvent::WheelScrolled {
|
||||
delta,
|
||||
modifiers: make_modifiers(modifiers),
|
||||
}));
|
||||
}
|
||||
|
||||
fn get_drag_position(sender: id) -> Point {
|
||||
let point: NSPoint = unsafe { msg_send![sender, draggingLocation] };
|
||||
Point::new(point.x, point.y)
|
||||
}
|
||||
|
||||
fn get_drop_data(sender: id) -> DropData {
|
||||
if sender == nil {
|
||||
return DropData::None;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let pasteboard: id = msg_send![sender, draggingPasteboard];
|
||||
let file_list: id = msg_send![pasteboard, propertyListForType: NSFilenamesPboardType];
|
||||
|
||||
if file_list == nil {
|
||||
return DropData::None;
|
||||
}
|
||||
|
||||
let mut files = vec![];
|
||||
for i in 0..NSArray::count(file_list) {
|
||||
let data = NSArray::objectAtIndex(file_list, i);
|
||||
files.push(from_nsstring(data).into());
|
||||
}
|
||||
|
||||
DropData::Files(files)
|
||||
}
|
||||
}
|
||||
|
||||
fn on_event(window_state: &WindowState, event: MouseEvent) -> NSUInteger {
|
||||
let event_status = window_state.trigger_event(Event::Mouse(event));
|
||||
match event_status {
|
||||
EventStatus::AcceptDrop(DropEffect::Copy) => NSDragOperationCopy,
|
||||
EventStatus::AcceptDrop(DropEffect::Move) => NSDragOperationMove,
|
||||
EventStatus::AcceptDrop(DropEffect::Link) => NSDragOperationLink,
|
||||
EventStatus::AcceptDrop(DropEffect::Scroll) => NSDragOperationGeneric,
|
||||
_ => NSDragOperationNone,
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn dragging_entered(this: &Object, _sel: Sel, sender: id) -> NSUInteger {
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
let modifiers = state.keyboard_state().last_mods();
|
||||
let drop_data = get_drop_data(sender);
|
||||
|
||||
let event = MouseEvent::DragEntered {
|
||||
position: get_drag_position(sender),
|
||||
modifiers: make_modifiers(modifiers),
|
||||
data: drop_data,
|
||||
};
|
||||
|
||||
on_event(&state, event)
|
||||
}
|
||||
|
||||
extern "C" fn dragging_updated(this: &Object, _sel: Sel, sender: id) -> NSUInteger {
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
let modifiers = state.keyboard_state().last_mods();
|
||||
let drop_data = get_drop_data(sender);
|
||||
|
||||
let event = MouseEvent::DragMoved {
|
||||
position: get_drag_position(sender),
|
||||
modifiers: make_modifiers(modifiers),
|
||||
data: drop_data,
|
||||
};
|
||||
|
||||
on_event(&state, event)
|
||||
}
|
||||
|
||||
extern "C" fn prepare_for_drag_operation(_this: &Object, _sel: Sel, _sender: id) -> BOOL {
|
||||
// Always accept drag operation if we get this far
|
||||
// This function won't be called unless dragging_entered/updated
|
||||
// has returned an acceptable operation
|
||||
YES
|
||||
}
|
||||
|
||||
extern "C" fn perform_drag_operation(this: &Object, _sel: Sel, sender: id) -> BOOL {
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
let modifiers = state.keyboard_state().last_mods();
|
||||
let drop_data = get_drop_data(sender);
|
||||
|
||||
let event = MouseEvent::DragDropped {
|
||||
position: get_drag_position(sender),
|
||||
modifiers: make_modifiers(modifiers),
|
||||
data: drop_data,
|
||||
};
|
||||
|
||||
let event_status = state.trigger_event(Event::Mouse(event));
|
||||
match event_status {
|
||||
EventStatus::AcceptDrop(_) => YES,
|
||||
_ => NO,
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" fn dragging_exited(this: &Object, _sel: Sel, _sender: id) {
|
||||
let state = unsafe { WindowState::from_view(this) };
|
||||
|
||||
on_event(&state, MouseEvent::DragLeft);
|
||||
}
|
||||
|
||||
extern "C" fn handle_notification(this: &Object, _cmd: Sel, notification: id) {
|
||||
unsafe {
|
||||
let state = WindowState::from_view(this);
|
||||
|
||||
// The subject of the notication, in this case an NSWindow object.
|
||||
let notification_object: id = msg_send![notification, object];
|
||||
|
||||
// The NSWindow object associated with our NSView.
|
||||
let window: id = msg_send![this, window];
|
||||
|
||||
let first_responder: id = msg_send![window, firstResponder];
|
||||
|
||||
// Only trigger focus events if the NSWindow that's being notified about is our window,
|
||||
// and if the window's first responder is our NSView.
|
||||
// If the first responder isn't our NSView, the focus events will instead be triggered
|
||||
// by the becomeFirstResponder and resignFirstResponder methods on the NSView itself.
|
||||
if notification_object == window && std::ptr::eq(first_responder, this) {
|
||||
let is_key_window: BOOL = msg_send![window, isKeyWindow];
|
||||
state.trigger_event(Event::Window(if is_key_window == YES {
|
||||
WindowEvent::Focused
|
||||
} else {
|
||||
WindowEvent::Unfocused
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
481
crates/baseview/src/macos/window.rs
Normal file
481
crates/baseview/src/macos/window.rs
Normal file
@@ -0,0 +1,481 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::c_void;
|
||||
use std::ptr;
|
||||
use std::rc::Rc;
|
||||
|
||||
use cocoa::appkit::{
|
||||
NSApp, NSApplication, NSApplicationActivationPolicyRegular, NSBackingStoreBuffered,
|
||||
NSPasteboard, NSView, NSWindow, NSWindowStyleMask,
|
||||
};
|
||||
use cocoa::base::{id, nil, BOOL, NO, YES};
|
||||
use cocoa::foundation::{NSAutoreleasePool, NSPoint, NSRect, NSSize, NSString};
|
||||
use core_foundation::runloop::{
|
||||
CFRunLoop, CFRunLoopTimer, CFRunLoopTimerContext, __CFRunLoopTimer, kCFRunLoopDefaultMode,
|
||||
};
|
||||
use keyboard_types::KeyboardEvent;
|
||||
use objc::class;
|
||||
use objc::{msg_send, runtime::Object, sel, sel_impl};
|
||||
use raw_window_handle::{
|
||||
AppKitDisplayHandle, AppKitWindowHandle, HasRawDisplayHandle, HasRawWindowHandle,
|
||||
RawDisplayHandle, RawWindowHandle,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Event, EventStatus, MouseCursor, Size, WindowHandler, WindowInfo, WindowOpenOptions,
|
||||
WindowScalePolicy,
|
||||
};
|
||||
|
||||
use super::keyboard::KeyboardState;
|
||||
use super::view::{create_view, BASEVIEW_STATE_IVAR};
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
use crate::gl::{GlConfig, GlContext};
|
||||
|
||||
pub struct WindowHandle {
|
||||
state: Rc<WindowState>,
|
||||
}
|
||||
|
||||
impl WindowHandle {
|
||||
pub fn close(&mut self) {
|
||||
self.state.window_inner.close();
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.state.window_inner.open.get()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl HasRawWindowHandle for WindowHandle {
|
||||
fn raw_window_handle(&self) -> RawWindowHandle {
|
||||
self.state.window_inner.raw_window_handle()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct WindowInner {
|
||||
open: Cell<bool>,
|
||||
|
||||
/// Only set if we created the parent window, i.e. we are running in
|
||||
/// parentless mode
|
||||
ns_app: Cell<Option<id>>,
|
||||
/// Only set if we created the parent window, i.e. we are running in
|
||||
/// parentless mode
|
||||
ns_window: Cell<Option<id>>,
|
||||
/// Our subclassed NSView
|
||||
ns_view: id,
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
gl_context: Option<GlContext>,
|
||||
}
|
||||
|
||||
impl WindowInner {
|
||||
pub(super) fn close(&self) {
|
||||
if self.open.get() {
|
||||
self.open.set(false);
|
||||
unsafe {
|
||||
// Take back ownership of the NSView's Rc<WindowState>
|
||||
let state_ptr: *const c_void = *(*self.ns_view).get_ivar(BASEVIEW_STATE_IVAR);
|
||||
let window_state = Rc::from_raw(state_ptr as *mut WindowState);
|
||||
|
||||
// Cancel the frame timer
|
||||
if let Some(frame_timer) = window_state.frame_timer.take() {
|
||||
CFRunLoop::get_current().remove_timer(&frame_timer, kCFRunLoopDefaultMode);
|
||||
}
|
||||
|
||||
// Deregister NSView from NotificationCenter.
|
||||
let notification_center: id =
|
||||
msg_send![class!(NSNotificationCenter), defaultCenter];
|
||||
let () = msg_send![notification_center, removeObserver:self.ns_view];
|
||||
|
||||
drop(window_state);
|
||||
|
||||
// Close the window if in non-parented mode
|
||||
if let Some(ns_window) = self.ns_window.take() {
|
||||
ns_window.close();
|
||||
}
|
||||
|
||||
// Ensure that the NSView is detached from the parent window
|
||||
self.ns_view.removeFromSuperview();
|
||||
let () = msg_send![self.ns_view as id, release];
|
||||
|
||||
// If in non-parented mode, we want to also quit the app altogether
|
||||
let app = self.ns_app.take();
|
||||
if let Some(app) = app {
|
||||
app.stop_(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn raw_window_handle(&self) -> RawWindowHandle {
|
||||
if self.open.get() {
|
||||
let ns_window = self.ns_window.get().unwrap_or(ptr::null_mut()) as *mut c_void;
|
||||
|
||||
let mut handle = AppKitWindowHandle::empty();
|
||||
handle.ns_window = ns_window;
|
||||
handle.ns_view = self.ns_view as *mut c_void;
|
||||
|
||||
return RawWindowHandle::AppKit(handle);
|
||||
}
|
||||
|
||||
RawWindowHandle::AppKit(AppKitWindowHandle::empty())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Window<'a> {
|
||||
inner: &'a WindowInner,
|
||||
}
|
||||
|
||||
impl<'a> Window<'a> {
|
||||
pub fn open_parented<P, H, B>(parent: &P, options: WindowOpenOptions, build: B) -> WindowHandle
|
||||
where
|
||||
P: HasRawWindowHandle,
|
||||
H: WindowHandler + 'static,
|
||||
B: FnOnce(&mut crate::Window) -> H,
|
||||
B: Send + 'static,
|
||||
{
|
||||
let pool = unsafe { NSAutoreleasePool::new(nil) };
|
||||
|
||||
let scaling = match options.scale {
|
||||
WindowScalePolicy::ScaleFactor(scale) => scale,
|
||||
WindowScalePolicy::SystemScaleFactor => 1.0,
|
||||
};
|
||||
|
||||
let window_info = WindowInfo::from_logical_size(options.size, scaling);
|
||||
|
||||
let handle = if let RawWindowHandle::AppKit(handle) = parent.raw_window_handle() {
|
||||
handle
|
||||
} else {
|
||||
panic!("Not a macOS window");
|
||||
};
|
||||
|
||||
let ns_view = unsafe { create_view(&options) };
|
||||
|
||||
let window_inner = WindowInner {
|
||||
open: Cell::new(true),
|
||||
ns_app: Cell::new(None),
|
||||
ns_window: Cell::new(None),
|
||||
ns_view,
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
gl_context: options
|
||||
.gl_config
|
||||
.map(|gl_config| Self::create_gl_context(None, ns_view, gl_config)),
|
||||
};
|
||||
|
||||
let window_handle = Self::init(window_inner, window_info, build);
|
||||
|
||||
unsafe {
|
||||
let _: id = msg_send![handle.ns_view as *mut Object, addSubview: ns_view];
|
||||
|
||||
let () = msg_send![pool, drain];
|
||||
}
|
||||
|
||||
window_handle
|
||||
}
|
||||
|
||||
pub fn open_blocking<H, B>(options: WindowOpenOptions, build: B)
|
||||
where
|
||||
H: WindowHandler + 'static,
|
||||
B: FnOnce(&mut crate::Window) -> H,
|
||||
B: Send + 'static,
|
||||
{
|
||||
let pool = unsafe { NSAutoreleasePool::new(nil) };
|
||||
|
||||
// It seems prudent to run NSApp() here before doing other
|
||||
// work. It runs [NSApplication sharedApplication], which is
|
||||
// what is run at the very start of the Xcode-generated main
|
||||
// function of a cocoa app according to:
|
||||
// https://developer.apple.com/documentation/appkit/nsapplication
|
||||
let app = unsafe { NSApp() };
|
||||
|
||||
unsafe {
|
||||
app.setActivationPolicy_(NSApplicationActivationPolicyRegular);
|
||||
}
|
||||
|
||||
let scaling = match options.scale {
|
||||
WindowScalePolicy::ScaleFactor(scale) => scale,
|
||||
WindowScalePolicy::SystemScaleFactor => 1.0,
|
||||
};
|
||||
|
||||
let window_info = WindowInfo::from_logical_size(options.size, scaling);
|
||||
|
||||
let rect = NSRect::new(
|
||||
NSPoint::new(0.0, 0.0),
|
||||
NSSize::new(window_info.logical_size().width, window_info.logical_size().height),
|
||||
);
|
||||
|
||||
let ns_window = unsafe {
|
||||
let ns_window = NSWindow::alloc(nil).initWithContentRect_styleMask_backing_defer_(
|
||||
rect,
|
||||
NSWindowStyleMask::NSTitledWindowMask
|
||||
| NSWindowStyleMask::NSClosableWindowMask
|
||||
| NSWindowStyleMask::NSMiniaturizableWindowMask,
|
||||
NSBackingStoreBuffered,
|
||||
NO,
|
||||
);
|
||||
ns_window.center();
|
||||
|
||||
let title = NSString::alloc(nil).init_str(&options.title).autorelease();
|
||||
ns_window.setTitle_(title);
|
||||
|
||||
ns_window.makeKeyAndOrderFront_(nil);
|
||||
|
||||
ns_window
|
||||
};
|
||||
|
||||
let ns_view = unsafe { create_view(&options) };
|
||||
|
||||
let window_inner = WindowInner {
|
||||
open: Cell::new(true),
|
||||
ns_app: Cell::new(Some(app)),
|
||||
ns_window: Cell::new(Some(ns_window)),
|
||||
ns_view,
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
gl_context: options
|
||||
.gl_config
|
||||
.map(|gl_config| Self::create_gl_context(Some(ns_window), ns_view, gl_config)),
|
||||
};
|
||||
|
||||
let _ = Self::init(window_inner, window_info, build);
|
||||
|
||||
unsafe {
|
||||
ns_window.setContentView_(ns_view);
|
||||
ns_window.setDelegate_(ns_view);
|
||||
|
||||
let () = msg_send![pool, drain];
|
||||
|
||||
app.run();
|
||||
}
|
||||
}
|
||||
|
||||
fn init<H, B>(window_inner: WindowInner, window_info: WindowInfo, build: B) -> WindowHandle
|
||||
where
|
||||
H: WindowHandler + 'static,
|
||||
B: FnOnce(&mut crate::Window) -> H,
|
||||
B: Send + 'static,
|
||||
{
|
||||
let mut window = crate::Window::new(Window { inner: &window_inner });
|
||||
let window_handler = Box::new(build(&mut window));
|
||||
|
||||
let ns_view = window_inner.ns_view;
|
||||
|
||||
let window_state = Rc::new(WindowState {
|
||||
window_inner,
|
||||
window_handler: RefCell::new(window_handler),
|
||||
keyboard_state: KeyboardState::new(),
|
||||
frame_timer: Cell::new(None),
|
||||
window_info: Cell::new(window_info),
|
||||
deferred_events: RefCell::default(),
|
||||
});
|
||||
|
||||
let window_state_ptr = Rc::into_raw(Rc::clone(&window_state));
|
||||
|
||||
unsafe {
|
||||
(*ns_view).set_ivar(BASEVIEW_STATE_IVAR, window_state_ptr as *const c_void);
|
||||
|
||||
WindowState::setup_timer(window_state_ptr);
|
||||
}
|
||||
|
||||
WindowHandle { state: window_state }
|
||||
}
|
||||
|
||||
pub fn close(&mut self) {
|
||||
self.inner.close();
|
||||
}
|
||||
|
||||
pub fn has_focus(&mut self) -> bool {
|
||||
unsafe {
|
||||
let view = self.inner.ns_view.as_mut().unwrap();
|
||||
let window: id = msg_send![view, window];
|
||||
if window == nil {
|
||||
return false;
|
||||
};
|
||||
let first_responder: id = msg_send![window, firstResponder];
|
||||
let is_key_window: BOOL = msg_send![window, isKeyWindow];
|
||||
let is_focused: BOOL = msg_send![view, isEqual: first_responder];
|
||||
is_key_window == YES && is_focused == YES
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus(&mut self) {
|
||||
unsafe {
|
||||
let view = self.inner.ns_view.as_mut().unwrap();
|
||||
let window: id = msg_send![view, window];
|
||||
if window != nil {
|
||||
msg_send![window, makeFirstResponder:view]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: Size) {
|
||||
if self.inner.open.get() {
|
||||
// NOTE: macOS gives you a personal rave if you pass in fractional pixels here. Even
|
||||
// though the size is in fractional pixels.
|
||||
let size = NSSize::new(size.width.round(), size.height.round());
|
||||
|
||||
unsafe { NSView::setFrameSize(self.inner.ns_view, size) };
|
||||
unsafe {
|
||||
let _: () = msg_send![self.inner.ns_view, setNeedsDisplay: YES];
|
||||
}
|
||||
|
||||
// When using OpenGL the `NSOpenGLView` needs to be resized separately? Why? Because
|
||||
// macOS.
|
||||
#[cfg(feature = "opengl")]
|
||||
if let Some(gl_context) = &self.inner.gl_context {
|
||||
gl_context.resize(size);
|
||||
}
|
||||
|
||||
// If this is a standalone window then we'll also need to resize the window itself
|
||||
if let Some(ns_window) = self.inner.ns_window.get() {
|
||||
unsafe { NSWindow::setContentSize_(ns_window, size) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_mouse_cursor(&mut self, _mouse_cursor: MouseCursor) {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
pub fn gl_context(&self) -> Option<&GlContext> {
|
||||
self.inner.gl_context.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
fn create_gl_context(ns_window: Option<id>, ns_view: id, config: GlConfig) -> GlContext {
|
||||
let mut handle = AppKitWindowHandle::empty();
|
||||
handle.ns_window = ns_window.unwrap_or(ptr::null_mut()) as *mut c_void;
|
||||
handle.ns_view = ns_view as *mut c_void;
|
||||
let handle = RawWindowHandle::AppKit(handle);
|
||||
|
||||
unsafe { GlContext::create(&handle, config).expect("Could not create OpenGL context") }
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct WindowState {
|
||||
pub(super) window_inner: WindowInner,
|
||||
window_handler: RefCell<Box<dyn WindowHandler>>,
|
||||
keyboard_state: KeyboardState,
|
||||
frame_timer: Cell<Option<CFRunLoopTimer>>,
|
||||
/// The last known window info for this window.
|
||||
pub window_info: Cell<WindowInfo>,
|
||||
|
||||
/// Events that will be triggered at the end of `window_handler`'s borrow.
|
||||
deferred_events: RefCell<VecDeque<Event>>,
|
||||
}
|
||||
|
||||
impl WindowState {
|
||||
/// Gets the `WindowState` held by a given `NSView`.
|
||||
///
|
||||
/// This method returns a cloned `Rc<WindowState>` rather than just a `&WindowState`, since the
|
||||
/// original `Rc<WindowState>` owned by the `NSView` can be dropped at any time
|
||||
/// (including during an event handler).
|
||||
pub(super) unsafe fn from_view(view: &Object) -> Rc<WindowState> {
|
||||
let state_ptr: *const c_void = *view.get_ivar(BASEVIEW_STATE_IVAR);
|
||||
|
||||
let state_rc = Rc::from_raw(state_ptr as *const WindowState);
|
||||
let state = Rc::clone(&state_rc);
|
||||
let _ = Rc::into_raw(state_rc);
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
/// Trigger the event immediately and return the event status.
|
||||
/// Will panic if `window_handler` is already borrowed (see `trigger_deferrable_event`).
|
||||
pub(super) fn trigger_event(&self, event: Event) -> EventStatus {
|
||||
let mut window = crate::Window::new(Window { inner: &self.window_inner });
|
||||
let mut window_handler = self.window_handler.borrow_mut();
|
||||
let status = window_handler.on_event(&mut window, event);
|
||||
self.send_deferred_events(window_handler.as_mut());
|
||||
status
|
||||
}
|
||||
|
||||
/// Trigger the event immediately if `window_handler` can be borrowed mutably,
|
||||
/// otherwise add the event to a queue that will be cleared once `window_handler`'s mutable borrow ends.
|
||||
/// As this method might result in the event triggering asynchronously, it can't reliably return the event status.
|
||||
pub(super) fn trigger_deferrable_event(&self, event: Event) {
|
||||
if let Ok(mut window_handler) = self.window_handler.try_borrow_mut() {
|
||||
let mut window = crate::Window::new(Window { inner: &self.window_inner });
|
||||
window_handler.on_event(&mut window, event);
|
||||
self.send_deferred_events(window_handler.as_mut());
|
||||
} else {
|
||||
self.deferred_events.borrow_mut().push_back(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn trigger_frame(&self) {
|
||||
let mut window = crate::Window::new(Window { inner: &self.window_inner });
|
||||
let mut window_handler = self.window_handler.borrow_mut();
|
||||
window_handler.on_frame(&mut window);
|
||||
self.send_deferred_events(window_handler.as_mut());
|
||||
}
|
||||
|
||||
pub(super) fn keyboard_state(&self) -> &KeyboardState {
|
||||
&self.keyboard_state
|
||||
}
|
||||
|
||||
pub(super) fn process_native_key_event(&self, event: *mut Object) -> Option<KeyboardEvent> {
|
||||
self.keyboard_state.process_native_event(event)
|
||||
}
|
||||
|
||||
unsafe fn setup_timer(window_state_ptr: *const WindowState) {
|
||||
extern "C" fn timer_callback(_: *mut __CFRunLoopTimer, window_state_ptr: *mut c_void) {
|
||||
unsafe {
|
||||
let window_state = &*(window_state_ptr as *const WindowState);
|
||||
|
||||
window_state.trigger_frame();
|
||||
}
|
||||
}
|
||||
|
||||
let mut timer_context = CFRunLoopTimerContext {
|
||||
version: 0,
|
||||
info: window_state_ptr as *mut c_void,
|
||||
retain: None,
|
||||
release: None,
|
||||
copyDescription: None,
|
||||
};
|
||||
|
||||
let timer = CFRunLoopTimer::new(0.0, 0.015, 0, 0, timer_callback, &mut timer_context);
|
||||
|
||||
CFRunLoop::get_current().add_timer(&timer, kCFRunLoopDefaultMode);
|
||||
|
||||
(*window_state_ptr).frame_timer.set(Some(timer));
|
||||
}
|
||||
|
||||
fn send_deferred_events(&self, window_handler: &mut dyn WindowHandler) {
|
||||
let mut window = crate::Window::new(Window { inner: &self.window_inner });
|
||||
loop {
|
||||
let next_event = self.deferred_events.borrow_mut().pop_front();
|
||||
if let Some(event) = next_event {
|
||||
window_handler.on_event(&mut window, event);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<'a> HasRawWindowHandle for Window<'a> {
|
||||
fn raw_window_handle(&self) -> RawWindowHandle {
|
||||
self.inner.raw_window_handle()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<'a> HasRawDisplayHandle for Window<'a> {
|
||||
fn raw_display_handle(&self) -> RawDisplayHandle {
|
||||
RawDisplayHandle::AppKit(AppKitDisplayHandle::empty())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_to_clipboard(string: &str) {
|
||||
unsafe {
|
||||
let pb = NSPasteboard::generalPasteboard(nil);
|
||||
|
||||
let ns_str = NSString::alloc(nil).init_str(string);
|
||||
|
||||
pb.clearContents();
|
||||
pb.setString_forType(ns_str, cocoa::appkit::NSPasteboardTypeString);
|
||||
}
|
||||
}
|
||||
44
crates/baseview/src/mouse_cursor.rs
Normal file
44
crates/baseview/src/mouse_cursor.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
#[derive(Debug, Default, Eq, PartialEq, Clone, Copy, PartialOrd, Ord, Hash)]
|
||||
pub enum MouseCursor {
|
||||
#[default]
|
||||
Default,
|
||||
Hand,
|
||||
HandGrabbing,
|
||||
Help,
|
||||
|
||||
Hidden,
|
||||
|
||||
Text,
|
||||
VerticalText,
|
||||
|
||||
Working,
|
||||
PtrWorking,
|
||||
|
||||
NotAllowed,
|
||||
PtrNotAllowed,
|
||||
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
|
||||
Alias,
|
||||
Copy,
|
||||
Move,
|
||||
AllScroll,
|
||||
Cell,
|
||||
Crosshair,
|
||||
|
||||
EResize,
|
||||
NResize,
|
||||
NeResize,
|
||||
NwResize,
|
||||
SResize,
|
||||
SeResize,
|
||||
SwResize,
|
||||
WResize,
|
||||
EwResize,
|
||||
NsResize,
|
||||
NwseResize,
|
||||
NeswResize,
|
||||
ColResize,
|
||||
RowResize,
|
||||
}
|
||||
54
crates/baseview/src/win/cursor.rs
Normal file
54
crates/baseview/src/win/cursor.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::MouseCursor;
|
||||
use winapi::{
|
||||
shared::ntdef::LPCWSTR,
|
||||
um::winuser::{
|
||||
IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, IDC_HELP, IDC_IBEAM, IDC_NO, IDC_SIZEALL,
|
||||
IDC_SIZENESW, IDC_SIZENS, IDC_SIZENWSE, IDC_SIZEWE, IDC_WAIT,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn cursor_to_lpcwstr(cursor: MouseCursor) -> LPCWSTR {
|
||||
match cursor {
|
||||
MouseCursor::Default => IDC_ARROW,
|
||||
MouseCursor::Hand => IDC_HAND,
|
||||
MouseCursor::HandGrabbing => IDC_SIZEALL,
|
||||
MouseCursor::Help => IDC_HELP,
|
||||
// an empty LPCWSTR results in the cursor being hidden
|
||||
MouseCursor::Hidden => std::ptr::null(),
|
||||
|
||||
MouseCursor::Text => IDC_IBEAM,
|
||||
MouseCursor::VerticalText => IDC_IBEAM,
|
||||
|
||||
MouseCursor::Working => IDC_WAIT,
|
||||
MouseCursor::PtrWorking => IDC_APPSTARTING,
|
||||
|
||||
MouseCursor::NotAllowed => IDC_NO,
|
||||
MouseCursor::PtrNotAllowed => IDC_NO,
|
||||
|
||||
MouseCursor::ZoomIn => IDC_ARROW,
|
||||
MouseCursor::ZoomOut => IDC_ARROW,
|
||||
|
||||
MouseCursor::Alias => IDC_ARROW,
|
||||
MouseCursor::Copy => IDC_ARROW,
|
||||
MouseCursor::Move => IDC_SIZEALL,
|
||||
MouseCursor::AllScroll => IDC_SIZEALL,
|
||||
MouseCursor::Cell => IDC_CROSS,
|
||||
MouseCursor::Crosshair => IDC_CROSS,
|
||||
|
||||
MouseCursor::EResize => IDC_SIZEWE,
|
||||
MouseCursor::NResize => IDC_SIZENS,
|
||||
MouseCursor::NeResize => IDC_SIZENESW,
|
||||
MouseCursor::NwResize => IDC_SIZENWSE,
|
||||
MouseCursor::SResize => IDC_SIZENS,
|
||||
MouseCursor::SeResize => IDC_SIZENWSE,
|
||||
MouseCursor::SwResize => IDC_SIZENESW,
|
||||
MouseCursor::WResize => IDC_SIZEWE,
|
||||
MouseCursor::EwResize => IDC_SIZEWE,
|
||||
MouseCursor::NsResize => IDC_SIZENS,
|
||||
MouseCursor::NwseResize => IDC_SIZENWSE,
|
||||
MouseCursor::NeswResize => IDC_SIZENESW,
|
||||
|
||||
MouseCursor::ColResize => IDC_SIZEWE,
|
||||
MouseCursor::RowResize => IDC_SIZENS,
|
||||
}
|
||||
}
|
||||
282
crates/baseview/src/win/drop_target.rs
Normal file
282
crates/baseview/src/win/drop_target.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
use std::ffi::OsString;
|
||||
use std::mem::transmute;
|
||||
use std::os::windows::prelude::OsStringExt;
|
||||
use std::ptr::null_mut;
|
||||
use std::rc::{Rc, Weak};
|
||||
|
||||
use winapi::shared::guiddef::{IsEqualIID, REFIID};
|
||||
use winapi::shared::minwindef::{DWORD, WPARAM};
|
||||
use winapi::shared::ntdef::{HRESULT, ULONG};
|
||||
use winapi::shared::windef::{POINT, POINTL};
|
||||
use winapi::shared::winerror::{E_NOINTERFACE, E_UNEXPECTED, S_OK};
|
||||
use winapi::shared::wtypes::DVASPECT_CONTENT;
|
||||
use winapi::um::objidl::{IDataObject, FORMATETC, STGMEDIUM, TYMED_HGLOBAL};
|
||||
use winapi::um::oleidl::{
|
||||
IDropTarget, IDropTargetVtbl, DROPEFFECT_COPY, DROPEFFECT_LINK, DROPEFFECT_MOVE,
|
||||
DROPEFFECT_NONE, DROPEFFECT_SCROLL,
|
||||
};
|
||||
use winapi::um::shellapi::{DragQueryFileW, HDROP};
|
||||
use winapi::um::unknwnbase::{IUnknown, IUnknownVtbl};
|
||||
use winapi::um::winuser::{ScreenToClient, CF_HDROP};
|
||||
use winapi::Interface;
|
||||
|
||||
use crate::{DropData, DropEffect, Event, EventStatus, MouseEvent, PhyPoint, Point};
|
||||
|
||||
use super::WindowState;
|
||||
|
||||
// These function pointers have to be stored in a (const) variable before they can be transmuted
|
||||
// Transmuting is needed because winapi has a bug where the pt parameter has an incorrect
|
||||
// type `*const POINTL`
|
||||
#[allow(non_snake_case)]
|
||||
const DRAG_ENTER_PTR: unsafe extern "system" fn(
|
||||
this: *mut IDropTarget,
|
||||
pDataObj: *const IDataObject,
|
||||
grfKeyState: DWORD,
|
||||
pt: POINTL,
|
||||
pdwEffect: *mut DWORD,
|
||||
) -> HRESULT = DropTarget::drag_enter;
|
||||
#[allow(non_snake_case)]
|
||||
const DRAG_OVER_PTR: unsafe extern "system" fn(
|
||||
this: *mut IDropTarget,
|
||||
grfKeyState: DWORD,
|
||||
pt: POINTL,
|
||||
pdwEffect: *mut DWORD,
|
||||
) -> HRESULT = DropTarget::drag_over;
|
||||
#[allow(non_snake_case)]
|
||||
const DROP_PTR: unsafe extern "system" fn(
|
||||
this: *mut IDropTarget,
|
||||
pDataObj: *const IDataObject,
|
||||
grfKeyState: DWORD,
|
||||
pt: POINTL,
|
||||
pdwEffect: *mut DWORD,
|
||||
) -> HRESULT = DropTarget::drop;
|
||||
|
||||
#[allow(clippy::missing_transmute_annotations)]
|
||||
const DROP_TARGET_VTBL: IDropTargetVtbl = IDropTargetVtbl {
|
||||
parent: IUnknownVtbl {
|
||||
QueryInterface: DropTarget::query_interface,
|
||||
AddRef: DropTarget::add_ref,
|
||||
Release: DropTarget::release,
|
||||
},
|
||||
DragEnter: unsafe { transmute(DRAG_ENTER_PTR) },
|
||||
DragOver: unsafe { transmute(DRAG_OVER_PTR) },
|
||||
DragLeave: DropTarget::drag_leave,
|
||||
Drop: unsafe { transmute(DROP_PTR) },
|
||||
};
|
||||
|
||||
#[repr(C)]
|
||||
pub(super) struct DropTarget {
|
||||
base: IDropTarget,
|
||||
|
||||
window_state: Weak<WindowState>,
|
||||
|
||||
// These are cached since DragOver and DragLeave callbacks don't provide them,
|
||||
// and handling drag move events gets awkward on the client end otherwise
|
||||
drag_position: Point,
|
||||
drop_data: DropData,
|
||||
}
|
||||
|
||||
impl DropTarget {
|
||||
pub(super) fn new(window_state: Weak<WindowState>) -> Self {
|
||||
Self {
|
||||
base: IDropTarget { lpVtbl: &DROP_TARGET_VTBL },
|
||||
|
||||
window_state,
|
||||
|
||||
drag_position: Point::new(0.0, 0.0),
|
||||
drop_data: DropData::None,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn on_event(&self, pdwEffect: Option<*mut DWORD>, event: MouseEvent) {
|
||||
let Some(window_state) = self.window_state.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
unsafe {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
let event = Event::Mouse(event);
|
||||
let event_status =
|
||||
window_state.handler_mut().as_mut().unwrap().on_event(&mut window, event);
|
||||
|
||||
if let Some(pdwEffect) = pdwEffect {
|
||||
match event_status {
|
||||
EventStatus::AcceptDrop(DropEffect::Copy) => *pdwEffect = DROPEFFECT_COPY,
|
||||
EventStatus::AcceptDrop(DropEffect::Move) => *pdwEffect = DROPEFFECT_MOVE,
|
||||
EventStatus::AcceptDrop(DropEffect::Link) => *pdwEffect = DROPEFFECT_LINK,
|
||||
EventStatus::AcceptDrop(DropEffect::Scroll) => *pdwEffect = DROPEFFECT_SCROLL,
|
||||
_ => *pdwEffect = DROPEFFECT_NONE,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_coordinates(&mut self, pt: POINTL) {
|
||||
let Some(window_state) = self.window_state.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let mut pt = POINT { x: pt.x, y: pt.y };
|
||||
unsafe { ScreenToClient(window_state.hwnd, &mut pt as *mut POINT) };
|
||||
let phy_point = PhyPoint::new(pt.x, pt.y);
|
||||
self.drag_position = phy_point.to_logical(&window_state.window_info());
|
||||
}
|
||||
|
||||
fn parse_drop_data(&mut self, data_object: &IDataObject) {
|
||||
let format = FORMATETC {
|
||||
cfFormat: CF_HDROP as u16,
|
||||
ptd: null_mut(),
|
||||
dwAspect: DVASPECT_CONTENT,
|
||||
lindex: -1,
|
||||
tymed: TYMED_HGLOBAL,
|
||||
};
|
||||
|
||||
let mut medium = STGMEDIUM { tymed: 0, u: null_mut(), pUnkForRelease: null_mut() };
|
||||
|
||||
unsafe {
|
||||
let hresult = data_object.GetData(&format, &mut medium);
|
||||
if hresult != S_OK {
|
||||
self.drop_data = DropData::None;
|
||||
return;
|
||||
}
|
||||
|
||||
let hdrop = *(*medium.u).hGlobal() as HDROP;
|
||||
|
||||
let item_count = DragQueryFileW(hdrop, 0xFFFFFFFF, null_mut(), 0);
|
||||
if item_count == 0 {
|
||||
self.drop_data = DropData::None;
|
||||
return;
|
||||
}
|
||||
|
||||
let mut paths = Vec::with_capacity(item_count as usize);
|
||||
|
||||
for i in 0..item_count {
|
||||
let characters = DragQueryFileW(hdrop, i, null_mut(), 0);
|
||||
let buffer_size = characters as usize + 1;
|
||||
let mut buffer = vec![0u16; buffer_size];
|
||||
|
||||
DragQueryFileW(hdrop, i, buffer.as_mut_ptr().cast(), buffer_size as u32);
|
||||
|
||||
paths.push(OsString::from_wide(&buffer[..characters as usize]).into())
|
||||
}
|
||||
|
||||
self.drop_data = DropData::Files(paths);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe extern "system" fn query_interface(
|
||||
this: *mut IUnknown, riid: REFIID, ppvObject: *mut *mut winapi::ctypes::c_void,
|
||||
) -> HRESULT {
|
||||
if IsEqualIID(&*riid, &IUnknown::uuidof()) || IsEqualIID(&*riid, &IDropTarget::uuidof()) {
|
||||
Self::add_ref(this);
|
||||
*ppvObject = this as *mut winapi::ctypes::c_void;
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
E_NOINTERFACE
|
||||
}
|
||||
|
||||
unsafe extern "system" fn add_ref(this: *mut IUnknown) -> ULONG {
|
||||
let arc = Rc::from_raw(this);
|
||||
let result = Rc::strong_count(&arc) + 1;
|
||||
let _ = Rc::into_raw(arc);
|
||||
|
||||
Rc::increment_strong_count(this);
|
||||
|
||||
result as ULONG
|
||||
}
|
||||
|
||||
unsafe extern "system" fn release(this: *mut IUnknown) -> ULONG {
|
||||
let arc = Rc::from_raw(this);
|
||||
let result = Rc::strong_count(&arc) - 1;
|
||||
let _ = Rc::into_raw(arc);
|
||||
|
||||
Rc::decrement_strong_count(this);
|
||||
|
||||
result as ULONG
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe extern "system" fn drag_enter(
|
||||
this: *mut IDropTarget, pDataObj: *const IDataObject, grfKeyState: DWORD, pt: POINTL,
|
||||
pdwEffect: *mut DWORD,
|
||||
) -> HRESULT {
|
||||
let drop_target = &mut *(this as *mut DropTarget);
|
||||
let Some(window_state) = drop_target.window_state.upgrade() else {
|
||||
return E_UNEXPECTED;
|
||||
};
|
||||
|
||||
let modifiers =
|
||||
window_state.keyboard_state().get_modifiers_from_mouse_wparam(grfKeyState as WPARAM);
|
||||
|
||||
drop_target.parse_coordinates(pt);
|
||||
drop_target.parse_drop_data(&*pDataObj);
|
||||
|
||||
let event = MouseEvent::DragEntered {
|
||||
position: drop_target.drag_position,
|
||||
modifiers,
|
||||
data: drop_target.drop_data.clone(),
|
||||
};
|
||||
|
||||
drop_target.on_event(Some(pdwEffect), event);
|
||||
S_OK
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe extern "system" fn drag_over(
|
||||
this: *mut IDropTarget, grfKeyState: DWORD, pt: POINTL, pdwEffect: *mut DWORD,
|
||||
) -> HRESULT {
|
||||
let drop_target = &mut *(this as *mut DropTarget);
|
||||
let Some(window_state) = drop_target.window_state.upgrade() else {
|
||||
return E_UNEXPECTED;
|
||||
};
|
||||
|
||||
let modifiers =
|
||||
window_state.keyboard_state().get_modifiers_from_mouse_wparam(grfKeyState as WPARAM);
|
||||
|
||||
drop_target.parse_coordinates(pt);
|
||||
|
||||
let event = MouseEvent::DragMoved {
|
||||
position: drop_target.drag_position,
|
||||
modifiers,
|
||||
data: drop_target.drop_data.clone(),
|
||||
};
|
||||
|
||||
drop_target.on_event(Some(pdwEffect), event);
|
||||
S_OK
|
||||
}
|
||||
|
||||
unsafe extern "system" fn drag_leave(this: *mut IDropTarget) -> HRESULT {
|
||||
let drop_target = &mut *(this as *mut DropTarget);
|
||||
drop_target.on_event(None, MouseEvent::DragLeft);
|
||||
S_OK
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
unsafe extern "system" fn drop(
|
||||
this: *mut IDropTarget, pDataObj: *const IDataObject, grfKeyState: DWORD, pt: POINTL,
|
||||
pdwEffect: *mut DWORD,
|
||||
) -> HRESULT {
|
||||
let drop_target = &mut *(this as *mut DropTarget);
|
||||
let Some(window_state) = drop_target.window_state.upgrade() else {
|
||||
return E_UNEXPECTED;
|
||||
};
|
||||
|
||||
let modifiers =
|
||||
window_state.keyboard_state().get_modifiers_from_mouse_wparam(grfKeyState as WPARAM);
|
||||
|
||||
drop_target.parse_coordinates(pt);
|
||||
drop_target.parse_drop_data(&*pDataObj);
|
||||
|
||||
let event = MouseEvent::DragDropped {
|
||||
position: drop_target.drag_position,
|
||||
modifiers,
|
||||
data: drop_target.drop_data.clone(),
|
||||
};
|
||||
|
||||
drop_target.on_event(Some(pdwEffect), event);
|
||||
S_OK
|
||||
}
|
||||
}
|
||||
142
crates/baseview/src/win/hook.rs
Normal file
142
crates/baseview/src/win/hook.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
ffi::c_int,
|
||||
ptr,
|
||||
sync::{LazyLock, RwLock},
|
||||
};
|
||||
|
||||
use winapi::{
|
||||
shared::{
|
||||
minwindef::{LPARAM, WPARAM},
|
||||
windef::{HHOOK, HWND, POINT},
|
||||
},
|
||||
um::{
|
||||
libloaderapi::GetModuleHandleW,
|
||||
processthreadsapi::GetCurrentThreadId,
|
||||
winuser::{
|
||||
CallNextHookEx, SetWindowsHookExW, UnhookWindowsHookEx, HC_ACTION, MSG, PM_REMOVE,
|
||||
WH_GETMESSAGE, WM_CHAR, WM_KEYDOWN, WM_KEYUP, WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP,
|
||||
WM_USER,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::win::wnd_proc;
|
||||
|
||||
// track all windows opened by this instance of baseview
|
||||
// we use an RwLock here since the vast majority of uses (event interceptions)
|
||||
// will only need to read from the HashSet
|
||||
static HOOK_STATE: LazyLock<RwLock<KeyboardHookState>> = LazyLock::new(|| RwLock::default());
|
||||
|
||||
pub(crate) struct KeyboardHookHandle(HWNDWrapper);
|
||||
|
||||
#[derive(Default)]
|
||||
struct KeyboardHookState {
|
||||
hook: Option<HHOOK>,
|
||||
open_windows: HashSet<HWNDWrapper>,
|
||||
}
|
||||
|
||||
#[derive(Hash, PartialEq, Eq, Clone, Copy)]
|
||||
struct HWNDWrapper(HWND);
|
||||
|
||||
// SAFETY: it's a pointer behind an RwLock. we'll live
|
||||
unsafe impl Send for KeyboardHookState {}
|
||||
unsafe impl Sync for KeyboardHookState {}
|
||||
|
||||
// SAFETY: we never access the underlying HWND ourselves, just use it as a HashSet entry
|
||||
unsafe impl Send for HWNDWrapper {}
|
||||
unsafe impl Sync for HWNDWrapper {}
|
||||
|
||||
impl Drop for KeyboardHookHandle {
|
||||
fn drop(&mut self) {
|
||||
deinit_keyboard_hook(self.0);
|
||||
}
|
||||
}
|
||||
|
||||
// initialize keyboard hook
|
||||
// some DAWs (particularly Ableton) intercept incoming keyboard messages,
|
||||
// but we're naughty so we intercept them right back
|
||||
pub(crate) fn init_keyboard_hook(hwnd: HWND) -> KeyboardHookHandle {
|
||||
let state = &mut *HOOK_STATE.write().unwrap();
|
||||
|
||||
// register hwnd to global window set
|
||||
state.open_windows.insert(HWNDWrapper(hwnd));
|
||||
|
||||
if state.hook.is_some() {
|
||||
// keyboard hook already exists, just return handle
|
||||
KeyboardHookHandle(HWNDWrapper(hwnd))
|
||||
} else {
|
||||
// keyboard hook doesn't exist (no windows open before this), create it
|
||||
let new_hook = unsafe {
|
||||
SetWindowsHookExW(
|
||||
WH_GETMESSAGE,
|
||||
Some(keyboard_hook_callback),
|
||||
GetModuleHandleW(ptr::null()),
|
||||
GetCurrentThreadId(),
|
||||
)
|
||||
};
|
||||
|
||||
state.hook = Some(new_hook);
|
||||
|
||||
KeyboardHookHandle(HWNDWrapper(hwnd))
|
||||
}
|
||||
}
|
||||
|
||||
fn deinit_keyboard_hook(hwnd: HWNDWrapper) {
|
||||
let state = &mut *HOOK_STATE.write().unwrap();
|
||||
|
||||
state.open_windows.remove(&hwnd);
|
||||
|
||||
if state.open_windows.is_empty() {
|
||||
if let Some(hhook) = state.hook {
|
||||
unsafe {
|
||||
UnhookWindowsHookEx(hhook);
|
||||
}
|
||||
|
||||
state.hook = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "system" fn keyboard_hook_callback(
|
||||
n_code: c_int, wparam: WPARAM, lparam: LPARAM,
|
||||
) -> isize {
|
||||
let msg = lparam as *mut MSG;
|
||||
|
||||
if n_code == HC_ACTION && wparam == PM_REMOVE as usize && offer_message_to_baseview(msg) {
|
||||
*msg = MSG {
|
||||
hwnd: ptr::null_mut(),
|
||||
message: WM_USER,
|
||||
wParam: 0,
|
||||
lParam: 0,
|
||||
time: 0,
|
||||
pt: POINT { x: 0, y: 0 },
|
||||
};
|
||||
|
||||
0
|
||||
} else {
|
||||
CallNextHookEx(ptr::null_mut(), n_code, wparam, lparam)
|
||||
}
|
||||
}
|
||||
|
||||
// check if `msg` is a keyboard message addressed to a window
|
||||
// in KeyboardHookState::open_windows, and intercept it if so
|
||||
unsafe fn offer_message_to_baseview(msg: *mut MSG) -> bool {
|
||||
let msg = &*msg;
|
||||
|
||||
// if this isn't a keyboard message, ignore it
|
||||
match msg.message {
|
||||
WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP | WM_CHAR | WM_SYSCHAR => {}
|
||||
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
// check if this is one of our windows. if so, intercept it
|
||||
if HOOK_STATE.read().unwrap().open_windows.contains(&HWNDWrapper(msg.hwnd)) {
|
||||
let _ = wnd_proc(msg.hwnd, msg.message, msg.wParam, msg.lParam);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
704
crates/baseview/src/win/keyboard.rs
Normal file
704
crates/baseview/src/win/keyboard.rs
Normal file
@@ -0,0 +1,704 @@
|
||||
// Copyright 2020 The Druid Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Baseview modifications to druid code:
|
||||
// - update imports, paths etc
|
||||
|
||||
//! Key event handling.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::mem;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use keyboard_types::{Code, Key, KeyState, KeyboardEvent, Location, Modifiers};
|
||||
|
||||
use winapi::shared::minwindef::{HKL, INT, LPARAM, UINT, WPARAM};
|
||||
use winapi::shared::ntdef::SHORT;
|
||||
use winapi::shared::windef::HWND;
|
||||
use winapi::um::winuser::{
|
||||
GetKeyState, GetKeyboardLayout, MapVirtualKeyExW, PeekMessageW, ToUnicodeEx, MAPVK_VK_TO_CHAR,
|
||||
MAPVK_VSC_TO_VK_EX, MK_CONTROL, MK_SHIFT, PM_NOREMOVE, VK_ACCEPT, VK_ADD, VK_APPS, VK_ATTN,
|
||||
VK_BACK, VK_BROWSER_BACK, VK_BROWSER_FAVORITES, VK_BROWSER_FORWARD, VK_BROWSER_HOME,
|
||||
VK_BROWSER_REFRESH, VK_BROWSER_SEARCH, VK_BROWSER_STOP, VK_CANCEL, VK_CAPITAL, VK_CLEAR,
|
||||
VK_CONTROL, VK_CONVERT, VK_CRSEL, VK_DECIMAL, VK_DELETE, VK_DIVIDE, VK_DOWN, VK_END, VK_EREOF,
|
||||
VK_ESCAPE, VK_EXECUTE, VK_EXSEL, VK_F1, VK_F10, VK_F11, VK_F12, VK_F2, VK_F3, VK_F4, VK_F5,
|
||||
VK_F6, VK_F7, VK_F8, VK_F9, VK_FINAL, VK_HELP, VK_HOME, VK_INSERT, VK_JUNJA, VK_KANA, VK_KANJI,
|
||||
VK_LAUNCH_APP1, VK_LAUNCH_APP2, VK_LAUNCH_MAIL, VK_LAUNCH_MEDIA_SELECT, VK_LCONTROL, VK_LEFT,
|
||||
VK_LMENU, VK_LSHIFT, VK_LWIN, VK_MEDIA_NEXT_TRACK, VK_MEDIA_PLAY_PAUSE, VK_MEDIA_PREV_TRACK,
|
||||
VK_MEDIA_STOP, VK_MENU, VK_MODECHANGE, VK_MULTIPLY, VK_NEXT, VK_NONCONVERT, VK_NUMLOCK,
|
||||
VK_NUMPAD0, VK_NUMPAD1, VK_NUMPAD2, VK_NUMPAD3, VK_NUMPAD4, VK_NUMPAD5, VK_NUMPAD6, VK_NUMPAD7,
|
||||
VK_NUMPAD8, VK_NUMPAD9, VK_OEM_ATTN, VK_OEM_CLEAR, VK_PAUSE, VK_PLAY, VK_PRINT, VK_PRIOR,
|
||||
VK_PROCESSKEY, VK_RCONTROL, VK_RETURN, VK_RIGHT, VK_RMENU, VK_RSHIFT, VK_RWIN, VK_SCROLL,
|
||||
VK_SELECT, VK_SHIFT, VK_SLEEP, VK_SNAPSHOT, VK_SUBTRACT, VK_TAB, VK_UP, VK_VOLUME_DOWN,
|
||||
VK_VOLUME_MUTE, VK_VOLUME_UP, VK_ZOOM, WM_CHAR, WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP,
|
||||
WM_SYSCHAR, WM_SYSKEYDOWN, WM_SYSKEYUP,
|
||||
};
|
||||
|
||||
const VK_ABNT_C2: INT = 0xc2;
|
||||
|
||||
/// A (non-extended) virtual key code.
|
||||
type VkCode = u8;
|
||||
|
||||
// This is really bitfields.
|
||||
type ShiftState = u8;
|
||||
const SHIFT_STATE_SHIFT: ShiftState = 1;
|
||||
const SHIFT_STATE_ALTGR: ShiftState = 2;
|
||||
const N_SHIFT_STATE: ShiftState = 4;
|
||||
|
||||
/// Per-window keyboard state.
|
||||
pub(super) struct KeyboardState {
|
||||
hkl: HKL,
|
||||
// A map from (vk, is_shifted) to string val
|
||||
key_vals: HashMap<(VkCode, ShiftState), String>,
|
||||
dead_keys: HashSet<(VkCode, ShiftState)>,
|
||||
has_altgr: bool,
|
||||
stash_vk: Option<VkCode>,
|
||||
stash_utf16: Vec<u16>,
|
||||
}
|
||||
|
||||
/// Virtual key codes that are considered printable.
|
||||
///
|
||||
/// This logic is borrowed from KeyboardLayout::GetKeyIndex
|
||||
/// in Mozilla.
|
||||
const PRINTABLE_VKS: &[RangeInclusive<VkCode>] = &[
|
||||
0x20..=0x20,
|
||||
0x30..=0x39,
|
||||
0x41..=0x5A,
|
||||
0x60..=0x6B,
|
||||
0x6D..=0x6F,
|
||||
0xBA..=0xC2,
|
||||
0xDB..=0xDF,
|
||||
0xE1..=0xE4,
|
||||
];
|
||||
|
||||
/// Bits of lparam indicating scan code, including extended bit.
|
||||
const SCAN_MASK: LPARAM = 0x1ff_0000;
|
||||
|
||||
/// Determine whether there are more messages in the queue for this key event.
|
||||
///
|
||||
/// When this function returns `false`, there is another message in the queue
|
||||
/// with a matching scan code, therefore it is reasonable to stash the data
|
||||
/// from this message and defer til later to actually produce the event.
|
||||
unsafe fn is_last_message(hwnd: HWND, msg: UINT, lparam: LPARAM) -> bool {
|
||||
let expected_msg = match msg {
|
||||
WM_KEYDOWN | WM_CHAR => WM_CHAR,
|
||||
WM_SYSKEYDOWN | WM_SYSCHAR => WM_SYSCHAR,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let mut msg = mem::zeroed();
|
||||
let avail = PeekMessageW(&mut msg, hwnd, expected_msg, expected_msg, PM_NOREMOVE);
|
||||
avail == 0 || msg.lParam & SCAN_MASK != lparam & SCAN_MASK
|
||||
}
|
||||
|
||||
const MODIFIER_MAP: &[(INT, Modifiers, SHORT)] = &[
|
||||
(VK_MENU, Modifiers::ALT, 0x80),
|
||||
(VK_CAPITAL, Modifiers::CAPS_LOCK, 0x1),
|
||||
(VK_CONTROL, Modifiers::CONTROL, 0x80),
|
||||
(VK_NUMLOCK, Modifiers::NUM_LOCK, 0x1),
|
||||
(VK_SCROLL, Modifiers::SCROLL_LOCK, 0x1),
|
||||
(VK_SHIFT, Modifiers::SHIFT, 0x80),
|
||||
];
|
||||
|
||||
/// Convert scan code to W3C standard code.
|
||||
///
|
||||
/// It's hard to get an authoritative source for this; it's mostly based
|
||||
/// on NativeKeyToDOMCodeName.h in Mozilla.
|
||||
fn scan_to_code(scan_code: u32) -> Code {
|
||||
use Code::*;
|
||||
match scan_code {
|
||||
0x1 => Escape,
|
||||
0x2 => Digit1,
|
||||
0x3 => Digit2,
|
||||
0x4 => Digit3,
|
||||
0x5 => Digit4,
|
||||
0x6 => Digit5,
|
||||
0x7 => Digit6,
|
||||
0x8 => Digit7,
|
||||
0x9 => Digit8,
|
||||
0xA => Digit9,
|
||||
0xB => Digit0,
|
||||
0xC => Minus,
|
||||
0xD => Equal,
|
||||
0xE => Backspace,
|
||||
0xF => Tab,
|
||||
0x10 => KeyQ,
|
||||
0x11 => KeyW,
|
||||
0x12 => KeyE,
|
||||
0x13 => KeyR,
|
||||
0x14 => KeyT,
|
||||
0x15 => KeyY,
|
||||
0x16 => KeyU,
|
||||
0x17 => KeyI,
|
||||
0x18 => KeyO,
|
||||
0x19 => KeyP,
|
||||
0x1A => BracketLeft,
|
||||
0x1B => BracketRight,
|
||||
0x1C => Enter,
|
||||
0x1D => ControlLeft,
|
||||
0x1E => KeyA,
|
||||
0x1F => KeyS,
|
||||
0x20 => KeyD,
|
||||
0x21 => KeyF,
|
||||
0x22 => KeyG,
|
||||
0x23 => KeyH,
|
||||
0x24 => KeyJ,
|
||||
0x25 => KeyK,
|
||||
0x26 => KeyL,
|
||||
0x27 => Semicolon,
|
||||
0x28 => Quote,
|
||||
0x29 => Backquote,
|
||||
0x2A => ShiftLeft,
|
||||
0x2B => Backslash,
|
||||
0x2C => KeyZ,
|
||||
0x2D => KeyX,
|
||||
0x2E => KeyC,
|
||||
0x2F => KeyV,
|
||||
0x30 => KeyB,
|
||||
0x31 => KeyN,
|
||||
0x32 => KeyM,
|
||||
0x33 => Comma,
|
||||
0x34 => Period,
|
||||
0x35 => Slash,
|
||||
0x36 => ShiftRight,
|
||||
0x37 => NumpadMultiply,
|
||||
0x38 => AltLeft,
|
||||
0x39 => Space,
|
||||
0x3A => CapsLock,
|
||||
0x3B => F1,
|
||||
0x3C => F2,
|
||||
0x3D => F3,
|
||||
0x3E => F4,
|
||||
0x3F => F5,
|
||||
0x40 => F6,
|
||||
0x41 => F7,
|
||||
0x42 => F8,
|
||||
0x43 => F9,
|
||||
0x44 => F10,
|
||||
0x45 => Pause,
|
||||
0x46 => ScrollLock,
|
||||
0x47 => Numpad7,
|
||||
0x48 => Numpad8,
|
||||
0x49 => Numpad9,
|
||||
0x4A => NumpadSubtract,
|
||||
0x4B => Numpad4,
|
||||
0x4C => Numpad5,
|
||||
0x4D => Numpad6,
|
||||
0x4E => NumpadAdd,
|
||||
0x4F => Numpad1,
|
||||
0x50 => Numpad2,
|
||||
0x51 => Numpad3,
|
||||
0x52 => Numpad0,
|
||||
0x53 => NumpadDecimal,
|
||||
0x54 => PrintScreen,
|
||||
0x56 => IntlBackslash,
|
||||
0x57 => F11,
|
||||
0x58 => F12,
|
||||
0x59 => NumpadEqual,
|
||||
0x70 => KanaMode,
|
||||
0x71 => Lang2,
|
||||
0x72 => Lang1,
|
||||
0x73 => IntlRo,
|
||||
0x79 => Convert,
|
||||
0x7B => NonConvert,
|
||||
0x7D => IntlYen,
|
||||
0x7E => NumpadComma,
|
||||
0x110 => MediaTrackPrevious,
|
||||
0x119 => MediaTrackNext,
|
||||
0x11C => NumpadEnter,
|
||||
0x11D => ControlRight,
|
||||
0x120 => AudioVolumeMute,
|
||||
0x121 => LaunchApp2,
|
||||
0x122 => MediaPlayPause,
|
||||
0x124 => MediaStop,
|
||||
0x12E => AudioVolumeDown,
|
||||
0x130 => AudioVolumeUp,
|
||||
0x132 => BrowserHome,
|
||||
0x135 => NumpadDivide,
|
||||
0x137 => PrintScreen,
|
||||
0x138 => AltRight,
|
||||
0x145 => NumLock,
|
||||
0x147 => Home,
|
||||
0x148 => ArrowUp,
|
||||
0x149 => PageUp,
|
||||
0x14B => ArrowLeft,
|
||||
0x14D => ArrowRight,
|
||||
0x14F => End,
|
||||
0x150 => ArrowDown,
|
||||
0x151 => PageDown,
|
||||
0x152 => Insert,
|
||||
0x153 => Delete,
|
||||
0x15B => MetaLeft,
|
||||
0x15C => MetaRight,
|
||||
0x15D => ContextMenu,
|
||||
0x15E => Power,
|
||||
0x165 => BrowserSearch,
|
||||
0x166 => BrowserFavorites,
|
||||
0x167 => BrowserRefresh,
|
||||
0x168 => BrowserStop,
|
||||
0x169 => BrowserForward,
|
||||
0x16A => BrowserBack,
|
||||
0x16B => LaunchApp1,
|
||||
0x16C => LaunchMail,
|
||||
0x16D => MediaSelect,
|
||||
0x1F1 => Lang2,
|
||||
0x1F2 => Lang1,
|
||||
_ => Unidentified,
|
||||
}
|
||||
}
|
||||
|
||||
fn vk_to_key(vk: VkCode) -> Option<Key> {
|
||||
Some(match vk as INT {
|
||||
VK_CANCEL => Key::Cancel,
|
||||
VK_BACK => Key::Backspace,
|
||||
VK_TAB => Key::Tab,
|
||||
VK_CLEAR => Key::Clear,
|
||||
VK_RETURN => Key::Enter,
|
||||
VK_SHIFT | VK_LSHIFT | VK_RSHIFT => Key::Shift,
|
||||
VK_CONTROL | VK_LCONTROL | VK_RCONTROL => Key::Control,
|
||||
VK_MENU | VK_LMENU | VK_RMENU => Key::Alt,
|
||||
VK_PAUSE => Key::Pause,
|
||||
VK_CAPITAL => Key::CapsLock,
|
||||
// TODO: disambiguate kana and hangul? same vk
|
||||
VK_KANA => Key::KanaMode,
|
||||
VK_JUNJA => Key::JunjaMode,
|
||||
VK_FINAL => Key::FinalMode,
|
||||
VK_KANJI => Key::KanjiMode,
|
||||
VK_ESCAPE => Key::Escape,
|
||||
VK_NONCONVERT => Key::NonConvert,
|
||||
VK_ACCEPT => Key::Accept,
|
||||
VK_PRIOR => Key::PageUp,
|
||||
VK_NEXT => Key::PageDown,
|
||||
VK_END => Key::End,
|
||||
VK_HOME => Key::Home,
|
||||
VK_LEFT => Key::ArrowLeft,
|
||||
VK_UP => Key::ArrowUp,
|
||||
VK_RIGHT => Key::ArrowRight,
|
||||
VK_DOWN => Key::ArrowDown,
|
||||
VK_SELECT => Key::Select,
|
||||
VK_PRINT => Key::Print,
|
||||
VK_EXECUTE => Key::Execute,
|
||||
VK_SNAPSHOT => Key::PrintScreen,
|
||||
VK_INSERT => Key::Insert,
|
||||
VK_DELETE => Key::Delete,
|
||||
VK_HELP => Key::Help,
|
||||
VK_LWIN | VK_RWIN => Key::Meta,
|
||||
VK_APPS => Key::ContextMenu,
|
||||
VK_SLEEP => Key::Standby,
|
||||
VK_F1 => Key::F1,
|
||||
VK_F2 => Key::F2,
|
||||
VK_F3 => Key::F3,
|
||||
VK_F4 => Key::F4,
|
||||
VK_F5 => Key::F5,
|
||||
VK_F6 => Key::F6,
|
||||
VK_F7 => Key::F7,
|
||||
VK_F8 => Key::F8,
|
||||
VK_F9 => Key::F9,
|
||||
VK_F10 => Key::F10,
|
||||
VK_F11 => Key::F11,
|
||||
VK_F12 => Key::F12,
|
||||
VK_NUMLOCK => Key::NumLock,
|
||||
VK_SCROLL => Key::ScrollLock,
|
||||
VK_BROWSER_BACK => Key::BrowserBack,
|
||||
VK_BROWSER_FORWARD => Key::BrowserForward,
|
||||
VK_BROWSER_REFRESH => Key::BrowserRefresh,
|
||||
VK_BROWSER_STOP => Key::BrowserStop,
|
||||
VK_BROWSER_SEARCH => Key::BrowserSearch,
|
||||
VK_BROWSER_FAVORITES => Key::BrowserFavorites,
|
||||
VK_BROWSER_HOME => Key::BrowserHome,
|
||||
VK_VOLUME_MUTE => Key::AudioVolumeMute,
|
||||
VK_VOLUME_DOWN => Key::AudioVolumeDown,
|
||||
VK_VOLUME_UP => Key::AudioVolumeUp,
|
||||
VK_MEDIA_NEXT_TRACK => Key::MediaTrackNext,
|
||||
VK_MEDIA_PREV_TRACK => Key::MediaTrackPrevious,
|
||||
VK_MEDIA_STOP => Key::MediaStop,
|
||||
VK_MEDIA_PLAY_PAUSE => Key::MediaPlayPause,
|
||||
VK_LAUNCH_MAIL => Key::LaunchMail,
|
||||
VK_LAUNCH_MEDIA_SELECT => Key::LaunchMediaPlayer,
|
||||
VK_LAUNCH_APP1 => Key::LaunchApplication1,
|
||||
VK_LAUNCH_APP2 => Key::LaunchApplication2,
|
||||
VK_OEM_ATTN => Key::Alphanumeric,
|
||||
VK_CONVERT => Key::Convert,
|
||||
VK_MODECHANGE => Key::ModeChange,
|
||||
VK_PROCESSKEY => Key::Process,
|
||||
VK_ATTN => Key::Attn,
|
||||
VK_CRSEL => Key::CrSel,
|
||||
VK_EXSEL => Key::ExSel,
|
||||
VK_EREOF => Key::EraseEof,
|
||||
VK_PLAY => Key::Play,
|
||||
VK_ZOOM => Key::ZoomToggle,
|
||||
VK_OEM_CLEAR => Key::Clear,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn code_unit_to_key(code_unit: u32) -> Key {
|
||||
match code_unit {
|
||||
0x8 | 0x7F => Key::Backspace,
|
||||
0x9 => Key::Tab,
|
||||
0xA | 0xD => Key::Enter,
|
||||
0x1B => Key::Escape,
|
||||
_ if code_unit >= 0x20 => {
|
||||
if let Some(c) = std::char::from_u32(code_unit) {
|
||||
Key::Character(c.to_string())
|
||||
} else {
|
||||
// UTF-16 error, very unlikely
|
||||
Key::Unidentified
|
||||
}
|
||||
}
|
||||
_ => Key::Unidentified,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get location from virtual key code.
|
||||
///
|
||||
/// This logic is based on NativeKey::GetKeyLocation from Mozilla.
|
||||
fn vk_to_location(vk: VkCode, is_extended: bool) -> Location {
|
||||
match vk as INT {
|
||||
VK_LSHIFT | VK_LCONTROL | VK_LMENU | VK_LWIN => Location::Left,
|
||||
VK_RSHIFT | VK_RCONTROL | VK_RMENU | VK_RWIN => Location::Right,
|
||||
VK_RETURN if is_extended => Location::Numpad,
|
||||
VK_INSERT | VK_DELETE | VK_END | VK_DOWN | VK_NEXT | VK_LEFT | VK_CLEAR | VK_RIGHT
|
||||
| VK_HOME | VK_UP | VK_PRIOR => {
|
||||
if is_extended {
|
||||
Location::Standard
|
||||
} else {
|
||||
Location::Numpad
|
||||
}
|
||||
}
|
||||
VK_NUMPAD0 | VK_NUMPAD1 | VK_NUMPAD2 | VK_NUMPAD3 | VK_NUMPAD4 | VK_NUMPAD5
|
||||
| VK_NUMPAD6 | VK_NUMPAD7 | VK_NUMPAD8 | VK_NUMPAD9 | VK_DECIMAL | VK_DIVIDE
|
||||
| VK_MULTIPLY | VK_SUBTRACT | VK_ADD | VK_ABNT_C2 => Location::Numpad,
|
||||
_ => Location::Standard,
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyboardState {
|
||||
/// Create a new keyboard state.
|
||||
///
|
||||
/// There should be one of these per window. It loads the current keyboard
|
||||
/// layout and retains some mapping information from it.
|
||||
pub(crate) fn new() -> KeyboardState {
|
||||
unsafe {
|
||||
let hkl = GetKeyboardLayout(0);
|
||||
let key_vals = HashMap::new();
|
||||
let dead_keys = HashSet::new();
|
||||
let stash_vk = None;
|
||||
let stash_utf16 = Vec::new();
|
||||
let has_altgr = false;
|
||||
let mut result =
|
||||
KeyboardState { hkl, key_vals, dead_keys, has_altgr, stash_vk, stash_utf16 };
|
||||
result.load_keyboard_layout();
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Process one message from the platform.
|
||||
///
|
||||
/// This is the main interface point for generating cooked keyboard events
|
||||
/// from raw platform messages. It should be called for each relevant message,
|
||||
/// which comprises: `WM_KEYDOWN`, `WM_KEYUP`, `WM_CHAR`, `WM_SYSKEYDOWN`,
|
||||
/// `WM_SYSKEYUP`, `WM_SYSCHAR`, and `WM_INPUTLANGCHANGE`.
|
||||
///
|
||||
/// As a general theory, many keyboard events generate a sequence of platform
|
||||
/// messages. In these cases, we stash information from all messages but the
|
||||
/// last, and generate the event from the last (using `PeekMessage` to detect
|
||||
/// that case). Mozilla handling is slightly different; it actually tries to
|
||||
/// do the processing on the first message, fetching the subsequent messages
|
||||
/// from the queue. We believe our handling is simpler and more robust.
|
||||
///
|
||||
/// A simple example of a multi-message sequence is the key "=". In a US layout,
|
||||
/// we'd expect `WM_KEYDOWN` with `wparam = VK_OEM_PLUS` and lparam encoding the
|
||||
/// keycode that translates into `Code::Equal`, followed by a `WM_CHAR` with
|
||||
/// `wparam = b"="` and the same scancode.
|
||||
///
|
||||
/// A more complex example of a multi-message sequence is the second press of
|
||||
/// that key in a German layout, where it's mapped to the dead key for accent
|
||||
/// acute. Then we expect `WM_KEYDOWN` with `wparam = VK_OEM_6` followed by
|
||||
/// two `WM_CHAR` with `wparam = 0xB4` (corresponding to U+00B4 = acute accent).
|
||||
/// In this case, the result (produced on the final message in the sequence) is
|
||||
/// a key event with `key = Key::Character("´´")`, which also matches browser
|
||||
/// behavior.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The `hwnd` argument must be a valid `HWND`. Similarly, the `lparam` must be
|
||||
/// a valid `HKL` reference in the `WM_INPUTLANGCHANGE` message. Actual danger
|
||||
/// is likely low, though.
|
||||
pub(crate) unsafe fn process_message(
|
||||
&mut self, hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM,
|
||||
) -> Option<KeyboardEvent> {
|
||||
match msg {
|
||||
WM_KEYDOWN | WM_SYSKEYDOWN => {
|
||||
//println!("keydown wparam {:x} lparam {:x}", wparam, lparam);
|
||||
let scan_code = ((lparam & SCAN_MASK) >> 16) as u32;
|
||||
let vk = self.refine_vk(wparam as u8, scan_code);
|
||||
if is_last_message(hwnd, msg, lparam) {
|
||||
let modifiers = self.get_modifiers();
|
||||
let code = scan_to_code(scan_code);
|
||||
let key = vk_to_key(vk).unwrap_or_else(|| self.get_base_key(vk, modifiers));
|
||||
let repeat = (lparam & 0x4000_0000) != 0;
|
||||
let is_extended = (lparam & 0x100_0000) != 0;
|
||||
let location = vk_to_location(vk, is_extended);
|
||||
let state = KeyState::Down;
|
||||
let event = KeyboardEvent {
|
||||
state,
|
||||
modifiers,
|
||||
code,
|
||||
key,
|
||||
is_composing: false,
|
||||
location,
|
||||
repeat,
|
||||
};
|
||||
Some(event)
|
||||
} else {
|
||||
self.stash_vk = Some(vk);
|
||||
None
|
||||
}
|
||||
}
|
||||
WM_KEYUP | WM_SYSKEYUP => {
|
||||
let scan_code = ((lparam & SCAN_MASK) >> 16) as u32;
|
||||
let vk = self.refine_vk(wparam as u8, scan_code);
|
||||
let modifiers = self.get_modifiers();
|
||||
let code = scan_to_code(scan_code);
|
||||
let key = vk_to_key(vk).unwrap_or_else(|| self.get_base_key(vk, modifiers));
|
||||
let repeat = false;
|
||||
let is_extended = (lparam & 0x100_0000) != 0;
|
||||
let location = vk_to_location(vk, is_extended);
|
||||
let state = KeyState::Up;
|
||||
let event = KeyboardEvent {
|
||||
state,
|
||||
modifiers,
|
||||
code,
|
||||
key,
|
||||
is_composing: false,
|
||||
location,
|
||||
repeat,
|
||||
};
|
||||
Some(event)
|
||||
}
|
||||
WM_CHAR | WM_SYSCHAR => {
|
||||
//println!("char wparam {:x} lparam {:x}", wparam, lparam);
|
||||
if is_last_message(hwnd, msg, lparam) {
|
||||
let stash_vk = self.stash_vk.take();
|
||||
let modifiers = self.get_modifiers();
|
||||
let scan_code = ((lparam & SCAN_MASK) >> 16) as u32;
|
||||
let vk = self.refine_vk(stash_vk.unwrap_or(0), scan_code);
|
||||
let code = scan_to_code(scan_code);
|
||||
let key = if self.stash_utf16.is_empty() && wparam < 0x20 {
|
||||
vk_to_key(vk).unwrap_or_else(|| self.get_base_key(vk, modifiers))
|
||||
} else {
|
||||
self.stash_utf16.push(wparam as u16);
|
||||
if let Ok(s) = String::from_utf16(&self.stash_utf16) {
|
||||
Key::Character(s)
|
||||
} else {
|
||||
Key::Unidentified
|
||||
}
|
||||
};
|
||||
self.stash_utf16.clear();
|
||||
let repeat = (lparam & 0x4000_0000) != 0;
|
||||
let is_extended = (lparam & 0x100_0000) != 0;
|
||||
let location = vk_to_location(vk, is_extended);
|
||||
let state = KeyState::Down;
|
||||
let event = KeyboardEvent {
|
||||
state,
|
||||
modifiers,
|
||||
code,
|
||||
key,
|
||||
is_composing: false,
|
||||
location,
|
||||
repeat,
|
||||
};
|
||||
Some(event)
|
||||
} else {
|
||||
self.stash_utf16.push(wparam as u16);
|
||||
None
|
||||
}
|
||||
}
|
||||
WM_INPUTLANGCHANGE => {
|
||||
self.hkl = lparam as HKL;
|
||||
self.load_keyboard_layout();
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the modifier state.
|
||||
///
|
||||
/// This function is designed to be called from a message handler, and
|
||||
/// gives the modifier state at the time of the message (ie is the
|
||||
/// synchronous variant). See [`GetKeyState`] for more context.
|
||||
///
|
||||
/// The interpretation of modifiers depends on the keyboard layout, as
|
||||
/// some layouts have [AltGr] and others do not.
|
||||
///
|
||||
/// [`GetKeyState`]: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getkeystate
|
||||
/// [AltGr]: https://en.wikipedia.org/wiki/AltGr_key
|
||||
pub(crate) fn get_modifiers(&self) -> Modifiers {
|
||||
unsafe {
|
||||
let mut modifiers = Modifiers::empty();
|
||||
for &(vk, modifier, mask) in MODIFIER_MAP {
|
||||
if GetKeyState(vk) & mask != 0 {
|
||||
modifiers |= modifier;
|
||||
}
|
||||
}
|
||||
if self.has_altgr && GetKeyState(VK_RMENU) & 0x80 != 0 {
|
||||
modifiers |= Modifiers::ALT_GRAPH;
|
||||
modifiers &= !(Modifiers::CONTROL | Modifiers::ALT);
|
||||
}
|
||||
modifiers
|
||||
}
|
||||
}
|
||||
|
||||
/// The same as [Self::get_modifiers()], but it reads the Ctrl and Shift state from a mouse
|
||||
/// event's wParam parameter. Saves two calls to [GetKeyState()].
|
||||
pub(crate) fn get_modifiers_from_mouse_wparam(&self, wparam: WPARAM) -> Modifiers {
|
||||
unsafe {
|
||||
let mut modifiers = Modifiers::empty();
|
||||
for &(vk, modifier, mask) in MODIFIER_MAP {
|
||||
let modifier_active = match modifier {
|
||||
Modifiers::CONTROL => wparam & MK_CONTROL != 0,
|
||||
Modifiers::SHIFT => wparam & MK_SHIFT != 0,
|
||||
_ => GetKeyState(vk) & mask != 0,
|
||||
};
|
||||
|
||||
if modifier_active {
|
||||
modifiers |= modifier;
|
||||
}
|
||||
}
|
||||
if self.has_altgr && GetKeyState(VK_RMENU) & 0x80 != 0 {
|
||||
modifiers |= Modifiers::ALT_GRAPH;
|
||||
modifiers &= !(Modifiers::CONTROL | Modifiers::ALT);
|
||||
}
|
||||
modifiers
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a keyboard layout.
|
||||
///
|
||||
/// We need to retain a map of virtual key codes in various modifier
|
||||
/// states, because it's not practical to query that at keyboard event
|
||||
/// time (the main culprit is that `ToUnicodeEx` is stateful).
|
||||
///
|
||||
/// The logic is based on Mozilla KeyboardLayout::LoadLayout but is
|
||||
/// considerably simplified.
|
||||
fn load_keyboard_layout(&mut self) {
|
||||
unsafe {
|
||||
self.key_vals.clear();
|
||||
self.dead_keys.clear();
|
||||
self.has_altgr = false;
|
||||
let mut key_state = [0u8; 256];
|
||||
let mut uni_chars = [0u16; 5];
|
||||
// Right now, we're only getting the values for base and shifted
|
||||
// variants. Mozilla goes through 16 mod states.
|
||||
for shift_state in 0..N_SHIFT_STATE {
|
||||
let has_shift = shift_state & SHIFT_STATE_SHIFT != 0;
|
||||
let has_altgr = shift_state & SHIFT_STATE_ALTGR != 0;
|
||||
key_state[VK_SHIFT as usize] = if has_shift { 0x80 } else { 0 };
|
||||
key_state[VK_CONTROL as usize] = if has_altgr { 0x80 } else { 0 };
|
||||
key_state[VK_LCONTROL as usize] = if has_altgr { 0x80 } else { 0 };
|
||||
key_state[VK_MENU as usize] = if has_altgr { 0x80 } else { 0 };
|
||||
key_state[VK_RMENU as usize] = if has_altgr { 0x80 } else { 0 };
|
||||
#[allow(clippy::iter_overeager_cloned)]
|
||||
for vk in PRINTABLE_VKS.iter().cloned().flatten() {
|
||||
let ret = ToUnicodeEx(
|
||||
vk as UINT,
|
||||
0,
|
||||
key_state.as_ptr(),
|
||||
uni_chars.as_mut_ptr(),
|
||||
uni_chars.len() as _,
|
||||
0,
|
||||
self.hkl,
|
||||
);
|
||||
match ret.cmp(&0) {
|
||||
Ordering::Greater => {
|
||||
let utf16_slice = &uni_chars[..ret as usize];
|
||||
if let Ok(strval) = String::from_utf16(utf16_slice) {
|
||||
self.key_vals.insert((vk, shift_state), strval);
|
||||
}
|
||||
// If the AltGr version of the key has a different string than
|
||||
// the base, then the layout has AltGr. Note that Mozilla also
|
||||
// checks dead keys for change.
|
||||
if has_altgr
|
||||
&& !self.has_altgr
|
||||
&& self.key_vals.get(&(vk, shift_state))
|
||||
!= self.key_vals.get(&(vk, shift_state & !SHIFT_STATE_ALTGR))
|
||||
{
|
||||
self.has_altgr = true;
|
||||
}
|
||||
}
|
||||
Ordering::Less => {
|
||||
// It's a dead key, press it again to reset the state.
|
||||
self.dead_keys.insert((vk, shift_state));
|
||||
let _ = ToUnicodeEx(
|
||||
vk as UINT,
|
||||
0,
|
||||
key_state.as_ptr(),
|
||||
uni_chars.as_mut_ptr(),
|
||||
uni_chars.len() as _,
|
||||
0,
|
||||
self.hkl,
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_base_key(&self, vk: VkCode, modifiers: Modifiers) -> Key {
|
||||
let mut shift_state = 0;
|
||||
if modifiers.contains(Modifiers::SHIFT) {
|
||||
shift_state |= SHIFT_STATE_SHIFT;
|
||||
}
|
||||
if modifiers.contains(Modifiers::ALT_GRAPH) {
|
||||
shift_state |= SHIFT_STATE_ALTGR;
|
||||
}
|
||||
if let Some(s) = self.key_vals.get(&(vk, shift_state)) {
|
||||
Key::Character(s.clone())
|
||||
} else {
|
||||
let mapped = self.map_vk(vk);
|
||||
if mapped >= (1 << 31) {
|
||||
Key::Dead
|
||||
} else {
|
||||
code_unit_to_key(mapped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Map a virtual key code to a code unit, also indicate if dead key.
|
||||
///
|
||||
/// Bit 31 is set if the mapping is to a dead key. The bottom bits contain the code unit.
|
||||
fn map_vk(&self, vk: VkCode) -> u32 {
|
||||
unsafe { MapVirtualKeyExW(vk as _, MAPVK_VK_TO_CHAR, self.hkl) }
|
||||
}
|
||||
|
||||
/// Refine a virtual key code to distinguish left and right.
|
||||
///
|
||||
/// This only does the mapping if the original code is ambiguous, as otherwise the
|
||||
/// virtual key code reported in `wparam` is more reliable.
|
||||
fn refine_vk(&self, vk: VkCode, mut scan_code: u32) -> VkCode {
|
||||
match vk as INT {
|
||||
0 | VK_SHIFT | VK_CONTROL | VK_MENU => {
|
||||
if scan_code >= 0x100 {
|
||||
scan_code += 0xE000 - 0x100;
|
||||
}
|
||||
unsafe { MapVirtualKeyExW(scan_code, MAPVK_VSC_TO_VK_EX, self.hkl) as u8 }
|
||||
}
|
||||
_ => vk,
|
||||
}
|
||||
}
|
||||
}
|
||||
7
crates/baseview/src/win/mod.rs
Normal file
7
crates/baseview/src/win/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod cursor;
|
||||
mod drop_target;
|
||||
mod hook;
|
||||
mod keyboard;
|
||||
mod window;
|
||||
|
||||
pub use window::*;
|
||||
856
crates/baseview/src/win/window.rs
Normal file
856
crates/baseview/src/win/window.rs
Normal file
@@ -0,0 +1,856 @@
|
||||
use winapi::shared::guiddef::GUID;
|
||||
use winapi::shared::minwindef::{ATOM, FALSE, LOWORD, LPARAM, LRESULT, UINT, WPARAM};
|
||||
use winapi::shared::windef::{HWND, RECT};
|
||||
use winapi::um::combaseapi::CoCreateGuid;
|
||||
use winapi::um::ole2::{OleInitialize, RegisterDragDrop, RevokeDragDrop};
|
||||
use winapi::um::oleidl::LPDROPTARGET;
|
||||
use winapi::um::winuser::{
|
||||
AdjustWindowRectEx, CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW,
|
||||
GetDpiForWindow, GetFocus, GetMessageW, GetWindowLongPtrW, LoadCursorW, PostMessageW,
|
||||
RegisterClassW, ReleaseCapture, SetCapture, SetCursor, SetFocus, SetProcessDpiAwarenessContext,
|
||||
SetTimer, SetWindowLongPtrW, SetWindowPos, TrackMouseEvent, TranslateMessage, UnregisterClassW,
|
||||
CS_OWNDC, GET_XBUTTON_WPARAM, GWLP_USERDATA, HTCLIENT, IDC_ARROW, MSG, SWP_NOMOVE,
|
||||
SWP_NOZORDER, TRACKMOUSEEVENT, WHEEL_DELTA, WM_CHAR, WM_CLOSE, WM_CREATE, WM_DPICHANGED,
|
||||
WM_INPUTLANGCHANGE, WM_KEYDOWN, WM_KEYUP, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN,
|
||||
WM_MBUTTONUP, WM_MOUSEHWHEEL, WM_MOUSELEAVE, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCDESTROY,
|
||||
WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SHOWWINDOW, WM_SIZE, WM_SYSCHAR, WM_SYSKEYDOWN,
|
||||
WM_SYSKEYUP, WM_TIMER, WM_USER, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSW, WS_CAPTION, WS_CHILD,
|
||||
WS_CLIPSIBLINGS, WS_MAXIMIZEBOX, WS_MINIMIZEBOX, WS_POPUPWINDOW, WS_SIZEBOX, WS_VISIBLE,
|
||||
XBUTTON1, XBUTTON2,
|
||||
};
|
||||
|
||||
use std::cell::{Cell, Ref, RefCell, RefMut};
|
||||
use std::collections::VecDeque;
|
||||
use std::ffi::{c_void, OsStr};
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::ptr::null_mut;
|
||||
use std::rc::Rc;
|
||||
|
||||
use raw_window_handle::{
|
||||
HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, Win32WindowHandle,
|
||||
WindowsDisplayHandle,
|
||||
};
|
||||
|
||||
const BV_WINDOW_MUST_CLOSE: UINT = WM_USER + 1;
|
||||
|
||||
use crate::win::hook::{self, KeyboardHookHandle};
|
||||
use crate::{
|
||||
Event, MouseButton, MouseCursor, MouseEvent, PhyPoint, PhySize, ScrollDelta, Size, WindowEvent,
|
||||
WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy,
|
||||
};
|
||||
|
||||
use super::cursor::cursor_to_lpcwstr;
|
||||
use super::drop_target::DropTarget;
|
||||
use super::keyboard::KeyboardState;
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
use crate::gl::GlContext;
|
||||
|
||||
unsafe fn generate_guid() -> String {
|
||||
let mut guid: GUID = std::mem::zeroed();
|
||||
CoCreateGuid(&mut guid);
|
||||
format!(
|
||||
"{:0X}-{:0X}-{:0X}-{:0X}{:0X}-{:0X}{:0X}{:0X}{:0X}{:0X}{:0X}\0",
|
||||
guid.Data1,
|
||||
guid.Data2,
|
||||
guid.Data3,
|
||||
guid.Data4[0],
|
||||
guid.Data4[1],
|
||||
guid.Data4[2],
|
||||
guid.Data4[3],
|
||||
guid.Data4[4],
|
||||
guid.Data4[5],
|
||||
guid.Data4[6],
|
||||
guid.Data4[7]
|
||||
)
|
||||
}
|
||||
|
||||
const WIN_FRAME_TIMER: usize = 4242;
|
||||
|
||||
pub struct WindowHandle {
|
||||
hwnd: Option<HWND>,
|
||||
is_open: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
impl WindowHandle {
|
||||
pub fn close(&mut self) {
|
||||
if let Some(hwnd) = self.hwnd.take() {
|
||||
unsafe {
|
||||
PostMessageW(hwnd, BV_WINDOW_MUST_CLOSE, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.is_open.get()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl HasRawWindowHandle for WindowHandle {
|
||||
fn raw_window_handle(&self) -> RawWindowHandle {
|
||||
if let Some(hwnd) = self.hwnd {
|
||||
let mut handle = Win32WindowHandle::empty();
|
||||
handle.hwnd = hwnd as *mut c_void;
|
||||
|
||||
RawWindowHandle::Win32(handle)
|
||||
} else {
|
||||
RawWindowHandle::Win32(Win32WindowHandle::empty())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ParentHandle {
|
||||
is_open: Rc<Cell<bool>>,
|
||||
}
|
||||
|
||||
impl ParentHandle {
|
||||
pub fn new(hwnd: HWND) -> (Self, WindowHandle) {
|
||||
let is_open = Rc::new(Cell::new(true));
|
||||
|
||||
let handle = WindowHandle { hwnd: Some(hwnd), is_open: Rc::clone(&is_open) };
|
||||
|
||||
(Self { is_open }, handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ParentHandle {
|
||||
fn drop(&mut self) {
|
||||
self.is_open.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) unsafe extern "system" fn wnd_proc(
|
||||
hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM,
|
||||
) -> LRESULT {
|
||||
if msg == WM_CREATE {
|
||||
PostMessageW(hwnd, WM_SHOWWINDOW, 0, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
let window_state_ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut WindowState;
|
||||
if !window_state_ptr.is_null() {
|
||||
let result = wnd_proc_inner(hwnd, msg, wparam, lparam, &*window_state_ptr);
|
||||
|
||||
// If any of the above event handlers caused tasks to be pushed to the deferred tasks list,
|
||||
// then we'll try to handle them now
|
||||
loop {
|
||||
// NOTE: This is written like this instead of using a `while let` loop to avoid exending
|
||||
// the borrow of `window_state.deferred_tasks` into the call of
|
||||
// `window_state.handle_deferred_task()` since that may also generate additional
|
||||
// messages.
|
||||
let task = match (*window_state_ptr).deferred_tasks.borrow_mut().pop_front() {
|
||||
Some(task) => task,
|
||||
None => break,
|
||||
};
|
||||
|
||||
(*window_state_ptr).handle_deferred_task(task);
|
||||
}
|
||||
|
||||
// NOTE: This is not handled in `wnd_proc_inner` because of the deferred task loop above
|
||||
if msg == WM_NCDESTROY {
|
||||
RevokeDragDrop(hwnd);
|
||||
unregister_wnd_class((*window_state_ptr).window_class);
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
|
||||
drop(Rc::from_raw(window_state_ptr));
|
||||
}
|
||||
|
||||
// The actual custom window proc has been moved to another function so we can always handle
|
||||
// the deferred tasks regardless of whether the custom window proc returns early or not
|
||||
if let Some(result) = result {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
DefWindowProcW(hwnd, msg, wparam, lparam)
|
||||
}
|
||||
|
||||
/// Our custom `wnd_proc` handler. If the result contains a value, then this is returned after
|
||||
/// handling any deferred tasks. otherwise the default window procedure is invoked.
|
||||
unsafe fn wnd_proc_inner(
|
||||
hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM, window_state: &WindowState,
|
||||
) -> Option<LRESULT> {
|
||||
match msg {
|
||||
WM_MOUSEMOVE => {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
let mut mouse_was_outside_window = window_state.mouse_was_outside_window.borrow_mut();
|
||||
if *mouse_was_outside_window {
|
||||
// this makes Windows track whether the mouse leaves the window.
|
||||
// When the mouse leaves it results in a `WM_MOUSELEAVE` event.
|
||||
let mut track_mouse = TRACKMOUSEEVENT {
|
||||
cbSize: std::mem::size_of::<TRACKMOUSEEVENT>() as u32,
|
||||
dwFlags: winapi::um::winuser::TME_LEAVE,
|
||||
hwndTrack: hwnd,
|
||||
dwHoverTime: winapi::um::winuser::HOVER_DEFAULT,
|
||||
};
|
||||
// Couldn't find a good way to track whether the mouse enters,
|
||||
// but if `WM_MOUSEMOVE` happens, the mouse must have entered.
|
||||
TrackMouseEvent(&mut track_mouse);
|
||||
*mouse_was_outside_window = false;
|
||||
|
||||
let enter_event = Event::Mouse(MouseEvent::CursorEntered);
|
||||
window_state
|
||||
.handler
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.on_event(&mut window, enter_event);
|
||||
}
|
||||
|
||||
let x = (lparam & 0xFFFF) as i16 as i32;
|
||||
let y = ((lparam >> 16) & 0xFFFF) as i16 as i32;
|
||||
|
||||
let physical_pos = PhyPoint { x, y };
|
||||
let logical_pos = physical_pos.to_logical(&window_state.window_info.borrow());
|
||||
let move_event = Event::Mouse(MouseEvent::CursorMoved {
|
||||
position: logical_pos,
|
||||
modifiers: window_state
|
||||
.keyboard_state
|
||||
.borrow()
|
||||
.get_modifiers_from_mouse_wparam(wparam),
|
||||
});
|
||||
window_state.handler.borrow_mut().as_mut().unwrap().on_event(&mut window, move_event);
|
||||
Some(0)
|
||||
}
|
||||
|
||||
WM_MOUSELEAVE => {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
let event = Event::Mouse(MouseEvent::CursorLeft);
|
||||
window_state.handler.borrow_mut().as_mut().unwrap().on_event(&mut window, event);
|
||||
|
||||
*window_state.mouse_was_outside_window.borrow_mut() = true;
|
||||
Some(0)
|
||||
}
|
||||
WM_MOUSEWHEEL | WM_MOUSEHWHEEL => {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
let value = (wparam >> 16) as i16;
|
||||
let value = value as i32;
|
||||
let value = value as f32 / WHEEL_DELTA as f32;
|
||||
|
||||
let event = Event::Mouse(MouseEvent::WheelScrolled {
|
||||
delta: if msg == WM_MOUSEWHEEL {
|
||||
ScrollDelta::Lines { x: 0.0, y: value }
|
||||
} else {
|
||||
ScrollDelta::Lines { x: value, y: 0.0 }
|
||||
},
|
||||
modifiers: window_state
|
||||
.keyboard_state
|
||||
.borrow()
|
||||
.get_modifiers_from_mouse_wparam(wparam),
|
||||
});
|
||||
|
||||
window_state.handler.borrow_mut().as_mut().unwrap().on_event(&mut window, event);
|
||||
|
||||
Some(0)
|
||||
}
|
||||
WM_LBUTTONDOWN | WM_LBUTTONUP | WM_MBUTTONDOWN | WM_MBUTTONUP | WM_RBUTTONDOWN
|
||||
| WM_RBUTTONUP | WM_XBUTTONDOWN | WM_XBUTTONUP => {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
let mut mouse_button_counter = window_state.mouse_button_counter.get();
|
||||
|
||||
let button = match msg {
|
||||
WM_LBUTTONDOWN | WM_LBUTTONUP => Some(MouseButton::Left),
|
||||
WM_MBUTTONDOWN | WM_MBUTTONUP => Some(MouseButton::Middle),
|
||||
WM_RBUTTONDOWN | WM_RBUTTONUP => Some(MouseButton::Right),
|
||||
WM_XBUTTONDOWN | WM_XBUTTONUP => match GET_XBUTTON_WPARAM(wparam) {
|
||||
XBUTTON1 => Some(MouseButton::Back),
|
||||
XBUTTON2 => Some(MouseButton::Forward),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(button) = button {
|
||||
let event = match msg {
|
||||
WM_LBUTTONDOWN | WM_MBUTTONDOWN | WM_RBUTTONDOWN | WM_XBUTTONDOWN => {
|
||||
// Capture the mouse cursor on button down
|
||||
mouse_button_counter = mouse_button_counter.saturating_add(1);
|
||||
SetCapture(hwnd);
|
||||
MouseEvent::ButtonPressed {
|
||||
button,
|
||||
modifiers: window_state
|
||||
.keyboard_state
|
||||
.borrow()
|
||||
.get_modifiers_from_mouse_wparam(wparam),
|
||||
}
|
||||
}
|
||||
WM_LBUTTONUP | WM_MBUTTONUP | WM_RBUTTONUP | WM_XBUTTONUP => {
|
||||
// Release the mouse cursor capture when all buttons are released
|
||||
mouse_button_counter = mouse_button_counter.saturating_sub(1);
|
||||
if mouse_button_counter == 0 {
|
||||
ReleaseCapture();
|
||||
}
|
||||
|
||||
MouseEvent::ButtonReleased {
|
||||
button,
|
||||
modifiers: window_state
|
||||
.keyboard_state
|
||||
.borrow()
|
||||
.get_modifiers_from_mouse_wparam(wparam),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
unreachable!()
|
||||
}
|
||||
};
|
||||
|
||||
window_state.mouse_button_counter.set(mouse_button_counter);
|
||||
|
||||
window_state
|
||||
.handler
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.on_event(&mut window, Event::Mouse(event));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
WM_TIMER => {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
if wparam == WIN_FRAME_TIMER {
|
||||
window_state.handler.borrow_mut().as_mut().unwrap().on_frame(&mut window);
|
||||
}
|
||||
|
||||
Some(0)
|
||||
}
|
||||
WM_CLOSE => {
|
||||
// Make sure to release the borrow before the DefWindowProc call
|
||||
{
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
window_state
|
||||
.handler
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.on_event(&mut window, Event::Window(WindowEvent::WillClose));
|
||||
}
|
||||
|
||||
// DestroyWindow(hwnd);
|
||||
// Some(0)
|
||||
Some(DefWindowProcW(hwnd, msg, wparam, lparam))
|
||||
}
|
||||
WM_CHAR | WM_SYSCHAR | WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP
|
||||
| WM_INPUTLANGCHANGE => {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
let opt_event =
|
||||
window_state.keyboard_state.borrow_mut().process_message(hwnd, msg, wparam, lparam);
|
||||
|
||||
if let Some(event) = opt_event {
|
||||
window_state
|
||||
.handler
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.on_event(&mut window, Event::Keyboard(event));
|
||||
}
|
||||
|
||||
if msg != WM_SYSKEYDOWN {
|
||||
Some(0)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
WM_SIZE => {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
let width = (lparam & 0xFFFF) as u16 as u32;
|
||||
let height = ((lparam >> 16) & 0xFFFF) as u16 as u32;
|
||||
|
||||
let new_window_info = {
|
||||
let mut window_info = window_state.window_info.borrow_mut();
|
||||
let new_window_info =
|
||||
WindowInfo::from_physical_size(PhySize { width, height }, window_info.scale());
|
||||
|
||||
// Only send the event if anything changed
|
||||
if window_info.physical_size() == new_window_info.physical_size() {
|
||||
return None;
|
||||
}
|
||||
|
||||
*window_info = new_window_info;
|
||||
|
||||
new_window_info
|
||||
};
|
||||
|
||||
window_state
|
||||
.handler
|
||||
.borrow_mut()
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.on_event(&mut window, Event::Window(WindowEvent::Resized(new_window_info)));
|
||||
|
||||
None
|
||||
}
|
||||
WM_DPICHANGED => {
|
||||
// To avoid weirdness with the realtime borrow checker.
|
||||
let new_rect = {
|
||||
if let WindowScalePolicy::SystemScaleFactor = window_state.scale_policy {
|
||||
let dpi = (wparam & 0xFFFF) as u16 as u32;
|
||||
let scale_factor = dpi as f64 / 96.0;
|
||||
|
||||
let mut window_info = window_state.window_info.borrow_mut();
|
||||
*window_info =
|
||||
WindowInfo::from_logical_size(window_info.logical_size(), scale_factor);
|
||||
|
||||
Some((
|
||||
RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
// todo: check if usize fits into i32
|
||||
right: window_info.physical_size().width as i32,
|
||||
bottom: window_info.physical_size().height as i32,
|
||||
},
|
||||
window_state.dw_style,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some((mut new_rect, dw_style)) = new_rect {
|
||||
// Convert this desired "client rectangle" size to the actual "window rectangle"
|
||||
// size (Because of course you have to do that).
|
||||
AdjustWindowRectEx(&mut new_rect, dw_style, 0, 0);
|
||||
|
||||
// Windows makes us resize the window manually. This will trigger another `WM_SIZE` event,
|
||||
// which we can then send the user the new scale factor.
|
||||
SetWindowPos(
|
||||
hwnd,
|
||||
hwnd,
|
||||
new_rect.left,
|
||||
new_rect.top,
|
||||
new_rect.right - new_rect.left,
|
||||
new_rect.bottom - new_rect.top,
|
||||
SWP_NOZORDER | SWP_NOMOVE,
|
||||
);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
// If WM_SETCURSOR returns `None`, WM_SETCURSOR continues to get handled by the outer window(s),
|
||||
// If it returns `Some(1)`, the current window decides what the cursor is
|
||||
WM_SETCURSOR => {
|
||||
let low_word = LOWORD(lparam as u32) as isize;
|
||||
let mouse_in_window = low_word == HTCLIENT;
|
||||
if mouse_in_window {
|
||||
// Here we need to set the cursor back to what the state says, since it can have changed when outside the window
|
||||
let cursor =
|
||||
LoadCursorW(null_mut(), cursor_to_lpcwstr(window_state.cursor_icon.get()));
|
||||
unsafe {
|
||||
SetCursor(cursor);
|
||||
}
|
||||
Some(1)
|
||||
} else {
|
||||
// Cursor is being changed by some other window, e.g. when having mouse on the borders to resize it
|
||||
None
|
||||
}
|
||||
}
|
||||
// NOTE: `WM_NCDESTROY` is handled in the outer function because this deallocates the window
|
||||
// state
|
||||
BV_WINDOW_MUST_CLOSE => {
|
||||
DestroyWindow(hwnd);
|
||||
Some(0)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn register_wnd_class() -> ATOM {
|
||||
// We generate a unique name for the new window class to prevent name collisions
|
||||
let class_name_str = format!("Baseview-{}", generate_guid());
|
||||
let mut class_name: Vec<u16> = OsStr::new(&class_name_str).encode_wide().collect();
|
||||
class_name.push(0);
|
||||
|
||||
let wnd_class = WNDCLASSW {
|
||||
style: CS_OWNDC,
|
||||
lpfnWndProc: Some(wnd_proc),
|
||||
hInstance: null_mut(),
|
||||
lpszClassName: class_name.as_ptr(),
|
||||
cbClsExtra: 0,
|
||||
cbWndExtra: 0,
|
||||
hIcon: null_mut(),
|
||||
hCursor: LoadCursorW(null_mut(), IDC_ARROW),
|
||||
hbrBackground: null_mut(),
|
||||
lpszMenuName: null_mut(),
|
||||
};
|
||||
|
||||
RegisterClassW(&wnd_class)
|
||||
}
|
||||
|
||||
unsafe fn unregister_wnd_class(wnd_class: ATOM) {
|
||||
UnregisterClassW(wnd_class as _, null_mut());
|
||||
}
|
||||
|
||||
/// All data associated with the window. This uses internal mutability so the outer struct doesn't
|
||||
/// need to be mutably borrowed. Mutably borrowing the entire `WindowState` can be problematic
|
||||
/// because of the Windows message loops' reentrant nature. Care still needs to be taken to prevent
|
||||
/// `handler` from indirectly triggering other events that would also need to be handled using
|
||||
/// `handler`.
|
||||
pub(super) struct WindowState {
|
||||
/// The HWND belonging to this window. The window's actual state is stored in the `WindowState`
|
||||
/// struct associated with this HWND through `unsafe { GetWindowLongPtrW(self.hwnd,
|
||||
/// GWLP_USERDATA) } as *const WindowState`.
|
||||
pub hwnd: HWND,
|
||||
window_class: ATOM,
|
||||
window_info: RefCell<WindowInfo>,
|
||||
_parent_handle: Option<ParentHandle>,
|
||||
keyboard_state: RefCell<KeyboardState>,
|
||||
mouse_button_counter: Cell<usize>,
|
||||
mouse_was_outside_window: RefCell<bool>,
|
||||
cursor_icon: Cell<MouseCursor>,
|
||||
// Initialized late so the `Window` can hold a reference to this `WindowState`
|
||||
handler: RefCell<Option<Box<dyn WindowHandler>>>,
|
||||
_drop_target: RefCell<Option<Rc<DropTarget>>>,
|
||||
scale_policy: WindowScalePolicy,
|
||||
dw_style: u32,
|
||||
|
||||
// handle to the win32 keyboard hook
|
||||
// we don't need to read from this, just carry it around so the Drop impl can run
|
||||
#[allow(dead_code)]
|
||||
kb_hook: KeyboardHookHandle,
|
||||
|
||||
/// Tasks that should be executed at the end of `wnd_proc`. This is needed to avoid mutably
|
||||
/// borrowing the fields from `WindowState` more than once. For instance, when the window
|
||||
/// handler requests a resize in response to a keyboard event, the window state will already be
|
||||
/// borrowed in `wnd_proc`. So the `resize()` function below cannot also mutably borrow that
|
||||
/// window state at the same time.
|
||||
pub deferred_tasks: RefCell<VecDeque<WindowTask>>,
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
pub gl_context: Option<GlContext>,
|
||||
}
|
||||
|
||||
impl WindowState {
|
||||
pub(super) fn create_window(&self) -> Window<'_> {
|
||||
Window { state: self }
|
||||
}
|
||||
|
||||
pub(super) fn window_info(&self) -> Ref<'_, WindowInfo> {
|
||||
self.window_info.borrow()
|
||||
}
|
||||
|
||||
pub(super) fn keyboard_state(&self) -> Ref<'_, KeyboardState> {
|
||||
self.keyboard_state.borrow()
|
||||
}
|
||||
|
||||
pub(super) fn handler_mut(&self) -> RefMut<'_, Option<Box<dyn WindowHandler>>> {
|
||||
self.handler.borrow_mut()
|
||||
}
|
||||
|
||||
/// Handle a deferred task as described in [`Self::deferred_tasks`].
|
||||
pub(self) fn handle_deferred_task(&self, task: WindowTask) {
|
||||
match task {
|
||||
WindowTask::Resize(size) => {
|
||||
// `self.window_info` will be modified in response to the `WM_SIZE` event that
|
||||
// follows the `SetWindowPos()` call
|
||||
let scaling = self.window_info.borrow().scale();
|
||||
let window_info = WindowInfo::from_logical_size(size, scaling);
|
||||
|
||||
// If the window is a standalone window then the size needs to include the window
|
||||
// decorations
|
||||
let mut rect = RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: window_info.physical_size().width as i32,
|
||||
bottom: window_info.physical_size().height as i32,
|
||||
};
|
||||
unsafe {
|
||||
AdjustWindowRectEx(&mut rect, self.dw_style, 0, 0);
|
||||
SetWindowPos(
|
||||
self.hwnd,
|
||||
self.hwnd,
|
||||
0,
|
||||
0,
|
||||
rect.right - rect.left,
|
||||
rect.bottom - rect.top,
|
||||
SWP_NOZORDER | SWP_NOMOVE,
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tasks that must be deferred until the end of [`wnd_proc()`] to avoid reentrant `WindowState`
|
||||
/// borrows. See the docstring on [`WindowState::deferred_tasks`] for more information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) enum WindowTask {
|
||||
/// Resize the window to the given size. The size is in logical pixels. DPI scaling is applied
|
||||
/// automatically.
|
||||
Resize(Size),
|
||||
}
|
||||
|
||||
pub struct Window<'a> {
|
||||
state: &'a WindowState,
|
||||
}
|
||||
|
||||
impl Window<'_> {
|
||||
pub fn open_parented<P, H, B>(parent: &P, options: WindowOpenOptions, build: B) -> WindowHandle
|
||||
where
|
||||
P: HasRawWindowHandle,
|
||||
H: WindowHandler + 'static,
|
||||
B: FnOnce(&mut crate::Window) -> H,
|
||||
B: Send + 'static,
|
||||
{
|
||||
let parent = match parent.raw_window_handle() {
|
||||
RawWindowHandle::Win32(h) => h.hwnd as HWND,
|
||||
h => panic!("unsupported parent handle {:?}", h),
|
||||
};
|
||||
|
||||
let (window_handle, _) = Self::open(true, parent, options, build);
|
||||
|
||||
window_handle
|
||||
}
|
||||
|
||||
pub fn open_blocking<H, B>(options: WindowOpenOptions, build: B)
|
||||
where
|
||||
H: WindowHandler + 'static,
|
||||
B: FnOnce(&mut crate::Window) -> H,
|
||||
B: Send + 'static,
|
||||
{
|
||||
let (_, hwnd) = Self::open(false, null_mut(), options, build);
|
||||
|
||||
unsafe {
|
||||
let mut msg: MSG = std::mem::zeroed();
|
||||
|
||||
loop {
|
||||
let status = GetMessageW(&mut msg, hwnd, 0, 0);
|
||||
|
||||
if status == -1 {
|
||||
break;
|
||||
}
|
||||
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open<H, B>(
|
||||
parented: bool, parent: HWND, options: WindowOpenOptions, build: B,
|
||||
) -> (WindowHandle, HWND)
|
||||
where
|
||||
H: WindowHandler + 'static,
|
||||
B: FnOnce(&mut crate::Window) -> H,
|
||||
B: Send + 'static,
|
||||
{
|
||||
unsafe {
|
||||
let mut title: Vec<u16> = OsStr::new(&options.title[..]).encode_wide().collect();
|
||||
title.push(0);
|
||||
|
||||
let window_class = register_wnd_class();
|
||||
// todo: manage error ^
|
||||
|
||||
let scaling = match options.scale {
|
||||
WindowScalePolicy::SystemScaleFactor => 1.0,
|
||||
WindowScalePolicy::ScaleFactor(scale) => scale,
|
||||
};
|
||||
|
||||
let window_info = WindowInfo::from_logical_size(options.size, scaling);
|
||||
|
||||
let mut rect = RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
// todo: check if usize fits into i32
|
||||
right: window_info.physical_size().width as i32,
|
||||
bottom: window_info.physical_size().height as i32,
|
||||
};
|
||||
|
||||
let flags = if parented {
|
||||
WS_CHILD | WS_VISIBLE
|
||||
} else {
|
||||
WS_POPUPWINDOW
|
||||
| WS_CAPTION
|
||||
| WS_VISIBLE
|
||||
| WS_SIZEBOX
|
||||
| WS_MINIMIZEBOX
|
||||
| WS_MAXIMIZEBOX
|
||||
| WS_CLIPSIBLINGS
|
||||
};
|
||||
|
||||
if !parented {
|
||||
AdjustWindowRectEx(&mut rect, flags, FALSE, 0);
|
||||
}
|
||||
|
||||
let hwnd = CreateWindowExW(
|
||||
0,
|
||||
window_class as _,
|
||||
title.as_ptr(),
|
||||
flags,
|
||||
0,
|
||||
0,
|
||||
rect.right - rect.left,
|
||||
rect.bottom - rect.top,
|
||||
parent as *mut _,
|
||||
null_mut(),
|
||||
null_mut(),
|
||||
null_mut(),
|
||||
);
|
||||
// todo: manage error ^
|
||||
|
||||
let kb_hook = hook::init_keyboard_hook(hwnd);
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
let gl_context: Option<GlContext> = options.gl_config.map(|gl_config| {
|
||||
let mut handle = Win32WindowHandle::empty();
|
||||
handle.hwnd = hwnd as *mut c_void;
|
||||
let handle = RawWindowHandle::Win32(handle);
|
||||
|
||||
GlContext::create(&handle, gl_config).expect("Could not create OpenGL context")
|
||||
});
|
||||
|
||||
let (parent_handle, window_handle) = ParentHandle::new(hwnd);
|
||||
let parent_handle = if parented { Some(parent_handle) } else { None };
|
||||
|
||||
let window_state = Rc::new(WindowState {
|
||||
hwnd,
|
||||
window_class,
|
||||
window_info: RefCell::new(window_info),
|
||||
_parent_handle: parent_handle,
|
||||
keyboard_state: RefCell::new(KeyboardState::new()),
|
||||
mouse_button_counter: Cell::new(0),
|
||||
mouse_was_outside_window: RefCell::new(true),
|
||||
cursor_icon: Cell::new(MouseCursor::Default),
|
||||
// The Window refers to this `WindowState`, so this `handler` needs to be
|
||||
// initialized later
|
||||
handler: RefCell::new(None),
|
||||
_drop_target: RefCell::new(None),
|
||||
scale_policy: options.scale,
|
||||
dw_style: flags,
|
||||
|
||||
deferred_tasks: RefCell::new(VecDeque::with_capacity(4)),
|
||||
|
||||
kb_hook,
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
gl_context,
|
||||
});
|
||||
|
||||
let handler = {
|
||||
let mut window = crate::Window::new(window_state.create_window());
|
||||
|
||||
build(&mut window)
|
||||
};
|
||||
*window_state.handler.borrow_mut() = Some(Box::new(handler));
|
||||
|
||||
// Only works on Windows 10 unfortunately.
|
||||
SetProcessDpiAwarenessContext(
|
||||
winapi::shared::windef::DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE,
|
||||
);
|
||||
|
||||
// Now we can get the actual dpi of the window.
|
||||
let new_rect = if let WindowScalePolicy::SystemScaleFactor = options.scale {
|
||||
// Only works on Windows 10 unfortunately.
|
||||
let dpi = GetDpiForWindow(hwnd);
|
||||
let scale_factor = dpi as f64 / 96.0;
|
||||
|
||||
let mut window_info = window_state.window_info.borrow_mut();
|
||||
if window_info.scale() != scale_factor {
|
||||
*window_info =
|
||||
WindowInfo::from_logical_size(window_info.logical_size(), scale_factor);
|
||||
|
||||
Some(RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
// todo: check if usize fits into i32
|
||||
right: window_info.physical_size().width as i32,
|
||||
bottom: window_info.physical_size().height as i32,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let drop_target = Rc::new(DropTarget::new(Rc::downgrade(&window_state)));
|
||||
*window_state._drop_target.borrow_mut() = Some(drop_target.clone());
|
||||
|
||||
OleInitialize(null_mut());
|
||||
RegisterDragDrop(hwnd, Rc::as_ptr(&drop_target) as LPDROPTARGET);
|
||||
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, Rc::into_raw(window_state) as *const _ as _);
|
||||
SetTimer(hwnd, WIN_FRAME_TIMER, 15, None);
|
||||
|
||||
if let Some(mut new_rect) = new_rect {
|
||||
// Convert this desired"client rectangle" size to the actual "window rectangle"
|
||||
// size (Because of course you have to do that).
|
||||
AdjustWindowRectEx(&mut new_rect, flags, 0, 0);
|
||||
|
||||
// Windows makes us resize the window manually. This will trigger another `WM_SIZE` event,
|
||||
// which we can then send the user the new scale factor.
|
||||
SetWindowPos(
|
||||
hwnd,
|
||||
hwnd,
|
||||
new_rect.left,
|
||||
new_rect.top,
|
||||
new_rect.right - new_rect.left,
|
||||
new_rect.bottom - new_rect.top,
|
||||
SWP_NOZORDER | SWP_NOMOVE,
|
||||
);
|
||||
}
|
||||
|
||||
(window_handle, hwnd)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(&mut self) {
|
||||
unsafe {
|
||||
PostMessageW(self.state.hwnd, BV_WINDOW_MUST_CLOSE, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_focus(&mut self) -> bool {
|
||||
let focused_window = unsafe { GetFocus() };
|
||||
focused_window == self.state.hwnd
|
||||
}
|
||||
|
||||
pub fn focus(&mut self) {
|
||||
unsafe {
|
||||
SetFocus(self.state.hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: Size) {
|
||||
// To avoid reentrant event handler calls we'll defer the actual resizing until after the
|
||||
// event has been handled
|
||||
let task = WindowTask::Resize(size);
|
||||
self.state.deferred_tasks.borrow_mut().push_back(task);
|
||||
}
|
||||
|
||||
pub fn set_mouse_cursor(&mut self, mouse_cursor: MouseCursor) {
|
||||
self.state.cursor_icon.set(mouse_cursor);
|
||||
unsafe {
|
||||
let cursor = LoadCursorW(null_mut(), cursor_to_lpcwstr(mouse_cursor));
|
||||
SetCursor(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
pub fn gl_context(&self) -> Option<&GlContext> {
|
||||
self.state.gl_context.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl HasRawWindowHandle for Window<'_> {
|
||||
fn raw_window_handle(&self) -> RawWindowHandle {
|
||||
let mut handle = Win32WindowHandle::empty();
|
||||
handle.hwnd = self.state.hwnd as *mut c_void;
|
||||
|
||||
RawWindowHandle::Win32(handle)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl HasRawDisplayHandle for Window<'_> {
|
||||
fn raw_display_handle(&self) -> RawDisplayHandle {
|
||||
RawDisplayHandle::Windows(WindowsDisplayHandle::empty())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_to_clipboard(_data: &str) {
|
||||
todo!()
|
||||
}
|
||||
131
crates/baseview/src/window.rs
Normal file
131
crates/baseview/src/window.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use raw_window_handle::{
|
||||
HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle,
|
||||
};
|
||||
|
||||
use crate::event::{Event, EventStatus};
|
||||
use crate::window_open_options::WindowOpenOptions;
|
||||
use crate::{MouseCursor, Size};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::macos as platform;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::win as platform;
|
||||
#[cfg(target_os = "linux")]
|
||||
use crate::x11 as platform;
|
||||
|
||||
pub struct WindowHandle {
|
||||
window_handle: platform::WindowHandle,
|
||||
// so that WindowHandle is !Send on all platforms
|
||||
phantom: PhantomData<*mut ()>,
|
||||
}
|
||||
|
||||
impl WindowHandle {
|
||||
fn new(window_handle: platform::WindowHandle) -> Self {
|
||||
Self { window_handle, phantom: PhantomData }
|
||||
}
|
||||
|
||||
/// Close the window
|
||||
pub fn close(&mut self) {
|
||||
self.window_handle.close();
|
||||
}
|
||||
|
||||
/// Returns `true` if the window is still open, and returns `false`
|
||||
/// if the window was closed/dropped.
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.window_handle.is_open()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl HasRawWindowHandle for WindowHandle {
|
||||
fn raw_window_handle(&self) -> RawWindowHandle {
|
||||
self.window_handle.raw_window_handle()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait WindowHandler {
|
||||
fn on_frame(&mut self, window: &mut Window);
|
||||
fn on_event(&mut self, window: &mut Window, event: Event) -> EventStatus;
|
||||
}
|
||||
|
||||
pub struct Window<'a> {
|
||||
window: platform::Window<'a>,
|
||||
|
||||
// so that Window is !Send on all platforms
|
||||
phantom: PhantomData<*mut ()>,
|
||||
}
|
||||
|
||||
impl<'a> Window<'a> {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub(crate) fn new(window: platform::Window<'a>) -> Window<'a> {
|
||||
Window { window, phantom: PhantomData }
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) fn new(window: platform::Window) -> Window {
|
||||
Window { window, phantom: PhantomData }
|
||||
}
|
||||
|
||||
pub fn open_parented<P, H, B>(parent: &P, options: WindowOpenOptions, build: B) -> WindowHandle
|
||||
where
|
||||
P: HasRawWindowHandle,
|
||||
H: WindowHandler + 'static,
|
||||
B: FnOnce(&mut Window) -> H,
|
||||
B: Send + 'static,
|
||||
{
|
||||
let window_handle = platform::Window::open_parented::<P, H, B>(parent, options, build);
|
||||
WindowHandle::new(window_handle)
|
||||
}
|
||||
|
||||
pub fn open_blocking<H, B>(options: WindowOpenOptions, build: B)
|
||||
where
|
||||
H: WindowHandler + 'static,
|
||||
B: FnOnce(&mut Window) -> H,
|
||||
B: Send + 'static,
|
||||
{
|
||||
platform::Window::open_blocking::<H, B>(options, build)
|
||||
}
|
||||
|
||||
/// Close the window
|
||||
pub fn close(&mut self) {
|
||||
self.window.close();
|
||||
}
|
||||
|
||||
/// Resize the window to the given size. The size is always in logical pixels. DPI scaling will
|
||||
/// automatically be accounted for.
|
||||
pub fn resize(&mut self, size: Size) {
|
||||
self.window.resize(size);
|
||||
}
|
||||
|
||||
pub fn set_mouse_cursor(&mut self, cursor: MouseCursor) {
|
||||
self.window.set_mouse_cursor(cursor);
|
||||
}
|
||||
|
||||
pub fn has_focus(&mut self) -> bool {
|
||||
self.window.has_focus()
|
||||
}
|
||||
|
||||
pub fn focus(&mut self) {
|
||||
self.window.focus()
|
||||
}
|
||||
|
||||
/// If provided, then an OpenGL context will be created for this window. You'll be able to
|
||||
/// access this context through [crate::Window::gl_context].
|
||||
#[cfg(feature = "opengl")]
|
||||
pub fn gl_context(&self) -> Option<&crate::gl::GlContext> {
|
||||
self.window.gl_context()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<'a> HasRawWindowHandle for Window<'a> {
|
||||
fn raw_window_handle(&self) -> RawWindowHandle {
|
||||
self.window.raw_window_handle()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<'a> HasRawDisplayHandle for Window<'a> {
|
||||
fn raw_display_handle(&self) -> RawDisplayHandle {
|
||||
self.window.raw_display_handle()
|
||||
}
|
||||
}
|
||||
144
crates/baseview/src/window_info.rs
Normal file
144
crates/baseview/src/window_info.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
/// The info about the window
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct WindowInfo {
|
||||
logical_size: Size,
|
||||
physical_size: PhySize,
|
||||
scale: f64,
|
||||
scale_recip: f64,
|
||||
}
|
||||
|
||||
impl WindowInfo {
|
||||
pub fn from_logical_size(logical_size: Size, scale: f64) -> Self {
|
||||
let scale_recip = if scale == 1.0 { 1.0 } else { 1.0 / scale };
|
||||
|
||||
let physical_size = PhySize {
|
||||
width: (logical_size.width * scale).round() as u32,
|
||||
height: (logical_size.height * scale).round() as u32,
|
||||
};
|
||||
|
||||
Self { logical_size, physical_size, scale, scale_recip }
|
||||
}
|
||||
|
||||
pub fn from_physical_size(physical_size: PhySize, scale: f64) -> Self {
|
||||
let scale_recip = if scale == 1.0 { 1.0 } else { 1.0 / scale };
|
||||
|
||||
let logical_size = Size {
|
||||
width: f64::from(physical_size.width) * scale_recip,
|
||||
height: f64::from(physical_size.height) * scale_recip,
|
||||
};
|
||||
|
||||
Self { logical_size, physical_size, scale, scale_recip }
|
||||
}
|
||||
|
||||
/// The logical size of the window
|
||||
pub fn logical_size(&self) -> Size {
|
||||
self.logical_size
|
||||
}
|
||||
|
||||
/// The physical size of the window
|
||||
pub fn physical_size(&self) -> PhySize {
|
||||
self.physical_size
|
||||
}
|
||||
|
||||
/// The scale factor of the window
|
||||
pub fn scale(&self) -> f64 {
|
||||
self.scale
|
||||
}
|
||||
|
||||
/// The reciprocal of the scale factor of the window
|
||||
pub fn scale_recip(&self) -> f64 {
|
||||
self.scale_recip
|
||||
}
|
||||
}
|
||||
|
||||
/// A point in logical coordinates
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct Point {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
}
|
||||
|
||||
impl Point {
|
||||
/// Create a new point in logical coordinates
|
||||
pub fn new(x: f64, y: f64) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
/// Convert to actual physical coordinates
|
||||
#[inline]
|
||||
pub fn to_physical(&self, window_info: &WindowInfo) -> PhyPoint {
|
||||
PhyPoint {
|
||||
x: (self.x * window_info.scale()).round() as i32,
|
||||
y: (self.y * window_info.scale()).round() as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A point in actual physical coordinates
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct PhyPoint {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl PhyPoint {
|
||||
/// Create a new point in actual physical coordinates
|
||||
pub fn new(x: i32, y: i32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
|
||||
/// Convert to logical coordinates
|
||||
#[inline]
|
||||
pub fn to_logical(&self, window_info: &WindowInfo) -> Point {
|
||||
Point {
|
||||
x: f64::from(self.x) * window_info.scale_recip(),
|
||||
y: f64::from(self.y) * window_info.scale_recip(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A size in logical coordinates
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct Size {
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
}
|
||||
|
||||
impl Size {
|
||||
/// Create a new size in logical coordinates
|
||||
pub fn new(width: f64, height: f64) -> Self {
|
||||
Self { width, height }
|
||||
}
|
||||
|
||||
/// Convert to actual physical size
|
||||
#[inline]
|
||||
pub fn to_physical(&self, window_info: &WindowInfo) -> PhySize {
|
||||
PhySize {
|
||||
width: (self.width * window_info.scale()).round() as u32,
|
||||
height: (self.height * window_info.scale()).round() as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An actual size in physical coordinates
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct PhySize {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl PhySize {
|
||||
/// Create a new size in actual physical coordinates
|
||||
pub fn new(width: u32, height: u32) -> Self {
|
||||
Self { width, height }
|
||||
}
|
||||
|
||||
/// Convert to logical size
|
||||
#[inline]
|
||||
pub fn to_logical(&self, window_info: &WindowInfo) -> Size {
|
||||
Size {
|
||||
width: f64::from(self.width) * window_info.scale_recip(),
|
||||
height: f64::from(self.height) * window_info.scale_recip(),
|
||||
}
|
||||
}
|
||||
}
|
||||
29
crates/baseview/src/window_open_options.rs
Normal file
29
crates/baseview/src/window_open_options.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use crate::Size;
|
||||
|
||||
/// The dpi scaling policy of the window
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum WindowScalePolicy {
|
||||
/// Use the system's dpi scale factor
|
||||
SystemScaleFactor,
|
||||
/// Use the given dpi scale factor (e.g. `1.0` = 96 dpi)
|
||||
ScaleFactor(f64),
|
||||
}
|
||||
|
||||
/// The options for opening a new window
|
||||
pub struct WindowOpenOptions {
|
||||
pub title: String,
|
||||
|
||||
/// The logical size of the window.
|
||||
///
|
||||
/// These dimensions will be scaled by the scaling policy specified in `scale`. Mouse
|
||||
/// position will be passed back as logical coordinates.
|
||||
pub size: Size,
|
||||
|
||||
/// The dpi scaling policy
|
||||
pub scale: WindowScalePolicy,
|
||||
|
||||
/// If provided, then an OpenGL context will be created for this window. You'll be able to
|
||||
/// access this context through [crate::Window::gl_context].
|
||||
#[cfg(feature = "opengl")]
|
||||
pub gl_config: Option<crate::gl::GlConfig>,
|
||||
}
|
||||
100
crates/baseview/src/x11/cursor.rs
Normal file
100
crates/baseview/src/x11/cursor.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use std::error::Error;
|
||||
|
||||
use x11rb::connection::Connection;
|
||||
use x11rb::cursor::Handle as CursorHandle;
|
||||
use x11rb::protocol::xproto::{ConnectionExt as _, Cursor};
|
||||
use x11rb::xcb_ffi::XCBConnection;
|
||||
|
||||
use crate::MouseCursor;
|
||||
|
||||
fn create_empty_cursor(conn: &XCBConnection, screen: usize) -> Result<Cursor, Box<dyn Error>> {
|
||||
let cursor_id = conn.generate_id()?;
|
||||
let pixmap_id = conn.generate_id()?;
|
||||
let root_window = conn.setup().roots[screen].root;
|
||||
conn.create_pixmap(1, pixmap_id, root_window, 1, 1)?;
|
||||
conn.create_cursor(cursor_id, pixmap_id, pixmap_id, 0, 0, 0, 0, 0, 0, 0, 0)?;
|
||||
conn.free_pixmap(pixmap_id)?;
|
||||
|
||||
Ok(cursor_id)
|
||||
}
|
||||
|
||||
fn load_cursor(
|
||||
conn: &XCBConnection, cursor_handle: &CursorHandle, name: &str,
|
||||
) -> Result<Option<Cursor>, Box<dyn Error>> {
|
||||
let cursor = cursor_handle.load_cursor(conn, name)?;
|
||||
if cursor != x11rb::NONE {
|
||||
Ok(Some(cursor))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn load_first_existing_cursor(
|
||||
conn: &XCBConnection, cursor_handle: &CursorHandle, names: &[&str],
|
||||
) -> Result<Option<Cursor>, Box<dyn Error>> {
|
||||
for name in names {
|
||||
let cursor = load_cursor(conn, cursor_handle, name)?;
|
||||
if cursor.is_some() {
|
||||
return Ok(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(super) fn get_xcursor(
|
||||
conn: &XCBConnection, screen: usize, cursor_handle: &CursorHandle, cursor: MouseCursor,
|
||||
) -> Result<Cursor, Box<dyn Error>> {
|
||||
let load = |name: &str| load_cursor(conn, cursor_handle, name);
|
||||
let loadn = |names: &[&str]| load_first_existing_cursor(conn, cursor_handle, names);
|
||||
|
||||
let cursor = match cursor {
|
||||
MouseCursor::Default => None, // catch this in the fallback case below
|
||||
|
||||
MouseCursor::Hand => loadn(&["hand2", "hand1"])?,
|
||||
MouseCursor::HandGrabbing => loadn(&["closedhand", "grabbing"])?,
|
||||
MouseCursor::Help => load("question_arrow")?,
|
||||
|
||||
MouseCursor::Hidden => Some(create_empty_cursor(conn, screen)?),
|
||||
|
||||
MouseCursor::Text => loadn(&["text", "xterm"])?,
|
||||
MouseCursor::VerticalText => load("vertical-text")?,
|
||||
|
||||
MouseCursor::Working => load("watch")?,
|
||||
MouseCursor::PtrWorking => load("left_ptr_watch")?,
|
||||
|
||||
MouseCursor::NotAllowed => load("crossed_circle")?,
|
||||
MouseCursor::PtrNotAllowed => loadn(&["no-drop", "crossed_circle"])?,
|
||||
|
||||
MouseCursor::ZoomIn => load("zoom-in")?,
|
||||
MouseCursor::ZoomOut => load("zoom-out")?,
|
||||
|
||||
MouseCursor::Alias => load("link")?,
|
||||
MouseCursor::Copy => load("copy")?,
|
||||
MouseCursor::Move => load("move")?,
|
||||
MouseCursor::AllScroll => load("all-scroll")?,
|
||||
MouseCursor::Cell => load("plus")?,
|
||||
MouseCursor::Crosshair => load("crosshair")?,
|
||||
|
||||
MouseCursor::EResize => load("right_side")?,
|
||||
MouseCursor::NResize => load("top_side")?,
|
||||
MouseCursor::NeResize => load("top_right_corner")?,
|
||||
MouseCursor::NwResize => load("top_left_corner")?,
|
||||
MouseCursor::SResize => load("bottom_side")?,
|
||||
MouseCursor::SeResize => load("bottom_right_corner")?,
|
||||
MouseCursor::SwResize => load("bottom_left_corner")?,
|
||||
MouseCursor::WResize => load("left_side")?,
|
||||
MouseCursor::EwResize => load("h_double_arrow")?,
|
||||
MouseCursor::NsResize => load("v_double_arrow")?,
|
||||
MouseCursor::NwseResize => loadn(&["bd_double_arrow", "size_bdiag"])?,
|
||||
MouseCursor::NeswResize => loadn(&["fd_double_arrow", "size_fdiag"])?,
|
||||
MouseCursor::ColResize => loadn(&["split_h", "h_double_arrow"])?,
|
||||
MouseCursor::RowResize => loadn(&["split_v", "v_double_arrow"])?,
|
||||
};
|
||||
|
||||
if let Some(cursor) = cursor {
|
||||
Ok(cursor)
|
||||
} else {
|
||||
Ok(load("left_ptr")?.unwrap_or(x11rb::NONE))
|
||||
}
|
||||
}
|
||||
299
crates/baseview/src/x11/event_loop.rs
Normal file
299
crates/baseview/src/x11/event_loop.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
use crate::x11::keyboard::{convert_key_press_event, convert_key_release_event, key_mods};
|
||||
use crate::x11::{ParentHandle, Window, WindowInner};
|
||||
use crate::{
|
||||
Event, MouseButton, MouseEvent, PhyPoint, PhySize, ScrollDelta, WindowEvent, WindowHandler,
|
||||
WindowInfo,
|
||||
};
|
||||
use std::error::Error;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::time::{Duration, Instant};
|
||||
use x11rb::connection::Connection;
|
||||
use x11rb::protocol::Event as XEvent;
|
||||
|
||||
pub(super) struct EventLoop {
|
||||
handler: Box<dyn WindowHandler>,
|
||||
window: WindowInner,
|
||||
parent_handle: Option<ParentHandle>,
|
||||
|
||||
new_physical_size: Option<PhySize>,
|
||||
frame_interval: Duration,
|
||||
event_loop_running: bool,
|
||||
}
|
||||
|
||||
impl EventLoop {
|
||||
pub fn new(
|
||||
window: WindowInner, handler: impl WindowHandler + 'static,
|
||||
parent_handle: Option<ParentHandle>,
|
||||
) -> Self {
|
||||
Self {
|
||||
window,
|
||||
handler: Box::new(handler),
|
||||
parent_handle,
|
||||
frame_interval: Duration::from_millis(15),
|
||||
event_loop_running: false,
|
||||
new_physical_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn drain_xcb_events(&mut self) -> Result<(), Box<dyn Error>> {
|
||||
// the X server has a tendency to send spurious/extraneous configure notify events when a
|
||||
// window is resized, and we need to batch those together and just send one resize event
|
||||
// when they've all been coalesced.
|
||||
self.new_physical_size = None;
|
||||
|
||||
while let Some(event) = self.window.xcb_connection.conn.poll_for_event()? {
|
||||
self.handle_xcb_event(event);
|
||||
}
|
||||
|
||||
if let Some(size) = self.new_physical_size.take() {
|
||||
self.window.window_info =
|
||||
WindowInfo::from_physical_size(size, self.window.window_info.scale());
|
||||
|
||||
let window_info = self.window.window_info;
|
||||
|
||||
self.handler.on_event(
|
||||
&mut crate::Window::new(Window { inner: &self.window }),
|
||||
Event::Window(WindowEvent::Resized(window_info)),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Event loop
|
||||
// FIXME: poll() acts fine on linux, sometimes funky on *BSD. XCB upstream uses a define to
|
||||
// switch between poll() and select() (the latter of which is fine on *BSD), and we should do
|
||||
// the same.
|
||||
pub fn run(&mut self) -> Result<(), Box<dyn Error>> {
|
||||
use nix::poll::*;
|
||||
|
||||
let xcb_fd = self.window.xcb_connection.conn.as_raw_fd();
|
||||
|
||||
let mut last_frame = Instant::now();
|
||||
self.event_loop_running = true;
|
||||
|
||||
while self.event_loop_running {
|
||||
// We'll try to keep a consistent frame pace. If the last frame couldn't be processed in
|
||||
// the expected frame time, this will throttle down to prevent multiple frames from
|
||||
// being queued up. The conditional here is needed because event handling and frame
|
||||
// drawing is interleaved. The `poll()` function below will wait until the next frame
|
||||
// can be drawn, or until the window receives an event. We thus need to manually check
|
||||
// if it's already time to draw a new frame.
|
||||
let next_frame = last_frame + self.frame_interval;
|
||||
if Instant::now() >= next_frame {
|
||||
self.handler.on_frame(&mut crate::Window::new(Window { inner: &self.window }));
|
||||
last_frame = Instant::max(next_frame, Instant::now() - self.frame_interval);
|
||||
}
|
||||
|
||||
let mut fds = [PollFd::new(xcb_fd, PollFlags::POLLIN)];
|
||||
|
||||
// Check for any events in the internal buffers
|
||||
// before going to sleep:
|
||||
self.drain_xcb_events()?;
|
||||
|
||||
// FIXME: handle errors
|
||||
poll(&mut fds, next_frame.duration_since(Instant::now()).subsec_millis() as i32)
|
||||
.unwrap();
|
||||
|
||||
if let Some(revents) = fds[0].revents() {
|
||||
if revents.contains(PollFlags::POLLERR) {
|
||||
panic!("xcb connection poll error");
|
||||
}
|
||||
|
||||
if revents.contains(PollFlags::POLLIN) {
|
||||
self.drain_xcb_events()?;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the parents's handle was dropped (such as when the host
|
||||
// requested the window to close)
|
||||
if let Some(parent_handle) = &self.parent_handle {
|
||||
if parent_handle.parent_did_drop() {
|
||||
self.handle_must_close();
|
||||
self.window.close_requested.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the user has requested the window to close
|
||||
if self.window.close_requested.get() {
|
||||
self.handle_must_close();
|
||||
self.window.close_requested.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_xcb_event(&mut self, event: XEvent) {
|
||||
// For all the keyboard and mouse events, you can fetch
|
||||
// `x`, `y`, `detail`, and `state`.
|
||||
// - `x` and `y` are the position inside the window where the cursor currently is
|
||||
// when the event happened.
|
||||
// - `detail` will tell you which keycode was pressed/released (for keyboard events)
|
||||
// or which mouse button was pressed/released (for mouse events).
|
||||
// For mouse events, here's what the value means (at least on my current mouse):
|
||||
// 1 = left mouse button
|
||||
// 2 = middle mouse button (scroll wheel)
|
||||
// 3 = right mouse button
|
||||
// 4 = scroll wheel up
|
||||
// 5 = scroll wheel down
|
||||
// 8 = lower side button ("back" button)
|
||||
// 9 = upper side button ("forward" button)
|
||||
// Note that you *will* get a "button released" event for even the scroll wheel
|
||||
// events, which you can probably ignore.
|
||||
// - `state` will tell you the state of the main three mouse buttons and some of
|
||||
// the keyboard modifier keys at the time of the event.
|
||||
// http://rtbo.github.io/rust-xcb/src/xcb/ffi/xproto.rs.html#445
|
||||
|
||||
match event {
|
||||
////
|
||||
// window
|
||||
////
|
||||
XEvent::ClientMessage(event) => {
|
||||
if event.format == 32
|
||||
&& event.data.as_data32()[0]
|
||||
== self.window.xcb_connection.atoms.WM_DELETE_WINDOW
|
||||
{
|
||||
self.handle_close_requested();
|
||||
}
|
||||
}
|
||||
|
||||
XEvent::ConfigureNotify(event) => {
|
||||
let new_physical_size = PhySize::new(event.width as u32, event.height as u32);
|
||||
|
||||
if self.new_physical_size.is_some()
|
||||
|| new_physical_size != self.window.window_info.physical_size()
|
||||
{
|
||||
self.new_physical_size = Some(new_physical_size);
|
||||
}
|
||||
}
|
||||
|
||||
////
|
||||
// mouse
|
||||
////
|
||||
XEvent::MotionNotify(event) => {
|
||||
let physical_pos = PhyPoint::new(event.event_x as i32, event.event_y as i32);
|
||||
let logical_pos = physical_pos.to_logical(&self.window.window_info);
|
||||
|
||||
self.handler.on_event(
|
||||
&mut crate::Window::new(Window { inner: &self.window }),
|
||||
Event::Mouse(MouseEvent::CursorMoved {
|
||||
position: logical_pos,
|
||||
modifiers: key_mods(event.state),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
XEvent::EnterNotify(event) => {
|
||||
self.handler.on_event(
|
||||
&mut crate::Window::new(Window { inner: &self.window }),
|
||||
Event::Mouse(MouseEvent::CursorEntered),
|
||||
);
|
||||
// since no `MOTION_NOTIFY` event is generated when `ENTER_NOTIFY` is generated,
|
||||
// we generate a CursorMoved as well, so the mouse position from here isn't lost
|
||||
let physical_pos = PhyPoint::new(event.event_x as i32, event.event_y as i32);
|
||||
let logical_pos = physical_pos.to_logical(&self.window.window_info);
|
||||
self.handler.on_event(
|
||||
&mut crate::Window::new(Window { inner: &self.window }),
|
||||
Event::Mouse(MouseEvent::CursorMoved {
|
||||
position: logical_pos,
|
||||
modifiers: key_mods(event.state),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
XEvent::LeaveNotify(_) => {
|
||||
self.handler.on_event(
|
||||
&mut crate::Window::new(Window { inner: &self.window }),
|
||||
Event::Mouse(MouseEvent::CursorLeft),
|
||||
);
|
||||
}
|
||||
|
||||
XEvent::ButtonPress(event) => match event.detail {
|
||||
4..=7 => {
|
||||
self.handler.on_event(
|
||||
&mut crate::Window::new(Window { inner: &self.window }),
|
||||
Event::Mouse(MouseEvent::WheelScrolled {
|
||||
delta: match event.detail {
|
||||
4 => ScrollDelta::Lines { x: 0.0, y: 1.0 },
|
||||
5 => ScrollDelta::Lines { x: 0.0, y: -1.0 },
|
||||
6 => ScrollDelta::Lines { x: -1.0, y: 0.0 },
|
||||
7 => ScrollDelta::Lines { x: 1.0, y: 0.0 },
|
||||
_ => unreachable!(),
|
||||
},
|
||||
modifiers: key_mods(event.state),
|
||||
}),
|
||||
);
|
||||
}
|
||||
detail => {
|
||||
let button_id = mouse_id(detail);
|
||||
self.handler.on_event(
|
||||
&mut crate::Window::new(Window { inner: &self.window }),
|
||||
Event::Mouse(MouseEvent::ButtonPressed {
|
||||
button: button_id,
|
||||
modifiers: key_mods(event.state),
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
XEvent::ButtonRelease(event) => {
|
||||
if !(4..=7).contains(&event.detail) {
|
||||
let button_id = mouse_id(event.detail);
|
||||
self.handler.on_event(
|
||||
&mut crate::Window::new(Window { inner: &self.window }),
|
||||
Event::Mouse(MouseEvent::ButtonReleased {
|
||||
button: button_id,
|
||||
modifiers: key_mods(event.state),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
////
|
||||
// keys
|
||||
////
|
||||
XEvent::KeyPress(event) => {
|
||||
self.handler.on_event(
|
||||
&mut crate::Window::new(Window { inner: &self.window }),
|
||||
Event::Keyboard(convert_key_press_event(&event)),
|
||||
);
|
||||
}
|
||||
|
||||
XEvent::KeyRelease(event) => {
|
||||
self.handler.on_event(
|
||||
&mut crate::Window::new(Window { inner: &self.window }),
|
||||
Event::Keyboard(convert_key_release_event(&event)),
|
||||
);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_close_requested(&mut self) {
|
||||
// FIXME: handler should decide whether window stays open or not
|
||||
self.handle_must_close();
|
||||
}
|
||||
|
||||
fn handle_must_close(&mut self) {
|
||||
self.handler.on_event(
|
||||
&mut crate::Window::new(Window { inner: &self.window }),
|
||||
Event::Window(WindowEvent::WillClose),
|
||||
);
|
||||
|
||||
self.event_loop_running = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_id(id: u8) -> MouseButton {
|
||||
match id {
|
||||
1 => MouseButton::Left,
|
||||
2 => MouseButton::Middle,
|
||||
3 => MouseButton::Right,
|
||||
8 => MouseButton::Back,
|
||||
9 => MouseButton::Forward,
|
||||
id => MouseButton::Other(id),
|
||||
}
|
||||
}
|
||||
406
crates/baseview/src/x11/keyboard.rs
Normal file
406
crates/baseview/src/x11/keyboard.rs
Normal file
@@ -0,0 +1,406 @@
|
||||
// Copyright 2020 The Druid Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Baseview modifications to druid code:
|
||||
// - collect functions from various files
|
||||
// - update imports, paths etc
|
||||
|
||||
//! X11 keyboard handling
|
||||
|
||||
use x11rb::protocol::xproto::{KeyButMask, KeyPressEvent, KeyReleaseEvent};
|
||||
|
||||
use keyboard_types::*;
|
||||
|
||||
use crate::keyboard::code_to_location;
|
||||
|
||||
/// Convert a hardware scan code to a key.
|
||||
///
|
||||
/// Note: this is a hardcoded layout. We need to detect the user's
|
||||
/// layout from the system and apply it.
|
||||
fn code_to_key(code: Code, m: Modifiers) -> Key {
|
||||
fn a(s: &str) -> Key {
|
||||
Key::Character(s.into())
|
||||
}
|
||||
fn s(mods: Modifiers, base: &str, shifted: &str) -> Key {
|
||||
if mods.contains(Modifiers::SHIFT) {
|
||||
Key::Character(shifted.into())
|
||||
} else {
|
||||
Key::Character(base.into())
|
||||
}
|
||||
}
|
||||
fn n(mods: Modifiers, base: Key, num: &str) -> Key {
|
||||
if mods.contains(Modifiers::NUM_LOCK) != mods.contains(Modifiers::SHIFT) {
|
||||
Key::Character(num.into())
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
match code {
|
||||
Code::KeyA => s(m, "a", "A"),
|
||||
Code::KeyB => s(m, "b", "B"),
|
||||
Code::KeyC => s(m, "c", "C"),
|
||||
Code::KeyD => s(m, "d", "D"),
|
||||
Code::KeyE => s(m, "e", "E"),
|
||||
Code::KeyF => s(m, "f", "F"),
|
||||
Code::KeyG => s(m, "g", "G"),
|
||||
Code::KeyH => s(m, "h", "H"),
|
||||
Code::KeyI => s(m, "i", "I"),
|
||||
Code::KeyJ => s(m, "j", "J"),
|
||||
Code::KeyK => s(m, "k", "K"),
|
||||
Code::KeyL => s(m, "l", "L"),
|
||||
Code::KeyM => s(m, "m", "M"),
|
||||
Code::KeyN => s(m, "n", "N"),
|
||||
Code::KeyO => s(m, "o", "O"),
|
||||
Code::KeyP => s(m, "p", "P"),
|
||||
Code::KeyQ => s(m, "q", "Q"),
|
||||
Code::KeyR => s(m, "r", "R"),
|
||||
Code::KeyS => s(m, "s", "S"),
|
||||
Code::KeyT => s(m, "t", "T"),
|
||||
Code::KeyU => s(m, "u", "U"),
|
||||
Code::KeyV => s(m, "v", "V"),
|
||||
Code::KeyW => s(m, "w", "W"),
|
||||
Code::KeyX => s(m, "x", "X"),
|
||||
Code::KeyY => s(m, "y", "Y"),
|
||||
Code::KeyZ => s(m, "z", "Z"),
|
||||
|
||||
Code::Digit0 => s(m, "0", ")"),
|
||||
Code::Digit1 => s(m, "1", "!"),
|
||||
Code::Digit2 => s(m, "2", "@"),
|
||||
Code::Digit3 => s(m, "3", "#"),
|
||||
Code::Digit4 => s(m, "4", "$"),
|
||||
Code::Digit5 => s(m, "5", "%"),
|
||||
Code::Digit6 => s(m, "6", "^"),
|
||||
Code::Digit7 => s(m, "7", "&"),
|
||||
Code::Digit8 => s(m, "8", "*"),
|
||||
Code::Digit9 => s(m, "9", "("),
|
||||
|
||||
Code::Backquote => s(m, "`", "~"),
|
||||
Code::Minus => s(m, "-", "_"),
|
||||
Code::Equal => s(m, "=", "+"),
|
||||
Code::BracketLeft => s(m, "[", "{"),
|
||||
Code::BracketRight => s(m, "]", "}"),
|
||||
Code::Backslash => s(m, "\\", "|"),
|
||||
Code::Semicolon => s(m, ";", ":"),
|
||||
Code::Quote => s(m, "'", "\""),
|
||||
Code::Comma => s(m, ",", "<"),
|
||||
Code::Period => s(m, ".", ">"),
|
||||
Code::Slash => s(m, "/", "?"),
|
||||
|
||||
Code::Space => a(" "),
|
||||
|
||||
Code::Escape => Key::Escape,
|
||||
Code::Backspace => Key::Backspace,
|
||||
Code::Tab => Key::Tab,
|
||||
Code::Enter => Key::Enter,
|
||||
Code::ControlLeft => Key::Control,
|
||||
Code::ShiftLeft => Key::Shift,
|
||||
Code::ShiftRight => Key::Shift,
|
||||
Code::NumpadMultiply => a("*"),
|
||||
Code::AltLeft => Key::Alt,
|
||||
Code::CapsLock => Key::CapsLock,
|
||||
Code::F1 => Key::F1,
|
||||
Code::F2 => Key::F2,
|
||||
Code::F3 => Key::F3,
|
||||
Code::F4 => Key::F4,
|
||||
Code::F5 => Key::F5,
|
||||
Code::F6 => Key::F6,
|
||||
Code::F7 => Key::F7,
|
||||
Code::F8 => Key::F8,
|
||||
Code::F9 => Key::F9,
|
||||
Code::F10 => Key::F10,
|
||||
Code::NumLock => Key::NumLock,
|
||||
Code::ScrollLock => Key::ScrollLock,
|
||||
Code::Numpad0 => n(m, Key::Insert, "0"),
|
||||
Code::Numpad1 => n(m, Key::End, "1"),
|
||||
Code::Numpad2 => n(m, Key::ArrowDown, "2"),
|
||||
Code::Numpad3 => n(m, Key::PageDown, "3"),
|
||||
Code::Numpad4 => n(m, Key::ArrowLeft, "4"),
|
||||
Code::Numpad5 => n(m, Key::Clear, "5"),
|
||||
Code::Numpad6 => n(m, Key::ArrowRight, "6"),
|
||||
Code::Numpad7 => n(m, Key::Home, "7"),
|
||||
Code::Numpad8 => n(m, Key::ArrowUp, "8"),
|
||||
Code::Numpad9 => n(m, Key::PageUp, "9"),
|
||||
Code::NumpadSubtract => a("-"),
|
||||
Code::NumpadAdd => a("+"),
|
||||
Code::NumpadDecimal => n(m, Key::Delete, "."),
|
||||
Code::IntlBackslash => s(m, "\\", "|"),
|
||||
Code::F11 => Key::F11,
|
||||
Code::F12 => Key::F12,
|
||||
// This mapping is based on the picture in the w3c spec.
|
||||
Code::IntlRo => a("\\"),
|
||||
Code::Convert => Key::Convert,
|
||||
Code::KanaMode => Key::KanaMode,
|
||||
Code::NonConvert => Key::NonConvert,
|
||||
Code::NumpadEnter => Key::Enter,
|
||||
Code::ControlRight => Key::Control,
|
||||
Code::NumpadDivide => a("/"),
|
||||
Code::PrintScreen => Key::PrintScreen,
|
||||
Code::AltRight => Key::Alt,
|
||||
Code::Home => Key::Home,
|
||||
Code::ArrowUp => Key::ArrowUp,
|
||||
Code::PageUp => Key::PageUp,
|
||||
Code::ArrowLeft => Key::ArrowLeft,
|
||||
Code::ArrowRight => Key::ArrowRight,
|
||||
Code::End => Key::End,
|
||||
Code::ArrowDown => Key::ArrowDown,
|
||||
Code::PageDown => Key::PageDown,
|
||||
Code::Insert => Key::Insert,
|
||||
Code::Delete => Key::Delete,
|
||||
Code::AudioVolumeMute => Key::AudioVolumeMute,
|
||||
Code::AudioVolumeDown => Key::AudioVolumeDown,
|
||||
Code::AudioVolumeUp => Key::AudioVolumeUp,
|
||||
Code::NumpadEqual => a("="),
|
||||
Code::Pause => Key::Pause,
|
||||
Code::NumpadComma => a(","),
|
||||
Code::Lang1 => Key::HangulMode,
|
||||
Code::Lang2 => Key::HanjaMode,
|
||||
Code::IntlYen => a("¥"),
|
||||
Code::MetaLeft => Key::Meta,
|
||||
Code::MetaRight => Key::Meta,
|
||||
Code::ContextMenu => Key::ContextMenu,
|
||||
Code::BrowserStop => Key::BrowserStop,
|
||||
Code::Again => Key::Again,
|
||||
Code::Props => Key::Props,
|
||||
Code::Undo => Key::Undo,
|
||||
Code::Select => Key::Select,
|
||||
Code::Copy => Key::Copy,
|
||||
Code::Open => Key::Open,
|
||||
Code::Paste => Key::Paste,
|
||||
Code::Find => Key::Find,
|
||||
Code::Cut => Key::Cut,
|
||||
Code::Help => Key::Help,
|
||||
Code::LaunchApp2 => Key::LaunchApplication2,
|
||||
Code::WakeUp => Key::WakeUp,
|
||||
Code::LaunchApp1 => Key::LaunchApplication1,
|
||||
Code::LaunchMail => Key::LaunchMail,
|
||||
Code::BrowserFavorites => Key::BrowserFavorites,
|
||||
Code::BrowserBack => Key::BrowserBack,
|
||||
Code::BrowserForward => Key::BrowserForward,
|
||||
Code::Eject => Key::Eject,
|
||||
Code::MediaTrackNext => Key::MediaTrackNext,
|
||||
Code::MediaPlayPause => Key::MediaPlayPause,
|
||||
Code::MediaTrackPrevious => Key::MediaTrackPrevious,
|
||||
Code::MediaStop => Key::MediaStop,
|
||||
Code::MediaSelect => Key::LaunchMediaPlayer,
|
||||
Code::BrowserHome => Key::BrowserHome,
|
||||
Code::BrowserRefresh => Key::BrowserRefresh,
|
||||
Code::BrowserSearch => Key::BrowserSearch,
|
||||
|
||||
_ => Key::Unidentified,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
/// Map hardware keycode to code.
|
||||
///
|
||||
/// In theory, the hardware keycode is device dependent, but in
|
||||
/// practice it's probably pretty reliable.
|
||||
///
|
||||
/// The logic is based on NativeKeyToDOMCodeName.h in Mozilla.
|
||||
fn hardware_keycode_to_code(hw_keycode: u16) -> Code {
|
||||
match hw_keycode {
|
||||
0x0009 => Code::Escape,
|
||||
0x000A => Code::Digit1,
|
||||
0x000B => Code::Digit2,
|
||||
0x000C => Code::Digit3,
|
||||
0x000D => Code::Digit4,
|
||||
0x000E => Code::Digit5,
|
||||
0x000F => Code::Digit6,
|
||||
0x0010 => Code::Digit7,
|
||||
0x0011 => Code::Digit8,
|
||||
0x0012 => Code::Digit9,
|
||||
0x0013 => Code::Digit0,
|
||||
0x0014 => Code::Minus,
|
||||
0x0015 => Code::Equal,
|
||||
0x0016 => Code::Backspace,
|
||||
0x0017 => Code::Tab,
|
||||
0x0018 => Code::KeyQ,
|
||||
0x0019 => Code::KeyW,
|
||||
0x001A => Code::KeyE,
|
||||
0x001B => Code::KeyR,
|
||||
0x001C => Code::KeyT,
|
||||
0x001D => Code::KeyY,
|
||||
0x001E => Code::KeyU,
|
||||
0x001F => Code::KeyI,
|
||||
0x0020 => Code::KeyO,
|
||||
0x0021 => Code::KeyP,
|
||||
0x0022 => Code::BracketLeft,
|
||||
0x0023 => Code::BracketRight,
|
||||
0x0024 => Code::Enter,
|
||||
0x0025 => Code::ControlLeft,
|
||||
0x0026 => Code::KeyA,
|
||||
0x0027 => Code::KeyS,
|
||||
0x0028 => Code::KeyD,
|
||||
0x0029 => Code::KeyF,
|
||||
0x002A => Code::KeyG,
|
||||
0x002B => Code::KeyH,
|
||||
0x002C => Code::KeyJ,
|
||||
0x002D => Code::KeyK,
|
||||
0x002E => Code::KeyL,
|
||||
0x002F => Code::Semicolon,
|
||||
0x0030 => Code::Quote,
|
||||
0x0031 => Code::Backquote,
|
||||
0x0032 => Code::ShiftLeft,
|
||||
0x0033 => Code::Backslash,
|
||||
0x0034 => Code::KeyZ,
|
||||
0x0035 => Code::KeyX,
|
||||
0x0036 => Code::KeyC,
|
||||
0x0037 => Code::KeyV,
|
||||
0x0038 => Code::KeyB,
|
||||
0x0039 => Code::KeyN,
|
||||
0x003A => Code::KeyM,
|
||||
0x003B => Code::Comma,
|
||||
0x003C => Code::Period,
|
||||
0x003D => Code::Slash,
|
||||
0x003E => Code::ShiftRight,
|
||||
0x003F => Code::NumpadMultiply,
|
||||
0x0040 => Code::AltLeft,
|
||||
0x0041 => Code::Space,
|
||||
0x0042 => Code::CapsLock,
|
||||
0x0043 => Code::F1,
|
||||
0x0044 => Code::F2,
|
||||
0x0045 => Code::F3,
|
||||
0x0046 => Code::F4,
|
||||
0x0047 => Code::F5,
|
||||
0x0048 => Code::F6,
|
||||
0x0049 => Code::F7,
|
||||
0x004A => Code::F8,
|
||||
0x004B => Code::F9,
|
||||
0x004C => Code::F10,
|
||||
0x004D => Code::NumLock,
|
||||
0x004E => Code::ScrollLock,
|
||||
0x004F => Code::Numpad7,
|
||||
0x0050 => Code::Numpad8,
|
||||
0x0051 => Code::Numpad9,
|
||||
0x0052 => Code::NumpadSubtract,
|
||||
0x0053 => Code::Numpad4,
|
||||
0x0054 => Code::Numpad5,
|
||||
0x0055 => Code::Numpad6,
|
||||
0x0056 => Code::NumpadAdd,
|
||||
0x0057 => Code::Numpad1,
|
||||
0x0058 => Code::Numpad2,
|
||||
0x0059 => Code::Numpad3,
|
||||
0x005A => Code::Numpad0,
|
||||
0x005B => Code::NumpadDecimal,
|
||||
0x005E => Code::IntlBackslash,
|
||||
0x005F => Code::F11,
|
||||
0x0060 => Code::F12,
|
||||
0x0061 => Code::IntlRo,
|
||||
0x0064 => Code::Convert,
|
||||
0x0065 => Code::KanaMode,
|
||||
0x0066 => Code::NonConvert,
|
||||
0x0068 => Code::NumpadEnter,
|
||||
0x0069 => Code::ControlRight,
|
||||
0x006A => Code::NumpadDivide,
|
||||
0x006B => Code::PrintScreen,
|
||||
0x006C => Code::AltRight,
|
||||
0x006E => Code::Home,
|
||||
0x006F => Code::ArrowUp,
|
||||
0x0070 => Code::PageUp,
|
||||
0x0071 => Code::ArrowLeft,
|
||||
0x0072 => Code::ArrowRight,
|
||||
0x0073 => Code::End,
|
||||
0x0074 => Code::ArrowDown,
|
||||
0x0075 => Code::PageDown,
|
||||
0x0076 => Code::Insert,
|
||||
0x0077 => Code::Delete,
|
||||
0x0079 => Code::AudioVolumeMute,
|
||||
0x007A => Code::AudioVolumeDown,
|
||||
0x007B => Code::AudioVolumeUp,
|
||||
0x007D => Code::NumpadEqual,
|
||||
0x007F => Code::Pause,
|
||||
0x0081 => Code::NumpadComma,
|
||||
0x0082 => Code::Lang1,
|
||||
0x0083 => Code::Lang2,
|
||||
0x0084 => Code::IntlYen,
|
||||
0x0085 => Code::MetaLeft,
|
||||
0x0086 => Code::MetaRight,
|
||||
0x0087 => Code::ContextMenu,
|
||||
0x0088 => Code::BrowserStop,
|
||||
0x0089 => Code::Again,
|
||||
0x008A => Code::Props,
|
||||
0x008B => Code::Undo,
|
||||
0x008C => Code::Select,
|
||||
0x008D => Code::Copy,
|
||||
0x008E => Code::Open,
|
||||
0x008F => Code::Paste,
|
||||
0x0090 => Code::Find,
|
||||
0x0091 => Code::Cut,
|
||||
0x0092 => Code::Help,
|
||||
0x0094 => Code::LaunchApp2,
|
||||
0x0097 => Code::WakeUp,
|
||||
0x0098 => Code::LaunchApp1,
|
||||
// key to right of volume controls on T430s produces 0x9C
|
||||
// but no documentation of what it should map to :/
|
||||
0x00A3 => Code::LaunchMail,
|
||||
0x00A4 => Code::BrowserFavorites,
|
||||
0x00A6 => Code::BrowserBack,
|
||||
0x00A7 => Code::BrowserForward,
|
||||
0x00A9 => Code::Eject,
|
||||
0x00AB => Code::MediaTrackNext,
|
||||
0x00AC => Code::MediaPlayPause,
|
||||
0x00AD => Code::MediaTrackPrevious,
|
||||
0x00AE => Code::MediaStop,
|
||||
0x00B3 => Code::MediaSelect,
|
||||
0x00B4 => Code::BrowserHome,
|
||||
0x00B5 => Code::BrowserRefresh,
|
||||
0x00E1 => Code::BrowserSearch,
|
||||
_ => Code::Unidentified,
|
||||
}
|
||||
}
|
||||
|
||||
// Extracts the keyboard modifiers from, e.g., the `state` field of
|
||||
// `x11rb::protocol::xproto::ButtonPressEvent`
|
||||
pub(super) fn key_mods(mods: KeyButMask) -> Modifiers {
|
||||
let mut ret = Modifiers::default();
|
||||
let key_masks = [
|
||||
(KeyButMask::SHIFT, Modifiers::SHIFT),
|
||||
(KeyButMask::CONTROL, Modifiers::CONTROL),
|
||||
// X11's mod keys are configurable, but this seems
|
||||
// like a reasonable default for US keyboards, at least,
|
||||
// where the "windows" key seems to be MOD_MASK_4.
|
||||
(KeyButMask::MOD1, Modifiers::ALT),
|
||||
(KeyButMask::MOD2, Modifiers::NUM_LOCK),
|
||||
(KeyButMask::MOD4, Modifiers::META),
|
||||
(KeyButMask::LOCK, Modifiers::CAPS_LOCK),
|
||||
];
|
||||
for (mask, modifiers) in &key_masks {
|
||||
if mods.contains(*mask) {
|
||||
ret |= *modifiers;
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub(super) fn convert_key_press_event(key_press: &KeyPressEvent) -> KeyboardEvent {
|
||||
let hw_keycode = key_press.detail;
|
||||
let code = hardware_keycode_to_code(hw_keycode.into());
|
||||
let modifiers = key_mods(key_press.state);
|
||||
let key = code_to_key(code, modifiers);
|
||||
let location = code_to_location(code);
|
||||
let state = KeyState::Down;
|
||||
|
||||
KeyboardEvent { code, key, modifiers, location, state, repeat: false, is_composing: false }
|
||||
}
|
||||
|
||||
pub(super) fn convert_key_release_event(key_release: &KeyReleaseEvent) -> KeyboardEvent {
|
||||
let hw_keycode = key_release.detail;
|
||||
let code = hardware_keycode_to_code(hw_keycode.into());
|
||||
let modifiers = key_mods(key_release.state);
|
||||
let key = code_to_key(code, modifiers);
|
||||
let location = code_to_location(code);
|
||||
let state = KeyState::Up;
|
||||
|
||||
KeyboardEvent { code, key, modifiers, location, state, repeat: false, is_composing: false }
|
||||
}
|
||||
10
crates/baseview/src/x11/mod.rs
Normal file
10
crates/baseview/src/x11/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
mod xcb_connection;
|
||||
use xcb_connection::XcbConnection;
|
||||
|
||||
mod window;
|
||||
pub use window::*;
|
||||
|
||||
mod cursor;
|
||||
mod event_loop;
|
||||
mod keyboard;
|
||||
mod visual_info;
|
||||
94
crates/baseview/src/x11/visual_info.rs
Normal file
94
crates/baseview/src/x11/visual_info.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use crate::x11::xcb_connection::XcbConnection;
|
||||
use std::error::Error;
|
||||
use x11rb::connection::Connection;
|
||||
use x11rb::protocol::xproto::{
|
||||
Colormap, ColormapAlloc, ConnectionExt, Screen, VisualClass, Visualid,
|
||||
};
|
||||
use x11rb::COPY_FROM_PARENT;
|
||||
|
||||
pub(super) struct WindowVisualConfig {
|
||||
#[cfg(feature = "opengl")]
|
||||
pub fb_config: Option<crate::gl::x11::FbConfig>,
|
||||
|
||||
pub visual_depth: u8,
|
||||
pub visual_id: Visualid,
|
||||
pub color_map: Option<Colormap>,
|
||||
}
|
||||
|
||||
// TODO: make visual negotiation actually check all of a visual's parameters
|
||||
impl WindowVisualConfig {
|
||||
#[cfg(feature = "opengl")]
|
||||
pub fn find_best_visual_config_for_gl(
|
||||
connection: &XcbConnection, gl_config: Option<crate::gl::GlConfig>,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
let Some(gl_config) = gl_config else { return Self::find_best_visual_config(connection) };
|
||||
|
||||
// SAFETY: TODO
|
||||
let (fb_config, window_config) = unsafe {
|
||||
crate::gl::platform::GlContext::get_fb_config_and_visual(connection.dpy, gl_config)
|
||||
}
|
||||
.expect("Could not fetch framebuffer config");
|
||||
|
||||
Ok(Self {
|
||||
fb_config: Some(fb_config),
|
||||
visual_depth: window_config.depth,
|
||||
visual_id: window_config.visual,
|
||||
color_map: Some(create_color_map(connection, window_config.visual)?),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn find_best_visual_config(connection: &XcbConnection) -> Result<Self, Box<dyn Error>> {
|
||||
match find_visual_for_depth(connection.screen(), 32) {
|
||||
None => Ok(Self::copy_from_parent()),
|
||||
Some(visual_id) => Ok(Self {
|
||||
#[cfg(feature = "opengl")]
|
||||
fb_config: None,
|
||||
visual_id,
|
||||
visual_depth: 32,
|
||||
color_map: Some(create_color_map(connection, visual_id)?),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
const fn copy_from_parent() -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "opengl")]
|
||||
fb_config: None,
|
||||
visual_depth: COPY_FROM_PARENT as u8,
|
||||
visual_id: COPY_FROM_PARENT,
|
||||
color_map: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For this 32-bit depth to work, you also need to define a color map and set a border
|
||||
// pixel: https://cgit.freedesktop.org/xorg/xserver/tree/dix/window.c#n818
|
||||
fn create_color_map(
|
||||
connection: &XcbConnection, visual_id: Visualid,
|
||||
) -> Result<Colormap, Box<dyn Error>> {
|
||||
let colormap = connection.conn.generate_id()?;
|
||||
connection.conn.create_colormap(
|
||||
ColormapAlloc::NONE,
|
||||
colormap,
|
||||
connection.screen().root,
|
||||
visual_id,
|
||||
)?;
|
||||
|
||||
Ok(colormap)
|
||||
}
|
||||
|
||||
fn find_visual_for_depth(screen: &Screen, depth: u8) -> Option<Visualid> {
|
||||
for candidate_depth in &screen.allowed_depths {
|
||||
if candidate_depth.depth != depth {
|
||||
continue;
|
||||
}
|
||||
|
||||
for candidate_visual in &candidate_depth.visuals {
|
||||
if candidate_visual.class == VisualClass::TRUE_COLOR {
|
||||
return Some(candidate_visual.visual_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
376
crates/baseview/src/x11/window.rs
Normal file
376
crates/baseview/src/x11/window.rs
Normal file
@@ -0,0 +1,376 @@
|
||||
use std::cell::Cell;
|
||||
use std::error::Error;
|
||||
use std::ffi::c_void;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc;
|
||||
use std::sync::Arc;
|
||||
use std::thread::{self, JoinHandle};
|
||||
|
||||
use raw_window_handle::{
|
||||
HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, XlibDisplayHandle,
|
||||
XlibWindowHandle,
|
||||
};
|
||||
|
||||
use x11rb::connection::Connection;
|
||||
use x11rb::protocol::xproto::{
|
||||
AtomEnum, ChangeWindowAttributesAux, ConfigureWindowAux, ConnectionExt as _, CreateGCAux,
|
||||
CreateWindowAux, EventMask, PropMode, Visualid, Window as XWindow, WindowClass,
|
||||
};
|
||||
use x11rb::wrapper::ConnectionExt as _;
|
||||
|
||||
use super::XcbConnection;
|
||||
use crate::{
|
||||
Event, MouseCursor, Size, WindowEvent, WindowHandler, WindowInfo, WindowOpenOptions,
|
||||
WindowScalePolicy,
|
||||
};
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
use crate::gl::{platform, GlContext};
|
||||
use crate::x11::event_loop::EventLoop;
|
||||
use crate::x11::visual_info::WindowVisualConfig;
|
||||
|
||||
pub struct WindowHandle {
|
||||
raw_window_handle: Option<RawWindowHandle>,
|
||||
event_loop_handle: Option<JoinHandle<()>>,
|
||||
close_requested: Arc<AtomicBool>,
|
||||
is_open: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl WindowHandle {
|
||||
pub fn close(&mut self) {
|
||||
self.close_requested.store(true, Ordering::Relaxed);
|
||||
if let Some(event_loop) = self.event_loop_handle.take() {
|
||||
let _ = event_loop.join();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.is_open.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl HasRawWindowHandle for WindowHandle {
|
||||
fn raw_window_handle(&self) -> RawWindowHandle {
|
||||
if let Some(raw_window_handle) = self.raw_window_handle {
|
||||
if self.is_open.load(Ordering::Relaxed) {
|
||||
return raw_window_handle;
|
||||
}
|
||||
}
|
||||
|
||||
RawWindowHandle::Xlib(XlibWindowHandle::empty())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ParentHandle {
|
||||
close_requested: Arc<AtomicBool>,
|
||||
is_open: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl ParentHandle {
|
||||
pub fn new() -> (Self, WindowHandle) {
|
||||
let close_requested = Arc::new(AtomicBool::new(false));
|
||||
let is_open = Arc::new(AtomicBool::new(true));
|
||||
let handle = WindowHandle {
|
||||
raw_window_handle: None,
|
||||
event_loop_handle: None,
|
||||
close_requested: Arc::clone(&close_requested),
|
||||
is_open: Arc::clone(&is_open),
|
||||
};
|
||||
|
||||
(Self { close_requested, is_open }, handle)
|
||||
}
|
||||
|
||||
pub fn parent_did_drop(&self) -> bool {
|
||||
self.close_requested.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ParentHandle {
|
||||
fn drop(&mut self) {
|
||||
self.is_open.store(false, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct WindowInner {
|
||||
// GlContext should be dropped **before** XcbConnection is dropped
|
||||
#[cfg(feature = "opengl")]
|
||||
gl_context: Option<GlContext>,
|
||||
|
||||
pub(crate) xcb_connection: XcbConnection,
|
||||
window_id: XWindow,
|
||||
pub(crate) window_info: WindowInfo,
|
||||
visual_id: Visualid,
|
||||
mouse_cursor: Cell<MouseCursor>,
|
||||
|
||||
pub(crate) close_requested: Cell<bool>,
|
||||
}
|
||||
|
||||
pub struct Window<'a> {
|
||||
pub(crate) inner: &'a WindowInner,
|
||||
}
|
||||
|
||||
// Hack to allow sending a RawWindowHandle between threads. Do not make public
|
||||
struct SendableRwh(RawWindowHandle);
|
||||
|
||||
unsafe impl Send for SendableRwh {}
|
||||
|
||||
type WindowOpenResult = Result<SendableRwh, ()>;
|
||||
|
||||
impl<'a> Window<'a> {
|
||||
pub fn open_parented<P, H, B>(parent: &P, options: WindowOpenOptions, build: B) -> WindowHandle
|
||||
where
|
||||
P: HasRawWindowHandle,
|
||||
H: WindowHandler + 'static,
|
||||
B: FnOnce(&mut crate::Window) -> H,
|
||||
B: Send + 'static,
|
||||
{
|
||||
// Convert parent into something that X understands
|
||||
let parent_id = match parent.raw_window_handle() {
|
||||
RawWindowHandle::Xlib(h) => h.window as u32,
|
||||
RawWindowHandle::Xcb(h) => h.window,
|
||||
h => panic!("unsupported parent handle type {:?}", h),
|
||||
};
|
||||
|
||||
let (tx, rx) = mpsc::sync_channel::<WindowOpenResult>(1);
|
||||
let (parent_handle, mut window_handle) = ParentHandle::new();
|
||||
let join_handle = thread::spawn(move || {
|
||||
Self::window_thread(Some(parent_id), options, build, tx.clone(), Some(parent_handle))
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
let raw_window_handle = rx.recv().unwrap().unwrap();
|
||||
window_handle.raw_window_handle = Some(raw_window_handle.0);
|
||||
window_handle.event_loop_handle = Some(join_handle);
|
||||
window_handle
|
||||
}
|
||||
|
||||
pub fn open_blocking<H, B>(options: WindowOpenOptions, build: B)
|
||||
where
|
||||
H: WindowHandler + 'static,
|
||||
B: FnOnce(&mut crate::Window) -> H,
|
||||
B: Send + 'static,
|
||||
{
|
||||
let (tx, rx) = mpsc::sync_channel::<WindowOpenResult>(1);
|
||||
|
||||
let thread = thread::spawn(move || {
|
||||
Self::window_thread(None, options, build, tx, None).unwrap();
|
||||
});
|
||||
|
||||
let _ = rx.recv().unwrap().unwrap();
|
||||
|
||||
thread.join().unwrap_or_else(|err| {
|
||||
eprintln!("Window thread panicked: {:#?}", err);
|
||||
});
|
||||
}
|
||||
|
||||
fn window_thread<H, B>(
|
||||
parent: Option<u32>, options: WindowOpenOptions, build: B,
|
||||
tx: mpsc::SyncSender<WindowOpenResult>, parent_handle: Option<ParentHandle>,
|
||||
) -> Result<(), Box<dyn Error>>
|
||||
where
|
||||
H: WindowHandler + 'static,
|
||||
B: FnOnce(&mut crate::Window) -> H,
|
||||
B: Send + 'static,
|
||||
{
|
||||
// Connect to the X server
|
||||
// FIXME: baseview error type instead of unwrap()
|
||||
let xcb_connection = XcbConnection::new()?;
|
||||
|
||||
// Get screen information
|
||||
let screen = xcb_connection.screen();
|
||||
let parent_id = parent.unwrap_or(screen.root);
|
||||
|
||||
let gc_id = xcb_connection.conn.generate_id()?;
|
||||
xcb_connection.conn.create_gc(
|
||||
gc_id,
|
||||
parent_id,
|
||||
&CreateGCAux::new().foreground(screen.black_pixel).graphics_exposures(0),
|
||||
)?;
|
||||
|
||||
let scaling = match options.scale {
|
||||
WindowScalePolicy::SystemScaleFactor => xcb_connection.get_scaling().unwrap_or(1.0),
|
||||
WindowScalePolicy::ScaleFactor(scale) => scale,
|
||||
};
|
||||
|
||||
let window_info = WindowInfo::from_logical_size(options.size, scaling);
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
let visual_info =
|
||||
WindowVisualConfig::find_best_visual_config_for_gl(&xcb_connection, options.gl_config)?;
|
||||
|
||||
#[cfg(not(feature = "opengl"))]
|
||||
let visual_info = WindowVisualConfig::find_best_visual_config(&xcb_connection)?;
|
||||
|
||||
let window_id = xcb_connection.conn.generate_id()?;
|
||||
xcb_connection.conn.create_window(
|
||||
visual_info.visual_depth,
|
||||
window_id,
|
||||
parent_id,
|
||||
0, // x coordinate of the new window
|
||||
0, // y coordinate of the new window
|
||||
window_info.physical_size().width as u16, // window width
|
||||
window_info.physical_size().height as u16, // window height
|
||||
0, // window border
|
||||
WindowClass::INPUT_OUTPUT,
|
||||
visual_info.visual_id,
|
||||
&CreateWindowAux::new()
|
||||
.event_mask(
|
||||
EventMask::EXPOSURE
|
||||
| EventMask::POINTER_MOTION
|
||||
| EventMask::BUTTON_PRESS
|
||||
| EventMask::BUTTON_RELEASE
|
||||
| EventMask::KEY_PRESS
|
||||
| EventMask::KEY_RELEASE
|
||||
| EventMask::STRUCTURE_NOTIFY
|
||||
| EventMask::ENTER_WINDOW
|
||||
| EventMask::LEAVE_WINDOW,
|
||||
)
|
||||
// As mentioned above, these two values are needed to be able to create a window
|
||||
// with a depth of 32-bits when the parent window has a different depth
|
||||
.colormap(visual_info.color_map)
|
||||
.border_pixel(0),
|
||||
)?;
|
||||
xcb_connection.conn.map_window(window_id)?;
|
||||
|
||||
// Change window title
|
||||
let title = options.title;
|
||||
xcb_connection.conn.change_property8(
|
||||
PropMode::REPLACE,
|
||||
window_id,
|
||||
AtomEnum::WM_NAME,
|
||||
AtomEnum::STRING,
|
||||
title.as_bytes(),
|
||||
)?;
|
||||
|
||||
xcb_connection.conn.change_property32(
|
||||
PropMode::REPLACE,
|
||||
window_id,
|
||||
xcb_connection.atoms.WM_PROTOCOLS,
|
||||
AtomEnum::ATOM,
|
||||
&[xcb_connection.atoms.WM_DELETE_WINDOW],
|
||||
)?;
|
||||
|
||||
xcb_connection.conn.flush()?;
|
||||
|
||||
// TODO: These APIs could use a couple tweaks now that everything is internal and there is
|
||||
// no error handling anymore at this point. Everything is more or less unchanged
|
||||
// compared to when raw-gl-context was a separate crate.
|
||||
#[cfg(feature = "opengl")]
|
||||
let gl_context = visual_info.fb_config.map(|fb_config| {
|
||||
use std::ffi::c_ulong;
|
||||
|
||||
let window = window_id as c_ulong;
|
||||
let display = xcb_connection.dpy;
|
||||
|
||||
// Because of the visual negotation we had to take some extra steps to create this context
|
||||
let context = unsafe { platform::GlContext::create(window, display, fb_config) }
|
||||
.expect("Could not create OpenGL context");
|
||||
GlContext::new(context)
|
||||
});
|
||||
|
||||
let mut inner = WindowInner {
|
||||
xcb_connection,
|
||||
window_id,
|
||||
window_info,
|
||||
visual_id: visual_info.visual_id,
|
||||
mouse_cursor: Cell::new(MouseCursor::default()),
|
||||
|
||||
close_requested: Cell::new(false),
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
gl_context,
|
||||
};
|
||||
|
||||
let mut window = crate::Window::new(Window { inner: &mut inner });
|
||||
|
||||
let mut handler = build(&mut window);
|
||||
|
||||
// Send an initial window resized event so the user is alerted of
|
||||
// the correct dpi scaling.
|
||||
handler.on_event(&mut window, Event::Window(WindowEvent::Resized(window_info)));
|
||||
|
||||
let _ = tx.send(Ok(SendableRwh(window.raw_window_handle())));
|
||||
|
||||
EventLoop::new(inner, handler, parent_handle).run()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_mouse_cursor(&self, mouse_cursor: MouseCursor) {
|
||||
if self.inner.mouse_cursor.get() == mouse_cursor {
|
||||
return;
|
||||
}
|
||||
|
||||
let xid = self.inner.xcb_connection.get_cursor(mouse_cursor).unwrap();
|
||||
|
||||
if xid != 0 {
|
||||
let _ = self.inner.xcb_connection.conn.change_window_attributes(
|
||||
self.inner.window_id,
|
||||
&ChangeWindowAttributesAux::new().cursor(xid),
|
||||
);
|
||||
let _ = self.inner.xcb_connection.conn.flush();
|
||||
}
|
||||
|
||||
self.inner.mouse_cursor.set(mouse_cursor);
|
||||
}
|
||||
|
||||
pub fn close(&mut self) {
|
||||
self.inner.close_requested.set(true);
|
||||
}
|
||||
|
||||
pub fn has_focus(&mut self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn focus(&mut self) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, size: Size) {
|
||||
let scaling = self.inner.window_info.scale();
|
||||
let new_window_info = WindowInfo::from_logical_size(size, scaling);
|
||||
|
||||
let _ = self.inner.xcb_connection.conn.configure_window(
|
||||
self.inner.window_id,
|
||||
&ConfigureWindowAux::new()
|
||||
.width(new_window_info.physical_size().width)
|
||||
.height(new_window_info.physical_size().height),
|
||||
);
|
||||
let _ = self.inner.xcb_connection.conn.flush();
|
||||
|
||||
// This will trigger a `ConfigureNotify` event which will in turn change `self.window_info`
|
||||
// and notify the window handler about it
|
||||
}
|
||||
|
||||
#[cfg(feature = "opengl")]
|
||||
pub fn gl_context(&self) -> Option<&crate::gl::GlContext> {
|
||||
self.inner.gl_context.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<'a> HasRawWindowHandle for Window<'a> {
|
||||
fn raw_window_handle(&self) -> RawWindowHandle {
|
||||
let mut handle = XlibWindowHandle::empty();
|
||||
|
||||
handle.window = self.inner.window_id.into();
|
||||
handle.visual_id = self.inner.visual_id.into();
|
||||
|
||||
RawWindowHandle::Xlib(handle)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<'a> HasRawDisplayHandle for Window<'a> {
|
||||
fn raw_display_handle(&self) -> RawDisplayHandle {
|
||||
let display = self.inner.xcb_connection.dpy;
|
||||
let mut handle = XlibDisplayHandle::empty();
|
||||
|
||||
handle.display = display as *mut c_void;
|
||||
handle.screen = unsafe { x11::xlib::XDefaultScreen(display) };
|
||||
|
||||
RawDisplayHandle::Xlib(handle)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_to_clipboard(_data: &str) {
|
||||
todo!()
|
||||
}
|
||||
132
crates/baseview/src/x11/xcb_connection.rs
Normal file
132
crates/baseview/src/x11/xcb_connection.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::hash_map::{Entry, HashMap};
|
||||
use std::error::Error;
|
||||
|
||||
use x11::{xlib, xlib::Display, xlib_xcb};
|
||||
|
||||
use x11rb::connection::Connection;
|
||||
use x11rb::cursor::Handle as CursorHandle;
|
||||
use x11rb::protocol::xproto::{Cursor, Screen};
|
||||
use x11rb::resource_manager;
|
||||
use x11rb::xcb_ffi::XCBConnection;
|
||||
|
||||
use crate::MouseCursor;
|
||||
|
||||
use super::cursor;
|
||||
|
||||
x11rb::atom_manager! {
|
||||
pub Atoms: AtomsCookie {
|
||||
WM_PROTOCOLS,
|
||||
WM_DELETE_WINDOW,
|
||||
}
|
||||
}
|
||||
|
||||
/// A very light abstraction around the XCB connection.
|
||||
///
|
||||
/// Keeps track of the xcb connection itself and the xlib display ID that was used to connect.
|
||||
pub struct XcbConnection {
|
||||
pub(crate) dpy: *mut Display,
|
||||
pub(crate) conn: XCBConnection,
|
||||
pub(crate) screen: usize,
|
||||
pub(crate) atoms: Atoms,
|
||||
pub(crate) resources: resource_manager::Database,
|
||||
pub(crate) cursor_handle: CursorHandle,
|
||||
pub(super) cursor_cache: RefCell<HashMap<MouseCursor, u32>>,
|
||||
}
|
||||
|
||||
impl XcbConnection {
|
||||
pub fn new() -> Result<Self, Box<dyn Error>> {
|
||||
let dpy = unsafe { xlib::XOpenDisplay(std::ptr::null()) };
|
||||
assert!(!dpy.is_null());
|
||||
let xcb_connection = unsafe { xlib_xcb::XGetXCBConnection(dpy) };
|
||||
assert!(!xcb_connection.is_null());
|
||||
let screen = unsafe { xlib::XDefaultScreen(dpy) } as usize;
|
||||
let conn = unsafe { XCBConnection::from_raw_xcb_connection(xcb_connection, false)? };
|
||||
unsafe {
|
||||
xlib_xcb::XSetEventQueueOwner(dpy, xlib_xcb::XEventQueueOwner::XCBOwnsEventQueue)
|
||||
};
|
||||
|
||||
let atoms = Atoms::new(&conn)?.reply()?;
|
||||
let resources = resource_manager::new_from_default(&conn)?;
|
||||
let cursor_handle = CursorHandle::new(&conn, screen, &resources)?.reply()?;
|
||||
|
||||
Ok(Self {
|
||||
dpy,
|
||||
conn,
|
||||
screen,
|
||||
atoms,
|
||||
resources,
|
||||
cursor_handle,
|
||||
cursor_cache: RefCell::new(HashMap::new()),
|
||||
})
|
||||
}
|
||||
|
||||
// Try to get the scaling with this function first.
|
||||
// If this gives you `None`, fall back to `get_scaling_screen_dimensions`.
|
||||
// If neither work, I guess just assume 96.0 and don't do any scaling.
|
||||
fn get_scaling_xft(&self) -> Result<Option<f64>, Box<dyn Error>> {
|
||||
if let Some(dpi) = self.resources.get_value::<u32>("Xft.dpi", "")? {
|
||||
Ok(Some(dpi as f64 / 96.0))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get the scaling with `get_scaling_xft` first.
|
||||
// Only use this function as a fallback.
|
||||
// If neither work, I guess just assume 96.0 and don't do any scaling.
|
||||
fn get_scaling_screen_dimensions(&self) -> f64 {
|
||||
// Figure out screen information
|
||||
let screen = self.screen();
|
||||
|
||||
// Get the DPI from the screen struct
|
||||
//
|
||||
// there are 2.54 centimeters to an inch; so there are 25.4 millimeters.
|
||||
// dpi = N pixels / (M millimeters / (25.4 millimeters / 1 inch))
|
||||
// = N pixels / (M inch / 25.4)
|
||||
// = N * 25.4 pixels / M inch
|
||||
let width_px = screen.width_in_pixels as f64;
|
||||
let width_mm = screen.width_in_millimeters as f64;
|
||||
let height_px = screen.height_in_pixels as f64;
|
||||
let height_mm = screen.height_in_millimeters as f64;
|
||||
let _xres = width_px * 25.4 / width_mm;
|
||||
let yres = height_px * 25.4 / height_mm;
|
||||
|
||||
// TODO: choose between `xres` and `yres`? (probably both are the same?)
|
||||
yres / 96.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_scaling(&self) -> Result<f64, Box<dyn Error>> {
|
||||
Ok(self.get_scaling_xft()?.unwrap_or(self.get_scaling_screen_dimensions()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_cursor(&self, cursor: MouseCursor) -> Result<Cursor, Box<dyn Error>> {
|
||||
// PANIC: this function is the only point where we access the cache, and we never call
|
||||
// external functions that may make a reentrant call to this function
|
||||
let mut cursor_cache = self.cursor_cache.borrow_mut();
|
||||
|
||||
match cursor_cache.entry(cursor) {
|
||||
Entry::Occupied(entry) => Ok(*entry.get()),
|
||||
Entry::Vacant(entry) => {
|
||||
let cursor =
|
||||
cursor::get_xcursor(&self.conn, self.screen, &self.cursor_handle, cursor)?;
|
||||
entry.insert(cursor);
|
||||
Ok(cursor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn screen(&self) -> &Screen {
|
||||
&self.conn.setup().roots[self.screen]
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for XcbConnection {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
xlib::XCloseDisplay(self.dpy);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
crates/clap/Cargo.toml
Normal file
30
crates/clap/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "cagire-clap"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
description = "Cagire as a CLAP audio plugin"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
||||
[dependencies]
|
||||
cagire = { path = "../.." }
|
||||
cagire-forth = { path = "../forth" }
|
||||
cagire-project = { path = "../project" }
|
||||
cagire-ratatui = { path = "../ratatui" }
|
||||
doux = { git = "https://github.com/Bubobubobubobubo/doux", features = ["native"] }
|
||||
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] }
|
||||
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" }
|
||||
egui_ratatui = "2.1"
|
||||
soft_ratatui = { version = "0.1.3", features = ["unicodefonts"] }
|
||||
ratatui = "0.30"
|
||||
crossterm = "0.29"
|
||||
crossbeam-channel = "0.5"
|
||||
arc-swap = "1"
|
||||
parking_lot = "0.12"
|
||||
rand = "0.8"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
ringbuf = "0.4"
|
||||
274
crates/clap/src/editor.rs
Normal file
274
crates/clap/src/editor.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use crossbeam_channel::Sender;
|
||||
use egui_ratatui::RataguiBackend;
|
||||
use nih_plug::prelude::*;
|
||||
use nih_plug_egui::egui;
|
||||
use nih_plug_egui::{create_egui_editor, EguiState};
|
||||
use ratatui::Terminal;
|
||||
use soft_ratatui::embedded_graphics_unicodefonts::{
|
||||
mono_10x20_atlas, mono_6x13_atlas, mono_6x13_bold_atlas, mono_6x13_italic_atlas,
|
||||
mono_7x13_atlas, mono_7x13_bold_atlas, mono_7x13_italic_atlas, mono_8x13_atlas,
|
||||
mono_8x13_bold_atlas, mono_8x13_italic_atlas, mono_9x15_atlas, mono_9x15_bold_atlas,
|
||||
mono_9x18_atlas, mono_9x18_bold_atlas,
|
||||
};
|
||||
use soft_ratatui::{EmbeddedGraphics, SoftBackend};
|
||||
|
||||
use cagire::app::App;
|
||||
use cagire::engine::{AudioCommand, LinkState, SequencerSnapshot};
|
||||
use cagire::input::{handle_key, handle_mouse, InputContext};
|
||||
use cagire::model::{Dictionary, Rng, Variables};
|
||||
use cagire::theme;
|
||||
use cagire::views;
|
||||
|
||||
use crate::input_egui::{convert_egui_events, convert_egui_mouse};
|
||||
use crate::params::CagireParams;
|
||||
use crate::PluginBridge;
|
||||
|
||||
type TerminalType = Terminal<RataguiBackend<EmbeddedGraphics>>;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum FontChoice {
|
||||
Size6x13,
|
||||
Size7x13,
|
||||
Size8x13,
|
||||
Size9x15,
|
||||
Size9x18,
|
||||
Size10x20,
|
||||
}
|
||||
|
||||
impl FontChoice {
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Size6x13 => "6x13 (Compact)",
|
||||
Self::Size7x13 => "7x13",
|
||||
Self::Size8x13 => "8x13 (Default)",
|
||||
Self::Size9x15 => "9x15",
|
||||
Self::Size9x18 => "9x18",
|
||||
Self::Size10x20 => "10x20 (Large)",
|
||||
}
|
||||
}
|
||||
|
||||
const ALL: [Self; 6] = [
|
||||
Self::Size6x13,
|
||||
Self::Size7x13,
|
||||
Self::Size8x13,
|
||||
Self::Size9x15,
|
||||
Self::Size9x18,
|
||||
Self::Size10x20,
|
||||
];
|
||||
}
|
||||
|
||||
fn create_terminal(font: FontChoice) -> TerminalType {
|
||||
let (regular, bold, italic) = match font {
|
||||
FontChoice::Size6x13 => (
|
||||
mono_6x13_atlas(),
|
||||
Some(mono_6x13_bold_atlas()),
|
||||
Some(mono_6x13_italic_atlas()),
|
||||
),
|
||||
FontChoice::Size7x13 => (
|
||||
mono_7x13_atlas(),
|
||||
Some(mono_7x13_bold_atlas()),
|
||||
Some(mono_7x13_italic_atlas()),
|
||||
),
|
||||
FontChoice::Size8x13 => (
|
||||
mono_8x13_atlas(),
|
||||
Some(mono_8x13_bold_atlas()),
|
||||
Some(mono_8x13_italic_atlas()),
|
||||
),
|
||||
FontChoice::Size9x15 => (mono_9x15_atlas(), Some(mono_9x15_bold_atlas()), None),
|
||||
FontChoice::Size9x18 => (mono_9x18_atlas(), Some(mono_9x18_bold_atlas()), None),
|
||||
FontChoice::Size10x20 => (mono_10x20_atlas(), None, None),
|
||||
};
|
||||
|
||||
let soft = SoftBackend::<EmbeddedGraphics>::new(80, 24, regular, bold, italic);
|
||||
Terminal::new(RataguiBackend::new("cagire", soft)).expect("terminal")
|
||||
}
|
||||
|
||||
struct EditorState {
|
||||
app: App,
|
||||
terminal: TerminalType,
|
||||
link: Arc<LinkState>,
|
||||
snapshot: SequencerSnapshot,
|
||||
playing: Arc<AtomicBool>,
|
||||
nudge_us: Arc<AtomicI64>,
|
||||
audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
|
||||
bridge: Arc<PluginBridge>,
|
||||
params: Arc<CagireParams>,
|
||||
last_frame: Instant,
|
||||
current_font: FontChoice,
|
||||
zoom_factor: f32,
|
||||
}
|
||||
|
||||
// SAFETY: EditorState is only accessed from the GUI thread via RwLock.
|
||||
// The non-Send types (RefCell in App's tachyonfx effects) are never shared
|
||||
// across threads — baseview's event loop guarantees single-threaded access.
|
||||
unsafe impl Send for EditorState {}
|
||||
unsafe impl Sync for EditorState {}
|
||||
|
||||
pub fn create_editor(
|
||||
params: Arc<CagireParams>,
|
||||
egui_state: Arc<EguiState>,
|
||||
bridge: Arc<PluginBridge>,
|
||||
variables: Variables,
|
||||
dict: Dictionary,
|
||||
rng: Rng,
|
||||
) -> Option<Box<dyn Editor>> {
|
||||
create_egui_editor(
|
||||
egui_state,
|
||||
None::<EditorState>,
|
||||
|ctx, _state| {
|
||||
ctx.set_visuals(egui::Visuals {
|
||||
panel_fill: egui::Color32::BLACK,
|
||||
..egui::Visuals::dark()
|
||||
});
|
||||
},
|
||||
move |ctx, _setter, state| {
|
||||
let editor = state.get_or_insert_with(|| {
|
||||
let palette =
|
||||
cagire::state::ColorScheme::default().to_palette();
|
||||
theme::set(cagire_ratatui::theme::build::build(&palette));
|
||||
|
||||
let mut app = App::with_shared(
|
||||
Arc::clone(&variables),
|
||||
Arc::clone(&dict),
|
||||
Arc::clone(&rng),
|
||||
);
|
||||
app.plugin_mode = true;
|
||||
app.audio.section = cagire::state::EngineSection::Settings;
|
||||
app.audio.setting_kind = cagire::state::SettingKind::Polyphony;
|
||||
|
||||
// Load persisted project
|
||||
app.project_state.project = params.project.lock().clone();
|
||||
app.mark_all_patterns_dirty();
|
||||
|
||||
EditorState {
|
||||
app,
|
||||
terminal: create_terminal(FontChoice::Size8x13),
|
||||
link: Arc::new(LinkState::new(
|
||||
params.tempo.value() as f64,
|
||||
4.0,
|
||||
)),
|
||||
snapshot: SequencerSnapshot::empty(),
|
||||
playing: Arc::new(AtomicBool::new(false)),
|
||||
nudge_us: Arc::new(AtomicI64::new(0)),
|
||||
audio_tx: Arc::new(ArcSwap::from_pointee(bridge.audio_cmd_tx.clone())),
|
||||
bridge: Arc::clone(&bridge),
|
||||
params: Arc::clone(¶ms),
|
||||
last_frame: Instant::now(),
|
||||
current_font: FontChoice::Size8x13,
|
||||
zoom_factor: 1.0,
|
||||
}
|
||||
});
|
||||
|
||||
// Flush pattern data and queued changes to the audio thread
|
||||
let had_dirty = editor.app.flush_dirty_patterns(&editor.bridge.cmd_tx);
|
||||
editor.app.flush_queued_changes(&editor.bridge.cmd_tx);
|
||||
|
||||
// Sync project changes back to persisted params
|
||||
if had_dirty {
|
||||
*editor.params.project.lock() = editor.app.project_state.project.clone();
|
||||
}
|
||||
|
||||
// Sync sample registry from the audio engine (set once after initialize)
|
||||
if editor.app.audio.sample_registry.is_none() {
|
||||
if let Some(reg) = editor.bridge.sample_registry.load().as_ref() {
|
||||
editor.app.audio.sample_registry = Some(Arc::clone(reg));
|
||||
}
|
||||
}
|
||||
|
||||
// Read live snapshot from the audio thread
|
||||
let shared = editor.bridge.shared_state.load();
|
||||
editor.snapshot = SequencerSnapshot::from(shared.as_ref());
|
||||
|
||||
// Feed scope and spectrum data into app metrics
|
||||
editor.app.metrics.scope = editor.bridge.scope_buffer.read();
|
||||
(editor.app.metrics.peak_left, editor.app.metrics.peak_right) =
|
||||
editor.bridge.scope_buffer.peaks();
|
||||
editor.app.metrics.spectrum = editor.bridge.spectrum_buffer.read();
|
||||
|
||||
// Handle input
|
||||
let term = editor.terminal.get_frame().area();
|
||||
let widget_rect = ctx.content_rect();
|
||||
|
||||
for mouse in convert_egui_mouse(ctx, widget_rect, term) {
|
||||
let mut input_ctx = InputContext {
|
||||
app: &mut editor.app,
|
||||
link: &editor.link,
|
||||
snapshot: &editor.snapshot,
|
||||
playing: &editor.playing,
|
||||
audio_tx: &editor.audio_tx,
|
||||
seq_cmd_tx: &editor.bridge.cmd_tx,
|
||||
nudge_us: &editor.nudge_us,
|
||||
};
|
||||
handle_mouse(&mut input_ctx, mouse, term);
|
||||
}
|
||||
|
||||
for key in convert_egui_events(ctx) {
|
||||
let mut input_ctx = InputContext {
|
||||
app: &mut editor.app,
|
||||
link: &editor.link,
|
||||
snapshot: &editor.snapshot,
|
||||
playing: &editor.playing,
|
||||
audio_tx: &editor.audio_tx,
|
||||
seq_cmd_tx: &editor.bridge.cmd_tx,
|
||||
nudge_us: &editor.nudge_us,
|
||||
};
|
||||
let _ = handle_key(&mut input_ctx, key);
|
||||
}
|
||||
|
||||
cagire::state::effects::tick_effects(&mut editor.app.ui, editor.app.page);
|
||||
|
||||
let elapsed = editor.last_frame.elapsed();
|
||||
editor.last_frame = Instant::now();
|
||||
|
||||
let link = &editor.link;
|
||||
let app = &editor.app;
|
||||
let snapshot = &editor.snapshot;
|
||||
editor
|
||||
.terminal
|
||||
.draw(|frame| views::render(frame, app, link, snapshot, elapsed))
|
||||
.expect("draw");
|
||||
|
||||
let current_font = editor.current_font;
|
||||
let current_zoom = editor.zoom_factor;
|
||||
|
||||
egui::CentralPanel::default()
|
||||
.frame(egui::Frame::NONE.fill(egui::Color32::BLACK))
|
||||
.show(ctx, |ui| {
|
||||
ui.add(editor.terminal.backend_mut());
|
||||
|
||||
let response = ui.interact(
|
||||
ui.max_rect(),
|
||||
egui::Id::new("terminal_context"),
|
||||
egui::Sense::click(),
|
||||
);
|
||||
response.context_menu(|ui| {
|
||||
ui.menu_button("Font", |ui| {
|
||||
for choice in FontChoice::ALL {
|
||||
if ui.selectable_label(current_font == choice, choice.label()).clicked() {
|
||||
editor.terminal = create_terminal(choice);
|
||||
editor.current_font = choice;
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.menu_button("Zoom", |ui| {
|
||||
for &level in &[0.5_f32, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0] {
|
||||
let selected = (current_zoom - level).abs() < 0.01;
|
||||
let label = format!("{:.0}%", level * 100.0);
|
||||
if ui.selectable_label(selected, label).clicked() {
|
||||
editor.zoom_factor = level;
|
||||
ctx.set_zoom_factor(level);
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
258
crates/clap/src/input_egui.rs
Normal file
258
crates/clap/src/input_egui.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
|
||||
use nih_plug_egui::egui;
|
||||
use ratatui::layout::Rect;
|
||||
|
||||
pub fn convert_egui_mouse(
|
||||
ctx: &egui::Context,
|
||||
widget_rect: egui::Rect,
|
||||
term: Rect,
|
||||
) -> Vec<MouseEvent> {
|
||||
let mut events = Vec::new();
|
||||
if widget_rect.width() < 1.0
|
||||
|| widget_rect.height() < 1.0
|
||||
|| term.width == 0
|
||||
|| term.height == 0
|
||||
{
|
||||
return events;
|
||||
}
|
||||
|
||||
ctx.input(|i| {
|
||||
let Some(pos) = i.pointer.latest_pos() else {
|
||||
return;
|
||||
};
|
||||
if !widget_rect.contains(pos) {
|
||||
return;
|
||||
}
|
||||
|
||||
let col =
|
||||
((pos.x - widget_rect.left()) / widget_rect.width() * term.width as f32) as u16;
|
||||
let row =
|
||||
((pos.y - widget_rect.top()) / widget_rect.height() * term.height as f32) as u16;
|
||||
let col = col.min(term.width.saturating_sub(1));
|
||||
let row = row.min(term.height.saturating_sub(1));
|
||||
|
||||
if i.pointer.button_clicked(egui::PointerButton::Primary) {
|
||||
events.push(MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column: col,
|
||||
row,
|
||||
modifiers: KeyModifiers::empty(),
|
||||
});
|
||||
}
|
||||
|
||||
let scroll = i.raw_scroll_delta.y;
|
||||
if scroll > 1.0 {
|
||||
events.push(MouseEvent {
|
||||
kind: MouseEventKind::ScrollUp,
|
||||
column: col,
|
||||
row,
|
||||
modifiers: KeyModifiers::empty(),
|
||||
});
|
||||
} else if scroll < -1.0 {
|
||||
events.push(MouseEvent {
|
||||
kind: MouseEventKind::ScrollDown,
|
||||
column: col,
|
||||
row,
|
||||
modifiers: KeyModifiers::empty(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
events
|
||||
}
|
||||
|
||||
pub fn convert_egui_events(ctx: &egui::Context) -> Vec<KeyEvent> {
|
||||
let mut events = Vec::new();
|
||||
|
||||
for event in &ctx.input(|i| i.events.clone()) {
|
||||
if let Some(key_event) = convert_event(event) {
|
||||
events.push(key_event);
|
||||
}
|
||||
}
|
||||
|
||||
events
|
||||
}
|
||||
|
||||
fn convert_event(event: &egui::Event) -> Option<KeyEvent> {
|
||||
match event {
|
||||
egui::Event::Key {
|
||||
key,
|
||||
pressed,
|
||||
modifiers,
|
||||
..
|
||||
} => {
|
||||
if !*pressed {
|
||||
return None;
|
||||
}
|
||||
let mods = convert_modifiers(*modifiers);
|
||||
if is_character_key(*key)
|
||||
&& !mods.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let code = convert_key(*key)?;
|
||||
Some(KeyEvent::new(code, mods))
|
||||
}
|
||||
egui::Event::Text(text) => {
|
||||
if text.len() == 1 {
|
||||
let c = text.chars().next()?;
|
||||
if !c.is_control() {
|
||||
return Some(KeyEvent::new(KeyCode::Char(c), KeyModifiers::empty()));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
egui::Event::Copy => Some(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)),
|
||||
egui::Event::Cut => Some(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL)),
|
||||
egui::Event::Paste(_) => Some(KeyEvent::new(KeyCode::Char('v'), KeyModifiers::CONTROL)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_key(key: egui::Key) -> Option<KeyCode> {
|
||||
Some(match key {
|
||||
egui::Key::ArrowDown => KeyCode::Down,
|
||||
egui::Key::ArrowLeft => KeyCode::Left,
|
||||
egui::Key::ArrowRight => KeyCode::Right,
|
||||
egui::Key::ArrowUp => KeyCode::Up,
|
||||
egui::Key::Escape => KeyCode::Esc,
|
||||
egui::Key::Tab => KeyCode::Tab,
|
||||
egui::Key::Backspace => KeyCode::Backspace,
|
||||
egui::Key::Enter => KeyCode::Enter,
|
||||
egui::Key::Space => KeyCode::Char(' '),
|
||||
egui::Key::Insert => KeyCode::Insert,
|
||||
egui::Key::Delete => KeyCode::Delete,
|
||||
egui::Key::Home => KeyCode::Home,
|
||||
egui::Key::End => KeyCode::End,
|
||||
egui::Key::PageUp => KeyCode::PageUp,
|
||||
egui::Key::PageDown => KeyCode::PageDown,
|
||||
egui::Key::F1 => KeyCode::F(1),
|
||||
egui::Key::F2 => KeyCode::F(2),
|
||||
egui::Key::F3 => KeyCode::F(3),
|
||||
egui::Key::F4 => KeyCode::F(4),
|
||||
egui::Key::F5 => KeyCode::F(5),
|
||||
egui::Key::F6 => KeyCode::F(6),
|
||||
egui::Key::F7 => KeyCode::F(7),
|
||||
egui::Key::F8 => KeyCode::F(8),
|
||||
egui::Key::F9 => KeyCode::F(9),
|
||||
egui::Key::F10 => KeyCode::F(10),
|
||||
egui::Key::F11 => KeyCode::F(11),
|
||||
egui::Key::F12 => KeyCode::F(12),
|
||||
egui::Key::A => KeyCode::Char('a'),
|
||||
egui::Key::B => KeyCode::Char('b'),
|
||||
egui::Key::C => KeyCode::Char('c'),
|
||||
egui::Key::D => KeyCode::Char('d'),
|
||||
egui::Key::E => KeyCode::Char('e'),
|
||||
egui::Key::F => KeyCode::Char('f'),
|
||||
egui::Key::G => KeyCode::Char('g'),
|
||||
egui::Key::H => KeyCode::Char('h'),
|
||||
egui::Key::I => KeyCode::Char('i'),
|
||||
egui::Key::J => KeyCode::Char('j'),
|
||||
egui::Key::K => KeyCode::Char('k'),
|
||||
egui::Key::L => KeyCode::Char('l'),
|
||||
egui::Key::M => KeyCode::Char('m'),
|
||||
egui::Key::N => KeyCode::Char('n'),
|
||||
egui::Key::O => KeyCode::Char('o'),
|
||||
egui::Key::P => KeyCode::Char('p'),
|
||||
egui::Key::Q => KeyCode::Char('q'),
|
||||
egui::Key::R => KeyCode::Char('r'),
|
||||
egui::Key::S => KeyCode::Char('s'),
|
||||
egui::Key::T => KeyCode::Char('t'),
|
||||
egui::Key::U => KeyCode::Char('u'),
|
||||
egui::Key::V => KeyCode::Char('v'),
|
||||
egui::Key::W => KeyCode::Char('w'),
|
||||
egui::Key::X => KeyCode::Char('x'),
|
||||
egui::Key::Y => KeyCode::Char('y'),
|
||||
egui::Key::Z => KeyCode::Char('z'),
|
||||
egui::Key::Num0 => KeyCode::Char('0'),
|
||||
egui::Key::Num1 => KeyCode::Char('1'),
|
||||
egui::Key::Num2 => KeyCode::Char('2'),
|
||||
egui::Key::Num3 => KeyCode::Char('3'),
|
||||
egui::Key::Num4 => KeyCode::Char('4'),
|
||||
egui::Key::Num5 => KeyCode::Char('5'),
|
||||
egui::Key::Num6 => KeyCode::Char('6'),
|
||||
egui::Key::Num7 => KeyCode::Char('7'),
|
||||
egui::Key::Num8 => KeyCode::Char('8'),
|
||||
egui::Key::Num9 => KeyCode::Char('9'),
|
||||
egui::Key::Minus => KeyCode::Char('-'),
|
||||
egui::Key::Equals => KeyCode::Char('='),
|
||||
egui::Key::OpenBracket => KeyCode::Char('['),
|
||||
egui::Key::CloseBracket => KeyCode::Char(']'),
|
||||
egui::Key::Semicolon => KeyCode::Char(';'),
|
||||
egui::Key::Comma => KeyCode::Char(','),
|
||||
egui::Key::Period => KeyCode::Char('.'),
|
||||
egui::Key::Slash => KeyCode::Char('/'),
|
||||
egui::Key::Backslash => KeyCode::Char('\\'),
|
||||
egui::Key::Backtick => KeyCode::Char('`'),
|
||||
egui::Key::Quote => KeyCode::Char('\''),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn convert_modifiers(mods: egui::Modifiers) -> KeyModifiers {
|
||||
let mut result = KeyModifiers::empty();
|
||||
if mods.shift {
|
||||
result |= KeyModifiers::SHIFT;
|
||||
}
|
||||
if mods.ctrl || mods.command {
|
||||
result |= KeyModifiers::CONTROL;
|
||||
}
|
||||
if mods.alt {
|
||||
result |= KeyModifiers::ALT;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn is_character_key(key: egui::Key) -> bool {
|
||||
matches!(
|
||||
key,
|
||||
egui::Key::A
|
||||
| egui::Key::B
|
||||
| egui::Key::C
|
||||
| egui::Key::D
|
||||
| egui::Key::E
|
||||
| egui::Key::F
|
||||
| egui::Key::G
|
||||
| egui::Key::H
|
||||
| egui::Key::I
|
||||
| egui::Key::J
|
||||
| egui::Key::K
|
||||
| egui::Key::L
|
||||
| egui::Key::M
|
||||
| egui::Key::N
|
||||
| egui::Key::O
|
||||
| egui::Key::P
|
||||
| egui::Key::Q
|
||||
| egui::Key::R
|
||||
| egui::Key::S
|
||||
| egui::Key::T
|
||||
| egui::Key::U
|
||||
| egui::Key::V
|
||||
| egui::Key::W
|
||||
| egui::Key::X
|
||||
| egui::Key::Y
|
||||
| egui::Key::Z
|
||||
| egui::Key::Num0
|
||||
| egui::Key::Num1
|
||||
| egui::Key::Num2
|
||||
| egui::Key::Num3
|
||||
| egui::Key::Num4
|
||||
| egui::Key::Num5
|
||||
| egui::Key::Num6
|
||||
| egui::Key::Num7
|
||||
| egui::Key::Num8
|
||||
| egui::Key::Num9
|
||||
| egui::Key::Space
|
||||
| egui::Key::Minus
|
||||
| egui::Key::Equals
|
||||
| egui::Key::OpenBracket
|
||||
| egui::Key::CloseBracket
|
||||
| egui::Key::Semicolon
|
||||
| egui::Key::Comma
|
||||
| egui::Key::Period
|
||||
| egui::Key::Slash
|
||||
| egui::Key::Backslash
|
||||
| egui::Key::Backtick
|
||||
| egui::Key::Quote
|
||||
)
|
||||
}
|
||||
460
crates/clap/src/lib.rs
Normal file
460
crates/clap/src/lib.rs
Normal file
@@ -0,0 +1,460 @@
|
||||
mod editor;
|
||||
mod input_egui;
|
||||
mod params;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use crossbeam_channel::{bounded, Receiver, Sender};
|
||||
use nih_plug::prelude::*;
|
||||
use parking_lot::Mutex;
|
||||
use rand::rngs::StdRng;
|
||||
use rand::SeedableRng;
|
||||
use ringbuf::traits::Producer;
|
||||
|
||||
use cagire::engine::{
|
||||
parse_midi_command, spawn_analysis_thread, AnalysisHandle, AudioCommand, MidiCommand,
|
||||
PatternSnapshot, ScopeBuffer, SeqCommand, SequencerState, SharedSequencerState, SpectrumBuffer,
|
||||
StepSnapshot, TickInput,
|
||||
};
|
||||
use cagire::model::{Dictionary, Rng, Variables};
|
||||
use params::CagireParams;
|
||||
|
||||
pub struct PluginBridge {
|
||||
pub cmd_tx: Sender<SeqCommand>,
|
||||
pub cmd_rx: Receiver<SeqCommand>,
|
||||
pub audio_cmd_tx: Sender<AudioCommand>,
|
||||
pub audio_cmd_rx: Receiver<AudioCommand>,
|
||||
pub shared_state: Arc<ArcSwap<SharedSequencerState>>,
|
||||
pub scope_buffer: Arc<ScopeBuffer>,
|
||||
pub spectrum_buffer: Arc<SpectrumBuffer>,
|
||||
pub sample_registry: ArcSwap<Option<Arc<doux::SampleRegistry>>>,
|
||||
}
|
||||
|
||||
struct PendingNoteOff {
|
||||
target_sample: u64,
|
||||
channel: u8,
|
||||
note: u8,
|
||||
}
|
||||
|
||||
pub struct CagirePlugin {
|
||||
params: Arc<CagireParams>,
|
||||
seq_state: Option<SequencerState>,
|
||||
engine: Option<doux::Engine>,
|
||||
sample_rate: f32,
|
||||
prev_beat: f64,
|
||||
sample_pos: u64,
|
||||
bridge: Arc<PluginBridge>,
|
||||
variables: Variables,
|
||||
dict: Dictionary,
|
||||
rng: Rng,
|
||||
cmd_buffer: String,
|
||||
audio_buffer: Vec<f32>,
|
||||
fft_producer: Option<ringbuf::HeapProd<f32>>,
|
||||
_analysis: Option<AnalysisHandle>,
|
||||
pending_note_offs: Vec<PendingNoteOff>,
|
||||
}
|
||||
|
||||
impl Default for CagirePlugin {
|
||||
fn default() -> Self {
|
||||
let variables: Variables = Arc::new(ArcSwap::from_pointee(HashMap::new()));
|
||||
let dict: Dictionary = Arc::new(Mutex::new(HashMap::new()));
|
||||
let rng: Rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
|
||||
|
||||
let (cmd_tx, cmd_rx) = bounded(64);
|
||||
let (audio_cmd_tx, audio_cmd_rx) = bounded(64);
|
||||
let bridge = Arc::new(PluginBridge {
|
||||
cmd_tx,
|
||||
cmd_rx,
|
||||
audio_cmd_tx,
|
||||
audio_cmd_rx,
|
||||
shared_state: Arc::new(ArcSwap::from_pointee(SharedSequencerState::default())),
|
||||
scope_buffer: Arc::new(ScopeBuffer::default()),
|
||||
spectrum_buffer: Arc::new(SpectrumBuffer::default()),
|
||||
sample_registry: ArcSwap::from_pointee(None),
|
||||
});
|
||||
|
||||
Self {
|
||||
params: Arc::new(CagireParams::default()),
|
||||
seq_state: None,
|
||||
engine: None,
|
||||
sample_rate: 44100.0,
|
||||
prev_beat: -1.0,
|
||||
sample_pos: 0,
|
||||
bridge,
|
||||
variables,
|
||||
dict,
|
||||
rng,
|
||||
cmd_buffer: String::with_capacity(256),
|
||||
audio_buffer: Vec::new(),
|
||||
fft_producer: None,
|
||||
_analysis: None,
|
||||
pending_note_offs: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for CagirePlugin {
|
||||
type SysExMessage = ();
|
||||
type BackgroundTask = ();
|
||||
|
||||
const NAME: &'static str = "Cagire";
|
||||
const VENDOR: &'static str = "Bubobubobubobubo";
|
||||
const URL: &'static str = "https://cagire.raphaelforment.fr";
|
||||
const EMAIL: &'static str = "raphael.forment@gmail.com";
|
||||
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout {
|
||||
main_input_channels: None,
|
||||
main_output_channels: Some(new_nonzero_u32(2)),
|
||||
aux_input_ports: &[],
|
||||
aux_output_ports: &[],
|
||||
names: PortNames {
|
||||
layout: Some("Stereo"),
|
||||
main_input: None,
|
||||
main_output: Some("Output"),
|
||||
aux_inputs: &[],
|
||||
aux_outputs: &[],
|
||||
},
|
||||
}];
|
||||
|
||||
const MIDI_OUTPUT: MidiConfig = MidiConfig::MidiCCs;
|
||||
|
||||
fn params(&self) -> Arc<dyn Params> {
|
||||
self.params.clone()
|
||||
}
|
||||
|
||||
fn editor(&mut self, _async_executor: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
|
||||
editor::create_editor(
|
||||
self.params.clone(),
|
||||
self.params.editor_state.clone(),
|
||||
Arc::clone(&self.bridge),
|
||||
Arc::clone(&self.variables),
|
||||
Arc::clone(&self.dict),
|
||||
Arc::clone(&self.rng),
|
||||
)
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
&mut self,
|
||||
_audio_io_layout: &AudioIOLayout,
|
||||
buffer_config: &BufferConfig,
|
||||
_context: &mut impl InitContext<Self>,
|
||||
) -> bool {
|
||||
self.sample_rate = buffer_config.sample_rate;
|
||||
self.sample_pos = 0;
|
||||
self.prev_beat = -1.0;
|
||||
|
||||
self.seq_state = Some(SequencerState::new(
|
||||
Arc::clone(&self.variables),
|
||||
Arc::clone(&self.dict),
|
||||
Arc::clone(&self.rng),
|
||||
None,
|
||||
));
|
||||
|
||||
let engine = doux::Engine::new_with_channels(
|
||||
self.sample_rate,
|
||||
2,
|
||||
64,
|
||||
);
|
||||
self.bridge
|
||||
.sample_registry
|
||||
.store(Arc::new(Some(Arc::clone(&engine.sample_registry))));
|
||||
self.engine = Some(engine);
|
||||
|
||||
let (fft_producer, analysis_handle) = spawn_analysis_thread(
|
||||
self.sample_rate,
|
||||
Arc::clone(&self.bridge.spectrum_buffer),
|
||||
);
|
||||
self.fft_producer = Some(fft_producer);
|
||||
self._analysis = Some(analysis_handle);
|
||||
|
||||
// Seed sequencer with persisted project data
|
||||
let project = self.params.project.lock().clone();
|
||||
for (bank_idx, bank) in project.banks.iter().enumerate() {
|
||||
for (pat_idx, pat) in bank.patterns.iter().enumerate() {
|
||||
let has_content = pat.steps.iter().any(|s| !s.script.is_empty());
|
||||
if !has_content {
|
||||
continue;
|
||||
}
|
||||
let snapshot = PatternSnapshot {
|
||||
speed: pat.speed,
|
||||
length: pat.length,
|
||||
steps: pat
|
||||
.steps
|
||||
.iter()
|
||||
.take(pat.length)
|
||||
.map(|s| StepSnapshot {
|
||||
active: s.active,
|
||||
script: s.script.clone(),
|
||||
source: s.source,
|
||||
})
|
||||
.collect(),
|
||||
quantization: pat.quantization,
|
||||
sync_mode: pat.sync_mode,
|
||||
};
|
||||
let _ = self.bridge.cmd_tx.send(SeqCommand::PatternUpdate {
|
||||
bank: bank_idx,
|
||||
pattern: pat_idx,
|
||||
data: snapshot,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.prev_beat = -1.0;
|
||||
self.sample_pos = 0;
|
||||
if let Some(engine) = &mut self.engine {
|
||||
engine.hush();
|
||||
}
|
||||
}
|
||||
|
||||
fn process(
|
||||
&mut self,
|
||||
buffer: &mut Buffer,
|
||||
_aux: &mut AuxiliaryBuffers,
|
||||
context: &mut impl ProcessContext<Self>,
|
||||
) -> ProcessStatus {
|
||||
let Some(seq_state) = &mut self.seq_state else {
|
||||
return ProcessStatus::Normal;
|
||||
};
|
||||
let Some(engine) = &mut self.engine else {
|
||||
return ProcessStatus::Normal;
|
||||
};
|
||||
|
||||
let transport = context.transport();
|
||||
let buffer_len = buffer.samples();
|
||||
|
||||
let playing = transport.playing;
|
||||
let tempo = transport.tempo.unwrap_or(self.params.tempo.value() as f64);
|
||||
let beat = transport.pos_beats().unwrap_or(0.0);
|
||||
let quantum = transport
|
||||
.time_sig_numerator
|
||||
.map(|n| n as f64)
|
||||
.unwrap_or(4.0);
|
||||
|
||||
let effective_tempo = if self.params.sync_to_host.value() {
|
||||
tempo
|
||||
} else {
|
||||
self.params.tempo.value() as f64
|
||||
};
|
||||
|
||||
let buffer_secs = buffer_len as f64 / self.sample_rate as f64;
|
||||
let lookahead_beats = if effective_tempo > 0.0 {
|
||||
buffer_secs * effective_tempo / 60.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let lookahead_end = beat + lookahead_beats;
|
||||
|
||||
let engine_time = self.sample_pos as f64 / self.sample_rate as f64;
|
||||
|
||||
// Drain commands from the editor
|
||||
let commands: Vec<SeqCommand> = self.bridge.cmd_rx.try_iter().collect();
|
||||
|
||||
let input = TickInput {
|
||||
commands,
|
||||
playing,
|
||||
beat,
|
||||
lookahead_end,
|
||||
tempo: effective_tempo,
|
||||
quantum,
|
||||
fill: false,
|
||||
nudge_secs: 0.0,
|
||||
current_time_us: 0,
|
||||
engine_time,
|
||||
mouse_x: 0.5,
|
||||
mouse_y: 0.5,
|
||||
mouse_down: 0.0,
|
||||
};
|
||||
|
||||
let output = seq_state.tick(input);
|
||||
|
||||
// Publish snapshot for the editor
|
||||
self.bridge
|
||||
.shared_state
|
||||
.store(Arc::new(output.shared_state));
|
||||
|
||||
// Drain audio commands from the editor (preview, hush, load samples, etc.)
|
||||
for audio_cmd in self.bridge.audio_cmd_rx.try_iter() {
|
||||
match audio_cmd {
|
||||
AudioCommand::Evaluate { ref cmd, time } => {
|
||||
let cmd_ref = match time {
|
||||
Some(t) => {
|
||||
self.cmd_buffer.clear();
|
||||
use std::fmt::Write;
|
||||
let _ = write!(&mut self.cmd_buffer, "{cmd}/time/{t:.6}");
|
||||
self.cmd_buffer.as_str()
|
||||
}
|
||||
None => cmd.as_str(),
|
||||
};
|
||||
engine.evaluate(cmd_ref);
|
||||
}
|
||||
AudioCommand::Hush => engine.hush(),
|
||||
AudioCommand::Panic => engine.panic(),
|
||||
AudioCommand::LoadSamples(samples) => {
|
||||
engine.sample_index.extend(samples);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drain expired pending note-offs
|
||||
self.pending_note_offs.retain(|off| {
|
||||
if off.target_sample <= self.sample_pos {
|
||||
context.send_event(NoteEvent::NoteOff {
|
||||
timing: 0,
|
||||
voice_id: None,
|
||||
channel: off.channel,
|
||||
note: off.note,
|
||||
velocity: 0.0,
|
||||
});
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
// Feed audio + MIDI commands from sequencer
|
||||
for tsc in &output.audio_commands {
|
||||
if tsc.cmd.starts_with("/midi/") {
|
||||
if let Some((midi_cmd, dur, _delta)) = parse_midi_command(&tsc.cmd) {
|
||||
match midi_cmd {
|
||||
MidiCommand::NoteOn { channel, note, velocity, .. } => {
|
||||
context.send_event(NoteEvent::NoteOn {
|
||||
timing: 0,
|
||||
voice_id: None,
|
||||
channel,
|
||||
note,
|
||||
velocity: velocity as f32 / 127.0,
|
||||
});
|
||||
if let Some(dur) = dur {
|
||||
self.pending_note_offs.push(PendingNoteOff {
|
||||
target_sample: self.sample_pos
|
||||
+ (dur * self.sample_rate as f64) as u64,
|
||||
channel,
|
||||
note,
|
||||
});
|
||||
}
|
||||
}
|
||||
MidiCommand::NoteOff { channel, note, .. } => {
|
||||
context.send_event(NoteEvent::NoteOff {
|
||||
timing: 0,
|
||||
voice_id: None,
|
||||
channel,
|
||||
note,
|
||||
velocity: 0.0,
|
||||
});
|
||||
}
|
||||
MidiCommand::CC { channel, cc, value, .. } => {
|
||||
context.send_event(NoteEvent::MidiCC {
|
||||
timing: 0,
|
||||
channel,
|
||||
cc,
|
||||
value: value as f32 / 127.0,
|
||||
});
|
||||
}
|
||||
MidiCommand::PitchBend { channel, value, .. } => {
|
||||
context.send_event(NoteEvent::MidiPitchBend {
|
||||
timing: 0,
|
||||
channel,
|
||||
value: value as f32 / 16383.0,
|
||||
});
|
||||
}
|
||||
MidiCommand::Pressure { channel, value, .. } => {
|
||||
context.send_event(NoteEvent::MidiChannelPressure {
|
||||
timing: 0,
|
||||
channel,
|
||||
pressure: value as f32 / 127.0,
|
||||
});
|
||||
}
|
||||
MidiCommand::ProgramChange { channel, program, .. } => {
|
||||
context.send_event(NoteEvent::MidiProgramChange {
|
||||
timing: 0,
|
||||
channel,
|
||||
program,
|
||||
});
|
||||
}
|
||||
MidiCommand::Clock { .. }
|
||||
| MidiCommand::Start { .. }
|
||||
| MidiCommand::Stop { .. }
|
||||
| MidiCommand::Continue { .. } => {}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let cmd_ref = match tsc.time {
|
||||
Some(t) => {
|
||||
self.cmd_buffer.clear();
|
||||
use std::fmt::Write;
|
||||
let _ = write!(&mut self.cmd_buffer, "{}/time/{t:.6}", tsc.cmd);
|
||||
self.cmd_buffer.as_str()
|
||||
}
|
||||
None => &tsc.cmd,
|
||||
};
|
||||
engine.evaluate(cmd_ref);
|
||||
}
|
||||
|
||||
// Process audio block — doux writes interleaved stereo into our buffer
|
||||
let num_samples = buffer_len * 2;
|
||||
self.audio_buffer.resize(num_samples, 0.0);
|
||||
self.audio_buffer.fill(0.0);
|
||||
engine.process_block(&mut self.audio_buffer, &[], &[]);
|
||||
|
||||
// Feed scope and spectrum analysis
|
||||
self.bridge.scope_buffer.write(&self.audio_buffer);
|
||||
if let Some(producer) = &mut self.fft_producer {
|
||||
for chunk in self.audio_buffer.chunks(2) {
|
||||
let mono = (chunk[0] + chunk.get(1).copied().unwrap_or(0.0)) * 0.5;
|
||||
let _ = producer.try_push(mono);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy interleaved doux output → nih-plug channel slices
|
||||
let mut channel_iter = buffer.iter_samples();
|
||||
for frame_idx in 0..buffer_len {
|
||||
if let Some(mut frame) = channel_iter.next() {
|
||||
let left = self.audio_buffer[frame_idx * 2];
|
||||
let right = self.audio_buffer[frame_idx * 2 + 1];
|
||||
if let Some(sample) = frame.get_mut(0) {
|
||||
*sample = left;
|
||||
}
|
||||
if let Some(sample) = frame.get_mut(1) {
|
||||
*sample = right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.sample_pos += buffer_len as u64;
|
||||
self.prev_beat = lookahead_end;
|
||||
|
||||
ProcessStatus::Normal
|
||||
}
|
||||
}
|
||||
|
||||
impl ClapPlugin for CagirePlugin {
|
||||
const CLAP_ID: &'static str = "com.sova.cagire";
|
||||
const CLAP_DESCRIPTION: Option<&'static str> = Some("Forth-based music sequencer");
|
||||
const CLAP_MANUAL_URL: Option<&'static str> = Some("https://cagire.raphaelforment.fr");
|
||||
const CLAP_SUPPORT_URL: Option<&'static str> = Some("https://cagire.raphaelforment.fr");
|
||||
const CLAP_FEATURES: &'static [ClapFeature] = &[
|
||||
ClapFeature::Instrument,
|
||||
ClapFeature::Synthesizer,
|
||||
ClapFeature::Stereo,
|
||||
];
|
||||
}
|
||||
|
||||
impl Vst3Plugin for CagirePlugin {
|
||||
const VST3_CLASS_ID: [u8; 16] = *b"CagireSovaVST3!!";
|
||||
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = &[
|
||||
Vst3SubCategory::Instrument,
|
||||
Vst3SubCategory::Synth,
|
||||
Vst3SubCategory::Stereo,
|
||||
];
|
||||
}
|
||||
|
||||
nih_export_clap!(CagirePlugin);
|
||||
nih_export_vst3!(CagirePlugin);
|
||||
12
crates/clap/src/main.rs
Normal file
12
crates/clap/src/main.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use cagire_clap::CagirePlugin;
|
||||
use nih_plug::prelude::*;
|
||||
|
||||
fn main() {
|
||||
let mut args: Vec<String> = std::env::args().collect();
|
||||
// Default to 44100 Hz — nih-plug defaults to 48000 which causes CoreAudio
|
||||
// to deliver mismatched buffer sizes, crashing the standalone wrapper.
|
||||
if !args.iter().any(|a| a == "--sample-rate" || a == "-r") {
|
||||
args.extend(["--sample-rate".into(), "44100".into()]);
|
||||
}
|
||||
nih_export_standalone_with_args::<CagirePlugin, _>(args);
|
||||
}
|
||||
46
crates/clap/src/params.rs
Normal file
46
crates/clap/src/params.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use cagire_project::Project;
|
||||
use nih_plug::prelude::*;
|
||||
use nih_plug_egui::EguiState;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
#[derive(Params)]
|
||||
pub struct CagireParams {
|
||||
#[persist = "editor-state"]
|
||||
pub editor_state: Arc<EguiState>,
|
||||
|
||||
#[persist = "project"]
|
||||
pub project: Arc<Mutex<Project>>,
|
||||
|
||||
#[id = "tempo"]
|
||||
pub tempo: FloatParam,
|
||||
|
||||
#[id = "sync"]
|
||||
pub sync_to_host: BoolParam,
|
||||
|
||||
#[id = "bank"]
|
||||
pub bank: IntParam,
|
||||
|
||||
#[id = "pattern"]
|
||||
pub pattern: IntParam,
|
||||
}
|
||||
|
||||
impl Default for CagireParams {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
editor_state: EguiState::from_size(1200, 800),
|
||||
project: Arc::new(Mutex::new(Project::default())),
|
||||
|
||||
tempo: FloatParam::new("Tempo", 120.0, FloatRange::Linear { min: 40.0, max: 300.0 })
|
||||
.with_unit(" BPM")
|
||||
.with_step_size(0.1),
|
||||
|
||||
sync_to_host: BoolParam::new("Sync to Host", true),
|
||||
|
||||
bank: IntParam::new("Bank", 0, IntRange::Linear { min: 0, max: 31 }),
|
||||
|
||||
pattern: IntParam::new("Pattern", 0, IntRange::Linear { min: 0, max: 31 }),
|
||||
}
|
||||
}
|
||||
}
|
||||
24
crates/egui-baseview/Cargo.toml
Normal file
24
crates/egui-baseview/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "egui-baseview"
|
||||
version = "0.7.0"
|
||||
authors = ["Billy Messenger <60663878+BillyDM@users.noreply.github.com>"]
|
||||
description = "A baseview backend for egui"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
[features]
|
||||
default = ["opengl", "default_fonts", "tracing"]
|
||||
default_fonts = ["egui/default_fonts"]
|
||||
opengl = ["dep:egui_glow", "baseview/opengl"]
|
||||
tracing = ["dep:tracing"]
|
||||
|
||||
[dependencies]
|
||||
baseview = { git = "https://github.com/RustAudio/baseview.git", rev = "237d323c729f3aa99476ba3efa50129c5e86cad3" }
|
||||
raw-window-handle = "0.5"
|
||||
egui = { version = "0.33", default-features = false, features = ["bytemuck"] }
|
||||
egui_glow = { version = "0.33", features = ["x11"], optional = true }
|
||||
keyboard-types = { version = "0.6", default-features = false }
|
||||
copypasta = { version = "0.10", default-features = false, features = ["x11"] }
|
||||
tracing = { version = "0.1", optional = true }
|
||||
open = "5.1"
|
||||
thiserror = "2.0"
|
||||
21
crates/egui-baseview/LICENSE
Normal file
21
crates/egui-baseview/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Billy Messenger
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
10
crates/egui-baseview/src/lib.rs
Normal file
10
crates/egui-baseview/src/lib.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
mod renderer;
|
||||
mod translate;
|
||||
mod window;
|
||||
|
||||
pub use window::{EguiWindow, KeyCapture, Queue};
|
||||
|
||||
pub use egui;
|
||||
pub use renderer::GraphicsConfig;
|
||||
|
||||
pub use keyboard_types::Key;
|
||||
4
crates/egui-baseview/src/renderer.rs
Normal file
4
crates/egui-baseview/src/renderer.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
#[cfg(feature = "opengl")]
|
||||
mod opengl;
|
||||
#[cfg(feature = "opengl")]
|
||||
pub use opengl::renderer::{GraphicsConfig, Renderer};
|
||||
12
crates/egui-baseview/src/renderer/opengl.rs
Normal file
12
crates/egui-baseview/src/renderer/opengl.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use egui_glow::PainterError;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod renderer;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum OpenGlError {
|
||||
#[error("Failed to get baseview's GL context")]
|
||||
NoContext,
|
||||
#[error("Error occured when initializing painter: \n {0}")]
|
||||
CreatePainter(PainterError),
|
||||
}
|
||||
130
crates/egui-baseview/src/renderer/opengl/renderer.rs
Normal file
130
crates/egui-baseview/src/renderer/opengl/renderer.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use baseview::{PhySize, Window};
|
||||
use egui::FullOutput;
|
||||
use egui_glow::Painter;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::OpenGlError;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GraphicsConfig {
|
||||
/// Controls whether to apply dithering to minimize banding artifacts.
|
||||
///
|
||||
/// Dithering assumes an sRGB output and thus will apply noise to any input value that lies between
|
||||
/// two 8bit values after applying the sRGB OETF function, i.e. if it's not a whole 8bit value in "gamma space".
|
||||
/// This means that only inputs from texture interpolation and vertex colors should be affected in practice.
|
||||
///
|
||||
/// Defaults to true.
|
||||
pub dithering: bool,
|
||||
|
||||
/// Needed for cross compiling for VirtualBox VMSVGA driver with OpenGL ES 2.0 and OpenGL 2.1 which doesn't support SRGB texture.
|
||||
/// See <https://github.com/emilk/egui/pull/1993>.
|
||||
///
|
||||
/// For OpenGL ES 2.0: set this to [`egui_glow::ShaderVersion::Es100`] to solve blank texture problem (by using the "fallback shader").
|
||||
pub shader_version: Option<egui_glow::ShaderVersion>,
|
||||
}
|
||||
|
||||
impl Default for GraphicsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
shader_version: None,
|
||||
dithering: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Renderer {
|
||||
glow_context: Arc<egui_glow::glow::Context>,
|
||||
painter: Painter,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
pub fn new(window: &Window, config: GraphicsConfig) -> Result<Self, OpenGlError> {
|
||||
let context = window.gl_context().ok_or(OpenGlError::NoContext)?;
|
||||
unsafe {
|
||||
context.make_current();
|
||||
}
|
||||
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
let glow_context = Arc::new(unsafe {
|
||||
egui_glow::glow::Context::from_loader_function(|s| context.get_proc_address(s))
|
||||
});
|
||||
|
||||
let painter = egui_glow::Painter::new(
|
||||
Arc::clone(&glow_context),
|
||||
"",
|
||||
config.shader_version,
|
||||
config.dithering,
|
||||
)
|
||||
.map_err(OpenGlError::CreatePainter)?;
|
||||
|
||||
unsafe {
|
||||
context.make_not_current();
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
glow_context,
|
||||
painter,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn max_texture_side(&self) -> usize {
|
||||
self.painter.max_texture_side()
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
window: &Window,
|
||||
bg_color: egui::Rgba,
|
||||
physical_size: PhySize,
|
||||
pixels_per_point: f32,
|
||||
egui_ctx: &mut egui::Context,
|
||||
full_output: &mut FullOutput,
|
||||
) {
|
||||
let PhySize {
|
||||
width: canvas_width,
|
||||
height: canvas_height,
|
||||
} = physical_size;
|
||||
|
||||
let shapes = std::mem::take(&mut full_output.shapes);
|
||||
let textures_delta = &mut full_output.textures_delta;
|
||||
|
||||
let context = window
|
||||
.gl_context()
|
||||
.expect("failed to get baseview gl context");
|
||||
unsafe {
|
||||
context.make_current();
|
||||
}
|
||||
|
||||
unsafe {
|
||||
use egui_glow::glow::HasContext as _;
|
||||
self.glow_context
|
||||
.clear_color(bg_color.r(), bg_color.g(), bg_color.b(), bg_color.a());
|
||||
self.glow_context.clear(egui_glow::glow::COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
for (id, image_delta) in &textures_delta.set {
|
||||
self.painter.set_texture(*id, image_delta);
|
||||
}
|
||||
|
||||
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
|
||||
let dimensions: [u32; 2] = [canvas_width, canvas_height];
|
||||
|
||||
self.painter
|
||||
.paint_primitives(dimensions, pixels_per_point, &clipped_primitives);
|
||||
|
||||
for id in textures_delta.free.drain(..) {
|
||||
self.painter.free_texture(id);
|
||||
}
|
||||
|
||||
unsafe {
|
||||
context.swap_buffers();
|
||||
context.make_not_current();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Renderer {
|
||||
fn drop(&mut self) {
|
||||
self.painter.destroy()
|
||||
}
|
||||
}
|
||||
118
crates/egui-baseview/src/translate.rs
Normal file
118
crates/egui-baseview/src/translate.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
pub(crate) fn translate_mouse_button(button: baseview::MouseButton) -> Option<egui::PointerButton> {
|
||||
match button {
|
||||
baseview::MouseButton::Left => Some(egui::PointerButton::Primary),
|
||||
baseview::MouseButton::Right => Some(egui::PointerButton::Secondary),
|
||||
baseview::MouseButton::Middle => Some(egui::PointerButton::Middle),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn translate_virtual_key(key: &keyboard_types::Key) -> Option<egui::Key> {
|
||||
use egui::Key;
|
||||
use keyboard_types::Key as K;
|
||||
|
||||
Some(match key {
|
||||
K::ArrowDown => Key::ArrowDown,
|
||||
K::ArrowLeft => Key::ArrowLeft,
|
||||
K::ArrowRight => Key::ArrowRight,
|
||||
K::ArrowUp => Key::ArrowUp,
|
||||
|
||||
K::Escape => Key::Escape,
|
||||
K::Tab => Key::Tab,
|
||||
K::Backspace => Key::Backspace,
|
||||
K::Enter => Key::Enter,
|
||||
|
||||
K::Insert => Key::Insert,
|
||||
K::Delete => Key::Delete,
|
||||
K::Home => Key::Home,
|
||||
K::End => Key::End,
|
||||
K::PageUp => Key::PageUp,
|
||||
K::PageDown => Key::PageDown,
|
||||
|
||||
K::Character(s) => match s.chars().next()? {
|
||||
' ' => Key::Space,
|
||||
'0' => Key::Num0,
|
||||
'1' => Key::Num1,
|
||||
'2' => Key::Num2,
|
||||
'3' => Key::Num3,
|
||||
'4' => Key::Num4,
|
||||
'5' => Key::Num5,
|
||||
'6' => Key::Num6,
|
||||
'7' => Key::Num7,
|
||||
'8' => Key::Num8,
|
||||
'9' => Key::Num9,
|
||||
'a' => Key::A,
|
||||
'b' => Key::B,
|
||||
'c' => Key::C,
|
||||
'd' => Key::D,
|
||||
'e' => Key::E,
|
||||
'f' => Key::F,
|
||||
'g' => Key::G,
|
||||
'h' => Key::H,
|
||||
'i' => Key::I,
|
||||
'j' => Key::J,
|
||||
'k' => Key::K,
|
||||
'l' => Key::L,
|
||||
'm' => Key::M,
|
||||
'n' => Key::N,
|
||||
'o' => Key::O,
|
||||
'p' => Key::P,
|
||||
'q' => Key::Q,
|
||||
'r' => Key::R,
|
||||
's' => Key::S,
|
||||
't' => Key::T,
|
||||
'u' => Key::U,
|
||||
'v' => Key::V,
|
||||
'w' => Key::W,
|
||||
'x' => Key::X,
|
||||
'y' => Key::Y,
|
||||
'z' => Key::Z,
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn translate_cursor_icon(cursor: egui::CursorIcon) -> baseview::MouseCursor {
|
||||
match cursor {
|
||||
egui::CursorIcon::Default => baseview::MouseCursor::Default,
|
||||
egui::CursorIcon::None => baseview::MouseCursor::Hidden,
|
||||
egui::CursorIcon::ContextMenu => baseview::MouseCursor::Hand,
|
||||
egui::CursorIcon::Help => baseview::MouseCursor::Help,
|
||||
egui::CursorIcon::PointingHand => baseview::MouseCursor::Hand,
|
||||
egui::CursorIcon::Progress => baseview::MouseCursor::PtrWorking,
|
||||
egui::CursorIcon::Wait => baseview::MouseCursor::Working,
|
||||
egui::CursorIcon::Cell => baseview::MouseCursor::Cell,
|
||||
egui::CursorIcon::Crosshair => baseview::MouseCursor::Crosshair,
|
||||
egui::CursorIcon::Text => baseview::MouseCursor::Text,
|
||||
egui::CursorIcon::VerticalText => baseview::MouseCursor::VerticalText,
|
||||
egui::CursorIcon::Alias => baseview::MouseCursor::Alias,
|
||||
egui::CursorIcon::Copy => baseview::MouseCursor::Copy,
|
||||
egui::CursorIcon::Move => baseview::MouseCursor::Move,
|
||||
egui::CursorIcon::NoDrop => baseview::MouseCursor::NotAllowed,
|
||||
egui::CursorIcon::NotAllowed => baseview::MouseCursor::NotAllowed,
|
||||
egui::CursorIcon::Grab => baseview::MouseCursor::Hand,
|
||||
egui::CursorIcon::Grabbing => baseview::MouseCursor::HandGrabbing,
|
||||
egui::CursorIcon::AllScroll => baseview::MouseCursor::AllScroll,
|
||||
egui::CursorIcon::ResizeHorizontal => baseview::MouseCursor::EwResize,
|
||||
egui::CursorIcon::ResizeNeSw => baseview::MouseCursor::NeswResize,
|
||||
egui::CursorIcon::ResizeNwSe => baseview::MouseCursor::NwseResize,
|
||||
egui::CursorIcon::ResizeVertical => baseview::MouseCursor::NsResize,
|
||||
egui::CursorIcon::ResizeEast => baseview::MouseCursor::EResize,
|
||||
egui::CursorIcon::ResizeSouthEast => baseview::MouseCursor::SeResize,
|
||||
egui::CursorIcon::ResizeSouth => baseview::MouseCursor::SResize,
|
||||
egui::CursorIcon::ResizeSouthWest => baseview::MouseCursor::SwResize,
|
||||
egui::CursorIcon::ResizeWest => baseview::MouseCursor::WResize,
|
||||
egui::CursorIcon::ResizeNorthWest => baseview::MouseCursor::NwResize,
|
||||
egui::CursorIcon::ResizeNorth => baseview::MouseCursor::NResize,
|
||||
egui::CursorIcon::ResizeNorthEast => baseview::MouseCursor::NeResize,
|
||||
egui::CursorIcon::ResizeColumn => baseview::MouseCursor::ColResize,
|
||||
egui::CursorIcon::ResizeRow => baseview::MouseCursor::RowResize,
|
||||
egui::CursorIcon::ZoomIn => baseview::MouseCursor::ZoomIn,
|
||||
egui::CursorIcon::ZoomOut => baseview::MouseCursor::ZoomOut,
|
||||
}
|
||||
}
|
||||
685
crates/egui-baseview/src/window.rs
Normal file
685
crates/egui-baseview/src/window.rs
Normal file
@@ -0,0 +1,685 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use baseview::{
|
||||
Event, EventStatus, PhySize, Window, WindowHandle, WindowHandler, WindowOpenOptions,
|
||||
WindowScalePolicy,
|
||||
};
|
||||
use copypasta::ClipboardProvider;
|
||||
use egui::{pos2, vec2, Pos2, Rect, Rgba, ViewportCommand};
|
||||
use keyboard_types::Modifiers;
|
||||
use raw_window_handle::HasRawWindowHandle;
|
||||
|
||||
use crate::{renderer::Renderer, GraphicsConfig};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
use tracing::{error, warn};
|
||||
|
||||
pub struct Queue<'a> {
|
||||
bg_color: &'a mut Rgba,
|
||||
close_requested: &'a mut bool,
|
||||
physical_size: &'a mut PhySize,
|
||||
key_capture: &'a mut KeyCapture,
|
||||
}
|
||||
|
||||
impl<'a> Queue<'a> {
|
||||
pub(crate) fn new(
|
||||
bg_color: &'a mut Rgba,
|
||||
close_requested: &'a mut bool,
|
||||
physical_size: &'a mut PhySize,
|
||||
key_capture: &'a mut KeyCapture,
|
||||
) -> Self {
|
||||
Self {
|
||||
bg_color,
|
||||
//renderer,
|
||||
//repaint_requested,
|
||||
close_requested,
|
||||
physical_size,
|
||||
key_capture,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the background color.
|
||||
pub fn bg_color(&mut self, bg_color: Rgba) {
|
||||
*self.bg_color = bg_color;
|
||||
}
|
||||
|
||||
/// Set size of the window.
|
||||
pub fn resize(&mut self, physical_size: PhySize) {
|
||||
*self.physical_size = physical_size;
|
||||
}
|
||||
|
||||
/// Close the window.
|
||||
pub fn close_window(&mut self) {
|
||||
*self.close_requested = true;
|
||||
}
|
||||
|
||||
/// Set how to handle capturing key events from the host.
|
||||
pub fn set_key_capture(&mut self, key_capture: KeyCapture) {
|
||||
*self.key_capture = key_capture;
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenSettings {
|
||||
scale_policy: WindowScalePolicy,
|
||||
logical_width: f64,
|
||||
logical_height: f64,
|
||||
title: String,
|
||||
}
|
||||
|
||||
impl OpenSettings {
|
||||
fn new(settings: &WindowOpenOptions) -> Self {
|
||||
// WindowScalePolicy does not implement copy/clone.
|
||||
let scale_policy = match &settings.scale {
|
||||
WindowScalePolicy::SystemScaleFactor => WindowScalePolicy::SystemScaleFactor,
|
||||
WindowScalePolicy::ScaleFactor(scale) => WindowScalePolicy::ScaleFactor(*scale),
|
||||
};
|
||||
|
||||
Self {
|
||||
scale_policy,
|
||||
logical_width: settings.size.width,
|
||||
logical_height: settings.size.height,
|
||||
title: settings.title.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes how to handle capturing key events from the host.
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
pub enum KeyCapture {
|
||||
#[default]
|
||||
/// All keys will be captured from the host.
|
||||
CaptureAll,
|
||||
/// No keys will be captured from the host.
|
||||
IgnoreAll,
|
||||
/// Only the given keys will be captured from the host.
|
||||
CaptureKeys(Vec<keyboard_types::Key>),
|
||||
/// All keys except the given ones will be captured from the host.
|
||||
IgnoreKeys(Vec<keyboard_types::Key>),
|
||||
}
|
||||
|
||||
/// Handles an egui-baseview application
|
||||
pub struct EguiWindow<State, U>
|
||||
where
|
||||
State: 'static + Send,
|
||||
U: FnMut(&egui::Context, &mut Queue, &mut State),
|
||||
U: 'static + Send,
|
||||
{
|
||||
user_state: Option<State>,
|
||||
user_update: U,
|
||||
|
||||
egui_ctx: egui::Context,
|
||||
viewport_id: egui::ViewportId,
|
||||
start_time: Instant,
|
||||
egui_input: egui::RawInput,
|
||||
pointer_pos_in_points: Option<egui::Pos2>,
|
||||
current_cursor_icon: baseview::MouseCursor,
|
||||
|
||||
renderer: Renderer,
|
||||
|
||||
clipboard_ctx: Option<copypasta::ClipboardContext>,
|
||||
|
||||
physical_size: PhySize,
|
||||
scale_policy: WindowScalePolicy,
|
||||
pixels_per_point: f32,
|
||||
points_per_pixel: f32,
|
||||
bg_color: Rgba,
|
||||
close_requested: bool,
|
||||
repaint_after: Option<Instant>,
|
||||
key_capture: KeyCapture,
|
||||
}
|
||||
|
||||
impl<State, U> EguiWindow<State, U>
|
||||
where
|
||||
State: 'static + Send,
|
||||
U: FnMut(&egui::Context, &mut Queue, &mut State),
|
||||
U: 'static + Send,
|
||||
{
|
||||
fn new<B>(
|
||||
window: &mut baseview::Window<'_>,
|
||||
open_settings: OpenSettings,
|
||||
graphics_config: GraphicsConfig,
|
||||
mut build: B,
|
||||
update: U,
|
||||
mut state: State,
|
||||
) -> EguiWindow<State, U>
|
||||
where
|
||||
B: FnMut(&egui::Context, &mut Queue, &mut State),
|
||||
B: 'static + Send,
|
||||
{
|
||||
let renderer = Renderer::new(window, graphics_config).unwrap_or_else(|err| {
|
||||
// TODO: better error log and not panicking, but that's gonna require baseview changes
|
||||
error!("oops! the gpu backend couldn't initialize! \n {err}");
|
||||
panic!("gpu backend failed to initialize: \n {err}")
|
||||
});
|
||||
let egui_ctx = egui::Context::default();
|
||||
|
||||
// Assume scale for now until there is an event with a new one.
|
||||
let pixels_per_point = match open_settings.scale_policy {
|
||||
WindowScalePolicy::ScaleFactor(scale) => scale,
|
||||
WindowScalePolicy::SystemScaleFactor => 1.0,
|
||||
} as f32;
|
||||
let points_per_pixel = pixels_per_point.recip();
|
||||
|
||||
let screen_rect = Rect::from_min_size(
|
||||
Pos2::new(0f32, 0f32),
|
||||
vec2(
|
||||
open_settings.logical_width as f32,
|
||||
open_settings.logical_height as f32,
|
||||
),
|
||||
);
|
||||
|
||||
let viewport_info = egui::ViewportInfo {
|
||||
parent: None,
|
||||
title: Some(open_settings.title),
|
||||
native_pixels_per_point: Some(pixels_per_point),
|
||||
focused: Some(true),
|
||||
inner_rect: Some(screen_rect),
|
||||
..Default::default()
|
||||
};
|
||||
let viewport_id = egui::ViewportId::default();
|
||||
|
||||
let mut egui_input = egui::RawInput {
|
||||
max_texture_side: Some(renderer.max_texture_side()),
|
||||
screen_rect: Some(screen_rect),
|
||||
..Default::default()
|
||||
};
|
||||
let _ = egui_input.viewports.insert(viewport_id, viewport_info);
|
||||
|
||||
let mut physical_size = PhySize {
|
||||
width: (open_settings.logical_width * pixels_per_point as f64).round() as u32,
|
||||
height: (open_settings.logical_height * pixels_per_point as f64).round() as u32,
|
||||
};
|
||||
|
||||
let mut bg_color = Rgba::BLACK;
|
||||
let mut close_requested = false;
|
||||
let mut key_capture = KeyCapture::default();
|
||||
let mut queue = Queue::new(
|
||||
&mut bg_color,
|
||||
&mut close_requested,
|
||||
&mut physical_size,
|
||||
&mut key_capture,
|
||||
);
|
||||
(build)(&egui_ctx, &mut queue, &mut state);
|
||||
|
||||
let clipboard_ctx = match copypasta::ClipboardContext::new() {
|
||||
Ok(clipboard_ctx) => Some(clipboard_ctx),
|
||||
Err(e) => {
|
||||
error!("Failed to initialize clipboard: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let start_time = Instant::now();
|
||||
|
||||
Self {
|
||||
user_state: Some(state),
|
||||
user_update: update,
|
||||
|
||||
egui_ctx,
|
||||
viewport_id,
|
||||
start_time,
|
||||
egui_input,
|
||||
pointer_pos_in_points: None,
|
||||
current_cursor_icon: baseview::MouseCursor::Default,
|
||||
|
||||
renderer,
|
||||
|
||||
clipboard_ctx,
|
||||
|
||||
physical_size,
|
||||
pixels_per_point,
|
||||
points_per_pixel,
|
||||
scale_policy: open_settings.scale_policy,
|
||||
bg_color,
|
||||
close_requested,
|
||||
repaint_after: Some(start_time),
|
||||
key_capture,
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a new child window.
|
||||
///
|
||||
/// * `parent` - The parent window.
|
||||
/// * `settings` - The settings of the window.
|
||||
/// * `state` - The initial state of your application.
|
||||
/// * `build` - Called once before the first frame. Allows you to do setup code and to
|
||||
/// call `ctx.set_fonts()`. Optional.
|
||||
/// * `update` - Called before each frame. Here you should update the state of your
|
||||
/// application and build the UI.
|
||||
pub fn open_parented<P, B>(
|
||||
parent: &P,
|
||||
#[allow(unused_mut)] mut settings: WindowOpenOptions,
|
||||
graphics_config: GraphicsConfig,
|
||||
state: State,
|
||||
build: B,
|
||||
update: U,
|
||||
) -> WindowHandle
|
||||
where
|
||||
P: HasRawWindowHandle,
|
||||
B: FnMut(&egui::Context, &mut Queue, &mut State),
|
||||
B: 'static + Send,
|
||||
{
|
||||
#[cfg(feature = "opengl")]
|
||||
if settings.gl_config.is_none() {
|
||||
settings.gl_config = Some(Default::default());
|
||||
}
|
||||
|
||||
let open_settings = OpenSettings::new(&settings);
|
||||
|
||||
Window::open_parented(
|
||||
parent,
|
||||
settings,
|
||||
move |window: &mut baseview::Window<'_>| -> EguiWindow<State, U> {
|
||||
EguiWindow::new(window, open_settings, graphics_config, build, update, state)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Open a new window that blocks the current thread until the window is destroyed.
|
||||
///
|
||||
/// * `settings` - The settings of the window.
|
||||
/// * `state` - The initial state of your application.
|
||||
/// * `build` - Called once before the first frame. Allows you to do setup code and to
|
||||
/// call `ctx.set_fonts()`. Optional.
|
||||
/// * `update` - Called before each frame. Here you should update the state of your
|
||||
/// application and build the UI.
|
||||
pub fn open_blocking<B>(
|
||||
#[allow(unused_mut)] mut settings: WindowOpenOptions,
|
||||
graphics_config: GraphicsConfig,
|
||||
state: State,
|
||||
build: B,
|
||||
update: U,
|
||||
) where
|
||||
B: FnMut(&egui::Context, &mut Queue, &mut State),
|
||||
B: 'static + Send,
|
||||
{
|
||||
#[cfg(feature = "opengl")]
|
||||
if settings.gl_config.is_none() {
|
||||
settings.gl_config = Some(Default::default());
|
||||
}
|
||||
|
||||
let open_settings = OpenSettings::new(&settings);
|
||||
|
||||
Window::open_blocking(
|
||||
settings,
|
||||
move |window: &mut baseview::Window<'_>| -> EguiWindow<State, U> {
|
||||
EguiWindow::new(window, open_settings, graphics_config, build, update, state)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Update the pressed key modifiers when a mouse event has sent a new set of modifiers.
|
||||
fn update_modifiers(&mut self, modifiers: &Modifiers) {
|
||||
self.egui_input.modifiers.alt = !(*modifiers & Modifiers::ALT).is_empty();
|
||||
self.egui_input.modifiers.shift = !(*modifiers & Modifiers::SHIFT).is_empty();
|
||||
self.egui_input.modifiers.command = !(*modifiers & Modifiers::CONTROL).is_empty();
|
||||
}
|
||||
}
|
||||
|
||||
impl<State, U> WindowHandler for EguiWindow<State, U>
|
||||
where
|
||||
State: 'static + Send,
|
||||
U: FnMut(&egui::Context, &mut Queue, &mut State),
|
||||
U: 'static + Send,
|
||||
{
|
||||
fn on_frame(&mut self, window: &mut Window) {
|
||||
let Some(state) = &mut self.user_state else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.egui_input.time = Some(self.start_time.elapsed().as_secs_f64());
|
||||
self.egui_input.screen_rect = Some(calculate_screen_rect(
|
||||
self.physical_size,
|
||||
self.points_per_pixel,
|
||||
));
|
||||
|
||||
self.egui_ctx.begin_pass(self.egui_input.take());
|
||||
|
||||
//let mut repaint_requested = false;
|
||||
let mut queue = Queue::new(
|
||||
&mut self.bg_color,
|
||||
&mut self.close_requested,
|
||||
&mut self.physical_size,
|
||||
&mut self.key_capture,
|
||||
);
|
||||
|
||||
(self.user_update)(&self.egui_ctx, &mut queue, state);
|
||||
|
||||
if self.close_requested {
|
||||
window.close();
|
||||
}
|
||||
|
||||
// Prevent data from being allocated every frame by storing this
|
||||
// in a member field.
|
||||
let mut full_output = self.egui_ctx.end_pass();
|
||||
|
||||
let Some(viewport_output) = full_output.viewport_output.get(&self.viewport_id) else {
|
||||
// The main window was closed by egui.
|
||||
window.close();
|
||||
return;
|
||||
};
|
||||
|
||||
for command in viewport_output.commands.iter() {
|
||||
match command {
|
||||
ViewportCommand::Close => {
|
||||
window.close();
|
||||
}
|
||||
ViewportCommand::InnerSize(size) => window.resize(baseview::Size {
|
||||
width: size.x.max(1.0) as f64,
|
||||
height: size.y.max(1.0) as f64,
|
||||
}),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
let do_repaint_now = if let Some(t) = self.repaint_after {
|
||||
now >= t || viewport_output.repaint_delay.is_zero()
|
||||
} else {
|
||||
viewport_output.repaint_delay.is_zero()
|
||||
};
|
||||
|
||||
if do_repaint_now {
|
||||
self.renderer.render(
|
||||
#[cfg(feature = "opengl")]
|
||||
window,
|
||||
self.bg_color,
|
||||
self.physical_size,
|
||||
self.pixels_per_point,
|
||||
&mut self.egui_ctx,
|
||||
&mut full_output,
|
||||
);
|
||||
|
||||
self.repaint_after = None;
|
||||
} else if let Some(repaint_after) = now.checked_add(viewport_output.repaint_delay) {
|
||||
// Schedule to repaint after the requested time has elapsed.
|
||||
self.repaint_after = Some(repaint_after);
|
||||
}
|
||||
|
||||
for command in full_output.platform_output.commands {
|
||||
match command {
|
||||
egui::OutputCommand::CopyText(text) => {
|
||||
if let Some(clipboard_ctx) = &mut self.clipboard_ctx {
|
||||
if let Err(err) = clipboard_ctx.set_contents(text) {
|
||||
error!("Copy/Cut error: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
egui::OutputCommand::CopyImage(_) => {
|
||||
warn!("Copying images is not supported in egui_baseview.");
|
||||
}
|
||||
egui::OutputCommand::OpenUrl(open_url) => {
|
||||
if let Err(err) = open::that_detached(&open_url.url) {
|
||||
error!("Open error: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cursor_icon =
|
||||
crate::translate::translate_cursor_icon(full_output.platform_output.cursor_icon);
|
||||
if self.current_cursor_icon != cursor_icon {
|
||||
self.current_cursor_icon = cursor_icon;
|
||||
|
||||
// TODO: Set mouse cursor for MacOS once baseview supports it.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
window.set_mouse_cursor(cursor_icon);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn on_event(&mut self, _window: &mut Window, event: Event) -> EventStatus {
|
||||
let mut return_status = EventStatus::Captured;
|
||||
|
||||
match &event {
|
||||
baseview::Event::Mouse(event) => match event {
|
||||
baseview::MouseEvent::CursorMoved {
|
||||
position,
|
||||
modifiers,
|
||||
} => {
|
||||
self.update_modifiers(modifiers);
|
||||
|
||||
let pos = pos2(position.x as f32, position.y as f32);
|
||||
self.pointer_pos_in_points = Some(pos);
|
||||
self.egui_input.events.push(egui::Event::PointerMoved(pos));
|
||||
}
|
||||
baseview::MouseEvent::ButtonPressed { button, modifiers } => {
|
||||
self.update_modifiers(modifiers);
|
||||
|
||||
if let Some(pos) = self.pointer_pos_in_points {
|
||||
if let Some(button) = crate::translate::translate_mouse_button(*button) {
|
||||
self.egui_input.events.push(egui::Event::PointerButton {
|
||||
pos,
|
||||
button,
|
||||
pressed: true,
|
||||
modifiers: self.egui_input.modifiers,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
baseview::MouseEvent::ButtonReleased { button, modifiers } => {
|
||||
self.update_modifiers(modifiers);
|
||||
|
||||
if let Some(pos) = self.pointer_pos_in_points {
|
||||
if let Some(button) = crate::translate::translate_mouse_button(*button) {
|
||||
self.egui_input.events.push(egui::Event::PointerButton {
|
||||
pos,
|
||||
button,
|
||||
pressed: false,
|
||||
modifiers: self.egui_input.modifiers,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
baseview::MouseEvent::WheelScrolled {
|
||||
delta: scroll_delta,
|
||||
modifiers,
|
||||
} => {
|
||||
self.update_modifiers(modifiers);
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let (unit, mut delta) = match scroll_delta {
|
||||
baseview::ScrollDelta::Lines { x, y } => {
|
||||
(egui::MouseWheelUnit::Line, egui::vec2(*x, *y))
|
||||
}
|
||||
|
||||
baseview::ScrollDelta::Pixels { x, y } => (
|
||||
egui::MouseWheelUnit::Point,
|
||||
egui::vec2(*x, *y) * self.points_per_pixel,
|
||||
),
|
||||
};
|
||||
|
||||
if cfg!(target_os = "macos") {
|
||||
// This is still buggy in winit despite
|
||||
// https://github.com/rust-windowing/winit/issues/1695 being closed
|
||||
//
|
||||
// TODO: See if this is an issue in baseview as well.
|
||||
delta.x *= -1.0;
|
||||
}
|
||||
|
||||
self.egui_input.events.push(egui::Event::MouseWheel {
|
||||
unit,
|
||||
delta,
|
||||
modifiers: self.egui_input.modifiers,
|
||||
});
|
||||
}
|
||||
baseview::MouseEvent::CursorLeft => {
|
||||
self.pointer_pos_in_points = None;
|
||||
self.egui_input.events.push(egui::Event::PointerGone);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
baseview::Event::Keyboard(event) => {
|
||||
use keyboard_types::Code;
|
||||
|
||||
let pressed = event.state == keyboard_types::KeyState::Down;
|
||||
|
||||
match event.code {
|
||||
Code::ShiftLeft | Code::ShiftRight => self.egui_input.modifiers.shift = pressed,
|
||||
Code::ControlLeft | Code::ControlRight => {
|
||||
self.egui_input.modifiers.ctrl = pressed;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
self.egui_input.modifiers.command = pressed;
|
||||
}
|
||||
}
|
||||
Code::AltLeft | Code::AltRight => self.egui_input.modifiers.alt = pressed,
|
||||
Code::MetaLeft | Code::MetaRight => {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
self.egui_input.modifiers.mac_cmd = pressed;
|
||||
self.egui_input.modifiers.command = pressed;
|
||||
}
|
||||
// prevent `rustfmt` from breaking this
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
if let Some(key) = crate::translate::translate_virtual_key(&event.key) {
|
||||
self.egui_input.events.push(egui::Event::Key {
|
||||
key,
|
||||
physical_key: None,
|
||||
pressed,
|
||||
repeat: event.repeat,
|
||||
modifiers: self.egui_input.modifiers,
|
||||
});
|
||||
}
|
||||
|
||||
if pressed {
|
||||
// VirtualKeyCode::Paste etc in winit are broken/untrustworthy,
|
||||
// so we detect these things manually:
|
||||
//
|
||||
// TODO: See if this is an issue in baseview as well.
|
||||
if is_cut_command(self.egui_input.modifiers, event.code) {
|
||||
self.egui_input.events.push(egui::Event::Cut);
|
||||
} else if is_copy_command(self.egui_input.modifiers, event.code) {
|
||||
self.egui_input.events.push(egui::Event::Copy);
|
||||
} else if is_paste_command(self.egui_input.modifiers, event.code) {
|
||||
if let Some(clipboard_ctx) = &mut self.clipboard_ctx {
|
||||
match clipboard_ctx.get_contents() {
|
||||
Ok(contents) => {
|
||||
self.egui_input.events.push(egui::Event::Text(contents))
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Paste error: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let keyboard_types::Key::Character(written) = &event.key {
|
||||
if !self.egui_input.modifiers.ctrl && !self.egui_input.modifiers.command {
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::Text(written.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match &self.key_capture {
|
||||
KeyCapture::CaptureAll => {}
|
||||
KeyCapture::IgnoreAll => return_status = EventStatus::Ignored,
|
||||
KeyCapture::CaptureKeys(keys) => {
|
||||
if !keys.contains(&event.key) {
|
||||
return_status = EventStatus::Ignored
|
||||
}
|
||||
}
|
||||
KeyCapture::IgnoreKeys(keys) => {
|
||||
if keys.contains(&event.key) {
|
||||
return_status = EventStatus::Ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
baseview::Event::Window(event) => match event {
|
||||
baseview::WindowEvent::Resized(window_info) => {
|
||||
self.pixels_per_point = match self.scale_policy {
|
||||
WindowScalePolicy::ScaleFactor(scale) => scale,
|
||||
WindowScalePolicy::SystemScaleFactor => window_info.scale(),
|
||||
} as f32;
|
||||
self.points_per_pixel = self.pixels_per_point.recip();
|
||||
|
||||
self.physical_size = window_info.physical_size();
|
||||
|
||||
let screen_rect =
|
||||
calculate_screen_rect(self.physical_size, self.points_per_pixel);
|
||||
|
||||
self.egui_input.screen_rect = Some(screen_rect);
|
||||
|
||||
let viewport_info = self
|
||||
.egui_input
|
||||
.viewports
|
||||
.get_mut(&self.viewport_id)
|
||||
.unwrap();
|
||||
viewport_info.native_pixels_per_point = Some(self.pixels_per_point);
|
||||
viewport_info.inner_rect = Some(screen_rect);
|
||||
|
||||
// Schedule to repaint on the next frame.
|
||||
self.repaint_after = Some(Instant::now());
|
||||
}
|
||||
baseview::WindowEvent::Focused => {
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::WindowFocused(true));
|
||||
self.egui_input
|
||||
.viewports
|
||||
.get_mut(&self.viewport_id)
|
||||
.unwrap()
|
||||
.focused = Some(true);
|
||||
}
|
||||
baseview::WindowEvent::Unfocused => {
|
||||
self.egui_input
|
||||
.events
|
||||
.push(egui::Event::WindowFocused(false));
|
||||
self.egui_input
|
||||
.viewports
|
||||
.get_mut(&self.viewport_id)
|
||||
.unwrap()
|
||||
.focused = Some(false);
|
||||
}
|
||||
baseview::WindowEvent::WillClose => {}
|
||||
},
|
||||
}
|
||||
|
||||
match &event {
|
||||
baseview::Event::Keyboard(_) => return_status,
|
||||
baseview::Event::Mouse(_) => {
|
||||
if self.egui_ctx.is_using_pointer() || self.egui_ctx.wants_pointer_input() {
|
||||
EventStatus::Captured
|
||||
} else {
|
||||
EventStatus::Ignored
|
||||
}
|
||||
}
|
||||
baseview::Event::Window(_) => EventStatus::Captured,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_cut_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
|
||||
(modifiers.command && keycode == keyboard_types::Code::KeyX)
|
||||
|| (cfg!(target_os = "windows")
|
||||
&& modifiers.shift
|
||||
&& keycode == keyboard_types::Code::Delete)
|
||||
}
|
||||
|
||||
fn is_copy_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
|
||||
(modifiers.command && keycode == keyboard_types::Code::KeyC)
|
||||
|| (cfg!(target_os = "windows")
|
||||
&& modifiers.ctrl
|
||||
&& keycode == keyboard_types::Code::Insert)
|
||||
}
|
||||
|
||||
fn is_paste_command(modifiers: egui::Modifiers, keycode: keyboard_types::Code) -> bool {
|
||||
(modifiers.command && keycode == keyboard_types::Code::KeyV)
|
||||
|| (cfg!(target_os = "windows")
|
||||
&& modifiers.shift
|
||||
&& keycode == keyboard_types::Code::Insert)
|
||||
}
|
||||
|
||||
/// Calculate screen rectangle in logical size.
|
||||
fn calculate_screen_rect(physical_size: PhySize, points_per_pixel: f32) -> Rect {
|
||||
let logical_size = (
|
||||
physical_size.width as f32 * points_per_pixel,
|
||||
physical_size.height as f32 * points_per_pixel,
|
||||
);
|
||||
Rect::from_min_size(Pos2::new(0f32, 0f32), vec2(logical_size.0, logical_size.1))
|
||||
}
|
||||
@@ -60,11 +60,8 @@ pub struct StepContext<'a> {
|
||||
pub cc_access: Option<&'a dyn CcAccess>,
|
||||
pub speed_key: &'a str,
|
||||
pub chain_key: &'a str,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_x: f64,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_y: f64,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_down: f64,
|
||||
}
|
||||
|
||||
|
||||
@@ -662,11 +662,8 @@ impl Forth {
|
||||
"speed" => Value::Float(ctx.speed, None),
|
||||
"stepdur" => Value::Float(ctx.step_duration(), None),
|
||||
"fill" => Value::Int(if ctx.fill { 1 } else { 0 }, None),
|
||||
#[cfg(feature = "desktop")]
|
||||
"mx" => Value::Float(ctx.mouse_x, None),
|
||||
#[cfg(feature = "desktop")]
|
||||
"my" => Value::Float(ctx.mouse_y, None),
|
||||
#[cfg(feature = "desktop")]
|
||||
"mdown" => Value::Float(ctx.mouse_down, None),
|
||||
_ => Value::Int(0, None),
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::commands::AppCommand;
|
||||
use crate::engine::{LinkState, SequencerSnapshot};
|
||||
use crate::services::{dict_nav, euclidean, help_nav, pattern_editor};
|
||||
use crate::state::{undo::UndoEntry, CyclicEnum, FlashKind, Modal, StagedPropChange};
|
||||
use crate::state::{undo::UndoEntry, FlashKind, Modal, StagedPropChange};
|
||||
|
||||
use super::App;
|
||||
|
||||
@@ -344,8 +344,8 @@ impl App {
|
||||
|
||||
// Audio settings (engine page)
|
||||
AppCommand::AudioSetSection(section) => self.audio.section = section,
|
||||
AppCommand::AudioNextSection => self.audio.next_section(),
|
||||
AppCommand::AudioPrevSection => self.audio.prev_section(),
|
||||
AppCommand::AudioNextSection => self.audio.next_section(self.plugin_mode),
|
||||
AppCommand::AudioPrevSection => self.audio.prev_section(self.plugin_mode),
|
||||
AppCommand::AudioOutputListUp => self.audio.output_list.move_up(),
|
||||
AppCommand::AudioOutputListDown(count) => self.audio.output_list.move_down(count),
|
||||
AppCommand::AudioOutputPageUp => self.audio.output_list.page_up(),
|
||||
@@ -353,8 +353,8 @@ impl App {
|
||||
AppCommand::AudioInputListUp => self.audio.input_list.move_up(),
|
||||
AppCommand::AudioInputListDown(count) => self.audio.input_list.move_down(count),
|
||||
AppCommand::AudioInputPageDown(count) => self.audio.input_list.page_down(count),
|
||||
AppCommand::AudioSettingNext => self.audio.setting_kind = self.audio.setting_kind.next(),
|
||||
AppCommand::AudioSettingPrev => self.audio.setting_kind = self.audio.setting_kind.prev(),
|
||||
AppCommand::AudioSettingNext => self.audio.next_setting(self.plugin_mode),
|
||||
AppCommand::AudioSettingPrev => self.audio.prev_setting(self.plugin_mode),
|
||||
AppCommand::SetOutputDevice(name) => self.audio.config.output_device = Some(name),
|
||||
AppCommand::SetInputDevice(name) => self.audio.config.input_device = Some(name),
|
||||
AppCommand::SetDeviceKind(kind) => self.audio.device_kind = kind,
|
||||
|
||||
@@ -69,6 +69,7 @@ pub struct App {
|
||||
pub options: OptionsState,
|
||||
pub panel: PanelState,
|
||||
pub midi: MidiState,
|
||||
pub plugin_mode: bool,
|
||||
}
|
||||
|
||||
impl Default for App {
|
||||
@@ -82,6 +83,15 @@ impl App {
|
||||
let variables = Arc::new(ArcSwap::from_pointee(HashMap::new()));
|
||||
let dict = Arc::new(Mutex::new(HashMap::new()));
|
||||
let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
|
||||
Self::build(variables, dict, rng)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn with_shared(variables: Variables, dict: Dictionary, rng: Rng) -> Self {
|
||||
Self::build(variables, dict, rng)
|
||||
}
|
||||
|
||||
fn build(variables: Variables, dict: Dictionary, rng: Rng) -> Self {
|
||||
let script_engine =
|
||||
ScriptEngine::new(Arc::clone(&variables), Arc::clone(&dict), Arc::clone(&rng));
|
||||
let live_keys = Arc::new(LiveKeyState::new());
|
||||
@@ -113,6 +123,7 @@ impl App {
|
||||
options: OptionsState::default(),
|
||||
panel: PanelState::default(),
|
||||
midi: MidiState::new(),
|
||||
plugin_mode: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,11 +32,8 @@ impl App {
|
||||
cc_access: None,
|
||||
speed_key: "",
|
||||
chain_key: "",
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,10 @@ impl App {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn flush_dirty_patterns(&mut self, cmd_tx: &Sender<SeqCommand>) {
|
||||
for (bank, pattern) in self.project_state.take_dirty() {
|
||||
pub fn flush_dirty_patterns(&mut self, cmd_tx: &Sender<SeqCommand>) -> bool {
|
||||
let dirty = self.project_state.take_dirty();
|
||||
let had_dirty = !dirty.is_empty();
|
||||
for (bank, pattern) in dirty {
|
||||
let pat = self.project_state.project.pattern_at(bank, pattern);
|
||||
let snapshot = PatternSnapshot {
|
||||
speed: pat.speed,
|
||||
@@ -59,5 +61,6 @@ impl App {
|
||||
data: snapshot,
|
||||
});
|
||||
}
|
||||
had_dirty
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,14 @@ pub use timing::{substeps_in_window, StepTiming, SyncTime};
|
||||
|
||||
// AnalysisHandle and SequencerHandle are used by src/bin/desktop.rs
|
||||
#[allow(unused_imports)]
|
||||
pub use audio::{build_stream, AnalysisHandle, AudioStreamConfig, ScopeBuffer, SpectrumBuffer};
|
||||
pub use audio::{
|
||||
build_stream, spawn_analysis_thread, AnalysisHandle, AudioStreamConfig, ScopeBuffer,
|
||||
SpectrumBuffer,
|
||||
};
|
||||
pub use link::LinkState;
|
||||
#[allow(unused_imports)]
|
||||
pub use sequencer::{
|
||||
spawn_sequencer, AudioCommand, MidiCommand, PatternChange, PatternSnapshot, SeqCommand,
|
||||
SequencerConfig, SequencerHandle, SequencerSnapshot, StepSnapshot,
|
||||
parse_midi_command, spawn_sequencer, AudioCommand, MidiCommand, PatternChange,
|
||||
PatternSnapshot, SeqCommand, SequencerConfig, SequencerHandle, SequencerSnapshot,
|
||||
SequencerState, SharedSequencerState, StepSnapshot, TickInput, TickOutput, TimestampedCommand,
|
||||
};
|
||||
|
||||
@@ -166,7 +166,26 @@ pub struct SequencerSnapshot {
|
||||
pub event_count: usize,
|
||||
}
|
||||
|
||||
impl From<&SharedSequencerState> for SequencerSnapshot {
|
||||
fn from(s: &SharedSequencerState) -> Self {
|
||||
Self {
|
||||
active_patterns: s.active_patterns.clone(),
|
||||
step_traces: Arc::clone(&s.step_traces),
|
||||
event_count: s.event_count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SequencerSnapshot {
|
||||
#[allow(dead_code)]
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
active_patterns: Vec::new(),
|
||||
step_traces: Arc::new(HashMap::new()),
|
||||
event_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_playing(&self, bank: usize, pattern: usize) -> bool {
|
||||
self.active_patterns
|
||||
.iter()
|
||||
@@ -452,7 +471,7 @@ impl RunsCounter {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TickInput {
|
||||
pub struct TickInput {
|
||||
pub commands: Vec<SeqCommand>,
|
||||
pub playing: bool,
|
||||
pub beat: f64,
|
||||
@@ -463,11 +482,8 @@ pub(crate) struct TickInput {
|
||||
pub nudge_secs: f64,
|
||||
pub current_time_us: SyncTime,
|
||||
pub engine_time: f64,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_x: f64,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_y: f64,
|
||||
#[cfg(feature = "desktop")]
|
||||
pub mouse_down: f64,
|
||||
}
|
||||
|
||||
@@ -476,7 +492,7 @@ pub struct TimestampedCommand {
|
||||
pub time: Option<f64>,
|
||||
}
|
||||
|
||||
pub(crate) struct TickOutput {
|
||||
pub struct TickOutput {
|
||||
pub audio_commands: Vec<TimestampedCommand>,
|
||||
pub new_tempo: Option<f64>,
|
||||
pub shared_state: SharedSequencerState,
|
||||
@@ -528,7 +544,7 @@ fn format_chain_key(buf: &mut String, bank: usize, pattern: usize) -> &str {
|
||||
buf
|
||||
}
|
||||
|
||||
pub(crate) struct SequencerState {
|
||||
pub struct SequencerState {
|
||||
audio_state: AudioState,
|
||||
pattern_cache: PatternCache,
|
||||
pending_updates: HashMap<(usize, usize), PatternSnapshot>,
|
||||
@@ -710,11 +726,8 @@ impl SequencerState {
|
||||
input.nudge_secs,
|
||||
input.current_time_us,
|
||||
input.engine_time,
|
||||
#[cfg(feature = "desktop")]
|
||||
input.mouse_x,
|
||||
#[cfg(feature = "desktop")]
|
||||
input.mouse_y,
|
||||
#[cfg(feature = "desktop")]
|
||||
input.mouse_down,
|
||||
);
|
||||
|
||||
@@ -842,9 +855,9 @@ impl SequencerState {
|
||||
nudge_secs: f64,
|
||||
_current_time_us: SyncTime,
|
||||
engine_time: f64,
|
||||
#[cfg(feature = "desktop")] mouse_x: f64,
|
||||
#[cfg(feature = "desktop")] mouse_y: f64,
|
||||
#[cfg(feature = "desktop")] mouse_down: f64,
|
||||
mouse_x: f64,
|
||||
mouse_y: f64,
|
||||
mouse_down: f64,
|
||||
) -> StepResult {
|
||||
self.buf_audio_commands.clear();
|
||||
self.buf_completed_iterations.clear();
|
||||
@@ -924,11 +937,8 @@ impl SequencerState {
|
||||
cc_access: self.cc_access.as_deref(),
|
||||
speed_key,
|
||||
chain_key,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down,
|
||||
};
|
||||
if let Some(script) = resolved_script {
|
||||
@@ -1170,10 +1180,16 @@ fn sequencer_loop(
|
||||
engine_time,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: f32::from_bits(mouse_x.load(Ordering::Relaxed)) as f64,
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
mouse_x: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y: f32::from_bits(mouse_y.load(Ordering::Relaxed)) as f64,
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
mouse_y: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down: f32::from_bits(mouse_down.load(Ordering::Relaxed)) as f64,
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
mouse_down: 0.0,
|
||||
};
|
||||
|
||||
let output = seq_state.tick(input);
|
||||
@@ -1231,7 +1247,7 @@ fn sequencer_loop(
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>, f64)> {
|
||||
pub fn parse_midi_command(cmd: &str) -> Option<(MidiCommand, Option<f64>, f64)> {
|
||||
if !cmd.starts_with("/midi/") {
|
||||
return None;
|
||||
}
|
||||
@@ -1384,11 +1400,8 @@ mod tests {
|
||||
nudge_secs: 0.0,
|
||||
current_time_us: 0,
|
||||
engine_time: 0.0,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down: 0.0,
|
||||
}
|
||||
}
|
||||
@@ -1405,11 +1418,8 @@ mod tests {
|
||||
nudge_secs: 0.0,
|
||||
current_time_us: 0,
|
||||
engine_time: 0.0,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ pub struct InitArgs {
|
||||
pub buffer: Option<u32>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct Init {
|
||||
pub app: App,
|
||||
pub link: Arc<LinkState>,
|
||||
|
||||
@@ -33,7 +33,7 @@ pub(crate) fn cycle_engine_setting(ctx: &mut InputContext, right: bool) {
|
||||
|
||||
pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
KeyCode::Char('q') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
@@ -42,17 +42,17 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
KeyCode::Tab => ctx.dispatch(AppCommand::AudioNextSection),
|
||||
KeyCode::BackTab => ctx.dispatch(AppCommand::AudioPrevSection),
|
||||
KeyCode::Up => match ctx.app.audio.section {
|
||||
EngineSection::Devices => match ctx.app.audio.device_kind {
|
||||
EngineSection::Devices if !ctx.app.plugin_mode => match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => ctx.dispatch(AppCommand::AudioOutputListUp),
|
||||
DeviceKind::Input => ctx.dispatch(AppCommand::AudioInputListUp),
|
||||
},
|
||||
EngineSection::Settings => {
|
||||
ctx.dispatch(AppCommand::AudioSettingPrev);
|
||||
}
|
||||
EngineSection::Samples => {}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Down => match ctx.app.audio.section {
|
||||
EngineSection::Devices => match ctx.app.audio.device_kind {
|
||||
EngineSection::Devices if !ctx.app.plugin_mode => match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => {
|
||||
let count = ctx.app.audio.output_devices.len();
|
||||
ctx.dispatch(AppCommand::AudioOutputListDown(count));
|
||||
@@ -65,10 +65,10 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
EngineSection::Settings => {
|
||||
ctx.dispatch(AppCommand::AudioSettingNext);
|
||||
}
|
||||
EngineSection::Samples => {}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::PageUp => {
|
||||
if ctx.app.audio.section == EngineSection::Devices {
|
||||
if !ctx.app.plugin_mode && ctx.app.audio.section == EngineSection::Devices {
|
||||
match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => ctx.dispatch(AppCommand::AudioOutputPageUp),
|
||||
DeviceKind::Input => ctx.app.audio.input_list.page_up(),
|
||||
@@ -76,7 +76,7 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
}
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
if ctx.app.audio.section == EngineSection::Devices {
|
||||
if !ctx.app.plugin_mode && ctx.app.audio.section == EngineSection::Devices {
|
||||
match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => {
|
||||
let count = ctx.app.audio.output_devices.len();
|
||||
@@ -90,7 +90,7 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
}
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
if ctx.app.audio.section == EngineSection::Devices {
|
||||
if !ctx.app.plugin_mode && ctx.app.audio.section == EngineSection::Devices {
|
||||
match ctx.app.audio.device_kind {
|
||||
DeviceKind::Output => {
|
||||
let cursor = ctx.app.audio.output_list.cursor;
|
||||
@@ -112,20 +112,22 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
}
|
||||
}
|
||||
KeyCode::Left => match ctx.app.audio.section {
|
||||
EngineSection::Devices => {
|
||||
EngineSection::Devices if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::SetDeviceKind(DeviceKind::Output));
|
||||
}
|
||||
EngineSection::Settings => cycle_engine_setting(ctx, false),
|
||||
EngineSection::Samples => {}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Right => match ctx.app.audio.section {
|
||||
EngineSection::Devices => {
|
||||
EngineSection::Devices if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::SetDeviceKind(DeviceKind::Input));
|
||||
}
|
||||
EngineSection::Settings => cycle_engine_setting(ctx, true),
|
||||
EngineSection::Samples => {}
|
||||
_ => {}
|
||||
},
|
||||
KeyCode::Char('R') => ctx.dispatch(AppCommand::AudioTriggerRestart),
|
||||
KeyCode::Char('R') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::AudioTriggerRestart);
|
||||
}
|
||||
KeyCode::Char('A') => {
|
||||
use crate::state::file_browser::FileBrowserState;
|
||||
let state = FileBrowserState::new_load(String::new());
|
||||
@@ -134,7 +136,7 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
KeyCode::Char('D') => {
|
||||
if ctx.app.audio.section == EngineSection::Samples {
|
||||
ctx.dispatch(AppCommand::RemoveLastSamplePath);
|
||||
} else {
|
||||
} else if !ctx.app.plugin_mode {
|
||||
ctx.dispatch(AppCommand::AudioRefreshDevices);
|
||||
let out_count = ctx.app.audio.output_devices.len();
|
||||
let in_count = ctx.app.audio.input_devices.len();
|
||||
@@ -144,15 +146,19 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
}
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Hush);
|
||||
if !ctx.app.plugin_mode {
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Hush);
|
||||
}
|
||||
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
|
||||
}
|
||||
KeyCode::Char('p') => {
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Panic);
|
||||
if !ctx.app.plugin_mode {
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Panic);
|
||||
}
|
||||
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
|
||||
}
|
||||
KeyCode::Char('r') => ctx.dispatch(AppCommand::ResetPeakVoices),
|
||||
KeyCode::Char('t') => {
|
||||
KeyCode::Char('t') if !ctx.app.plugin_mode => {
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Evaluate {
|
||||
cmd: "/sound/sine/dur/0.5/decay/0.2".into(),
|
||||
time: None,
|
||||
|
||||
@@ -66,7 +66,7 @@ pub(super) fn handle_help_page(ctx: &mut InputContext, key: KeyEvent) -> InputRe
|
||||
},
|
||||
KeyCode::PageDown => ctx.dispatch(AppCommand::HelpScrollDown(10)),
|
||||
KeyCode::PageUp => ctx.dispatch(AppCommand::HelpScrollUp(10)),
|
||||
KeyCode::Char('q') => {
|
||||
KeyCode::Char('q') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
@@ -236,7 +236,7 @@ pub(super) fn handle_dict_page(ctx: &mut InputContext, key: KeyEvent) -> InputRe
|
||||
},
|
||||
KeyCode::PageDown => ctx.dispatch(AppCommand::DictScrollDown(10)),
|
||||
KeyCode::PageUp => ctx.dispatch(AppCommand::DictScrollUp(10)),
|
||||
KeyCode::Char('q') => {
|
||||
KeyCode::Char('q') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
|
||||
@@ -23,7 +23,7 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
|
||||
ctx.app.panel.focus = PanelFocus::Side;
|
||||
}
|
||||
}
|
||||
KeyCode::Char('q') => {
|
||||
KeyCode::Char('q') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
|
||||
@@ -434,7 +434,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
}
|
||||
}
|
||||
Modal::KeybindingsHelp { scroll } => {
|
||||
let bindings_count = crate::views::keybindings::bindings_for(ctx.app.page).len();
|
||||
let bindings_count = crate::views::keybindings::bindings_for(ctx.app.page, ctx.app.plugin_mode).len();
|
||||
match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('?') => ctx.dispatch(AppCommand::CloseModal),
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
|
||||
@@ -149,7 +149,7 @@ pub(crate) fn cycle_option_value(ctx: &mut InputContext, right: bool) {
|
||||
|
||||
pub(super) fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => {
|
||||
KeyCode::Char('q') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
|
||||
@@ -95,7 +95,7 @@ pub(super) fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> Inp
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('q') => {
|
||||
KeyCode::Char('q') if !ctx.app.plugin_mode => {
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Confirm {
|
||||
action: ConfirmAction::Quit,
|
||||
selected: false,
|
||||
|
||||
@@ -61,11 +61,8 @@ pub fn update_cache(editor_ctx: &EditorContext) {
|
||||
cc_access: None,
|
||||
speed_key: "",
|
||||
chain_key: "",
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_x: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_y: 0.5,
|
||||
#[cfg(feature = "desktop")]
|
||||
mouse_down: 0.0,
|
||||
};
|
||||
|
||||
|
||||
@@ -258,12 +258,52 @@ impl AudioSettings {
|
||||
self.input_devices = doux::audio::list_input_devices();
|
||||
}
|
||||
|
||||
pub fn next_section(&mut self) {
|
||||
self.section = self.section.next();
|
||||
pub fn next_section(&mut self, plugin_mode: bool) {
|
||||
self.section = if plugin_mode {
|
||||
match self.section {
|
||||
EngineSection::Settings => EngineSection::Samples,
|
||||
EngineSection::Samples => EngineSection::Settings,
|
||||
EngineSection::Devices => EngineSection::Settings,
|
||||
}
|
||||
} else {
|
||||
self.section.next()
|
||||
};
|
||||
}
|
||||
|
||||
pub fn prev_section(&mut self) {
|
||||
self.section = self.section.prev();
|
||||
pub fn prev_section(&mut self, plugin_mode: bool) {
|
||||
self.section = if plugin_mode {
|
||||
match self.section {
|
||||
EngineSection::Settings => EngineSection::Samples,
|
||||
EngineSection::Samples => EngineSection::Settings,
|
||||
EngineSection::Devices => EngineSection::Settings,
|
||||
}
|
||||
} else {
|
||||
self.section.prev()
|
||||
};
|
||||
}
|
||||
|
||||
pub fn next_setting(&mut self, plugin_mode: bool) {
|
||||
self.setting_kind = if plugin_mode {
|
||||
match self.setting_kind {
|
||||
SettingKind::Polyphony => SettingKind::Nudge,
|
||||
SettingKind::Nudge => SettingKind::Polyphony,
|
||||
_ => SettingKind::Polyphony,
|
||||
}
|
||||
} else {
|
||||
self.setting_kind.next()
|
||||
};
|
||||
}
|
||||
|
||||
pub fn prev_setting(&mut self, plugin_mode: bool) {
|
||||
self.setting_kind = if plugin_mode {
|
||||
match self.setting_kind {
|
||||
SettingKind::Polyphony => SettingKind::Nudge,
|
||||
SettingKind::Nudge => SettingKind::Polyphony,
|
||||
_ => SettingKind::Polyphony,
|
||||
}
|
||||
} else {
|
||||
self.setting_kind.prev()
|
||||
};
|
||||
}
|
||||
|
||||
pub fn current_output_device_index(&self) -> usize {
|
||||
|
||||
@@ -46,24 +46,31 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
|
||||
};
|
||||
|
||||
// Calculate section heights
|
||||
let devices_lines = devices_section_height(app) as usize;
|
||||
let settings_lines: usize = 8; // header(1) + divider(1) + 6 rows
|
||||
let plugin_mode = app.plugin_mode;
|
||||
let devices_lines = if plugin_mode {
|
||||
0
|
||||
} else {
|
||||
devices_section_height(app) as usize
|
||||
};
|
||||
let settings_lines: usize = if plugin_mode { 5 } else { 8 }; // plugin: header(1) + divider(1) + 3 rows
|
||||
let samples_lines: usize = 6; // header(1) + divider(1) + content(3) + hint(1)
|
||||
let total_lines = devices_lines + 1 + settings_lines + 1 + samples_lines;
|
||||
|
||||
let sections_gap = if plugin_mode { 1 } else { 2 }; // 1 gap without devices, 2 gaps with
|
||||
let total_lines = devices_lines + settings_lines + samples_lines + sections_gap;
|
||||
|
||||
let max_visible = padded.height as usize;
|
||||
|
||||
// Calculate scroll offset based on focused section
|
||||
let settings_start = if plugin_mode { 0 } else { devices_lines + 1 };
|
||||
let (focus_start, focus_height) = match app.audio.section {
|
||||
EngineSection::Devices => (0, devices_lines),
|
||||
EngineSection::Settings => (devices_lines + 1, settings_lines),
|
||||
EngineSection::Samples => (devices_lines + 1 + settings_lines + 1, samples_lines),
|
||||
EngineSection::Settings => (settings_start, settings_lines),
|
||||
EngineSection::Samples => (settings_start + settings_lines + 1, samples_lines),
|
||||
};
|
||||
|
||||
let scroll_offset = if total_lines <= max_visible {
|
||||
0
|
||||
} else {
|
||||
// Keep focused section in view (top-aligned when possible)
|
||||
let focus_end = focus_start + focus_height;
|
||||
if focus_end <= max_visible {
|
||||
0
|
||||
@@ -75,25 +82,26 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let viewport_top = padded.y as i32;
|
||||
let viewport_bottom = (padded.y + padded.height) as i32;
|
||||
|
||||
// Render each section at adjusted position
|
||||
let mut y = viewport_top - scroll_offset as i32;
|
||||
|
||||
// Devices section
|
||||
let devices_top = y;
|
||||
let devices_bottom = y + devices_lines as i32;
|
||||
if devices_bottom > viewport_top && devices_top < viewport_bottom {
|
||||
let clipped_y = devices_top.max(viewport_top) as u16;
|
||||
let clipped_height =
|
||||
(devices_bottom.min(viewport_bottom) - devices_top.max(viewport_top)) as u16;
|
||||
let devices_area = Rect {
|
||||
x: padded.x,
|
||||
y: clipped_y,
|
||||
width: padded.width,
|
||||
height: clipped_height,
|
||||
};
|
||||
render_devices(frame, app, devices_area);
|
||||
// Devices section (skip in plugin mode)
|
||||
if !plugin_mode {
|
||||
let devices_top = y;
|
||||
let devices_bottom = y + devices_lines as i32;
|
||||
if devices_bottom > viewport_top && devices_top < viewport_bottom {
|
||||
let clipped_y = devices_top.max(viewport_top) as u16;
|
||||
let clipped_height =
|
||||
(devices_bottom.min(viewport_bottom) - devices_top.max(viewport_top)) as u16;
|
||||
let devices_area = Rect {
|
||||
x: padded.x,
|
||||
y: clipped_y,
|
||||
width: padded.width,
|
||||
height: clipped_height,
|
||||
};
|
||||
render_devices(frame, app, devices_area);
|
||||
}
|
||||
y += devices_lines as i32 + 1;
|
||||
}
|
||||
y += devices_lines as i32 + 1; // +1 for blank line
|
||||
|
||||
// Settings section
|
||||
let settings_top = y;
|
||||
@@ -310,8 +318,6 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let label_style = Style::new().fg(theme.engine.label);
|
||||
let value_style = Style::new().fg(theme.engine.value);
|
||||
|
||||
let channels_focused = section_focused && app.audio.setting_kind == SettingKind::Channels;
|
||||
let buffer_focused = section_focused && app.audio.setting_kind == SettingKind::BufferSize;
|
||||
let polyphony_focused = section_focused && app.audio.setting_kind == SettingKind::Polyphony;
|
||||
let nudge_focused = section_focused && app.audio.setting_kind == SettingKind::Nudge;
|
||||
let nudge_ms = app.metrics.nudge_ms;
|
||||
@@ -321,8 +327,15 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
|
||||
format!("{nudge_ms:+.1} ms")
|
||||
};
|
||||
|
||||
let rows = vec![
|
||||
Row::new(vec![
|
||||
let mut rows = Vec::new();
|
||||
|
||||
if !app.plugin_mode {
|
||||
let channels_focused =
|
||||
section_focused && app.audio.setting_kind == SettingKind::Channels;
|
||||
let buffer_focused =
|
||||
section_focused && app.audio.setting_kind == SettingKind::BufferSize;
|
||||
|
||||
rows.push(Row::new(vec![
|
||||
Span::styled(
|
||||
if channels_focused {
|
||||
"> Channels"
|
||||
@@ -337,8 +350,8 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
]));
|
||||
rows.push(Row::new(vec![
|
||||
Span::styled(
|
||||
if buffer_focused {
|
||||
"> Buffer"
|
||||
@@ -357,38 +370,42 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled(
|
||||
if polyphony_focused {
|
||||
"> Voices"
|
||||
} else {
|
||||
" Voices"
|
||||
},
|
||||
label_style,
|
||||
),
|
||||
render_selector(
|
||||
&format!("{}", app.audio.config.max_voices),
|
||||
polyphony_focused,
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled(
|
||||
if nudge_focused { "> Nudge" } else { " Nudge" },
|
||||
label_style,
|
||||
),
|
||||
render_selector(&nudge_label, nudge_focused, highlight, normal),
|
||||
]),
|
||||
Row::new(vec![
|
||||
Span::styled(" Sample rate", label_style),
|
||||
Span::styled(
|
||||
format!("{:.0} Hz", app.audio.config.sample_rate),
|
||||
value_style,
|
||||
),
|
||||
]),
|
||||
Row::new(vec![
|
||||
]));
|
||||
}
|
||||
|
||||
rows.push(Row::new(vec![
|
||||
Span::styled(
|
||||
if polyphony_focused {
|
||||
"> Voices"
|
||||
} else {
|
||||
" Voices"
|
||||
},
|
||||
label_style,
|
||||
),
|
||||
render_selector(
|
||||
&format!("{}", app.audio.config.max_voices),
|
||||
polyphony_focused,
|
||||
highlight,
|
||||
normal,
|
||||
),
|
||||
]));
|
||||
rows.push(Row::new(vec![
|
||||
Span::styled(
|
||||
if nudge_focused { "> Nudge" } else { " Nudge" },
|
||||
label_style,
|
||||
),
|
||||
render_selector(&nudge_label, nudge_focused, highlight, normal),
|
||||
]));
|
||||
rows.push(Row::new(vec![
|
||||
Span::styled(" Sample rate", label_style),
|
||||
Span::styled(
|
||||
format!("{:.0} Hz", app.audio.config.sample_rate),
|
||||
value_style,
|
||||
),
|
||||
]));
|
||||
|
||||
if !app.plugin_mode {
|
||||
rows.push(Row::new(vec![
|
||||
Span::styled(" Audio host", label_style),
|
||||
Span::styled(
|
||||
if app.audio.config.host_name.is_empty() {
|
||||
@@ -398,8 +415,8 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
|
||||
},
|
||||
value_style,
|
||||
),
|
||||
]),
|
||||
];
|
||||
]));
|
||||
}
|
||||
|
||||
let table = Table::new(rows, [Constraint::Length(14), Constraint::Fill(1)]);
|
||||
frame.render_widget(table, content_area);
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
use crate::page::Page;
|
||||
|
||||
pub fn bindings_for(page: Page) -> Vec<(&'static str, &'static str, &'static str)> {
|
||||
pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'static str, &'static str)> {
|
||||
let mut bindings = vec![
|
||||
("F1–F6", "Go to view", "Dict/Patterns/Options/Help/Sequencer/Engine"),
|
||||
("Ctrl+←→↑↓", "Navigate", "Switch between adjacent views"),
|
||||
("q", "Quit", "Quit application"),
|
||||
];
|
||||
if !plugin_mode {
|
||||
bindings.push(("q", "Quit", "Quit application"));
|
||||
}
|
||||
bindings.extend([
|
||||
("s", "Save", "Save project"),
|
||||
("l", "Load", "Load project"),
|
||||
("?", "Keybindings", "Show this help"),
|
||||
];
|
||||
]);
|
||||
|
||||
// Page-specific bindings
|
||||
match page {
|
||||
|
||||
@@ -78,7 +78,7 @@ pub fn render(
|
||||
frame.render_widget(Block::new().style(Style::default().bg(bg_color)), term);
|
||||
|
||||
if app.ui.show_title {
|
||||
title_view::render(frame, term, &app.ui);
|
||||
title_view::render(frame, term, &app.ui, app.plugin_mode);
|
||||
|
||||
let mut fx = app.ui.title_fx.borrow_mut();
|
||||
if let Some(effect) = fx.as_mut() {
|
||||
@@ -1036,7 +1036,7 @@ fn render_modal_keybindings(frame: &mut Frame, app: &App, scroll: usize, term: R
|
||||
.border_color(theme.modal.editor)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let bindings = super::keybindings::bindings_for(app.page);
|
||||
let bindings = super::keybindings::bindings_for(app.page, app.plugin_mode);
|
||||
let visible_rows = inner.height.saturating_sub(2) as usize;
|
||||
|
||||
let rows: Vec<Row> = bindings
|
||||
|
||||
@@ -8,7 +8,7 @@ use tui_big_text::{BigText, PixelSize};
|
||||
use crate::state::ui::UiState;
|
||||
use crate::theme;
|
||||
|
||||
pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) {
|
||||
pub fn render(frame: &mut Frame, area: Rect, ui: &UiState, plugin_mode: bool) {
|
||||
let theme = theme::get();
|
||||
frame.render_widget(&ui.sparkles, area);
|
||||
|
||||
@@ -39,15 +39,17 @@ pub fn render(frame: &mut Frame, area: Rect, ui: &UiState) {
|
||||
Line::from(Span::styled("AGPL-3.0", license_style)),
|
||||
];
|
||||
|
||||
let keybindings = [
|
||||
let mut keybindings = vec![
|
||||
("Ctrl+Arrows", "Navigate Views"),
|
||||
("Enter", "Edit Step"),
|
||||
("Space", "Play/Stop"),
|
||||
("s", "Save"),
|
||||
("l", "Load"),
|
||||
("q", "Quit"),
|
||||
("?", "Keybindings"),
|
||||
];
|
||||
if !plugin_mode {
|
||||
keybindings.push(("q", "Quit"));
|
||||
}
|
||||
keybindings.push(("?", "Keybindings"));
|
||||
|
||||
let key_style = Style::new().fg(theme.modal.confirm);
|
||||
let desc_style = Style::new().fg(theme.ui.text_primary);
|
||||
|
||||
@@ -23,6 +23,9 @@ pub fn default_ctx() -> StepContext<'static> {
|
||||
cc_access: None,
|
||||
speed_key: "__speed_0_0__",
|
||||
chain_key: "__chain_0_0__",
|
||||
mouse_x: 0.5,
|
||||
mouse_y: 0.5,
|
||||
mouse_down: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
7
xtask/Cargo.toml
Normal file
7
xtask/Cargo.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "xtask"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
nih_plug_xtask = { git = "https://github.com/robbert-vdh/nih-plug" }
|
||||
3
xtask/src/main.rs
Normal file
3
xtask/src/main.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() -> nih_plug_xtask::Result<()> {
|
||||
nih_plug_xtask::main()
|
||||
}
|
||||
Reference in New Issue
Block a user