WIP: clap

This commit is contained in:
2026-02-20 22:14:21 +01:00
parent bf361d3ab9
commit 6216b9341b
76 changed files with 9103 additions and 143 deletions

1
.cargo/baseview Submodule

Submodule .cargo/baseview added at 237d323c72

1
.cargo/egui-baseview Submodule

Submodule .cargo/egui-baseview added at c5ad2f4bd0

View File

@@ -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
View File

@@ -3,6 +3,9 @@ Cargo.lock
*.prof
.DS_Store
# Cargo config
.cargo/config.toml
# Claude
.claude/
CLAUDE.md

View File

@@ -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"

View 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"

View 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

View 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.

View 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)
}

View 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),
}

View 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];
}
}
}

View 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);
}
}

View 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);
}
}
}

View 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);
}
}
}

View 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 {}

View 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,
}
}

View 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::*;

View 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
}

View 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::*;

View 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
}));
}
}
}

View 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);
}
}

View 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,
}

View 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,
}
}

View 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
}
}

View 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
}

View 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,
}
}
}

View File

@@ -0,0 +1,7 @@
mod cursor;
mod drop_target;
mod hook;
mod keyboard;
mod window;
pub use window::*;

View 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!()
}

View 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()
}
}

View 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(),
}
}
}

View 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>,
}

View 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))
}
}

View 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),
}
}

View 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 }
}

View 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;

View 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
}

View 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!()
}

View 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
View 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
View 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(&params),
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();
}
}
});
});
});
},
)
}

View 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
View 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
View 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
View 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 }),
}
}
}

View 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"

View 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.

View 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;

View File

@@ -0,0 +1,4 @@
#[cfg(feature = "opengl")]
mod opengl;
#[cfg(feature = "opengl")]
pub use opengl::renderer::{GraphicsConfig, Renderer};

View 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),
}

View 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()
}
}

View 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,
}
}

View 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))
}

View File

@@ -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,
}

View File

@@ -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),
};

View File

@@ -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,

View File

@@ -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,
}
}

View File

@@ -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,
}
}

View File

@@ -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
}
}

View File

@@ -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,
};

View File

@@ -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,
}
}

View File

@@ -25,6 +25,7 @@ pub struct InitArgs {
pub buffer: Option<u32>,
}
#[allow(dead_code)]
pub struct Init {
pub app: App,
pub link: Arc<LinkState>,

View File

@@ -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') => {
if !ctx.app.plugin_mode {
let _ = ctx.audio_tx.load().send(AudioCommand::Hush);
}
let _ = ctx.seq_cmd_tx.send(SeqCommand::StopAll);
}
KeyCode::Char('p') => {
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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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') => {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
};

View File

@@ -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 {

View File

@@ -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,10 +82,10 @@ 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
// 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 {
@@ -93,7 +100,8 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
};
render_devices(frame, app, devices_area);
}
y += devices_lines as i32 + 1; // +1 for blank line
y += devices_lines as i32 + 1;
}
// 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,8 +370,10 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
highlight,
normal,
),
]),
Row::new(vec![
]));
}
rows.push(Row::new(vec![
Span::styled(
if polyphony_focused {
"> Voices"
@@ -373,22 +388,24 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
highlight,
normal,
),
]),
Row::new(vec![
]));
rows.push(Row::new(vec![
Span::styled(
if nudge_focused { "> Nudge" } else { " Nudge" },
label_style,
),
render_selector(&nudge_label, nudge_focused, highlight, normal),
]),
Row::new(vec![
]));
rows.push(Row::new(vec![
Span::styled(" Sample rate", label_style),
Span::styled(
format!("{:.0} Hz", app.audio.config.sample_rate),
value_style,
),
]),
Row::new(vec![
]));
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);

View File

@@ -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![
("F1F6", "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 {

View File

@@ -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

View File

@@ -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);

View File

@@ -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
View 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
View File

@@ -0,0 +1,3 @@
fn main() -> nih_plug_xtask::Result<()> {
nih_plug_xtask::main()
}