Integrate native Rime and stabilize input, output, and rendering
- Add optional modifier-only Super binding whose tap is cancelled by other input, focus changes, grabs, locks, and session teardown. - Ship the native ferese-rime user service, Rime defaults, installer inventory, package dependencies, session environment, and docs. - Vendor Smithay 8a27023 with the SHM texture-cache format fix and add GPU regressions for format changes and resize snapshots. - Release reused CRTC owners before staging output modes, then restore changed outputs before former owners when a transaction fails. - Wait for resize-capture GPU completion, expand filtered surface damage, and preserve IME popup origin under fractional scaling. - Mark weekends like holidays in both calendar layouts.
This commit is contained in:
1 parent
a2be421974
commit
993bc0f88f
391 files changed
+130908
-76
No files matched your search
Generated
+1
-1
@@ -2073,6 +2073,7 @@ dependencies = [
|
||||
"fff-search",
|
||||
"freedesktop-desktop-entry",
|
||||
"iced_accessibility",
|
||||
"iced_winit",
|
||||
"image",
|
||||
"jiff",
|
||||
"libc",
|
||||
@@ -6119,7 +6120,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "smithay"
|
||||
version = "0.7.0"
|
||||
source = "git+https://github.com/Smithay/smithay?rev=8a270232f1bfc416c781273c98a3f52a812a0bba#8a270232f1bfc416c781273c98a3f52a812a0bba"
|
||||
dependencies = [
|
||||
"appendlist",
|
||||
"bitflags 2.13.2",
|
||||
|
||||
@@ -21,6 +21,7 @@ members = [
|
||||
"portal",
|
||||
]
|
||||
resolver = "3"
|
||||
exclude = ["vendor/smithay"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
@@ -72,3 +73,6 @@ wayland-client = "0.31.15"
|
||||
wayland-scanner = "0.31.11"
|
||||
wayland-server = "0.31.14"
|
||||
wayland-protocols-wlr = { version = "0.3.12", features = ["client"] }
|
||||
|
||||
[patch."https://github.com/Smithay/smithay"]
|
||||
smithay = { path = "vendor/smithay" }
|
||||
@@ -31,7 +31,7 @@ use smithay::backend::drm::compositor::{DrmCompositor, FrameFlags, PrimaryPlaneE
|
||||
use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
|
||||
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, NodeType};
|
||||
use smithay::backend::egl::EGLDisplay;
|
||||
use smithay::backend::input::InputEvent;
|
||||
use smithay::backend::input::{Device as _, InputEvent};
|
||||
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
|
||||
use smithay::backend::renderer::element::{RenderElement, UnderlyingStorage};
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||
@@ -425,6 +425,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
}
|
||||
|
||||
InputEvent::DeviceRemoved { device } => {
|
||||
state.super_tap.device_removed(&device.id());
|
||||
state.cancel_window_interaction();
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
backend
|
||||
@@ -439,6 +440,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
.handle()
|
||||
.insert_source(notifier, move |event, _, state| match event {
|
||||
SessionEvent::PauseSession => {
|
||||
state.super_tap.reset();
|
||||
state.cancel_window_interaction();
|
||||
state.set_binding_mode("default");
|
||||
let handle = state.loop_handle.clone();
|
||||
|
||||
@@ -450,8 +450,8 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// All allocations/tests precede commits. Logical publication waits for the
|
||||
/// final usable inventory; devices are independently applied and restored.
|
||||
/// A complete device test precedes application. Release former connector owners
|
||||
/// before per-surface tests; publish only after the final inventory is usable.
|
||||
pub(super) fn apply_device(
|
||||
state: &mut Ferese,
|
||||
node: DrmNode,
|
||||
@@ -483,6 +483,17 @@ pub(super) fn apply_device(
|
||||
.filter(|crtc| !selections.contains_key(crtc))
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
// A connector replacement can reuse the same CRTC, so it is not a removal
|
||||
// from the keyed inventory. Its old routing/resources must still be released
|
||||
// before staging any other output's new mode (for example HDMI -> DP).
|
||||
let mut releases = removals.clone();
|
||||
releases.extend(selections.iter().filter_map(|(crtc, selection)| {
|
||||
device
|
||||
.outputs
|
||||
.get(crtc)
|
||||
.and_then(|output| (output.connector != selection.connector.handle()).then_some(*crtc))
|
||||
}));
|
||||
releases.sort_by_key(|crtc| u32::from(*crtc));
|
||||
let hardware_changes = changed
|
||||
.iter()
|
||||
.filter(|crtc| {
|
||||
@@ -500,7 +511,7 @@ pub(super) fn apply_device(
|
||||
staged: HashMap::new(),
|
||||
replaced: HashSet::new(),
|
||||
};
|
||||
let result = crate::output_transaction::apply(&mut driver, &hardware_changes, &removals);
|
||||
let result = crate::output_transaction::apply(&mut driver, &hardware_changes, &releases);
|
||||
let mut staged = std::mem::take(&mut driver.staged);
|
||||
if let Err(failure) = result {
|
||||
for crtc in failure.unavailable {
|
||||
|
||||
@@ -123,6 +123,7 @@ pub(super) fn parse_binding(
|
||||
BindingMatch::Keysym => BindingTrigger::Keysym(identity.key),
|
||||
BindingMatch::Physical => BindingTrigger::Physical(Keycode::new(identity.key)),
|
||||
BindingMatch::Pointer => BindingTrigger::Pointer(PointerTrigger::from_key(identity.key)),
|
||||
BindingMatch::SuperTap => BindingTrigger::SuperTap,
|
||||
BindingMatch::Swipe => BindingTrigger::Swipe {
|
||||
fingers: identity.key / 4,
|
||||
direction: [
|
||||
@@ -154,6 +155,21 @@ pub(super) fn binding_identity(
|
||||
configured: &BindingConfig,
|
||||
keymap: &xkb::Keymap,
|
||||
) -> Result<BindingIdentity, ConfigError> {
|
||||
if matches!(
|
||||
configured.keys.trim().to_ascii_lowercase().as_str(),
|
||||
"super" | "logo" | "mod4"
|
||||
) {
|
||||
if configured.match_mode != BindingMatch::Keysym {
|
||||
return Err(ConfigError::Binding(
|
||||
"Super tap cannot use physical key matching".into(),
|
||||
));
|
||||
}
|
||||
return Ok(BindingIdentity {
|
||||
modifiers: BindingModifiers::default(),
|
||||
match_mode: BindingMatch::SuperTap,
|
||||
key: 0,
|
||||
});
|
||||
}
|
||||
let (modifiers, key) = parse_chord(&configured.keys)?;
|
||||
if let Some(trigger) = PointerTrigger::parse(&key)? {
|
||||
if configured.match_mode != BindingMatch::Keysym {
|
||||
@@ -192,7 +208,7 @@ pub(super) fn binding_identity(
|
||||
configured.keys
|
||||
))
|
||||
})?,
|
||||
BindingMatch::Swipe | BindingMatch::Pointer => {
|
||||
BindingMatch::Swipe | BindingMatch::Pointer | BindingMatch::SuperTap => {
|
||||
return Err(ConfigError::Binding(
|
||||
"gesture keys must use Swipe3Left/Right/Up/Down (3–5 fingers)".into(),
|
||||
));
|
||||
@@ -211,6 +227,7 @@ pub(super) fn binding_identity_for_runtime(binding: &Binding) -> BindingIdentity
|
||||
BindingTrigger::Keysym(key) => (BindingMatch::Keysym, key),
|
||||
BindingTrigger::Physical(key) => (BindingMatch::Physical, key.raw()),
|
||||
BindingTrigger::Pointer(trigger) => (BindingMatch::Pointer, trigger.key()),
|
||||
BindingTrigger::SuperTap => (BindingMatch::SuperTap, 0),
|
||||
BindingTrigger::Swipe { fingers, direction } => {
|
||||
(BindingMatch::Swipe, fingers * 4 + direction as u32)
|
||||
}
|
||||
@@ -574,6 +591,7 @@ pub(super) enum BindingTrigger {
|
||||
Keysym(u32),
|
||||
Physical(Keycode),
|
||||
Pointer(PointerTrigger),
|
||||
SuperTap,
|
||||
Swipe {
|
||||
fingers: u32,
|
||||
direction: crate::gestures::SwipeDirection,
|
||||
@@ -679,6 +697,7 @@ impl Binding {
|
||||
})
|
||||
}
|
||||
(BindingTrigger::Physical(left), BindingTrigger::Physical(right)) => left == right,
|
||||
(BindingTrigger::SuperTap, BindingTrigger::SuperTap) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -702,7 +721,9 @@ impl Binding {
|
||||
&& match self.trigger {
|
||||
BindingTrigger::Keysym(expected) => keysyms.contains(&expected),
|
||||
BindingTrigger::Physical(expected) => expected == keycode,
|
||||
BindingTrigger::Swipe { .. } | BindingTrigger::Pointer(_) => false,
|
||||
BindingTrigger::Swipe { .. }
|
||||
| BindingTrigger::Pointer(_)
|
||||
| BindingTrigger::SuperTap => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,6 +818,7 @@ impl Binding {
|
||||
}
|
||||
}
|
||||
parts.push(match self.trigger {
|
||||
BindingTrigger::SuperTap => "Super (tap)".into(),
|
||||
BindingTrigger::Pointer(trigger) => trigger.label(),
|
||||
BindingTrigger::Keysym(symbol) => {
|
||||
let name = xkb::keysym_get_name(xkb::Keysym::new(symbol));
|
||||
@@ -852,6 +874,8 @@ pub(super) enum BindingMatch {
|
||||
Swipe,
|
||||
#[serde(skip)]
|
||||
Pointer,
|
||||
#[serde(skip)]
|
||||
SuperTap,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, schemars::JsonSchema, serde::Serialize)]
|
||||
|
||||
@@ -27,7 +27,10 @@ impl From<Vec<Binding>> for BindingSet {
|
||||
|
||||
if matches!(
|
||||
identity.match_mode,
|
||||
BindingMatch::Keysym | BindingMatch::Physical | BindingMatch::Pointer
|
||||
BindingMatch::Keysym
|
||||
| BindingMatch::Physical
|
||||
| BindingMatch::Pointer
|
||||
| BindingMatch::SuperTap
|
||||
) {
|
||||
index.entry(identity).or_insert(position);
|
||||
}
|
||||
@@ -46,6 +49,16 @@ impl std::ops::Deref for BindingSet {
|
||||
}
|
||||
|
||||
impl BindingSet {
|
||||
pub(crate) fn super_tap(&self) -> Option<&Binding> {
|
||||
self.index
|
||||
.get(&BindingIdentity {
|
||||
modifiers: BindingModifiers::default(),
|
||||
match_mode: BindingMatch::SuperTap,
|
||||
key: 0,
|
||||
})
|
||||
.map(|index| &self.ordered[*index])
|
||||
}
|
||||
|
||||
pub(crate) fn binding_for_pointer(
|
||||
&self,
|
||||
trigger: super::PointerTrigger,
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::utils::on_commit_buffer_handler;
|
||||
use smithay::reexports::wayland_server::Client;
|
||||
use smithay::reexports::calloop::Interest;
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::{Client, Resource};
|
||||
use smithay::wayland::buffer::BufferHandler;
|
||||
use smithay::wayland::compositor::{
|
||||
CompositorClientState, CompositorHandler, CompositorState, get_parent, get_role,
|
||||
is_sync_subsurface,
|
||||
BufferAssignment, CompositorClientState, CompositorHandler, CompositorState, SurfaceAttributes,
|
||||
add_blocker, add_pre_commit_hook, get_parent, get_role, is_sync_subsurface, with_states,
|
||||
};
|
||||
use smithay::wayland::dmabuf::{
|
||||
DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier, get_dmabuf,
|
||||
};
|
||||
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
||||
use smithay::wayland::shm::{ShmHandler, ShmState};
|
||||
|
||||
use super::{layer_shell, xdg_shell};
|
||||
@@ -29,6 +32,39 @@ impl CompositorHandler for Ferese {
|
||||
|
||||
fn new_surface(&mut self, surface: &WlSurface) {
|
||||
self.update_surface_preferences(surface);
|
||||
// A commit is not proof that the GPU has finished writing its dma-buf.
|
||||
// Keep the previous surface state renderable until the acquire fence
|
||||
// signals, including during resize snapshots and subsurface updates.
|
||||
add_pre_commit_hook::<Self, _>(surface, |state, _dh, surface| {
|
||||
let dmabuf = with_states(surface, |states| {
|
||||
states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.pending()
|
||||
.buffer
|
||||
.as_ref()
|
||||
.and_then(|assignment| match assignment {
|
||||
BufferAssignment::NewBuffer(buffer) => get_dmabuf(buffer).ok().cloned(),
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
let Some((dmabuf, client)) = dmabuf.zip(surface.client()) else {
|
||||
return;
|
||||
};
|
||||
if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) {
|
||||
let inserted = state.loop_handle.insert_source(source, move |_, _, state| {
|
||||
let dh = state.display_handle.clone();
|
||||
state
|
||||
.client_compositor_state(&client)
|
||||
.blocker_cleared(state, &dh);
|
||||
Ok(())
|
||||
});
|
||||
match inserted {
|
||||
Ok(_) => add_blocker(surface, blocker),
|
||||
Err(error) => tracing::warn!(%error, "could not monitor surface GPU readiness"),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn new_subsurface(&mut self, surface: &WlSurface, _parent: &WlSurface) {
|
||||
|
||||
@@ -81,6 +81,7 @@ impl SeatHandler for Ferese {
|
||||
}
|
||||
|
||||
fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&WlSurface>) {
|
||||
self.super_tap.cancel();
|
||||
self.pending_layer_focus = None;
|
||||
// Smithay has already sent enter. last_enter uses a separate mutex;
|
||||
// querying current_focus here would reenter the held keyboard lock.
|
||||
@@ -157,6 +158,7 @@ impl KeyboardShortcutsInhibitHandler for Ferese {
|
||||
.and_then(|keyboard| keyboard.current_focus());
|
||||
|
||||
if focused.as_ref() == Some(inhibitor.wl_surface()) {
|
||||
self.super_tap.cancel();
|
||||
inhibitor.activate();
|
||||
self.active_shortcuts_inhibitor = Some(inhibitor);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ impl VirtualKeyboardHandler for Ferese {
|
||||
_ => None,
|
||||
};
|
||||
if let Some(device) = device {
|
||||
self.super_tap.cancel();
|
||||
keyboard.release_source(self, device.source());
|
||||
}
|
||||
return;
|
||||
@@ -78,7 +79,10 @@ impl VirtualKeyboardHandler for Ferese {
|
||||
context.set_modifier_mask(mods_depressed, mods_latched, mods_locked, group)
|
||||
});
|
||||
}
|
||||
InputEvent::DeviceRemoved { device } => keyboard.release_source(self, device.source()),
|
||||
InputEvent::DeviceRemoved { device } => {
|
||||
self.super_tap.cancel();
|
||||
keyboard.release_source(self, device.source());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod window;
|
||||
pub(crate) use window::WindowInteraction;
|
||||
mod pointer;
|
||||
pub(crate) mod super_tap;
|
||||
use ferese_layout::Direction;
|
||||
pub(crate) use pointer::ScrollBindings;
|
||||
use smithay::backend::input::{
|
||||
@@ -150,6 +151,16 @@ impl Ferese {
|
||||
event: InputEvent<I>,
|
||||
source: KeyboardSource,
|
||||
) {
|
||||
match &event {
|
||||
InputEvent::Keyboard { event } => self.super_tap.observe_key(source, event.key_code()),
|
||||
InputEvent::PointerButton { event } => self.super_tap.pointer_button(
|
||||
event.device().id(),
|
||||
event.button_code(),
|
||||
event.state(),
|
||||
),
|
||||
InputEvent::DeviceRemoved { device } => self.super_tap.device_removed(&device.id()),
|
||||
_ => self.super_tap.cancel(),
|
||||
}
|
||||
if self.input_capture.active() {
|
||||
self.set_binding_mode("default");
|
||||
}
|
||||
@@ -320,6 +331,10 @@ impl Ferese {
|
||||
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
|
||||
let keycode = event.key_code();
|
||||
let state = event.state();
|
||||
let keys_were_clear = keyboard.pressed_keys().is_empty();
|
||||
let tap_focus = keyboard.current_focus().map(|surface| surface.id());
|
||||
let tap_allowed = self.super_tap_allowed(&keyboard);
|
||||
let mut tap_action = None;
|
||||
|
||||
let was_captured = self.input_capture.active();
|
||||
if self.input_capture.captures(1) {
|
||||
@@ -349,6 +364,18 @@ impl Ferese {
|
||||
serial,
|
||||
event.time(),
|
||||
|data, modifiers, keysym| {
|
||||
if state == KeyState::Released {
|
||||
if data.super_tap.release(source, keycode, tap_allowed, tap_focus.clone()) {
|
||||
tap_action = data.binding_modes.bindings(&data.bindings)
|
||||
.super_tap().map(|binding| binding.action.clone());
|
||||
}
|
||||
} else if keys_were_clear && tap_allowed
|
||||
&& !modifiers.ctrl && !modifiers.alt && !modifiers.shift
|
||||
&& keysym.raw_syms().iter().any(|symbol| matches!(symbol.raw(), keysyms::KEY_Super_L | keysyms::KEY_Super_R))
|
||||
&& data.binding_modes.bindings(&data.bindings).super_tap().is_some()
|
||||
{
|
||||
data.super_tap.arm(source, keycode, tap_focus.clone());
|
||||
}
|
||||
if data.focus_cycle.is_some() {
|
||||
if data.session_lock.active() || data.input_capture.captures(1) {
|
||||
data.cancel_focus_cycle();
|
||||
@@ -530,6 +557,11 @@ impl Ferese {
|
||||
FilterResult::Intercept(())
|
||||
},
|
||||
);
|
||||
// Finish forwarding the original release to its owner before
|
||||
// changing focus or opening a layer surface for the action.
|
||||
if let Some(action) = tap_action {
|
||||
self.execute_binding(action);
|
||||
}
|
||||
if state == KeyState::Pressed && intercepted.is_none() && client_grab {
|
||||
self.record_activation_input(serial, interaction_focus);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
//! A modifier tap is armed only by an otherwise isolated Super press. It never
|
||||
//! owns the keyboard event: normal clients, IMEs and Super chords keep theirs.
|
||||
use std::collections::HashSet;
|
||||
|
||||
use smithay::backend::input::ButtonState;
|
||||
use smithay::input::keyboard::{KeyboardHandle, KeyboardSource, Keycode};
|
||||
use smithay::reexports::wayland_server::backend::ObjectId;
|
||||
use smithay::wayland::input_method::InputMethodKeyboardGrab;
|
||||
use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct SuperTap {
|
||||
candidate: Option<(KeyboardSource, Keycode, Option<ObjectId>)>,
|
||||
pointer_buttons: HashSet<(String, u32)>,
|
||||
}
|
||||
|
||||
impl SuperTap {
|
||||
pub(crate) fn cancel(&mut self) {
|
||||
self.candidate = None;
|
||||
}
|
||||
|
||||
pub(crate) fn reset(&mut self) {
|
||||
self.cancel();
|
||||
self.pointer_buttons.clear();
|
||||
}
|
||||
|
||||
pub(crate) fn device_removed(&mut self, device: &str) {
|
||||
self.cancel();
|
||||
self.pointer_buttons.retain(|(owner, _)| owner != device);
|
||||
}
|
||||
|
||||
pub(super) fn pointer_button(&mut self, device: String, button: u32, state: ButtonState) {
|
||||
self.cancel();
|
||||
if state == ButtonState::Pressed {
|
||||
self.pointer_buttons.insert((device, button));
|
||||
} else {
|
||||
self.pointer_buttons.remove(&(device, button));
|
||||
}
|
||||
}
|
||||
|
||||
// Observe before Smithay deduplicates a second input source's identical
|
||||
// keycode. That second source must invalidate an isolated tap as well.
|
||||
pub(super) fn observe_key(&mut self, source: KeyboardSource, key: Keycode) {
|
||||
if self
|
||||
.candidate
|
||||
.as_ref()
|
||||
.is_some_and(|(owner, held, _)| *owner != source || *held != key)
|
||||
{
|
||||
self.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn arm(&mut self, source: KeyboardSource, key: Keycode, focus: Option<ObjectId>) {
|
||||
self.candidate = Some((source, key, focus));
|
||||
}
|
||||
|
||||
pub(super) fn release(
|
||||
&mut self,
|
||||
source: KeyboardSource,
|
||||
key: Keycode,
|
||||
allowed: bool,
|
||||
focus: Option<ObjectId>,
|
||||
) -> bool {
|
||||
self.candidate.take().is_some_and(|(owner, held, initial)| {
|
||||
allowed && owner == source && held == key && focus == initial
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Ferese {
|
||||
pub(super) fn super_tap_allowed(&self, keyboard: &KeyboardHandle<Self>) -> bool {
|
||||
!self.session_lock.active()
|
||||
&& !self.input_capture.active()
|
||||
&& !self.seat.keyboard_shortcuts_inhibited()
|
||||
&& self.active_shortcuts_inhibitor.is_none()
|
||||
&& self.pointer_operation.is_none()
|
||||
&& self.super_tap.pointer_buttons.is_empty()
|
||||
&& self.seat.get_pointer().is_none_or(|pointer| !pointer.is_grabbed())
|
||||
// A native Rime grab is part of normal text input. Popup/client
|
||||
// grabs still retain their exclusive keyboard interaction.
|
||||
&& keyboard.with_grab(|_, grab| grab.is::<InputMethodKeyboardGrab>()).unwrap_or(true)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod security_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,181 @@
|
||||
use crate::Ferese;
|
||||
use crate::input::binding_mode_tests::key;
|
||||
use crate::state::{dispatch_test_client as dispatch, test_client_request as request, test_window};
|
||||
use smithay::reexports::calloop::EventLoop;
|
||||
use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::{
|
||||
ext_session_lock_manager_v1::ExtSessionLockManagerV1, ext_session_lock_v1::ExtSessionLockV1,
|
||||
};
|
||||
use smithay::reexports::wayland_protocols::wp::keyboard_shortcuts_inhibit::zv1::server::zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1;
|
||||
use smithay::reexports::wayland_server::{Client, Resource};
|
||||
use smithay::utils::SERIAL_COUNTER;
|
||||
use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat;
|
||||
use std::io::Read;
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
const SOURCE: &str = "binding Super binding-mode tapped\nbinding-mode tapped { binding a none; }\n";
|
||||
|
||||
fn fixture() -> (Ferese, EventLoop<'static, Ferese>) {
|
||||
let (mut state, events, _) = crate::input::layer_focus_tests::fixture();
|
||||
state.reload_config_source(SOURCE.into()).unwrap();
|
||||
(state, events)
|
||||
}
|
||||
|
||||
fn bind_seat(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, wire: &mut UnixStream) {
|
||||
request(wire, 1, 1, &[7], None);
|
||||
dispatch(events, state);
|
||||
wire.set_nonblocking(true).unwrap();
|
||||
let mut bytes = Vec::new();
|
||||
let _ = wire.read_to_end(&mut bytes);
|
||||
wire.set_nonblocking(false).unwrap();
|
||||
let mut offset = 0;
|
||||
let mut seat_global = None;
|
||||
while offset < bytes.len() {
|
||||
let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap());
|
||||
let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
|
||||
if object == 7 && header as u16 == 0 {
|
||||
let length =
|
||||
u32::from_ne_bytes(bytes[offset + 12..offset + 16].try_into().unwrap()) as usize;
|
||||
if &bytes[offset + 16..offset + 16 + length] == b"wl_seat\0" {
|
||||
seat_global = Some(u32::from_ne_bytes(
|
||||
bytes[offset + 8..offset + 12].try_into().unwrap(),
|
||||
));
|
||||
}
|
||||
}
|
||||
offset += (header >> 16) as usize;
|
||||
}
|
||||
let mut args = vec![seat_global.unwrap(), 8];
|
||||
args.extend(
|
||||
b"wl_seat\0"
|
||||
.chunks_exact(4)
|
||||
.map(|word| u32::from_ne_bytes(word.try_into().unwrap())),
|
||||
);
|
||||
args.extend([7, 8]);
|
||||
request(wire, 7, 0, &args, None);
|
||||
dispatch(events, state);
|
||||
}
|
||||
|
||||
fn create_manager<R: Resource + 'static>(state: &mut Ferese, client: &Client) -> R
|
||||
where
|
||||
Ferese: smithay::reexports::wayland_server::Dispatch<R, smithay::wayland::GlobalData>,
|
||||
{
|
||||
client
|
||||
.create_resource::<R, smithay::wayland::GlobalData, Ferese>(
|
||||
&state.display_handle,
|
||||
1,
|
||||
smithay::wayland::GlobalData,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inhibitor_protocol_cancels_a_held_tap_even_if_destroyed_before_release() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"input::super_tap::security_tests::inhibitor_protocol_cancels_a_held_tap_even_if_destroyed_before_release",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let (mut state, mut events) = fixture();
|
||||
let (window, mut wire) = test_window(&mut state, &mut events, 0xff102030);
|
||||
let surface = window.toplevel().unwrap().wl_surface().clone();
|
||||
let client = surface.client().unwrap();
|
||||
let keyboard = state.seat.get_keyboard().unwrap();
|
||||
keyboard.set_focus(
|
||||
&mut state,
|
||||
Some(surface.clone()),
|
||||
SERIAL_COUNTER.next_serial(),
|
||||
);
|
||||
bind_seat(&mut state, &mut events, &mut wire);
|
||||
let manager = create_manager::<ZwpKeyboardShortcutsInhibitManagerV1>(&mut state, &client);
|
||||
key(&mut state, 133, true);
|
||||
assert!(state.super_tap.candidate.is_some());
|
||||
let opcode = ZwpKeyboardShortcutsInhibitManagerV1::interface()
|
||||
.requests
|
||||
.iter()
|
||||
.position(|request| request.name == "inhibit_shortcuts")
|
||||
.unwrap() as u32;
|
||||
request(
|
||||
&mut wire,
|
||||
manager.id().protocol_id(),
|
||||
opcode,
|
||||
&[9, surface.id().protocol_id(), 8],
|
||||
None,
|
||||
);
|
||||
dispatch(&mut events, &mut state);
|
||||
assert!(state.seat.keyboard_shortcuts_inhibited());
|
||||
assert!(
|
||||
state.super_tap.candidate.is_none(),
|
||||
"activation must cancel immediately"
|
||||
);
|
||||
request(&mut wire, 9, 0, &[], None);
|
||||
dispatch(&mut events, &mut state);
|
||||
assert!(!state.seat.keyboard_shortcuts_inhibited());
|
||||
key(&mut state, 133, false);
|
||||
assert_eq!(state.binding_modes.active(), None);
|
||||
key(&mut state, 133, true);
|
||||
key(&mut state, 133, false);
|
||||
assert_eq!(state.binding_modes.active(), Some("tapped"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_lock_protocol_cancels_pending_tap_before_further_keyboard_input() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"input::super_tap::security_tests::session_lock_protocol_cancels_pending_tap_before_further_keyboard_input",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let (mut state, mut events) = fixture();
|
||||
let (window, mut wire) = test_window(&mut state, &mut events, 0xff102030);
|
||||
let surface = window.toplevel().unwrap().wl_surface().clone();
|
||||
let client = surface.client().unwrap();
|
||||
state.seat.get_keyboard().unwrap().set_focus(
|
||||
&mut state,
|
||||
Some(surface),
|
||||
SERIAL_COUNTER.next_serial(),
|
||||
);
|
||||
let manager = create_manager::<ExtSessionLockManagerV1>(&mut state, &client);
|
||||
key(&mut state, 133, true);
|
||||
assert!(state.super_tap.candidate.is_some());
|
||||
let opcode = ExtSessionLockManagerV1::interface()
|
||||
.requests
|
||||
.iter()
|
||||
.position(|request| request.name == "lock")
|
||||
.unwrap() as u32;
|
||||
request(&mut wire, manager.id().protocol_id(), opcode, &[7], None);
|
||||
dispatch(&mut events, &mut state);
|
||||
assert!(state.session_lock.active());
|
||||
assert!(
|
||||
client
|
||||
.object_from_protocol_id::<ExtSessionLockV1>(&state.display_handle, 7)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(state.super_tap.candidate.is_none());
|
||||
key(&mut state, 133, false);
|
||||
key(&mut state, 133, true);
|
||||
key(&mut state, 133, false);
|
||||
assert_eq!(state.binding_modes.active(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_departure_and_return_do_not_restore_a_held_tap() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"input::super_tap::security_tests::focus_departure_and_return_do_not_restore_a_held_tap",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let (mut state, mut events) = fixture();
|
||||
let (window, _wire) = test_window(&mut state, &mut events, 0xff102030);
|
||||
let surface = window.toplevel().unwrap().wl_surface().clone();
|
||||
let keyboard = state.seat.get_keyboard().unwrap();
|
||||
keyboard.set_focus(
|
||||
&mut state,
|
||||
Some(surface.clone()),
|
||||
SERIAL_COUNTER.next_serial(),
|
||||
);
|
||||
key(&mut state, 133, true);
|
||||
assert!(state.super_tap.candidate.is_some());
|
||||
keyboard.set_focus(&mut state, None, SERIAL_COUNTER.next_serial());
|
||||
keyboard.set_focus(&mut state, Some(surface), SERIAL_COUNTER.next_serial());
|
||||
assert!(state.super_tap.candidate.is_none());
|
||||
key(&mut state, 133, false);
|
||||
assert_eq!(state.binding_modes.active(), None);
|
||||
}
|
||||
@@ -0,0 +1,508 @@
|
||||
use super::*;
|
||||
use crate::Ferese;
|
||||
use crate::config::{BindingAction, Config};
|
||||
use crate::input::binding_mode_tests::key;
|
||||
use crate::input::{Event, InputBackend, InputEvent, PointerButtonEvent, PointerMotionEvent};
|
||||
use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1;
|
||||
use smithay::backend::input::{
|
||||
Axis, AxisRelativeDirection, AxisSource, InputTime, KeyState, PointerAxisEvent, UnusedEvent,
|
||||
};
|
||||
use smithay::backend::winit::WinitVirtualDevice;
|
||||
use smithay::input::keyboard::{GrabStartData, KeyboardGrab, KeyboardInnerHandle, ModifiersState};
|
||||
use smithay::reexports::calloop::EventLoop;
|
||||
use smithay::reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface};
|
||||
use smithay::utils::{SERIAL_COUNTER, Serial};
|
||||
use std::io::Read;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::Arc;
|
||||
|
||||
macro_rules! private {
|
||||
($test:ident) => {
|
||||
if !crate::startup_tests::private_runtime(concat!(
|
||||
"input::super_tap::tests::",
|
||||
stringify!($test)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
const SOURCE: &str = "binding Super binding-mode tapped\nbinding-mode tapped { binding a none; }\nbinding Super+Space binding-mode chord\nbinding-mode chord { binding a none; }\n";
|
||||
fn fixture(source: &str) -> (Ferese, EventLoop<'static, Ferese>) {
|
||||
let (mut state, events, _) = crate::input::layer_focus_tests::fixture();
|
||||
state.reload_config_source(source.into()).unwrap();
|
||||
(state, events)
|
||||
}
|
||||
fn tap(state: &mut Ferese, code: u32) {
|
||||
key(state, code, true);
|
||||
key(state, code, false);
|
||||
}
|
||||
fn tapped(state: &Ferese) -> bool {
|
||||
state.binding_modes.active() == Some("tapped")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_indexes_taps_separately_and_preserves_space_chord() {
|
||||
let config = Config::parse_source("binding Super toggle-launcher\n").unwrap();
|
||||
let bindings: crate::config::BindingSet = config
|
||||
.bindings(&config.input_settings().unwrap())
|
||||
.unwrap()
|
||||
.into();
|
||||
assert!(matches!(
|
||||
bindings.super_tap().unwrap().action,
|
||||
BindingAction::ToggleLauncher
|
||||
));
|
||||
assert!(
|
||||
bindings
|
||||
.binding_for_key(
|
||||
Keycode::new(133),
|
||||
&[smithay::input::keyboard::keysyms::KEY_Super_L],
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
assert!(matches!(
|
||||
bindings
|
||||
.binding_for_key(
|
||||
Keycode::new(65),
|
||||
&[smithay::input::keyboard::keysyms::KEY_space],
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
)
|
||||
.unwrap()
|
||||
.action,
|
||||
BindingAction::ToggleLauncher
|
||||
));
|
||||
let guide = bindings.super_tap().unwrap().guide_entry(None).unwrap();
|
||||
assert_eq!(guide["keys"], "Super (tap)");
|
||||
for source in [
|
||||
"binding Super toggle-launcher\nbinding Logo none\n",
|
||||
"binding Super match=physical action=toggle-launcher\n",
|
||||
"binding Ctrl toggle-launcher\n",
|
||||
] {
|
||||
let config = Config::parse_source(source).unwrap();
|
||||
assert!(
|
||||
config.bindings(&config.input_settings().unwrap()).is_err(),
|
||||
"{source}"
|
||||
);
|
||||
}
|
||||
let disabled = Config::parse_source("binding Super disabled=#true\n").unwrap();
|
||||
let bindings: crate::config::BindingSet = disabled
|
||||
.bindings(&disabled.input_settings().unwrap())
|
||||
.unwrap()
|
||||
.into();
|
||||
assert!(bindings.super_tap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left_right_taps_execute_on_release_and_repeat_does_not_rearm() {
|
||||
private!(left_right_taps_execute_on_release_and_repeat_does_not_rearm);
|
||||
let (mut state, _events) = fixture(SOURCE);
|
||||
for code in [133, 134] {
|
||||
key(&mut state, code, true);
|
||||
assert!(!tapped(&state));
|
||||
key(&mut state, code, true); // backend repeat, not another isolated tap
|
||||
assert!(!tapped(&state));
|
||||
key(&mut state, code, false);
|
||||
assert!(tapped(&state));
|
||||
state.set_binding_mode("default");
|
||||
key(&mut state, code, false); // duplicate release
|
||||
assert!(!tapped(&state));
|
||||
assert!(state.seat.get_keyboard().unwrap().pressed_keys().is_empty());
|
||||
assert!(
|
||||
state.intercepted_keys.is_empty(),
|
||||
"tap must not own either half of Super"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_chords_held_keys_and_both_super_keys_never_become_taps() {
|
||||
private!(existing_chords_held_keys_and_both_super_keys_never_become_taps);
|
||||
let (mut state, _events) = fixture(SOURCE);
|
||||
for sequence in [
|
||||
vec![(133, true), (38, true), (38, false), (133, false)],
|
||||
vec![(38, true), (133, true), (38, false), (133, false)],
|
||||
vec![(133, true), (134, true), (133, false), (134, false)],
|
||||
vec![(134, true), (133, true), (134, false), (133, false)],
|
||||
vec![(37, true), (133, true), (37, false), (133, false)],
|
||||
vec![(133, true), (50, true), (50, false), (133, false)],
|
||||
] {
|
||||
for (code, down) in sequence {
|
||||
key(&mut state, code, down);
|
||||
}
|
||||
assert!(!tapped(&state));
|
||||
assert!(state.seat.get_keyboard().unwrap().pressed_keys().is_empty());
|
||||
}
|
||||
key(&mut state, 133, true);
|
||||
tap(&mut state, 65); // existing Super+Space, action is deliberately distinct
|
||||
assert_eq!(state.binding_modes.active(), Some("chord"));
|
||||
key(&mut state, 133, false);
|
||||
assert_eq!(state.binding_modes.active(), Some("chord"));
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PointerInput;
|
||||
impl InputBackend for PointerInput {
|
||||
type Device = WinitVirtualDevice;
|
||||
type KeyboardKeyEvent = UnusedEvent;
|
||||
type PointerButtonEvent = Button;
|
||||
type PointerMotionEvent = Motion;
|
||||
type PointerAxisEvent = Scroll;
|
||||
type PointerMotionAbsoluteEvent = UnusedEvent;
|
||||
type GestureSwipeBeginEvent = UnusedEvent;
|
||||
type GestureSwipeUpdateEvent = UnusedEvent;
|
||||
type GestureSwipeEndEvent = UnusedEvent;
|
||||
type GesturePinchBeginEvent = UnusedEvent;
|
||||
type GesturePinchUpdateEvent = UnusedEvent;
|
||||
type GesturePinchEndEvent = UnusedEvent;
|
||||
type GestureHoldBeginEvent = UnusedEvent;
|
||||
type GestureHoldEndEvent = UnusedEvent;
|
||||
type TouchDownEvent = UnusedEvent;
|
||||
type TouchUpEvent = UnusedEvent;
|
||||
type TouchMotionEvent = UnusedEvent;
|
||||
type TouchCancelEvent = UnusedEvent;
|
||||
type TouchFrameEvent = UnusedEvent;
|
||||
type TabletToolAxisEvent = UnusedEvent;
|
||||
type TabletToolProximityEvent = UnusedEvent;
|
||||
type TabletToolTipEvent = UnusedEvent;
|
||||
type TabletToolButtonEvent = UnusedEvent;
|
||||
type SwitchToggleEvent = UnusedEvent;
|
||||
type SpecialEvent = UnusedEvent;
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct Button(ButtonState);
|
||||
#[derive(Debug)]
|
||||
struct Motion;
|
||||
#[derive(Debug)]
|
||||
struct Scroll;
|
||||
macro_rules! pointer_event {
|
||||
($event:ty) => {
|
||||
impl Event<PointerInput> for $event {
|
||||
fn time(&self) -> InputTime {
|
||||
InputTime::from_millis(1)
|
||||
}
|
||||
fn device(&self) -> WinitVirtualDevice {
|
||||
WinitVirtualDevice
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
pointer_event!(Button);
|
||||
pointer_event!(Motion);
|
||||
pointer_event!(Scroll);
|
||||
impl PointerButtonEvent<PointerInput> for Button {
|
||||
fn button_code(&self) -> u32 {
|
||||
0x111
|
||||
}
|
||||
fn state(&self) -> ButtonState {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
impl PointerMotionEvent<PointerInput> for Motion {
|
||||
fn delta_x(&self) -> f64 {
|
||||
1.
|
||||
}
|
||||
fn delta_y(&self) -> f64 {
|
||||
0.
|
||||
}
|
||||
fn delta_x_unaccel(&self) -> f64 {
|
||||
1.
|
||||
}
|
||||
fn delta_y_unaccel(&self) -> f64 {
|
||||
0.
|
||||
}
|
||||
}
|
||||
impl PointerAxisEvent<PointerInput> for Scroll {
|
||||
fn amount(&self, axis: Axis) -> Option<f64> {
|
||||
(axis == Axis::Vertical).then_some(15.)
|
||||
}
|
||||
fn amount_v120(&self, axis: Axis) -> Option<f64> {
|
||||
(axis == Axis::Vertical).then_some(120.)
|
||||
}
|
||||
fn source(&self) -> AxisSource {
|
||||
AxisSource::Wheel
|
||||
}
|
||||
fn relative_direction(&self, _: Axis) -> AxisRelativeDirection {
|
||||
AxisRelativeDirection::Identical
|
||||
}
|
||||
}
|
||||
fn button(state: &mut Ferese, down: bool) {
|
||||
state.process_input_event::<PointerInput>(InputEvent::PointerButton {
|
||||
event: Button(if down {
|
||||
ButtonState::Pressed
|
||||
} else {
|
||||
ButtonState::Released
|
||||
}),
|
||||
});
|
||||
}
|
||||
#[test]
|
||||
fn mouse_motion_buttons_scroll_and_already_held_button_cancel_taps() {
|
||||
private!(mouse_motion_buttons_scroll_and_already_held_button_cancel_taps);
|
||||
let (mut state, _events) = fixture(SOURCE);
|
||||
key(&mut state, 133, true);
|
||||
button(&mut state, true);
|
||||
button(&mut state, false);
|
||||
key(&mut state, 133, false);
|
||||
assert!(!tapped(&state));
|
||||
button(&mut state, true);
|
||||
tap(&mut state, 133);
|
||||
assert!(!tapped(&state));
|
||||
button(&mut state, false);
|
||||
key(&mut state, 133, true);
|
||||
state.process_input_event::<PointerInput>(InputEvent::PointerMotion { event: Motion });
|
||||
key(&mut state, 133, false);
|
||||
assert!(!tapped(&state));
|
||||
key(&mut state, 133, true);
|
||||
state.process_input_event::<PointerInput>(InputEvent::PointerAxis { event: Scroll });
|
||||
key(&mut state, 133, false);
|
||||
assert!(!tapped(&state));
|
||||
tap(&mut state, 133);
|
||||
assert!(
|
||||
tapped(&state),
|
||||
"fresh isolated tap must recover after mouse use"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reload_focus_loss_and_second_source_do_not_resurrect_pending_taps() {
|
||||
private!(reload_focus_loss_and_second_source_do_not_resurrect_pending_taps);
|
||||
let (mut state, _events) = fixture(SOURCE);
|
||||
key(&mut state, 133, true);
|
||||
state.reload_config_source(SOURCE.into()).unwrap();
|
||||
// An unchanged config need not replace bindings; an actual input/binding
|
||||
// edit must invalidate the press even if its action stays the same.
|
||||
state
|
||||
.reload_config_source(format!("{SOURCE}\nbinding b none\n"))
|
||||
.unwrap();
|
||||
key(&mut state, 133, false);
|
||||
assert!(!tapped(&state));
|
||||
key(&mut state, 133, true);
|
||||
state.release_nested_keyboard();
|
||||
key(&mut state, 133, false);
|
||||
assert!(!tapped(&state));
|
||||
let auxiliary = KeyboardSource::new_auxiliary();
|
||||
state
|
||||
.super_tap
|
||||
.arm(KeyboardSource::MAIN, Keycode::new(133), None);
|
||||
state.super_tap.observe_key(auxiliary, Keycode::new(133));
|
||||
assert!(
|
||||
!state
|
||||
.super_tap
|
||||
.release(KeyboardSource::MAIN, Keycode::new(133), true, None)
|
||||
);
|
||||
tap(&mut state, 134);
|
||||
assert!(tapped(&state));
|
||||
}
|
||||
|
||||
struct ClientGrab(GrabStartData<Ferese>);
|
||||
impl KeyboardGrab<Ferese> for ClientGrab {
|
||||
fn input(
|
||||
&mut self,
|
||||
data: &mut Ferese,
|
||||
handle: &mut KeyboardInnerHandle<'_, Ferese>,
|
||||
key: Keycode,
|
||||
state: KeyState,
|
||||
mods: Option<ModifiersState>,
|
||||
serial: Serial,
|
||||
time: InputTime,
|
||||
) {
|
||||
handle.input(data, key, state, mods, serial, time);
|
||||
}
|
||||
fn set_focus(
|
||||
&mut self,
|
||||
data: &mut Ferese,
|
||||
handle: &mut KeyboardInnerHandle<'_, Ferese>,
|
||||
focus: Option<WlSurface>,
|
||||
serial: Serial,
|
||||
) {
|
||||
handle.set_focus(data, focus, serial);
|
||||
}
|
||||
fn start_data(&self) -> &GrabStartData<Ferese> {
|
||||
&self.0
|
||||
}
|
||||
fn unset(&mut self, _: &mut Ferese) {}
|
||||
}
|
||||
#[test]
|
||||
fn native_ime_grab_allows_tap_but_client_grab_keeps_keyboard() {
|
||||
private!(native_ime_grab_allows_tap_but_client_grab_keeps_keyboard);
|
||||
let (mut state, mut _events) = fixture(SOURCE);
|
||||
let keyboard = state.seat.get_keyboard().unwrap();
|
||||
// Use a real input-method grab and its own virtual keyboard, as Rime
|
||||
// does, rather than an empty grab whose protocol resources do not exist.
|
||||
use smithay::reexports::wayland_protocols_misc::zwp_input_method_v2::server::{
|
||||
zwp_input_method_manager_v2::ZwpInputMethodManagerV2,
|
||||
zwp_input_method_v2::ZwpInputMethodV2,
|
||||
};
|
||||
use smithay::reexports::wayland_protocols_misc::zwp_virtual_keyboard_v1::server::zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1;
|
||||
use smithay::wayland::input_method::InputMethodSeat;
|
||||
use std::os::fd::AsRawFd;
|
||||
let (server, mut wire) = UnixStream::pair().unwrap();
|
||||
let client = state
|
||||
.display_handle
|
||||
.insert_client(server, Arc::new(crate::state::ClientState::default()))
|
||||
.unwrap();
|
||||
let request = crate::state::test_client_request;
|
||||
request(&mut wire, 1, 1, &[2], None); // display.get_registry
|
||||
crate::state::dispatch_test_client(&mut _events, &mut state);
|
||||
wire.set_nonblocking(true).unwrap();
|
||||
let mut bytes = Vec::new();
|
||||
let _ = wire.read_to_end(&mut bytes);
|
||||
wire.set_nonblocking(false).unwrap();
|
||||
let mut offset = 0;
|
||||
let mut seat_global = None;
|
||||
while offset < bytes.len() {
|
||||
let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap());
|
||||
let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
|
||||
if object == 2 && header as u16 == 0 {
|
||||
let length =
|
||||
u32::from_ne_bytes(bytes[offset + 12..offset + 16].try_into().unwrap()) as usize;
|
||||
if &bytes[offset + 16..offset + 16 + length] == b"wl_seat\0" {
|
||||
seat_global = Some(u32::from_ne_bytes(
|
||||
bytes[offset + 8..offset + 12].try_into().unwrap(),
|
||||
));
|
||||
}
|
||||
}
|
||||
offset += (header >> 16) as usize;
|
||||
}
|
||||
let mut bind = vec![seat_global.unwrap(), 8];
|
||||
bind.extend(
|
||||
b"wl_seat\0"
|
||||
.chunks_exact(4)
|
||||
.map(|word| u32::from_ne_bytes(word.try_into().unwrap())),
|
||||
);
|
||||
bind.extend([7, 3]);
|
||||
request(&mut wire, 2, 0, &bind, None);
|
||||
crate::state::dispatch_test_client(&mut _events, &mut state);
|
||||
let manager = client
|
||||
.create_resource::<ZwpInputMethodManagerV2, smithay::wayland::GlobalData, Ferese>(
|
||||
&state.display_handle,
|
||||
1,
|
||||
smithay::wayland::GlobalData,
|
||||
)
|
||||
.unwrap();
|
||||
let vk_manager = client
|
||||
.create_resource::<ZwpVirtualKeyboardManagerV1, smithay::wayland::GlobalData, Ferese>(
|
||||
&state.display_handle,
|
||||
1,
|
||||
smithay::wayland::GlobalData,
|
||||
)
|
||||
.unwrap();
|
||||
request(&mut wire, manager.id().protocol_id(), 0, &[3, 4], None);
|
||||
crate::state::dispatch_test_client(&mut _events, &mut state);
|
||||
let grab_opcode = ZwpInputMethodV2::interface()
|
||||
.requests
|
||||
.iter()
|
||||
.position(|request| request.name == "grab_keyboard")
|
||||
.unwrap() as u32;
|
||||
request(&mut wire, 4, grab_opcode, &[5], None);
|
||||
request(&mut wire, vk_manager.id().protocol_id(), 0, &[3, 6], None);
|
||||
let fd = tempfile::tempfile().unwrap();
|
||||
request(&mut wire, 6, 0, &[0, 0], Some(fd.as_raw_fd())); // no_keymap, retain seat mapping
|
||||
crate::state::dispatch_test_client(&mut _events, &mut state);
|
||||
assert_eq!(
|
||||
state.seat.input_method().keyboard_grab_client(),
|
||||
Some(client)
|
||||
);
|
||||
key(&mut state, 133, true);
|
||||
assert!(state.super_tap.candidate.is_some());
|
||||
request(&mut wire, 6, 1, &[1, 125, 1], None); // trusted Rime echo of physical Super press
|
||||
crate::state::dispatch_test_client(&mut _events, &mut state);
|
||||
assert!(
|
||||
state.super_tap.candidate.is_some(),
|
||||
"trusted IME echo must bypass auxiliary tap cancellation"
|
||||
);
|
||||
assert!(!tapped(&state));
|
||||
key(&mut state, 133, false);
|
||||
assert!(tapped(&state));
|
||||
request(&mut wire, 6, 1, &[2, 125, 0], None); // trusted release echo after the physical tap
|
||||
crate::state::dispatch_test_client(&mut _events, &mut state);
|
||||
assert!(tapped(&state));
|
||||
assert!(keyboard.pressed_keys().is_empty());
|
||||
state.set_binding_mode("default");
|
||||
keyboard.unset_grab(&mut state);
|
||||
keyboard.set_grab(
|
||||
&mut state,
|
||||
ClientGrab(GrabStartData { focus: None }),
|
||||
SERIAL_COUNTER.next_serial(),
|
||||
);
|
||||
tap(&mut state, 133);
|
||||
assert!(!tapped(&state));
|
||||
keyboard.unset_grab(&mut state);
|
||||
tap(&mut state, 133);
|
||||
assert!(tapped(&state));
|
||||
}
|
||||
|
||||
fn launcher_events(state: &mut Ferese, wire: &mut UnixStream, shell: &FereseShellV1) -> usize {
|
||||
state.display_handle.flush_clients().unwrap();
|
||||
wire.set_nonblocking(true).unwrap();
|
||||
let mut bytes = Vec::new();
|
||||
let _ = wire.read_to_end(&mut bytes);
|
||||
let opcode = FereseShellV1::interface()
|
||||
.events
|
||||
.iter()
|
||||
.position(|e| e.name == "toggle_launcher")
|
||||
.unwrap() as u16;
|
||||
let (mut offset, mut count) = (0, 0);
|
||||
while offset < bytes.len() {
|
||||
let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap());
|
||||
let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
|
||||
if object == shell.id().protocol_id() && header as u16 == opcode {
|
||||
count += 1;
|
||||
}
|
||||
offset += (header >> 16) as usize;
|
||||
}
|
||||
count
|
||||
}
|
||||
#[test]
|
||||
fn actual_launcher_protocol_is_sent_once_after_release_and_space_still_works() {
|
||||
private!(actual_launcher_protocol_is_sent_once_after_release_and_space_still_works);
|
||||
let (mut state, _events) = fixture("binding Super toggle-launcher\n");
|
||||
let (server, mut wire) = UnixStream::pair().unwrap();
|
||||
let client = state
|
||||
.display_handle
|
||||
.insert_client(server, Arc::new(crate::state::ClientState::default()))
|
||||
.unwrap();
|
||||
let shell = client
|
||||
.create_resource::<FereseShellV1, crate::shell_control::ShellData, Ferese>(
|
||||
&state.display_handle,
|
||||
10,
|
||||
Default::default(),
|
||||
)
|
||||
.unwrap();
|
||||
state.shell_resources.push(shell.downgrade());
|
||||
for code in [133, 134] {
|
||||
key(&mut state, code, true);
|
||||
assert_eq!(launcher_events(&mut state, &mut wire, &shell), 0);
|
||||
key(&mut state, code, false);
|
||||
assert_eq!(launcher_events(&mut state, &mut wire, &shell), 1);
|
||||
}
|
||||
key(&mut state, 133, true);
|
||||
tap(&mut state, 65);
|
||||
key(&mut state, 133, false);
|
||||
assert_eq!(
|
||||
launcher_events(&mut state, &mut wire, &shell),
|
||||
1,
|
||||
"Space chord must not also produce a tap"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lost_pointer_devices_and_session_reset_do_not_leave_taps_disabled() {
|
||||
let mut tap = SuperTap::default();
|
||||
tap.pointer_button("mouse-a".into(), 0x110, ButtonState::Pressed);
|
||||
tap.pointer_button("mouse-b".into(), 0x110, ButtonState::Pressed);
|
||||
tap.device_removed("mouse-a");
|
||||
assert_eq!(tap.pointer_buttons.len(), 1);
|
||||
tap.device_removed("mouse-b");
|
||||
assert!(tap.pointer_buttons.is_empty());
|
||||
tap.pointer_button("mouse-a".into(), 0x111, ButtonState::Pressed);
|
||||
tap.arm(KeyboardSource::MAIN, Keycode::new(133), None);
|
||||
tap.reset();
|
||||
assert!(tap.pointer_buttons.is_empty());
|
||||
assert!(tap.candidate.is_none());
|
||||
}
|
||||
@@ -17,30 +17,39 @@ pub(crate) struct Failure<K> {
|
||||
|
||||
/// Caller must validate the *complete* device plan first. No logical publication
|
||||
/// is permitted until success. Unavailable rollback results must be retired.
|
||||
/// `releases` includes removed outputs and old connector owners whose CRTC is
|
||||
/// reused by a change. Release those resources before per-surface staging tests.
|
||||
pub(crate) fn apply<T: HardwareTransaction>(
|
||||
driver: &mut T,
|
||||
changes: &[T::Key],
|
||||
removals: &[T::Key],
|
||||
releases: &[T::Key],
|
||||
) -> Result<(), Failure<T::Key>> {
|
||||
let mut touched = Vec::new();
|
||||
let mut touched_changes = Vec::new();
|
||||
let mut touched_releases = Vec::new();
|
||||
let result = (|| {
|
||||
for key in releases {
|
||||
touched_releases.push(*key);
|
||||
driver.disable(*key)?;
|
||||
}
|
||||
for key in changes {
|
||||
touched.push(*key);
|
||||
touched_changes.push(*key);
|
||||
driver.stage(*key)?;
|
||||
}
|
||||
for key in changes {
|
||||
driver.commit(*key)?;
|
||||
}
|
||||
for key in removals {
|
||||
touched.push(*key);
|
||||
driver.disable(*key)?;
|
||||
}
|
||||
Ok::<_, String>(())
|
||||
})();
|
||||
if let Err(error) = result {
|
||||
driver.abort_staged();
|
||||
let unavailable = touched
|
||||
// Drop new surfaces first, then restore surviving changed outputs before
|
||||
// re-enabling former owners. Restoring a released CRTC too early can
|
||||
// exceed the very resource budget that required its release.
|
||||
let unavailable = touched_changes
|
||||
.into_iter()
|
||||
.rev()
|
||||
.filter(|key| !touched_releases.contains(key))
|
||||
.chain(touched_releases.iter().rev().copied())
|
||||
.filter(|key| driver.restore(*key).is_err())
|
||||
.collect();
|
||||
return Err(Failure { error, unavailable });
|
||||
@@ -48,6 +57,149 @@ pub(crate) fn apply<T: HardwareTransaction>(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod resource_tests {
|
||||
use super::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// Per-CRTC staging probes see the current hardware plus that one proposed
|
||||
/// mode. The final plan fits, but an old HDMI route consumes the budget that
|
||||
/// a surviving DP output's higher refresh needs. CRTC 589 is then reused by
|
||||
/// another connector, so it appears in both changes and pre-releases.
|
||||
struct LimitedHardware {
|
||||
active: BTreeMap<u16, u8>,
|
||||
previous: BTreeMap<u16, u8>,
|
||||
desired: BTreeMap<u16, u8>,
|
||||
events: Vec<(&'static str, u16)>,
|
||||
fail: Option<(&'static str, u16)>,
|
||||
restore_fails: Option<u16>,
|
||||
}
|
||||
|
||||
impl LimitedHardware {
|
||||
fn new() -> Self {
|
||||
let previous = BTreeMap::from([(205, 0), (397, 1), (589, 2)]);
|
||||
Self {
|
||||
active: previous.clone(),
|
||||
previous,
|
||||
desired: BTreeMap::from([(205, 0), (397, 2), (589, 1)]),
|
||||
events: vec![],
|
||||
fail: None,
|
||||
restore_fails: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn fits(&self, key: u16, cost: u8) -> Result<(), String> {
|
||||
let mut proposed = self.active.clone();
|
||||
proposed.insert(key, cost);
|
||||
if proposed.values().copied().sum::<u8>() > 3 {
|
||||
Err("intermediate topology exceeds device resources".into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn event(&mut self, operation: &'static str, key: u16) -> Result<(), String> {
|
||||
self.events.push((operation, key));
|
||||
if self.fail == Some((operation, key)) {
|
||||
Err(format!("injected {operation} failure on {key}"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HardwareTransaction for LimitedHardware {
|
||||
type Key = u16;
|
||||
|
||||
fn stage(&mut self, key: u16) -> Result<(), String> {
|
||||
self.fits(key, self.desired[&key])?;
|
||||
self.event("stage", key)
|
||||
}
|
||||
|
||||
fn commit(&mut self, key: u16) -> Result<(), String> {
|
||||
self.fits(key, self.desired[&key])?;
|
||||
self.active.insert(key, self.desired[&key]);
|
||||
// A failed operation can have already changed hardware.
|
||||
self.event("commit", key)
|
||||
}
|
||||
|
||||
fn disable(&mut self, key: u16) -> Result<(), String> {
|
||||
self.active.remove(&key);
|
||||
self.event("disable", key)
|
||||
}
|
||||
|
||||
fn abort_staged(&mut self) {
|
||||
self.events.push(("abort", 0));
|
||||
self.active.retain(|key, _| self.previous.contains_key(key));
|
||||
}
|
||||
|
||||
fn restore(&mut self, key: u16) -> Result<(), String> {
|
||||
self.events.push(("restore", key));
|
||||
if self.restore_fails == Some(key) {
|
||||
self.active.remove(&key);
|
||||
return Err("previous output unavailable".into());
|
||||
}
|
||||
if let Some(cost) = self.previous.get(&key).copied() {
|
||||
self.fits(key, cost)?;
|
||||
self.active.insert(key, cost);
|
||||
} else {
|
||||
self.active.remove(&key);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reused_crtc_releases_its_old_connector_before_other_modes_are_tested() {
|
||||
let mut hardware = LimitedHardware::new();
|
||||
assert_eq!(hardware.desired.values().copied().sum::<u8>(), 3);
|
||||
apply(&mut hardware, &[397, 589], &[589]).unwrap();
|
||||
assert_eq!(hardware.active, hardware.desired);
|
||||
assert_eq!(hardware.events[0], ("disable", 589));
|
||||
assert!(hardware.events.iter().all(|(_, key)| *key != 205));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rollback_reverts_new_modes_before_restoring_released_connector_owners() {
|
||||
for failure in [("stage", 589), ("commit", 397), ("commit", 589)] {
|
||||
let mut hardware = LimitedHardware::new();
|
||||
hardware.fail = Some(failure);
|
||||
let result = apply(&mut hardware, &[397, 589], &[589]).unwrap_err();
|
||||
assert!(result.error.starts_with("injected"), "{}", result.error);
|
||||
assert!(result.unavailable.is_empty(), "{result:?}");
|
||||
assert_eq!(hardware.active, hardware.previous);
|
||||
assert_eq!(
|
||||
&hardware.events[hardware.events.len() - 3..],
|
||||
&[("abort", 0), ("restore", 397), ("restore", 589)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partially_failed_release_restores_the_old_output_without_staging_changes() {
|
||||
let mut hardware = LimitedHardware::new();
|
||||
hardware.fail = Some(("disable", 589));
|
||||
let result = apply(&mut hardware, &[397, 589], &[589]).unwrap_err();
|
||||
assert_eq!(result.error, "injected disable failure on 589");
|
||||
assert!(result.unavailable.is_empty());
|
||||
assert_eq!(hardware.active, hardware.previous);
|
||||
assert_eq!(
|
||||
hardware.events,
|
||||
[("disable", 589), ("abort", 0), ("restore", 589)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_rollback_reports_lost_output_and_still_restores_the_released_owner() {
|
||||
let mut hardware = LimitedHardware::new();
|
||||
hardware.fail = Some(("commit", 589));
|
||||
hardware.restore_fails = Some(397);
|
||||
let result = apply(&mut hardware, &[397, 589], &[589]).unwrap_err();
|
||||
assert_eq!(result.unavailable, [397]);
|
||||
assert_eq!(hardware.active, BTreeMap::from([(205, 0), (589, 2)]));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
//! Compare damaged swapchain frames with an independently repainted scene.
|
||||
use super::*;
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::element::solid::SolidColorBuffer;
|
||||
use smithay::utils::user_data::UserDataMap;
|
||||
|
||||
fn blur(
|
||||
renderer: &mut GlesRenderer,
|
||||
program: &BlurProgram,
|
||||
scale: f64,
|
||||
size: Size<i32, Physical>,
|
||||
transform: Transform,
|
||||
) -> BlurRenderElement {
|
||||
let sample = Rectangle::<i32, Logical>::new((0, 10).into(), (145, 80).into());
|
||||
let visible = Rectangle::<i32, Logical>::new((10, 20).into(), (120, 55).into());
|
||||
let sample_physical = sample.to_physical_precise_round(scale);
|
||||
let framebuffer = framebuffer_clip_rect(sample_physical, size, transform.invert());
|
||||
let capture = framebuffer_capture_rect(framebuffer);
|
||||
let texture = renderer
|
||||
.create_buffer(Fourcc::Abgr8888, (capture[2], capture[3]).into())
|
||||
.unwrap();
|
||||
BlurRenderElement::new(
|
||||
texture,
|
||||
program.0.clone(),
|
||||
&MaterialParameters {
|
||||
panel: None,
|
||||
corner_radii: [0.; 4],
|
||||
presentation_alpha: 1.,
|
||||
background_opacity: 0.,
|
||||
blur: (8. * scale) as f32,
|
||||
sample_geometry: sample,
|
||||
sample_physical,
|
||||
sample_framebuffer: framebuffer,
|
||||
shadow_rect: [0.; 4],
|
||||
shadow_values: [0.; 2],
|
||||
shadow_bounds: visible,
|
||||
geometry: visible,
|
||||
tint: [0.; 4],
|
||||
generation: 0,
|
||||
opaque: false,
|
||||
visible_framebuffer: framebuffer_clip_rect(
|
||||
visible.to_physical_precise_round(scale),
|
||||
size,
|
||||
transform.invert(),
|
||||
),
|
||||
},
|
||||
Arc::new(AtomicBool::new(true)),
|
||||
)
|
||||
}
|
||||
|
||||
fn pixels(renderer: &mut GlesRenderer, texture: &mut GlesTexture) -> Vec<u8> {
|
||||
let size = texture.size();
|
||||
let target = renderer.bind(texture).unwrap();
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888)
|
||||
.unwrap();
|
||||
renderer.map_texture(&mapping).unwrap().to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires an EGL rendering device"]
|
||||
fn blur_behind_moving_opaque_window_matches_full_frames_at_buffer_age_three() {
|
||||
let mut renderer = {
|
||||
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||
let software = std::env::var_os("FERESE_TEST_EGL_SOFTWARE").is_some();
|
||||
let device = EGLDevice::enumerate()
|
||||
.unwrap()
|
||||
.find(|device| device.is_software() == software)
|
||||
.unwrap();
|
||||
// SAFETY: The enumerated device remains owned by the EGL display/context.
|
||||
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||
// SAFETY: This test owns the renderer's fresh context exclusively.
|
||||
unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }
|
||||
};
|
||||
let mut resources = RenderResources::default();
|
||||
let program = blur_program(&mut resources, &mut renderer).unwrap();
|
||||
for scale in [1., 1.5, 2.] {
|
||||
for transform in [Transform::Normal, Transform::Flipped180] {
|
||||
let size: Size<i32, Physical> = ((160. * scale) as i32, (100. * scale) as i32).into();
|
||||
let mut tracker = OutputDamageTracker::new(size, scale, transform);
|
||||
let mut targets = (0..3)
|
||||
.map(|_| {
|
||||
renderer
|
||||
.create_buffer(Fourcc::Abgr8888, (size.w, size.h).into())
|
||||
.unwrap()
|
||||
})
|
||||
.collect::<Vec<GlesTexture>>();
|
||||
let mut reference: GlesTexture = renderer
|
||||
.create_buffer(Fourcc::Abgr8888, (size.w, size.h).into())
|
||||
.unwrap();
|
||||
let effect = blur(&mut renderer, &program, scale, size, transform);
|
||||
let mut background = SolidColorBuffer::new((160, 100), [0.1, 0.7, 0.2, 1.]);
|
||||
let foreground = SolidColorBuffer::new((60, 90), [0.2, 0.1, 0.8, 1.]);
|
||||
for frame_index in 0..9 {
|
||||
if frame_index % 3 == 0 {
|
||||
background.set_color([0.1, 0.3 + frame_index as f32 * 0.05, 0.2, 1.]);
|
||||
}
|
||||
let solid = || {
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&background,
|
||||
(0, 0),
|
||||
scale,
|
||||
1.,
|
||||
RenderElementKind::Unspecified,
|
||||
)
|
||||
};
|
||||
let front = SolidColorRenderElement::from_buffer(
|
||||
&foreground,
|
||||
(((100 - frame_index * 3) as f64 * scale) as i32, 0),
|
||||
scale,
|
||||
1.,
|
||||
RenderElementKind::Unspecified,
|
||||
);
|
||||
let elements: Vec<AnimatedWindowRenderElement> =
|
||||
vec![front.clone().into(), effect.clone().into(), solid().into()];
|
||||
backdrop::update(&elements, scale);
|
||||
let slot = frame_index as usize % 3;
|
||||
{
|
||||
let mut target = renderer.bind(&mut targets[slot]).unwrap();
|
||||
tracker
|
||||
.render_output(
|
||||
&mut renderer,
|
||||
&mut target,
|
||||
if frame_index < 3 { 0 } else { 3 },
|
||||
&elements,
|
||||
Color32F::TRANSPARENT,
|
||||
)
|
||||
.unwrap()
|
||||
.sync
|
||||
.wait()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Paint every layer without opaque culling or damage reuse.
|
||||
// A separate capture texture prevents the oracle from repairing
|
||||
// the incremental renderer's cached copy as a side effect.
|
||||
let reference_blur = blur(&mut renderer, &program, scale, size, transform);
|
||||
let full_elements: Vec<AnimatedWindowRenderElement> =
|
||||
vec![front.into(), reference_blur.into(), solid().into()];
|
||||
{
|
||||
let mut target = renderer.bind(&mut reference).unwrap();
|
||||
let mut frame = renderer
|
||||
.render(&mut target, size, transform.invert())
|
||||
.unwrap();
|
||||
let full = Rectangle::from_size(size);
|
||||
frame.clear(Color32F::TRANSPARENT, &[full]).unwrap();
|
||||
for element in full_elements.iter().rev() {
|
||||
let cache = UserDataMap::default();
|
||||
let rect = element.geometry(scale.into());
|
||||
if element.is_framebuffer_effect() {
|
||||
element
|
||||
.capture_framebuffer(&mut frame, element.src(), rect, &cache)
|
||||
.unwrap();
|
||||
}
|
||||
element
|
||||
.draw(
|
||||
&mut frame,
|
||||
element.src(),
|
||||
rect,
|
||||
&[Rectangle::from_size(rect.size)],
|
||||
&[],
|
||||
element.is_framebuffer_effect().then_some(&cache),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
frame.finish().unwrap().wait().unwrap();
|
||||
}
|
||||
let actual = pixels(&mut renderer, &mut targets[slot]);
|
||||
let expected = pixels(&mut renderer, &mut reference);
|
||||
let differing = actual
|
||||
.iter()
|
||||
.zip(&expected)
|
||||
.filter(|(a, b)| a.abs_diff(**b) > 1)
|
||||
.count();
|
||||
assert_eq!(
|
||||
differing, 0,
|
||||
"scale={scale} transform={transform:?} frame={frame_index}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ pub(crate) fn capture_resize_snapshot(
|
||||
) -> Result<Option<ResizeSnapshot>, GlesError> {
|
||||
// Called before on_commit_buffer_handler: renderer surface state still
|
||||
// owns the previous buffer during an actual resize handoff.
|
||||
render_window_texture(renderer, window, geometry, scale, remaining, None)
|
||||
render_window_texture(renderer, window, geometry, scale, remaining, None, true)
|
||||
}
|
||||
|
||||
fn render_window_texture(
|
||||
@@ -28,6 +28,7 @@ fn render_window_texture(
|
||||
scale: f64,
|
||||
remaining: usize,
|
||||
reusable: Option<GlesTexture>,
|
||||
before_source_release: bool,
|
||||
) -> Result<Option<ResizeSnapshot>, GlesError> {
|
||||
let Some(toplevel) = window.toplevel() else {
|
||||
return Ok(None);
|
||||
@@ -68,7 +69,13 @@ fn render_window_texture(
|
||||
let damage = Rectangle::from_size(size);
|
||||
frame.clear(Color32F::new(0.0, 0.0, 0.0, 0.0), &[damage])?;
|
||||
draw_render_elements(&mut frame, scale, &content, &[damage])?;
|
||||
let _ = frame.finish()?;
|
||||
let copied = frame.finish()?;
|
||||
if before_source_release {
|
||||
// This function runs before the old wl_buffer is replaced. The
|
||||
// client may immediately reuse it after release, so the detached
|
||||
// animation image must be complete before that release is possible.
|
||||
copied.wait().map_err(|_| GlesError::SyncInterrupted)?;
|
||||
}
|
||||
}
|
||||
Ok(Some(ResizeSnapshot {
|
||||
texture,
|
||||
@@ -118,6 +125,7 @@ pub(crate) fn capture_window_frame(
|
||||
scale,
|
||||
128 * 1024 * 1024,
|
||||
reusable,
|
||||
false,
|
||||
)
|
||||
.map_err(|error| error.to_string())?
|
||||
.ok_or("Window has no capturable content")?;
|
||||
@@ -422,6 +430,7 @@ pub(crate) fn capture_stream_window(
|
||||
info.scale,
|
||||
128 * 1024 * 1024,
|
||||
reusable,
|
||||
false,
|
||||
)
|
||||
.map_err(|error| error.to_string())?
|
||||
.ok_or("Window has no capturable content")?;
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Real input-method protocol resources and SHM bounds; no host GUI is used.
|
||||
use super::layer_popup_geometry;
|
||||
use crate::Ferese;
|
||||
use crate::state::{dispatch_test_client as dispatch, test_client_request as request, test_window};
|
||||
use smithay::desktop::PopupKind;
|
||||
use smithay::reexports::calloop::EventLoop;
|
||||
use smithay::reexports::wayland_protocols_misc::zwp_input_method_v2::server::zwp_input_method_manager_v2::ZwpInputMethodManagerV2;
|
||||
use smithay::reexports::wayland_server::protocol::{
|
||||
wl_compositor::WlCompositor, wl_shm::WlShm, wl_surface::WlSurface,
|
||||
};
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::utils::{Physical, Point, Rectangle};
|
||||
use smithay::wayland::input_method::PopupParent;
|
||||
use smithay::wayland::text_input::TextInputSeat;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
fn bind_seat(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, wire: &mut UnixStream) {
|
||||
request(wire, 1, 1, &[7], None);
|
||||
dispatch(events, state);
|
||||
wire.set_nonblocking(true).unwrap();
|
||||
let mut bytes = Vec::new();
|
||||
let _ = wire.read_to_end(&mut bytes);
|
||||
wire.set_nonblocking(false).unwrap();
|
||||
let mut offset = 0;
|
||||
let mut seat = None;
|
||||
while offset < bytes.len() {
|
||||
let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap());
|
||||
let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap());
|
||||
if object == 7 && header as u16 == 0 {
|
||||
let length =
|
||||
u32::from_ne_bytes(bytes[offset + 12..offset + 16].try_into().unwrap()) as usize;
|
||||
if &bytes[offset + 16..offset + 16 + length] == b"wl_seat\0" {
|
||||
seat = Some(u32::from_ne_bytes(
|
||||
bytes[offset + 8..offset + 12].try_into().unwrap(),
|
||||
));
|
||||
}
|
||||
}
|
||||
offset += (header >> 16) as usize;
|
||||
}
|
||||
let mut args = vec![seat.unwrap(), 8];
|
||||
args.extend(
|
||||
b"wl_seat\0"
|
||||
.chunks_exact(4)
|
||||
.map(|word| u32::from_ne_bytes(word.try_into().unwrap())),
|
||||
);
|
||||
args.extend([7, 8]);
|
||||
request(wire, 7, 0, &args, None);
|
||||
dispatch(events, state);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_method_popup_preserves_layer_origin_and_uses_own_surface_bounds() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"render::scene::layer_popup_tests::input_method_popup_preserves_layer_origin_and_uses_own_surface_bounds",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let mut events = EventLoop::try_new().unwrap();
|
||||
let mut state = crate::startup_tests::state(&mut events);
|
||||
let (window, mut wire) = test_window(&mut state, &mut events, 0xff102030);
|
||||
let parent = window.toplevel().unwrap().wl_surface().clone();
|
||||
let client = parent.client().unwrap();
|
||||
state.seat.text_input().set_focus(Some(parent.clone()));
|
||||
bind_seat(&mut state, &mut events, &mut wire);
|
||||
let manager = client
|
||||
.create_resource::<ZwpInputMethodManagerV2, smithay::wayland::GlobalData, Ferese>(
|
||||
&state.display_handle,
|
||||
1,
|
||||
smithay::wayland::GlobalData,
|
||||
)
|
||||
.unwrap();
|
||||
let compositor = client
|
||||
.create_resource::<WlCompositor, smithay::wayland::GlobalData, Ferese>(
|
||||
&state.display_handle,
|
||||
6,
|
||||
smithay::wayland::GlobalData,
|
||||
)
|
||||
.unwrap();
|
||||
let shm = client
|
||||
.create_resource::<WlShm, smithay::wayland::GlobalData, Ferese>(
|
||||
&state.display_handle,
|
||||
1,
|
||||
smithay::wayland::GlobalData,
|
||||
)
|
||||
.unwrap();
|
||||
request(&mut wire, manager.id().protocol_id(), 0, &[8, 9], None);
|
||||
request(&mut wire, compositor.id().protocol_id(), 0, &[10], None);
|
||||
request(&mut wire, 9, 4, &[11, 10], None); // get_input_popup_surface
|
||||
let mut file = tempfile::tempfile().unwrap();
|
||||
file.write_all(&0xff204060u32.to_ne_bytes().repeat(364 * 80))
|
||||
.unwrap();
|
||||
request(
|
||||
&mut wire,
|
||||
shm.id().protocol_id(),
|
||||
0,
|
||||
&[12, 364 * 80 * 4],
|
||||
Some(file.as_raw_fd()),
|
||||
);
|
||||
request(&mut wire, 12, 0, &[13, 0, 364, 80, 364 * 4, 0], None);
|
||||
request(&mut wire, 10, 1, &[13, 0, 0], None);
|
||||
request(&mut wire, 10, 6, &[], None);
|
||||
dispatch(&mut events, &mut state);
|
||||
let surface = client
|
||||
.object_from_protocol_id::<WlSurface>(&state.display_handle, 10)
|
||||
.unwrap();
|
||||
let Some(PopupKind::InputMethod(mut popup)) = state.popups.find_popup(&surface) else {
|
||||
panic!("input-method popup must be tracked");
|
||||
};
|
||||
|
||||
// Parent geometry is global, but the layer renderer receives output-local
|
||||
// coordinates. Cover a primary output and a secondary output left/above it.
|
||||
for output_origin in [(0, 0), (-1920, -300)] {
|
||||
let layer_origin = Point::from((360, 241));
|
||||
let global_parent =
|
||||
Rectangle::new(layer_origin + Point::from(output_origin), (560, 70).into());
|
||||
popup.set_parent(Some(PopupParent {
|
||||
surface: parent.clone(),
|
||||
location: global_parent,
|
||||
}));
|
||||
popup.set_location((148, 55).into());
|
||||
let kind = PopupKind::InputMethod(popup.clone());
|
||||
assert_eq!(kind.geometry(), global_parent);
|
||||
let (bounds, content_origin) = layer_popup_geometry(layer_origin, popup.location(), &kind);
|
||||
assert_eq!(content_origin, Point::from((508, 296)));
|
||||
assert_eq!(
|
||||
bounds,
|
||||
Rectangle::new((508, 296).into(), (364, 80).into()),
|
||||
"candidate bounds must come from its own buffer, not its parent"
|
||||
);
|
||||
for scale in [1.0, 1.5, 2.0] {
|
||||
let pixels: Rectangle<i32, Physical> = bounds.to_physical_precise_round(scale);
|
||||
assert_eq!(pixels.loc, content_origin.to_physical_precise_round(scale));
|
||||
assert_eq!(pixels.size.h, (80.0 * scale) as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
+128
-24
@@ -1,4 +1,6 @@
|
||||
mod backdrop;
|
||||
#[cfg(test)]
|
||||
mod backdrop_gpu_tests;
|
||||
mod capture;
|
||||
mod closing;
|
||||
mod window_blur;
|
||||
@@ -272,9 +274,25 @@ impl Element for BlurRenderElement {
|
||||
fn kind(&self) -> RenderElementKind {
|
||||
RenderElementKind::Unspecified
|
||||
}
|
||||
|
||||
fn is_framebuffer_effect(&self) -> bool {
|
||||
// The complete sampling region must be repainted before capture,
|
||||
// including pixels hidden by opaque elements above this blur.
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderElement<GlesRenderer> for BlurRenderElement {
|
||||
fn capture_framebuffer(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_, '_>,
|
||||
_src: Rectangle<f64, Buffer>,
|
||||
_dst: Rectangle<i32, Physical>,
|
||||
_cache: &smithay::utils::user_data::UserDataMap,
|
||||
) -> Result<(), GlesError> {
|
||||
self.capture(frame)
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_, '_>,
|
||||
@@ -282,14 +300,37 @@ impl RenderElement<GlesRenderer> for BlurRenderElement {
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque_regions: &[Rectangle<i32, Physical>],
|
||||
_cache: Option<&smithay::utils::user_data::UserDataMap>,
|
||||
cache: Option<&smithay::utils::user_data::UserDataMap>,
|
||||
) -> Result<(), GlesError> {
|
||||
// Simple full-frame renderers do not implement the effect callback.
|
||||
// Damage-tracked outputs must use its callback, never the dirty flag:
|
||||
// the tracker knows when occlusion or buffer restoration needs a copy.
|
||||
if cache.is_none() && self.capture_dirty.load(Ordering::Relaxed) {
|
||||
self.capture(frame)?;
|
||||
}
|
||||
frame.render_texture_from_to(
|
||||
&self.texture,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
Transform::Normal,
|
||||
1.0,
|
||||
Some(&self.program),
|
||||
&self.uniforms,
|
||||
)
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl BlurRenderElement {
|
||||
fn capture(&self, frame: &mut GlesFrame<'_, '_>) -> Result<(), GlesError> {
|
||||
let texture = self.texture.tex_id();
|
||||
let [x, y, width, height] = self.capture_rect;
|
||||
// Buffer-age restoration and unrelated partial damage must reuse the
|
||||
// clean captured scene. Recapturing a partly repainted framebuffer
|
||||
// would feed previous blurred pixels back into the blur.
|
||||
if self.capture_dirty.load(Ordering::Relaxed) {
|
||||
{
|
||||
// SAFETY: with_context makes this frame context current. GL writes into live local
|
||||
// scalars, and the capture texture belongs to this renderer with the allocated
|
||||
// framebuffer bounds.
|
||||
@@ -341,22 +382,7 @@ impl RenderElement<GlesRenderer> for BlurRenderElement {
|
||||
}
|
||||
self.capture_dirty.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
frame.render_texture_from_to(
|
||||
&self.texture,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque_regions,
|
||||
Transform::Normal,
|
||||
1.0,
|
||||
Some(&self.program),
|
||||
&self.uniforms,
|
||||
)
|
||||
}
|
||||
|
||||
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
|
||||
None
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -538,6 +564,47 @@ fn clipped_surface_damage(
|
||||
}
|
||||
}
|
||||
|
||||
fn sampled_surface_damage(
|
||||
size: Size<i32, Physical>,
|
||||
source: Size<f64, Buffer>,
|
||||
transform: Transform,
|
||||
clip_changed: bool,
|
||||
client_damage: impl FnOnce() -> DamageSet<i32, Physical>,
|
||||
) -> DamageSet<i32, Physical> {
|
||||
clipped_surface_damage(size, clip_changed, || {
|
||||
// Linear filtering also changes destination samples just outside the
|
||||
// changed texels. Smithay maps their rectangle to physical coordinates,
|
||||
// but does not include this half-texel sampling footprint. Expanding in
|
||||
// physical pixels prevents stale fringes on fractional-scale redraws.
|
||||
let source = transform.transform_size(source);
|
||||
let footprint = |pixels: i32, texels: f64| {
|
||||
if texels > 0.0 {
|
||||
(f64::from(pixels) / texels * 0.5).ceil() as i32
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
let x = footprint(size.w, source.w);
|
||||
let y = footprint(size.h, source.h);
|
||||
let bounds = Rectangle::from_size(size);
|
||||
client_damage()
|
||||
.into_iter()
|
||||
.filter(|rect| !rect.is_empty())
|
||||
.filter_map(|rect| {
|
||||
Rectangle::new(
|
||||
(rect.loc.x.saturating_sub(x), rect.loc.y.saturating_sub(y)).into(),
|
||||
(
|
||||
rect.size.w.saturating_add(x.saturating_mul(2)),
|
||||
rect.size.h.saturating_add(y.saturating_mul(2)),
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
.intersection(bounds)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
impl Element for RoundedSurfaceRenderElement {
|
||||
fn id(&self) -> &Id {
|
||||
self.inner.id()
|
||||
@@ -564,9 +631,13 @@ impl Element for RoundedSurfaceRenderElement {
|
||||
scale: RenderScale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> DamageSet<i32, Physical> {
|
||||
clipped_surface_damage(self.inner.geometry(scale).size, self.clip_changed, || {
|
||||
self.inner.damage_since(scale, commit)
|
||||
})
|
||||
sampled_surface_damage(
|
||||
self.inner.geometry(scale).size,
|
||||
self.inner.src().size,
|
||||
self.inner.transform(),
|
||||
self.clip_changed,
|
||||
|| self.inner.damage_since(scale, commit),
|
||||
)
|
||||
}
|
||||
|
||||
fn opaque_regions(&self, scale: RenderScale<f64>) -> OpaqueRegions<i32, Physical> {
|
||||
@@ -688,6 +759,39 @@ mod tests {
|
||||
shadow_bounds,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn filtered_surface_damage_stays_local_and_preserves_full_clip_changes() {
|
||||
use smithay::backend::renderer::utils::DamageSet;
|
||||
let size = (120, 80).into();
|
||||
let source = (80.0, 40.0).into();
|
||||
for (transform, x_padding) in [(Transform::Normal, 1), (Transform::_90, 2)] {
|
||||
let damage = super::sampled_surface_damage(size, source, transform, false, || {
|
||||
DamageSet::from_slice(&[
|
||||
Rectangle::new((0, 0).into(), (4, 3).into()),
|
||||
Rectangle::new((118, 78).into(), (2, 2).into()),
|
||||
])
|
||||
});
|
||||
assert_eq!(
|
||||
damage.iter().copied().collect::<Vec<_>>(),
|
||||
vec![
|
||||
Rectangle::new((0, 0).into(), (4 + x_padding, 4).into()),
|
||||
Rectangle::new((118 - x_padding, 77).into(), (2 + x_padding, 3).into(),),
|
||||
],
|
||||
);
|
||||
assert!(
|
||||
super::sampled_surface_damage(size, source, transform, false, DamageSet::default)
|
||||
.is_empty()
|
||||
);
|
||||
let full = super::sampled_surface_damage(size, source, transform, true, || {
|
||||
panic!("a changed clip must keep full damage without consulting the client")
|
||||
});
|
||||
assert_eq!(
|
||||
full.iter().copied().collect::<Vec<_>>(),
|
||||
vec![Rectangle::from_size(size)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires an EGL rendering device"]
|
||||
fn solid_corner_mask_preserves_premultiplied_color() {
|
||||
|
||||
@@ -556,9 +556,7 @@ pub(super) fn layer_elements(
|
||||
let mut elements = Vec::new();
|
||||
for (geometry, layer) in layers {
|
||||
for (popup, offset) in PopupManager::popups_for_surface(layer.wl_surface()) {
|
||||
let mut bounds = popup.geometry();
|
||||
let content_origin = geometry.loc + offset - bounds.loc;
|
||||
bounds.loc = geometry.loc + offset;
|
||||
let (bounds, content_origin) = layer_popup_geometry(geometry.loc, offset, &popup);
|
||||
append_material_surface(
|
||||
state,
|
||||
renderer,
|
||||
@@ -587,6 +585,27 @@ pub(super) fn layer_elements(
|
||||
elements
|
||||
}
|
||||
|
||||
fn layer_popup_geometry(
|
||||
layer_origin: Point<i32, Logical>,
|
||||
offset: Point<i32, Logical>,
|
||||
popup: &smithay::desktop::PopupKind,
|
||||
) -> (Rectangle<i32, Logical>, Point<i32, Logical>) {
|
||||
if matches!(popup, smithay::desktop::PopupKind::InputMethod(_)) {
|
||||
// IME geometry describes its parent in global coordinates, unlike
|
||||
// xdg window geometry. Its location is already parent-relative, and
|
||||
// its candidate surface can extend beyond the parent's bounds.
|
||||
let origin = layer_origin + offset;
|
||||
return (
|
||||
smithay::desktop::utils::bbox_from_surface_tree(popup.wl_surface(), origin),
|
||||
origin,
|
||||
);
|
||||
}
|
||||
let mut bounds = popup.geometry();
|
||||
let origin = layer_origin + offset - bounds.loc;
|
||||
bounds.loc = layer_origin + offset;
|
||||
(bounds, origin)
|
||||
}
|
||||
|
||||
fn layer_output_crop(size: Size<i32, Physical>, transform: Transform) -> Rectangle<i32, Physical> {
|
||||
Rectangle::from_size(transform.transform_size(size))
|
||||
}
|
||||
@@ -675,6 +694,10 @@ pub(super) fn cursor_elements(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "layer_popup_tests.rs"]
|
||||
mod layer_popup_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod layer_crop_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -452,6 +452,7 @@ impl SessionLockHandler for Ferese {
|
||||
}
|
||||
|
||||
fn lock(&mut self, confirmation: SessionLocker) {
|
||||
self.super_tap.cancel();
|
||||
self.dismiss_launcher();
|
||||
if !self.session_lock.lifecycle.can_acquire() {
|
||||
return;
|
||||
|
||||
@@ -505,6 +505,7 @@ pub struct Ferese {
|
||||
pub(crate) binding_modes: crate::config::BindingModes,
|
||||
pub(crate) binding_mode_pressed: HashSet<Keycode>,
|
||||
pub(crate) binding_mode_repeats: HashMap<Keycode, crate::config::Binding>,
|
||||
pub(crate) super_tap: crate::input::super_tap::SuperTap,
|
||||
workspace_auto_back_and_forth: bool,
|
||||
pub(crate) window_rules: Vec<WindowRule>,
|
||||
pub(crate) theme_settings: ThemeSettings,
|
||||
@@ -847,6 +848,7 @@ impl Ferese {
|
||||
binding_modes: config.binding_modes,
|
||||
binding_mode_pressed: HashSet::new(),
|
||||
binding_mode_repeats: HashMap::new(),
|
||||
super_tap: Default::default(),
|
||||
workspace_auto_back_and_forth: config.workspace_auto_back_and_forth,
|
||||
window_rules: config.window_rules,
|
||||
theme_settings: config.theme_settings,
|
||||
@@ -1073,6 +1075,7 @@ impl Ferese {
|
||||
self.gap_config = config.gap_config;
|
||||
self.input_settings = config.input_settings;
|
||||
if bindings_changed || input_changed {
|
||||
self.super_tap.cancel();
|
||||
self.binding_mode_repeats.clear();
|
||||
self.bindings = config.bindings.into();
|
||||
self.binding_modes = config.binding_modes;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
//! Private protocol clients and offscreen EGL; no host desktop or DRM required.
|
||||
#[path = "resize_scene_gpu_tests.rs"]
|
||||
mod resize_scene_gpu_tests;
|
||||
#[path = "partial_shm_gpu_tests.rs"]
|
||||
mod partial_shm_gpu_tests;
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::os::fd::{AsRawFd, RawFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
//! Streaming SHM updates compared with independent full imports and full repaint.
|
||||
use super::*;
|
||||
use std::borrow::Cow;
|
||||
use std::fs::File;
|
||||
use std::os::unix::fs::FileExt;
|
||||
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::element::{Element, RenderElement};
|
||||
use smithay::backend::renderer::{Color32F, Frame, Renderer};
|
||||
use smithay::utils::{Buffer, Physical, Size};
|
||||
|
||||
const WIDTH: u32 = 193;
|
||||
const HEIGHT: u32 = 137;
|
||||
const STRIDE: u32 = (WIDTH + 7) * 4;
|
||||
const OFFSET: u32 = 64;
|
||||
|
||||
struct ReusableShm {
|
||||
file: File,
|
||||
buffer: u32,
|
||||
version: usize,
|
||||
format: u32,
|
||||
}
|
||||
|
||||
fn buffer_pixels(pixels: &[u8], format: u32, switching_formats: bool) -> Cow<'_, [u8]> {
|
||||
if !switching_formats {
|
||||
return Cow::Borrowed(pixels);
|
||||
}
|
||||
let mut encoded = pixels.to_vec();
|
||||
for pixel in encoded.chunks_exact_mut(4) {
|
||||
if format == 1 {
|
||||
// XRGB's unused high byte may legally be zero.
|
||||
pixel[3] = 0;
|
||||
} else {
|
||||
// A valid premultiplied translucent ARGB surface makes a stale
|
||||
// opaque shader variant visibly differ in RGB as well as alpha.
|
||||
for channel in &mut pixel[..3] {
|
||||
*channel = (u16::from(*channel) * 128 / 255) as u8;
|
||||
}
|
||||
pixel[3] = 128;
|
||||
}
|
||||
}
|
||||
Cow::Owned(encoded)
|
||||
}
|
||||
|
||||
fn write_region(file: &File, pixels: &[u8], rect: Rectangle<i32, Buffer>) {
|
||||
for y in rect.loc.y..rect.loc.y + rect.size.h {
|
||||
let source = (y as u32 * WIDTH + rect.loc.x as u32) as usize * 4;
|
||||
let target = u64::from(OFFSET + y as u32 * STRIDE + rect.loc.x as u32 * 4);
|
||||
file.write_all_at(&pixels[source..source + rect.size.w as usize * 4], target)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn pixels(renderer: &mut GlesRenderer, texture: &mut GlesTexture) -> Vec<u8> {
|
||||
let size = smithay::backend::renderer::Texture::size(texture);
|
||||
let target = renderer.bind(texture).unwrap();
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888)
|
||||
.unwrap();
|
||||
renderer.map_texture(&mapping).unwrap().to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a private Wayland socket and an EGL rendering device"]
|
||||
fn reused_shm_partial_damage_matches_fresh_full_uploads_at_fractional_scale() {
|
||||
const TEST: &str = "state::capture_privacy::tests::partial_shm_gpu_tests::reused_shm_partial_damage_matches_fresh_full_uploads_at_fractional_scale";
|
||||
if !crate::startup_tests::private_runtime(TEST) {
|
||||
return;
|
||||
}
|
||||
run_shm_scenarios(&[(1, None), (3, None)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a private Wayland socket and an EGL rendering device"]
|
||||
fn same_sized_argb_xrgb_switches_match_fresh_full_uploads() {
|
||||
const TEST: &str = "state::capture_privacy::tests::partial_shm_gpu_tests::same_sized_argb_xrgb_switches_match_fresh_full_uploads";
|
||||
if !crate::startup_tests::private_runtime(TEST) {
|
||||
return;
|
||||
}
|
||||
run_shm_scenarios(&[(2, Some(0)), (2, Some(1))]);
|
||||
}
|
||||
|
||||
fn run_shm_scenarios(scenarios: &[(usize, Option<u32>)]) {
|
||||
let software = std::env::var_os("FERESE_TEST_EGL_SOFTWARE").is_some();
|
||||
let device = EGLDevice::enumerate()
|
||||
.unwrap()
|
||||
.find(|device| device.is_software() == software)
|
||||
.expect("requested EGL device");
|
||||
eprintln!(
|
||||
"partial SHM EGL software={software}, render node={:?}",
|
||||
device.render_device_path()
|
||||
);
|
||||
// SAFETY: The display owns the enumerated device; each renderer below owns
|
||||
// a fresh, unshared context used only by this test thread.
|
||||
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||
let mut renderer = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
|
||||
let scale = 1.5;
|
||||
let physical: Size<i32, Physical> = (480, 360).into();
|
||||
let mut mismatching_frames = 0;
|
||||
|
||||
for &(buffer_count, first_format) in scenarios {
|
||||
let switching_formats = first_format.is_some();
|
||||
let mut events = EventLoop::try_new().unwrap();
|
||||
let config = crate::config::Config::parse_source(
|
||||
"animations { enabled #false; }\nlayout { inner-gap 4; outer-gap 4; }\nwindow-rule transient=#false floating=#true\n",
|
||||
)
|
||||
.unwrap()
|
||||
.runtime_config()
|
||||
.unwrap();
|
||||
let mut state = Ferese::new(&mut events, Display::new().unwrap(), config).unwrap();
|
||||
state.animation_test_time = Some(Duration::ZERO);
|
||||
state.focus_effect.active_opacity = 1.0;
|
||||
state.focus_effect.inactive_opacity = 1.0;
|
||||
state.focus_effect.inactive_dim = 0.0;
|
||||
state.theme_settings.window_radius = 12.0;
|
||||
let output = Output::new(
|
||||
"partial-shm".into(),
|
||||
PhysicalProperties {
|
||||
serial_number: String::new(),
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: "test".into(),
|
||||
model: "test".into(),
|
||||
},
|
||||
);
|
||||
output.change_current_state(
|
||||
Some(Mode {
|
||||
size: physical,
|
||||
refresh: 60_000,
|
||||
}),
|
||||
Some(Transform::Normal),
|
||||
Some(Scale::Fractional(scale)),
|
||||
None,
|
||||
);
|
||||
state.space.map_output(&output, (0, 0));
|
||||
state.register_output(&output, "partial-shm".into());
|
||||
let (window, mut wire) = window(&mut state, &mut events, 0xff203c5c);
|
||||
let id = state.windows.ids()[&window];
|
||||
state.focused_window = Some(id);
|
||||
state.set_floating_window_geometry(
|
||||
&window,
|
||||
(56, 64).into(),
|
||||
(WIDTH as i32, HEIGHT as i32).into(),
|
||||
);
|
||||
state.display_handle.flush_clients().unwrap();
|
||||
ack_configure(&mut wire, 3);
|
||||
let client = window.toplevel().unwrap().wl_surface().client().unwrap();
|
||||
let shm = client
|
||||
.create_resource::<WlShm, smithay::wayland::GlobalData, Ferese>(
|
||||
&state.display_handle,
|
||||
1,
|
||||
smithay::wayland::GlobalData,
|
||||
)
|
||||
.unwrap();
|
||||
let compositor = client
|
||||
.create_resource::<WlCompositor, smithay::wayland::GlobalData, Ferese>(
|
||||
&state.display_handle,
|
||||
6,
|
||||
smithay::wayland::GlobalData,
|
||||
)
|
||||
.unwrap();
|
||||
let mut raster = 0xff203c5cu32
|
||||
.to_ne_bytes()
|
||||
.repeat((WIDTH * HEIGHT) as usize);
|
||||
let full: Rectangle<i32, Buffer> =
|
||||
Rectangle::from_size((WIDTH as i32, HEIGHT as i32).into());
|
||||
let mut buffers = Vec::new();
|
||||
for slot in 0..buffer_count {
|
||||
let format = first_format.map_or(0, |first| first ^ (slot as u32 % 2));
|
||||
let file = tempfile::tempfile().unwrap();
|
||||
let len = OFFSET + STRIDE * HEIGHT;
|
||||
file.set_len(u64::from(len)).unwrap();
|
||||
// Nontrivial offset/stride/padding exercise pixel-store state too.
|
||||
file.write_all_at(&vec![0xa5; len as usize], 0).unwrap();
|
||||
write_region(
|
||||
&file,
|
||||
&buffer_pixels(&raster, format, switching_formats),
|
||||
full,
|
||||
);
|
||||
let pool = 7 + slot as u32 * 2;
|
||||
let buffer = pool + 1;
|
||||
request(
|
||||
&mut wire,
|
||||
shm.id().protocol_id(),
|
||||
0,
|
||||
&[pool, len],
|
||||
Some(file.as_raw_fd()),
|
||||
);
|
||||
request(
|
||||
&mut wire,
|
||||
pool,
|
||||
0,
|
||||
&[buffer, OFFSET, WIDTH, HEIGHT, STRIDE, format],
|
||||
None,
|
||||
);
|
||||
buffers.push(ReusableShm {
|
||||
file,
|
||||
buffer,
|
||||
version: 0,
|
||||
format,
|
||||
});
|
||||
}
|
||||
let region = 7 + buffer_count as u32 * 2;
|
||||
request(&mut wire, compositor.id().protocol_id(), 1, &[region], None);
|
||||
if switching_formats {
|
||||
// ARGB is genuinely translucent in this scenario; XRGB receives
|
||||
// automatic opacity from its declared SHM format.
|
||||
request(&mut wire, 2, 4, &[0], None);
|
||||
} else {
|
||||
request(&mut wire, region, 1, &[0, 0, WIDTH, HEIGHT], None);
|
||||
request(&mut wire, 2, 4, &[region], None);
|
||||
}
|
||||
request(&mut wire, region, 0, &[], None);
|
||||
request(&mut wire, 3, 3, &[0, 0, WIDTH, HEIGHT], None);
|
||||
request(&mut wire, 2, 1, &[buffers[0].buffer, 0, 0], None);
|
||||
request(&mut wire, 2, 9, &[0, 0, WIDTH, HEIGHT], None);
|
||||
request(&mut wire, 2, 6, &[], None);
|
||||
dispatch(&mut events, &mut state);
|
||||
state.windows.update(id, |record| record.opening = None);
|
||||
state.animations_enabled = true;
|
||||
state.advance_animations_at(Duration::from_secs(1), Duration::from_secs(1));
|
||||
let fixed_geometry = state.windows.geometry(&id).unwrap().visual.current;
|
||||
assert_eq!(window.geometry().size, (WIDTH as i32, HEIGHT as i32).into());
|
||||
|
||||
let mut tracker = OutputDamageTracker::from_output(&output);
|
||||
let mut targets: Vec<GlesTexture> = (0..3)
|
||||
.map(|_| {
|
||||
renderer
|
||||
.create_buffer(Fourcc::Abgr8888, (physical.w, physical.h).into())
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
let mut history = Vec::new();
|
||||
let mut total_changed_bytes = 0;
|
||||
let mut partial_frames = 0;
|
||||
for frame_index in 0..24 {
|
||||
let mut changed = None;
|
||||
if frame_index > 0 {
|
||||
let rect = if switching_formats {
|
||||
full
|
||||
} else {
|
||||
Rectangle::<i32, Buffer>::new(
|
||||
(
|
||||
(frame_index * 37 % (WIDTH as usize - 29)) as i32,
|
||||
(frame_index * 23 % (HEIGHT as usize - 13)) as i32,
|
||||
)
|
||||
.into(),
|
||||
(29, 13).into(),
|
||||
)
|
||||
};
|
||||
let color = 0xff000000u32
|
||||
| (((frame_index * 41) % 220 + 20) as u32) << 16
|
||||
| (((frame_index * 67) % 220 + 20) as u32) << 8
|
||||
| (((frame_index * 97) % 220 + 20) as u32);
|
||||
for y in rect.loc.y..rect.loc.y + rect.size.h {
|
||||
for x in rect.loc.x..rect.loc.x + rect.size.w {
|
||||
let at = (y as u32 * WIDTH + x as u32) as usize * 4;
|
||||
raster[at..at + 4].copy_from_slice(&color.to_ne_bytes());
|
||||
}
|
||||
}
|
||||
history.push(rect);
|
||||
let buffer = &mut buffers[frame_index % buffer_count];
|
||||
let encoded = buffer_pixels(&raster, buffer.format, switching_formats);
|
||||
// A recycled client buffer catches up only the rectangles changed
|
||||
// since its last use. Surface damage describes this frame's delta
|
||||
// from the preceding surface commit, independent of buffer age.
|
||||
for pending in &history[buffer.version..] {
|
||||
write_region(&buffer.file, &encoded, *pending);
|
||||
total_changed_bytes += pending.size.w * pending.size.h * 4;
|
||||
}
|
||||
buffer.version = history.len();
|
||||
request(&mut wire, 2, 1, &[buffer.buffer, 0, 0], None);
|
||||
request(
|
||||
&mut wire,
|
||||
2,
|
||||
9,
|
||||
&[
|
||||
rect.loc.x as u32,
|
||||
rect.loc.y as u32,
|
||||
rect.size.w as u32,
|
||||
rect.size.h as u32,
|
||||
],
|
||||
None,
|
||||
);
|
||||
request(&mut wire, 2, 6, &[], None);
|
||||
dispatch(&mut events, &mut state);
|
||||
changed = Some((rect, color));
|
||||
partial_frames += 1;
|
||||
}
|
||||
state.advance_animations_at(
|
||||
Duration::from_millis(16),
|
||||
Duration::from_millis(1016 + frame_index as u64 * 16),
|
||||
);
|
||||
let scene = state.sample_frame(&output, Duration::ZERO);
|
||||
assert_eq!(
|
||||
scene.windows[&id].presentation.bounds.current, fixed_geometry,
|
||||
"streaming moved the window"
|
||||
);
|
||||
assert_eq!(scene.windows[&id].presentation.alpha(), 1.0);
|
||||
let elements = crate::render::sampled_output_elements(
|
||||
&mut state,
|
||||
&mut renderer,
|
||||
&output,
|
||||
false,
|
||||
&scene,
|
||||
);
|
||||
assert!(
|
||||
elements
|
||||
.iter()
|
||||
.all(|element| !element.is_framebuffer_effect())
|
||||
);
|
||||
let slot = frame_index % 3;
|
||||
{
|
||||
let mut target = renderer.bind(&mut targets[slot]).unwrap();
|
||||
tracker
|
||||
.render_output(
|
||||
&mut renderer,
|
||||
&mut target,
|
||||
if frame_index < 3 { 0 } else { 3 },
|
||||
&elements,
|
||||
Color32F::new(0.035, 0.04, 0.055, 1.0),
|
||||
)
|
||||
.unwrap()
|
||||
.sync
|
||||
.wait()
|
||||
.unwrap();
|
||||
}
|
||||
let actual = pixels(&mut renderer, &mut targets[slot]);
|
||||
let cached_format = smithay::backend::renderer::utils::with_renderer_surface_state(
|
||||
window.toplevel().unwrap().wl_surface(),
|
||||
|surface| {
|
||||
surface
|
||||
.texture::<GlesTexture>(renderer.context_id())
|
||||
.and_then(smithay::backend::renderer::Texture::format)
|
||||
},
|
||||
)
|
||||
.flatten();
|
||||
|
||||
// A fresh, unshared context cannot reuse the display path's imported
|
||||
// SHM texture. Smithay creates a new texture with a complete TexImage2D
|
||||
// upload regardless of partial damage, then this oracle repaints every
|
||||
// layer without damage history or opaque-region culling.
|
||||
let mut oracle =
|
||||
unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
|
||||
let oracle_context = oracle.context_id().erased();
|
||||
let mut reference: GlesTexture = oracle
|
||||
.create_buffer(Fourcc::Abgr8888, (physical.w, physical.h).into())
|
||||
.unwrap();
|
||||
let full_elements = crate::render::sampled_output_elements(
|
||||
&mut state,
|
||||
&mut oracle,
|
||||
&output,
|
||||
false,
|
||||
&scene,
|
||||
);
|
||||
let fresh_format = smithay::backend::renderer::utils::with_renderer_surface_state(
|
||||
window.toplevel().unwrap().wl_surface(),
|
||||
|surface| {
|
||||
surface
|
||||
.texture::<GlesTexture>(oracle.context_id())
|
||||
.and_then(smithay::backend::renderer::Texture::format)
|
||||
},
|
||||
)
|
||||
.flatten();
|
||||
{
|
||||
let mut target = oracle.bind(&mut reference).unwrap();
|
||||
let mut frame = oracle
|
||||
.render(&mut target, physical, Transform::Normal)
|
||||
.unwrap();
|
||||
frame
|
||||
.clear(
|
||||
Color32F::new(0.035, 0.04, 0.055, 1.0),
|
||||
&[Rectangle::from_size(physical)],
|
||||
)
|
||||
.unwrap();
|
||||
for element in full_elements.iter().rev() {
|
||||
let rect = element.geometry(scale.into());
|
||||
element
|
||||
.draw(
|
||||
&mut frame,
|
||||
element.src(),
|
||||
rect,
|
||||
&[Rectangle::from_size(rect.size)],
|
||||
&[],
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
frame.finish().unwrap().wait().unwrap();
|
||||
}
|
||||
let expected = pixels(&mut oracle, &mut reference);
|
||||
let differing = actual
|
||||
.iter()
|
||||
.zip(&expected)
|
||||
.filter(|(a, b)| a.abs_diff(**b) > 1)
|
||||
.count();
|
||||
if differing > 0 {
|
||||
mismatching_frames += 1;
|
||||
let differences = actual
|
||||
.chunks_exact(4)
|
||||
.zip(expected.chunks_exact(4))
|
||||
.enumerate()
|
||||
.filter(|(_, (a, b))| a.iter().zip(*b).any(|(x, y)| x.abs_diff(*y) > 1))
|
||||
.map(|(at, (a, b))| {
|
||||
(
|
||||
(at % physical.w as usize, at / physical.w as usize),
|
||||
a.to_vec(),
|
||||
b.to_vec(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let bounds = differences.iter().fold(
|
||||
(usize::MAX, usize::MAX, 0, 0),
|
||||
|(l, t, r, b), ((x, y), _, _)| (l.min(*x), t.min(*y), r.max(*x), b.max(*y)),
|
||||
);
|
||||
eprintln!(
|
||||
"reused client buffers={buffer_count}, partial frame={frame_index}, different channels={differing}, pixel bounds={bounds:?}, samples={:?}",
|
||||
&differences[..differences.len().min(8)]
|
||||
);
|
||||
if switching_formats {
|
||||
let rgb_differing = actual
|
||||
.chunks_exact(4)
|
||||
.zip(expected.chunks_exact(4))
|
||||
.filter(|(a, b)| {
|
||||
a[..3].iter().zip(&b[..3]).any(|(x, y)| x.abs_diff(*y) > 1)
|
||||
})
|
||||
.count();
|
||||
eprintln!(
|
||||
"same-size SHM first_format={first_format:?} attached_format={} cached_texture_format={cached_format:?} fresh_texture_format={fresh_format:?} RGB-mismatching-pixels={rgb_differing}",
|
||||
buffers[frame_index % buffer_count].format
|
||||
);
|
||||
}
|
||||
}
|
||||
if !switching_formats && let Some((rect, color)) = changed {
|
||||
let x = ((fixed_geometry.x + f64::from(rect.loc.x) + f64::from(rect.size.w) / 2.)
|
||||
* scale) as usize;
|
||||
let y = ((fixed_geometry.y + f64::from(rect.loc.y) + f64::from(rect.size.h) / 2.)
|
||||
* scale) as usize;
|
||||
let at = (y * physical.w as usize + x) * 4;
|
||||
let cpu_rgba = [(color >> 16) as u8, (color >> 8) as u8, color as u8, 255];
|
||||
assert_eq!(
|
||||
&actual[at..at + 4],
|
||||
&cpu_rgba,
|
||||
"partial upload missed CPU raster at frame={frame_index}"
|
||||
);
|
||||
}
|
||||
state.render.forget_context(&oracle_context);
|
||||
}
|
||||
assert_eq!(partial_frames, 23);
|
||||
if !switching_formats {
|
||||
assert!(total_changed_bytes < (WIDTH * HEIGHT * 4 * partial_frames as u32) as i32);
|
||||
}
|
||||
eprintln!(
|
||||
"stable scale={scale}, reused wl_buffers={buffer_count}: 24 age-3 frames, {partial_frames} partial commits, {total_changed_bytes} locally written SHM bytes, 24 fresh-context full-upload oracle frames"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
mismatching_frames, 0,
|
||||
"partial SHM frames differ from complete fresh uploads"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
//! Exercise the display scene's damage history, independently of capture repainting.
|
||||
use super::*;
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::element::{Element, RenderElement};
|
||||
use smithay::backend::renderer::{Color32F, Frame, Renderer, Texture};
|
||||
use smithay::utils::{Physical, Size};
|
||||
|
||||
fn commit_raster(
|
||||
state: &mut Ferese,
|
||||
events: &mut EventLoop<'static, Ferese>,
|
||||
window: &Window,
|
||||
wire: &mut UnixStream,
|
||||
object: &mut u32,
|
||||
size: (u32, u32),
|
||||
phase: u32,
|
||||
) {
|
||||
let client = window.toplevel().unwrap().wl_surface().client().unwrap();
|
||||
let shm = client
|
||||
.create_resource::<WlShm, smithay::wayland::GlobalData, Ferese>(
|
||||
&state.display_handle,
|
||||
1,
|
||||
smithay::wayland::GlobalData,
|
||||
)
|
||||
.unwrap();
|
||||
let compositor = client
|
||||
.create_resource::<WlCompositor, smithay::wayland::GlobalData, Ferese>(
|
||||
&state.display_handle,
|
||||
6,
|
||||
smithay::wayland::GlobalData,
|
||||
)
|
||||
.unwrap();
|
||||
let (width, height) = size;
|
||||
// A high-contrast grid catches stale/cropped pixels, unlike a solid window.
|
||||
let mut raster = Vec::with_capacity((width * height * 4) as usize);
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let color = if (x / 13 + y / 11 + phase) % 2 == 0 {
|
||||
0xff26b976_u32
|
||||
} else {
|
||||
0xffd44381_u32
|
||||
};
|
||||
raster.extend_from_slice(&color.to_ne_bytes());
|
||||
}
|
||||
}
|
||||
let mut file = tempfile::tempfile().unwrap();
|
||||
file.write_all(&raster).unwrap();
|
||||
request(
|
||||
wire,
|
||||
shm.id().protocol_id(),
|
||||
0,
|
||||
&[*object, width * height * 4],
|
||||
Some(file.as_raw_fd()),
|
||||
);
|
||||
request(
|
||||
wire,
|
||||
*object,
|
||||
0,
|
||||
&[*object + 1, 0, width, height, width * 4, 0],
|
||||
None,
|
||||
);
|
||||
request(wire, 3, 3, &[0, 0, width, height], None);
|
||||
request(wire, 2, 1, &[*object + 1, 0, 0], None);
|
||||
// Real opaque desktop clients declare an opaque region. Without it the
|
||||
// damage tracker cannot exercise the rounded element's occlusion path.
|
||||
request(wire, compositor.id().protocol_id(), 1, &[*object + 2], None);
|
||||
request(wire, *object + 2, 1, &[0, 0, width, height], None);
|
||||
request(wire, 2, 4, &[*object + 2], None);
|
||||
request(wire, *object + 2, 0, &[], None);
|
||||
request(wire, 2, 2, &[0, 0, width, height], None);
|
||||
request(wire, 2, 6, &[], None);
|
||||
*object += 3;
|
||||
dispatch(events, state);
|
||||
}
|
||||
|
||||
fn read_pixels(renderer: &mut GlesRenderer, texture: &mut GlesTexture) -> Vec<u8> {
|
||||
let size = texture.size();
|
||||
let target = renderer.bind(texture).unwrap();
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888)
|
||||
.unwrap();
|
||||
renderer.map_texture(&mapping).unwrap().to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires a private Wayland socket and an EGL rendering device"]
|
||||
fn floating_maximize_handoff_matches_full_repaint_at_buffer_age_three() {
|
||||
const TEST: &str = "state::capture_privacy::tests::resize_scene_gpu_tests::floating_maximize_handoff_matches_full_repaint_at_buffer_age_three";
|
||||
if !crate::startup_tests::private_runtime(TEST) {
|
||||
return;
|
||||
}
|
||||
let software = std::env::var_os("FERESE_TEST_EGL_SOFTWARE").is_some();
|
||||
let device = EGLDevice::enumerate()
|
||||
.unwrap()
|
||||
.find(|device| device.is_software() == software)
|
||||
.expect("requested EGL rendering device");
|
||||
eprintln!(
|
||||
"resize scene EGL software={software}, render node={:?}",
|
||||
device.render_device_path()
|
||||
);
|
||||
// SAFETY: This test owns the enumerated device and the fresh EGL context.
|
||||
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||
// SAFETY: GL work remains on this renderer's owning test thread.
|
||||
let mut renderer = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
|
||||
|
||||
for scale in [1.0, 1.5] {
|
||||
let mut events = EventLoop::try_new().unwrap();
|
||||
let config = crate::config::Config::parse_source(
|
||||
"animations { enabled #false; }\nlayout { inner-gap 4; outer-gap 4; }\nwindow-rule transient=#false floating=#true\n",
|
||||
)
|
||||
.unwrap()
|
||||
.runtime_config()
|
||||
.unwrap();
|
||||
let mut state = Ferese::new(&mut events, Display::new().unwrap(), config).unwrap();
|
||||
state.animation_test_time = Some(Duration::ZERO);
|
||||
state.focus_effect.active_opacity = 1.0;
|
||||
state.focus_effect.inactive_opacity = 1.0;
|
||||
state.focus_effect.inactive_dim = 0.0;
|
||||
state.theme_settings.window_radius = 12.0;
|
||||
let output = Output::new(
|
||||
"resize-age-three".into(),
|
||||
PhysicalProperties {
|
||||
serial_number: String::new(),
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: "test".into(),
|
||||
model: "test".into(),
|
||||
},
|
||||
);
|
||||
let physical: Size<i32, Physical> = ((320.0 * scale) as i32, (240.0 * scale) as i32).into();
|
||||
output.change_current_state(
|
||||
Some(Mode {
|
||||
size: physical,
|
||||
refresh: 60_000,
|
||||
}),
|
||||
Some(Transform::Normal),
|
||||
Some(Scale::Fractional(scale)),
|
||||
None,
|
||||
);
|
||||
state.space.map_output(&output, (0, 0));
|
||||
state.register_output(&output, "resize-age-three".into());
|
||||
let (window, mut wire) = window(&mut state, &mut events, 0xff26b976);
|
||||
let id = state.windows.ids()[&window];
|
||||
state.focused_window = Some(id);
|
||||
state.set_floating_window_geometry(&window, (106, 92).into(), (128, 80).into());
|
||||
state.display_handle.flush_clients().unwrap();
|
||||
ack_configure(&mut wire, 3);
|
||||
let mut object = 7;
|
||||
commit_raster(
|
||||
&mut state,
|
||||
&mut events,
|
||||
&window,
|
||||
&mut wire,
|
||||
&mut object,
|
||||
(128, 80),
|
||||
0,
|
||||
);
|
||||
state.windows.update(id, |record| record.opening = None);
|
||||
state.animations_enabled = true;
|
||||
|
||||
let mut tracker = OutputDamageTracker::from_output(&output);
|
||||
let mut targets: Vec<GlesTexture> = (0..3)
|
||||
.map(|_| {
|
||||
renderer
|
||||
.create_buffer(Fourcc::Abgr8888, (physical.w, physical.h).into())
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
let mut reference: GlesTexture = renderer
|
||||
.create_buffer(Fourcc::Abgr8888, (physical.w, physical.h).into())
|
||||
.unwrap();
|
||||
let mut frame_index = 0;
|
||||
let mut now = Duration::ZERO;
|
||||
let mut snapshot_frames = 0;
|
||||
let mut fill_frames = 0;
|
||||
let mut moving_frames = 0;
|
||||
let mut repaint_frames = 0;
|
||||
let mut native_draw_frames = 0;
|
||||
let mut opaque_frames = 0;
|
||||
|
||||
// Delayed commits exercise both old-frame cropping on shrink and neutral
|
||||
// fill on grow. Repeating in both directions also reuses every target.
|
||||
for (cycle, delay) in [3, 1, 5, 0].into_iter().enumerate() {
|
||||
state.set_window_maximized(id, cycle % 2 == 0);
|
||||
state.display_handle.flush_clients().unwrap();
|
||||
let (_, configured) = ack_configure_with_size(&mut wire, 3, Some(4));
|
||||
let requested = configured.expect("maximize/restore configure");
|
||||
for tick in 0..28 {
|
||||
if tick == delay {
|
||||
// The offscreen fixture has no DirectBackend. Invoke the
|
||||
// same capture-before-release operation used by that backend.
|
||||
let snapshot = crate::render::capture_resize_snapshot(
|
||||
&mut renderer,
|
||||
&window,
|
||||
window.geometry(),
|
||||
scale,
|
||||
32 * 1024 * 1024,
|
||||
)
|
||||
.unwrap()
|
||||
.expect("old client pixels");
|
||||
state.render.set_snapshot(id, snapshot);
|
||||
commit_raster(
|
||||
&mut state,
|
||||
&mut events,
|
||||
&window,
|
||||
&mut wire,
|
||||
&mut object,
|
||||
requested,
|
||||
cycle as u32 + 1,
|
||||
);
|
||||
repaint_frames += 1;
|
||||
} else if tick > delay && tick % 2 == 0 {
|
||||
// IDE repainting alternates with unchanged client buffers,
|
||||
// so both client damage and compositor-only motion matter.
|
||||
commit_raster(
|
||||
&mut state,
|
||||
&mut events,
|
||||
&window,
|
||||
&mut wire,
|
||||
&mut object,
|
||||
requested,
|
||||
cycle as u32 + tick as u32,
|
||||
);
|
||||
repaint_frames += 1;
|
||||
}
|
||||
now += Duration::from_millis(16);
|
||||
state.advance_animations_at(Duration::from_millis(16), now);
|
||||
let scene = state.sample_frame(&output, Duration::from_millis(5));
|
||||
let sample = &scene.windows[&id];
|
||||
assert_eq!(sample.presentation.alpha(), 1.0, "ordinary opaque window");
|
||||
if sample.geometry.visual.current != sample.geometry.visual.target {
|
||||
moving_frames += 1;
|
||||
}
|
||||
snapshot_frames += usize::from(state.render.snapshot(&id).is_some());
|
||||
let source = window.geometry().size;
|
||||
let visual = sample.presentation.bounds.current;
|
||||
fill_frames += usize::from(
|
||||
f64::from(source.w) < visual.width || f64::from(source.h) < visual.height,
|
||||
);
|
||||
let elements = crate::render::sampled_output_elements(
|
||||
&mut state,
|
||||
&mut renderer,
|
||||
&output,
|
||||
false,
|
||||
&scene,
|
||||
);
|
||||
native_draw_frames +=
|
||||
usize::from(state.render.snapshot(&id).is_some_and(|snapshot| {
|
||||
elements.iter().any(|element| element.id() == &snapshot.id)
|
||||
}));
|
||||
opaque_frames += usize::from(
|
||||
elements
|
||||
.iter()
|
||||
.any(|element| !element.opaque_regions(scale.into()).is_empty()),
|
||||
);
|
||||
assert!(
|
||||
elements
|
||||
.iter()
|
||||
.all(|element| !element.is_framebuffer_effect()),
|
||||
"this regression must cover opaque resize without backdrop blur"
|
||||
);
|
||||
let slot = frame_index % 3;
|
||||
{
|
||||
let mut target = renderer.bind(&mut targets[slot]).unwrap();
|
||||
tracker
|
||||
.render_output(
|
||||
&mut renderer,
|
||||
&mut target,
|
||||
if frame_index < 3 { 0 } else { 3 },
|
||||
&elements,
|
||||
Color32F::new(0.035, 0.04, 0.055, 1.0),
|
||||
)
|
||||
.unwrap()
|
||||
.sync
|
||||
.wait()
|
||||
.unwrap();
|
||||
}
|
||||
// Independent oracle: no damage history, no opaque-region
|
||||
// culling, and every layer painted back-to-front into a clean FBO.
|
||||
{
|
||||
let mut target = renderer.bind(&mut reference).unwrap();
|
||||
let mut frame = renderer
|
||||
.render(&mut target, physical, Transform::Normal)
|
||||
.unwrap();
|
||||
frame
|
||||
.clear(
|
||||
Color32F::new(0.035, 0.04, 0.055, 1.0),
|
||||
&[Rectangle::from_size(physical)],
|
||||
)
|
||||
.unwrap();
|
||||
for element in elements.iter().rev() {
|
||||
let rect = element.geometry(scale.into());
|
||||
element
|
||||
.draw(
|
||||
&mut frame,
|
||||
element.src(),
|
||||
rect,
|
||||
&[Rectangle::from_size(rect.size)],
|
||||
&[],
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
frame.finish().unwrap().wait().unwrap();
|
||||
}
|
||||
let actual = read_pixels(&mut renderer, &mut targets[slot]);
|
||||
let expected = read_pixels(&mut renderer, &mut reference);
|
||||
let differing = actual
|
||||
.iter()
|
||||
.zip(&expected)
|
||||
.filter(|(a, b)| a.abs_diff(**b) > 1)
|
||||
.count();
|
||||
assert_eq!(
|
||||
differing, 0,
|
||||
"scale={scale} cycle={cycle} tick={tick} frame={frame_index} source={source:?} visual={visual:?}"
|
||||
);
|
||||
frame_index += 1;
|
||||
}
|
||||
}
|
||||
assert!(snapshot_frames > 20, "resize handoff was not exercised");
|
||||
assert!(
|
||||
fill_frames > 0,
|
||||
"growing native content never needed resize fill"
|
||||
);
|
||||
eprintln!(
|
||||
"opaque resize scale={scale}: {frame_index} age-3 frames, {snapshot_frames} snapshot frames, {native_draw_frames} snapshot draw frames, {fill_frames} fill frames, {moving_frames} moving frames, {repaint_frames} client repaint frames, {opaque_frames} opaque frames"
|
||||
);
|
||||
assert!(moving_frames > 20, "maximize/restore animation did not run");
|
||||
assert!(
|
||||
repaint_frames > 40,
|
||||
"client repainting did not overlap motion"
|
||||
);
|
||||
assert!(
|
||||
native_draw_frames > 20,
|
||||
"retained snapshots were not in the display scene"
|
||||
);
|
||||
assert!(
|
||||
opaque_frames > 20,
|
||||
"opaque-region culling was not exercised"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -271,6 +271,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
|
||||
impl Ferese {
|
||||
pub(crate) fn release_nested_keyboard(&mut self) {
|
||||
self.super_tap.reset();
|
||||
// Host shortcuts can consume releases while this window is unfocused.
|
||||
// Release only the physical/nested source, including modifiers, without
|
||||
// executing compositor shortcuts. Auxiliary keyboards retain ownership.
|
||||
|
||||
@@ -1778,7 +1778,7 @@ mod manager_loop {
|
||||
state
|
||||
.reload_config_source(
|
||||
"xwayland { enabled #true; startup \"on-demand\"; path \"".to_owned()
|
||||
+ &wrapper(&directory, &[]).display().to_string()
|
||||
+ wrapper(&directory, &[]).display().to_string().as_str()
|
||||
+ "\"; }",
|
||||
)
|
||||
.expect("the reload must be accepted");
|
||||
|
||||
@@ -103,6 +103,12 @@ fn grid_with_density<'a, M: Clone + 'a>(
|
||||
let month = first.month();
|
||||
let primary = palette.text.scale_alpha(opacity);
|
||||
let muted = palette.muted.scale_alpha(opacity);
|
||||
let holiday_color = if palette.appearance == ferese_config::theme::Appearance::Light {
|
||||
cosmic::iced::Color::from_rgb8(184, 38, 49)
|
||||
} else {
|
||||
cosmic::iced::Color::from_rgb8(244, 110, 120)
|
||||
}
|
||||
.scale_alpha(opacity);
|
||||
let [previous, today_button, next] = navigation;
|
||||
let header = row![
|
||||
crate::text(
|
||||
@@ -133,12 +139,19 @@ fn grid_with_density<'a, M: Clone + 'a>(
|
||||
} else {
|
||||
Length::Shrink
|
||||
});
|
||||
for label in weekday_labels(ferese_config::locale::resolved()) {
|
||||
for (weekday, label) in weekday_labels(ferese_config::locale::resolved())
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
weekdays = weekdays.push(
|
||||
container(
|
||||
crate::text(label, font)
|
||||
.size(if compact { 9 } else { 11 })
|
||||
.class(theme::Text::Color(muted)),
|
||||
.class(theme::Text::Color(if matches!(weekday, 0 | 6) {
|
||||
holiday_color
|
||||
} else {
|
||||
muted
|
||||
})),
|
||||
)
|
||||
.width(cell_width)
|
||||
.center_x(cell_width),
|
||||
@@ -161,14 +174,9 @@ fn grid_with_density<'a, M: Clone + 'a>(
|
||||
let date = date.filter(|_| visible);
|
||||
let is_today = date == Some(today);
|
||||
let holiday = date.and_then(|date| settings.holiday_name(date));
|
||||
let holiday_color = if palette.appearance == ferese_config::theme::Appearance::Light {
|
||||
cosmic::iced::Color::from_rgb8(184, 38, 49)
|
||||
} else {
|
||||
cosmic::iced::Color::from_rgb8(244, 110, 120)
|
||||
}
|
||||
.scale_alpha(opacity * if in_month { 1. } else { 0.65 });
|
||||
let foreground = if holiday.is_some() {
|
||||
holiday_color
|
||||
let emphasized = date.is_some() && (holiday.is_some() || matches!(weekday, 0 | 6));
|
||||
let foreground = if emphasized {
|
||||
holiday_color.scale_alpha(if in_month { 1. } else { 0.65 })
|
||||
} else if is_today || in_month {
|
||||
primary
|
||||
} else {
|
||||
@@ -176,7 +184,7 @@ fn grid_with_density<'a, M: Clone + 'a>(
|
||||
};
|
||||
let label = date.map_or(String::new(), |date| date.day().to_string());
|
||||
let font = cosmic::iced::Font {
|
||||
weight: if holiday.is_some() {
|
||||
weight: if emphasized {
|
||||
cosmic::iced::font::Weight::Bold
|
||||
} else {
|
||||
cosmic::iced::font::Weight::Normal
|
||||
|
||||
@@ -18,6 +18,25 @@ Keys use XKB keysyms (`Enter`, letters, `[`/`]`) or physical XKB names (`AD06`).
|
||||
A binding replaces the same normalized chord/match mode. Duplicate bindings are
|
||||
invalid. A disabled binding must omit `action` and `argument`.
|
||||
|
||||
A modifier-only `Super` binding is optional:
|
||||
|
||||
```kdl
|
||||
binding "Super" action="toggle-launcher"
|
||||
```
|
||||
|
||||
Either Super key triggers this action when released after an isolated press.
|
||||
The original press and release still reach the focused application. Other keys,
|
||||
both Super keys together, pointer movement, clicks, scrolling, gestures and touch
|
||||
cancel the tap; a mouse button already held also prevents it. Existing Super
|
||||
chords, including Super+Space, keep their actions. Key repeat does not retrigger
|
||||
the tap. Locking, shortcut inhibition, client keyboard grabs, focus changes and
|
||||
device/session teardown cancel pending taps. Native input-method grabs remain
|
||||
compatible. `Logo` and `Mod4` name the same tap, so they cannot be configured as
|
||||
separate bindings. Physical matching is not supported for this trigger.
|
||||
|
||||
Use `binding "Super" disabled=#true` to remove a configured tap. Without a tap
|
||||
binding, Super keeps its existing modifier behavior.
|
||||
|
||||
| Actions | Argument |
|
||||
| --- | --- |
|
||||
| `spawn` | Name in `commands` |
|
||||
|
||||
@@ -37,6 +37,25 @@ librime 仍是 C++ 引擎,通过一个由安装头文件检查 ABI 的小型 C
|
||||
|
||||
## 构建与启动
|
||||
|
||||
### 本机日常会话安装
|
||||
|
||||
安装清单现包含 `ferese-rime`,Arch 包声明 `librime` 和 `rime-luna-pinyin`
|
||||
依赖。公共方案与词库仍由系统包维护于 `/usr/share/rime-data`,不复制个人词库。
|
||||
直接 DRM 会话默认启用原生输入法,并仅在 Ferese 会话内覆盖继承的 IBus Qt 环境。
|
||||
GTK 不强制指定输入模块,由实际的原生 Wayland/X11 后端自行选择;
|
||||
会话激活环境清空旧的 `GTK_IM_MODULE`,避免 XWayland 应用加载 GTK3 Wayland 模块时崩溃。
|
||||
这些变量也导入该会话的 D-Bus/systemd 激活环境。嵌套预览保留原有的显式输入法启动方式。
|
||||
|
||||
会话在 `$XDG_DATA_HOME/ferese/rime`(默认 `~/.local/share/ferese/rime`)建立独立用户目录,
|
||||
仅在 `default.custom.yaml` 不存在时安装默认简体拼音和每页九个候选的配置。
|
||||
不读取、迁移或覆盖 IBus/Fcitx 的个人词库,也保留该目录内已有的用户定制。
|
||||
Rime 由会话固定版本的 systemd 用户临时服务运行,异常后重启并启用十秒输入循环 watchdog;
|
||||
服务名称由合成器控制套接字稳定生成,Shell 硬崩溃恢复时仅替换该会话的旧输入法实例。
|
||||
会话结束时停止,输入法失败保留桌面。可用 `journalctl --user -u 'ferese-rime-session@*'`
|
||||
查看日志。`FERESE_ENABLE_INPUT_METHOD=0` 可在登录前显式停用此集成。
|
||||
|
||||
以下预览与硬件验证记录早于此安装集成,不能代替新安装会话的验收。
|
||||
|
||||
需要仓库的构建依赖,以及 `librime`、`xkbcommon` 和 Wayland 开发文件。
|
||||
验证环境使用系统 librime 1.17.0、GTK4 4.24.1、RTX 4090、NVIDIA 615.78.08。
|
||||
Rust 工具链由仓库固定为 1.95.0。
|
||||
|
||||
+10
-1
@@ -39,7 +39,8 @@ sudo pacman -S --needed python base-devel git curl cmake pkgconf wayland libxkbc
|
||||
libinput systemd seatd mesa libdrm fontconfig freetype2 expat dbus \
|
||||
desktop-file-utils foot pam clang pipewire libpipewire wireplumber polkit \
|
||||
xdg-desktop-portal xdg-desktop-portal-gtk \
|
||||
gstreamer gst-plugins-base gst-plugins-good gst-plugin-pipewire pipewire-pulse slurp
|
||||
gstreamer gst-plugins-base gst-plugins-good gst-plugin-pipewire pipewire-pulse slurp \
|
||||
librime rime-luna-pinyin
|
||||
```
|
||||
|
||||
### Debian and Ubuntu
|
||||
@@ -61,6 +62,14 @@ other distributions; check the [Smithay
|
||||
dependencies](https://github.com/Smithay/smithay#system-dependencies) for equivalent
|
||||
libraries.
|
||||
|
||||
This local fork also builds the native `ferese-rime` service. Outside Arch Linux,
|
||||
install librime and its development headers plus the Rime Prelude, Luna Pinyin
|
||||
and Stroke shared schemas under `/usr/share/rime-data` before building and logging
|
||||
in. The Fedora and Debian/Ubuntu commands above describe the upstream desktop
|
||||
dependencies and do not yet include these fork-specific Rime packages. Ferese
|
||||
keeps its user dictionary separate from IBus and Fcitx; see the
|
||||
[native Rime notes](development/rime-poc.md#本机日常会话安装).
|
||||
|
||||
### X11 support (optional)
|
||||
|
||||
To run X11-only applications, Ferese launches
|
||||
|
||||
@@ -24,6 +24,13 @@ current workspace. Power off, restart and logout also require **Alt + Enter**.
|
||||
|
||||
Run `feresectl launcher toggle` to open or close it from a script.
|
||||
|
||||
To also toggle the launcher by pressing and releasing Super alone, add
|
||||
`binding "Super" action="toggle-launcher"` to your configuration. The action runs
|
||||
on release; using another key, the pointer or a held mouse button cancels it, so
|
||||
Super+Space and Super+mouse combinations continue to work. See
|
||||
[modifier-only bindings](configuration/bindings.md) for input ownership and
|
||||
cancellation behavior.
|
||||
|
||||
## Move and resize with the mouse
|
||||
|
||||
| Action | Result |
|
||||
|
||||
@@ -7,7 +7,8 @@ url='https://github.com/ferese-wm/ferese'
|
||||
license=('MIT' 'Apache-2.0' 'BSD-2-Clause' 'BSD-3-Clause' 'BSL-1.0' 'CC0-1.0' 'CDLA-Permissive-2.0' 'ISC' 'MPL-2.0' 'Unicode-3.0' 'Unlicense' 'Zlib' 'OFL-1.1' 'Apache-2.0 WITH LLVM-exception')
|
||||
depends=('wayland' 'libxkbcommon' 'libinput' 'systemd-libs' 'seatd' 'mesa' 'libdrm'
|
||||
'fontconfig' 'freetype2' 'expat' 'dbus' 'python' 'bash' 'util-linux' 'systemd' 'pam' 'polkit' 'foot' 'pipewire'
|
||||
'wireplumber' 'libpipewire' 'gstreamer' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk' 'glib2')
|
||||
'wireplumber' 'libpipewire' 'gstreamer' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk' 'glib2'
|
||||
'librime' 'rime-luna-pinyin')
|
||||
makedepends=('cargo' 'cmake' 'clang' 'pkgconf' 'python' 'desktop-file-utils')
|
||||
checkdepends=('dbus' 'vulkan-swrast' 'xorg-xauth')
|
||||
optdepends=('brightnessctl: brightness controls' 'slurp: screenshot and recording region selection'
|
||||
|
||||
@@ -7,7 +7,8 @@ url='https://github.com/ferese-wm/ferese'
|
||||
license=('MIT' 'Apache-2.0' 'BSD-2-Clause' 'BSD-3-Clause' 'BSL-1.0' 'CC0-1.0' 'CDLA-Permissive-2.0' 'ISC' 'MPL-2.0' 'Unicode-3.0' 'Unlicense' 'Zlib' 'OFL-1.1' 'Apache-2.0 WITH LLVM-exception')
|
||||
depends=('wayland' 'libxkbcommon' 'libinput' 'systemd-libs' 'seatd' 'mesa' 'libdrm'
|
||||
'fontconfig' 'freetype2' 'expat' 'dbus' 'python' 'bash' 'util-linux' 'systemd' 'pam' 'polkit' 'foot' 'pipewire'
|
||||
'wireplumber' 'libpipewire' 'gstreamer' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk' 'glib2')
|
||||
'wireplumber' 'libpipewire' 'gstreamer' 'xdg-desktop-portal' 'xdg-desktop-portal-gtk' 'glib2'
|
||||
'librime' 'rime-luna-pinyin')
|
||||
makedepends=('cargo' 'cmake' 'clang' 'pkgconf' 'python' 'git' 'desktop-file-utils')
|
||||
checkdepends=('dbus' 'vulkan-swrast' 'xorg-xauth')
|
||||
optdepends=('brightnessctl: brightness controls' 'slurp: screenshot and recording region selection'
|
||||
|
||||
@@ -33,6 +33,18 @@ unset WAYLAND_SOCKET
|
||||
if [[ $backend == drm ]]; then
|
||||
unset DISPLAY WAYLAND_DISPLAY
|
||||
export FERESE_SESSION_IMPORT_ENV=1
|
||||
# This fork owns its native input-method service. Keep GNOME's IBus settings
|
||||
# outside this session, including apps launched later through activation.
|
||||
export FERESE_ENABLE_INPUT_METHOD=${FERESE_ENABLE_INPUT_METHOD:-1}
|
||||
if [[ $FERESE_ENABLE_INPUT_METHOD == 1 ]]; then
|
||||
# GTK selects its IM module from the actual GDK backend. Forcing its
|
||||
# Wayland module crashes GTK3 consumers that initialize an X11 display.
|
||||
unset GTK_IM_MODULE
|
||||
export QT_IM_MODULE=wayland QT_IM_MODULES=wayland
|
||||
export IBUS_ADDRESS=
|
||||
unset SDL_IM_MODULE
|
||||
export FERESE_RIME_CONTROL=${FERESE_RIME_CONTROL:-$XDG_RUNTIME_DIR/ferese/rime-control.sock}
|
||||
fi
|
||||
else
|
||||
export FERESE_SESSION_IMPORT_ENV=0
|
||||
fi
|
||||
|
||||
@@ -4,6 +4,7 @@ session_target_started=0
|
||||
shell_pid=
|
||||
agent_pid=
|
||||
autostart_pid=
|
||||
rime_unit=
|
||||
manage_session=0
|
||||
if [[ ${FERESE_SESSION_MODE:-desktop} == desktop && ${FERESE_SESSION_IMPORT_ENV:-0} == 1 ]]; then
|
||||
manage_session=1
|
||||
@@ -19,6 +20,9 @@ cleanup() {
|
||||
kill "$agent_pid" 2>/dev/null || true
|
||||
wait "$agent_pid" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -n $rime_unit ]]; then
|
||||
timeout --kill-after=1s 6s systemctl --user stop "$rime_unit" || true
|
||||
fi
|
||||
if ((session_target_started)); then
|
||||
systemctl --user stop ferese-session.target || true
|
||||
fi
|
||||
@@ -43,6 +47,10 @@ if ((manage_session)); then
|
||||
XDG_SESSION_DESKTOP
|
||||
XDG_SESSION_TYPE
|
||||
)
|
||||
if [[ ${FERESE_ENABLE_INPUT_METHOD:-0} == 1 ]]; then
|
||||
activation_vars+=("GTK_IM_MODULE=" QT_IM_MODULE QT_IM_MODULES IBUS_ADDRESS FERESE_RIME_CONTROL
|
||||
"SDL_IM_MODULE=" "XMODIFIERS=${XMODIFIERS:-}")
|
||||
fi
|
||||
if command -v dbus-update-activation-environment >/dev/null; then
|
||||
if ! env WAYLAND_DISPLAY="$public_display" \
|
||||
dbus-update-activation-environment --systemd "${activation_vars[@]}"; then
|
||||
@@ -70,6 +78,59 @@ if ((manage_session)); then
|
||||
fi
|
||||
fi
|
||||
release_dir=$(dirname -- "$(readlink -f -- "$0")")
|
||||
# Start the native input method on the public socket, without shell privileges.
|
||||
# Pin the executable to this release and let systemd recover both crashes and a
|
||||
# stalled input loop. A failed IME must leave the compositor and shell usable.
|
||||
start_rime() {
|
||||
local user_data=${FERESE_RIME_USER_DATA:-${XDG_DATA_HOME:-$HOME/.local/share}/ferese/rime}
|
||||
local shared_data=${FERESE_RIME_SHARED_DATA:-/usr/share/rime-data}
|
||||
local identity=${FERESE_SOCKET:-${XDG_RUNTIME_DIR:-}/$public_display}
|
||||
local name variable load_state deadline
|
||||
local -a environment=()
|
||||
name=$(systemd-escape [email protected] "$identity") || return 1
|
||||
[[ -x $release_dir/ferese-rime ]] || return 1
|
||||
[[ -r $shared_data/luna_pinyin_simp.schema.yaml ]] || {
|
||||
echo 'Ferese: install the Rime Luna Pinyin schemas before starting native input.' >&2
|
||||
return 1
|
||||
}
|
||||
mkdir -p -- "$user_data" || return 1
|
||||
# noclobber preserves existing customization, including a concurrent setup.
|
||||
if [[ ! -e $user_data/default.custom.yaml && ! -L $user_data/default.custom.yaml ]]; then
|
||||
(set -o noclobber; cat -- "$release_dir/rime/default.custom.yaml" > "$user_data/default.custom.yaml") || return 1
|
||||
fi
|
||||
# SIGKILL cannot run the previous helper's trap. The compositor control socket
|
||||
# survives helper recovery, so replace only its old IME instance. Bound all
|
||||
# manager calls and wait for the transient unit to be collected before reuse.
|
||||
load_state=$(timeout --kill-after=1s 2s systemctl --user show "$name" --property=LoadState --value) || return 1
|
||||
if [[ $load_state != not-found ]]; then
|
||||
timeout --kill-after=1s 6s systemctl --user stop "$name" || return 1
|
||||
deadline=$((SECONDS + 3))
|
||||
while [[ $load_state != not-found ]]; do
|
||||
((SECONDS < deadline)) || return 1
|
||||
load_state=$(timeout --kill-after=1s 1s systemctl --user show "$name" --property=LoadState --value) || return 1
|
||||
if [[ $load_state != not-found ]]; then sleep .05; fi
|
||||
done
|
||||
fi
|
||||
for variable in HOME PATH LANG XDG_RUNTIME_DIR XDG_CONFIG_HOME XDG_CACHE_HOME XDG_DATA_HOME XDG_STATE_HOME \
|
||||
DBUS_SESSION_BUS_ADDRESS XDG_CURRENT_DESKTOP XDG_SESSION_DESKTOP XDG_SESSION_TYPE \
|
||||
FERESE_SOCKET FERESE_RIME_CONTROL; do
|
||||
if [[ -v $variable ]]; then environment+=("--setenv=$variable=${!variable}"); fi
|
||||
done
|
||||
if timeout --kill-after=1s 6s systemd-run --user --collect --quiet --no-block --unit="$name" --service-type=notify \
|
||||
--property=Restart=on-failure --property=RestartSec=1 \
|
||||
--property=StartLimitIntervalSec=60 --property=StartLimitBurst=5 \
|
||||
--property=WatchdogSec=10 --property=WatchdogSignal=SIGKILL \
|
||||
--property=TimeoutStartSec=45 --property=TimeoutStopSec=3 \
|
||||
--property=BindsTo=ferese-session.target --property=PartOf=ferese-session.target \
|
||||
"${environment[@]}" \
|
||||
/usr/bin/env -u WAYLAND_SOCKET -u FERESE_SHELL_CONTROL_SOCKET -u FERESE_PRIVATE_X11_COOKIE \
|
||||
WAYLAND_DISPLAY="$public_display" "$release_dir/ferese-rime" \
|
||||
--user-data "$user_data" --shared-data "$shared_data"; then
|
||||
rime_unit=$name
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
# Agent failures must not take down a healthy shell. The compositor supervises
|
||||
# this helper (and creates fresh private connections) when the shell itself exits.
|
||||
supervise_agent() {
|
||||
@@ -96,6 +157,11 @@ supervise_agent &
|
||||
agent_pid=$!
|
||||
"$release_dir/ferese-shell" &
|
||||
shell_pid=$!
|
||||
if ((manage_session && session_target_started)) && [[ ${FERESE_ENABLE_INPUT_METHOD:-0} == 1 ]]; then
|
||||
if ! start_rime; then
|
||||
echo 'Ferese: native Rime could not start; the desktop remains available.' >&2
|
||||
fi
|
||||
fi
|
||||
if ((manage_session)) && [[ ${FERESE_CLIENT_RESTART:-0} != 1 && -x $release_dir/feresectl ]]; then
|
||||
env WAYLAND_DISPLAY="${FERESE_PUBLIC_WAYLAND_DISPLAY:-${WAYLAND_DISPLAY:-}}" \
|
||||
"$release_dir/feresectl" autostart &
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# First-run Ferese defaults; existing user customization is never overwritten.
|
||||
patch:
|
||||
schema_list:
|
||||
- schema: luna_pinyin_simp
|
||||
- schema: luna_pinyin
|
||||
menu/page_size: 9
|
||||
@@ -169,11 +169,11 @@ def validate_bundle(bundle, *, installed=False):
|
||||
actual = {str(path.relative_to(bundle)) for path in bundle.rglob('*') if path.is_file()}
|
||||
required = expected.keys()
|
||||
if installed:
|
||||
# Installed releases may predate the appearance wallpapers and license.
|
||||
# Installed releases may predate the appearance assets or native Rime.
|
||||
# New bundles must include them; upgrades and rollback verify the older
|
||||
# release's recorded files without requiring these later additions.
|
||||
required = required - {'wallpapers/ferese-wallpaper-dark.png', 'wallpapers/ferese-wallpaper-light.png',
|
||||
'licenses/Ferese-LICENSE.txt'}
|
||||
'licenses/Ferese-LICENSE.txt', 'ferese-rime', 'rime/default.custom.yaml'}
|
||||
required = {target for target in required if not target.startswith('activities/')}
|
||||
|
||||
if not required <= seen or actual != seen | {'manifest.json'}:
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"feresectl",
|
||||
"ferese-lock",
|
||||
"ferese-polkit-agent",
|
||||
"ferese-rime",
|
||||
"xdg-desktop-portal-ferese"
|
||||
],
|
||||
"files": [
|
||||
@@ -50,6 +51,17 @@
|
||||
"mode": "0755",
|
||||
"link": true
|
||||
},
|
||||
{
|
||||
"source": "target/release/ferese-rime",
|
||||
"target": "ferese-rime",
|
||||
"mode": "0755",
|
||||
"link": true
|
||||
},
|
||||
{
|
||||
"source": "packaging/rime/default.custom.yaml",
|
||||
"target": "rime/default.custom.yaml",
|
||||
"mode": "0644"
|
||||
},
|
||||
{
|
||||
"source": "target/release/xdg-desktop-portal-ferese",
|
||||
"target": "xdg-desktop-portal-ferese",
|
||||
|
||||
+17
-5
@@ -35,16 +35,24 @@ def check_tag(tag, release_version):
|
||||
def licenses(destination, metadata):
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
report = []
|
||||
workspace_members = set(metadata.get('workspace_members', []))
|
||||
workspace_root = Path(metadata.get('workspace_root', ROOT))
|
||||
for package in sorted(metadata['packages'], key=lambda p: (p['name'], p['version'])):
|
||||
if package['source'] is None:
|
||||
if package.get('id') in workspace_members:
|
||||
continue
|
||||
|
||||
directory = Path(package['manifest_path']).parent
|
||||
source_hash = hashlib.sha256(package['source'].encode()).hexdigest()[:8]
|
||||
source = package['source']
|
||||
if source is None:
|
||||
# A vendored/path dependency is third-party code even though Cargo
|
||||
# gives it no registry/Git source. Keep notices reproducible across
|
||||
# checkout locations without exporting absolute manifest paths.
|
||||
source = 'path+' + Path(os.path.relpath(directory, workspace_root)).as_posix()
|
||||
source_hash = hashlib.sha256(source.encode()).hexdigest()[:8]
|
||||
name = f"{package['name']}-{package['version']}-{source_hash}"
|
||||
files = set()
|
||||
search = [directory]
|
||||
if package['source'].startswith('git+'):
|
||||
if source.startswith('git+'):
|
||||
for ancestor in directory.parents:
|
||||
search.append(ancestor)
|
||||
if (ancestor / '.git').exists():
|
||||
@@ -66,7 +74,7 @@ def licenses(destination, metadata):
|
||||
shutil.copyfile(path, target)
|
||||
|
||||
report.append({'name': package['name'], 'version': package['version'],
|
||||
'source': package['source'], 'license': package['license'],
|
||||
'source': source, 'license': package['license'],
|
||||
'notice_directory': name})
|
||||
|
||||
(destination / 'dependencies.json').write_text(json.dumps(report, indent=2) + '\n')
|
||||
@@ -113,7 +121,11 @@ def stage(destdir, binary_dir, pam):
|
||||
|
||||
planned.extend([(ROOT / f'packaging/pam.d/ferese-lock.{pam}', 'etc/pam.d/ferese-lock', 0o644),
|
||||
(ROOT / 'LICENSE', 'usr/share/licenses/ferese/LICENSE', 0o644),
|
||||
(ROOT / 'README.md', 'usr/share/doc/ferese/README.md', 0o644)])
|
||||
(ROOT / 'README.md', 'usr/share/doc/ferese/README.md', 0o644),
|
||||
(ROOT / 'vendor/smithay/UPSTREAM.md',
|
||||
'usr/share/doc/ferese/vendor/smithay/UPSTREAM.md', 0o644),
|
||||
(ROOT / 'vendor/smithay/patches/shm-texture-cache-format.patch',
|
||||
'usr/share/doc/ferese/vendor/smithay/shm-texture-cache-format.patch', 0o644)])
|
||||
# Validate the complete inventory before writing any part of the package.
|
||||
for source, _, _ in planned:
|
||||
if not source.is_file():
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Real shell + native Rime test on a private Xvfb/nested Wayland/bus.
|
||||
No host GUI input or application profile is connected to this fixture.
|
||||
|
||||
FERESE_TEST_LAUNCHER_IME=1 python3 scripts/tests/test_launcher_rime_isolated.py
|
||||
Build the release compositor/shell/Rime/feresectl and debug ferese-rime-probe first.
|
||||
Synthetic screenshots and protocol traces stay under .artifacts/.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import hashlib,json,os,re,secrets,signal,shutil,subprocess,tempfile,time,unittest
|
||||
|
||||
REPO=Path(__file__).resolve().parents[2]
|
||||
BASE=REPO/'.artifacts/launcher-rime-isolated'
|
||||
COMPOSITOR=REPO/os.environ.get('FERESE_TEST_BINARY','target/release/ferese')
|
||||
PROBE=REPO/os.environ.get('FERESE_TEST_PROBE','target/debug/ferese-rime-probe')
|
||||
CTL=REPO/os.environ.get('FERESE_TEST_CTL','target/release/feresectl')
|
||||
RIME=REPO/os.environ.get('FERESE_TEST_RIME_BINARY','target/release/ferese-rime')
|
||||
|
||||
def wait(fn,label,timeout=20):
|
||||
end=time.monotonic()+timeout
|
||||
while time.monotonic()<end:
|
||||
value=fn()
|
||||
if value:return value
|
||||
time.sleep(.05)
|
||||
raise RuntimeError('Timed out: '+label)
|
||||
|
||||
def run_case(label,shell,xdisplay,xauth):
|
||||
case=BASE/label;case.mkdir(mode=0o700,exist_ok=True)
|
||||
runtime=Path(tempfile.mkdtemp(prefix='flime-',dir='/run/user/1000'))
|
||||
for name in ['config/ferese','data','state','cache','home','rime','tmp']:(case/name).mkdir(mode=0o700,parents=True,exist_ok=True)
|
||||
(case/'config/ferese/config.kdl').write_text('xwayland { enabled #false; }\nanimations { enabled #false; }\nstatus { keybinding-guide #false; }\n')
|
||||
(case/'rime/default.custom.yaml').write_text('patch:\n schema_list:\n - schema: luna_pinyin_simp\n')
|
||||
conf=runtime/'bus.conf';conf.write_text('<busconfig><type>session</type><listen>unix:path='+str(runtime/'bus')+'</listen><policy context="default"><allow send_destination="*"/><allow receive_sender="*"/><allow own="*"/></policy></busconfig>')
|
||||
env={k:v for k,v in os.environ.items() if k not in ['WAYLAND_DISPLAY','WAYLAND_SOCKET','FERESE_SOCKET','FERESE_SHELL_CONTROL_SOCKET','DBUS_SESSION_BUS_ADDRESS','GTK_IM_MODULE','QT_IM_MODULE','IBUS_ADDRESS','XMODIFIERS']}
|
||||
env.update(HOME=str(case/'home'),XDG_RUNTIME_DIR=str(runtime),XDG_CONFIG_HOME=str(case/'config'),XDG_DATA_HOME=str(case/'data'),XDG_CACHE_HOME=str(case/'cache'),XDG_STATE_HOME=str(case/'state'),XDG_DATA_DIRS='/usr/local/share:/usr/share',DISPLAY=xdisplay,XAUTHORITY=str(xauth),TMPDIR=str(case/'tmp'),FERESE_ENABLE_INPUT_METHOD='1',FERESE_ENABLE_SCREENCOPY='1',FERESE_SESSION_IMPORT_ENV='0',FERESE_RIME_CONTROL=str(runtime/'rime.sock'),LIBGL_ALWAYS_SOFTWARE='1',__EGL_VENDOR_LIBRARY_FILENAMES='/usr/share/glvnd/egl_vendor.d/50_mesa.json',WINIT_UNIX_BACKEND='x11',ICED_BACKEND='tiny-skia',PATH='/usr/bin',RUST_LOG='ferese=info')
|
||||
children=[];log=(case/'session.log').open('w')
|
||||
try:
|
||||
bus=subprocess.Popen(['dbus-daemon','--config-file='+str(conf),'--nofork','--print-address=1'],env=env,stdout=subprocess.PIPE,stderr=log,text=True,start_new_session=True);children.append(bus)
|
||||
env['DBUS_SESSION_BUS_ADDRESS']=bus.stdout.readline().strip();assert env['DBUS_SESSION_BUS_ADDRESS'].startswith('unix:')
|
||||
comp=subprocess.Popen([str(COMPOSITOR),'--backend','nested','--supervise-client','--grant-effects','--grant-shell-control','--','/usr/bin/env','WAYLAND_DEBUG=client',str(shell)],env=env,stdout=log,stderr=log,start_new_session=True);children.append(comp)
|
||||
def find_socket():
|
||||
candidates=list(runtime.glob('ferese/instances/*/control.sock'))
|
||||
return next((p for p in candidates if p.is_socket()),None)
|
||||
sock=wait(find_socket,'private IPC')
|
||||
def ctl(*args):return subprocess.check_output([str(CTL),'--socket',str(sock),*map(str,args)],env=env,text=True)
|
||||
outputs=json.loads(ctl('-j','outputs'));assert len(outputs)==1 and outputs[0]['name']=='ferese-winit',outputs
|
||||
wayland=wait(lambda:next((p for p in runtime.glob('wayland-*') if p.is_socket()),None),'private Wayland')
|
||||
child_env=env|{'WAYLAND_DISPLAY':str(wayland),'FERESE_SOCKET':str(sock)}
|
||||
ime=subprocess.Popen([str(RIME),'--user-data',str(case/'rime'),'--shared-data','/usr/share/rime-data'],env=child_env,stdout=log,stderr=log,start_new_session=True);children.append(ime)
|
||||
wait(lambda:(runtime/'rime.sock').is_socket(),'native Rime')
|
||||
def status():return json.loads(subprocess.check_output([str(RIME),'--status'],env=child_env,text=True))['state']
|
||||
wait(lambda: 'ferese_shell_manager_v1' in (case/'session.log').read_text(), 'real shell control binding')
|
||||
time.sleep(.5);ctl('launcher','toggle')
|
||||
wait(lambda:status()['active'],'launcher text-input enable')
|
||||
subprocess.run([str(RIME),'--mode','rime'],env=child_env,capture_output=True,text=True,check=True)
|
||||
def cursor_x():
|
||||
return [int(x) for x in re.findall(r'zwp_text_input_v3[@#]\d+\.set_cursor_rectangle\((-?\d+),', (case/'session.log').read_text())]
|
||||
initial=wait(lambda:cursor_x(),'initial caret')[-1]
|
||||
testenv=child_env|{'FERESE_TEST_INPUT':'1'}
|
||||
def probe(*args):
|
||||
p=subprocess.run([str(PROBE),*args],env=testenv,capture_output=True,text=True,timeout=10)
|
||||
assert p.returncode==0,p.stderr
|
||||
probe('--keys','nihao');time.sleep(.3)
|
||||
(case/'preedit.png').write_bytes(subprocess.check_output([str(CTL),'--socket',str(sock),'screenshot'],env=env))
|
||||
probe('--keys',' ');time.sleep(.6)
|
||||
(case/'commit.png').write_bytes(subprocess.check_output([str(CTL),'--socket',str(sock),'screenshot'],env=env))
|
||||
trace=(case/'session.log').read_text();xs=cursor_x()
|
||||
result={'label':label,'shell':str(shell),'shellSha256':hashlib.sha256(shell.read_bytes()).hexdigest(),'compositorPid':comp.pid,'singlePrivateOutputVerified':True,'nativeRimePid':ime.pid,'launcherImeActive':status()['active'],'committedSyntheticHelloSeenOnProtocol':bool(re.search(r'commit_string\("你好"\)',trace)),'initialCaretX':initial,'caretXs':xs,'committedTextMovedCaret':bool(xs and xs[-1]>initial+8),'privateHeadlessX11Parent':xdisplay,'hostGuiUsed':False,'userProfilesUsed':False,'messagesSent':False}
|
||||
(case/'result.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n');print(json.dumps(result,ensure_ascii=False))
|
||||
probe('--chord','Control_L+a','--key','BackSpace','--key','Escape')
|
||||
return result
|
||||
finally:
|
||||
for child in reversed(children):
|
||||
if child.poll() is None:
|
||||
os.killpg(child.pid,signal.SIGTERM)
|
||||
try:child.wait(timeout=4)
|
||||
except subprocess.TimeoutExpired:os.killpg(child.pid,signal.SIGKILL);child.wait()
|
||||
log.close()
|
||||
# Keep synthetic schema/screenshots/logs as evidence; remove dead private sockets.
|
||||
import shutil
|
||||
shutil.rmtree(runtime)
|
||||
|
||||
|
||||
|
||||
@unittest.skipUnless(os.environ.get('FERESE_TEST_LAUNCHER_IME') == '1', 'opt-in isolated launcher/Rime test')
|
||||
class LauncherRimeTest(unittest.TestCase):
|
||||
def test_preedit_and_commit_reach_launcher(self):
|
||||
for tool in ['Xvfb','xauth','dbus-daemon']:
|
||||
if shutil.which(tool) is None:self.skipTest('missing fixture tool: '+tool)
|
||||
shell=REPO/os.environ.get('FERESE_TEST_SHELL','target/release/ferese-shell')
|
||||
for binary in [COMPOSITOR,PROBE,CTL,RIME,shell]:
|
||||
if not binary.is_file():self.skipTest('build required binary: '+str(binary))
|
||||
BASE.mkdir(mode=0o700,parents=True,exist_ok=True)
|
||||
auth=BASE/'fixture.Xauthority';cookie=secrets.token_hex(16);xvfb=None
|
||||
with (BASE/'xvfb.log').open('w') as log:
|
||||
try:
|
||||
for number in range(190,230):
|
||||
if Path(f'/tmp/.X{number}-lock').exists() or Path(f'/tmp/.X11-unix/X{number}').exists():continue
|
||||
display=':'+str(number)
|
||||
subprocess.run(['xauth','-f',str(auth),'add',display,'MIT-MAGIC-COOKIE-1',cookie],capture_output=True,check=True)
|
||||
auth.chmod(0o600)
|
||||
xvfb=subprocess.Popen(['Xvfb',display,'-screen','0','1600x1000x24','-nolisten','tcp','-auth',str(auth)],stdout=log,stderr=log,start_new_session=True)
|
||||
try:wait(lambda:Path(f'/tmp/.X11-unix/X{number}').is_socket(),'Xvfb startup',5)
|
||||
except RuntimeError:
|
||||
if xvfb.poll() is None:os.killpg(xvfb.pid,signal.SIGTERM);xvfb.wait()
|
||||
xvfb=None;continue
|
||||
break
|
||||
self.assertIsNotNone(xvfb,'No private Xvfb display available')
|
||||
result=run_case('current',shell.resolve(),display,auth)
|
||||
self.assertTrue(result['singlePrivateOutputVerified'])
|
||||
self.assertTrue(result['committedSyntheticHelloSeenOnProtocol'])
|
||||
self.assertTrue(result['committedTextMovedCaret'], 'Wayland commit arrived but did not update launcher text/caret')
|
||||
finally:
|
||||
if xvfb is not None and xvfb.poll() is None:os.killpg(xvfb.pid,signal.SIGTERM);xvfb.wait(timeout=5)
|
||||
auth.unlink(missing_ok=True)
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
@@ -76,6 +76,14 @@ class PackageTests(unittest.TestCase):
|
||||
self.assertTrue((dest / 'usr/share/ferese/wallpapers/ferese.png').is_file())
|
||||
self.assertEqual((dest / 'usr/libexec/ferese/ferese-session').stat().st_mode & 0o777, 0o755)
|
||||
self.assertEqual((dest / 'usr/libexec/ferese').stat().st_mode & 0o777, 0o755)
|
||||
self.assertTrue((dest / 'usr/libexec/ferese/ferese-rime').is_file())
|
||||
self.assertIn('schema: luna_pinyin_simp',
|
||||
(dest / 'usr/libexec/ferese/rime/default.custom.yaml').read_text())
|
||||
provenance = dest / 'usr/share/doc/ferese/vendor/smithay'
|
||||
for name in ('UPSTREAM.md', 'shm-texture-cache-format.patch'):
|
||||
origin = PACKAGE.ROOT / 'vendor/smithay' / (
|
||||
name if name == 'UPSTREAM.md' else 'patches/' + name)
|
||||
self.assertEqual((provenance / name).read_bytes(), origin.read_bytes())
|
||||
|
||||
def test_same_version_from_different_sources_keeps_both_licenses(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
@@ -107,6 +115,36 @@ class PackageTests(unittest.TestCase):
|
||||
self.assertEqual(report[0]['license'], 'MIT')
|
||||
self.assertEqual(report[0]['source'], 'registry+test')
|
||||
|
||||
def test_vendored_notices_exclude_workspace_members_and_are_relocatable(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
reports = []
|
||||
for checkout in ('first-checkout', 'second-checkout'):
|
||||
workspace = root / checkout / 'ferese'
|
||||
packages = []
|
||||
for name, folder, license_text in (
|
||||
('ferese', workspace / 'compositor', 'workspace notice'),
|
||||
('smithay', workspace / 'vendor/smithay', 'Smithay MIT notice'),
|
||||
('external', workspace.parent / 'shared/external', 'external notice')):
|
||||
folder.mkdir(parents=True)
|
||||
(folder / 'LICENSE.txt').write_text(license_text)
|
||||
packages.append({'id': f'path+file://{folder}#{name}@0.7.0',
|
||||
'name': name, 'version': '0.7.0', 'source': None,
|
||||
'manifest_path': str(folder / 'Cargo.toml'),
|
||||
'license': 'MIT', 'license_file': None})
|
||||
destination = workspace / 'notices'
|
||||
PACKAGE.licenses(destination, {'packages': packages,
|
||||
'workspace_members': [packages[0]['id']], 'workspace_root': str(workspace)})
|
||||
report = json.loads((destination / 'dependencies.json').read_text())
|
||||
self.assertEqual({p['name']: p['source'] for p in report}, {
|
||||
'smithay': 'path+vendor/smithay', 'external': 'path+../shared/external'})
|
||||
notices = {p['name']: (destination / p['notice_directory'] / 'LICENSE.txt').read_text()
|
||||
for p in report}
|
||||
self.assertEqual(notices, {'smithay': 'Smithay MIT notice', 'external': 'external notice'})
|
||||
self.assertNotIn(str(root), (destination / 'dependencies.json').read_text())
|
||||
reports.append(report)
|
||||
self.assertEqual(reports[0], reports[1])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -141,6 +141,18 @@ m.main()
|
||||
self.assertEqual(os.readlink(self.base / 'current'), 'releases/first')
|
||||
self.assertEqual(installer.Installation(self.root).manifest('first')['release'], 'first')
|
||||
|
||||
def test_upgrade_and_rollback_accept_installed_release_before_native_rime(self):
|
||||
self.install()
|
||||
self.without_files(self.base / 'releases/first', {'ferese-rime', 'rime/default.custom.yaml'})
|
||||
# Older releases did not publish the native input command either.
|
||||
(self.root / 'usr/local/bin/ferese-rime').unlink()
|
||||
self.install(1)
|
||||
self.assertTrue((self.root / 'usr/local/bin/ferese-rime').is_symlink())
|
||||
self.assertTrue((self.base / 'releases/second/ferese-rime').is_file())
|
||||
self.run_command('rollback')
|
||||
self.assertFalse((self.root / 'usr/local/bin/ferese-rime').exists())
|
||||
self.assertEqual(installer.Installation(self.root).manifest('first')['release'], 'first')
|
||||
|
||||
def test_upgrade_and_rollback_accept_installed_release_before_appearance_wallpapers(self):
|
||||
self.install()
|
||||
legacy = self.base / 'releases/first'
|
||||
|
||||
@@ -13,19 +13,46 @@ SCRIPT = Path(__file__).resolve().parents[2] / 'packaging/ferese-session-shell'
|
||||
|
||||
class SessionStartupTest(unittest.TestCase):
|
||||
def run_session(self, direct=True, fail_target=False, shell_status=0, terminate=False,
|
||||
display=None, xauthority=None, extra_env=None, programs_override=None):
|
||||
display=None, xauthority=None, extra_env=None, programs_override=None,
|
||||
user_customization=None, restart_after_kill=False):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
helper = root / 'ferese-session-shell'
|
||||
shutil.copyfile(SCRIPT, helper)
|
||||
(root / 'rime').mkdir()
|
||||
shutil.copyfile(SCRIPT.parent / 'rime/default.custom.yaml', root / 'rime/default.custom.yaml')
|
||||
shared = root / 'rime-shared'
|
||||
shared.mkdir()
|
||||
(shared / 'luna_pinyin_simp.schema.yaml').write_text('fixture')
|
||||
units = root / 'units'
|
||||
units.mkdir()
|
||||
foreign_unit = units / '[email protected]'
|
||||
foreign_unit.write_text('foreign service')
|
||||
if user_customization is not None:
|
||||
user_rime = root / 'data/ferese/rime'
|
||||
user_rime.mkdir(parents=True)
|
||||
(user_rime / 'default.custom.yaml').write_text(user_customization)
|
||||
programs = {
|
||||
'dbus-update-activation-environment': (
|
||||
'printf "import:%s\\n" "$WAYLAND_DISPLAY" >> "$TEST_ROOT/events"\n'
|
||||
'printf "vars:%s\\n" "$*" >> "$TEST_ROOT/events"\n'),
|
||||
'dbus-send': 'echo reload-bus >> "$TEST_ROOT/events"\n',
|
||||
'systemctl': 'echo "$*" >> "$TEST_ROOT/events"\nif [[ $* == "--user start ferese-session.target" && $TEST_FAIL == 1 ]]; then exit 1; fi\n',
|
||||
'systemctl': (
|
||||
'echo "$*" >> "$TEST_ROOT/events"\n'
|
||||
'if [[ $* == "--user start ferese-session.target" && $TEST_FAIL == 1 ]]; then exit 1; fi\n'
|
||||
'if [[ ${2:-} == show && ${3:-} == ferese-rime-session@* ]]; then\n'
|
||||
' if [[ -f $TEST_ROOT/units/$3 ]]; then echo loaded; else echo not-found; fi\n'
|
||||
'elif [[ ${2:-} == stop && ${3:-} == ferese-rime-session@* ]]; then\n'
|
||||
' rm -f -- "$TEST_ROOT/units/$3"\nfi\n'),
|
||||
'ferese-polkit-agent': 'exec sleep 30\n',
|
||||
'ferese-shell': 'printf "shell:%s\\n" "$WAYLAND_SOCKET" >> "$TEST_ROOT/events"\nif [[ $TEST_WAIT == 1 ]]; then exec sleep 30; fi\nexit "$TEST_STATUS"\n',
|
||||
'ferese-rime': 'exit 0\n',
|
||||
'systemd-run': (
|
||||
'for arg; do if [[ $arg == --unit=* ]]; then unit=${arg#--unit=}; fi; done\n'
|
||||
'(set -o noclobber; echo running > "$TEST_ROOT/units/$unit") || exit 1\n'
|
||||
'printf "rime-arg:%s\\n" "$@" >> "$TEST_ROOT/events"\n'
|
||||
'printf "rime-config:%s\\n" "$(cat "$XDG_DATA_HOME/ferese/rime/default.custom.yaml")" >> "$TEST_ROOT/events"\n'
|
||||
'touch "$TEST_ROOT/rime-started"\n'),
|
||||
}
|
||||
programs.update(programs_override or {})
|
||||
for name, body in programs.items():
|
||||
@@ -36,7 +63,10 @@ class SessionStartupTest(unittest.TestCase):
|
||||
FERESE_SESSION_MODE='desktop',
|
||||
FERESE_SESSION_IMPORT_ENV=str(int(direct)), WAYLAND_DISPLAY='private-display',
|
||||
FERESE_PUBLIC_WAYLAND_DISPLAY='public-display', WAYLAND_SOCKET='77',
|
||||
TEST_FAIL=str(int(fail_target)), TEST_STATUS=str(shell_status), TEST_WAIT=str(int(terminate)))
|
||||
FERESE_SOCKET=str(root / 'control.sock'),
|
||||
XDG_DATA_HOME=str(root / 'data'), FERESE_RIME_SHARED_DATA=str(shared),
|
||||
TEST_FAIL=str(int(fail_target)), TEST_STATUS=str(shell_status),
|
||||
TEST_WAIT=str(int(terminate or restart_after_kill)))
|
||||
env.pop('DISPLAY', None)
|
||||
env.pop('XAUTHORITY', None)
|
||||
if display is not None:
|
||||
@@ -44,7 +74,25 @@ class SessionStartupTest(unittest.TestCase):
|
||||
if xauthority is not None:
|
||||
env['XAUTHORITY'] = xauthority
|
||||
env.update(extra_env or {})
|
||||
child = subprocess.Popen(['bash', str(helper)], env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
child = subprocess.Popen(['bash', str(helper)], env=env, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, start_new_session=True)
|
||||
if restart_after_kill:
|
||||
deadline = time.monotonic() + 3
|
||||
while not (root / 'rime-started').exists():
|
||||
if time.monotonic() > deadline:
|
||||
os.killpg(child.pid, signal.SIGKILL)
|
||||
self.fail('Native input unit did not start')
|
||||
time.sleep(.01)
|
||||
child.kill()
|
||||
child.wait(timeout=2)
|
||||
# The helper cannot run cleanup after SIGKILL. Dispose of fake
|
||||
# shell/agent processes, while its fake systemd service survives.
|
||||
os.killpg(child.pid, signal.SIGKILL)
|
||||
child.communicate(timeout=2)
|
||||
self.assertEqual(len(list(units.glob('*.service'))), 2)
|
||||
env.update(TEST_WAIT='0', FERESE_CLIENT_RESTART='1')
|
||||
child = subprocess.Popen(['bash', str(helper)], env=env,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if terminate:
|
||||
deadline = time.monotonic() + 3
|
||||
while not (root / 'events').exists() or 'shell:77' not in (root / 'events').read_text():
|
||||
@@ -54,6 +102,7 @@ class SessionStartupTest(unittest.TestCase):
|
||||
time.sleep(.01)
|
||||
child.send_signal(signal.SIGTERM)
|
||||
child.communicate(timeout=4)
|
||||
self.assertEqual(foreign_unit.read_text(), 'foreign service')
|
||||
return child.returncode, (root / 'events').read_text().splitlines()
|
||||
|
||||
@staticmethod
|
||||
@@ -78,6 +127,78 @@ class SessionStartupTest(unittest.TestCase):
|
||||
self.assertEqual(status, 0)
|
||||
self.assertIn('FERESE_SOCKET=/run/user/1000/ferese/control.sock', self.imported_vars(events))
|
||||
|
||||
def test_native_rime_is_release_pinned_supervised_and_stopped_with_its_session(self):
|
||||
status, events = self.run_session(extra_env={'FERESE_ENABLE_INPUT_METHOD': '1'})
|
||||
self.assertEqual(status, 0)
|
||||
args = [event.removeprefix('rime-arg:') for event in events if event.startswith('rime-arg:')]
|
||||
self.assertIn('--service-type=notify', args)
|
||||
for setting in ('Restart=on-failure', 'WatchdogSec=10', 'StartLimitBurst=5',
|
||||
'BindsTo=ferese-session.target'):
|
||||
self.assertIn('--property=' + setting, args)
|
||||
self.assertIn('WAYLAND_DISPLAY=public-display', args)
|
||||
self.assertIn('WAYLAND_SOCKET', args)
|
||||
self.assertIn('FERESE_SHELL_CONTROL_SOCKET', args)
|
||||
executable = next(arg for arg in args if arg.endswith('/ferese-rime'))
|
||||
self.assertNotEqual(executable, '/usr/bin/ferese-rime')
|
||||
unit = next(arg.removeprefix('--unit=') for arg in args if arg.startswith('--unit='))
|
||||
self.assertIn('--user stop ' + unit, events)
|
||||
self.assertEqual(events[-1], '--user stop ferese-session.target')
|
||||
self.assertIn(' - schema: luna_pinyin_simp', events)
|
||||
|
||||
def test_native_rime_preserves_existing_user_customization(self):
|
||||
status, events = self.run_session(extra_env={'FERESE_ENABLE_INPUT_METHOD': '1'},
|
||||
user_customization='my-custom-dictionary: preserved\n')
|
||||
self.assertEqual(status, 0)
|
||||
self.assertIn('rime-config:my-custom-dictionary: preserved', events)
|
||||
|
||||
def test_hard_killed_helper_replaces_only_its_existing_native_rime_unit(self):
|
||||
status, events = self.run_session(extra_env={'FERESE_ENABLE_INPUT_METHOD': '1'},
|
||||
restart_after_kill=True)
|
||||
self.assertEqual(status, 0)
|
||||
starts = [event.removeprefix('rime-arg:--unit=') for event in events
|
||||
if event.startswith('rime-arg:--unit=')]
|
||||
self.assertEqual(len(starts), 2)
|
||||
self.assertEqual(starts[0], starts[1])
|
||||
stopped = '--user stop ' + starts[0]
|
||||
start_events = [index for index, event in enumerate(events) if event == 'rime-arg:--unit=' + starts[0]]
|
||||
self.assertIn(stopped, events[start_events[0] + 1:start_events[1]])
|
||||
self.assertNotIn('--user stop [email protected]', events)
|
||||
|
||||
def test_unresponsive_old_rime_unit_blocks_replacement_without_stopping_the_desktop(self):
|
||||
# Model a timed-out manager request. The helper must not create a second
|
||||
# input service while it cannot confirm the state of the first one.
|
||||
status, events = self.run_session(extra_env={'FERESE_ENABLE_INPUT_METHOD': '1'},
|
||||
programs_override={'systemctl': (
|
||||
'echo "$*" >> "$TEST_ROOT/events"\n'
|
||||
'if [[ ${2:-} == show ]]; then exit 124; fi\n')})
|
||||
self.assertEqual(status, 0)
|
||||
self.assertIn('shell:77', events)
|
||||
self.assertFalse(any(event.startswith('rime-arg:') for event in events))
|
||||
|
||||
def test_failed_rime_start_leaves_the_desktop_running(self):
|
||||
status, events = self.run_session(extra_env={'FERESE_ENABLE_INPUT_METHOD': '1'},
|
||||
programs_override={'systemd-run': 'exit 7\n'})
|
||||
self.assertEqual(status, 0)
|
||||
self.assertIn('shell:77', events)
|
||||
self.assertFalse(any(event.startswith('--user stop ferese-rime-') for event in events))
|
||||
|
||||
def test_nested_session_never_starts_native_rime(self):
|
||||
status, events = self.run_session(direct=False, extra_env={'FERESE_ENABLE_INPUT_METHOD': '1'})
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(events, ['shell:77'])
|
||||
|
||||
def test_native_input_environment_is_imported_for_activated_applications(self):
|
||||
status, events = self.run_session(extra_env={'FERESE_ENABLE_INPUT_METHOD': '1',
|
||||
'GTK_IM_MODULE': '', 'QT_IM_MODULE': 'wayland', 'QT_IM_MODULES': 'wayland',
|
||||
'IBUS_ADDRESS': '', 'XMODIFIERS': '@im=ibus', 'FERESE_RIME_CONTROL': '/private/rime.sock'})
|
||||
self.assertEqual(status, 0)
|
||||
imported = self.imported_vars(events)
|
||||
for name in ('QT_IM_MODULE', 'QT_IM_MODULES', 'IBUS_ADDRESS', 'FERESE_RIME_CONTROL'):
|
||||
self.assertIn(name, imported)
|
||||
self.assertIn('GTK_IM_MODULE=', imported)
|
||||
self.assertIn('SDL_IM_MODULE=', imported)
|
||||
self.assertIn('XMODIFIERS=@im=ibus', imported)
|
||||
|
||||
def test_a_managed_x11_endpoint_is_imported_for_dbus_activators(self):
|
||||
status, events = self.run_session(display=':7', xauthority='/run/user/1000/ferese-xauth-abcd')
|
||||
self.assertEqual(status, 0)
|
||||
@@ -161,5 +282,46 @@ class SessionStartupTest(unittest.TestCase):
|
||||
self.assertEqual(events.count('autostart'), int(restarted == '0'))
|
||||
|
||||
|
||||
class SessionLauncherTest(unittest.TestCase):
|
||||
def launch(self, nested=False, enabled=None):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
launcher = root / 'ferese-session'
|
||||
shutil.copyfile(SCRIPT.parent / 'ferese-session', launcher)
|
||||
for name, body in {'ferese': '/usr/bin/env > "$TEST_ROOT/environment"\n',
|
||||
'systemctl': 'exit 0\n'}.items():
|
||||
path = root / name
|
||||
path.write_text('#!/bin/bash\n' + body)
|
||||
path.chmod(0o755)
|
||||
env = dict(os.environ, PATH=f'{root}:{os.environ["PATH"]}', TEST_ROOT=directory,
|
||||
HOME=directory, XDG_RUNTIME_DIR=directory, DBUS_SESSION_BUS_ADDRESS='fixture',
|
||||
XDG_STATE_HOME=str(root / 'state'), GTK_IM_MODULE='ibus', QT_IM_MODULE='ibus',
|
||||
QT_IM_MODULES='wayland;ibus', IBUS_ADDRESS='old-ibus', XMODIFIERS='@im=ibus', SDL_IM_MODULE='ibus')
|
||||
env.pop('FERESE_ENABLE_INPUT_METHOD', None)
|
||||
if enabled is not None:
|
||||
env['FERESE_ENABLE_INPUT_METHOD'] = enabled
|
||||
subprocess.run(['bash', str(launcher), *(['--nested'] if nested else [])],
|
||||
env=env, capture_output=True, check=True)
|
||||
return dict(line.split('=', 1) for line in (root / 'environment').read_text().splitlines())
|
||||
|
||||
def test_drm_session_overrides_inherited_ibus_only_for_native_input(self):
|
||||
env = self.launch()
|
||||
self.assertEqual(env['FERESE_ENABLE_INPUT_METHOD'], '1')
|
||||
self.assertNotIn('GTK_IM_MODULE', env)
|
||||
for variable in ('QT_IM_MODULE', 'QT_IM_MODULES'):
|
||||
self.assertEqual(env[variable], 'wayland')
|
||||
self.assertEqual(env['IBUS_ADDRESS'], '')
|
||||
self.assertEqual(env['XMODIFIERS'], '@im=ibus')
|
||||
self.assertNotIn('SDL_IM_MODULE', env)
|
||||
self.assertTrue(env['FERESE_RIME_CONTROL'].endswith('/ferese/rime-control.sock'))
|
||||
|
||||
def test_nested_and_explicit_opt_out_preserve_input_configuration(self):
|
||||
for options in ({'nested': True}, {'enabled': '0'}):
|
||||
with self.subTest(options=options):
|
||||
env = self.launch(**options)
|
||||
self.assertEqual(env['QT_IM_MODULE'], 'ibus')
|
||||
self.assertNotEqual(env.get('FERESE_ENABLE_INPUT_METHOD'), '1')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -24,6 +24,10 @@ jiff.workspace = true
|
||||
libc.workspace = true
|
||||
# Prefer GPU rendering, retaining software rendering as a fallback/override.
|
||||
libcosmic = { workspace = true, features = ["wgpu", "a11y"] }
|
||||
# The pinned Wayland backend gates text-input-v3 event dispatch on this empty
|
||||
# feature. Enable it directly without changing the shell's ordinary app::run or
|
||||
# pulling libcosmic's unrelated single-instance/D-Bus application support.
|
||||
iced_winit = { git = "https://github.com/ferese-wm/libcosmic", tag = "ferese-v0.1.3-rc.3", default-features = false, features = ["single-instance"] }
|
||||
freedesktop-desktop-entry = { version = "0.8.3", default-features = false }
|
||||
iced_accessibility.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
type: bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report!
|
||||
Please don't use generative AI to generate the text of this issue.
|
||||
See our [AI Policy](https://github.com/Smithay/smithay/blob/master/AI.md) for more information.
|
||||
- type: checkboxes
|
||||
id: terms
|
||||
attributes:
|
||||
label: AI Policy
|
||||
options:
|
||||
- label: I have read the [AI policy](https://github.com/Smithay/smithay/blob/master/AI.md).
|
||||
required: true
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Issue description
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
## Description
|
||||
<!-- Please include a summary of the change -->
|
||||
<!-- Any details that you think are important to review this PR? -->
|
||||
<!-- Are there issues / other PRs related to this one? -->
|
||||
|
||||
<!-- If your work contains any usage of generative AI, please read our Policy (https://github.com/Smithay/smithay/blob/master/AI.md) and consider alternatives before submitting this PR. -->
|
||||
|
||||
## Checklist
|
||||
<!-- You need to set this checkbox, for your PR to be considered. -->
|
||||
* [ ] I agree to smithay's [Developer Certificate of Origin](https://github.com/Smithay/smithay/blob/master/DCO.md).
|
||||
+441
@@ -0,0 +1,441 @@
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
format:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
- name: Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
- name: Cargo cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-rust_stable-${{ hashFiles('**/Cargo.toml') }}
|
||||
- name: Format
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
clippy-check:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
- name: Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- name: Get date for registry cache
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: Cargo registry cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }}
|
||||
restore-keys: ${{ runner.os }}-cargo-registry-
|
||||
- name: Get cargo-cache latest version
|
||||
id: cargocacheversion
|
||||
run: echo "::set-output name=version::$(cargo search 'cargo-cache' --limit 1 | head -n 1 | cut -d ' ' -f 3 | cut -d '"' -f 2)"
|
||||
- name: Cargo binaries cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/cargo-cache
|
||||
~/.cargo/.crates.toml
|
||||
~/.cargo/.crates2.json
|
||||
key: ${{ runner.os }}-cargo-binaries-${{ steps.cargocacheversion.outputs.version }}
|
||||
- name: Install cargo-cache
|
||||
run: cargo install cargo-cache --version ${{ steps.cargocacheversion.outputs.version }}
|
||||
- name: Clean cargo cache of old items
|
||||
run: cargo cache clean-unref
|
||||
- name: System dependencies
|
||||
run: sudo apt-get update; sudo apt-get install -y libdrm-dev libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libdisplay-info-dev
|
||||
- name: Clippy Smithay
|
||||
run: cargo clippy --features "test_all_features" -- -D warnings
|
||||
- name: Clippy Anvil
|
||||
run: cargo clippy --manifest-path "./anvil/Cargo.toml" --features "test_all_features" --no-deps -- -D warnings
|
||||
- name: Clippy Smallvil
|
||||
run: cargo clippy --manifest-path "./smallvil/Cargo.toml" --no-deps -- -D warnings
|
||||
- name: Clippy drm-extras
|
||||
run: cargo clippy --manifest-path "./smithay-drm-extras/Cargo.toml" --no-deps -- -D warnings
|
||||
|
||||
check-msrv:
|
||||
env:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- smithay-check-features
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
- name: Rust toolchain
|
||||
uses: dtolnay/[email protected]
|
||||
- name: Get date for registry cache
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: Cargo registry cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }}
|
||||
restore-keys: ${{ runner.os }}-cargo-registry-
|
||||
- name: System dependencies
|
||||
run: sudo apt-get update; sudo apt-get install -y libdrm-dev libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev
|
||||
- name: Check
|
||||
run: cargo check --features "test_all_features"
|
||||
|
||||
check-minimal:
|
||||
env:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- smithay-check-features
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
- name: Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
- name: Get date for registry cache
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: Cargo registry cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }}
|
||||
restore-keys: ${{ runner.os }}-cargo-registry-
|
||||
- name: System dependencies
|
||||
run: sudo apt-get update; sudo apt-get install -y libdrm-dev libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev
|
||||
- name: Downgrade to minimal dependencies
|
||||
run: cargo -Z minimal-versions generate-lockfile
|
||||
# Needed to convert url -> string in winit
|
||||
- name: Selectively upgrade url
|
||||
run: cargo update -p [email protected] --precise 2.3.0
|
||||
# Needed to fix WlTouch trait bounds
|
||||
- name: Selectively upgrade client-toolkit
|
||||
run: cargo update -p [email protected] --precise 0.21.1
|
||||
- name: Rust toolchain
|
||||
uses: dtolnay/[email protected]
|
||||
- name: Check
|
||||
run: cargo check --features "test_all_features"
|
||||
|
||||
smithay-check-features:
|
||||
env:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get date for registry cache
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: Cargo registry cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }}
|
||||
restore-keys: ${{ runner.os }}-cargo-registry-
|
||||
|
||||
- name: Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install cargo-hack
|
||||
uses: taiki-e/install-action@cargo-hack
|
||||
|
||||
- name: System dependencies
|
||||
run: sudo apt-get update; sudo apt-get install -y libdrm-dev libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev
|
||||
|
||||
- name: Test features
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
run: cargo hack check --each-feature --no-dev-deps --exclude-features use_bindgen
|
||||
|
||||
smithay-tests:
|
||||
needs:
|
||||
- smithay-check-features
|
||||
|
||||
env:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get date for registry cache
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: Cargo registry cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }}
|
||||
restore-keys: ${{ runner.os }}-cargo-registry-
|
||||
|
||||
- name: Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: System dependencies
|
||||
run: sudo apt-get update; sudo apt-get install -y libdrm-dev libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpixman-1-dev
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
run: cargo test --features "test_all_features"
|
||||
|
||||
smallvil-check:
|
||||
needs:
|
||||
- smithay-check-features
|
||||
|
||||
env:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get date for registry cache
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: Cargo registry cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }}
|
||||
restore-keys: ${{ runner.os }}-cargo-registry-
|
||||
|
||||
- name: Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: System dependencies
|
||||
run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libegl1-mesa-dev libwayland-dev
|
||||
|
||||
- name: Test smallvil
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
run: cargo check --manifest-path "./smallvil/Cargo.toml"
|
||||
|
||||
anvil-check-features:
|
||||
needs:
|
||||
- smithay-check-features
|
||||
|
||||
env:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get date for registry cache
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: Cargo registry cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }}
|
||||
restore-keys: ${{ runner.os }}-cargo-registry-
|
||||
|
||||
- name: Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install cargo-hack
|
||||
uses: taiki-e/install-action@cargo-hack
|
||||
|
||||
- name: System dependencies
|
||||
run: sudo apt-get update; sudo apt-get install -y libdrm-dev libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libdisplay-info-dev
|
||||
|
||||
- name: Test features
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
run: cargo hack check --each-feature --manifest-path "./anvil/Cargo.toml" --exclude-features profile-with-puffin,profile-with-tracy,profile-with-tracy-mem
|
||||
|
||||
anvil-wlcs:
|
||||
needs:
|
||||
- anvil-check-features
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- job_name: "Core tests"
|
||||
gtest_filter: "SelfTest*:FrameSubmission*"
|
||||
flag: core
|
||||
- job_name: "Output tests"
|
||||
gtest_filter: "XdgOutputV1Test*"
|
||||
flag: output
|
||||
- job_name: "Pointer input tests"
|
||||
gtest_filter: "*/SurfacePointerMotionTest*:RelativePointer*"
|
||||
flag: pointer-input
|
||||
- job_name: "Bad Buffer Test"
|
||||
gtest_filter: "BadBufferTest*"
|
||||
flag: buffer
|
||||
|
||||
name: "WLCS: ${{ matrix.job_name }}"
|
||||
|
||||
env:
|
||||
RUSTFLAGS: "-D warnings"
|
||||
GRCOV_VERSION: "0.8.10"
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get date for registry cache
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: Cargo registry cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }}
|
||||
restore-keys: ${{ runner.os }}-cargo-registry-
|
||||
|
||||
- name: Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: llvm-tools-preview
|
||||
|
||||
- name: grcov cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/grcov
|
||||
~/.cargo/.crates.toml
|
||||
~/.cargo/.crates2.json
|
||||
key: ${{ runner.os }}-grcov-${{ env.GRCOV_VERSION }}
|
||||
|
||||
- name: Install grcov
|
||||
run: cargo install grcov --version ${{ env.GRCOV_VERSION }}
|
||||
|
||||
- name: System dependencies
|
||||
run: sudo apt-get update; sudo apt-get install -y cmake libgtest-dev google-mock libboost-dev libxkbcommon-dev libwayland-dev libdisplay-info-dev
|
||||
|
||||
- name: WLCS build cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: wlcs
|
||||
key: ${{ runner.os }}-build-wlcs-${{ hashFiles('./compile_wlcs.sh') }}
|
||||
|
||||
- name: Build WLCS
|
||||
run: ./compile_wlcs.sh
|
||||
|
||||
- name: Build anvil WLCS plugin
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
RUSTFLAGS: "-Cinstrument-coverage --cfg coverage"
|
||||
run: cargo build --manifest-path "./wlcs_anvil/Cargo.toml"
|
||||
|
||||
- name: Run test suite
|
||||
run: ./wlcs/wlcs ./target/debug/libwlcs_anvil.so --gtest_filter="${{ matrix.gtest_filter }}"
|
||||
env:
|
||||
LLVM_PROFILE_FILE: "anvil-wlcs-%p-%m.profraw"
|
||||
|
||||
- name: Coverage
|
||||
run: grcov . --binary-path ./target/debug -s . -t lcov --branch --llvm --ignore-not-existing --ignore '*/examples/*' --ignore '*/tests/*' --ignore '*/.cargo/registry/*' --excl-br-start "mod tests \{" --excl-start "mod tests \{" --excl-br-line "#\[derive\(" --excl-line "#\[derive\(" -o lcov.info
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
flags: wlcs-${{ matrix.flag }}
|
||||
|
||||
doc:
|
||||
name: Documentation on Github Pages
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- smithay-check-features
|
||||
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get date for registry cache
|
||||
id: date
|
||||
run: echo "::set-output name=date::$(date +'%Y-%m-%d')"
|
||||
- name: Cargo registry cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }}
|
||||
restore-keys: ${{ runner.os }}-cargo-registry-
|
||||
|
||||
- name: Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-09-26
|
||||
|
||||
- name: System dependencies
|
||||
run: sudo apt-get update; sudo apt-get install -y libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev
|
||||
|
||||
- name: Build Documentation
|
||||
env:
|
||||
RUSTDOCFLAGS: --cfg=docsrs -D warnings
|
||||
run: cargo doc --no-deps --features "test_all_features" -p smithay -p calloop:0.14 -p drm -p gbm -p input -p udev -p wayland-server -p wayland-backend -p wayland-protocols -p winit -p x11rb -p tracing
|
||||
|
||||
- name: Setup index
|
||||
run: cp ./doc_index.html ./target/doc/index.html
|
||||
|
||||
- name: Upload static files as artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: ./target/doc
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: check_typos
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: typos-action
|
||||
uses: crate-ci/[email protected]
|
||||
@@ -0,0 +1,6 @@
|
||||
target
|
||||
Cargo.lock
|
||||
*.bk
|
||||
.vscode
|
||||
.vagga
|
||||
/wlcs
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
max_width = 110
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
The following policy applies if you make a contribution to Smithay's
|
||||
code, documentation, issues, comments, artwork or any other type of resource.
|
||||
|
||||
It is your responsibility to ensure that:
|
||||
- You fully understand how the code in your contribution works.
|
||||
- Your change is accurately described in the commit message.
|
||||
- Your contribution is tested and working as described.
|
||||
- To the best of your knowledge, you are not introducing bugs; any exceptions to this rule must be presented with clear reasoning.
|
||||
- You are not violating copyright (such as software licenses, creative commons or any other). (See also our [DCO](https://github.com/Smithay/smithay/blob/master/DCO.md).)
|
||||
- Use of any AI generated code or documentation is clearly indicated in your commit messages.
|
||||
|
||||
If you repeatedly fail to meet these standards, your contributions will no longer
|
||||
be considered by Smithay's maintainers and possibly removed without further notice.
|
||||
|
||||
Due to the requirements listed above, we strongly advise against using large
|
||||
language models or other probabilistic tools such as Copilot and ChatGPT.
|
||||
Code produced by these systems is usually more difficult to reason about
|
||||
and fails to express important technical design decisions in a concise form.
|
||||
This results in additional maintainer burden, draining the (already limited)
|
||||
time and energy of our reviewers. Additionally, the legal circumstances of the training sets these
|
||||
tools are based on are subject of active legal proceedings, which puts the copyright of generated
|
||||
contributions into question.
|
||||
|
||||
Last but not least, we ask you to not use generative AI to write comments or descriptions
|
||||
on our issue tracker, discussions and pull requests. In our experience these tools tend to generate
|
||||
very inconcise text and more often incorrect statements about the cause of underlying issues or code.
|
||||
|
||||
If you rely on such tools to make up for a language barrier, we strongly encourage you to use
|
||||
(ML-based or otherwise) translation tools as our maintainers much prefer to deal with
|
||||
imperfect english than generated walls of text. Many of us aren't native english speakers as well.
|
||||
Vendored
+841
@@ -0,0 +1,841 @@
|
||||
# Smithay Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Breaking changes
|
||||
|
||||
`wayland::virtual_keyboard` no longer sends key events to focused clients on its
|
||||
own. A virtual keyboard is now an regular input device:
|
||||
`VirtualKeyboardManagerState::new` requires the state to implement the new
|
||||
`VirtualKeyboardHandler`, which receives `InputEvent<VirtualKeyboardBackend>`s
|
||||
for the compositor to handle like real devices (so special keys, compositor
|
||||
bindings, idle handling and focus works properly). The compositor has to
|
||||
activate the device's keymap (`VirtualKeyboardDevice::keymap`) before processing
|
||||
its key events. Modifiers set with the `modifiers` request are not reset when
|
||||
the device is destroyed (the masks are absolute, so that would also clear state
|
||||
not set by the device, like locks set by other devices on a shared keymap). The
|
||||
compositor is responsible for reconciling the modifier state on `DeviceRemoved`
|
||||
(e.g., by tracking the bits set by it). `VirtualKeyboardUserData` is no longer
|
||||
generic over the state type.
|
||||
|
||||
`crate::wayland::selection::data_device::start_dnd` was removed in favor of exposing the
|
||||
underlying `DnDGrab` and associated types to make it possible to write external Drag&Drop sources
|
||||
and targets (e.g. for other shell implementations). See the `Additions`-section for more info.
|
||||
|
||||
This also means the `ServerDndGrabHandler` was removed. The `ClientDndGrabHandler` was split into
|
||||
a generic `DndGrabHandler` and a specific `WaylandDndGrabHandler`. `WaylandDndGrabHandler::started` needs to
|
||||
be implemented and explicitly start a `DnDGrab` for wayland DnD-operations to work.
|
||||
|
||||
`X11WM::start_wm` now needs a `DisplayHandle`-reference and adds requirements for the State type to
|
||||
implement `DndGrabHandler` and the `SeatHandler`s `PointerFocus` and `TouchFocus`-types to implement
|
||||
the new `DndFocus` trait.
|
||||
|
||||
`GbmFramebufferExporter::new` now accepts a `NodeFilter` as import node, which
|
||||
enables accepting dmabufs originating from any node.
|
||||
|
||||
```diff
|
||||
-GbmFramebufferExporter::new(gbm, node);
|
||||
+GbmFramebufferExporter::new(gbm, node.into());
|
||||
|
||||
-GbmFramebufferExporter::new(gbm, None);
|
||||
+GbmFramebufferExporter::new(gbm, NodeFilter::None);
|
||||
|
||||
-GbmFramebufferExporter::new(gbm, Some(node));
|
||||
+GbmFramebufferExporter::new(gbm, node.into());
|
||||
|
||||
+GbmFramebufferExporter::new(gbm, NodeFilter::All);
|
||||
```
|
||||
|
||||
`DrmOutputManager` does now provide an explicit `lock`-method returning a [`LockedDrmOutputManager`].
|
||||
All other methods have moved to this new type, allowing compositors to take the same lock across multiple
|
||||
operations.
|
||||
|
||||
xdg_shell, layer_shell, and session_lock surfaces now track `last_acked` state for each commit.
|
||||
You can find it in the `last_acked` field of the corresponding `...CachedState` structs.
|
||||
|
||||
The "current state" and "current serial" attribute fields were removed; use `last_acked` from the new `with_committed_state()` accessors instead (and verify that you didn't actually want `with_pending_state()` instead where you used the "current state").
|
||||
|
||||
```diff
|
||||
-ToplevelSurface::current_state();
|
||||
+ToplevelSurface::with_committed_state(|state| {
|
||||
+ // ...
|
||||
+});
|
||||
|
||||
-PopupSurface::current_state();
|
||||
+PopupSurface::with_committed_state(|state| {
|
||||
+ // ...
|
||||
+});
|
||||
|
||||
-LayerSurface::current_state();
|
||||
+LayerSurface::with_committed_state(|state| {
|
||||
+ // ...
|
||||
+});
|
||||
|
||||
-LockSurface::current_state();
|
||||
+LockSurface::with_committed_state(|state| {
|
||||
+ // ...
|
||||
+});
|
||||
|
||||
struct XdgToplevelSurfaceRoleAttributes {
|
||||
- configured: bool,
|
||||
- configure_serial: Option<Serial>,
|
||||
- current: ToplevelState,
|
||||
- current_serial: Option<Serial>,
|
||||
- last_acked: Option<ToplevelState>,
|
||||
+ last_acked: Option<ToplevelConfigure>,
|
||||
// ...
|
||||
}
|
||||
|
||||
struct XdgPopupSurfaceRoleAttributes {
|
||||
- configured: bool,
|
||||
- configure_serial: Option<Serial>,
|
||||
- current: PopupState,
|
||||
- current_serial: Option<Serial>,
|
||||
- committed: bool,
|
||||
- last_acked: Option<PopupState>,
|
||||
+ last_acked: Option<PopupConfigure>,
|
||||
// ...
|
||||
}
|
||||
|
||||
struct LayerSurfaceAttributes {
|
||||
- configured: bool,
|
||||
- configure_serial: Option<Serial>,
|
||||
- current: LayerSurfaceState,
|
||||
- last_acked: Option<LayerSurfaceState>,
|
||||
+ last_acked: Option<LayerSurfaceConfigure>,
|
||||
// ...
|
||||
}
|
||||
|
||||
-impl Copy for LayerSurfaceCachedState;
|
||||
```
|
||||
|
||||
The following methods are no longer needed as Smithay does them automatically now:
|
||||
```diff
|
||||
-ToplevelSurface::reset_initial_configure_sent();
|
||||
-PopupSurface::reset_initial_configure_sent();
|
||||
```
|
||||
|
||||
You also no longer need to manually set `LayerSurfaceAttributes::initial_configure_sent`, Smithay handles it automatically.
|
||||
|
||||
The `delegate_*!` macros have been replaced with a single `delegate_dispatch2!` macro, which implements dispatch in terms
|
||||
of new `Dipsatch2` and `GlobalDispatch2` traits. These will replace `Dispatch` and `GlobalDispatch` in a future version of
|
||||
`wayland_server`.
|
||||
|
||||
The `SpaceElement::geometry` impl for `X11Surface` is no longer equivalent to its bbox. For clients that set
|
||||
`_GTK_FRAME_EXTENTS`, the shadow extents are subtracted from the bbox to give the geometry.
|
||||
|
||||
`X11Surface::geometry` has been renamed to `X11Surface::last_configure`. `X11Surface::geometry` now returns the bounding
|
||||
box minus the surface's frame extents.
|
||||
|
||||
`SessionLockHandler::unlock` will no longer be called if the `ext_session_lock_v1` instance that sent the
|
||||
`unlock_and_destroy` request is not the same instance as the one that successfully locked the session.
|
||||
|
||||
`SessionLockHandler::new_surface` can be called for multiple `SessionLocker` instances that could be in-flight at the same
|
||||
time. `LockSurface::ext_session_lock` has been added to allow the compositor to disambiguate, and associate lock surfaces
|
||||
with the correct lock instance.
|
||||
|
||||
`backend::input` and `input` now use `backend::input::InputTime` for timestamps instead of `u32` or `u64`. `InputTime::now()`
|
||||
is used for timestamps for synthesized events.
|
||||
|
||||
`TextInputActivation` must be implemented. It allows for hooks to be run when a text input is `activated` and `deactivated`.
|
||||
|
||||
`KeyboardHandle::input_intercept()` now has a `source` argument and early-returns `None` when the key was already pressed/released, matching the behavior of `KeyboardHandle::input()`.
|
||||
|
||||
```diff
|
||||
-let (res, mods_changed) = keyboard.input_intercept(data, keycode, state, filter);
|
||||
+let Some((res, mods_changed)) = keyboard.input_intercept(
|
||||
+ KeyboardSource::MAIN,
|
||||
+ data,
|
||||
+ keycode,
|
||||
+ state,
|
||||
+ filter,
|
||||
+) else {
|
||||
+ return;
|
||||
+};
|
||||
```
|
||||
|
||||
### Additions
|
||||
|
||||
- Add `WmWindowProperty::Other` to forward unrecognized X11 property changes to the compositor.
|
||||
|
||||
|
||||
- The wlr virtual pointer protocol is now available in `smithay::wayland::virtual_pointer`.
|
||||
|
||||
- `Xkb::keymap_as_string` returns the active keymap as `XKB_KEYMAP_FORMAT_TEXT_V1`.
|
||||
|
||||
- `XkbContext::set_modifier_mask` applies raw xkb modifier masks to a keyboard
|
||||
for clients that report modifier state directly instead of through key presses
|
||||
(e.g., `zwp_virtual_keyboard_v1.modifiers`).
|
||||
|
||||
- `KeyboardHandle::input_forward_bypassing_grab` sends a key skipping the active
|
||||
grab, which can be used by compositors to prevent infinite loops when a client
|
||||
with a grab forwards a key through its own virtual keyboard, which can be
|
||||
tracked with `InputMethodHandle::keyboard_grab_client` and
|
||||
`VirtualKeyboardDevice::client`.
|
||||
|
||||
- `VirtualKeyboardDevice::source` gives each virtual keyboard its own
|
||||
`KeyboardSource` so keys pressed on multiple devices are released when the
|
||||
last one is released.
|
||||
|
||||
- ExtBackgroundEffect protocol is now available in `smithay::wayland::background_effect` module.
|
||||
|
||||
`crate::input::dnd` was introduced to enable implementation of Drag&Drop operations on custom types.
|
||||
Internally the same types and traits are used to implement `wayland::data_device` dnd-operations and XDND
|
||||
operations (see below).
|
||||
|
||||
`DnDGrab` is the new entry-point for DnD operations. It requires `SeatHandler::PointerFocus` or
|
||||
`SeatHandler::TouchFocus` respectively to implement `DndFocus`, which is used to send events to
|
||||
types, which can be targets of DnD operations. `Source` is a new trait for types that represent
|
||||
sources of data for DnD operations.
|
||||
|
||||
`Source::accepted` is called when a target accepts a mime-type, or rejects the offer with `None`.
|
||||
Sources that proxy an upstream offer need it: a nested compositor forwarding a drag to its own
|
||||
clients only receives `drop` from the host compositor once it has itself accepted on the upstream
|
||||
offer, and previously had no way to know when a nested client had done so. It has a default no-op
|
||||
implementation, so sources that originate their own data are unaffected.
|
||||
|
||||
The Xwayland WM can now handle XDND operations and bridge them over to the new generic DnD interface
|
||||
allowing DnD operations between X11 and Wayland clients (both directions).
|
||||
|
||||
`X11Surface` now has a new `surface_under`-method, which is also internally used by `SpaceElement::is_in_input_region` and `crate::desktop::Window::surface_under`. Any direct usage of `under_from_surface_tree` on the underlying `wl_surface` of an `X11Surface` should be replaced with this method for XDND to work correctly.
|
||||
|
||||
`X11Surface` now parses and exposes the Motif WM hints via `motif_hints()`.
|
||||
|
||||
`X11Wm` allows you to remove and clear XSETTINGS settings.
|
||||
|
||||
xdg_shell and layer_shell now enforce the client acking a configure before committing a buffer, as required by the protocols.
|
||||
|
||||
```rs
|
||||
struct ToplevelCachedState {
|
||||
/// Configure last acknowledged by the client at the time of the commit.
|
||||
last_acked: Option<ToplevelConfigure>,
|
||||
}
|
||||
|
||||
/// Provides access to the current committed cached state.
|
||||
fn ToplevelSurface::with_cached_state<T>(&self, f: impl FnOnce(&ToplevelCachedState) -> T) -> T;
|
||||
/// Provides access to the current committed state.
|
||||
fn ToplevelSurface::with_committed_state<T>(&self, f: impl FnOnce(Option<&ToplevelState>) -> T) -> T;
|
||||
|
||||
struct PopupCachedState {
|
||||
/// Configure last acknowledged by the client at the time of the commit.
|
||||
last_acked: Option<ToplevelConfigure>,
|
||||
}
|
||||
|
||||
/// Provides access to the current committed cached state.
|
||||
fn PopupSurface::with_cached_state<T>(&self, f: impl FnOnce(&PopupCachedState) -> T) -> T;
|
||||
/// Provides access to the current committed state.
|
||||
fn PopupSurface::with_committed_state<T>(&self, f: impl FnOnce(Option<&PopupState>) -> T) -> T;
|
||||
|
||||
struct LayerSurfaceCachedState {
|
||||
/// Configure last acknowledged by the client at the time of the commit.
|
||||
last_acked: Option<LayerSurfaceConfigure>,
|
||||
// ...
|
||||
}
|
||||
|
||||
/// Provides access to the current committed cached state.
|
||||
fn LayerSurface::with_cached_state<T>(&self, f: impl FnOnce(&LayerSurfaceCachedState) -> T) -> T;
|
||||
/// Provides access to the current committed state.
|
||||
fn LayerSurface::with_committed_state<T>(&self, f: impl FnOnce(Option<&LayerSurfaceState>) -> T) -> T;
|
||||
|
||||
struct LockSurfaceCachedState {
|
||||
/// Configure last acknowledged by the client at the time of the commit.
|
||||
last_acked: Option<LockSurfaceConfigure>,
|
||||
}
|
||||
|
||||
/// Provides access to the current committed cached state.
|
||||
fn LockSurface::with_cached_state<T>(&self, f: impl FnOnce(&LockSurfaceCachedState) -> T) -> T;
|
||||
/// Provides access to the current committed state.
|
||||
fn LockSurface::with_committed_state<T>(&self, f: impl FnOnce(Option<&LockSurfaceState>) -> T) -> T;
|
||||
|
||||
struct LockSurfaceAttributes {
|
||||
server_pending: Option<LockSurfaceState>,
|
||||
pending_configures: Vec<LockSurfaceConfigure>,
|
||||
last_acked: Option<LockSurfaceConfigure>,
|
||||
}
|
||||
|
||||
type LockSurfaceData = Mutex<LockSurfaceAttributes>;
|
||||
|
||||
struct LockSurfaceConfigure {
|
||||
state: LockSurfaceState,
|
||||
serial: Serial,
|
||||
}
|
||||
```
|
||||
|
||||
Added `Element::is_framebuffer_effect` and `RenderElement::capture_framebuffer` to better support render elements,
|
||||
that take the current framebuffers contents and modify them, e.g. blurring the backgrounds of windows.
|
||||
See the documentation for these functions for how to make use of them, but note, that they provide
|
||||
default implementations, which result in skipping the new functionality. As such any custom `RenderElements` wrappers
|
||||
(not using `crate::render_elements!`) need to be updated.
|
||||
|
||||
`Space` has new functionality that allows for more fine-grained control over window stacking:
|
||||
|
||||
- `map_element_above()`: maps a new element directly above an existing one.
|
||||
- `raise_element_above()`: same as above, but moves an already-mapped element.
|
||||
- `lower_element()`: lowers an element to the bottom of the stack, respecting its z-index group.
|
||||
- `relocate_element()`: moves an element to a new location in the space without changing the stacking order.
|
||||
|
||||
`Output` now has `owns_xdg_output()` which allows you to match an XDG output protocol object with an `Output`.
|
||||
|
||||
Added `Renderer::invalidate_caches`, which unconditionally drops all renderer-internal caches,
|
||||
including entries `Renderer::cleanup_texture_cache` retains while their source buffers are still
|
||||
alive.
|
||||
|
||||
Added `GpuManager::cleanup_texture_cache` and `GpuManager::invalidate_caches`, which apply the
|
||||
corresponding `Renderer` method to every enumerated device. `GpuManager::invalidate_caches`
|
||||
additionally drops the buffers cached for copying between a render and a target node, which no
|
||||
`Renderer` owns.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
`input_method_v2`: a second input method binding a seat that already has one is now the object told
|
||||
it is `unavailable`, per the protocol, leaving the existing instance in place. Previously the event
|
||||
went to the existing instance and the newcomer was dropped untracked. Destroying such an inert
|
||||
newcomer no longer clears the active instance, and its requests no longer drive the seat.
|
||||
|
||||
`DrmSurface::use_vrr(false)` now succeeds as a no-op when using legacy DRM. Attempts to enable VRR
|
||||
on legacy DRM still return `UnknownProperty`.
|
||||
|
||||
The `xdg_toplevel` request handler now posts an `invalid_size` error when a client performs a `set_max_size` or `set_min_size` request with a negative width or height.
|
||||
|
||||
`DrmDeviceFd::new` no longer tries to become DRM master on a render node, which the kernel always
|
||||
refuses, so it no longer warns about it.
|
||||
|
||||
`SimpleCrtcMapper` (in `smithay-drm-extras`) now releases the CRTC reservation of any connector that
|
||||
is no longer connected, including connectors that have disappeared from the resource list entirely
|
||||
rather than being reported as disconnected. Previously such connectors (for example DP-MST sink
|
||||
connectors when a dock is unplugged, or across suspend/resume) leaked their CRTC reservation
|
||||
indefinitely, which could accumulate until a newly connected output could no longer be assigned a
|
||||
CRTC.
|
||||
|
||||
XWayland now honors WM_HINTS request to map the window in IconicState(minimized). Compositors should
|
||||
check X11Surface::is_hidden() at mapping time and minimize the window appropriately.
|
||||
|
||||
Space::refresh() now clears stale outputs before emitting any enter events so no wl_surface.enter events
|
||||
are emitted with a stale wl_output anymore.
|
||||
|
||||
The XWayland WM implementation previously incorrectly prefixed large transfers from X11 windows with
|
||||
the four INCR bytes.
|
||||
|
||||
`MultiRenderer::cleanup_texture_cache` now also cleans up the devices other than the render and
|
||||
target device. Buffers that cannot be imported on the render node directly are imported on their
|
||||
source device, so those devices accumulate cached imports that previously were never released.
|
||||
Cleanup is now also attempted on every device even if it fails on one of them, with every failure
|
||||
logged and the first error returned.
|
||||
|
||||
`send_frames_surface_tree` (and `Window::send_frame`, `LayerSurface::send_frame`) no longer sends frame
|
||||
callbacks to surfaces whose renderer state has no buffer, or to their subsurfaces.
|
||||
|
||||
`GlesRenderer::cleanup_texture_cache` and `GlesRenderer::invalidate_caches` now only make their EGL
|
||||
context current when resources are queued for destruction. This avoids waking idle GPUs and
|
||||
blocking the compositor while they resume.
|
||||
|
||||
`ext_image_copy_capture_session_v1` now raises the `duplicate_frame` protocol error when a client
|
||||
calls `create_frame` while the session still has an active frame, as the protocol requires. Previously
|
||||
the request was accepted and the extra frame was silently added to the session.
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Breaking changes
|
||||
`DrmSyncobjHandler::drm_syncobj_state` is now Optional.
|
||||
Whenever state is None `ImportTimeline` request will result in `InvalidTimeline` protocol error.
|
||||
This allows one to destroy DrmSyncobjState by calling `DrmSyncobjState::into_global()`
|
||||
```diff
|
||||
-fn DrmSyncobjHandler::drm_syncobj_state(&mut self) -> &mut DrmSyncobjState
|
||||
+fn DrmSyncobjHandler::drm_syncobj_state(&mut self) -> Option<&mut DrmSyncobjState>
|
||||
```
|
||||
|
||||
`DrmTimeline` constructor now accepts OwnedFd:
|
||||
```diff
|
||||
-fn DrmTimeline::new(device: &DrmDeviceFd, fd: BorrowedFd<'_>) -> io::Result<Self>
|
||||
+fn DrmTimeline::new(device: &DrmDeviceFd, fd: OwnedFd) -> io::Result<Self>
|
||||
```
|
||||
`DrmSyncPoint::eventfd` now returns `Arc<OwnedFd>`:
|
||||
```diff
|
||||
-fn DrmSyncPoint::eventfd(&self) -> io::Result<OwnedFd>
|
||||
+fn DrmSyncPoint::eventfd(&self) -> io::Result<Arc<OwnedFd>>
|
||||
```
|
||||
`GbmFramebufferExporter` constructor now accepts `import_node`,
|
||||
it will be used to filter DMA-BUFs to only those originating from a specific device before considering them for direct scanout.
|
||||
If `import_node` is `None` direct-scanout of client-buffers won't be used.
|
||||
```diff
|
||||
-fn GbmFramebufferExporter::new(gbm: Device<A>) -> Self
|
||||
+fn GbmFramebufferExporter::new(gbm: Device<A>, import_node: Option<DrmNode>) -> Self
|
||||
```
|
||||
|
||||
### Additions
|
||||
|
||||
#### WeakDrmDeviceFd
|
||||
```rs
|
||||
/// Returns a weak reference to the underlying device
|
||||
fn DrmDeviceFd::downgrade(&self) -> WeakDrmDeviceFd;
|
||||
/// Construct an empty Weak reference, that will never upgrade successfully
|
||||
fn WeakDrmDeviceFd::new() -> Self;
|
||||
/// Try to upgrade to a strong reference
|
||||
fn WeakDrmDeviceFd::upgrade(&self) -> Option<DrmDeviceFd>;
|
||||
```
|
||||
|
||||
#### KeyboardHandle::set_modifier_state
|
||||
```rs
|
||||
/// Set the modifiers state.
|
||||
fn KeyboardHandle::set_modifier_state(&self, mods_state: ModifiersState) -> u32;
|
||||
/// Serialize modifier state back to be sent to xkb.
|
||||
fn ModifiersState::serialize_back(&self, state: &xkb::State) -> SerializedMods;
|
||||
```
|
||||
|
||||
#### XWaylandKeyboardGrab
|
||||
```rs
|
||||
/// Get the `zwp_xwayland_keyboard_grab_v1` object that created the grab
|
||||
fn XWaylandKeyboardGrab::grab(&self) -> &ZwpXwaylandKeyboardGrabV1;
|
||||
/// Grab is now cloneable
|
||||
impl Clone for XWaylandKeyboardGrab;
|
||||
```
|
||||
|
||||
#### X11Surface
|
||||
```rs
|
||||
/// Returns the hints for the underlying X11 window
|
||||
fn X11Surface::hints(&self) -> Option<x11rb::properties::WmHints>;
|
||||
/// Get the client PID associated with the X11 window.
|
||||
fn X11Surface::get_client_pid(&self) -> Result<u32, Box<dyn Error>>;
|
||||
```
|
||||
|
||||
#### DrmSyncobjState
|
||||
```rs
|
||||
/// Destroys the state and returns the `GlobalId` for compositors to disable/destroy.
|
||||
fn DrmSyncobjState::into_global(self) -> GlobalId;
|
||||
/// Sets a new `import_device` to import the syncobj fds and wait on them.
|
||||
fn DrmSyncobjState::update_device(&mut self, import_device: DrmDeviceFd);
|
||||
```
|
||||
|
||||
#### xdg-toplevel-tag-v1
|
||||
XdgToplevelTag protocol is now available in `smithay::wayland::xdg_toplevel_tag` module.
|
||||
|
||||
#### cursor-shape-v1
|
||||
CursorShape is now updated to version `2`
|
||||
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
`RenderContext::draw` callback now accepts a mutable reference
|
||||
```diff
|
||||
-fn smithay::backend::renderer::element::texture::RenderContext::draw(&mut self, f: impl FnOnce(&T))
|
||||
+fn smithay::backend::renderer::element::texture::RenderContext::draw(&mut self, f: impl FnOnce(&mut T))
|
||||
```
|
||||
|
||||
Framebuffer now requires `Texture` implementation
|
||||
```rs
|
||||
type smithay::backend::renderer::RendererSuper::Framebuffer: smithay::backend::renderer::Texture
|
||||
```
|
||||
|
||||
`Output::client_outputs` no longer returns a Vec
|
||||
```diff
|
||||
-fn smithay::output::Output::client_outputs(&self, client: &Client) -> Vec<WlOutput>;
|
||||
+fn smithay::output::Output::client_outputs(&self, client: &Client) -> impl Iterator<Item = WlOutput>;
|
||||
```
|
||||
DamageBag/DamageSnapshot damage getters got renamed
|
||||
```diff
|
||||
-fn smithay::backend::renderer::utils::DamageBag::damage(&self) -> impl Iterator<Item = impl Iterator<Item = &Rectangle>>
|
||||
+fn smithay::backend::renderer::utils::DamageBag::raw(&self) -> impl Iterator<Item = impl Iterator<Item = &Rectangle>>
|
||||
-fn smithay::backend::renderer::utils::DamageSnapshot::damage(&self) -> impl Iterator<Item = impl Iterator<Item = &Rectangle>>
|
||||
+fn smithay::backend::renderer::utils::DamageSnapshot::raw(&self) -> impl Iterator<Item = impl Iterator<Item = &Rectangle>>
|
||||
```
|
||||
RendererSurfaceState::damage now returns a DamageSnapshot
|
||||
```diff
|
||||
-fn smithay::backend::renderer::utils::RendererSurfaceState::damage(&self) -> impl core::iter::traits::iterator::Iterator<Item = impl core::iter::traits::iterator::Iterator<Item = &smithay::utils::Rectangle<i32, smithay::utils::Buffer>>>
|
||||
+fn smithay::backend::renderer::utils::RendererSurfaceState::damage(&self) -> smithay::backend::renderer::utils::DamageSnapshot<i32, smithay::utils::Buffer>
|
||||
```
|
||||
Client scale can now be fractional
|
||||
```diff
|
||||
-fn smithay::wayland::compositor::CompositorClientState::client_scale(&self) -> u32
|
||||
+fn smithay::wayland::compositor::CompositorClientState::client_scale(&self) -> f64
|
||||
-fn smithay::wayland::compositor::CompositorClientState::set_client_scale(&self, new_scale: u32)
|
||||
+fn smithay::wayland::compositor::CompositorClientState::set_client_scale(&self, new_scale: f64)
|
||||
```
|
||||
|
||||
Raw `renderer_id` got replaced with new `smithay::backend::renderer::ContextId` newtype
|
||||
```diff
|
||||
-fn smithay::backend::renderer::gles::GlesFrame::id(&self) -> usize;
|
||||
+fn smithay::backend::renderer::gles::GlesFrame::context_id(&self) -> ContextId;
|
||||
-fn smithay::backend::renderer::gles::GlesRenderer::id(&self) -> usize;
|
||||
+fn smithay::backend::renderer::gles::GlesRenderer::context_id(&self) -> ContextId;
|
||||
-fn smithay::backend::renderer::glow::GlowFrame::id(&self) -> usize;
|
||||
+fn smithay::backend::renderer::glow::GlowFrame::context_id(&self) -> ContextId;
|
||||
-fn smithay::backend::renderer::glow::GlowRenderer::id(&self) -> usize;
|
||||
+fn smithay::backend::renderer::glow::GlowRenderer::context_id(&self) -> ContextId;
|
||||
-fn smithay::backend::renderer::multigpu::MultiFrame::id(&self) -> usize;
|
||||
+fn smithay::backend::renderer::multigpu::MultiFrame::context_id(&self) -> ContextId;
|
||||
-fn smithay::backend::renderer::multigpu::MultiRenderer::id(&self) -> usize;
|
||||
+fn smithay::backend::renderer::multigpu::MultiRenderer::context_id(&self) -> ContextId;
|
||||
-fn smithay::backend::renderer::pixman::PixmanFrame::id(&self) -> usize;
|
||||
+fn smithay::backend::renderer::pixman::PixmanFrame::context_id(&self) -> ContextId;
|
||||
-fn smithay::backend::renderer::pixman::PixmanRenderer::id(&self) -> usize;
|
||||
+fn smithay::backend::renderer::pixman::PixmanRenderer::context_id(&self) -> ContextId;
|
||||
-fn smithay::backend::renderer::test::DummyFrame::id(&self) -> usize;
|
||||
+fn smithay::backend::renderer::test::DummyFrame::context_id(&self) -> ContextId;
|
||||
-fn smithay::backend::renderer::test::DummyRenderer::id(&self) -> usize;
|
||||
+fn smithay::backend::renderer::test::DummyRenderer::context_id(&self) -> ContextId;
|
||||
-fn smithay::backend::renderer::Frame::id(&self) -> usize;
|
||||
+fn smithay::backend::renderer::Frame::context_id(&self) -> ContextId;
|
||||
-fn smithay::backend::renderer::Renderer::id(&self) -> usize;
|
||||
+fn smithay::backend::renderer::Renderer::context_id(&self) -> ContextId;
|
||||
-fn smithay::backend::renderer::element::texture::TextureRenderElement::from_static_texture(id: Id, renderer_id: usize, ...) -> Self;
|
||||
+fn smithay::backend::renderer::element::texture::TextureRenderElement::from_static_texture(id: Id, context_id: ContextId, ...) -> Self;
|
||||
-fn smithay::backend::renderer::element::texture::TextureRenderElement::from_texture_with_damage(id: Id, renderer_id: usize, ...) -> Self;
|
||||
+fn smithay::backend::renderer::element::texture::TextureRenderElement::from_texture_with_damage(id: Id, context_id: ContextId, ...) -> Self;
|
||||
-fn smithay::backend::renderer::utils::RendererSurfaceState::texture<R>(&self, id: usize) -> Option<&TextureId>;
|
||||
+fn smithay::backend::renderer::utils::RendererSurfaceState::texture<R>(&self, id: &ContextId) -> Option<&TextureId>;
|
||||
```
|
||||
|
||||
`CursorShapeDeviceUserData` now has an additional generic argument
|
||||
```diff
|
||||
-struct smithay::wayland::cursor_shape::CursorShapeDeviceUserData;
|
||||
+struct smithay::wayland::cursor_shape::CursorShapeDeviceUserData<D: SeatHandler>;
|
||||
```
|
||||
|
||||
The explicit frame buffers got introduced, but for the sake of my sanity those changes are not described here, you can look at: https://github.com/Smithay/smithay/commit/df08c6f29eb6ebfa2fce6fc374590483bcbaf21a
|
||||
|
||||
### API Additions
|
||||
|
||||
It is now possible to check if the OpenGL context is shared with another.
|
||||
```rs
|
||||
fn smithay::backend::egl::context::EGLContext::is_shared();
|
||||
```
|
||||
It is now possible to check that there are no other references to the underlying GL texture.
|
||||
```rs
|
||||
fn smithay::backend::renderer::gles::GlesTexture::is_unique_reference();
|
||||
```
|
||||
|
||||
There is a new gles capability for support of fencing and exporting to EGL
|
||||
```rs
|
||||
smithay::backend::renderer::gles::Capability::ExportFence;
|
||||
```
|
||||
|
||||
There is a new BlitFrame trait for frames that support blitting contents from/to the current framebuffer to/from another.
|
||||
```rs
|
||||
trait smithay::backend::renderer::BlitFrame;
|
||||
impl BlitFrame for smithay::backend::renderer::gles::GlesFrame;
|
||||
impl BlitFrame for smithay::backend::renderer::glow::GlowFrame;
|
||||
impl BlitFrame for smithay::backend::renderer::multigpu::MultiFrame;
|
||||
```
|
||||
|
||||
It is now possible to iterate over all known tokens and their associated data
|
||||
```rs
|
||||
fn smithay::wayland::xdg_activation::XdgActivationState::tokens() -> impl Iterator<Item = (&XdgActivationToken, &XdgActivationTokenData)>;
|
||||
```
|
||||
|
||||
There are new errors for missing DRM crtc/connector/plane mapping
|
||||
```rs
|
||||
smithay::backend::drm::DrmError::{UnknownConnector, UnknownCrtc, UnknownPlane};
|
||||
```
|
||||
|
||||
Texture has a few new implementations
|
||||
```rs
|
||||
impl Texture for smithay::backend::renderer::gles::GlesTarget;
|
||||
impl Texture for smithay::backend::renderer::multigpu:MultiFramebuffer;
|
||||
impl Texture for smithay::backend::renderer::pixman::PixmanTarget;
|
||||
impl Texture for smithay::backend::renderer::test::DummyFramebuffer
|
||||
```
|
||||
|
||||
It is now possible to access WlKeyboard/WlPointer instances
|
||||
```rs
|
||||
fn smithay::input::keyboard::KeyboardHandle::client_keyboards(&self, client: &Client) -> impl Iterator<Item = WlKeyboard>;
|
||||
fn smithay::input::pointer::PointerHandle::client_pointers(&self, client: &:Client) -> impl Iterator<Item = WlPointer>;
|
||||
```
|
||||
|
||||
New APIs for X11 randr output management
|
||||
```rs
|
||||
enum smithay::xwayland::xwm::PrimaryOutputError { OutputUnknown, X11Error(x11rb::errors::ReplyError) };
|
||||
|
||||
impl From<x11rb::errors::ConnectionError> for smithay::xwayland::xwm::PrimaryOutputError;
|
||||
fn smithay::xwayland::xwm::PrimaryOutputError::from(value: x11rb::errors::ConnectionError) -> Self;
|
||||
fn smithay::xwayland::xwm::X11Wm::get_randr_primary_output(&self) -> Result<Option<String>, x11rb::errors::ReplyError>;
|
||||
fn smithay::xwayland::xwm::X11Wm::set_randr_primary_output(&mut self, output: Option<&smithay::output::Output>) -> Result<(), smithay::xwayland::xwm::PrimaryOutputError>;
|
||||
fn smithay::xwayland::xwm::XwmHandler::randr_primary_output_change(&mut self, xwm: smithay::xwayland::xwm::XwmId, output_name: Option<String>);
|
||||
```
|
||||
|
||||
It is now possible to get the DrmNode of the device the buffer was allocated on
|
||||
```rs
|
||||
fn smithay::backend::allocator::gbm::GbmBuffer::device_node(&self) -> Option<drm::node::DrmNode>;
|
||||
```
|
||||
|
||||
It is now possible to create a `GbmBuffer` from an existing `BufferObject` explicitly defining the device node
|
||||
```rs
|
||||
fn smithay::backend::allocator::gbm::GbmBuffer::from_bo_with_node(bo: gbm::buffer_object::BufferObject<()>, implicit: bool, drm_node: core::option::Option<drm::node::DrmNode>) -> Self;
|
||||
```
|
||||
|
||||
It is now possible to access the `Allocator` of this output manager
|
||||
```rs
|
||||
fn smithay::backend::drm::output::DrmOutputManager::allocator(&self) -> &Allocator;
|
||||
```
|
||||
|
||||
Is is now possible to check if EGLDevice is backed by actual device node or is it a software device.
|
||||
```rs
|
||||
fn smithay::backend::egl::EGLDevice::is_software(&self) -> bool
|
||||
```
|
||||
|
||||
This adds a way to query next deadline of a commit timing barrier.
|
||||
Allows a compositor to schedule re-evaluating commit timers without
|
||||
busy looping.
|
||||
```rs
|
||||
fn smithay::wayland::commit_timing::CommitTimerBarrierState::next_deadline(&self) -> Option<smithay::wayland::commit_timing::Timestamp>;
|
||||
```
|
||||
|
||||
Support for casting `Timestamp` back to `Time`
|
||||
This might be useful to compare the next deadline with a monotonic time
|
||||
from the presentation clock
|
||||
```rs
|
||||
impl From<smithay::wayland::commit_timing::Timestamp> for smithay::utils::Time;
|
||||
```
|
||||
|
||||
Support for creating a weak reference to a `Seat`
|
||||
```rs
|
||||
fn smithay::input::Seat::downgrade(&self) -> smithay::input::WeakSeat;
|
||||
|
||||
fn smithay::input::WeakSeat::is_alive(&self) -> bool;
|
||||
pub fn smithay::input::WeakSeat::upgrade(&self) -> Option<smithay::input::Seat>;
|
||||
```
|
||||
|
||||
## 0.5.0
|
||||
|
||||
### API Changes
|
||||
Items either removed or deprecated from the public API
|
||||
```rs
|
||||
/// Use Clone instead
|
||||
impl Copy for smithay::utils::HookId;
|
||||
/// Use `from_extremities` instead
|
||||
fn smithay::utils::Rectangle::from_extemities(topleft, bottomright) -> Self;
|
||||
```
|
||||
|
||||
Items added to the public API
|
||||
```rs
|
||||
/// Replaces deprecated `from_extemities`
|
||||
fn smithay::utils::Rectangle::from_extremities(topleft, bottomright) -> Self;
|
||||
/// Access the active text-input instance for the currently focused surface.
|
||||
fn smithay::wayland::text_input::TextInputHandle::with_active_text_input(&self, f);
|
||||
/// Just a new protocol
|
||||
mod smithay::wayland::selection::ext_data_control;
|
||||
```
|
||||
|
||||
Items changed in the public API
|
||||
```diff
|
||||
# create_external_token now accepts `XdgActivationTokenData` instead of `String`
|
||||
-fn smithay::wayland::xdg_activation::XdgActivationState::create_external_token(&mut self, app_id: impl Into<Option<String>>);
|
||||
+fn smithay::wayland::xdg_activation::XdgActivationState::create_external_token(&mut self, data: impl Into<Option<XdgActivationTokenData>>);
|
||||
```
|
||||
|
||||
### New protocols
|
||||
* Introduce ext data control protocol by @PolyMeilex in https://github.com/Smithay/smithay/pull/1577
|
||||
* Update idle notify to version 2 by @PolyMeilex in https://github.com/Smithay/smithay/pull/1618
|
||||
|
||||
### TextInput improvements
|
||||
* Revert "Fix repeated key input issue in Chrome with multiple windows" by @Drakulix in https://github.com/Smithay/smithay/pull/1647
|
||||
* text-input: fix active instance tracking by @kchibisov in https://github.com/Smithay/smithay/pull/1648
|
||||
* text-input: properly handle double buffered state by @kchibisov in https://github.com/Smithay/smithay/pull/1649
|
||||
|
||||
### Miscellaneous
|
||||
* clock: Fix current monotonic time in millis u32 overflow panic by @YaLTeR in https://github.com/Smithay/smithay/pull/1645
|
||||
* xwm: Update override-redirect flag on map request by @Ottatop in https://github.com/Smithay/smithay/pull/1656
|
||||
* utils: Rework `HookId` recycle logic by @Paraworker in https://github.com/Smithay/smithay/pull/1657
|
||||
* rename Rectangle::from_extemities to Rectangle::from_extremities by @m4rch3n1ng in https://github.com/Smithay/smithay/pull/1646
|
||||
* xdg_activation: Allow passing all data in XdgActivationState::create_external_token by @bbb651 in https://github.com/Smithay/smithay/pull/1658
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
**`wayland-server` was updated to 0.30:**
|
||||
- Most of the wayland frontend API is changed to follow the new request dispatching mechanism built around the `Dispatch` trait from `wayland-server`
|
||||
- Modules that provide handlers for Wayland globals now provide `DelegateDispatch` implementations, as well as macros to simplify the dispatching from your main state
|
||||
|
||||
#### Clients & Protocols
|
||||
|
||||
- Remove `xdg-shell-unstable-v6` backwards compatibility
|
||||
- `XdgPositionerState` moved to `XdgPopupState` and added to `XdgRequest::NewPopup`
|
||||
- `PopupSurface::send_configure` now checks the protocol version and returns an `Result`
|
||||
- `KeyboardHandle::input` filter closure now receives a `KeysymHandle` instead of a `Keysym` and returns a `FilterResult`.
|
||||
- `PointerButtonEvent::button` now returns an `Option<MouseButton>`.
|
||||
- `MouseButton` is now non-exhaustive.
|
||||
- Remove `Other` and add `Forward` and `Back` variants to `MouseButton`. Use the new `PointerButtonEvent::button_code` in place of `Other`.
|
||||
- `GrabStartData` has been renamed to `PointerGrabStartData`
|
||||
- The `slot` method on touch events no longer returns an `Option` and multi-touch capability is thus opaque to the compositor
|
||||
- `wayland::output::Output` now is created separately from it's `Global` as reflected by [`Output::new`] and the new [`Output::create_global] method.
|
||||
- `PointerHandle` no longer sends an implicit motion event when a grab is set, `time` has been replaced by an explicit `focus` parameter in [`PointerHandle::set_grab`]
|
||||
- `ToplevelSurface::send_configure`/`PopupSurface::send_configure`/`LayerSurface::send_configure` now always send a configure event regardless of changes and return
|
||||
the serial of the configure event. `send_pending_configure` can be used to only send a configure event on pending changes.
|
||||
|
||||
#### Backends
|
||||
|
||||
- Rename `WinitInputBacked` to `WinitEventLoop`.
|
||||
- Rename `WinitInputError` to `WinitError`;
|
||||
- `WinitInputBackend` no longer implements `InputBackend`. Input events are now received from the `WinitEvent::Input` variant.
|
||||
- All winit backend internal event types now use `WinitInput` as the backend type.
|
||||
- `WinitEventLoop::dispatch_new_events` is now used to receive some `WinitEvent`s.
|
||||
- Added `TabletToolType::Unknown` as an option for tablet events
|
||||
- `render_texture` was removed from `Frame`, use `render_texture_at` or `render_texture_from_to` instead or use `Gles2Renderer::render_texture` as a direct replacement.
|
||||
- Remove `InputBackend::dispatch_new_events`, turning `InputBackend` into a definition of backend event types. Future input backends should be a `calloop::EventSource`.
|
||||
- Remove `InputBackend::EventError` associated type as it is unneeded since `dispatch_new_events` was removed.
|
||||
- `Swapchain` does not have a generic Userdata-parameter anymore, but utilizes `UserDataMap` instead
|
||||
- `GbmBufferedSurface::next_buffer` now additionally returns the age of the buffer
|
||||
- `Present` was merged into the `X11Surface`
|
||||
- `X11Surface::buffer` now additionally returns the age of the buffer
|
||||
- `X11Surface` now has an explicit `submit` function
|
||||
- `X11Surface` is now multi-window capable.
|
||||
- `Renderer::clear` now expects a second argument to optionally only clear parts of the buffer/surface
|
||||
- `Transform::transform_size` now takes a `Size` instead of two `u32`
|
||||
- `Gles2Renderer` now automatically flips the `render` result to account for OpenGLs coordinate system
|
||||
- `Frame::clear`, `Frame::render_texture_at` and `Frame::render_texture_from_to` now have an additional damage argument
|
||||
- `EGLNativeSurface` implementations overriding `swap_buffers` now receive and additional `damage` attribute to be used with `eglSwapBuffersWithDamageEXT` if desired
|
||||
- `EGLSurface::swap_buffers` now accepts an optional `damage` parameter
|
||||
- `WinitGraphicsBackend` does no longer provide a `render`-method and exposes its `Renderer` directly instead including new functions `bind` and `submit` to handle swapping buffers.
|
||||
- `ImportShm` was renamed to `ImportMem`
|
||||
- `ImportMem` and `ImportDma` were split and do now have accompanying traits `ImportMemWl` and `ImportDmaWl` to import wayland buffers.
|
||||
- Added `EGLSurface::get_size`
|
||||
- `EGLDisplay::get_extensions` was renamed to `extensions` and now returns a `&[String]`.
|
||||
- Added gesture input events, which are supported with the libinput backend.
|
||||
|
||||
### Additions
|
||||
|
||||
#### Clients & Protocols
|
||||
|
||||
- `xdg_activation_v1` support
|
||||
- `wlr-layer-shell-unstable-v1` support
|
||||
- Added public api constants for the roles of `wl_shell_surface`, `zxdg_toplevel` and `xdg_toplevel`. See the
|
||||
`shell::legacy` and `shell::xdg` modules for these constants.
|
||||
- Whether a surface is toplevel equivalent can be determined with the new function `shell::is_toplevel_equivalent`.
|
||||
- Setting the parent of a toplevel surface is now possible with the `xdg::ToplevelSurface::set_parent` function.
|
||||
- Add support for the zxdg-foreign-v2 protocol.
|
||||
- Support for `xdg_wm_base` protocol version 3
|
||||
- Added the option to initialize the dmabuf global with a client filter
|
||||
- `wayland::output::Output` now has user data attached to it and more functions to query its properties
|
||||
- Added a `KeyboardGrab` similar to the existing `PointerGrab`
|
||||
- `wayland::output::Output` now has a `current_scale` method to quickly retrieve its set scale.
|
||||
- `wayland::shell::wlr_layer::KeyboardInteractivity` now implements `PartialEq` and `Eq`.
|
||||
- Added `TouchHandle` for Wayland client touch support (see `Seat::get_touch`)
|
||||
- `wayland::output::Scale` was introduced to handle fractional scale values better
|
||||
- Support for `wl_output` global version 4
|
||||
- Support for `wl_seat` global version 7
|
||||
- Support for `wl_compositor` global version 5
|
||||
- Support for the `wp_viewporter` protocol
|
||||
- Support for the `zwp_input_method_v2` protocol
|
||||
- Support for the `zwp_text_input_v3` protocol
|
||||
|
||||
#### Backends
|
||||
|
||||
- New `x11` backend to run the compositor as an X11 client. Enabled through the `backend_x11` feature.
|
||||
- `x11rb` event source integration used in anvil's XWayland implementation is now part of smithay at `utils::x11rb`. Enabled through the `x11rb_event_source` feature.
|
||||
- `KeyState`, `MouseButton`, `ButtonState` and `Axis` in `backend::input` now derive `Hash`.
|
||||
- New `DrmNode` type in drm backend. This is primarily for use a backend which needs to run as client inside another session.
|
||||
- The button code for a `PointerButtonEvent` may now be obtained using `PointerButtonEvent::button_code`.
|
||||
- `Renderer` now allows texture filtering methods to be set.
|
||||
- `backend::renderer` has a new `utils`-module that can take care of client buffer management for you.
|
||||
- `EGLSurface::buffer_age` can be used to query the surface buffer age.
|
||||
- `GbmBufferedSurface::reset_buffers` can now be used to reset underlying buffers.
|
||||
- Added new `Offscreen` trait to create offscreen surfaces for `Renderer`s
|
||||
- Added functions to `ImportMem` to upload bitmaps from memory
|
||||
- Added `ExportDma` trait to export framebuffers and textures into dmabufs
|
||||
- Added `ExportMem` trait to copy framebuffers and textures into memory
|
||||
- Added `multigpu`-module to the renderer, which makes handling multi-gpu setups easier!
|
||||
- Added `backend::renderer::utils::import_surface_tree` to be able to import buffers before rendering
|
||||
- Added `EGLContext::display` to allow getting the underlying display of some context.
|
||||
- Make `EGLContext::dmabuf_render_formats` and `EGLContext::dmabuf_texture_formats` also accessible from `EGLDisplay`.
|
||||
|
||||
#### Desktop
|
||||
|
||||
- New `desktop` module to handle window placement, tracks popups, layer surface and various rendering helpers including automatic damage-tracking! (+so much more)
|
||||
|
||||
#### Utils
|
||||
|
||||
- `Rectangle` can now also be converted from f64 to i32 variants
|
||||
- `Rectangle::contains_rect` can be used to check if a rectangle is contained within another
|
||||
- `Coordinate` is now part of the public api, so it can be used for coordinate agnositic functions outside of the utils module or even out-of-tree
|
||||
|
||||
### Bugfixes
|
||||
|
||||
#### Clients & Protocols
|
||||
|
||||
- `Multicache::has()` now correctly does what is expected of it
|
||||
- `xdg_shell` had an issue where it was possible that configured state gets overwritten before it was acked/committed.
|
||||
- `wl_keyboard` rewind the `keymap` file before passing it to the client
|
||||
- `wl_shm` properly validates parameters when creating a `wl_buffer`.
|
||||
- `ServerDnDGrab` and `DnDGrab` now correctly send data device `leave` event on button release
|
||||
- Client are now allowed to reassign the same role to a surface
|
||||
- `xdg_output` now applies the output transforms to the reported logical size
|
||||
|
||||
#### Backends
|
||||
|
||||
- EGLBufferReader now checks if buffers are alive before using them.
|
||||
- LibSeat no longer panics on seat disable event.
|
||||
- X11 backend will report an error when trying to present a dmabuf fails.
|
||||
|
||||
### Anvil
|
||||
|
||||
- Anvil now implements the x11 backend in smithay. Run by passing `--x11` into the arguments when launching.
|
||||
- Passing `ANVIL_MUTEX_LOG` in environment variables now uses the slower `Mutex` logging drain.
|
||||
- Only toplevel surfaces now get implicit keyboard focus
|
||||
- Fix popup drawing for fullscreen windows
|
||||
|
||||
## version 0.3.0 (2021-07-25)
|
||||
|
||||
Large parts of Smithay were changed with numerous API changes. It is thus recommended to
|
||||
approach version 0.3 as if it was a new crate altogether compared to 0.2.
|
||||
|
||||
The most notable changes are:
|
||||
|
||||
- Deep refactor of the graphics backends around a workflows centered on allocating graphics buffers,
|
||||
and a Gles2-based renderer abstraction is provided.
|
||||
- Support for DRM atomic modesetting as well as client-provided DMABUF
|
||||
- Most backends are now `calloop` event sources generating events. The recommended organization for
|
||||
your smithay-based compositor is thus to centralize most of your logic on a global state struct,
|
||||
and delegate event handling to it via the shared data mechanism of `calloop`. Most of the callbacks
|
||||
you provide to Smithay are given mutable access to this shared data.
|
||||
- The `wayland::compositor` handling logic now automatically handles state tracking and delayed commit
|
||||
for wayland surfaces.
|
||||
|
||||
Many thanks to the new contributors to Smithay, who contributed the following:
|
||||
|
||||
- Support for [`libseat`](https://sr.ht/~kennylevinsen/seatd/) as a session backend, by
|
||||
@PolyMeilex
|
||||
- Support for graphics tablets via the `tablet` protocol extension, by @PolyMeilex
|
||||
- Support for running Smithay on `aarch64` architectures, by @cmeissl
|
||||
- A rework of the `xdg-shell` handlers to better fit the protocol logic and correctly track configure
|
||||
events, by @cmeissl
|
||||
- Basic Xwayland support, by @psychon
|
||||
|
||||
## version 0.2.0 (2019-01-03)
|
||||
|
||||
### General
|
||||
|
||||
- **[Breaking]** Upgrade to wayland-rs 0.21
|
||||
- **[Breaking]** Moving the public dependencies to a `reexports` module
|
||||
- Migrate the codebase to Rust 2018
|
||||
|
||||
### Backends
|
||||
|
||||
- **[Breaking]** WinitBackend: Upgrade to winit 0.18
|
||||
- **[Breaking]** Global refactor of the DRM & Session backends
|
||||
- **[Breaking]** Restructuration of the backends around the `calloop` event-loop
|
||||
|
||||
### Clients & Protocol
|
||||
|
||||
- Basic XWayland support
|
||||
- Data device & Drag'n'Drop support
|
||||
- Custom client pointers support
|
||||
|
||||
## version 0.1.0 (2017-10-01)
|
||||
|
||||
### Protocol handling
|
||||
|
||||
- Low-level handling routines for several wayland globals:
|
||||
- `wayland::shm` handles `wl_shm`
|
||||
- `wayland::compositor` handles `wl_compositor` and `wl_subcompositor`
|
||||
- `wayland::shell` handles `wl_shell` and `xdg_shell`
|
||||
- `wayland::seat` handles `wl_seat`
|
||||
- `wayland::output` handles `wl_output`
|
||||
|
||||
### Backend
|
||||
|
||||
- Winit backend (EGL context & input)
|
||||
- DRM backend
|
||||
- libinput backend
|
||||
- glium integration
|
||||
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
# Contributing
|
||||
|
||||
Smithay is open to contributions from anyone. Here are a few tips to get started if you want to participate.
|
||||
|
||||
## Coordination
|
||||
|
||||
Most discussion about features and their implementations takes place on github.
|
||||
If you have questions, suggestions, ideas, you can open an issue to discuss it, or add your message in an already existing issue
|
||||
if it fits its scope.
|
||||
|
||||
If you want a more realtime discussion I (@vberger) have a Matrix room dedicated to Smithay and
|
||||
my other wayland crates: [#smithay:matrix.org](https://matrix.to/#/#smithay:matrix.org). If you don't want to
|
||||
use matrix, this room is also bridged to libera.chat IRC on #smithay.
|
||||
|
||||
## Scope
|
||||
|
||||
Smithay attempts to be as generic and un-opinionated as possible. As such, if you have an idea of a feature that would be useful
|
||||
for your compositor project and would like it to be integrated in Smithay, please consider whether it is in its scope:
|
||||
|
||||
- If this is a very generic feature that probably many different projects would find useful, it can be integrated in Smithay
|
||||
- If it is a rather specific feature, but can be framed as a special case of a more general feature, this general feature is
|
||||
likely worth adding to Smithay
|
||||
- If this feature is really specific to your use-case, it is out of scope for Smithay
|
||||
|
||||
## Structure
|
||||
|
||||
Smithay aims to be a modular hierarchical library:
|
||||
|
||||
- Functionalities should be split into independent modules as much as possible
|
||||
- There can be dependencies in functionalities
|
||||
- Even if most people would directly use a high-level functionality, the lower level abstractions it is built on should
|
||||
still be exposed independently if possible
|
||||
|
||||
The goal is for Smithay to be a "use what you want" library, and features that are not used should have no impact on the
|
||||
application built with Smithay.
|
||||
Vendored
+150
@@ -0,0 +1,150 @@
|
||||
[package]
|
||||
name = "smithay"
|
||||
version = "0.7.0"
|
||||
authors = ["Victor Berger <[email protected]>", "Drakulix (Victoria Brekenfeld)"]
|
||||
license = "MIT"
|
||||
description = "Smithay is a library for writing wayland compositors."
|
||||
repository = "https://github.com/Smithay/smithay"
|
||||
edition = "2024"
|
||||
readme = "README.md"
|
||||
homepage = "https://smithay.github.io/"
|
||||
keywords = ["wayland", "compositor", "graphics", "server"]
|
||||
categories = ["gui"]
|
||||
rust-version = "1.87"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["test_all_features", "libseat/docs_rs"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = [
|
||||
"smithay-drm-extras",
|
||||
"smallvil",
|
||||
"anvil",
|
||||
"wlcs_anvil",
|
||||
"test_clients",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
appendlist = "1.4"
|
||||
ash = { version = "0.38.0", optional = true }
|
||||
bitflags = "2.2.1"
|
||||
calloop = "0.14.5"
|
||||
cursor-icon = "1.2.0"
|
||||
downcast-rs = "1.2.0"
|
||||
drm-fourcc = "^2.2.0"
|
||||
drm = { version = "0.14.0", optional = true }
|
||||
drm-ffi = { version = "0.9.1", optional = true }
|
||||
drm-sys = { version = "0.8.1", optional = true }
|
||||
errno = "0.3.5"
|
||||
gbm = { version = "0.18.0", optional = true, default-features = false, features = ["drm-support"] }
|
||||
glow = { version = "0.16", optional = true }
|
||||
input = { version = "0.10.0", default-features = false, features=["libinput_1_19"], optional = true }
|
||||
indexmap = "2.11"
|
||||
libc = "0.2.103"
|
||||
libseat = { version = "0.2.3", optional = true, default-features = false }
|
||||
libloading = { version="0.8.0", optional = true }
|
||||
rustix = { version = "1.0.7", features = ["event", "fs", "mm", "net", "pipe", "process", "shm", "time", "param"] }
|
||||
rand = "0.10.0"
|
||||
scopeguard = { version = "1.1.0", optional = true }
|
||||
siphasher = { version = "1.0.3", optional = true }
|
||||
tracing = "0.1.37"
|
||||
tempfile = { version = "3.0", optional = true }
|
||||
thiserror = "2.0.12"
|
||||
udev = { version = "0.9.0", optional = true }
|
||||
# Require never `wayland-client`/`wayland-cursor` than winit uses to fix `-Z minimal-versions`
|
||||
# due to issue in older version.
|
||||
wayland-client = { version = "0.31.14", optional = true }
|
||||
wayland-cursor = { version = "0.31.14", optional = true }
|
||||
wayland-egl = { version = "0.32.11", optional = true }
|
||||
wayland-protocols = { version = "0.32.13", features = ["unstable", "staging", "server"], optional = true }
|
||||
wayland-protocols-wlr = { version = "0.3.12", features = ["server"], optional = true }
|
||||
wayland-protocols-misc = { version = "0.3.12", features = ["server"], optional = true }
|
||||
wayland-server = { version = "0.31.13", optional = true }
|
||||
wayland-sys = { version = "0.31.11", optional = true }
|
||||
wayland-backend = { version = "0.3.15", optional = true }
|
||||
winit = { version = "=0.31.0-beta.3", default-features = false, features = ["wayland", "wayland-dlopen", "x11"], optional = true }
|
||||
x11rb = { version = "0.13.0", optional = true, features = ["res", "sync"]}
|
||||
xkbcommon = { version = "0.9.0", features = ["wayland"]}
|
||||
encoding_rs = { version = "0.8.33", optional = true }
|
||||
profiling = "1.0.13"
|
||||
smallvec = "1.11"
|
||||
pixman = { version = "0.2.1", features = ["drm-fourcc", "sync"], optional = true }
|
||||
aliasable = { version = "0.1.3", optional = true }
|
||||
portable-atomic = { version = "1", features = ["float"] }
|
||||
tracy-client = { version = "0.18.4", default-features = false, optional = true }
|
||||
reis = { version = "0.7.0", features = ["calloop"] }
|
||||
glam = { version = "0.33.2", default-features = false, features = ["std"] }
|
||||
|
||||
[dev-dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
criterion = { version = "0.5" }
|
||||
image = "0.25"
|
||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||
|
||||
[build-dependencies]
|
||||
gl_generator = { version = "0.14", optional = true }
|
||||
pkg-config = { version = "0.3.17", optional = true }
|
||||
cc = { version = "1.0.79", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["backend_drm", "backend_gbm", "backend_libinput", "backend_udev", "backend_session_libseat", "backend_x11", "backend_winit", "desktop", "renderer_gl", "renderer_pixman", "renderer_multi", "xwayland", "wayland_frontend", "backend_vulkan", "backend_libei"]
|
||||
backend_winit = ["winit", "backend_egl", "wayland-client", "wayland-cursor", "wayland-egl", "renderer_gl"]
|
||||
backend_x11 = ["x11rb", "x11rb/dri3", "x11rb/xfixes", "x11rb/xinput", "x11rb/present", "x11rb_event_source", "backend_gbm", "backend_drm", "backend_egl"]
|
||||
backend_drm = ["drm", "drm-ffi", "drm-sys"]
|
||||
backend_gbm = ["gbm", "cc", "pkg-config", "backend_drm"]
|
||||
backend_gbm_has_fd_for_plane = []
|
||||
backend_gbm_has_create_with_modifiers2 = []
|
||||
backend_egl = ["gl_generator", "libloading"]
|
||||
backend_libinput = ["input"]
|
||||
backend_session = []
|
||||
backend_udev = ["udev", "input/udev"]
|
||||
backend_vulkan = ["ash", "scopeguard"]
|
||||
backend_session_libseat = ["backend_session", "libseat"]
|
||||
backend_libei = ["wayland_frontend"]
|
||||
desktop = []
|
||||
renderer_gl = ["gl_generator", "backend_egl"]
|
||||
renderer_glow = ["renderer_gl", "glow"]
|
||||
renderer_multi = ["backend_drm", "aliasable"]
|
||||
renderer_pixman = ["pixman"]
|
||||
renderer_test = []
|
||||
tracy_gpu_profiling = ["renderer_gl", "tracy-client"]
|
||||
use_system_lib = ["wayland_frontend", "wayland-backend/server_system", "wayland-sys", "gbm?/import-wayland"]
|
||||
use_bindgen = ["drm-ffi/use_bindgen", "gbm/use_bindgen", "input/use_bindgen"]
|
||||
wayland_frontend = ["wayland-server", "wayland-protocols", "wayland-protocols-wlr", "wayland-protocols-misc", "tempfile", "siphasher"]
|
||||
x11rb_event_source = ["x11rb"]
|
||||
xwayland = ["encoding_rs", "wayland_frontend", "x11rb/composite", "x11rb/xfixes", "x11rb/randr", "x11rb_event_source", "scopeguard"]
|
||||
test_all_features = ["default", "use_system_lib", "renderer_glow", "renderer_test"]
|
||||
|
||||
[[example]]
|
||||
name = "minimal"
|
||||
required-features = ["backend_winit", "renderer_gl", "wayland_frontend"]
|
||||
|
||||
[[example]]
|
||||
name = "seat"
|
||||
required-features = ["wayland_frontend"]
|
||||
|
||||
[[example]]
|
||||
name = "compositor"
|
||||
required-features = ["wayland_frontend"]
|
||||
|
||||
[[example]]
|
||||
name = "vulkan"
|
||||
required-features = ["backend_vulkan"]
|
||||
|
||||
[[example]]
|
||||
name = "buffer_test"
|
||||
required-features = ["backend_drm", "backend_gbm", "backend_egl", "backend_vulkan", "renderer_gl"]
|
||||
|
||||
[[bench]]
|
||||
name = "benchmark"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "geometry"
|
||||
harness = false
|
||||
|
||||
[profile.release-with-debug]
|
||||
inherits = "release"
|
||||
debug = true
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2026 Smithay maintainers
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
# Getting started
|
||||
|
||||
So you want to write a new Wayland compositor with Smithay, but don't know where to start?
|
||||
|
||||
## Wayland itself
|
||||
|
||||
Wayland is just a set of XML files describing the protocol by which a compositor and client communicate.
|
||||
To work on a compositor, you need to establish some understanding of the state mandated by the protocol,
|
||||
most importantly the lifetime of objects defined by it.
|
||||
|
||||
We recommend the official [wayland-docs](https://wayland.freedesktop.org/docs/html/) to get started, in particular [Chapter 4](https://wayland.freedesktop.org/docs/html/ch04.html),
|
||||
which talks about the protocol.
|
||||
|
||||
Additionally it can help to familiarize yourself with the viewpoint of clients. The [wayland-book](https://wayland-book.com/) is a good resource here.
|
||||
|
||||
There is also an (unfortunately stalled - at the time of writing this document -) effort to write a [smithay book](https://smithay.github.io/book/) to
|
||||
cover similar topics, but focused on [wayland-rs](https://github.com/Smithay/wayland-rs) (the underlying Rust-based Wayland implementation) rather than
|
||||
[libwayland](https://gitlab.freedesktop.org/wayland/wayland/). At least the client side of this is a worthwhile read as well.
|
||||
|
||||
## Rust
|
||||
|
||||
Smithay makes heavy use of Rust's features, which is likely why you selected the framework in the first place.
|
||||
As such we assume you have *some* experience with the language already.
|
||||
|
||||
If not the [awesome-rust resource section](https://github.com/rust-unofficial/awesome-rust?tab=readme-ov-file#resources) has you covered for various learning resources.
|
||||
|
||||
## Smithay
|
||||
|
||||
Unfortunately, the Smithay book doesn't cover the server-side (yet).
|
||||
|
||||
Which leaves you with the following resources to understand how Smithay works:
|
||||
|
||||
- A large portion of Smithay is build around [wayland-rs](https://github.com/smithay/wayland-rs) `Dispatch` machinery. It is thus highly recommended to read and understand
|
||||
the documentation of the [wayland-server](https://smithay.github.io/wayland-rs/wayland_server/) crate.
|
||||
- Smithay itself has [comprehensive documentation](https://smithay.github.io/smithay/) including for its [various](https://smithay.github.io/smithay/smithay/backend/index.html) [modules](https://smithay.github.io/smithay/smithay/wayland/index.html)!
|
||||
- [smallvil](https://github.com/Smithay/smithay/tree/master/smallvil) is the smallest (somewhat) usable compositor using Smithay and intended as a learning resource and potential starting point.
|
||||
- [anvil](https://github.com/Smithay/smithay/tree/master/anvil) is smithay's testing ground and thus a much more complete compositor to study. Its code strives to fill a gap between real-world examples and easy-to-understand/maintain code paths, however it is lacking in overall quality.
|
||||
- Many real world compositors use Smithay, please refer to our [README](https://github.com/Smithay/smithay/blob/master/README.md#other-compositors-that-use-smithay) for a list of them to study.
|
||||
|
||||
### Side-note: Which version of Smithay should I use?
|
||||
|
||||
Smithay itself does not follow a regular release schedule, as such many compositors choose to depend on a given git commit and frequent updates. We strive to do better in this department and try to publish semi-frequently to crates.io.
|
||||
|
||||
However the framework is still evolving a lot, thus we cannot really commit to any sort of ABI stability. Thus you currently need to rely on git-based updates to not only get new features, but also bugfixes. As a result released Smithay versions don't really have a benefit other than coming from crates.io.
|
||||
|
||||
If you are still unsure, check our [Changelog](https://github.com/Smithay/smithay/blob/master/CHANGELOG.md) and see how many unreleased features
|
||||
are currently waiting for a new Smithay release. The longer the list, the more we recommend to start off with the current git commit to make updating
|
||||
to the next released version as easy as possible.
|
||||
|
||||
## Further help
|
||||
|
||||
If you still feel stuck trying to work your way through these resources or have questions not covered here, please feel free to reach out in our chatroom.
|
||||
You can find us on Matrix: [#smithay:matrix.org](https://matrix.to/#/#smithay:matrix.org). If you don't want to use Matrix, this room is also bridged to libera.chat IRC on #smithay.
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Victor Berger and Victoria Brekenfeld
|
||||
|
||||
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.
|
||||
Vendored
+89
@@ -0,0 +1,89 @@
|
||||
<p align="center">
|
||||
<img width="240" src="https://github.com/Smithay/smithay/assets/20758186/7a84ab10-e229-4823-bad8-9c647546407b" alt="Smithay"/>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://crates.io/crates/smithay"><img src="https://img.shields.io/crates/v/smithay.svg" alt="Crates.io"></a>
|
||||
<a href="https://docs.rs/smithay"><img src="https://docs.rs/smithay/badge.svg" alt="docs.rs"></a>
|
||||
<a href="https://github.com/Smithay/smithay/actions"><img src="https://github.com/Smithay/smithay/workflows/Continuous%20Integration/badge.svg" alt="CI"></a>
|
||||
<a href="https://matrix.to/#/#smithay:matrix.org"><img src="https://img.shields.io/badge/Matrix-%23smithay%3Amatrix.org-blue.svg" alt="Matrix"></a>
|
||||
<a href="https://libera.chat/"><img src="https://img.shields.io/badge/IRC-%23Smithay-blue.svg" alt="IRC"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<strong>A smithy for rusty Wayland compositors</strong>
|
||||
</p>
|
||||
|
||||
## Goals
|
||||
|
||||
Smithay aims to provide building blocks to create Wayland compositors in Rust. While not
|
||||
being a full-blown compositor, it'll provide objects and interfaces implementing common
|
||||
functionalities that pretty much any compositor will need, in a generic fashion.
|
||||
|
||||
It supports the [core Wayland protocols](https://gitlab.freedesktop.org/wayland/wayland), the official [protocol extensions](https://gitlab.freedesktop.org/wayland/wayland-protocols), and *some* external extensions, such as those made by and for [wlroots](https://gitlab.freedesktop.org/wlroots/wlr-protocols) and [KDE](https://invent.kde.org/libraries/plasma-wayland-protocols).
|
||||
<!-- https://github.com/Smithay/smithay/pull/779#discussion_r993640470 https://github.com/Smithay/smithay/issues/778 -->
|
||||
|
||||
Also:
|
||||
|
||||
- **Documented:** Smithay strives to maintain clear and detailed documentation of its API and
|
||||
functionalities. Compiled documentation is available on [docs.rs](https://docs.rs/smithay) for released
|
||||
versions, and [here](https://smithay.github.io/smithay) for the master branch.
|
||||
- **Safety:** Smithay aims to be memory-safe and reliable, thanks to Rust.
|
||||
- **Modularity:** Smithay is not a framework and does not impose constraints. You are never required
|
||||
to use components you don’t need.
|
||||
- **High-level:** You should not have to worry about gory low-level stuff (but Smithay won't
|
||||
stop you if you really want to dive into it).
|
||||
|
||||
## Getting started
|
||||
|
||||
If you want to learn how to build a compositor with Smithay, the best place to start is the [getting started](https://github.com/Smithay/smithay/blob/master/GETTING_STARTED.md) guide.
|
||||
|
||||
## Anvil
|
||||
|
||||
Smithay as a compositor library has its own sample compositor: `anvil`.
|
||||
|
||||
To get information about it and how you can run it, check out the [`anvil` README](https://github.com/Smithay/smithay/blob/master/anvil/README.md)
|
||||
|
||||
## Other compositors that use Smithay
|
||||
|
||||
- [Cosmic](https://github.com/pop-os/cosmic-epoch): Next-generation Cosmic desktop environment
|
||||
- [Catacomb](https://github.com/catacombing/catacomb): A Wayland Mobile Compositor
|
||||
- [emskin](https://github.com/emskin/emskin): A nested Wayland compositor for embedding any app inside Emacs
|
||||
- [MagmaWM](https://github.com/MagmaWM/MagmaWM): A versatile and customizable Wayland Compositor
|
||||
- [Niri](https://github.com/YaLTeR/niri): A scrollable-tiling Wayland compositor
|
||||
- [Strata](https://github.com/StrataWM/strata): A cutting-edge, robust and sleek Wayland compositor
|
||||
- [Pinnacle](https://github.com/Ottatop/pinnacle): A WIP Wayland compositor, inspired by AwesomeWM
|
||||
- [Sudbury](https://gitlab.freedesktop.org/bwidawsk/sudbury): Compositor designed for ChromeOS
|
||||
- [wprs](https://github.com/wayland-transpositor/wprs): Like [xpra](https://en.wikipedia.org/wiki/Xpra), but for Wayland, and written in
|
||||
Rust.
|
||||
- [Local Desktop](https://github.com/localdesktop/localdesktop): An Android app for running GUI Linux via PRoot and Wayland.
|
||||
- [Otto](https://github.com/nongio/otto): A gesture-driven stacking compositor.
|
||||
- [DriftWM](https://github.com/malbiruk/driftwm): A trackpad-first infinite canvas Wayland compositor.
|
||||
- [Denial](https://github.com/denialwm/denial): A stacking, tiling, and scrolling-tiling Wayland compositor with a Flutter-based shell.
|
||||
|
||||
## System Dependencies
|
||||
|
||||
(This list can depend on the features you enable)
|
||||
|
||||
- `libwayland`
|
||||
- `libxkbcommon`
|
||||
- `libudev`
|
||||
- `libinput`
|
||||
- `libgbm`
|
||||
- [`libseat`](https://git.sr.ht/~kennylevinsen/seatd)
|
||||
- `xwayland`
|
||||
- `libEGL`
|
||||
- `libpixman`
|
||||
- `libdisplay-info`
|
||||
|
||||
## Contact us
|
||||
|
||||
If you have questions or want to discuss the project with us, our main chatroom is on Matrix: [`#smithay:matrix.org`](https://matrix.to/#/#smithay:matrix.org).
|
||||
|
||||
## Contributing
|
||||
|
||||
General notes on contributing to Smithay can be found [here](https://github.com/Smithay/smithay/blob/master/CONTRIBUTING.md).
|
||||
|
||||
Please note that to submit code to Smithay, you must agree to our [Developer Certificate of Origin](https://github.com/Smithay/smithay/blob/master/DCO.md).
|
||||
|
||||
If you are used to using generative AI, please ensure you read our [Policy](https://github.com/Smithay/smithay/blob/master/AI.md) before starting.
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
# Vendored Smithay
|
||||
|
||||
- Repository: https://github.com/Smithay/smithay
|
||||
- Revision: `8a270232f1bfc416c781273c98a3f52a812a0bba`
|
||||
- Source: complete tracked snapshot from `git archive --format=tar 8a270232f1bfc416c781273c98a3f52a812a0bba`. No `.git`, `target`, or untracked Cargo cache markers are included.
|
||||
- Original archive: 4894720 bytes; SHA-256 `81a42cfd4c3626c9bad8051023970151dcea0fe7bedd02fc09105db360a847c2`.
|
||||
- Original snapshot: 345 regular files; 4559959 content bytes. File contents, symlink targets and Git executable bits were independently verified against the archive. Archive ownership and write permissions follow the extracting user's normal umask.
|
||||
- Licenses are preserved in `LICENSE.txt`, `smithay-drm-extras/LICENSE`, and individual source headers.
|
||||
|
||||
## Local patch
|
||||
|
||||
`patches/shm-texture-cache-format.patch` changes the GLES SHM surface-cache compatibility check to match dimensions, GL internal format, and alpha interpretation. Previously an equal-sized ARGB/XRGB change reused stale `has_alpha`, selecting the wrong shader variant. A newly allocated texture also forces a complete upload. Context identity is already part of the cache key. Offset and stride describe each upload's source and are read anew; SHM cache entries always have normal orientation and non-external storage, so those properties do not add compatibility criteria.
|
||||
|
||||
At most 16 debug records per process describe format incompatibilities. Records contain only numeric format/dimension fields, alpha booleans, and a fixed event message. They contain no pixel data, application text, titles, or credentials.
|
||||
|
||||
The independent private-Wayland GPU regression is in Ferese's `compositor/src/state/partial_shm_gpu_tests.rs`. It compares the cached renderer with a new, unshared EGL context and full upload/full repaint on every frame, in both ARGB-first and XRGB-first directions. The ordinary partial-damage regression shares the fixture.
|
||||
|
||||
Vendored
+65
@@ -0,0 +1,65 @@
|
||||
[package]
|
||||
authors = ["Victor Berger <[email protected]>", "Drakulix (Victoria Brekenfeld)"]
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
name = "anvil"
|
||||
publish = false
|
||||
version = "0.0.1"
|
||||
|
||||
[dependencies]
|
||||
bitflags = "2.2.1"
|
||||
fps_ticker = {version = "1.0.0", optional = true}
|
||||
image = {version = "0.25.1", default-features = false, optional = true, features = ["png"]}
|
||||
rand = "0.8"
|
||||
tracing = { version = "0.1.37", features = ["max_level_trace", "release_max_level_debug"] }
|
||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||
thiserror = "1"
|
||||
xcursor = {version = "0.3.3", optional = true}
|
||||
xkbcommon = "0.8.0"
|
||||
renderdoc = {version = "0.11.0", optional = true}
|
||||
smithay-drm-extras = {path = "../smithay-drm-extras", optional = true}
|
||||
puffin_http = { version = "0.13", optional = true }
|
||||
profiling = { version = "1.0" }
|
||||
|
||||
[dependencies.smithay]
|
||||
default-features = false
|
||||
features = ["desktop", "wayland_frontend"]
|
||||
path = ".."
|
||||
|
||||
[dependencies.x11rb]
|
||||
default-features = false
|
||||
features = ["composite"]
|
||||
optional = true
|
||||
version = "0.13.0"
|
||||
|
||||
[build-dependencies]
|
||||
gl_generator = "0.14"
|
||||
|
||||
[features]
|
||||
debug = ["fps_ticker", "image/png", "renderdoc"]
|
||||
default = ["egl", "winit", "x11", "udev", "xwayland", "libei"]
|
||||
egl = ["smithay/use_system_lib", "smithay/backend_egl"]
|
||||
test_all_features = ["default", "debug"]
|
||||
udev = [
|
||||
"smithay-drm-extras",
|
||||
"smithay/backend_libinput",
|
||||
"smithay/backend_udev",
|
||||
"smithay/backend_drm",
|
||||
"smithay/backend_gbm",
|
||||
"smithay/backend_vulkan",
|
||||
"smithay/backend_egl",
|
||||
"smithay/backend_session_libseat",
|
||||
"image",
|
||||
"smithay/renderer_gl",
|
||||
"smithay/renderer_pixman",
|
||||
"smithay/renderer_multi",
|
||||
"xcursor",
|
||||
]
|
||||
winit = ["smithay/backend_winit", "smithay/backend_drm"]
|
||||
x11 = ["smithay/backend_x11", "x11rb", "smithay/renderer_gl", "smithay/backend_vulkan"]
|
||||
xwayland = ["smithay/xwayland", "x11rb", "smithay/x11rb_event_source", "xcursor"]
|
||||
libei = ["smithay/backend_libei", "udev"]
|
||||
profile-with-puffin = ["profiling/profile-with-puffin", "puffin_http"]
|
||||
profile-with-tracy = ["profiling/profile-with-tracy"]
|
||||
profile-with-tracy-mem = ["profile-with-tracy"]
|
||||
renderer_sync = []
|
||||
Vendored
+54
@@ -0,0 +1,54 @@
|
||||
# Anvil
|
||||
|
||||
A compositor used as a testing ground for new smithay features.
|
||||
For a simple example compositor consider reading [smallvil](https://github.com/Smithay/smithay/tree/master/smallvil)
|
||||
|
||||
## Dependencies
|
||||
|
||||
You'll need to install the following dependencies (note, that those package
|
||||
names may vary depending on your OS and linux distribution):
|
||||
|
||||
- `libwayland`
|
||||
- `libxkbcommon`
|
||||
|
||||
#### These are needed for the "Udev/DRM backend"
|
||||
|
||||
- `libudev`
|
||||
- `libinput`
|
||||
- `libgbm`
|
||||
- [`libseat`](https://git.sr.ht/~kennylevinsen/seatd)
|
||||
|
||||
If you want to enable X11 support (to run X11 applications within anvil),
|
||||
then you'll need to install the following packages as well:
|
||||
- `xwayland`
|
||||
|
||||
## Build and run
|
||||
|
||||
You can run it with cargo after having cloned this repository:
|
||||
|
||||
```
|
||||
cd anvil;
|
||||
|
||||
cargo run -- --{backend}
|
||||
```
|
||||
|
||||
The currently available backends are:
|
||||
|
||||
- `--x11`: start anvil as an X11 client. This allows you to run the compositor inside an X11 session or any compositor supporting XWayland. Should be preferred over the winit backend where possible.
|
||||
- `--winit`: start anvil as a [Winit](https://github.com/tomaka/winit) application. This allows you to run it
|
||||
inside of an other X11 or Wayland session.
|
||||
- `--tty-udev`: start anvil in a tty with udev support. This is the "traditional" launch of a Wayland
|
||||
compositor. Note that this requires you to start anvil as root if your system does not have logind
|
||||
available.
|
||||
|
||||
### Supported Environment Variables
|
||||
|
||||
| Variable | Example | Backends |
|
||||
|-------------------------------|-----------------|-----------|
|
||||
| ANVIL_DRM_DEVICE | /dev/dri/card0 | tty-udev |
|
||||
| ANVIL_DISABLE_10BIT | any | tty-udev |
|
||||
| ANVIL_DISABLE_DIRECT_SCANOUT | any | tty-udev |
|
||||
| ANVIL_GLES_DISABLE_INSTANCING | any | tty-udev |
|
||||
| ANVIL_NO_VULKAN | 1,true,yes,y | x11 |
|
||||
| SMITHAY_USE_LEGACY | 1,true,yes,y | tty-udev |
|
||||
| SMITHAY_VK_VERSION | 1.3 | |
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
Vendored
+96
@@ -0,0 +1,96 @@
|
||||
use std::{io::Read, time::Duration};
|
||||
|
||||
use tracing::warn;
|
||||
use xcursor::{
|
||||
CursorTheme,
|
||||
parser::{Image, parse_xcursor},
|
||||
};
|
||||
|
||||
static FALLBACK_CURSOR_DATA: &[u8] = include_bytes!("../resources/cursor.rgba");
|
||||
|
||||
pub struct Cursor {
|
||||
icons: Vec<Image>,
|
||||
size: u32,
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
pub fn load() -> Cursor {
|
||||
let name = std::env::var("XCURSOR_THEME")
|
||||
.ok()
|
||||
.unwrap_or_else(|| "default".into());
|
||||
let size = std::env::var("XCURSOR_SIZE")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(24);
|
||||
|
||||
let theme = CursorTheme::load(&name);
|
||||
let icons = load_icon(&theme)
|
||||
.map_err(|err| warn!("Unable to load xcursor: {}, using fallback cursor", err))
|
||||
.unwrap_or_else(|_| {
|
||||
vec![Image {
|
||||
size: 32,
|
||||
width: 64,
|
||||
height: 64,
|
||||
xhot: 1,
|
||||
yhot: 1,
|
||||
delay: 1,
|
||||
pixels_rgba: Vec::from(FALLBACK_CURSOR_DATA),
|
||||
pixels_argb: vec![], //unused
|
||||
}]
|
||||
});
|
||||
|
||||
Cursor { icons, size }
|
||||
}
|
||||
|
||||
pub fn get_image(&self, scale: u32, time: Duration) -> Image {
|
||||
let size = self.size * scale;
|
||||
frame(time.as_millis() as u32, size, &self.icons)
|
||||
}
|
||||
}
|
||||
|
||||
fn nearest_images(size: u32, images: &[Image]) -> impl Iterator<Item = &Image> {
|
||||
// Follow the nominal size of the cursor to choose the nearest
|
||||
let nearest_image = images
|
||||
.iter()
|
||||
.min_by_key(|image| (size as i32 - image.size as i32).abs())
|
||||
.unwrap();
|
||||
|
||||
images
|
||||
.iter()
|
||||
.filter(move |image| image.width == nearest_image.width && image.height == nearest_image.height)
|
||||
}
|
||||
|
||||
fn frame(mut millis: u32, size: u32, images: &[Image]) -> Image {
|
||||
let total = nearest_images(size, images).fold(0, |acc, image| acc + image.delay);
|
||||
if total == 0 {
|
||||
return nearest_images(size, images).next().unwrap().clone();
|
||||
}
|
||||
millis %= total;
|
||||
|
||||
for img in nearest_images(size, images) {
|
||||
if millis < img.delay {
|
||||
return img.clone();
|
||||
}
|
||||
millis -= img.delay;
|
||||
}
|
||||
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
enum Error {
|
||||
#[error("Theme has no default cursor")]
|
||||
NoDefaultCursor,
|
||||
#[error("Error opening xcursor file: {0}")]
|
||||
File(#[from] std::io::Error),
|
||||
#[error("Failed to parse XCursor file")]
|
||||
Parse,
|
||||
}
|
||||
|
||||
fn load_icon(theme: &CursorTheme) -> Result<Vec<Image>, Error> {
|
||||
let icon_path = theme.load_icon("default").ok_or(Error::NoDefaultCursor)?;
|
||||
let mut cursor_file = std::fs::File::open(icon_path)?;
|
||||
let mut cursor_data = Vec::new();
|
||||
cursor_file.read_to_end(&mut cursor_data)?;
|
||||
parse_xcursor(&cursor_data).ok_or(Error::Parse)
|
||||
}
|
||||
Vendored
+259
@@ -0,0 +1,259 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use smithay::{
|
||||
backend::renderer::{
|
||||
Color32F, ImportAll, ImportMem, Renderer, Texture,
|
||||
element::{
|
||||
AsRenderElements, Kind,
|
||||
memory::{MemoryRenderBuffer, MemoryRenderBufferRenderElement},
|
||||
surface::WaylandSurfaceRenderElement,
|
||||
},
|
||||
},
|
||||
input::pointer::CursorImageStatus,
|
||||
render_elements,
|
||||
utils::{Physical, Point, Scale},
|
||||
};
|
||||
#[cfg(feature = "debug")]
|
||||
use smithay::{
|
||||
backend::renderer::{
|
||||
Frame,
|
||||
element::{Element, Id, RenderElement},
|
||||
utils::CommitCounter,
|
||||
},
|
||||
utils::{Buffer, Logical, Rectangle, Size, Transform, user_data::UserDataMap},
|
||||
};
|
||||
|
||||
pub static CLEAR_COLOR: Color32F = Color32F::new(0.8, 0.8, 0.9, 1.0);
|
||||
pub static CLEAR_COLOR_FULLSCREEN: Color32F = Color32F::new(0.0, 0.0, 0.0, 0.0);
|
||||
|
||||
pub struct PointerElement {
|
||||
buffer: Option<MemoryRenderBuffer>,
|
||||
status: CursorImageStatus,
|
||||
}
|
||||
|
||||
impl Default for PointerElement {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buffer: Default::default(),
|
||||
status: CursorImageStatus::default_named(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerElement {
|
||||
pub fn set_status(&mut self, status: CursorImageStatus) {
|
||||
self.status = status;
|
||||
}
|
||||
|
||||
pub fn set_buffer(&mut self, buffer: MemoryRenderBuffer) {
|
||||
self.buffer = Some(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
render_elements! {
|
||||
pub PointerRenderElement<R> where R: ImportAll + ImportMem;
|
||||
Surface=WaylandSurfaceRenderElement<R>,
|
||||
Memory=MemoryRenderBufferRenderElement<R>,
|
||||
}
|
||||
|
||||
impl<R: Renderer> std::fmt::Debug for PointerRenderElement<R> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Surface(arg0) => f.debug_tuple("Surface").field(arg0).finish(),
|
||||
Self::Memory(arg0) => f.debug_tuple("Memory").field(arg0).finish(),
|
||||
Self::_GenericCatcher(arg0) => f.debug_tuple("_GenericCatcher").field(arg0).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Texture + Clone + Send + 'static, R> AsRenderElements<R> for PointerElement
|
||||
where
|
||||
R: Renderer<TextureId = T> + ImportAll + ImportMem,
|
||||
{
|
||||
type RenderElement = PointerRenderElement<R>;
|
||||
fn render_elements<E>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
location: Point<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
alpha: f32,
|
||||
) -> Vec<E>
|
||||
where
|
||||
E: From<PointerRenderElement<R>>,
|
||||
{
|
||||
match &self.status {
|
||||
CursorImageStatus::Hidden => vec![],
|
||||
// Always render `Default` for a named shape.
|
||||
CursorImageStatus::Named(_) => {
|
||||
if let Some(buffer) = self.buffer.as_ref() {
|
||||
vec![
|
||||
PointerRenderElement::<R>::from(
|
||||
MemoryRenderBufferRenderElement::from_buffer(
|
||||
renderer,
|
||||
location.to_f64(),
|
||||
buffer,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Kind::Cursor,
|
||||
)
|
||||
.expect("Lost system pointer buffer"),
|
||||
)
|
||||
.into(),
|
||||
]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
CursorImageStatus::Surface(surface) => {
|
||||
let elements: Vec<PointerRenderElement<R>> =
|
||||
smithay::backend::renderer::element::surface::render_elements_from_surface_tree(
|
||||
renderer,
|
||||
surface,
|
||||
location,
|
||||
scale,
|
||||
alpha,
|
||||
Kind::Cursor,
|
||||
);
|
||||
elements.into_iter().map(E::from).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
pub static FPS_NUMBERS_PNG: &[u8] = include_bytes!("../resources/numbers.png");
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FpsElement<T: Texture> {
|
||||
id: Id,
|
||||
value: u32,
|
||||
texture: T,
|
||||
commit_counter: CommitCounter,
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
impl<T: Texture> FpsElement<T> {
|
||||
pub fn new(texture: T) -> Self {
|
||||
FpsElement {
|
||||
id: Id::new(),
|
||||
texture,
|
||||
value: 0,
|
||||
commit_counter: CommitCounter::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_fps(&mut self, fps: u32) {
|
||||
if self.value != fps {
|
||||
self.value = fps;
|
||||
self.commit_counter.increment();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
impl<T> Element for FpsElement<T>
|
||||
where
|
||||
T: Texture + 'static,
|
||||
{
|
||||
fn id(&self) -> &Id {
|
||||
&self.id
|
||||
}
|
||||
|
||||
fn location(&self, _scale: Scale<f64>) -> Point<i32, Physical> {
|
||||
(0, 0).into()
|
||||
}
|
||||
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
let digits = if self.value < 10 {
|
||||
1
|
||||
} else if self.value < 100 {
|
||||
2
|
||||
} else {
|
||||
3
|
||||
};
|
||||
Rectangle::from_size((24 * digits, 35).into()).to_f64()
|
||||
}
|
||||
|
||||
fn geometry(&self, scale: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
let digits = if self.value < 10 {
|
||||
1
|
||||
} else if self.value < 100 {
|
||||
2
|
||||
} else {
|
||||
3
|
||||
};
|
||||
Rectangle::from_size((24 * digits, 35).into()).to_physical_precise_round(scale)
|
||||
}
|
||||
|
||||
fn current_commit(&self) -> CommitCounter {
|
||||
self.commit_counter
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
impl<R> RenderElement<R> for FpsElement<R::TextureId>
|
||||
where
|
||||
R: Renderer + ImportAll,
|
||||
R::TextureId: 'static,
|
||||
{
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut R::Frame<'_, '_>,
|
||||
_src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
_opaque_regions: &[Rectangle<i32, Physical>],
|
||||
_cache: Option<&UserDataMap>,
|
||||
) -> Result<(), R::Error> {
|
||||
// FIXME: respect the src for cropping
|
||||
let scale = dst.size.to_f64() / self.src().size;
|
||||
let value_str = std::cmp::min(self.value, 999).to_string();
|
||||
let mut offset: Point<f64, Physical> = Point::from((0.0, 0.0));
|
||||
for digit in value_str.chars().map(|d| d.to_digit(10).unwrap()) {
|
||||
let digit_location = dst.loc.to_f64() + offset;
|
||||
let digit_size = Size::<i32, Logical>::from((22, 35)).to_f64().to_physical(scale);
|
||||
let dst = Rectangle::new(
|
||||
digit_location.to_i32_round(),
|
||||
((digit_size.to_point() + digit_location).to_i32_round() - digit_location.to_i32_round())
|
||||
.to_size(),
|
||||
);
|
||||
let damage = damage
|
||||
.iter()
|
||||
.cloned()
|
||||
.flat_map(|x| x.intersection(dst))
|
||||
.map(|mut x| {
|
||||
x.loc -= dst.loc;
|
||||
x
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let texture_src: Rectangle<i32, Buffer> = match digit {
|
||||
9 => Rectangle::from_size((22, 35).into()),
|
||||
6 => Rectangle::new((22, 0).into(), (22, 35).into()),
|
||||
3 => Rectangle::new((44, 0).into(), (22, 35).into()),
|
||||
1 => Rectangle::new((66, 0).into(), (22, 35).into()),
|
||||
8 => Rectangle::new((0, 35).into(), (22, 35).into()),
|
||||
0 => Rectangle::new((22, 35).into(), (22, 35).into()),
|
||||
2 => Rectangle::new((44, 35).into(), (22, 35).into()),
|
||||
7 => Rectangle::new((0, 70).into(), (22, 35).into()),
|
||||
4 => Rectangle::new((22, 70).into(), (22, 35).into()),
|
||||
5 => Rectangle::new((44, 70).into(), (22, 35).into()),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
frame.render_texture_from_to(
|
||||
&self.texture,
|
||||
texture_src.to_f64(),
|
||||
dst,
|
||||
&damage,
|
||||
&[],
|
||||
Transform::Normal,
|
||||
1.0,
|
||||
)?;
|
||||
offset += Point::from((24.0, 0.0)).to_physical(scale);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Vendored
+712
@@ -0,0 +1,712 @@
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
#[cfg(feature = "xwayland")]
|
||||
use smithay::xwayland::X11Surface;
|
||||
#[cfg(feature = "xwayland")]
|
||||
use smithay::xwayland::xwm::XwmOfferData;
|
||||
pub use smithay::{
|
||||
backend::input::{InputTime, KeyState},
|
||||
desktop::{LayerSurface, PopupKind},
|
||||
input::{
|
||||
Seat,
|
||||
keyboard::{KeyboardTarget, KeysymHandle, ModifiersState},
|
||||
pointer::{AxisFrame, ButtonEvent, MotionEvent, PointerTarget, RelativeMotionEvent},
|
||||
},
|
||||
reexports::wayland_server::{Resource, backend::ObjectId, protocol::wl_surface::WlSurface},
|
||||
utils::{IsAlive, Serial},
|
||||
wayland::seat::WaylandFocus,
|
||||
};
|
||||
use smithay::{
|
||||
desktop::{Window, WindowSurface},
|
||||
input::{
|
||||
dnd::{DndFocus, OfferData, Source},
|
||||
pointer::{
|
||||
GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent,
|
||||
GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
|
||||
},
|
||||
tablet::tool::TabletToolTarget,
|
||||
touch::{FrameMarker, TouchTarget},
|
||||
},
|
||||
reexports::wayland_server::DisplayHandle,
|
||||
utils::{Logical, Point},
|
||||
wayland::selection::data_device::WlOfferData,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
shell::{SSD, WindowElement},
|
||||
state::{AnvilState, Backend},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum KeyboardFocusTarget {
|
||||
Window(Window),
|
||||
LayerSurface(LayerSurface),
|
||||
Popup(PopupKind),
|
||||
}
|
||||
|
||||
impl IsAlive for KeyboardFocusTarget {
|
||||
#[inline]
|
||||
fn alive(&self) -> bool {
|
||||
match self {
|
||||
KeyboardFocusTarget::Window(w) => w.alive(),
|
||||
KeyboardFocusTarget::LayerSurface(l) => l.alive(),
|
||||
KeyboardFocusTarget::Popup(p) => p.alive(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum PointerFocusTarget {
|
||||
WlSurface(WlSurface),
|
||||
#[cfg(feature = "xwayland")]
|
||||
X11Surface(X11Surface),
|
||||
SSD(SSD),
|
||||
}
|
||||
|
||||
impl IsAlive for PointerFocusTarget {
|
||||
#[inline]
|
||||
fn alive(&self) -> bool {
|
||||
match self {
|
||||
PointerFocusTarget::WlSurface(w) => w.alive(),
|
||||
#[cfg(feature = "xwayland")]
|
||||
PointerFocusTarget::X11Surface(w) => w.alive(),
|
||||
PointerFocusTarget::SSD(x) => x.alive(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PointerFocusTarget> for WlSurface {
|
||||
#[inline]
|
||||
fn from(target: PointerFocusTarget) -> Self {
|
||||
target.wl_surface().unwrap().into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyboardFocusTarget {
|
||||
fn inner_keyboard_target<BackendData: Backend>(&self) -> &dyn KeyboardTarget<AnvilState<BackendData>> {
|
||||
match self {
|
||||
Self::Window(w) => match w.underlying_surface() {
|
||||
WindowSurface::Wayland(w) => w.wl_surface(),
|
||||
#[cfg(feature = "xwayland")]
|
||||
WindowSurface::X11(s) => s,
|
||||
},
|
||||
Self::LayerSurface(l) => l.wl_surface(),
|
||||
Self::Popup(p) => p.wl_surface(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerFocusTarget {
|
||||
fn inner_pointer_target<BackendData: Backend>(&self) -> &dyn PointerTarget<AnvilState<BackendData>> {
|
||||
match self {
|
||||
Self::WlSurface(w) => w,
|
||||
#[cfg(feature = "xwayland")]
|
||||
Self::X11Surface(w) => w,
|
||||
Self::SSD(w) => w,
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_touch_target<BackendData: Backend>(&self) -> &dyn TouchTarget<AnvilState<BackendData>> {
|
||||
match self {
|
||||
Self::WlSurface(w) => w,
|
||||
#[cfg(feature = "xwayland")]
|
||||
Self::X11Surface(w) => w,
|
||||
Self::SSD(w) => w,
|
||||
}
|
||||
}
|
||||
|
||||
fn inner_tablet_tool_target<BackendData: Backend>(
|
||||
&self,
|
||||
) -> &dyn TabletToolTarget<AnvilState<BackendData>> {
|
||||
match self {
|
||||
Self::WlSurface(w) => w,
|
||||
#[cfg(feature = "xwayland")]
|
||||
Self::X11Surface(w) => w,
|
||||
Self::SSD(w) => w,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> PointerTarget<AnvilState<BackendData>> for PointerFocusTarget {
|
||||
fn enter(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
self.inner_pointer_target().enter(seat, data, event)
|
||||
}
|
||||
fn motion(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
self.inner_pointer_target().motion(seat, data, event)
|
||||
}
|
||||
fn relative_motion(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
self.inner_pointer_target().relative_motion(seat, data, event)
|
||||
}
|
||||
fn button(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
self.inner_pointer_target().button(seat, data, event)
|
||||
}
|
||||
fn axis(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
frame: AxisFrame,
|
||||
) {
|
||||
self.inner_pointer_target().axis(seat, data, frame)
|
||||
}
|
||||
fn frame(&self, seat: &Seat<AnvilState<BackendData>>, data: &mut AnvilState<BackendData>) {
|
||||
self.inner_pointer_target().frame(seat, data)
|
||||
}
|
||||
fn leave(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
serial: Serial,
|
||||
time: InputTime,
|
||||
) {
|
||||
self.inner_pointer_target().leave(seat, data, serial, time)
|
||||
}
|
||||
fn gesture_swipe_begin(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
self.inner_pointer_target().gesture_swipe_begin(seat, data, event)
|
||||
}
|
||||
fn gesture_swipe_update(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
self.inner_pointer_target()
|
||||
.gesture_swipe_update(seat, data, event)
|
||||
}
|
||||
fn gesture_swipe_end(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
self.inner_pointer_target().gesture_swipe_end(seat, data, event)
|
||||
}
|
||||
fn gesture_pinch_begin(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
self.inner_pointer_target().gesture_pinch_begin(seat, data, event)
|
||||
}
|
||||
fn gesture_pinch_update(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
self.inner_pointer_target()
|
||||
.gesture_pinch_update(seat, data, event)
|
||||
}
|
||||
fn gesture_pinch_end(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
self.inner_pointer_target().gesture_pinch_end(seat, data, event)
|
||||
}
|
||||
fn gesture_hold_begin(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
self.inner_pointer_target().gesture_hold_begin(seat, data, event)
|
||||
}
|
||||
fn gesture_hold_end(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
self.inner_pointer_target().gesture_hold_end(seat, data, event)
|
||||
}
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> KeyboardTarget<AnvilState<BackendData>> for KeyboardFocusTarget {
|
||||
fn enter(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
keys: Vec<KeysymHandle<'_>>,
|
||||
serial: Serial,
|
||||
) {
|
||||
self.inner_keyboard_target().enter(seat, data, keys, serial)
|
||||
}
|
||||
fn leave(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
serial: Serial,
|
||||
) {
|
||||
self.inner_keyboard_target().leave(seat, data, serial)
|
||||
}
|
||||
fn key(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
key: KeysymHandle<'_>,
|
||||
state: KeyState,
|
||||
serial: Serial,
|
||||
time: InputTime,
|
||||
) {
|
||||
self.inner_keyboard_target()
|
||||
.key(seat, data, key, state, serial, time)
|
||||
}
|
||||
fn modifiers(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
modifiers: ModifiersState,
|
||||
serial: Serial,
|
||||
) {
|
||||
self.inner_keyboard_target()
|
||||
.modifiers(seat, data, modifiers, serial)
|
||||
}
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> TouchTarget<AnvilState<BackendData>> for PointerFocusTarget {
|
||||
fn down(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &smithay::input::touch::DownEvent,
|
||||
) {
|
||||
self.inner_touch_target().down(seat, data, event)
|
||||
}
|
||||
|
||||
fn up(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &smithay::input::touch::UpEvent,
|
||||
) {
|
||||
self.inner_touch_target().up(seat, data, event)
|
||||
}
|
||||
|
||||
fn motion(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &smithay::input::touch::MotionEvent,
|
||||
) {
|
||||
self.inner_touch_target().motion(seat, data, event)
|
||||
}
|
||||
|
||||
fn frame(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
marker: FrameMarker,
|
||||
) {
|
||||
self.inner_touch_target().frame(seat, data, marker)
|
||||
}
|
||||
|
||||
fn cancel(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
marker: FrameMarker,
|
||||
) {
|
||||
self.inner_touch_target().cancel(seat, data, marker)
|
||||
}
|
||||
|
||||
fn shape(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &smithay::input::touch::ShapeEvent,
|
||||
) {
|
||||
self.inner_touch_target().shape(seat, data, event)
|
||||
}
|
||||
|
||||
fn orientation(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &smithay::input::touch::OrientationEvent,
|
||||
) {
|
||||
self.inner_touch_target().orientation(seat, data, event)
|
||||
}
|
||||
|
||||
fn last_frame(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
) -> Option<FrameMarker> {
|
||||
self.inner_touch_target().last_frame(seat, data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> TabletToolTarget<AnvilState<BackendData>> for PointerFocusTarget {
|
||||
fn proximity_in(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
tool_descriptor: &smithay::backend::input::TabletToolDescriptor,
|
||||
tablet: &smithay::input::tablet::Tablet,
|
||||
serial: Serial,
|
||||
) {
|
||||
self.inner_tablet_tool_target()
|
||||
.proximity_in(seat, data, tool_descriptor, tablet, serial);
|
||||
}
|
||||
|
||||
fn proximity_out(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
tool_descriptor: &smithay::backend::input::TabletToolDescriptor,
|
||||
) {
|
||||
self.inner_tablet_tool_target()
|
||||
.proximity_out(seat, data, tool_descriptor);
|
||||
}
|
||||
|
||||
fn down(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
tool_descriptor: &smithay::backend::input::TabletToolDescriptor,
|
||||
event: &smithay::input::tablet::tool::DownEvent,
|
||||
) {
|
||||
self.inner_tablet_tool_target()
|
||||
.down(seat, data, tool_descriptor, event);
|
||||
}
|
||||
|
||||
fn up(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
tool_descriptor: &smithay::backend::input::TabletToolDescriptor,
|
||||
event: &smithay::input::tablet::tool::UpEvent,
|
||||
) {
|
||||
self.inner_tablet_tool_target()
|
||||
.up(seat, data, tool_descriptor, event);
|
||||
}
|
||||
|
||||
fn motion(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
tool_descriptor: &smithay::backend::input::TabletToolDescriptor,
|
||||
event: &smithay::input::tablet::tool::MotionEvent,
|
||||
) {
|
||||
self.inner_tablet_tool_target()
|
||||
.motion(seat, data, tool_descriptor, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
tool_descriptor: &smithay::backend::input::TabletToolDescriptor,
|
||||
event: &smithay::input::tablet::tool::ButtonEvent,
|
||||
) {
|
||||
self.inner_tablet_tool_target()
|
||||
.button(seat, data, tool_descriptor, event);
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
tool_descriptor: &smithay::backend::input::TabletToolDescriptor,
|
||||
frame: smithay::input::tablet::tool::AxisFrame,
|
||||
) {
|
||||
self.inner_tablet_tool_target()
|
||||
.axis(seat, data, tool_descriptor, frame);
|
||||
}
|
||||
|
||||
fn frame(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
tool_descriptor: &smithay::backend::input::TabletToolDescriptor,
|
||||
time: InputTime,
|
||||
) {
|
||||
self.inner_tablet_tool_target()
|
||||
.frame(seat, data, tool_descriptor, time);
|
||||
}
|
||||
}
|
||||
|
||||
impl WaylandFocus for PointerFocusTarget {
|
||||
#[inline]
|
||||
fn wl_surface(&self) -> Option<Cow<'_, WlSurface>> {
|
||||
match self {
|
||||
PointerFocusTarget::WlSurface(w) => w.wl_surface(),
|
||||
#[cfg(feature = "xwayland")]
|
||||
PointerFocusTarget::X11Surface(w) => w.wl_surface().map(Cow::Owned),
|
||||
PointerFocusTarget::SSD(_) => None,
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
fn same_client_as(&self, object_id: &ObjectId) -> bool {
|
||||
match self {
|
||||
PointerFocusTarget::WlSurface(w) => w.same_client_as(object_id),
|
||||
#[cfg(feature = "xwayland")]
|
||||
PointerFocusTarget::X11Surface(w) => w.same_client_as(object_id),
|
||||
PointerFocusTarget::SSD(w) => w
|
||||
.wl_surface()
|
||||
.map(|surface| surface.same_client_as(object_id))
|
||||
.unwrap_or(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WaylandFocus for KeyboardFocusTarget {
|
||||
#[inline]
|
||||
fn wl_surface(&self) -> Option<Cow<'_, WlSurface>> {
|
||||
match self {
|
||||
KeyboardFocusTarget::Window(w) => w.wl_surface(),
|
||||
KeyboardFocusTarget::LayerSurface(l) => Some(Cow::Borrowed(l.wl_surface())),
|
||||
KeyboardFocusTarget::Popup(p) => Some(Cow::Borrowed(p.wl_surface())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum AnvilOfferData<S: Source> {
|
||||
Wayland(WlOfferData<S>),
|
||||
#[cfg(feature = "xwayland")]
|
||||
X11(XwmOfferData<S>),
|
||||
}
|
||||
|
||||
impl<S: Source> OfferData for AnvilOfferData<S> {
|
||||
fn disable(&self) {
|
||||
match self {
|
||||
AnvilOfferData::Wayland(data) => data.disable(),
|
||||
#[cfg(feature = "xwayland")]
|
||||
AnvilOfferData::X11(data) => data.disable(),
|
||||
}
|
||||
}
|
||||
|
||||
fn drop(&self) {
|
||||
match self {
|
||||
AnvilOfferData::Wayland(data) => data.drop(),
|
||||
#[cfg(feature = "xwayland")]
|
||||
AnvilOfferData::X11(data) => data.drop(),
|
||||
}
|
||||
}
|
||||
|
||||
fn validated(&self) -> bool {
|
||||
match self {
|
||||
AnvilOfferData::Wayland(data) => data.validated(),
|
||||
#[cfg(feature = "xwayland")]
|
||||
AnvilOfferData::X11(data) => data.validated(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
impl<BackendData: Backend> DndFocus<AnvilState<BackendData>> for PointerFocusTarget {
|
||||
type OfferData<S>
|
||||
= AnvilOfferData<S>
|
||||
where
|
||||
S: Source;
|
||||
|
||||
fn enter<S: Source>(
|
||||
&self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
dh: &DisplayHandle,
|
||||
source: Arc<S>,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
location: Point<f64, Logical>,
|
||||
serial: &Serial,
|
||||
) -> Option<AnvilOfferData<S>> {
|
||||
match self {
|
||||
PointerFocusTarget::WlSurface(surface) => {
|
||||
DndFocus::enter(surface, data, dh, source, seat, location, serial)
|
||||
.map(AnvilOfferData::Wayland)
|
||||
}
|
||||
#[cfg(feature = "xwayland")]
|
||||
PointerFocusTarget::X11Surface(surface) => {
|
||||
DndFocus::enter(surface, data, dh, source, seat, location, serial).map(AnvilOfferData::X11)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn motion<S: Source>(
|
||||
&self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
offer: Option<&mut AnvilOfferData<S>>,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
location: Point<f64, Logical>,
|
||||
time: InputTime,
|
||||
) {
|
||||
match self {
|
||||
PointerFocusTarget::WlSurface(surface) => {
|
||||
let offer = match offer {
|
||||
Some(AnvilOfferData::Wayland(offer)) => Some(offer),
|
||||
None => None,
|
||||
_ => return,
|
||||
};
|
||||
DndFocus::motion(surface, data, offer, seat, location, time)
|
||||
}
|
||||
#[cfg(feature = "xwayland")]
|
||||
PointerFocusTarget::X11Surface(surface) => {
|
||||
let offer = match offer {
|
||||
Some(AnvilOfferData::X11(offer)) => Some(offer),
|
||||
None => None,
|
||||
_ => return,
|
||||
};
|
||||
DndFocus::motion(surface, data, offer, seat, location, time)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn leave<S: Source>(
|
||||
&self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
offer: Option<&mut AnvilOfferData<S>>,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
) {
|
||||
match self {
|
||||
PointerFocusTarget::WlSurface(surface) => {
|
||||
let offer = match offer {
|
||||
Some(AnvilOfferData::Wayland(offer)) => Some(offer),
|
||||
None => None,
|
||||
_ => return,
|
||||
};
|
||||
DndFocus::leave(surface, data, offer, seat)
|
||||
}
|
||||
#[cfg(feature = "xwayland")]
|
||||
PointerFocusTarget::X11Surface(surface) => {
|
||||
let offer = match offer {
|
||||
Some(AnvilOfferData::X11(offer)) => Some(offer),
|
||||
None => None,
|
||||
_ => return,
|
||||
};
|
||||
DndFocus::leave(surface, data, offer, seat)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn drop<S: Source>(
|
||||
&self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
offer: Option<&mut AnvilOfferData<S>>,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
) {
|
||||
match self {
|
||||
PointerFocusTarget::WlSurface(surface) => {
|
||||
let offer = match offer {
|
||||
Some(AnvilOfferData::Wayland(offer)) => Some(offer),
|
||||
None => None,
|
||||
_ => return,
|
||||
};
|
||||
DndFocus::drop(surface, data, offer, seat)
|
||||
}
|
||||
#[cfg(feature = "xwayland")]
|
||||
PointerFocusTarget::X11Surface(surface) => {
|
||||
let offer = match offer {
|
||||
Some(AnvilOfferData::X11(offer)) => Some(offer),
|
||||
None => None,
|
||||
_ => return,
|
||||
};
|
||||
DndFocus::drop(surface, data, offer, seat)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WlSurface> for PointerFocusTarget {
|
||||
#[inline]
|
||||
fn from(value: WlSurface) -> Self {
|
||||
PointerFocusTarget::WlSurface(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&WlSurface> for PointerFocusTarget {
|
||||
#[inline]
|
||||
fn from(value: &WlSurface) -> Self {
|
||||
PointerFocusTarget::from(value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PopupKind> for PointerFocusTarget {
|
||||
#[inline]
|
||||
fn from(value: PopupKind) -> Self {
|
||||
PointerFocusTarget::from(value.wl_surface())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "xwayland")]
|
||||
impl From<X11Surface> for PointerFocusTarget {
|
||||
#[inline]
|
||||
fn from(value: X11Surface) -> Self {
|
||||
PointerFocusTarget::X11Surface(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "xwayland")]
|
||||
impl From<&X11Surface> for PointerFocusTarget {
|
||||
#[inline]
|
||||
fn from(value: &X11Surface) -> Self {
|
||||
PointerFocusTarget::from(value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WindowElement> for KeyboardFocusTarget {
|
||||
#[inline]
|
||||
fn from(w: WindowElement) -> Self {
|
||||
KeyboardFocusTarget::Window(w.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LayerSurface> for KeyboardFocusTarget {
|
||||
#[inline]
|
||||
fn from(l: LayerSurface) -> Self {
|
||||
KeyboardFocusTarget::LayerSurface(l)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PopupKind> for KeyboardFocusTarget {
|
||||
#[inline]
|
||||
fn from(p: PopupKind) -> Self {
|
||||
KeyboardFocusTarget::Popup(p)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeyboardFocusTarget> for PointerFocusTarget {
|
||||
#[inline]
|
||||
fn from(value: KeyboardFocusTarget) -> Self {
|
||||
match value {
|
||||
KeyboardFocusTarget::Window(w) => match w.underlying_surface() {
|
||||
WindowSurface::Wayland(w) => PointerFocusTarget::from(w.wl_surface()),
|
||||
#[cfg(feature = "xwayland")]
|
||||
WindowSurface::X11(s) => PointerFocusTarget::from(s),
|
||||
},
|
||||
KeyboardFocusTarget::LayerSurface(surface) => PointerFocusTarget::from(surface.wl_surface()),
|
||||
KeyboardFocusTarget::Popup(popup) => PointerFocusTarget::from(popup.wl_surface()),
|
||||
}
|
||||
}
|
||||
}
|
||||
+1457
File diff suppressed because it is too large.
Load diff
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
#![warn(rust_2018_idioms)]
|
||||
#![allow(clippy::collapsible_match)]
|
||||
// If no backend is enabled, a large portion of the codebase is unused.
|
||||
// So silence this useless warning for the CI.
|
||||
#![cfg_attr(
|
||||
not(any(feature = "winit", feature = "x11", feature = "udev")),
|
||||
allow(dead_code, unused_imports)
|
||||
)]
|
||||
|
||||
#[cfg(any(feature = "udev", feature = "xwayland"))]
|
||||
pub mod cursor;
|
||||
pub mod drawing;
|
||||
pub mod focus;
|
||||
pub mod input_handler;
|
||||
#[cfg(feature = "libei")]
|
||||
pub mod libei;
|
||||
pub mod render;
|
||||
pub mod shell;
|
||||
pub mod state;
|
||||
#[cfg(feature = "udev")]
|
||||
pub mod udev;
|
||||
#[cfg(feature = "winit")]
|
||||
pub mod winit;
|
||||
#[cfg(feature = "x11")]
|
||||
pub mod x11;
|
||||
|
||||
pub use state::{AnvilState, ClientState};
|
||||
Vendored
+50
@@ -0,0 +1,50 @@
|
||||
use smithay::{
|
||||
backend::libei::{EiInput, EiInputEvent},
|
||||
input::keyboard::XkbConfig,
|
||||
reexports::{
|
||||
calloop,
|
||||
reis::{calloop::EisListenerSource, eis},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{state::AnvilState, udev::UdevData};
|
||||
|
||||
pub fn listen_eis(handle: &calloop::LoopHandle<'static, AnvilState<UdevData>>) {
|
||||
let listener = match eis::Listener::bind_auto() {
|
||||
Ok(listener) => listener,
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to bind EI listener socket: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
unsafe { std::env::set_var("LIBEI_SOCKET", listener.path()) };
|
||||
|
||||
let listener_source = EisListenerSource::new(listener);
|
||||
let handle_clone = handle.clone();
|
||||
handle
|
||||
.insert_source(listener_source, move |context, _, _| {
|
||||
let source = EiInput::new(context);
|
||||
handle_clone
|
||||
.insert_source(source, |event, connection, data| match event {
|
||||
EiInputEvent::Connected => {
|
||||
let seat = connection.add_seat("default");
|
||||
let _ = seat.add_keyboard("virtual keyboard", XkbConfig::default());
|
||||
seat.add_pointer("virtual pointer");
|
||||
seat.add_pointer_absolute("virtual absolute pointer", &[]);
|
||||
seat.add_touch("virtual touch", &[]);
|
||||
}
|
||||
EiInputEvent::Disconnected => {}
|
||||
EiInputEvent::Event(event) => {
|
||||
let dh = data.display_handle.clone();
|
||||
data.process_input_event(&dh, event);
|
||||
}
|
||||
EiInputEvent::TextKeysym { .. } | EiInputEvent::TextUtf8 { .. } => {
|
||||
// Anvil doesn't add a text device to the libei seat
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
Ok(calloop::PostAction::Continue)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
Vendored
+69
@@ -0,0 +1,69 @@
|
||||
static POSSIBLE_BACKENDS: &[&str] = &[
|
||||
#[cfg(feature = "winit")]
|
||||
"--winit : Run anvil as a X11 or Wayland client using winit.",
|
||||
#[cfg(feature = "udev")]
|
||||
"--tty-udev : Run anvil as a tty udev client (requires root if without logind).",
|
||||
#[cfg(feature = "x11")]
|
||||
"--x11 : Run anvil as an X11 client.",
|
||||
];
|
||||
|
||||
#[cfg(feature = "profile-with-tracy-mem")]
|
||||
#[global_allocator]
|
||||
static GLOBAL: profiling::tracy_client::ProfiledAllocator<std::alloc::System> =
|
||||
profiling::tracy_client::ProfiledAllocator::new(std::alloc::System, 10);
|
||||
|
||||
// Allow in this function because of existing usage
|
||||
#[allow(clippy::uninlined_format_args)]
|
||||
fn main() {
|
||||
if let Ok(env_filter) = tracing_subscriber::EnvFilter::try_from_default_env() {
|
||||
tracing_subscriber::fmt()
|
||||
.compact()
|
||||
.with_env_filter(env_filter)
|
||||
.init();
|
||||
} else {
|
||||
tracing_subscriber::fmt().compact().init();
|
||||
}
|
||||
|
||||
#[cfg(feature = "profile-with-tracy")]
|
||||
profiling::tracy_client::Client::start();
|
||||
|
||||
profiling::register_thread!("Main Thread");
|
||||
|
||||
#[cfg(feature = "profile-with-puffin")]
|
||||
let _server = puffin_http::Server::new(&format!("0.0.0.0:{}", puffin_http::DEFAULT_PORT)).unwrap();
|
||||
#[cfg(feature = "profile-with-puffin")]
|
||||
profiling::puffin::set_scopes_on(true);
|
||||
|
||||
let arg = ::std::env::args().nth(1);
|
||||
match arg.as_ref().map(|s| &s[..]) {
|
||||
#[cfg(feature = "winit")]
|
||||
Some("--winit") => {
|
||||
tracing::info!("Starting anvil with winit backend");
|
||||
anvil::winit::run_winit();
|
||||
}
|
||||
#[cfg(feature = "udev")]
|
||||
Some("--tty-udev") => {
|
||||
tracing::info!("Starting anvil on a tty using udev");
|
||||
anvil::udev::run_udev();
|
||||
}
|
||||
#[cfg(feature = "x11")]
|
||||
Some("--x11") => {
|
||||
tracing::info!("Starting anvil with x11 backend");
|
||||
anvil::x11::run_x11();
|
||||
}
|
||||
Some(other) => {
|
||||
tracing::error!("Unknown backend: {}", other);
|
||||
}
|
||||
None => {
|
||||
#[allow(clippy::disallowed_macros)]
|
||||
{
|
||||
println!("USAGE: anvil --backend");
|
||||
println!();
|
||||
println!("Possible backends are:");
|
||||
for b in POSSIBLE_BACKENDS {
|
||||
println!("\t{b}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+210
@@ -0,0 +1,210 @@
|
||||
use smithay::{
|
||||
backend::renderer::{
|
||||
Color32F, ImportAll, ImportMem, Renderer,
|
||||
damage::{Error as OutputDamageTrackerError, OutputDamageTracker, RenderOutputResult},
|
||||
element::{
|
||||
AsRenderElements, RenderElement, Wrap,
|
||||
surface::WaylandSurfaceRenderElement,
|
||||
utils::{
|
||||
ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, RelocateRenderElement,
|
||||
RescaleRenderElement,
|
||||
},
|
||||
},
|
||||
},
|
||||
desktop::space::{
|
||||
ConstrainBehavior, ConstrainReference, Space, SpaceRenderElements, constrain_space_element,
|
||||
},
|
||||
output::Output,
|
||||
utils::{Point, Rectangle, Size},
|
||||
};
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
use crate::drawing::FpsElement;
|
||||
use crate::{
|
||||
drawing::{CLEAR_COLOR, CLEAR_COLOR_FULLSCREEN, PointerRenderElement},
|
||||
shell::{FullscreenSurface, WindowElement, WindowRenderElement},
|
||||
};
|
||||
|
||||
smithay::backend::renderer::element::render_elements! {
|
||||
pub CustomRenderElements<R> where
|
||||
R: ImportAll + ImportMem;
|
||||
Pointer=PointerRenderElement<R>,
|
||||
Surface=WaylandSurfaceRenderElement<R>,
|
||||
#[cfg(feature = "debug")]
|
||||
// Note: We would like to borrow this element instead, but that would introduce
|
||||
// a feature-dependent lifetime, which introduces a lot more feature bounds
|
||||
// as the whole type changes and we can't have an unused lifetime (for when "debug" is disabled)
|
||||
// in the declaration.
|
||||
Fps=FpsElement<R::TextureId>,
|
||||
}
|
||||
|
||||
impl<R: Renderer> std::fmt::Debug for CustomRenderElements<R> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Pointer(arg0) => f.debug_tuple("Pointer").field(arg0).finish(),
|
||||
Self::Surface(arg0) => f.debug_tuple("Surface").field(arg0).finish(),
|
||||
#[cfg(feature = "debug")]
|
||||
Self::Fps(arg0) => f.debug_tuple("Fps").field(arg0).finish(),
|
||||
Self::_GenericCatcher(arg0) => f.debug_tuple("_GenericCatcher").field(arg0).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
smithay::backend::renderer::element::render_elements! {
|
||||
pub OutputRenderElements<R, E> where R: ImportAll + ImportMem;
|
||||
Space=SpaceRenderElements<R, E>,
|
||||
Window=Wrap<E>,
|
||||
Custom=CustomRenderElements<R>,
|
||||
Preview=CropRenderElement<RelocateRenderElement<RescaleRenderElement<WindowRenderElement<R>>>>,
|
||||
}
|
||||
|
||||
impl<R: Renderer + ImportAll + ImportMem, E: RenderElement<R> + std::fmt::Debug> std::fmt::Debug
|
||||
for OutputRenderElements<R, E>
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Space(arg0) => f.debug_tuple("Space").field(arg0).finish(),
|
||||
Self::Window(arg0) => f.debug_tuple("Window").field(arg0).finish(),
|
||||
Self::Custom(arg0) => f.debug_tuple("Custom").field(arg0).finish(),
|
||||
Self::Preview(arg0) => f.debug_tuple("Preview").field(arg0).finish(),
|
||||
Self::_GenericCatcher(arg0) => f.debug_tuple("_GenericCatcher").field(arg0).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn space_preview_elements<'a, R, C>(
|
||||
renderer: &'a mut R,
|
||||
space: &'a Space<WindowElement>,
|
||||
output: &'a Output,
|
||||
) -> impl Iterator<Item = C> + 'a
|
||||
where
|
||||
R: Renderer + ImportAll + ImportMem,
|
||||
R::TextureId: Clone + 'static,
|
||||
C: From<CropRenderElement<RelocateRenderElement<RescaleRenderElement<WindowRenderElement<R>>>>> + 'a,
|
||||
{
|
||||
let constrain_behavior = ConstrainBehavior {
|
||||
reference: ConstrainReference::BoundingBox,
|
||||
behavior: ConstrainScaleBehavior::Fit,
|
||||
align: ConstrainAlign::CENTER,
|
||||
};
|
||||
|
||||
let preview_padding = 10;
|
||||
|
||||
let elements_on_space = space.elements_for_output(output).count();
|
||||
let output_scale = output.current_scale().fractional_scale();
|
||||
let output_transform = output.current_transform();
|
||||
let output_size = output
|
||||
.current_mode()
|
||||
.map(|mode| {
|
||||
output_transform
|
||||
.transform_size(mode.size)
|
||||
.to_f64()
|
||||
.to_logical(output_scale)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let max_elements_per_row = 4;
|
||||
let elements_per_row = usize::min(elements_on_space, max_elements_per_row);
|
||||
let rows = f64::ceil(elements_on_space as f64 / elements_per_row as f64);
|
||||
|
||||
let preview_size = Size::from((
|
||||
f64::round(output_size.w / elements_per_row as f64) as i32 - preview_padding * 2,
|
||||
f64::round(output_size.h / rows) as i32 - preview_padding * 2,
|
||||
));
|
||||
|
||||
space
|
||||
.elements_for_output(output)
|
||||
.enumerate()
|
||||
.flat_map(move |(element_index, window)| {
|
||||
let column = element_index % elements_per_row;
|
||||
let row = element_index / elements_per_row;
|
||||
let preview_location = Point::from((
|
||||
preview_padding + (preview_padding + preview_size.w) * column as i32,
|
||||
preview_padding + (preview_padding + preview_size.h) * row as i32,
|
||||
));
|
||||
let constrain = Rectangle::new(preview_location, preview_size);
|
||||
constrain_space_element(
|
||||
renderer,
|
||||
window,
|
||||
preview_location,
|
||||
1.0,
|
||||
output_scale,
|
||||
constrain,
|
||||
constrain_behavior,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
pub fn output_elements<R>(
|
||||
output: &Output,
|
||||
space: &Space<WindowElement>,
|
||||
custom_elements: impl IntoIterator<Item = CustomRenderElements<R>>,
|
||||
renderer: &mut R,
|
||||
show_window_preview: bool,
|
||||
) -> (Vec<OutputRenderElements<R, WindowRenderElement<R>>>, Color32F)
|
||||
where
|
||||
R: Renderer + ImportAll + ImportMem,
|
||||
R::TextureId: Clone + 'static,
|
||||
{
|
||||
if let Some(window) = output
|
||||
.user_data()
|
||||
.get::<FullscreenSurface>()
|
||||
.and_then(|f| f.get())
|
||||
{
|
||||
let scale = output.current_scale().fractional_scale().into();
|
||||
let window_render_elements: Vec<WindowRenderElement<R>> =
|
||||
AsRenderElements::<R>::render_elements(&window, renderer, (0, 0).into(), scale, 1.0);
|
||||
|
||||
let elements = custom_elements
|
||||
.into_iter()
|
||||
.map(OutputRenderElements::from)
|
||||
.chain(
|
||||
window_render_elements
|
||||
.into_iter()
|
||||
.map(|e| OutputRenderElements::Window(Wrap::from(e))),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
(elements, CLEAR_COLOR_FULLSCREEN)
|
||||
} else {
|
||||
let mut output_render_elements = custom_elements
|
||||
.into_iter()
|
||||
.map(OutputRenderElements::from)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if show_window_preview && space.elements_for_output(output).count() > 0 {
|
||||
output_render_elements.extend(space_preview_elements(renderer, space, output));
|
||||
}
|
||||
|
||||
let space_elements = smithay::desktop::space::space_render_elements::<_, WindowElement, _>(
|
||||
renderer,
|
||||
[space],
|
||||
output,
|
||||
1.0,
|
||||
)
|
||||
.expect("output without mode?");
|
||||
output_render_elements.extend(space_elements.into_iter().map(OutputRenderElements::Space));
|
||||
|
||||
(output_render_elements, CLEAR_COLOR)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn render_output<'a, 'd, R>(
|
||||
output: &'a Output,
|
||||
space: &'a Space<WindowElement>,
|
||||
custom_elements: impl IntoIterator<Item = CustomRenderElements<R>>,
|
||||
renderer: &'a mut R,
|
||||
framebuffer: &'a mut R::Framebuffer<'_>,
|
||||
damage_tracker: &'d mut OutputDamageTracker,
|
||||
age: usize,
|
||||
show_window_preview: bool,
|
||||
) -> Result<RenderOutputResult<'d>, OutputDamageTrackerError<R::Error>>
|
||||
where
|
||||
R: Renderer + ImportAll + ImportMem,
|
||||
R::TextureId: Clone + 'static,
|
||||
{
|
||||
let (elements, clear_color) =
|
||||
output_elements(output, space, custom_elements, renderer, show_window_preview);
|
||||
damage_tracker.render_output(renderer, framebuffer, age, &elements, clear_color)
|
||||
}
|
||||
+564
@@ -0,0 +1,564 @@
|
||||
use std::{borrow::Cow, time::Duration};
|
||||
|
||||
use smithay::{
|
||||
backend::{
|
||||
input::InputTime,
|
||||
renderer::{
|
||||
ImportAll, ImportMem, Renderer, Texture,
|
||||
element::{
|
||||
AsRenderElements, solid::SolidColorRenderElement, surface::WaylandSurfaceRenderElement,
|
||||
},
|
||||
},
|
||||
},
|
||||
desktop::{
|
||||
Window, WindowSurface, WindowSurfaceType, space::SpaceElement, utils::OutputPresentationFeedback,
|
||||
},
|
||||
input::{
|
||||
Seat,
|
||||
pointer::{
|
||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
|
||||
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent,
|
||||
GestureSwipeUpdateEvent, MotionEvent, PointerTarget, RelativeMotionEvent,
|
||||
},
|
||||
tablet::tool::TabletToolTarget,
|
||||
touch::{FrameMarker, TouchTarget},
|
||||
},
|
||||
output::Output,
|
||||
reexports::{
|
||||
wayland_protocols::wp::presentation_time::server::wp_presentation_feedback,
|
||||
wayland_server::protocol::wl_surface::WlSurface,
|
||||
},
|
||||
render_elements,
|
||||
utils::{IsAlive, Logical, Physical, Point, Rectangle, Scale, Serial, user_data::UserDataMap},
|
||||
wayland::{compositor::SurfaceData as WlSurfaceData, dmabuf::DmabufFeedback, seat::WaylandFocus},
|
||||
};
|
||||
|
||||
use super::ssd::HEADER_BAR_HEIGHT;
|
||||
use crate::{AnvilState, focus::PointerFocusTarget, state::Backend};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct WindowElement(pub Window);
|
||||
|
||||
impl WindowElement {
|
||||
pub fn surface_under(
|
||||
&self,
|
||||
location: Point<f64, Logical>,
|
||||
window_type: WindowSurfaceType,
|
||||
) -> Option<(PointerFocusTarget, Point<i32, Logical>)> {
|
||||
let state = self.decoration_state();
|
||||
if state.is_ssd && location.y < HEADER_BAR_HEIGHT as f64 {
|
||||
return Some((PointerFocusTarget::SSD(SSD(self.clone())), Point::default()));
|
||||
}
|
||||
let offset = if state.is_ssd {
|
||||
Point::from((0, HEADER_BAR_HEIGHT))
|
||||
} else {
|
||||
Point::default()
|
||||
};
|
||||
|
||||
let surface_under = self.0.surface_under(location - offset.to_f64(), window_type);
|
||||
let (under, loc) = match self.0.underlying_surface() {
|
||||
WindowSurface::Wayland(_) => {
|
||||
surface_under.map(|(surface, loc)| (PointerFocusTarget::WlSurface(surface), loc))
|
||||
}
|
||||
#[cfg(feature = "xwayland")]
|
||||
WindowSurface::X11(s) => {
|
||||
surface_under.map(|(_, loc)| (PointerFocusTarget::X11Surface(s.clone()), loc))
|
||||
}
|
||||
}?;
|
||||
Some((under, loc + offset))
|
||||
}
|
||||
|
||||
pub fn with_surfaces<F>(&self, processor: F)
|
||||
where
|
||||
F: FnMut(&WlSurface, &WlSurfaceData),
|
||||
{
|
||||
self.0.with_surfaces(processor);
|
||||
}
|
||||
|
||||
pub fn send_frame<T, F>(
|
||||
&self,
|
||||
output: &Output,
|
||||
time: T,
|
||||
throttle: Option<Duration>,
|
||||
primary_scan_out_output: F,
|
||||
) where
|
||||
T: Into<Duration>,
|
||||
F: FnMut(&WlSurface, &WlSurfaceData) -> Option<Output> + Copy,
|
||||
{
|
||||
self.0.send_frame(output, time, throttle, primary_scan_out_output)
|
||||
}
|
||||
|
||||
pub fn send_dmabuf_feedback<'a, P, F>(
|
||||
&self,
|
||||
output: &Output,
|
||||
primary_scan_out_output: P,
|
||||
select_dmabuf_feedback: F,
|
||||
) where
|
||||
P: FnMut(&WlSurface, &WlSurfaceData) -> Option<Output> + Copy,
|
||||
F: Fn(&WlSurface, &WlSurfaceData) -> &'a DmabufFeedback + Copy,
|
||||
{
|
||||
self.0
|
||||
.send_dmabuf_feedback(output, primary_scan_out_output, select_dmabuf_feedback)
|
||||
}
|
||||
|
||||
pub fn take_presentation_feedback<F1, F2>(
|
||||
&self,
|
||||
output_feedback: &mut OutputPresentationFeedback,
|
||||
primary_scan_out_output: F1,
|
||||
presentation_feedback_flags: F2,
|
||||
) where
|
||||
F1: FnMut(&WlSurface, &WlSurfaceData) -> Option<Output> + Copy,
|
||||
F2: FnMut(&WlSurface, &WlSurfaceData) -> wp_presentation_feedback::Kind + Copy,
|
||||
{
|
||||
self.0.take_presentation_feedback(
|
||||
output_feedback,
|
||||
primary_scan_out_output,
|
||||
presentation_feedback_flags,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "xwayland")]
|
||||
#[inline]
|
||||
pub fn is_x11(&self) -> bool {
|
||||
self.0.is_x11()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_wayland(&self) -> bool {
|
||||
self.0.is_wayland()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn wl_surface(&self) -> Option<Cow<'_, WlSurface>> {
|
||||
self.0.wl_surface()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn user_data(&self) -> &UserDataMap {
|
||||
self.0.user_data()
|
||||
}
|
||||
}
|
||||
|
||||
impl IsAlive for WindowElement {
|
||||
#[inline]
|
||||
fn alive(&self) -> bool {
|
||||
self.0.alive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SSD(WindowElement);
|
||||
|
||||
impl IsAlive for SSD {
|
||||
#[inline]
|
||||
fn alive(&self) -> bool {
|
||||
self.0.alive()
|
||||
}
|
||||
}
|
||||
|
||||
impl WaylandFocus for SSD {
|
||||
#[inline]
|
||||
fn wl_surface(&self) -> Option<Cow<'_, WlSurface>> {
|
||||
self.0.wl_surface()
|
||||
}
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> PointerTarget<AnvilState<BackendData>> for SSD {
|
||||
fn enter(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
let mut state = self.0.decoration_state();
|
||||
if state.is_ssd {
|
||||
state.header_bar.pointer_enter(event.location);
|
||||
}
|
||||
}
|
||||
fn motion(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
let mut state = self.0.decoration_state();
|
||||
if state.is_ssd {
|
||||
state.header_bar.pointer_enter(event.location);
|
||||
}
|
||||
}
|
||||
fn relative_motion(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_event: &RelativeMotionEvent,
|
||||
) {
|
||||
}
|
||||
fn button(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
let mut state = self.0.decoration_state();
|
||||
if state.is_ssd {
|
||||
state.header_bar.clicked(seat, data, &self.0, event.serial);
|
||||
}
|
||||
}
|
||||
fn axis(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_frame: AxisFrame,
|
||||
) {
|
||||
}
|
||||
fn frame(&self, _seat: &Seat<AnvilState<BackendData>>, _data: &mut AnvilState<BackendData>) {}
|
||||
fn leave(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_serial: Serial,
|
||||
_time: InputTime,
|
||||
) {
|
||||
let mut state = self.0.decoration_state();
|
||||
if state.is_ssd {
|
||||
state.header_bar.pointer_leave();
|
||||
}
|
||||
}
|
||||
fn gesture_swipe_begin(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
}
|
||||
fn gesture_swipe_update(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
}
|
||||
fn gesture_swipe_end(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
}
|
||||
fn gesture_pinch_begin(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
}
|
||||
fn gesture_pinch_update(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
}
|
||||
fn gesture_pinch_end(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_event: &GesturePinchEndEvent,
|
||||
) {
|
||||
}
|
||||
fn gesture_hold_begin(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
}
|
||||
fn gesture_hold_end(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_event: &GestureHoldEndEvent,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> TouchTarget<AnvilState<BackendData>> for SSD {
|
||||
fn down(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
event: &smithay::input::touch::DownEvent,
|
||||
) {
|
||||
let mut state = self.0.decoration_state();
|
||||
if state.is_ssd {
|
||||
state.header_bar.pointer_enter(event.location);
|
||||
state.header_bar.touch_down(seat, data, &self.0, event.serial);
|
||||
}
|
||||
}
|
||||
|
||||
fn up(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
_event: &smithay::input::touch::UpEvent,
|
||||
) {
|
||||
let mut state = self.0.decoration_state();
|
||||
if state.is_ssd {
|
||||
state.header_bar.touch_up(seat, data, &self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn motion(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
event: &smithay::input::touch::MotionEvent,
|
||||
) {
|
||||
let mut state = self.0.decoration_state();
|
||||
if state.is_ssd {
|
||||
state.header_bar.pointer_enter(event.location);
|
||||
}
|
||||
}
|
||||
|
||||
fn frame(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_marker: FrameMarker,
|
||||
) {
|
||||
}
|
||||
|
||||
fn cancel(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_marker: FrameMarker,
|
||||
) {
|
||||
}
|
||||
|
||||
fn shape(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_event: &smithay::input::touch::ShapeEvent,
|
||||
) {
|
||||
}
|
||||
|
||||
fn orientation(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_event: &smithay::input::touch::OrientationEvent,
|
||||
) {
|
||||
}
|
||||
|
||||
fn last_frame(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
) -> Option<FrameMarker> {
|
||||
// It would be more correct to store the marker on frame and cancel,
|
||||
// but since we're ignoring those anyway, no need for the added complexity.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> TabletToolTarget<AnvilState<BackendData>> for SSD {
|
||||
fn proximity_in(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_tool_descriptor: &smithay::backend::input::TabletToolDescriptor,
|
||||
_tablet: &smithay::input::tablet::Tablet,
|
||||
_serial: Serial,
|
||||
) {
|
||||
}
|
||||
|
||||
fn proximity_out(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_tool_descriptor: &smithay::backend::input::TabletToolDescriptor,
|
||||
) {
|
||||
let mut state = self.0.decoration_state();
|
||||
if state.is_ssd {
|
||||
state.header_bar.pointer_leave();
|
||||
}
|
||||
}
|
||||
|
||||
fn down(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
_tool_descriptor: &smithay::backend::input::TabletToolDescriptor,
|
||||
event: &smithay::input::tablet::tool::DownEvent,
|
||||
) {
|
||||
let mut state = self.0.decoration_state();
|
||||
if state.is_ssd {
|
||||
state.header_bar.touch_down(seat, data, &self.0, event.serial);
|
||||
}
|
||||
}
|
||||
|
||||
fn up(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
_tool_descriptor: &smithay::backend::input::TabletToolDescriptor,
|
||||
_event: &smithay::input::tablet::tool::UpEvent,
|
||||
) {
|
||||
let mut state = self.0.decoration_state();
|
||||
if state.is_ssd {
|
||||
state.header_bar.touch_up(seat, data, &self.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn motion(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_tool_descriptor: &smithay::backend::input::TabletToolDescriptor,
|
||||
event: &smithay::input::tablet::tool::MotionEvent,
|
||||
) {
|
||||
let mut state = self.0.decoration_state();
|
||||
if state.is_ssd {
|
||||
state.header_bar.pointer_enter(event.location);
|
||||
}
|
||||
}
|
||||
|
||||
fn button(
|
||||
&self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
_tool_descriptor: &smithay::backend::input::TabletToolDescriptor,
|
||||
event: &smithay::input::tablet::tool::ButtonEvent,
|
||||
) {
|
||||
let mut state = self.0.decoration_state();
|
||||
if state.is_ssd {
|
||||
state.header_bar.clicked(seat, data, &self.0, event.serial);
|
||||
}
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_tool_descriptor: &smithay::backend::input::TabletToolDescriptor,
|
||||
_frame: smithay::input::tablet::tool::AxisFrame,
|
||||
) {
|
||||
}
|
||||
|
||||
fn frame(
|
||||
&self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_tool_descriptor: &smithay::backend::input::TabletToolDescriptor,
|
||||
_time: InputTime,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl SpaceElement for WindowElement {
|
||||
fn geometry(&self) -> Rectangle<i32, Logical> {
|
||||
let mut geo = SpaceElement::geometry(&self.0);
|
||||
if self.decoration_state().is_ssd {
|
||||
geo.size.h += HEADER_BAR_HEIGHT;
|
||||
}
|
||||
geo
|
||||
}
|
||||
fn bbox(&self) -> Rectangle<i32, Logical> {
|
||||
let mut bbox = SpaceElement::bbox(&self.0);
|
||||
if self.decoration_state().is_ssd {
|
||||
bbox.size.h += HEADER_BAR_HEIGHT;
|
||||
}
|
||||
bbox
|
||||
}
|
||||
fn is_in_input_region(&self, point: &Point<f64, Logical>) -> bool {
|
||||
if self.decoration_state().is_ssd {
|
||||
point.y < HEADER_BAR_HEIGHT as f64
|
||||
|| SpaceElement::is_in_input_region(
|
||||
&self.0,
|
||||
&(*point - Point::from((0.0, HEADER_BAR_HEIGHT as f64))),
|
||||
)
|
||||
} else {
|
||||
SpaceElement::is_in_input_region(&self.0, point)
|
||||
}
|
||||
}
|
||||
fn z_index(&self) -> u8 {
|
||||
SpaceElement::z_index(&self.0)
|
||||
}
|
||||
|
||||
fn set_activate(&self, activated: bool) {
|
||||
SpaceElement::set_activate(&self.0, activated);
|
||||
}
|
||||
fn output_enter(&self, output: &Output, overlap: Rectangle<i32, Logical>) {
|
||||
SpaceElement::output_enter(&self.0, output, overlap);
|
||||
}
|
||||
fn output_leave(&self, output: &Output) {
|
||||
SpaceElement::output_leave(&self.0, output);
|
||||
}
|
||||
#[profiling::function]
|
||||
fn refresh(&self) {
|
||||
SpaceElement::refresh(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
render_elements!(
|
||||
pub WindowRenderElement<R> where R: ImportAll + ImportMem;
|
||||
Window=WaylandSurfaceRenderElement<R>,
|
||||
Decoration=SolidColorRenderElement,
|
||||
);
|
||||
|
||||
impl<R: Renderer> std::fmt::Debug for WindowRenderElement<R> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Window(arg0) => f.debug_tuple("Window").field(arg0).finish(),
|
||||
Self::Decoration(arg0) => f.debug_tuple("Decoration").field(arg0).finish(),
|
||||
Self::_GenericCatcher(arg0) => f.debug_tuple("_GenericCatcher").field(arg0).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> AsRenderElements<R> for WindowElement
|
||||
where
|
||||
R: Renderer + ImportAll + ImportMem,
|
||||
R::TextureId: Clone + Texture + 'static,
|
||||
{
|
||||
type RenderElement = WindowRenderElement<R>;
|
||||
|
||||
fn render_elements<C: From<Self::RenderElement>>(
|
||||
&self,
|
||||
renderer: &mut R,
|
||||
mut location: Point<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
alpha: f32,
|
||||
) -> Vec<C> {
|
||||
let window_bbox = SpaceElement::bbox(&self.0);
|
||||
|
||||
if self.decoration_state().is_ssd && !window_bbox.is_empty() {
|
||||
let window_geo = SpaceElement::geometry(&self.0);
|
||||
|
||||
let mut state = self.decoration_state();
|
||||
let width = window_geo.size.w;
|
||||
state.header_bar.redraw(width as u32);
|
||||
let mut vec = AsRenderElements::<R>::render_elements::<WindowRenderElement<R>>(
|
||||
&state.header_bar,
|
||||
renderer,
|
||||
location,
|
||||
scale,
|
||||
alpha,
|
||||
);
|
||||
|
||||
location.y += (scale.y * HEADER_BAR_HEIGHT as f64) as i32;
|
||||
|
||||
let window_elements =
|
||||
AsRenderElements::render_elements(&self.0, renderer, location, scale, alpha);
|
||||
vec.extend(window_elements);
|
||||
vec.into_iter().map(C::from).collect()
|
||||
} else {
|
||||
AsRenderElements::render_elements(&self.0, renderer, location, scale, alpha)
|
||||
.into_iter()
|
||||
.map(C::from)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
+855
@@ -0,0 +1,855 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use smithay::{
|
||||
desktop::{WindowSurface, space::SpaceElement},
|
||||
input::{
|
||||
pointer::{
|
||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
|
||||
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent,
|
||||
GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab,
|
||||
PointerInnerHandle, RelativeMotionEvent,
|
||||
},
|
||||
touch::{GrabStartData as TouchGrabStartData, TouchGrab},
|
||||
},
|
||||
reexports::wayland_protocols::xdg::shell::server::xdg_toplevel,
|
||||
utils::{IsAlive, Logical, Point, Serial, Size},
|
||||
wayland::{compositor::with_states, shell::xdg::SurfaceCachedState},
|
||||
};
|
||||
#[cfg(feature = "xwayland")]
|
||||
use smithay::{utils::Rectangle, xwayland::xwm::ResizeEdge as X11ResizeEdge};
|
||||
|
||||
use super::{SurfaceData, WindowElement};
|
||||
use crate::{
|
||||
focus::PointerFocusTarget,
|
||||
state::{AnvilState, Backend},
|
||||
};
|
||||
|
||||
pub struct PointerMoveSurfaceGrab<BackendData: Backend + 'static> {
|
||||
pub start_data: PointerGrabStartData<AnvilState<BackendData>>,
|
||||
pub window: WindowElement,
|
||||
pub initial_window_location: Point<i32, Logical>,
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> PointerGrab<AnvilState<BackendData>> for PointerMoveSurfaceGrab<BackendData> {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
_focus: Option<(PointerFocusTarget, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus
|
||||
handle.motion(data, None, event);
|
||||
|
||||
let delta = event.location - self.start_data.location;
|
||||
let new_location = self.initial_window_location.to_f64() + delta;
|
||||
|
||||
data.space
|
||||
.map_element(self.window.clone(), new_location.to_i32_round(), true);
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
focus: Option<(PointerFocusTarget, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
handle.relative_motion(data, focus, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
handle.button(data, event);
|
||||
if handle.current_pressed().is_empty() {
|
||||
// No more buttons are pressed, release the grab.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
details: AxisFrame,
|
||||
) {
|
||||
handle.axis(data, details)
|
||||
}
|
||||
|
||||
fn frame(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
) {
|
||||
handle.frame(data);
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
handle.gesture_swipe_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_update(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
handle.gesture_swipe_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_end(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
handle.gesture_swipe_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_begin(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
handle.gesture_pinch_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_update(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
handle.gesture_pinch_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_end(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
handle.gesture_pinch_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_begin(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
handle.gesture_hold_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_end(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
handle.gesture_hold_end(data, event);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &PointerGrabStartData<AnvilState<BackendData>> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, _data: &mut AnvilState<BackendData>) {}
|
||||
}
|
||||
|
||||
pub struct TouchMoveSurfaceGrab<BackendData: Backend + 'static> {
|
||||
pub start_data: TouchGrabStartData<AnvilState<BackendData>>,
|
||||
pub window: WindowElement,
|
||||
pub initial_window_location: Point<i32, Logical>,
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> TouchGrab<AnvilState<BackendData>> for TouchMoveSurfaceGrab<BackendData> {
|
||||
fn down(
|
||||
&mut self,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState<BackendData>>,
|
||||
_focus: Option<(
|
||||
<AnvilState<BackendData> as smithay::input::SeatHandler>::TouchFocus,
|
||||
Point<f64, Logical>,
|
||||
)>,
|
||||
_event: &smithay::input::touch::DownEvent,
|
||||
) {
|
||||
}
|
||||
|
||||
fn up(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &smithay::input::touch::UpEvent,
|
||||
) {
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
handle.up(data, event);
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
_handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState<BackendData>>,
|
||||
_focus: Option<(
|
||||
<AnvilState<BackendData> as smithay::input::SeatHandler>::TouchFocus,
|
||||
Point<f64, Logical>,
|
||||
)>,
|
||||
event: &smithay::input::touch::MotionEvent,
|
||||
) {
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
let delta = event.location - self.start_data.location;
|
||||
let new_location = self.initial_window_location.to_f64() + delta;
|
||||
data.space
|
||||
.map_element(self.window.clone(), new_location.to_i32_round(), true);
|
||||
}
|
||||
|
||||
fn frame(
|
||||
&mut self,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState<BackendData>>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn cancel(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState<BackendData>>,
|
||||
) {
|
||||
handle.cancel(data);
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn shape(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &smithay::input::touch::ShapeEvent,
|
||||
) {
|
||||
handle.shape(data, event);
|
||||
}
|
||||
|
||||
fn orientation(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &smithay::input::touch::OrientationEvent,
|
||||
) {
|
||||
handle.orientation(data, event);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &smithay::input::touch::GrabStartData<AnvilState<BackendData>> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, _data: &mut AnvilState<BackendData>) {}
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ResizeEdge: u32 {
|
||||
const NONE = 0;
|
||||
const TOP = 1;
|
||||
const BOTTOM = 2;
|
||||
const LEFT = 4;
|
||||
const TOP_LEFT = 5;
|
||||
const BOTTOM_LEFT = 6;
|
||||
const RIGHT = 8;
|
||||
const TOP_RIGHT = 9;
|
||||
const BOTTOM_RIGHT = 10;
|
||||
}
|
||||
}
|
||||
|
||||
impl From<xdg_toplevel::ResizeEdge> for ResizeEdge {
|
||||
#[inline]
|
||||
fn from(x: xdg_toplevel::ResizeEdge) -> Self {
|
||||
Self::from_bits(x as u32).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResizeEdge> for xdg_toplevel::ResizeEdge {
|
||||
#[inline]
|
||||
fn from(x: ResizeEdge) -> Self {
|
||||
Self::try_from(x.bits()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "xwayland")]
|
||||
impl From<X11ResizeEdge> for ResizeEdge {
|
||||
#[inline]
|
||||
fn from(edge: X11ResizeEdge) -> Self {
|
||||
match edge {
|
||||
X11ResizeEdge::Bottom => ResizeEdge::BOTTOM,
|
||||
X11ResizeEdge::BottomLeft => ResizeEdge::BOTTOM_LEFT,
|
||||
X11ResizeEdge::BottomRight => ResizeEdge::BOTTOM_RIGHT,
|
||||
X11ResizeEdge::Left => ResizeEdge::LEFT,
|
||||
X11ResizeEdge::Right => ResizeEdge::RIGHT,
|
||||
X11ResizeEdge::Top => ResizeEdge::TOP,
|
||||
X11ResizeEdge::TopLeft => ResizeEdge::TOP_LEFT,
|
||||
X11ResizeEdge::TopRight => ResizeEdge::TOP_RIGHT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about the resize operation.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct ResizeData {
|
||||
/// The edges the surface is being resized with.
|
||||
pub edges: ResizeEdge,
|
||||
/// The initial window location.
|
||||
pub initial_window_location: Point<i32, Logical>,
|
||||
/// The initial window size (geometry width and height).
|
||||
pub initial_window_size: Size<i32, Logical>,
|
||||
}
|
||||
|
||||
/// State of the resize operation.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
|
||||
pub enum ResizeState {
|
||||
/// The surface is not being resized.
|
||||
#[default]
|
||||
NotResizing,
|
||||
/// The surface is currently being resized.
|
||||
Resizing(ResizeData),
|
||||
/// The resize has finished, and the surface needs to ack the final configure.
|
||||
WaitingForFinalAck(ResizeData, Serial),
|
||||
/// The resize has finished, and the surface needs to commit its final state.
|
||||
WaitingForCommit(ResizeData),
|
||||
}
|
||||
|
||||
pub struct PointerResizeSurfaceGrab<BackendData: Backend + 'static> {
|
||||
pub start_data: PointerGrabStartData<AnvilState<BackendData>>,
|
||||
pub window: WindowElement,
|
||||
pub edges: ResizeEdge,
|
||||
pub initial_window_location: Point<i32, Logical>,
|
||||
pub initial_window_size: Size<i32, Logical>,
|
||||
pub last_window_size: Size<i32, Logical>,
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> PointerGrab<AnvilState<BackendData>> for PointerResizeSurfaceGrab<BackendData> {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
_focus: Option<(PointerFocusTarget, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus
|
||||
handle.motion(data, None, event);
|
||||
|
||||
// It is impossible to get `min_size` and `max_size` of dead toplevel, so we return early.
|
||||
if !self.window.alive() {
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
return;
|
||||
}
|
||||
|
||||
let (mut dx, mut dy) = (event.location - self.start_data.location).into();
|
||||
|
||||
let mut new_window_width = self.initial_window_size.w;
|
||||
let mut new_window_height = self.initial_window_size.h;
|
||||
|
||||
let left_right = ResizeEdge::LEFT | ResizeEdge::RIGHT;
|
||||
let top_bottom = ResizeEdge::TOP | ResizeEdge::BOTTOM;
|
||||
|
||||
if self.edges.intersects(left_right) {
|
||||
if self.edges.intersects(ResizeEdge::LEFT) {
|
||||
dx = -dx;
|
||||
}
|
||||
|
||||
new_window_width = (self.initial_window_size.w as f64 + dx) as i32;
|
||||
}
|
||||
|
||||
if self.edges.intersects(top_bottom) {
|
||||
if self.edges.intersects(ResizeEdge::TOP) {
|
||||
dy = -dy;
|
||||
}
|
||||
|
||||
new_window_height = (self.initial_window_size.h as f64 + dy) as i32;
|
||||
}
|
||||
|
||||
let (min_size, max_size) = if let Some(surface) = self.window.wl_surface() {
|
||||
with_states(&surface, |states| {
|
||||
let mut guard = states.cached_state.get::<SurfaceCachedState>();
|
||||
let data = guard.current();
|
||||
(data.min_size, data.max_size)
|
||||
})
|
||||
} else {
|
||||
((0, 0).into(), (0, 0).into())
|
||||
};
|
||||
|
||||
let min_width = min_size.w.max(1);
|
||||
let min_height = min_size.h.max(1);
|
||||
let max_width = if max_size.w == 0 { i32::MAX } else { max_size.w };
|
||||
let max_height = if max_size.h == 0 { i32::MAX } else { max_size.h };
|
||||
|
||||
new_window_width = new_window_width.max(min_width).min(max_width);
|
||||
new_window_height = new_window_height.max(min_height).min(max_height);
|
||||
|
||||
self.last_window_size = (new_window_width, new_window_height).into();
|
||||
|
||||
match &self.window.0.underlying_surface() {
|
||||
WindowSurface::Wayland(xdg) => {
|
||||
xdg.with_pending_state(|state| {
|
||||
state.states.set(xdg_toplevel::State::Resizing);
|
||||
state.size = Some(self.last_window_size);
|
||||
});
|
||||
xdg.send_pending_configure();
|
||||
}
|
||||
#[cfg(feature = "xwayland")]
|
||||
WindowSurface::X11(x11) => {
|
||||
let location = data.space.element_location(&self.window).unwrap();
|
||||
x11.configure_with_sync(Rectangle::new(location, self.last_window_size), None)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
focus: Option<(PointerFocusTarget, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
handle.relative_motion(data, focus, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
handle.button(data, event);
|
||||
if handle.current_pressed().is_empty() {
|
||||
// No more buttons are pressed, release the grab.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
|
||||
// If toplevel is dead, we can't resize it, so we return early.
|
||||
if !self.window.alive() {
|
||||
return;
|
||||
}
|
||||
|
||||
match &self.window.0.underlying_surface() {
|
||||
WindowSurface::Wayland(xdg) => {
|
||||
xdg.with_pending_state(|state| {
|
||||
state.states.unset(xdg_toplevel::State::Resizing);
|
||||
state.size = Some(self.last_window_size);
|
||||
});
|
||||
xdg.send_pending_configure();
|
||||
if self.edges.intersects(ResizeEdge::TOP_LEFT) {
|
||||
let geometry = self.window.geometry();
|
||||
let mut location = data.space.element_location(&self.window).unwrap();
|
||||
|
||||
if self.edges.intersects(ResizeEdge::LEFT) {
|
||||
location.x = self.initial_window_location.x
|
||||
+ (self.initial_window_size.w - geometry.size.w);
|
||||
}
|
||||
if self.edges.intersects(ResizeEdge::TOP) {
|
||||
location.y = self.initial_window_location.y
|
||||
+ (self.initial_window_size.h - geometry.size.h);
|
||||
}
|
||||
|
||||
data.space.map_element(self.window.clone(), location, true);
|
||||
}
|
||||
|
||||
with_states(&self.window.wl_surface().unwrap(), |states| {
|
||||
let mut data = states
|
||||
.data_map
|
||||
.get::<RefCell<SurfaceData>>()
|
||||
.unwrap()
|
||||
.borrow_mut();
|
||||
if let ResizeState::Resizing(resize_data) = data.resize_state {
|
||||
data.resize_state = ResizeState::WaitingForFinalAck(resize_data, event.serial);
|
||||
} else {
|
||||
panic!("invalid resize state: {:?}", data.resize_state);
|
||||
}
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "xwayland")]
|
||||
WindowSurface::X11(x11) => {
|
||||
let mut location = data.space.element_location(&self.window).unwrap();
|
||||
if self.edges.intersects(ResizeEdge::TOP_LEFT) {
|
||||
let geometry = self.window.geometry();
|
||||
|
||||
if self.edges.intersects(ResizeEdge::LEFT) {
|
||||
location.x = self.initial_window_location.x
|
||||
+ (self.initial_window_size.w - geometry.size.w);
|
||||
}
|
||||
if self.edges.intersects(ResizeEdge::TOP) {
|
||||
location.y = self.initial_window_location.y
|
||||
+ (self.initial_window_size.h - geometry.size.h);
|
||||
}
|
||||
|
||||
data.space.map_element(self.window.clone(), location, true);
|
||||
}
|
||||
x11.configure_with_sync(Rectangle::new(location, self.last_window_size), None)
|
||||
.unwrap();
|
||||
|
||||
let Some(surface) = self.window.wl_surface() else {
|
||||
// X11 Window got unmapped, abort
|
||||
return;
|
||||
};
|
||||
with_states(&surface, |states| {
|
||||
let mut data = states
|
||||
.data_map
|
||||
.get::<RefCell<SurfaceData>>()
|
||||
.unwrap()
|
||||
.borrow_mut();
|
||||
if let ResizeState::Resizing(resize_data) = data.resize_state {
|
||||
data.resize_state = ResizeState::WaitingForCommit(resize_data);
|
||||
} else {
|
||||
panic!("invalid resize state: {:?}", data.resize_state);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
details: AxisFrame,
|
||||
) {
|
||||
handle.axis(data, details)
|
||||
}
|
||||
|
||||
fn frame(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
) {
|
||||
handle.frame(data);
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
handle.gesture_swipe_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_update(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
handle.gesture_swipe_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_swipe_end(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
handle.gesture_swipe_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_begin(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
handle.gesture_pinch_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_update(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
handle.gesture_pinch_update(data, event);
|
||||
}
|
||||
|
||||
fn gesture_pinch_end(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
handle.gesture_pinch_end(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_begin(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
handle.gesture_hold_begin(data, event);
|
||||
}
|
||||
|
||||
fn gesture_hold_end(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut PointerInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
handle.gesture_hold_end(data, event);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &PointerGrabStartData<AnvilState<BackendData>> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, _data: &mut AnvilState<BackendData>) {}
|
||||
}
|
||||
|
||||
pub struct TouchResizeSurfaceGrab<BackendData: Backend + 'static> {
|
||||
pub start_data: TouchGrabStartData<AnvilState<BackendData>>,
|
||||
pub window: WindowElement,
|
||||
pub edges: ResizeEdge,
|
||||
pub initial_window_location: Point<i32, Logical>,
|
||||
pub initial_window_size: Size<i32, Logical>,
|
||||
pub last_window_size: Size<i32, Logical>,
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> TouchGrab<AnvilState<BackendData>> for TouchResizeSurfaceGrab<BackendData> {
|
||||
fn down(
|
||||
&mut self,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState<BackendData>>,
|
||||
_focus: Option<(
|
||||
<AnvilState<BackendData> as smithay::input::SeatHandler>::TouchFocus,
|
||||
Point<f64, Logical>,
|
||||
)>,
|
||||
_event: &smithay::input::touch::DownEvent,
|
||||
) {
|
||||
}
|
||||
|
||||
fn up(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &smithay::input::touch::UpEvent,
|
||||
) {
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
handle.unset_grab(self, data);
|
||||
|
||||
// If toplevel is dead, we can't resize it, so we return early.
|
||||
if !self.window.alive() {
|
||||
return;
|
||||
}
|
||||
|
||||
match self.window.0.underlying_surface() {
|
||||
WindowSurface::Wayland(xdg) => {
|
||||
xdg.with_pending_state(|state| {
|
||||
state.states.unset(xdg_toplevel::State::Resizing);
|
||||
state.size = Some(self.last_window_size);
|
||||
});
|
||||
xdg.send_pending_configure();
|
||||
if self.edges.intersects(ResizeEdge::TOP_LEFT) {
|
||||
let geometry = self.window.geometry();
|
||||
let mut location = data.space.element_location(&self.window).unwrap();
|
||||
|
||||
if self.edges.intersects(ResizeEdge::LEFT) {
|
||||
location.x =
|
||||
self.initial_window_location.x + (self.initial_window_size.w - geometry.size.w);
|
||||
}
|
||||
if self.edges.intersects(ResizeEdge::TOP) {
|
||||
location.y =
|
||||
self.initial_window_location.y + (self.initial_window_size.h - geometry.size.h);
|
||||
}
|
||||
|
||||
data.space.map_element(self.window.clone(), location, true);
|
||||
}
|
||||
|
||||
with_states(&self.window.wl_surface().unwrap(), |states| {
|
||||
let mut data = states
|
||||
.data_map
|
||||
.get::<RefCell<SurfaceData>>()
|
||||
.unwrap()
|
||||
.borrow_mut();
|
||||
if let ResizeState::Resizing(resize_data) = data.resize_state {
|
||||
data.resize_state = ResizeState::WaitingForFinalAck(resize_data, event.serial);
|
||||
} else {
|
||||
panic!("invalid resize state: {:?}", data.resize_state);
|
||||
}
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "xwayland")]
|
||||
WindowSurface::X11(x11) => {
|
||||
let mut location = data.space.element_location(&self.window).unwrap();
|
||||
if self.edges.intersects(ResizeEdge::TOP_LEFT) {
|
||||
let geometry = self.window.geometry();
|
||||
|
||||
if self.edges.intersects(ResizeEdge::LEFT) {
|
||||
location.x =
|
||||
self.initial_window_location.x + (self.initial_window_size.w - geometry.size.w);
|
||||
}
|
||||
if self.edges.intersects(ResizeEdge::TOP) {
|
||||
location.y =
|
||||
self.initial_window_location.y + (self.initial_window_size.h - geometry.size.h);
|
||||
}
|
||||
|
||||
data.space.map_element(self.window.clone(), location, true);
|
||||
}
|
||||
x11.configure_with_sync(Rectangle::new(location, self.last_window_size), None)
|
||||
.unwrap();
|
||||
|
||||
let Some(surface) = self.window.wl_surface() else {
|
||||
// X11 Window got unmapped, abort
|
||||
return;
|
||||
};
|
||||
with_states(&surface, |states| {
|
||||
let mut data = states
|
||||
.data_map
|
||||
.get::<RefCell<SurfaceData>>()
|
||||
.unwrap()
|
||||
.borrow_mut();
|
||||
if let ResizeState::Resizing(resize_data) = data.resize_state {
|
||||
data.resize_state = ResizeState::WaitingForCommit(resize_data);
|
||||
} else {
|
||||
panic!("invalid resize state: {:?}", data.resize_state);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState<BackendData>>,
|
||||
_focus: Option<(
|
||||
<AnvilState<BackendData> as smithay::input::SeatHandler>::TouchFocus,
|
||||
Point<f64, Logical>,
|
||||
)>,
|
||||
event: &smithay::input::touch::MotionEvent,
|
||||
) {
|
||||
if event.slot != self.start_data.slot {
|
||||
return;
|
||||
}
|
||||
|
||||
// It is impossible to get `min_size` and `max_size` of dead toplevel, so we return early.
|
||||
if !self.window.alive() {
|
||||
handle.unset_grab(self, data);
|
||||
return;
|
||||
}
|
||||
|
||||
let (mut dx, mut dy) = (event.location - self.start_data.location).into();
|
||||
|
||||
let mut new_window_width = self.initial_window_size.w;
|
||||
let mut new_window_height = self.initial_window_size.h;
|
||||
|
||||
let left_right = ResizeEdge::LEFT | ResizeEdge::RIGHT;
|
||||
let top_bottom = ResizeEdge::TOP | ResizeEdge::BOTTOM;
|
||||
|
||||
if self.edges.intersects(left_right) {
|
||||
if self.edges.intersects(ResizeEdge::LEFT) {
|
||||
dx = -dx;
|
||||
}
|
||||
|
||||
new_window_width = (self.initial_window_size.w as f64 + dx) as i32;
|
||||
}
|
||||
|
||||
if self.edges.intersects(top_bottom) {
|
||||
if self.edges.intersects(ResizeEdge::TOP) {
|
||||
dy = -dy;
|
||||
}
|
||||
|
||||
new_window_height = (self.initial_window_size.h as f64 + dy) as i32;
|
||||
}
|
||||
|
||||
let (min_size, max_size) = if let Some(surface) = self.window.wl_surface() {
|
||||
with_states(&surface, |states| {
|
||||
let mut guard = states.cached_state.get::<SurfaceCachedState>();
|
||||
let data = guard.current();
|
||||
(data.min_size, data.max_size)
|
||||
})
|
||||
} else {
|
||||
((0, 0).into(), (0, 0).into())
|
||||
};
|
||||
|
||||
let min_width = min_size.w.max(1);
|
||||
let min_height = min_size.h.max(1);
|
||||
let max_width = if max_size.w == 0 { i32::MAX } else { max_size.w };
|
||||
let max_height = if max_size.h == 0 { i32::MAX } else { max_size.h };
|
||||
|
||||
new_window_width = new_window_width.max(min_width).min(max_width);
|
||||
new_window_height = new_window_height.max(min_height).min(max_height);
|
||||
|
||||
self.last_window_size = (new_window_width, new_window_height).into();
|
||||
|
||||
match self.window.0.underlying_surface() {
|
||||
WindowSurface::Wayland(xdg) => {
|
||||
xdg.with_pending_state(|state| {
|
||||
state.states.set(xdg_toplevel::State::Resizing);
|
||||
state.size = Some(self.last_window_size);
|
||||
});
|
||||
xdg.send_pending_configure();
|
||||
}
|
||||
#[cfg(feature = "xwayland")]
|
||||
WindowSurface::X11(x11) => {
|
||||
let location = data.space.element_location(&self.window).unwrap();
|
||||
x11.configure_with_sync(Rectangle::new(location, self.last_window_size), None)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn frame(
|
||||
&mut self,
|
||||
_data: &mut AnvilState<BackendData>,
|
||||
_handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState<BackendData>>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn cancel(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState<BackendData>>,
|
||||
) {
|
||||
handle.cancel(data);
|
||||
handle.unset_grab(self, data);
|
||||
}
|
||||
|
||||
fn shape(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &smithay::input::touch::ShapeEvent,
|
||||
) {
|
||||
handle.shape(data, event);
|
||||
}
|
||||
|
||||
fn orientation(
|
||||
&mut self,
|
||||
data: &mut AnvilState<BackendData>,
|
||||
handle: &mut smithay::input::touch::TouchInnerHandle<'_, AnvilState<BackendData>>,
|
||||
event: &smithay::input::touch::OrientationEvent,
|
||||
) {
|
||||
handle.orientation(data, event);
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &smithay::input::touch::GrabStartData<AnvilState<BackendData>> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, _data: &mut AnvilState<BackendData>) {}
|
||||
}
|
||||
+475
@@ -0,0 +1,475 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[cfg(feature = "xwayland")]
|
||||
use smithay::xwayland::XWaylandClientData;
|
||||
|
||||
#[cfg(feature = "udev")]
|
||||
use smithay::wayland::drm_syncobj::DrmSyncobjCachedState;
|
||||
|
||||
use smithay::{
|
||||
backend::renderer::utils::on_commit_buffer_handler,
|
||||
desktop::{
|
||||
LayerSurface, PopupKind, PopupManager, Space, WindowSurfaceType, layer_map_for_output,
|
||||
space::SpaceElement,
|
||||
},
|
||||
input::pointer::{CursorImageStatus, CursorImageSurfaceData},
|
||||
output::Output,
|
||||
reexports::{
|
||||
calloop::Interest,
|
||||
wayland_server::{
|
||||
Client, Resource,
|
||||
protocol::{wl_buffer::WlBuffer, wl_output, wl_surface::WlSurface},
|
||||
},
|
||||
},
|
||||
utils::{IsAlive, Logical, Point, Rectangle, Size},
|
||||
wayland::{
|
||||
buffer::BufferHandler,
|
||||
compositor::{
|
||||
BufferAssignment, CompositorClientState, CompositorHandler, CompositorState, SurfaceAttributes,
|
||||
TraversalAction, add_blocker, add_pre_commit_hook, get_parent, is_sync_subsurface, with_states,
|
||||
with_surface_tree_upward,
|
||||
},
|
||||
dmabuf::get_dmabuf,
|
||||
shell::{
|
||||
wlr_layer::{
|
||||
Layer, LayerSurface as WlrLayerSurface, LayerSurfaceData, WlrLayerShellHandler,
|
||||
WlrLayerShellState,
|
||||
},
|
||||
xdg::XdgToplevelSurfaceData,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
ClientState,
|
||||
state::{AnvilState, Backend},
|
||||
};
|
||||
|
||||
mod element;
|
||||
mod grabs;
|
||||
pub(crate) mod ssd;
|
||||
#[cfg(feature = "xwayland")]
|
||||
mod x11;
|
||||
mod xdg;
|
||||
|
||||
pub use self::element::*;
|
||||
pub use self::grabs::*;
|
||||
|
||||
fn fullscreen_output_geometry(
|
||||
wl_surface: &WlSurface,
|
||||
wl_output: Option<&wl_output::WlOutput>,
|
||||
space: &mut Space<WindowElement>,
|
||||
) -> Option<Rectangle<i32, Logical>> {
|
||||
// First test if a specific output has been requested
|
||||
// if the requested output is not found ignore the request
|
||||
wl_output
|
||||
.and_then(Output::from_resource)
|
||||
.or_else(|| {
|
||||
let w = space
|
||||
.elements()
|
||||
.find(|window| window.wl_surface().map(|s| &*s == wl_surface).unwrap_or(false));
|
||||
w.and_then(|w| space.outputs_for_element(w).first().cloned())
|
||||
})
|
||||
.as_ref()
|
||||
.and_then(|o| space.output_geometry(o))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FullscreenSurface(RefCell<Option<WindowElement>>);
|
||||
|
||||
impl FullscreenSurface {
|
||||
pub fn set(&self, window: WindowElement) {
|
||||
*self.0.borrow_mut() = Some(window);
|
||||
}
|
||||
|
||||
pub fn get(&self) -> Option<WindowElement> {
|
||||
let mut window = self.0.borrow_mut();
|
||||
if window.as_ref().map(|w| !w.alive()).unwrap_or(false) {
|
||||
*window = None;
|
||||
}
|
||||
window.clone()
|
||||
}
|
||||
|
||||
pub fn clear(&self) -> Option<WindowElement> {
|
||||
self.0.borrow_mut().take()
|
||||
}
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> BufferHandler for AnvilState<BackendData> {
|
||||
fn buffer_destroyed(&mut self, _buffer: &WlBuffer) {}
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> CompositorHandler for AnvilState<BackendData> {
|
||||
fn compositor_state(&mut self) -> &mut CompositorState {
|
||||
&mut self.compositor_state
|
||||
}
|
||||
fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
|
||||
#[cfg(feature = "xwayland")]
|
||||
if let Some(state) = client.get_data::<XWaylandClientData>() {
|
||||
return &state.compositor_state;
|
||||
}
|
||||
if let Some(state) = client.get_data::<ClientState>() {
|
||||
return &state.compositor_state;
|
||||
}
|
||||
panic!("Unknown client data type")
|
||||
}
|
||||
|
||||
fn new_surface(&mut self, surface: &WlSurface) {
|
||||
add_pre_commit_hook::<Self, _>(surface, move |state, _dh, surface| {
|
||||
#[cfg(feature = "udev")]
|
||||
let mut acquire_point = None;
|
||||
let maybe_dmabuf = with_states(surface, |surface_data| {
|
||||
#[cfg(feature = "udev")]
|
||||
acquire_point.clone_from(
|
||||
&surface_data
|
||||
.cached_state
|
||||
.get::<DrmSyncobjCachedState>()
|
||||
.pending()
|
||||
.acquire_point,
|
||||
);
|
||||
surface_data
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.pending()
|
||||
.buffer
|
||||
.as_ref()
|
||||
.and_then(|assignment| match assignment {
|
||||
BufferAssignment::NewBuffer(buffer) => get_dmabuf(buffer).cloned().ok(),
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
if let Some(dmabuf) = maybe_dmabuf {
|
||||
#[cfg(feature = "udev")]
|
||||
if let Some(acquire_point) = acquire_point {
|
||||
if let Ok((blocker, source)) = acquire_point.generate_blocker() {
|
||||
let client = surface.client().unwrap();
|
||||
let res = state.handle.insert_source(source, move |_, _, data| {
|
||||
let dh = data.display_handle.clone();
|
||||
data.client_compositor_state(&client).blocker_cleared(data, &dh);
|
||||
Ok(())
|
||||
});
|
||||
if res.is_ok() {
|
||||
add_blocker(surface, blocker);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok((blocker, source)) = dmabuf.generate_blocker(Interest::READ) {
|
||||
if let Some(client) = surface.client() {
|
||||
let res = state.handle.insert_source(source, move |_, _, data| {
|
||||
let dh = data.display_handle.clone();
|
||||
data.client_compositor_state(&client).blocker_cleared(data, &dh);
|
||||
Ok(())
|
||||
});
|
||||
if res.is_ok() {
|
||||
add_blocker(surface, blocker);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn commit(&mut self, surface: &WlSurface) {
|
||||
on_commit_buffer_handler::<Self>(surface);
|
||||
self.backend_data.early_import(surface);
|
||||
|
||||
if !is_sync_subsurface(surface) {
|
||||
let mut root = surface.clone();
|
||||
while let Some(parent) = get_parent(&root) {
|
||||
root = parent;
|
||||
}
|
||||
if let Some(window) = self.window_for_surface(&root) {
|
||||
window.0.on_commit();
|
||||
|
||||
if &root == surface {
|
||||
let buffer_offset = with_states(surface, |states| {
|
||||
states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.current()
|
||||
.buffer_delta
|
||||
.take()
|
||||
});
|
||||
|
||||
if let Some(buffer_offset) = buffer_offset {
|
||||
let current_loc = self.space.element_location(&window).unwrap();
|
||||
self.space.map_element(window, current_loc + buffer_offset, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.popups.commit(surface);
|
||||
|
||||
if matches!(&self.cursor_status, CursorImageStatus::Surface(cursor_surface) if cursor_surface == surface)
|
||||
{
|
||||
with_states(surface, |states| {
|
||||
let cursor_image_attributes = states.data_map.get::<CursorImageSurfaceData>();
|
||||
|
||||
if let Some(mut cursor_image_attributes) =
|
||||
cursor_image_attributes.map(|attrs| attrs.lock().unwrap())
|
||||
{
|
||||
let buffer_delta = states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.current()
|
||||
.buffer_delta
|
||||
.take();
|
||||
if let Some(buffer_delta) = buffer_delta {
|
||||
tracing::trace!(hotspot = ?cursor_image_attributes.hotspot, ?buffer_delta, "decrementing cursor hotspot");
|
||||
cursor_image_attributes.hotspot -= buffer_delta;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if matches!(&self.dnd_icon, Some(icon) if &icon.surface == surface) {
|
||||
let dnd_icon = self.dnd_icon.as_mut().unwrap();
|
||||
with_states(&dnd_icon.surface, |states| {
|
||||
let buffer_delta = states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.current()
|
||||
.buffer_delta
|
||||
.take()
|
||||
.unwrap_or_default();
|
||||
tracing::trace!(offset = ?dnd_icon.offset, ?buffer_delta, "moving dnd offset");
|
||||
dnd_icon.offset += buffer_delta;
|
||||
});
|
||||
}
|
||||
|
||||
ensure_initial_configure(surface, &self.space, &mut self.popups)
|
||||
}
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> WlrLayerShellHandler for AnvilState<BackendData> {
|
||||
fn shell_state(&mut self) -> &mut WlrLayerShellState {
|
||||
&mut self.layer_shell_state
|
||||
}
|
||||
|
||||
fn new_layer_surface(
|
||||
&mut self,
|
||||
surface: WlrLayerSurface,
|
||||
wl_output: Option<wl_output::WlOutput>,
|
||||
_layer: Layer,
|
||||
namespace: String,
|
||||
) {
|
||||
let output = wl_output
|
||||
.as_ref()
|
||||
.and_then(Output::from_resource)
|
||||
.unwrap_or_else(|| self.space.outputs().next().unwrap().clone());
|
||||
let mut map = layer_map_for_output(&output);
|
||||
map.map_layer(&LayerSurface::new(surface, namespace)).unwrap();
|
||||
}
|
||||
|
||||
fn layer_destroyed(&mut self, surface: WlrLayerSurface) {
|
||||
if let Some((mut map, layer)) = self.space.outputs().find_map(|o| {
|
||||
let map = layer_map_for_output(o);
|
||||
let layer = map
|
||||
.layers()
|
||||
.find(|&layer| layer.layer_surface() == &surface)
|
||||
.cloned();
|
||||
layer.map(|layer| (map, layer))
|
||||
}) {
|
||||
map.unmap_layer(&layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> AnvilState<BackendData> {
|
||||
pub fn window_for_surface(&self, surface: &WlSurface) -> Option<WindowElement> {
|
||||
self.space
|
||||
.elements()
|
||||
.find(|window| window.wl_surface().map(|s| &*s == surface).unwrap_or(false))
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SurfaceData {
|
||||
pub geometry: Option<Rectangle<i32, Logical>>,
|
||||
pub resize_state: ResizeState,
|
||||
}
|
||||
|
||||
fn ensure_initial_configure(surface: &WlSurface, space: &Space<WindowElement>, popups: &mut PopupManager) {
|
||||
with_surface_tree_upward(
|
||||
surface,
|
||||
(),
|
||||
|_, _, _| TraversalAction::DoChildren(()),
|
||||
|_, states, _| {
|
||||
states
|
||||
.data_map
|
||||
.insert_if_missing(|| RefCell::new(SurfaceData::default()));
|
||||
},
|
||||
|_, _, _| true,
|
||||
);
|
||||
|
||||
if let Some(window) = space
|
||||
.elements()
|
||||
.find(|window| window.wl_surface().map(|s| &*s == surface).unwrap_or(false))
|
||||
.cloned()
|
||||
{
|
||||
// send the initial configure if relevant
|
||||
#[cfg_attr(not(feature = "xwayland"), allow(irrefutable_let_patterns))]
|
||||
if let Some(toplevel) = window.0.toplevel() {
|
||||
let initial_configure_sent = with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.initial_configure_sent
|
||||
});
|
||||
if !initial_configure_sent {
|
||||
toplevel.send_configure();
|
||||
}
|
||||
}
|
||||
|
||||
with_states(surface, |states| {
|
||||
let mut data = states
|
||||
.data_map
|
||||
.get::<RefCell<SurfaceData>>()
|
||||
.unwrap()
|
||||
.borrow_mut();
|
||||
|
||||
// Finish resizing.
|
||||
if let ResizeState::WaitingForCommit(_) = data.resize_state {
|
||||
data.resize_state = ResizeState::NotResizing;
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(popup) = popups.find_popup(surface) {
|
||||
let popup = match popup {
|
||||
PopupKind::Xdg(ref popup) => popup,
|
||||
// Doesn't require configure
|
||||
PopupKind::InputMethod(ref _input_popup) => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !popup.is_initial_configure_sent() {
|
||||
// NOTE: This should never fail as the initial configure is always
|
||||
// allowed.
|
||||
popup.send_configure().expect("initial configure failed");
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(output) = space.outputs().find(|o| {
|
||||
let map = layer_map_for_output(o);
|
||||
map.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
|
||||
.is_some()
|
||||
}) {
|
||||
let initial_configure_sent = with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<LayerSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.initial_configure_sent
|
||||
});
|
||||
|
||||
let mut map = layer_map_for_output(output);
|
||||
|
||||
// arrange the layers before sending the initial configure
|
||||
// to respect any size the client may have sent
|
||||
map.arrange();
|
||||
// send the initial configure if relevant
|
||||
if !initial_configure_sent {
|
||||
let layer = map
|
||||
.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
|
||||
.unwrap();
|
||||
|
||||
layer.layer_surface().send_configure();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn place_new_window(
|
||||
space: &mut Space<WindowElement>,
|
||||
pointer_location: Point<f64, Logical>,
|
||||
window: &WindowElement,
|
||||
activate: bool,
|
||||
) {
|
||||
// place the window at a random location on same output as pointer
|
||||
// or if there is not output in a [0;800]x[0;800] square
|
||||
use rand::distributions::{Distribution, Uniform};
|
||||
|
||||
let output = space
|
||||
.output_under(pointer_location)
|
||||
.next()
|
||||
.or_else(|| space.outputs().next())
|
||||
.cloned();
|
||||
let output_geometry = output
|
||||
.and_then(|o| {
|
||||
let geo = space.output_geometry(&o)?;
|
||||
let map = layer_map_for_output(&o);
|
||||
let zone = map.non_exclusive_zone();
|
||||
Some(Rectangle::new(geo.loc + zone.loc, zone.size))
|
||||
})
|
||||
.unwrap_or_else(|| Rectangle::from_size((800, 800).into()));
|
||||
|
||||
// set the initial toplevel bounds
|
||||
#[allow(irrefutable_let_patterns)]
|
||||
if let Some(toplevel) = window.0.toplevel() {
|
||||
toplevel.with_pending_state(|state| {
|
||||
state.bounds = Some(output_geometry.size);
|
||||
});
|
||||
}
|
||||
|
||||
let max_x = output_geometry.loc.x + (((output_geometry.size.w as f32) / 3.0) * 2.0) as i32;
|
||||
let max_y = output_geometry.loc.y + (((output_geometry.size.h as f32) / 3.0) * 2.0) as i32;
|
||||
let x_range = Uniform::new(output_geometry.loc.x, max_x);
|
||||
let y_range = Uniform::new(output_geometry.loc.y, max_y);
|
||||
let mut rng = rand::thread_rng();
|
||||
let x = x_range.sample(&mut rng);
|
||||
let y = y_range.sample(&mut rng);
|
||||
|
||||
space.map_element(window.clone(), (x, y), activate);
|
||||
}
|
||||
|
||||
pub fn fixup_positions(space: &mut Space<WindowElement>, pointer_location: Point<f64, Logical>) {
|
||||
// fixup outputs
|
||||
let mut offset = Point::<i32, Logical>::from((0, 0));
|
||||
for output in space.outputs().cloned().collect::<Vec<_>>().into_iter() {
|
||||
let size = space
|
||||
.output_geometry(&output)
|
||||
.map(|geo| geo.size)
|
||||
.unwrap_or_else(|| Size::from((0, 0)));
|
||||
space.map_output(&output, offset);
|
||||
layer_map_for_output(&output).arrange();
|
||||
offset.x += size.w;
|
||||
}
|
||||
|
||||
// fixup windows
|
||||
let mut orphaned_windows = Vec::new();
|
||||
let outputs = space
|
||||
.outputs()
|
||||
.flat_map(|o| {
|
||||
let geo = space.output_geometry(o)?;
|
||||
let map = layer_map_for_output(o);
|
||||
let zone = map.non_exclusive_zone();
|
||||
Some(Rectangle::new(geo.loc + zone.loc, zone.size))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for window in space.elements() {
|
||||
let window_location = match space.element_location(window) {
|
||||
Some(loc) => loc,
|
||||
None => continue,
|
||||
};
|
||||
let geo_loc = window.bbox().loc + window_location;
|
||||
|
||||
if !outputs.iter().any(|o_geo| o_geo.contains(geo_loc)) {
|
||||
orphaned_windows.push(window.clone());
|
||||
}
|
||||
}
|
||||
for window in orphaned_windows.into_iter() {
|
||||
place_new_window(space, pointer_location, &window, false);
|
||||
}
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
use smithay::{
|
||||
backend::renderer::{
|
||||
Renderer,
|
||||
element::{
|
||||
AsRenderElements, Kind,
|
||||
solid::{SolidColorBuffer, SolidColorRenderElement},
|
||||
},
|
||||
},
|
||||
desktop::WindowSurface,
|
||||
input::Seat,
|
||||
utils::{Logical, Point, Serial},
|
||||
wayland::shell::xdg::XdgShellHandler,
|
||||
};
|
||||
|
||||
use std::cell::{RefCell, RefMut};
|
||||
|
||||
use crate::{AnvilState, state::Backend};
|
||||
|
||||
use super::WindowElement;
|
||||
|
||||
pub struct WindowState {
|
||||
pub is_ssd: bool,
|
||||
pub header_bar: HeaderBar,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HeaderBar {
|
||||
pub pointer_loc: Option<Point<f64, Logical>>,
|
||||
pub width: u32,
|
||||
pub close_button_hover: bool,
|
||||
pub maximize_button_hover: bool,
|
||||
pub background: SolidColorBuffer,
|
||||
pub close_button: SolidColorBuffer,
|
||||
pub maximize_button: SolidColorBuffer,
|
||||
}
|
||||
|
||||
const BG_COLOR: [f32; 4] = [0.75f32, 0.9f32, 0.78f32, 1f32];
|
||||
const MAX_COLOR: [f32; 4] = [1f32, 0.965f32, 0.71f32, 1f32];
|
||||
const CLOSE_COLOR: [f32; 4] = [1f32, 0.66f32, 0.612f32, 1f32];
|
||||
const MAX_COLOR_HOVER: [f32; 4] = [0.71f32, 0.624f32, 0f32, 1f32];
|
||||
const CLOSE_COLOR_HOVER: [f32; 4] = [0.75f32, 0.11f32, 0.016f32, 1f32];
|
||||
|
||||
pub const HEADER_BAR_HEIGHT: i32 = 32;
|
||||
const BUTTON_HEIGHT: u32 = HEADER_BAR_HEIGHT as u32;
|
||||
const BUTTON_WIDTH: u32 = 32;
|
||||
|
||||
impl HeaderBar {
|
||||
pub fn pointer_enter(&mut self, loc: Point<f64, Logical>) {
|
||||
self.pointer_loc = Some(loc);
|
||||
}
|
||||
|
||||
pub fn pointer_leave(&mut self) {
|
||||
self.pointer_loc = None;
|
||||
}
|
||||
|
||||
pub fn clicked<BackendData: Backend>(
|
||||
&mut self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
state: &mut AnvilState<BackendData>,
|
||||
window: &WindowElement,
|
||||
serial: Serial,
|
||||
) {
|
||||
match self.pointer_loc.as_ref() {
|
||||
Some(loc) if loc.x >= (self.width - BUTTON_WIDTH) as f64 => {
|
||||
match window.0.underlying_surface() {
|
||||
WindowSurface::Wayland(w) => w.send_close(),
|
||||
#[cfg(feature = "xwayland")]
|
||||
WindowSurface::X11(w) => {
|
||||
let _ = w.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
Some(loc) if loc.x >= (self.width - (BUTTON_WIDTH * 2)) as f64 => {
|
||||
match window.0.underlying_surface() {
|
||||
WindowSurface::Wayland(w) => state.maximize_request(w.clone()),
|
||||
#[cfg(feature = "xwayland")]
|
||||
WindowSurface::X11(w) => {
|
||||
let surface = w.clone();
|
||||
state
|
||||
.handle
|
||||
.insert_idle(move |data| data.maximize_request_x11(&surface));
|
||||
}
|
||||
};
|
||||
}
|
||||
Some(_) => {
|
||||
match window.0.underlying_surface() {
|
||||
WindowSurface::Wayland(w) => {
|
||||
let seat = seat.clone();
|
||||
let toplevel = w.clone();
|
||||
state
|
||||
.handle
|
||||
.insert_idle(move |data| data.move_request_xdg(&toplevel, &seat, serial));
|
||||
}
|
||||
#[cfg(feature = "xwayland")]
|
||||
WindowSurface::X11(w) => {
|
||||
let window = w.clone();
|
||||
state
|
||||
.handle
|
||||
.insert_idle(move |data| data.move_request_x11(&window));
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn touch_down<BackendData: Backend>(
|
||||
&mut self,
|
||||
seat: &Seat<AnvilState<BackendData>>,
|
||||
state: &mut AnvilState<BackendData>,
|
||||
window: &WindowElement,
|
||||
serial: Serial,
|
||||
) {
|
||||
match self.pointer_loc.as_ref() {
|
||||
Some(loc) if loc.x >= (self.width - BUTTON_WIDTH) as f64 => {}
|
||||
Some(loc) if loc.x >= (self.width - (BUTTON_WIDTH * 2)) as f64 => {}
|
||||
Some(_) => {
|
||||
match window.0.underlying_surface() {
|
||||
WindowSurface::Wayland(w) => {
|
||||
let seat = seat.clone();
|
||||
let toplevel = w.clone();
|
||||
state
|
||||
.handle
|
||||
.insert_idle(move |data| data.move_request_xdg(&toplevel, &seat, serial));
|
||||
}
|
||||
#[cfg(feature = "xwayland")]
|
||||
WindowSurface::X11(w) => {
|
||||
let window = w.clone();
|
||||
state
|
||||
.handle
|
||||
.insert_idle(move |data| data.move_request_x11(&window));
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn touch_up<BackendData: Backend>(
|
||||
&mut self,
|
||||
_seat: &Seat<AnvilState<BackendData>>,
|
||||
state: &mut AnvilState<BackendData>,
|
||||
window: &WindowElement,
|
||||
) {
|
||||
match self.pointer_loc.as_ref() {
|
||||
Some(loc) if loc.x >= (self.width - BUTTON_WIDTH) as f64 => {
|
||||
match window.0.underlying_surface() {
|
||||
WindowSurface::Wayland(w) => w.send_close(),
|
||||
#[cfg(feature = "xwayland")]
|
||||
WindowSurface::X11(w) => {
|
||||
let _ = w.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
Some(loc) if loc.x >= (self.width - (BUTTON_WIDTH * 2)) as f64 => {
|
||||
match window.0.underlying_surface() {
|
||||
WindowSurface::Wayland(w) => state.maximize_request(w.clone()),
|
||||
#[cfg(feature = "xwayland")]
|
||||
WindowSurface::X11(w) => {
|
||||
let surface = w.clone();
|
||||
state
|
||||
.handle
|
||||
.insert_idle(move |data| data.maximize_request_x11(&surface));
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn redraw(&mut self, width: u32) {
|
||||
if width == 0 {
|
||||
self.width = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
self.background
|
||||
.update((width as i32, HEADER_BAR_HEIGHT), BG_COLOR);
|
||||
|
||||
let mut needs_redraw_buttons = false;
|
||||
if width != self.width {
|
||||
needs_redraw_buttons = true;
|
||||
self.width = width;
|
||||
}
|
||||
|
||||
if self
|
||||
.pointer_loc
|
||||
.as_ref()
|
||||
.map(|l| l.x >= (width - BUTTON_WIDTH) as f64)
|
||||
.unwrap_or(false)
|
||||
&& (needs_redraw_buttons || !self.close_button_hover)
|
||||
{
|
||||
self.close_button
|
||||
.update((BUTTON_WIDTH as i32, BUTTON_HEIGHT as i32), CLOSE_COLOR_HOVER);
|
||||
self.close_button_hover = true;
|
||||
} else if !self
|
||||
.pointer_loc
|
||||
.as_ref()
|
||||
.map(|l| l.x >= (width - BUTTON_WIDTH) as f64)
|
||||
.unwrap_or(false)
|
||||
&& (needs_redraw_buttons || self.close_button_hover)
|
||||
{
|
||||
self.close_button
|
||||
.update((BUTTON_WIDTH as i32, BUTTON_HEIGHT as i32), CLOSE_COLOR);
|
||||
self.close_button_hover = false;
|
||||
}
|
||||
|
||||
if self
|
||||
.pointer_loc
|
||||
.as_ref()
|
||||
.map(|l| l.x >= (width - BUTTON_WIDTH * 2) as f64 && l.x <= (width - BUTTON_WIDTH) as f64)
|
||||
.unwrap_or(false)
|
||||
&& (needs_redraw_buttons || !self.maximize_button_hover)
|
||||
{
|
||||
self.maximize_button
|
||||
.update((BUTTON_WIDTH as i32, BUTTON_HEIGHT as i32), MAX_COLOR_HOVER);
|
||||
self.maximize_button_hover = true;
|
||||
} else if !self
|
||||
.pointer_loc
|
||||
.as_ref()
|
||||
.map(|l| l.x >= (width - BUTTON_WIDTH * 2) as f64 && l.x <= (width - BUTTON_WIDTH) as f64)
|
||||
.unwrap_or(false)
|
||||
&& (needs_redraw_buttons || self.maximize_button_hover)
|
||||
{
|
||||
self.maximize_button
|
||||
.update((BUTTON_WIDTH as i32, BUTTON_HEIGHT as i32), MAX_COLOR);
|
||||
self.maximize_button_hover = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Renderer> AsRenderElements<R> for HeaderBar {
|
||||
type RenderElement = SolidColorRenderElement;
|
||||
|
||||
fn render_elements<C: From<Self::RenderElement>>(
|
||||
&self,
|
||||
_renderer: &mut R,
|
||||
location: Point<i32, smithay::utils::Physical>,
|
||||
scale: smithay::utils::Scale<f64>,
|
||||
alpha: f32,
|
||||
) -> Vec<C> {
|
||||
let header_end_offset: Point<i32, Logical> = Point::from((self.width as i32, 0));
|
||||
let button_offset: Point<i32, Logical> = Point::from((BUTTON_WIDTH as i32, 0));
|
||||
|
||||
vec![
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&self.close_button,
|
||||
location + (header_end_offset - button_offset).to_physical_precise_round(scale),
|
||||
scale,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
SolidColorRenderElement::from_buffer(
|
||||
&self.maximize_button,
|
||||
location + (header_end_offset - button_offset.upscale(2)).to_physical_precise_round(scale),
|
||||
scale,
|
||||
alpha,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
.into(),
|
||||
SolidColorRenderElement::from_buffer(&self.background, location, scale, alpha, Kind::Unspecified)
|
||||
.into(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowElement {
|
||||
pub fn decoration_state(&self) -> RefMut<'_, WindowState> {
|
||||
self.user_data().insert_if_missing(|| {
|
||||
RefCell::new(WindowState {
|
||||
is_ssd: false,
|
||||
header_bar: HeaderBar {
|
||||
pointer_loc: None,
|
||||
width: 0,
|
||||
close_button_hover: false,
|
||||
maximize_button_hover: false,
|
||||
background: SolidColorBuffer::default(),
|
||||
close_button: SolidColorBuffer::default(),
|
||||
maximize_button: SolidColorBuffer::default(),
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
self.user_data()
|
||||
.get::<RefCell<WindowState>>()
|
||||
.unwrap()
|
||||
.borrow_mut()
|
||||
}
|
||||
|
||||
pub fn set_ssd(&self, ssd: bool) {
|
||||
self.decoration_state().is_ssd = ssd;
|
||||
}
|
||||
}
|
||||
+431
@@ -0,0 +1,431 @@
|
||||
use std::{cell::RefCell, os::unix::io::OwnedFd};
|
||||
|
||||
use smithay::{
|
||||
desktop::{Window, space::SpaceElement},
|
||||
input::pointer::Focus,
|
||||
utils::{Logical, Rectangle, SERIAL_COUNTER},
|
||||
wayland::{
|
||||
compositor::with_states,
|
||||
selection::{
|
||||
SelectionTarget,
|
||||
data_device::{
|
||||
clear_data_device_selection, current_data_device_selection_userdata,
|
||||
request_data_device_client_selection, set_data_device_selection,
|
||||
},
|
||||
primary_selection::{
|
||||
clear_primary_selection, current_primary_selection_userdata,
|
||||
request_primary_client_selection, set_primary_selection,
|
||||
},
|
||||
},
|
||||
xwayland_shell::{XWaylandShellHandler, XWaylandShellState},
|
||||
},
|
||||
xwayland::{
|
||||
X11Surface, X11Wm, XwmHandler,
|
||||
xwm::{Reorder, ResizeEdge as X11ResizeEdge, XwmId},
|
||||
},
|
||||
};
|
||||
use tracing::{error, trace};
|
||||
|
||||
use crate::{AnvilState, focus::KeyboardFocusTarget, state::Backend};
|
||||
|
||||
use super::{
|
||||
FullscreenSurface, PointerMoveSurfaceGrab, PointerResizeSurfaceGrab, ResizeData, ResizeState,
|
||||
SurfaceData, TouchMoveSurfaceGrab, WindowElement, place_new_window,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct OldGeometry(RefCell<Option<Rectangle<i32, Logical>>>);
|
||||
impl OldGeometry {
|
||||
pub fn save(&self, geo: Rectangle<i32, Logical>) {
|
||||
*self.0.borrow_mut() = Some(geo);
|
||||
}
|
||||
|
||||
pub fn restore(&self) -> Option<Rectangle<i32, Logical>> {
|
||||
self.0.borrow_mut().take()
|
||||
}
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> XWaylandShellHandler for AnvilState<BackendData> {
|
||||
fn xwayland_shell_state(&mut self) -> &mut XWaylandShellState {
|
||||
&mut self.xwayland_shell_state
|
||||
}
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> XwmHandler for AnvilState<BackendData> {
|
||||
fn xwm_state(&mut self, _xwm: XwmId) -> &mut X11Wm {
|
||||
self.xwm.as_mut().unwrap()
|
||||
}
|
||||
|
||||
fn new_window(&mut self, _xwm: XwmId, _window: X11Surface) {}
|
||||
fn new_override_redirect_window(&mut self, _xwm: XwmId, _window: X11Surface) {}
|
||||
|
||||
fn map_window_request(&mut self, _xwm: XwmId, window: X11Surface) {
|
||||
window.set_mapped(true).unwrap();
|
||||
let window = WindowElement(Window::new_x11_window(window));
|
||||
place_new_window(&mut self.space, self.pointer.current_location(), &window, true);
|
||||
let bbox = self.space.element_bbox(&window).unwrap();
|
||||
let Some(xsurface) = window.0.x11_surface() else {
|
||||
unreachable!()
|
||||
};
|
||||
xsurface.configure(Some(bbox)).unwrap();
|
||||
window.set_ssd(!xsurface.is_decorated());
|
||||
}
|
||||
|
||||
fn mapped_override_redirect_window(&mut self, _xwm: XwmId, window: X11Surface) {
|
||||
let location = window.last_configure().loc;
|
||||
let window = WindowElement(Window::new_x11_window(window));
|
||||
self.space.map_element(window, location, true);
|
||||
}
|
||||
|
||||
fn unmapped_window(&mut self, _xwm: XwmId, window: X11Surface) {
|
||||
let maybe = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|e| matches!(e.0.x11_surface(), Some(w) if w == &window))
|
||||
.cloned();
|
||||
if let Some(elem) = maybe {
|
||||
self.space.unmap_elem(&elem)
|
||||
}
|
||||
if !window.is_override_redirect() {
|
||||
window.set_mapped(false).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn destroyed_window(&mut self, _xwm: XwmId, _window: X11Surface) {}
|
||||
|
||||
fn configure_request(
|
||||
&mut self,
|
||||
_xwm: XwmId,
|
||||
window: X11Surface,
|
||||
_x: Option<i32>,
|
||||
_y: Option<i32>,
|
||||
w: Option<u32>,
|
||||
h: Option<u32>,
|
||||
_reorder: Option<Reorder>,
|
||||
) {
|
||||
// we just set the new size, but don't let windows move themselves around freely
|
||||
let mut geo = window.last_configure();
|
||||
if let Some(w) = w {
|
||||
geo.size.w = w as i32;
|
||||
}
|
||||
if let Some(h) = h {
|
||||
geo.size.h = h as i32;
|
||||
}
|
||||
let _ = window.configure(geo);
|
||||
}
|
||||
|
||||
fn configure_notify(
|
||||
&mut self,
|
||||
_xwm: XwmId,
|
||||
window: X11Surface,
|
||||
geometry: Rectangle<i32, Logical>,
|
||||
_above: Option<u32>,
|
||||
) {
|
||||
let Some(elem) = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|e| matches!(e.0.x11_surface(), Some(w) if w == &window))
|
||||
.cloned()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.space.map_element(elem, geometry.loc, false);
|
||||
// TODO: We don't properly handle the order of override-redirect windows here,
|
||||
// they are always mapped top and then never reordered.
|
||||
}
|
||||
|
||||
fn maximize_request(&mut self, _xwm: XwmId, window: X11Surface) {
|
||||
self.maximize_request_x11(&window);
|
||||
}
|
||||
|
||||
fn unmaximize_request(&mut self, _xwm: XwmId, window: X11Surface) {
|
||||
let Some(elem) = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|e| matches!(e.0.x11_surface(), Some(w) if w == &window))
|
||||
.cloned()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
window.set_maximized(false).unwrap();
|
||||
if let Some(old_geo) = window
|
||||
.user_data()
|
||||
.get::<OldGeometry>()
|
||||
.and_then(|data| data.restore())
|
||||
{
|
||||
window.configure(old_geo).unwrap();
|
||||
self.space.map_element(elem, old_geo.loc, false);
|
||||
}
|
||||
}
|
||||
|
||||
fn fullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {
|
||||
if let Some(elem) = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|e| matches!(e.0.x11_surface(), Some(w) if w == &window))
|
||||
{
|
||||
let outputs_for_window = self.space.outputs_for_element(elem);
|
||||
let output = outputs_for_window
|
||||
.first()
|
||||
// The window hasn't been mapped yet, use the primary output instead
|
||||
.or_else(|| self.space.outputs().next())
|
||||
// Assumes that at least one output exists
|
||||
.expect("No outputs found");
|
||||
let geometry = self.space.output_geometry(output).unwrap();
|
||||
|
||||
window.set_fullscreen(true).unwrap();
|
||||
elem.set_ssd(false);
|
||||
window.configure(geometry).unwrap();
|
||||
output.user_data().insert_if_missing(FullscreenSurface::default);
|
||||
output
|
||||
.user_data()
|
||||
.get::<FullscreenSurface>()
|
||||
.unwrap()
|
||||
.set(elem.clone());
|
||||
trace!("Fullscreening: {:?}", elem);
|
||||
}
|
||||
}
|
||||
|
||||
fn unfullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {
|
||||
if let Some(elem) = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|e| matches!(e.0.x11_surface(), Some(w) if w == &window))
|
||||
{
|
||||
window.set_fullscreen(false).unwrap();
|
||||
elem.set_ssd(!window.is_decorated());
|
||||
if let Some(output) = self.space.outputs().find(|o| {
|
||||
o.user_data()
|
||||
.get::<FullscreenSurface>()
|
||||
.and_then(|f| f.get())
|
||||
.map(|w| &w == elem)
|
||||
.unwrap_or(false)
|
||||
}) {
|
||||
trace!("Unfullscreening: {:?}", elem);
|
||||
output.user_data().get::<FullscreenSurface>().unwrap().clear();
|
||||
window.configure(self.space.element_bbox(elem)).unwrap();
|
||||
self.backend_data.reset_buffers(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32, edges: X11ResizeEdge) {
|
||||
// luckily anvil only supports one seat anyway...
|
||||
let start_data = self.pointer.grab_start_data().unwrap();
|
||||
|
||||
let Some(element) = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|e| matches!(e.0.x11_surface(), Some(w) if w == &window))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let geometry = element.geometry();
|
||||
let loc = self.space.element_location(element).unwrap();
|
||||
let (initial_window_location, initial_window_size) = (loc, geometry.size);
|
||||
|
||||
with_states(&element.wl_surface().unwrap(), move |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<RefCell<SurfaceData>>()
|
||||
.unwrap()
|
||||
.borrow_mut()
|
||||
.resize_state = ResizeState::Resizing(ResizeData {
|
||||
edges: edges.into(),
|
||||
initial_window_location,
|
||||
initial_window_size,
|
||||
});
|
||||
});
|
||||
|
||||
let grab = PointerResizeSurfaceGrab {
|
||||
start_data,
|
||||
window: element.clone(),
|
||||
edges: edges.into(),
|
||||
initial_window_location,
|
||||
initial_window_size,
|
||||
last_window_size: initial_window_size,
|
||||
};
|
||||
|
||||
let pointer = self.pointer.clone();
|
||||
pointer.set_grab(self, grab, SERIAL_COUNTER.next_serial(), Focus::Clear);
|
||||
}
|
||||
|
||||
fn move_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32) {
|
||||
self.move_request_x11(&window)
|
||||
}
|
||||
|
||||
fn allow_selection_access(&mut self, xwm: XwmId, _selection: SelectionTarget) -> bool {
|
||||
if let Some(keyboard) = self.seat.get_keyboard() {
|
||||
// check that an X11 window is focused
|
||||
if let Some(KeyboardFocusTarget::Window(w)) = keyboard.current_focus() {
|
||||
if let Some(surface) = w.x11_surface() {
|
||||
if surface.xwm_id().unwrap() == xwm {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn send_selection(&mut self, _xwm: XwmId, selection: SelectionTarget, mime_type: String, fd: OwnedFd) {
|
||||
match selection {
|
||||
SelectionTarget::Clipboard => {
|
||||
if let Err(err) = request_data_device_client_selection(&self.seat, mime_type, fd) {
|
||||
error!(?err, "Failed to request current wayland clipboard for Xwayland",);
|
||||
}
|
||||
}
|
||||
SelectionTarget::Primary => {
|
||||
if let Err(err) = request_primary_client_selection(&self.seat, mime_type, fd) {
|
||||
error!(
|
||||
?err,
|
||||
"Failed to request current wayland primary selection for Xwayland",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_selection(&mut self, _xwm: XwmId, selection: SelectionTarget, mime_types: Vec<String>) {
|
||||
trace!(?selection, ?mime_types, "Got Selection from X11",);
|
||||
// TODO check, that focused windows is X11 window before doing this
|
||||
match selection {
|
||||
SelectionTarget::Clipboard => {
|
||||
set_data_device_selection(&self.display_handle, &self.seat, mime_types, ())
|
||||
}
|
||||
SelectionTarget::Primary => {
|
||||
set_primary_selection(&self.display_handle, &self.seat, mime_types, ())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cleared_selection(&mut self, _xwm: XwmId, selection: SelectionTarget) {
|
||||
match selection {
|
||||
SelectionTarget::Clipboard => {
|
||||
if current_data_device_selection_userdata(&self.seat).is_some() {
|
||||
clear_data_device_selection(&self.display_handle, &self.seat)
|
||||
}
|
||||
}
|
||||
SelectionTarget::Primary => {
|
||||
if current_primary_selection_userdata(&self.seat).is_some() {
|
||||
clear_primary_selection(&self.display_handle, &self.seat)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn disconnected(&mut self, _xwm: XwmId) {
|
||||
self.xwm = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> AnvilState<BackendData> {
|
||||
pub fn maximize_request_x11(&mut self, window: &X11Surface) {
|
||||
let Some(elem) = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|e| matches!(e.0.x11_surface(), Some(w) if w == window))
|
||||
.cloned()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let old_geo = self.space.element_bbox(&elem).unwrap();
|
||||
let outputs_for_window = self.space.outputs_for_element(&elem);
|
||||
let output = outputs_for_window
|
||||
.first()
|
||||
// The window hasn't been mapped yet, use the primary output instead
|
||||
.or_else(|| self.space.outputs().next())
|
||||
// Assumes that at least one output exists
|
||||
.expect("No outputs found");
|
||||
let geometry = self.space.output_geometry(output).unwrap();
|
||||
|
||||
window.set_maximized(true).unwrap();
|
||||
window.configure(geometry).unwrap();
|
||||
window.user_data().insert_if_missing(OldGeometry::default);
|
||||
window.user_data().get::<OldGeometry>().unwrap().save(old_geo);
|
||||
self.space.map_element(elem, geometry.loc, false);
|
||||
}
|
||||
|
||||
pub fn move_request_x11(&mut self, window: &X11Surface) {
|
||||
if let Some(touch) = self.seat.get_touch() {
|
||||
if let Some(start_data) = touch.grab_start_data() {
|
||||
let element = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|e| matches!(e.0.x11_surface(), Some(w) if w == window));
|
||||
|
||||
if let Some(element) = element {
|
||||
let mut initial_window_location = self.space.element_location(element).unwrap();
|
||||
|
||||
// If surface is maximized then unmaximize it
|
||||
if window.is_maximized() {
|
||||
window.set_maximized(false).unwrap();
|
||||
let pos = start_data.location;
|
||||
initial_window_location = (pos.x as i32, pos.y as i32).into();
|
||||
if let Some(old_geo) = window
|
||||
.user_data()
|
||||
.get::<OldGeometry>()
|
||||
.and_then(|data| data.restore())
|
||||
{
|
||||
window
|
||||
.configure(Rectangle::new(initial_window_location, old_geo.size))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let grab = TouchMoveSurfaceGrab {
|
||||
start_data,
|
||||
window: element.clone(),
|
||||
initial_window_location,
|
||||
};
|
||||
|
||||
touch.set_grab(self, grab, SERIAL_COUNTER.next_serial());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// luckily anvil only supports one seat anyway...
|
||||
let Some(start_data) = self.pointer.grab_start_data() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(element) = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|e| matches!(e.0.x11_surface(), Some(w) if w == window))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut initial_window_location = self.space.element_location(element).unwrap();
|
||||
|
||||
// If surface is maximized then unmaximize it
|
||||
if window.is_maximized() {
|
||||
window.set_maximized(false).unwrap();
|
||||
let pos = self.pointer.current_location();
|
||||
initial_window_location = (pos.x as i32, pos.y as i32).into();
|
||||
if let Some(old_geo) = window
|
||||
.user_data()
|
||||
.get::<OldGeometry>()
|
||||
.and_then(|data| data.restore())
|
||||
{
|
||||
window
|
||||
.configure(Rectangle::new(initial_window_location, old_geo.size))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let grab = PointerMoveSurfaceGrab {
|
||||
start_data,
|
||||
window: element.clone(),
|
||||
initial_window_location,
|
||||
};
|
||||
|
||||
let pointer = self.pointer.clone();
|
||||
pointer.set_grab(self, grab, SERIAL_COUNTER.next_serial(), Focus::Clear);
|
||||
}
|
||||
}
|
||||
+641
@@ -0,0 +1,641 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use smithay::{
|
||||
desktop::{
|
||||
PopupKeyboardGrab, PopupKind, PopupPointerGrab, PopupUngrabStrategy, Space, Window,
|
||||
WindowSurfaceType, find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output,
|
||||
space::SpaceElement,
|
||||
},
|
||||
input::{Seat, pointer::Focus},
|
||||
output::Output,
|
||||
reexports::{
|
||||
wayland_protocols::xdg::{decoration as xdg_decoration, shell::server::xdg_toplevel},
|
||||
wayland_server::{
|
||||
Resource,
|
||||
protocol::{wl_output, wl_seat, wl_surface::WlSurface},
|
||||
},
|
||||
},
|
||||
utils::{Logical, Point, Serial},
|
||||
wayland::{
|
||||
compositor::{self, with_states},
|
||||
seat::WaylandFocus,
|
||||
shell::xdg::{
|
||||
Configure, PopupSurface, PositionerState, ToplevelCachedState, ToplevelSurface, XdgShellHandler,
|
||||
XdgShellState,
|
||||
},
|
||||
},
|
||||
};
|
||||
use tracing::{trace, warn};
|
||||
|
||||
use crate::{
|
||||
focus::KeyboardFocusTarget,
|
||||
shell::{TouchMoveSurfaceGrab, TouchResizeSurfaceGrab},
|
||||
state::{AnvilState, Backend},
|
||||
};
|
||||
|
||||
use super::{
|
||||
FullscreenSurface, PointerMoveSurfaceGrab, PointerResizeSurfaceGrab, ResizeData, ResizeEdge, ResizeState,
|
||||
SurfaceData, WindowElement, fullscreen_output_geometry, place_new_window,
|
||||
};
|
||||
|
||||
impl<BackendData: Backend> XdgShellHandler for AnvilState<BackendData> {
|
||||
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
|
||||
&mut self.xdg_shell_state
|
||||
}
|
||||
|
||||
fn new_toplevel(&mut self, surface: ToplevelSurface) {
|
||||
// Do not send a configure here, the initial configure
|
||||
// of a xdg_surface has to be sent during the commit if
|
||||
// the surface is not already configured
|
||||
let window = WindowElement(Window::new_wayland_window(surface.clone()));
|
||||
place_new_window(&mut self.space, self.pointer.current_location(), &window, true);
|
||||
|
||||
compositor::add_post_commit_hook(surface.wl_surface(), |state: &mut Self, _, surface| {
|
||||
handle_toplevel_commit(&mut state.space, surface);
|
||||
});
|
||||
}
|
||||
|
||||
fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
|
||||
// Do not send a configure here, the initial configure
|
||||
// of a xdg_surface has to be sent during the commit if
|
||||
// the surface is not already configured
|
||||
|
||||
self.unconstrain_popup(&surface);
|
||||
|
||||
if let Err(err) = self.popups.track_popup(PopupKind::from(surface)) {
|
||||
warn!("Failed to track popup: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
fn reposition_request(&mut self, surface: PopupSurface, positioner: PositionerState, token: u32) {
|
||||
surface.with_pending_state(|state| {
|
||||
let geometry = positioner.get_geometry();
|
||||
state.geometry = geometry;
|
||||
state.positioner = positioner;
|
||||
});
|
||||
self.unconstrain_popup(&surface);
|
||||
surface.send_repositioned(token);
|
||||
}
|
||||
|
||||
fn move_request(&mut self, surface: ToplevelSurface, seat: wl_seat::WlSeat, serial: Serial) {
|
||||
let seat: Seat<AnvilState<BackendData>> = Seat::from_resource(&seat).unwrap();
|
||||
self.move_request_xdg(&surface, &seat, serial)
|
||||
}
|
||||
|
||||
fn resize_request(
|
||||
&mut self,
|
||||
surface: ToplevelSurface,
|
||||
seat: wl_seat::WlSeat,
|
||||
serial: Serial,
|
||||
edges: xdg_toplevel::ResizeEdge,
|
||||
) {
|
||||
let seat: Seat<AnvilState<BackendData>> = Seat::from_resource(&seat).unwrap();
|
||||
|
||||
if let Some(touch) = seat.get_touch() {
|
||||
if touch.has_grab(serial) {
|
||||
let start_data = touch.grab_start_data().unwrap();
|
||||
tracing::info!(?start_data);
|
||||
|
||||
// If the client disconnects after requesting a move
|
||||
// we can just ignore the request
|
||||
let Some(window) = self.window_for_surface(surface.wl_surface()) else {
|
||||
tracing::info!("no window");
|
||||
return;
|
||||
};
|
||||
|
||||
// If the focus was for a different surface, ignore the request.
|
||||
if start_data.focus.is_none()
|
||||
|| !start_data
|
||||
.focus
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.0
|
||||
.same_client_as(&surface.wl_surface().id())
|
||||
{
|
||||
tracing::info!("different surface");
|
||||
return;
|
||||
}
|
||||
let geometry = window.geometry();
|
||||
let loc = self.space.element_location(&window).unwrap();
|
||||
let (initial_window_location, initial_window_size) = (loc, geometry.size);
|
||||
|
||||
with_states(surface.wl_surface(), move |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<RefCell<SurfaceData>>()
|
||||
.unwrap()
|
||||
.borrow_mut()
|
||||
.resize_state = ResizeState::Resizing(ResizeData {
|
||||
edges: edges.into(),
|
||||
initial_window_location,
|
||||
initial_window_size,
|
||||
});
|
||||
});
|
||||
|
||||
let grab = TouchResizeSurfaceGrab {
|
||||
start_data,
|
||||
window,
|
||||
edges: edges.into(),
|
||||
initial_window_location,
|
||||
initial_window_size,
|
||||
last_window_size: initial_window_size,
|
||||
};
|
||||
|
||||
touch.set_grab(self, grab, serial);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let pointer = seat.get_pointer().unwrap();
|
||||
|
||||
// Check that this surface has a click grab.
|
||||
if !pointer.has_grab(serial) {
|
||||
return;
|
||||
}
|
||||
|
||||
let start_data = pointer.grab_start_data().unwrap();
|
||||
|
||||
let window = self.window_for_surface(surface.wl_surface()).unwrap();
|
||||
|
||||
// If the focus was for a different surface, ignore the request.
|
||||
if start_data.focus.is_none()
|
||||
|| !start_data
|
||||
.focus
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.0
|
||||
.same_client_as(&surface.wl_surface().id())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let geometry = window.geometry();
|
||||
let loc = self.space.element_location(&window).unwrap();
|
||||
let (initial_window_location, initial_window_size) = (loc, geometry.size);
|
||||
|
||||
with_states(surface.wl_surface(), move |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<RefCell<SurfaceData>>()
|
||||
.unwrap()
|
||||
.borrow_mut()
|
||||
.resize_state = ResizeState::Resizing(ResizeData {
|
||||
edges: edges.into(),
|
||||
initial_window_location,
|
||||
initial_window_size,
|
||||
});
|
||||
});
|
||||
|
||||
let grab = PointerResizeSurfaceGrab {
|
||||
start_data,
|
||||
window,
|
||||
edges: edges.into(),
|
||||
initial_window_location,
|
||||
initial_window_size,
|
||||
last_window_size: initial_window_size,
|
||||
};
|
||||
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
}
|
||||
|
||||
fn ack_configure(&mut self, surface: WlSurface, configure: Configure) {
|
||||
if let Configure::Toplevel(configure) = configure {
|
||||
if let Some(serial) = with_states(&surface, |states| {
|
||||
if let Some(data) = states.data_map.get::<RefCell<SurfaceData>>() {
|
||||
if let ResizeState::WaitingForFinalAck(_, serial) = data.borrow().resize_state {
|
||||
return Some(serial);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}) {
|
||||
// When the resize grab is released the surface
|
||||
// resize state will be set to WaitingForFinalAck
|
||||
// and the client will receive a configure request
|
||||
// without the resize state to inform the client
|
||||
// resizing has finished. Here we will wait for
|
||||
// the client to acknowledge the end of the
|
||||
// resizing. To check if the surface was resizing
|
||||
// before sending the configure we need to use
|
||||
// the current state as the received acknowledge
|
||||
// will no longer have the resize state set
|
||||
let is_resizing = with_states(&surface, |states| {
|
||||
states
|
||||
.cached_state
|
||||
.get::<ToplevelCachedState>()
|
||||
.current()
|
||||
.last_acked
|
||||
.as_ref()
|
||||
.is_some_and(|c| c.state.states.contains(xdg_toplevel::State::Resizing))
|
||||
});
|
||||
|
||||
if configure.serial >= serial && is_resizing {
|
||||
with_states(&surface, |states| {
|
||||
let mut data = states
|
||||
.data_map
|
||||
.get::<RefCell<SurfaceData>>()
|
||||
.unwrap()
|
||||
.borrow_mut();
|
||||
if let ResizeState::WaitingForFinalAck(resize_data, _) = data.resize_state {
|
||||
data.resize_state = ResizeState::WaitingForCommit(resize_data);
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let window = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|element| element.wl_surface().as_deref() == Some(&surface));
|
||||
if let Some(window) = window {
|
||||
use xdg_decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode;
|
||||
let is_ssd = configure
|
||||
.state
|
||||
.decoration_mode
|
||||
.map(|mode| mode == Mode::ServerSide)
|
||||
.unwrap_or(false);
|
||||
window.set_ssd(is_ssd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fullscreen_request(&mut self, surface: ToplevelSurface, mut wl_output: Option<wl_output::WlOutput>) {
|
||||
// NOTE: This is only one part of the solution. We can set the
|
||||
// location and configure size here, but the surface should be rendered fullscreen
|
||||
// independently from its buffer size
|
||||
let wl_surface = surface.wl_surface();
|
||||
|
||||
let output_geometry = fullscreen_output_geometry(wl_surface, wl_output.as_ref(), &mut self.space);
|
||||
|
||||
if let Some(geometry) = output_geometry {
|
||||
let output = wl_output
|
||||
.as_ref()
|
||||
.and_then(Output::from_resource)
|
||||
.unwrap_or_else(|| self.space.outputs().next().unwrap().clone());
|
||||
let client = match self.display_handle.get_client(wl_surface.id()) {
|
||||
Ok(client) => client,
|
||||
Err(_) => return,
|
||||
};
|
||||
for output in output.client_outputs(&client) {
|
||||
wl_output = Some(output);
|
||||
}
|
||||
let window = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|window| window.wl_surface().map(|s| &*s == wl_surface).unwrap_or(false))
|
||||
.unwrap();
|
||||
|
||||
surface.with_pending_state(|state| {
|
||||
state.states.set(xdg_toplevel::State::Fullscreen);
|
||||
state.size = Some(geometry.size);
|
||||
state.fullscreen_output = wl_output;
|
||||
});
|
||||
output.user_data().insert_if_missing(FullscreenSurface::default);
|
||||
output
|
||||
.user_data()
|
||||
.get::<FullscreenSurface>()
|
||||
.unwrap()
|
||||
.set(window.clone());
|
||||
trace!("Fullscreening: {:?}", window);
|
||||
}
|
||||
|
||||
// The protocol demands us to always reply with a configure,
|
||||
// regardless of we fulfilled the request or not
|
||||
if surface.is_initial_configure_sent() {
|
||||
surface.send_configure();
|
||||
} else {
|
||||
// Will be sent during initial configure
|
||||
}
|
||||
}
|
||||
|
||||
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
|
||||
let ret = surface.with_pending_state(|state| {
|
||||
state.states.unset(xdg_toplevel::State::Fullscreen);
|
||||
state.size = None;
|
||||
state.fullscreen_output.take()
|
||||
});
|
||||
if let Some(output) = ret {
|
||||
let output = Output::from_resource(&output).unwrap();
|
||||
if let Some(fullscreen) = output.user_data().get::<FullscreenSurface>() {
|
||||
trace!("Unfullscreening: {:?}", fullscreen.get());
|
||||
fullscreen.clear();
|
||||
self.backend_data.reset_buffers(&output);
|
||||
}
|
||||
}
|
||||
|
||||
// The protocol demands us to always reply with a configure,
|
||||
// regardless of we fulfilled the request or not
|
||||
if surface.is_initial_configure_sent() {
|
||||
surface.send_configure();
|
||||
} else {
|
||||
// Will be sent during initial configure
|
||||
}
|
||||
}
|
||||
|
||||
fn maximize_request(&mut self, surface: ToplevelSurface) {
|
||||
// NOTE: This should use layer-shell when it is implemented to
|
||||
// get the correct maximum size
|
||||
let window = self.window_for_surface(surface.wl_surface()).unwrap();
|
||||
let outputs_for_window = self.space.outputs_for_element(&window);
|
||||
let output = outputs_for_window
|
||||
.first()
|
||||
// The window hasn't been mapped yet, use the primary output instead
|
||||
.or_else(|| self.space.outputs().next())
|
||||
// Assumes that at least one output exists
|
||||
.expect("No outputs found");
|
||||
let geometry = self.space.output_geometry(output).unwrap();
|
||||
|
||||
surface.with_pending_state(|state| {
|
||||
state.states.set(xdg_toplevel::State::Maximized);
|
||||
state.size = Some(geometry.size);
|
||||
});
|
||||
self.space.map_element(window, geometry.loc, true);
|
||||
|
||||
// The protocol demands us to always reply with a configure,
|
||||
// regardless of we fulfilled the request or not
|
||||
if surface.is_initial_configure_sent() {
|
||||
surface.send_configure();
|
||||
} else {
|
||||
// Will be sent during initial configure
|
||||
}
|
||||
}
|
||||
|
||||
fn unmaximize_request(&mut self, surface: ToplevelSurface) {
|
||||
surface.with_pending_state(|state| {
|
||||
state.states.unset(xdg_toplevel::State::Maximized);
|
||||
state.size = None;
|
||||
});
|
||||
|
||||
// The protocol demands us to always reply with a configure,
|
||||
// regardless of we fulfilled the request or not
|
||||
if surface.is_initial_configure_sent() {
|
||||
surface.send_configure();
|
||||
} else {
|
||||
// Will be sent during initial configure
|
||||
}
|
||||
}
|
||||
|
||||
fn grab(&mut self, surface: PopupSurface, seat: wl_seat::WlSeat, serial: Serial) {
|
||||
let seat: Seat<AnvilState<BackendData>> = Seat::from_resource(&seat).unwrap();
|
||||
let kind = PopupKind::Xdg(surface);
|
||||
if let Some(root) = find_popup_root_surface(&kind).ok().and_then(|root| {
|
||||
self.space
|
||||
.elements()
|
||||
.find(|w| w.wl_surface().map(|s| *s == root).unwrap_or(false))
|
||||
.cloned()
|
||||
.map(KeyboardFocusTarget::from)
|
||||
.or_else(|| {
|
||||
self.space
|
||||
.outputs()
|
||||
.find_map(|o| {
|
||||
let map = layer_map_for_output(o);
|
||||
map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL).cloned()
|
||||
})
|
||||
.map(KeyboardFocusTarget::LayerSurface)
|
||||
})
|
||||
}) {
|
||||
let ret = self.popups.grab_popup(root, kind, &seat, serial);
|
||||
|
||||
if let Ok(mut grab) = ret {
|
||||
if let Some(keyboard) = seat.get_keyboard() {
|
||||
if keyboard.is_grabbed()
|
||||
&& !(keyboard.has_grab(serial)
|
||||
|| keyboard.has_grab(grab.previous_serial().unwrap_or(serial)))
|
||||
{
|
||||
grab.ungrab(PopupUngrabStrategy::All);
|
||||
return;
|
||||
}
|
||||
keyboard.set_focus(self, grab.current_grab(), serial);
|
||||
keyboard.set_grab(self, PopupKeyboardGrab::new(&grab), serial);
|
||||
}
|
||||
if let Some(pointer) = seat.get_pointer() {
|
||||
if pointer.is_grabbed()
|
||||
&& !(pointer.has_grab(serial)
|
||||
|| pointer.has_grab(grab.previous_serial().unwrap_or_else(|| grab.serial())))
|
||||
{
|
||||
grab.ungrab(PopupUngrabStrategy::All);
|
||||
return;
|
||||
}
|
||||
pointer.set_grab(self, PopupPointerGrab::new(&grab), serial, Focus::Keep);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<BackendData: Backend> AnvilState<BackendData> {
|
||||
pub fn move_request_xdg(&mut self, surface: &ToplevelSurface, seat: &Seat<Self>, serial: Serial) {
|
||||
if let Some(touch) = seat.get_touch() {
|
||||
if touch.has_grab(serial) {
|
||||
let start_data = touch.grab_start_data().unwrap();
|
||||
|
||||
// If the client disconnects after requesting a move
|
||||
// we can just ignore the request
|
||||
let Some(window) = self.window_for_surface(surface.wl_surface()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// If the focus was for a different surface, ignore the request.
|
||||
if start_data.focus.is_none()
|
||||
|| !start_data
|
||||
.focus
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.0
|
||||
.same_client_as(&surface.wl_surface().id())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let mut initial_window_location = self.space.element_location(&window).unwrap();
|
||||
|
||||
// If surface is maximized then unmaximize it
|
||||
let changed = surface.with_pending_state(|state| {
|
||||
if state.states.unset(xdg_toplevel::State::Maximized) {
|
||||
state.size = None;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
if changed {
|
||||
surface.send_configure();
|
||||
|
||||
// NOTE: In real compositor mouse location should be mapped to a new window size
|
||||
// For example, you could:
|
||||
// 1) transform mouse pointer position from compositor space to window space (location relative)
|
||||
// 2) divide the x coordinate by width of the window to get the percentage
|
||||
// - 0.0 would be on the far left of the window
|
||||
// - 0.5 would be in middle of the window
|
||||
// - 1.0 would be on the far right of the window
|
||||
// 3) multiply the percentage by new window width
|
||||
// 4) by doing that, drag will look a lot more natural
|
||||
//
|
||||
// but for anvil needs setting location to pointer location is fine
|
||||
initial_window_location = start_data.location.to_i32_round();
|
||||
}
|
||||
|
||||
let grab = TouchMoveSurfaceGrab {
|
||||
start_data,
|
||||
window,
|
||||
initial_window_location,
|
||||
};
|
||||
|
||||
touch.set_grab(self, grab, serial);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let pointer = seat.get_pointer().unwrap();
|
||||
|
||||
// Check that this surface has a click grab.
|
||||
if !pointer.has_grab(serial) {
|
||||
return;
|
||||
}
|
||||
|
||||
let start_data = pointer.grab_start_data().unwrap();
|
||||
|
||||
// If the client disconnects after requesting a move
|
||||
// we can just ignore the request
|
||||
let Some(window) = self.window_for_surface(surface.wl_surface()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// If the focus was for a different surface, ignore the request.
|
||||
if start_data.focus.is_none()
|
||||
|| !start_data
|
||||
.focus
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.0
|
||||
.same_client_as(&surface.wl_surface().id())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let mut initial_window_location = self.space.element_location(&window).unwrap();
|
||||
|
||||
// If surface is maximized then unmaximize it
|
||||
let changed = surface.with_pending_state(|state| {
|
||||
if state.states.unset(xdg_toplevel::State::Maximized) {
|
||||
state.size = None;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
if changed {
|
||||
surface.send_configure();
|
||||
|
||||
// NOTE: In real compositor mouse location should be mapped to a new window size
|
||||
// For example, you could:
|
||||
// 1) transform mouse pointer position from compositor space to window space (location relative)
|
||||
// 2) divide the x coordinate by width of the window to get the percentage
|
||||
// - 0.0 would be on the far left of the window
|
||||
// - 0.5 would be in middle of the window
|
||||
// - 1.0 would be on the far right of the window
|
||||
// 3) multiply the percentage by new window width
|
||||
// 4) by doing that, drag will look a lot more natural
|
||||
//
|
||||
// but for anvil needs setting location to pointer location is fine
|
||||
let pos = pointer.current_location();
|
||||
initial_window_location = (pos.x as i32, pos.y as i32).into();
|
||||
}
|
||||
|
||||
let grab = PointerMoveSurfaceGrab {
|
||||
start_data,
|
||||
window,
|
||||
initial_window_location,
|
||||
};
|
||||
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
}
|
||||
|
||||
fn unconstrain_popup(&self, popup: &PopupSurface) {
|
||||
let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else {
|
||||
return;
|
||||
};
|
||||
let Some(window) = self.window_for_surface(&root) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut outputs_for_window = self.space.outputs_for_element(&window);
|
||||
if outputs_for_window.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get a union of all outputs' geometries.
|
||||
let mut outputs_geo = self
|
||||
.space
|
||||
.output_geometry(&outputs_for_window.pop().unwrap())
|
||||
.unwrap();
|
||||
for output in outputs_for_window {
|
||||
outputs_geo = outputs_geo.merge(self.space.output_geometry(&output).unwrap());
|
||||
}
|
||||
|
||||
let window_geo = self.space.element_geometry(&window).unwrap();
|
||||
|
||||
// The target geometry for the positioner should be relative to its parent's geometry, so
|
||||
// we will compute that here.
|
||||
let mut target = outputs_geo;
|
||||
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
|
||||
target.loc -= window_geo.loc;
|
||||
|
||||
popup.with_pending_state(|state| {
|
||||
state.geometry = state.positioner.get_unconstrained_geometry(target);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Should be called on `WlSurface::commit` of xdg toplevel
|
||||
fn handle_toplevel_commit(space: &mut Space<WindowElement>, surface: &WlSurface) -> Option<()> {
|
||||
let window = space
|
||||
.elements()
|
||||
.find(|w| w.wl_surface().as_deref() == Some(surface))
|
||||
.cloned()?;
|
||||
|
||||
let mut window_loc = space.element_location(&window)?;
|
||||
let geometry = window.geometry();
|
||||
|
||||
let new_loc: Point<Option<i32>, Logical> = with_states(window.wl_surface().as_deref()?, |states| {
|
||||
let data = states.data_map.get::<RefCell<SurfaceData>>()?.borrow_mut();
|
||||
|
||||
if let ResizeState::Resizing(resize_data) = data.resize_state {
|
||||
let edges = resize_data.edges;
|
||||
let loc = resize_data.initial_window_location;
|
||||
let size = resize_data.initial_window_size;
|
||||
|
||||
// If the window is being resized by top or left, its location must be adjusted
|
||||
// accordingly.
|
||||
edges.intersects(ResizeEdge::TOP_LEFT).then(|| {
|
||||
let new_x = edges
|
||||
.intersects(ResizeEdge::LEFT)
|
||||
.then_some(loc.x + (size.w - geometry.size.w));
|
||||
|
||||
let new_y = edges
|
||||
.intersects(ResizeEdge::TOP)
|
||||
.then_some(loc.y + (size.h - geometry.size.h));
|
||||
|
||||
(new_x, new_y).into()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
|
||||
if let Some(new_x) = new_loc.x {
|
||||
window_loc.x = new_x;
|
||||
}
|
||||
if let Some(new_y) = new_loc.y {
|
||||
window_loc.y = new_y;
|
||||
}
|
||||
|
||||
if new_loc.x.is_some() || new_loc.y.is_some() {
|
||||
// If TOP or LEFT side of the window got resized, we have to move it
|
||||
space.map_element(window, window_loc, false);
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
Vendored
+1215
File diff suppressed because it is too large.
Load diff
Vendored
+1688
File diff suppressed because it is too large.
Load diff
Vendored
+463
@@ -0,0 +1,463 @@
|
||||
use std::{
|
||||
sync::{Mutex, atomic::Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(feature = "egl")]
|
||||
use smithay::backend::renderer::ImportEgl;
|
||||
#[cfg(feature = "debug")]
|
||||
use smithay::{
|
||||
backend::{allocator::Fourcc, renderer::ImportMem},
|
||||
reexports::winit::raw_window_handle::{HasWindowHandle, RawWindowHandle},
|
||||
};
|
||||
|
||||
use smithay::{
|
||||
backend::{
|
||||
SwapBuffersError,
|
||||
allocator::dmabuf::Dmabuf,
|
||||
egl::EGLDevice,
|
||||
renderer::{
|
||||
ImportDma, ImportMemWl,
|
||||
damage::{Error as OutputDamageTrackerError, OutputDamageTracker},
|
||||
element::AsRenderElements,
|
||||
gles::GlesRenderer,
|
||||
},
|
||||
winit::{self, WinitEvent, WinitGraphicsBackend},
|
||||
},
|
||||
input::{
|
||||
keyboard::LedState,
|
||||
pointer::{CursorImageAttributes, CursorImageStatus},
|
||||
},
|
||||
output::{Mode, Output, PhysicalProperties, Subpixel},
|
||||
reexports::{
|
||||
calloop::EventLoop,
|
||||
wayland_protocols::wp::presentation_time::server::wp_presentation_feedback,
|
||||
wayland_server::{Display, protocol::wl_surface},
|
||||
winit::event_loop::pump_events::PumpStatus,
|
||||
},
|
||||
utils::{IsAlive, Scale, Transform},
|
||||
wayland::{
|
||||
compositor,
|
||||
dmabuf::{
|
||||
DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier,
|
||||
},
|
||||
presentation::Refresh,
|
||||
},
|
||||
};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::state::{AnvilState, Backend, take_presentation_feedback, update_primary_scanout_output};
|
||||
use crate::{drawing::*, render::*};
|
||||
|
||||
pub const OUTPUT_NAME: &str = "winit";
|
||||
|
||||
pub struct WinitData {
|
||||
backend: WinitGraphicsBackend<GlesRenderer>,
|
||||
damage_tracker: OutputDamageTracker,
|
||||
dmabuf_state: (DmabufState, DmabufGlobal, Option<DmabufFeedback>),
|
||||
full_redraw: u8,
|
||||
#[cfg(feature = "debug")]
|
||||
pub fps: fps_ticker::Fps,
|
||||
}
|
||||
|
||||
impl DmabufHandler for AnvilState<WinitData> {
|
||||
fn dmabuf_state(&mut self) -> &mut DmabufState {
|
||||
&mut self.backend_data.dmabuf_state.0
|
||||
}
|
||||
|
||||
fn dmabuf_imported(&mut self, _global: &DmabufGlobal, dmabuf: Dmabuf, notifier: ImportNotifier) {
|
||||
if self
|
||||
.backend_data
|
||||
.backend
|
||||
.renderer()
|
||||
.import_dmabuf(&dmabuf, None)
|
||||
.is_ok()
|
||||
{
|
||||
let _ = notifier.successful::<AnvilState<WinitData>>();
|
||||
} else {
|
||||
notifier.failed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for WinitData {
|
||||
const HAS_GESTURES: bool = true;
|
||||
|
||||
fn seat_name(&self) -> String {
|
||||
String::from("winit")
|
||||
}
|
||||
fn reset_buffers(&mut self, _output: &Output) {
|
||||
self.full_redraw = 4;
|
||||
}
|
||||
fn early_import(&mut self, _surface: &wl_surface::WlSurface) {}
|
||||
fn update_led_state(&mut self, _led_state: LedState) {}
|
||||
}
|
||||
|
||||
pub fn run_winit() {
|
||||
let mut event_loop = EventLoop::try_new().unwrap();
|
||||
let display = Display::new().unwrap();
|
||||
let mut display_handle = display.handle();
|
||||
|
||||
#[cfg_attr(not(feature = "egl"), allow(unused_mut))]
|
||||
let (mut backend, mut winit) = match winit::init::<GlesRenderer>() {
|
||||
Ok(ret) => ret,
|
||||
Err(err) => {
|
||||
error!("Failed to initialize Winit backend: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let size = backend.window_size();
|
||||
|
||||
let mode = Mode {
|
||||
size,
|
||||
refresh: 60_000,
|
||||
};
|
||||
let output = Output::new(
|
||||
OUTPUT_NAME.to_string(),
|
||||
PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: "Smithay".into(),
|
||||
model: "Winit".into(),
|
||||
serial_number: "Unknown".into(),
|
||||
},
|
||||
);
|
||||
let _global = output.create_global::<AnvilState<WinitData>>(&display.handle());
|
||||
output.change_current_state(Some(mode), Some(Transform::Flipped180), None, Some((0, 0).into()));
|
||||
output.set_preferred(mode);
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
#[allow(deprecated)]
|
||||
let fps_image =
|
||||
image::io::Reader::with_format(std::io::Cursor::new(FPS_NUMBERS_PNG), image::ImageFormat::Png)
|
||||
.decode()
|
||||
.unwrap();
|
||||
#[cfg(feature = "debug")]
|
||||
let fps_texture = backend
|
||||
.renderer()
|
||||
.import_memory(
|
||||
&fps_image.to_rgba8(),
|
||||
Fourcc::Abgr8888,
|
||||
(fps_image.width() as i32, fps_image.height() as i32).into(),
|
||||
false,
|
||||
)
|
||||
.expect("Unable to upload FPS texture");
|
||||
#[cfg(feature = "debug")]
|
||||
let mut fps_element = FpsElement::new(fps_texture);
|
||||
|
||||
let render_node = EGLDevice::device_for_display(backend.renderer().egl_context().display())
|
||||
.and_then(|device| device.try_get_render_node());
|
||||
|
||||
let dmabuf_default_feedback = match render_node {
|
||||
Ok(Some(node)) => {
|
||||
let dmabuf_formats = backend.renderer().dmabuf_formats();
|
||||
let dmabuf_default_feedback = DmabufFeedbackBuilder::new(node.dev_id(), dmabuf_formats)
|
||||
.build()
|
||||
.unwrap();
|
||||
Some(dmabuf_default_feedback)
|
||||
}
|
||||
Ok(None) => {
|
||||
warn!("failed to query render node, dmabuf will use v3");
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(?err, "failed to egl device for display, dmabuf will use v3");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// if we failed to build dmabuf feedback we fall back to dmabuf v3
|
||||
// Note: egl on Mesa requires either v4 or wl_drm (initialized with bind_wl_display)
|
||||
let dmabuf_state = if let Some(default_feedback) = dmabuf_default_feedback {
|
||||
let mut dmabuf_state = DmabufState::new();
|
||||
let dmabuf_global = dmabuf_state.create_global_with_default_feedback::<AnvilState<WinitData>>(
|
||||
&display.handle(),
|
||||
&default_feedback,
|
||||
);
|
||||
(dmabuf_state, dmabuf_global, Some(default_feedback))
|
||||
} else {
|
||||
let dmabuf_formats = backend.renderer().dmabuf_formats();
|
||||
let mut dmabuf_state = DmabufState::new();
|
||||
let dmabuf_global =
|
||||
dmabuf_state.create_global::<AnvilState<WinitData>>(&display.handle(), dmabuf_formats);
|
||||
(dmabuf_state, dmabuf_global, None)
|
||||
};
|
||||
|
||||
#[cfg(feature = "egl")]
|
||||
if backend.renderer().bind_wl_display(&display.handle()).is_ok() {
|
||||
info!("EGL hardware-acceleration enabled");
|
||||
};
|
||||
|
||||
let data = {
|
||||
let damage_tracker = OutputDamageTracker::from_output(&output);
|
||||
|
||||
WinitData {
|
||||
backend,
|
||||
damage_tracker,
|
||||
dmabuf_state,
|
||||
full_redraw: 0,
|
||||
#[cfg(feature = "debug")]
|
||||
fps: fps_ticker::Fps::default(),
|
||||
}
|
||||
};
|
||||
let mut state = AnvilState::init(display, event_loop.handle(), data, true);
|
||||
state
|
||||
.shm_state
|
||||
.update_formats(state.backend_data.backend.renderer().shm_formats());
|
||||
state.space.map_output(&output, (0, 0));
|
||||
|
||||
#[cfg(feature = "xwayland")]
|
||||
state.start_xwayland();
|
||||
|
||||
info!("Initialization completed, starting the main loop.");
|
||||
|
||||
let mut pointer_element = PointerElement::default();
|
||||
|
||||
while state.running.load(Ordering::SeqCst) {
|
||||
let status = winit.dispatch_new_events(|event| match event {
|
||||
WinitEvent::Resized { size, .. } => {
|
||||
// We only have one output
|
||||
let output = state.space.outputs().next().unwrap().clone();
|
||||
state.space.map_output(&output, (0, 0));
|
||||
let mode = Mode {
|
||||
size,
|
||||
refresh: 60_000,
|
||||
};
|
||||
output.change_current_state(Some(mode), None, None, None);
|
||||
output.set_preferred(mode);
|
||||
crate::shell::fixup_positions(&mut state.space, state.pointer.current_location());
|
||||
}
|
||||
WinitEvent::Input(event) => state.process_input_event_windowed(event, OUTPUT_NAME),
|
||||
_ => (),
|
||||
});
|
||||
|
||||
if let PumpStatus::Exit(_) = status {
|
||||
state.running.store(false, Ordering::SeqCst);
|
||||
break;
|
||||
}
|
||||
|
||||
// drawing logic
|
||||
{
|
||||
let now = state.clock.now();
|
||||
let frame_target = now
|
||||
+ output
|
||||
.current_mode()
|
||||
.map(|mode| Duration::from_secs_f64(1_000f64 / mode.refresh as f64))
|
||||
.unwrap_or_default();
|
||||
state.pre_repaint(&output, frame_target);
|
||||
|
||||
let backend = &mut state.backend_data.backend;
|
||||
|
||||
// draw the cursor as relevant
|
||||
// reset the cursor if the surface is no longer alive
|
||||
let mut reset = false;
|
||||
if let CursorImageStatus::Surface(ref surface) = state.cursor_status {
|
||||
reset = !surface.alive();
|
||||
}
|
||||
if reset {
|
||||
state.cursor_status = CursorImageStatus::default_named();
|
||||
}
|
||||
let cursor_visible = !matches!(state.cursor_status, CursorImageStatus::Surface(_));
|
||||
|
||||
pointer_element.set_status(state.cursor_status.clone());
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
let fps = state.backend_data.fps.avg().round() as u32;
|
||||
#[cfg(feature = "debug")]
|
||||
fps_element.update_fps(fps);
|
||||
|
||||
let full_redraw = &mut state.backend_data.full_redraw;
|
||||
*full_redraw = full_redraw.saturating_sub(1);
|
||||
let space = &mut state.space;
|
||||
let damage_tracker = &mut state.backend_data.damage_tracker;
|
||||
let show_window_preview = state.show_window_preview;
|
||||
|
||||
let dnd_icon = state.dnd_icon.as_ref();
|
||||
|
||||
let scale = Scale::from(output.current_scale().fractional_scale());
|
||||
let cursor_hotspot = if let CursorImageStatus::Surface(ref surface) = state.cursor_status {
|
||||
compositor::with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<Mutex<CursorImageAttributes>>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.hotspot
|
||||
})
|
||||
} else {
|
||||
(0, 0).into()
|
||||
};
|
||||
let cursor_pos = state.pointer.current_location();
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
let mut renderdoc = state.renderdoc.as_mut();
|
||||
|
||||
let age = if *full_redraw > 0 {
|
||||
0
|
||||
} else {
|
||||
backend.buffer_age().unwrap_or(0)
|
||||
};
|
||||
#[cfg(feature = "debug")]
|
||||
let window_handle = backend
|
||||
.window()
|
||||
.window_handle()
|
||||
.map(|handle| {
|
||||
if let RawWindowHandle::Wayland(handle) = handle.as_raw() {
|
||||
handle.surface.as_ptr()
|
||||
} else {
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|_| std::ptr::null_mut());
|
||||
let render_res = backend.bind().and_then(|(renderer, mut fb)| {
|
||||
#[cfg(feature = "debug")]
|
||||
if let Some(renderdoc) = renderdoc.as_mut() {
|
||||
renderdoc.start_frame_capture(renderer.egl_context().get_context_handle(), window_handle);
|
||||
}
|
||||
|
||||
let mut elements = Vec::<CustomRenderElements<GlesRenderer>>::new();
|
||||
|
||||
elements.extend(
|
||||
pointer_element.render_elements(
|
||||
renderer,
|
||||
(cursor_pos - cursor_hotspot.to_f64())
|
||||
.to_physical(scale)
|
||||
.to_i32_round(),
|
||||
scale,
|
||||
1.0,
|
||||
),
|
||||
);
|
||||
|
||||
// draw the dnd icon if any
|
||||
if let Some(icon) = dnd_icon {
|
||||
let dnd_icon_pos = (cursor_pos + icon.offset.to_f64())
|
||||
.to_physical(scale)
|
||||
.to_i32_round();
|
||||
if icon.surface.alive() {
|
||||
elements.extend(AsRenderElements::<GlesRenderer>::render_elements(
|
||||
&smithay::desktop::space::SurfaceTree::from_surface(&icon.surface),
|
||||
renderer,
|
||||
dnd_icon_pos,
|
||||
scale,
|
||||
1.0,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
elements.push(CustomRenderElements::Fps(fps_element.clone()));
|
||||
|
||||
render_output(
|
||||
&output,
|
||||
space,
|
||||
elements,
|
||||
renderer,
|
||||
&mut fb,
|
||||
damage_tracker,
|
||||
age,
|
||||
show_window_preview,
|
||||
)
|
||||
.map_err(|err| match err {
|
||||
OutputDamageTrackerError::Rendering(err) => err.into(),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
});
|
||||
|
||||
match render_res {
|
||||
Ok(render_output_result) => {
|
||||
let has_rendered = render_output_result.damage.is_some();
|
||||
if let Some(damage) = render_output_result.damage {
|
||||
if let Err(err) = backend.submit(Some(damage)) {
|
||||
warn!("Failed to submit buffer: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
if let Some(renderdoc) = renderdoc.as_mut() {
|
||||
renderdoc.end_frame_capture(
|
||||
backend.renderer().egl_context().get_context_handle(),
|
||||
backend
|
||||
.window()
|
||||
.window_handle()
|
||||
.map(|handle| {
|
||||
if let RawWindowHandle::Wayland(handle) = handle.as_raw() {
|
||||
handle.surface.as_ptr()
|
||||
} else {
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|_| std::ptr::null_mut()),
|
||||
);
|
||||
}
|
||||
|
||||
backend.window().set_cursor_visible(cursor_visible);
|
||||
|
||||
let states = render_output_result.states;
|
||||
|
||||
update_primary_scanout_output(
|
||||
&state.space,
|
||||
&output,
|
||||
&state.dnd_icon,
|
||||
&state.cursor_status,
|
||||
&states,
|
||||
);
|
||||
|
||||
if has_rendered {
|
||||
let mut output_presentation_feedback =
|
||||
take_presentation_feedback(&output, &state.space, &states);
|
||||
output_presentation_feedback.presented(
|
||||
frame_target,
|
||||
output
|
||||
.current_mode()
|
||||
.map(|mode| {
|
||||
Refresh::fixed(Duration::from_secs_f64(1_000f64 / mode.refresh as f64))
|
||||
})
|
||||
.unwrap_or(Refresh::Unknown),
|
||||
0,
|
||||
wp_presentation_feedback::Kind::Vsync,
|
||||
)
|
||||
}
|
||||
|
||||
// Send frame events so that client start drawing their next frame
|
||||
state.post_repaint(&output, frame_target, None, &states);
|
||||
}
|
||||
Err(SwapBuffersError::ContextLost(err)) => {
|
||||
#[cfg(feature = "debug")]
|
||||
if let Some(renderdoc) = renderdoc.as_mut() {
|
||||
renderdoc.discard_frame_capture(
|
||||
backend.renderer().egl_context().get_context_handle(),
|
||||
backend
|
||||
.window()
|
||||
.window_handle()
|
||||
.map(|handle| {
|
||||
if let RawWindowHandle::Wayland(handle) = handle.as_raw() {
|
||||
handle.surface.as_ptr()
|
||||
} else {
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|_| std::ptr::null_mut()),
|
||||
);
|
||||
}
|
||||
|
||||
error!("Critical Rendering Error: {}", err);
|
||||
state.running.store(false, Ordering::SeqCst);
|
||||
}
|
||||
Err(err) => warn!("Rendering error: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
let result = event_loop.dispatch(Some(Duration::from_millis(1)), &mut state);
|
||||
if result.is_err() {
|
||||
state.running.store(false, Ordering::SeqCst);
|
||||
} else {
|
||||
state.space.refresh();
|
||||
state.popups.cleanup();
|
||||
display_handle.flush_clients().unwrap();
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
state.backend_data.fps.tick();
|
||||
}
|
||||
}
|
||||
Vendored
+494
@@ -0,0 +1,494 @@
|
||||
use std::{
|
||||
sync::{Mutex, atomic::Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
drawing::*,
|
||||
render::*,
|
||||
state::{AnvilState, Backend, take_presentation_feedback, update_primary_scanout_output},
|
||||
};
|
||||
#[cfg(feature = "egl")]
|
||||
use smithay::backend::renderer::ImportEgl;
|
||||
#[cfg(feature = "debug")]
|
||||
use smithay::backend::{allocator::Fourcc, renderer::ImportMem};
|
||||
|
||||
use smithay::{
|
||||
backend::{
|
||||
allocator::{
|
||||
dmabuf::{Dmabuf, DmabufAllocator},
|
||||
gbm::{GbmAllocator, GbmBufferFlags},
|
||||
vulkan::{ImageUsageFlags, VulkanAllocator},
|
||||
},
|
||||
egl::{EGLContext, EGLDisplay},
|
||||
renderer::{
|
||||
Bind, ImportDma, ImportMemWl, damage::OutputDamageTracker, element::AsRenderElements,
|
||||
gles::GlesRenderer,
|
||||
},
|
||||
vulkan::{Instance, PhysicalDevice, version::Version},
|
||||
x11::{WindowBuilder, X11Backend, X11Event, X11Surface},
|
||||
},
|
||||
input::{
|
||||
keyboard::LedState,
|
||||
pointer::{CursorImageAttributes, CursorImageStatus},
|
||||
},
|
||||
output::{Mode, Output, PhysicalProperties, Subpixel},
|
||||
reexports::{
|
||||
ash::ext,
|
||||
calloop::EventLoop,
|
||||
gbm,
|
||||
wayland_protocols::wp::presentation_time::server::wp_presentation_feedback,
|
||||
wayland_server::{Display, protocol::wl_surface},
|
||||
},
|
||||
utils::{DeviceFd, IsAlive, Scale},
|
||||
wayland::{
|
||||
compositor,
|
||||
dmabuf::{
|
||||
DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier,
|
||||
},
|
||||
presentation::Refresh,
|
||||
},
|
||||
};
|
||||
use tracing::{error, info, trace, warn};
|
||||
|
||||
pub const OUTPUT_NAME: &str = "x11";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct X11Data {
|
||||
render: bool,
|
||||
mode: Mode,
|
||||
// FIXME: If GlesRenderer is dropped before X11Surface, then the MakeCurrent call inside Gles2Renderer will
|
||||
// fail because the X11Surface is keeping gbm alive.
|
||||
renderer: GlesRenderer,
|
||||
damage_tracker: OutputDamageTracker,
|
||||
surface: X11Surface,
|
||||
dmabuf_state: DmabufState,
|
||||
_dmabuf_global: DmabufGlobal,
|
||||
_dmabuf_default_feedback: DmabufFeedback,
|
||||
#[cfg(feature = "debug")]
|
||||
fps: fps_ticker::Fps,
|
||||
}
|
||||
|
||||
impl DmabufHandler for AnvilState<X11Data> {
|
||||
fn dmabuf_state(&mut self) -> &mut DmabufState {
|
||||
&mut self.backend_data.dmabuf_state
|
||||
}
|
||||
|
||||
fn dmabuf_imported(&mut self, _global: &DmabufGlobal, dmabuf: Dmabuf, notifier: ImportNotifier) {
|
||||
if self.backend_data.renderer.import_dmabuf(&dmabuf, None).is_ok() {
|
||||
let _ = notifier.successful::<AnvilState<X11Data>>();
|
||||
} else {
|
||||
notifier.failed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for X11Data {
|
||||
fn seat_name(&self) -> String {
|
||||
"x11".to_owned()
|
||||
}
|
||||
fn reset_buffers(&mut self, _output: &Output) {
|
||||
self.surface.reset_buffers();
|
||||
}
|
||||
fn early_import(&mut self, _surface: &wl_surface::WlSurface) {}
|
||||
fn update_led_state(&mut self, _led_state: LedState) {}
|
||||
}
|
||||
|
||||
pub fn run_x11() {
|
||||
let mut event_loop = EventLoop::try_new().unwrap();
|
||||
let display = Display::new().unwrap();
|
||||
let mut display_handle = display.handle();
|
||||
|
||||
let backend = X11Backend::new().expect("Failed to initialize X11 backend");
|
||||
let handle = backend.handle();
|
||||
|
||||
// Obtain the DRM node the X server uses for direct rendering.
|
||||
let (node, fd) = handle
|
||||
.drm_node()
|
||||
.expect("Could not get DRM node used by X server");
|
||||
|
||||
// Create the gbm device for buffer allocation.
|
||||
let device = gbm::Device::new(DeviceFd::from(fd)).expect("Failed to create gbm device");
|
||||
// Initialize EGL using the GBM device.
|
||||
let egl = unsafe { EGLDisplay::new(device.clone()).expect("Failed to create EGLDisplay") };
|
||||
// Create the OpenGL context
|
||||
let context = EGLContext::new(&egl).expect("Failed to create EGLContext");
|
||||
|
||||
let window = WindowBuilder::new()
|
||||
.title("Anvil")
|
||||
.build(&handle)
|
||||
.expect("Failed to create first window");
|
||||
|
||||
let skip_vulkan = std::env::var("ANVIL_NO_VULKAN")
|
||||
.map(|x| {
|
||||
x == "1" || x.to_lowercase() == "true" || x.to_lowercase() == "yes" || x.to_lowercase() == "y"
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let vulkan_allocator = if !skip_vulkan {
|
||||
Instance::new(Version::VERSION_1_2, None)
|
||||
.ok()
|
||||
.and_then(|instance| {
|
||||
PhysicalDevice::enumerate(&instance).ok().and_then(|devices| {
|
||||
devices
|
||||
.filter(|phd| phd.has_device_extension(ext::physical_device_drm::NAME))
|
||||
.find(|phd| {
|
||||
phd.primary_node().unwrap() == Some(node)
|
||||
|| phd.render_node().unwrap() == Some(node)
|
||||
})
|
||||
})
|
||||
})
|
||||
.and_then(|physical_device| {
|
||||
VulkanAllocator::new(
|
||||
&physical_device,
|
||||
ImageUsageFlags::COLOR_ATTACHMENT | ImageUsageFlags::SAMPLED,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let surface = match vulkan_allocator {
|
||||
// Create the surface for the window.
|
||||
Some(vulkan_allocator) => handle
|
||||
.create_surface(
|
||||
&window,
|
||||
DmabufAllocator(vulkan_allocator),
|
||||
context
|
||||
.dmabuf_render_formats()
|
||||
.iter()
|
||||
.map(|format| format.modifier),
|
||||
)
|
||||
.expect("Failed to create X11 surface"),
|
||||
None => handle
|
||||
.create_surface(
|
||||
&window,
|
||||
DmabufAllocator(GbmAllocator::new(device, GbmBufferFlags::RENDERING)),
|
||||
context
|
||||
.dmabuf_render_formats()
|
||||
.iter()
|
||||
.map(|format| format.modifier),
|
||||
)
|
||||
.expect("Failed to create X11 surface"),
|
||||
};
|
||||
|
||||
#[cfg_attr(not(feature = "egl"), allow(unused_mut))]
|
||||
let mut renderer = unsafe { GlesRenderer::new(context) }.expect("Failed to initialize renderer");
|
||||
|
||||
#[cfg(feature = "egl")]
|
||||
if renderer.bind_wl_display(&display.handle()).is_ok() {
|
||||
info!("EGL hardware-acceleration enabled");
|
||||
}
|
||||
|
||||
let dmabuf_formats = renderer.dmabuf_formats();
|
||||
let dmabuf_default_feedback = DmabufFeedbackBuilder::new(node.dev_id(), dmabuf_formats)
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut dmabuf_state = DmabufState::new();
|
||||
let dmabuf_global = dmabuf_state.create_global_with_default_feedback::<AnvilState<X11Data>>(
|
||||
&display.handle(),
|
||||
&dmabuf_default_feedback,
|
||||
);
|
||||
|
||||
let size = {
|
||||
let s = window.size();
|
||||
|
||||
(s.w as i32, s.h as i32).into()
|
||||
};
|
||||
|
||||
let mode = Mode {
|
||||
size,
|
||||
refresh: 60_000,
|
||||
};
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
#[allow(deprecated)]
|
||||
let fps_image =
|
||||
image::io::Reader::with_format(std::io::Cursor::new(FPS_NUMBERS_PNG), image::ImageFormat::Png)
|
||||
.decode()
|
||||
.unwrap();
|
||||
#[cfg(feature = "debug")]
|
||||
let fps_texture = renderer
|
||||
.import_memory(
|
||||
&fps_image.to_rgba8(),
|
||||
Fourcc::Abgr8888,
|
||||
(fps_image.width() as i32, fps_image.height() as i32).into(),
|
||||
false,
|
||||
)
|
||||
.expect("Unable to upload FPS texture");
|
||||
#[cfg(feature = "debug")]
|
||||
let mut fps_element = FpsElement::new(fps_texture);
|
||||
let output = Output::new(
|
||||
OUTPUT_NAME.to_string(),
|
||||
PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: "Smithay".into(),
|
||||
model: "X11".into(),
|
||||
serial_number: "Unknown".into(),
|
||||
},
|
||||
);
|
||||
let _global = output.create_global::<AnvilState<X11Data>>(&display.handle());
|
||||
output.change_current_state(Some(mode), None, None, Some((0, 0).into()));
|
||||
output.set_preferred(mode);
|
||||
|
||||
let damage_tracker = OutputDamageTracker::from_output(&output);
|
||||
|
||||
let data = X11Data {
|
||||
render: true,
|
||||
mode,
|
||||
surface,
|
||||
renderer,
|
||||
damage_tracker,
|
||||
dmabuf_state,
|
||||
_dmabuf_global: dmabuf_global,
|
||||
_dmabuf_default_feedback: dmabuf_default_feedback,
|
||||
#[cfg(feature = "debug")]
|
||||
fps: fps_ticker::Fps::default(),
|
||||
};
|
||||
|
||||
let mut state = AnvilState::init(display, event_loop.handle(), data, true);
|
||||
state
|
||||
.shm_state
|
||||
.update_formats(state.backend_data.renderer.shm_formats());
|
||||
state.space.map_output(&output, (0, 0));
|
||||
|
||||
let output_clone = output.clone();
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(backend, move |event, _, data| match event {
|
||||
X11Event::CloseRequested { .. } => {
|
||||
data.running.store(false, Ordering::SeqCst);
|
||||
}
|
||||
X11Event::Resized { new_size, .. } => {
|
||||
let output = &output_clone;
|
||||
let size = { (new_size.w as i32, new_size.h as i32).into() };
|
||||
|
||||
data.backend_data.mode = Mode {
|
||||
size,
|
||||
refresh: 60_000,
|
||||
};
|
||||
output.delete_mode(output.current_mode().unwrap());
|
||||
output.change_current_state(Some(data.backend_data.mode), None, None, None);
|
||||
output.set_preferred(data.backend_data.mode);
|
||||
crate::shell::fixup_positions(&mut data.space, data.pointer.current_location());
|
||||
|
||||
data.backend_data.render = true;
|
||||
}
|
||||
X11Event::PresentCompleted { .. } | X11Event::Refresh { .. } => {
|
||||
data.backend_data.render = true;
|
||||
}
|
||||
X11Event::Input { event, .. } => data.process_input_event_windowed(event, OUTPUT_NAME),
|
||||
X11Event::Focus { focused: false, .. } => {
|
||||
data.release_all_keys();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.expect("Failed to insert X11 Backend into event loop");
|
||||
|
||||
#[cfg(feature = "xwayland")]
|
||||
state.start_xwayland();
|
||||
|
||||
info!("Initialization completed, starting the main loop.");
|
||||
|
||||
let mut pointer_element = PointerElement::default();
|
||||
|
||||
while state.running.load(Ordering::SeqCst) {
|
||||
if state.backend_data.render {
|
||||
profiling::scope!("render_frame");
|
||||
|
||||
let now = state.clock.now();
|
||||
let frame_target = now
|
||||
+ output
|
||||
.current_mode()
|
||||
.map(|mode| Duration::from_secs_f64(1_000f64 / mode.refresh as f64))
|
||||
.unwrap_or_default();
|
||||
state.pre_repaint(&output, frame_target);
|
||||
|
||||
let backend_data = &mut state.backend_data;
|
||||
// We need to borrow everything we want to refer to inside the renderer callback otherwise rustc is unhappy.
|
||||
#[cfg(feature = "debug")]
|
||||
let fps = backend_data.fps.avg().round() as u32;
|
||||
#[cfg(feature = "debug")]
|
||||
fps_element.update_fps(fps);
|
||||
|
||||
let (mut buffer, age) = backend_data.surface.buffer().expect("gbm device was destroyed");
|
||||
let mut fb = match backend_data.renderer.bind(&mut buffer) {
|
||||
Ok(fb) => fb,
|
||||
Err(err) => {
|
||||
error!("Error while binding buffer: {}", err);
|
||||
profiling::finish_frame!();
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
if let Some(renderdoc) = state.renderdoc.as_mut() {
|
||||
renderdoc.start_frame_capture(
|
||||
backend_data.renderer.egl_context().get_context_handle(),
|
||||
std::ptr::null(),
|
||||
);
|
||||
}
|
||||
|
||||
let mut elements: Vec<CustomRenderElements<GlesRenderer>> = Vec::new();
|
||||
|
||||
// draw the cursor as relevant
|
||||
// reset the cursor if the surface is no longer alive
|
||||
let mut reset = false;
|
||||
if let CursorImageStatus::Surface(ref surface) = state.cursor_status {
|
||||
reset = !surface.alive();
|
||||
}
|
||||
if reset {
|
||||
state.cursor_status = CursorImageStatus::default_named();
|
||||
}
|
||||
let cursor_visible = !matches!(state.cursor_status, CursorImageStatus::Surface(_));
|
||||
|
||||
let scale = Scale::from(output.current_scale().fractional_scale());
|
||||
let cursor_hotspot = if let CursorImageStatus::Surface(ref surface) = state.cursor_status {
|
||||
compositor::with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<Mutex<CursorImageAttributes>>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.hotspot
|
||||
})
|
||||
} else {
|
||||
(0, 0).into()
|
||||
};
|
||||
let cursor_pos = state.pointer.current_location();
|
||||
|
||||
pointer_element.set_status(state.cursor_status.clone());
|
||||
elements.extend(
|
||||
pointer_element.render_elements(
|
||||
&mut backend_data.renderer,
|
||||
(cursor_pos - cursor_hotspot.to_f64())
|
||||
.to_physical(scale)
|
||||
.to_i32_round(),
|
||||
scale,
|
||||
1.0,
|
||||
),
|
||||
);
|
||||
|
||||
// draw the dnd icon if any
|
||||
if let Some(icon) = state.dnd_icon.as_ref() {
|
||||
let dnd_icon_pos = (cursor_pos + icon.offset.to_f64())
|
||||
.to_physical(scale)
|
||||
.to_i32_round();
|
||||
if icon.surface.alive() {
|
||||
elements.extend(AsRenderElements::<GlesRenderer>::render_elements(
|
||||
&smithay::desktop::space::SurfaceTree::from_surface(&icon.surface),
|
||||
&mut backend_data.renderer,
|
||||
dnd_icon_pos,
|
||||
scale,
|
||||
1.0,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
elements.push(CustomRenderElements::Fps(fps_element.clone()));
|
||||
|
||||
let render_res = render_output(
|
||||
&output,
|
||||
&state.space,
|
||||
elements,
|
||||
&mut backend_data.renderer,
|
||||
&mut fb,
|
||||
&mut backend_data.damage_tracker,
|
||||
age.into(),
|
||||
state.show_window_preview,
|
||||
);
|
||||
|
||||
match render_res {
|
||||
Ok(render_output_result) => {
|
||||
trace!("Finished rendering");
|
||||
let submitted = if let Err(err) = backend_data.surface.submit() {
|
||||
backend_data.surface.reset_buffers();
|
||||
warn!("Failed to submit buffer: {}. Retrying", err);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
let states = render_output_result.states;
|
||||
|
||||
update_primary_scanout_output(
|
||||
&state.space,
|
||||
&output,
|
||||
&state.dnd_icon,
|
||||
&state.cursor_status,
|
||||
&states,
|
||||
);
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
let rendered = render_output_result.damage.is_some();
|
||||
if render_output_result.damage.is_some() {
|
||||
let mut output_presentation_feedback =
|
||||
take_presentation_feedback(&output, &state.space, &states);
|
||||
output_presentation_feedback.presented(
|
||||
frame_target,
|
||||
output
|
||||
.current_mode()
|
||||
.map(|mode| {
|
||||
Refresh::fixed(Duration::from_secs_f64(1_000f64 / mode.refresh as f64))
|
||||
})
|
||||
.unwrap_or(Refresh::Unknown),
|
||||
0,
|
||||
wp_presentation_feedback::Kind::Vsync,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
if rendered {
|
||||
if let Some(renderdoc) = state.renderdoc.as_mut() {
|
||||
renderdoc.end_frame_capture(
|
||||
state.backend_data.renderer.egl_context().get_context_handle(),
|
||||
std::ptr::null(),
|
||||
);
|
||||
}
|
||||
} else if let Some(renderdoc) = state.renderdoc.as_mut() {
|
||||
renderdoc.discard_frame_capture(
|
||||
state.backend_data.renderer.egl_context().get_context_handle(),
|
||||
std::ptr::null(),
|
||||
);
|
||||
}
|
||||
|
||||
state.backend_data.render = !submitted;
|
||||
|
||||
// Send frame events so that client start drawing their next frame
|
||||
state.post_repaint(&output, frame_target, None, &states);
|
||||
}
|
||||
Err(err) => {
|
||||
#[cfg(feature = "debug")]
|
||||
if let Some(renderdoc) = state.renderdoc.as_mut() {
|
||||
renderdoc.discard_frame_capture(
|
||||
backend_data.renderer.egl_context().get_context_handle(),
|
||||
std::ptr::null(),
|
||||
);
|
||||
}
|
||||
|
||||
backend_data.surface.reset_buffers();
|
||||
error!("Rendering error: {}", err);
|
||||
// TODO: convert RenderError into SwapBuffersError and skip temporary (will retry) and panic on ContextLost or recreate
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
state.backend_data.fps.tick();
|
||||
window.set_cursor_visible(cursor_visible);
|
||||
profiling::finish_frame!();
|
||||
}
|
||||
|
||||
let result = event_loop.dispatch(Some(Duration::from_millis(16)), &mut state);
|
||||
if result.is_err() {
|
||||
state.running.store(false, Ordering::SeqCst);
|
||||
} else {
|
||||
state.space.refresh();
|
||||
state.popups.cleanup();
|
||||
display_handle.flush_clients().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use smithay::utils::user_data::UserDataMap;
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
c.bench_function("UserDataMap::get", |b| {
|
||||
let udata_map = UserDataMap::new();
|
||||
udata_map.insert_if_missing(|| 17i32);
|
||||
b.iter(|| udata_map.get::<i32>())
|
||||
});
|
||||
c.bench_function("UserDataMap::get threadsafe", |b| {
|
||||
let udata_map = UserDataMap::new();
|
||||
udata_map.insert_if_missing_threadsafe(|| 17i32);
|
||||
b.iter(|| udata_map.get::<i32>())
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
Vendored
+50
@@ -0,0 +1,50 @@
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use rand::RngExt;
|
||||
use smithay::utils::{Physical, Rectangle, Size};
|
||||
|
||||
fn element_visible_size(test_element: Rectangle<i32, Physical>, opaque_regions: &[Rectangle<i32, Physical>]) {
|
||||
let mut workhouse = Vec::with_capacity(2048 * 4);
|
||||
workhouse.push(test_element);
|
||||
workhouse = Rectangle::subtract_rects_many_in_place(workhouse, opaque_regions.iter().copied());
|
||||
workhouse
|
||||
.iter()
|
||||
.fold(0usize, |acc, item| acc + (item.size.w * item.size.h) as usize);
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
let stage: Size<i32, Physical> = Size::from((800, 600));
|
||||
let element_size: Size<i32, Physical> = Size::from((200, 100));
|
||||
let max_x = stage.w - element_size.w;
|
||||
let max_y = stage.h - element_size.h;
|
||||
|
||||
let mut rand = rand::rng();
|
||||
let x = rand.random_range(0..max_x);
|
||||
let y = rand.random_range(0..max_y);
|
||||
let test_element = Rectangle::new((x, y).into(), element_size);
|
||||
|
||||
let x_min = (test_element.loc.x - element_size.w) + 1;
|
||||
let x_max = (test_element.loc.x + element_size.w) - 1;
|
||||
let y_min = (test_element.loc.y - element_size.h) + 1;
|
||||
let y_max = (test_element.loc.y + element_size.h) - 1;
|
||||
// let x_min = 0;
|
||||
// let x_max = stage.w - element_size.w;
|
||||
// let y_min = 0;
|
||||
// let y_max = stage.h - element_size.h;
|
||||
|
||||
let opaque_regions = (0..2048)
|
||||
.map(|_| {
|
||||
let x = rand.random_range(x_min..=x_max);
|
||||
let y = rand.random_range(y_min..=y_max);
|
||||
Rectangle::new((x, y).into(), element_size)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
c.bench_function("element_visible_size", |b| {
|
||||
b.iter(|| {
|
||||
element_visible_size(test_element, &opaque_regions);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
Vendored
+139
@@ -0,0 +1,139 @@
|
||||
#![allow(clippy::disallowed_macros)]
|
||||
#[cfg(any(feature = "backend_egl", feature = "renderer_gl"))]
|
||||
fn gl_generate() {
|
||||
use gl_generator::{Api, Fallbacks, Profile, Registry};
|
||||
use std::{env, fs::File, path::PathBuf};
|
||||
|
||||
let dest = PathBuf::from(&env::var("OUT_DIR").unwrap());
|
||||
|
||||
if env::var_os("CARGO_FEATURE_BACKEND_EGL").is_some() {
|
||||
let mut file = File::create(dest.join("egl_bindings.rs")).unwrap();
|
||||
Registry::new(
|
||||
Api::Egl,
|
||||
(1, 5),
|
||||
Profile::Core,
|
||||
Fallbacks::All,
|
||||
[
|
||||
"EGL_KHR_create_context",
|
||||
"EGL_EXT_create_context_robustness",
|
||||
"EGL_KHR_create_context_no_error",
|
||||
"EGL_KHR_no_config_context",
|
||||
"EGL_EXT_pixel_format_float",
|
||||
"EGL_EXT_device_base",
|
||||
"EGL_EXT_device_enumeration",
|
||||
"EGL_EXT_device_query",
|
||||
"EGL_EXT_device_drm",
|
||||
"EGL_EXT_device_drm_render_node",
|
||||
"EGL_KHR_stream",
|
||||
"EGL_KHR_stream_producer_eglsurface",
|
||||
"EGL_EXT_platform_base",
|
||||
"EGL_KHR_platform_x11",
|
||||
"EGL_EXT_platform_x11",
|
||||
"EGL_KHR_platform_wayland",
|
||||
"EGL_EXT_platform_wayland",
|
||||
"EGL_KHR_platform_gbm",
|
||||
"EGL_MESA_platform_gbm",
|
||||
"EGL_MESA_platform_surfaceless",
|
||||
"EGL_EXT_platform_device",
|
||||
"EGL_WL_bind_wayland_display",
|
||||
"EGL_KHR_image_base",
|
||||
"EGL_EXT_image_dma_buf_import",
|
||||
"EGL_EXT_image_dma_buf_import_modifiers",
|
||||
"EGL_MESA_image_dma_buf_export",
|
||||
"EGL_KHR_gl_image",
|
||||
"EGL_EXT_buffer_age",
|
||||
"EGL_EXT_swap_buffers_with_damage",
|
||||
"EGL_KHR_swap_buffers_with_damage",
|
||||
"EGL_KHR_fence_sync",
|
||||
"EGL_ANDROID_native_fence_sync",
|
||||
"EGL_IMG_context_priority",
|
||||
],
|
||||
)
|
||||
.write_bindings(gl_generator::GlobalGenerator, &mut file)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
if env::var_os("CARGO_FEATURE_RENDERER_GL").is_some() {
|
||||
let mut file = File::create(dest.join("gl_bindings.rs")).unwrap();
|
||||
Registry::new(
|
||||
Api::Gles2,
|
||||
(3, 2),
|
||||
Profile::Compatibility,
|
||||
Fallbacks::None,
|
||||
[
|
||||
"GL_OES_EGL_image",
|
||||
"GL_OES_EGL_image_external",
|
||||
"GL_EXT_texture_format_BGRA8888",
|
||||
"GL_EXT_unpack_subimage",
|
||||
"GL_OES_EGL_sync",
|
||||
"GL_EXT_disjoint_timer_query",
|
||||
],
|
||||
)
|
||||
.write_bindings(gl_generator::StructGenerator, &mut file)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "backend_gbm", not(feature = "backend_gbm_has_fd_for_plane")))]
|
||||
fn test_gbm_bo_fd_for_plane() {
|
||||
let gbm = match pkg_config::probe_library("gbm") {
|
||||
Ok(lib) => lib,
|
||||
Err(_) => {
|
||||
println!("cargo:warning=failed to find gbm, assuming gbm_bo_get_fd_for_plane is unavailable");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let has_gbm_bo_get_fd_for_plane = cc::Build::new()
|
||||
.file("test_gbm_bo_get_fd_for_plane.c")
|
||||
.includes(gbm.include_paths)
|
||||
.warnings_into_errors(true)
|
||||
.cargo_metadata(false)
|
||||
.try_compile("test_gbm_bo_get_fd_for_plane")
|
||||
.is_ok();
|
||||
|
||||
if has_gbm_bo_get_fd_for_plane {
|
||||
println!("cargo:rustc-cfg=feature=\"backend_gbm_has_fd_for_plane\"");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
feature = "backend_gbm",
|
||||
not(feature = "backend_gbm_has_create_with_modifiers2")
|
||||
))]
|
||||
fn test_gbm_bo_create_with_modifiers2() {
|
||||
let gbm = match pkg_config::probe_library("gbm") {
|
||||
Ok(lib) => lib,
|
||||
Err(_) => {
|
||||
println!(
|
||||
"cargo:warning=failed to find gbm, assuming gbm_bo_create_with_modifiers2 is unavailable"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let has_gbm_bo_create_with_modifiers2 = cc::Build::new()
|
||||
.file("test_gbm_bo_create_with_modifiers2.c")
|
||||
.includes(gbm.include_paths)
|
||||
.warnings_into_errors(true)
|
||||
.cargo_metadata(false)
|
||||
.try_compile("test_gbm_bo_create_with_modifiers2")
|
||||
.is_ok();
|
||||
|
||||
if has_gbm_bo_create_with_modifiers2 {
|
||||
println!("cargo:rustc-cfg=feature=\"backend_gbm_has_create_with_modifiers2\"");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(any(feature = "backend_egl", feature = "renderer_gl"))]
|
||||
gl_generate();
|
||||
|
||||
#[cfg(all(feature = "backend_gbm", not(feature = "backend_gbm_has_fd_for_plane")))]
|
||||
test_gbm_bo_fd_for_plane();
|
||||
#[cfg(all(
|
||||
feature = "backend_gbm",
|
||||
not(feature = "backend_gbm_has_create_with_modifiers2")
|
||||
))]
|
||||
test_gbm_bo_create_with_modifiers2();
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
msrv = "1.87"
|
||||
type-complexity-threshold = 400
|
||||
|
||||
disallowed-macros = [
|
||||
{ path = "std::print", reason = "We use tracing for logging" },
|
||||
{ path = "std::println", reason = "We use tracing for logging" },
|
||||
{ path = "std::eprint", reason = "We use tracing for logging" },
|
||||
{ path = "std::eprintln", reason = "We use tracing for logging" },
|
||||
]
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
|
||||
WLCS_SHA=2b385328366de5db19521b9b3eef81861430d984
|
||||
|
||||
if [ -f "./wlcs/wlcs" ] && [ "$(cd wlcs; git rev-parse HEAD)" = "${WLCS_SHA}" ] ; then
|
||||
echo "Using cached WLCS."
|
||||
else
|
||||
echo "Compiling WLCS."
|
||||
git clone https://github.com/MirServer/wlcs.git
|
||||
cd wlcs || exit
|
||||
# checkout a specific revision
|
||||
git reset --hard "${WLCS_SHA}"
|
||||
cmake -DWLCS_BUILD_ASAN=False -DWLCS_BUILD_TSAN=False -DWLCS_BUILD_UBSAN=False -DCMAKE_EXPORT_COMPILE_COMMANDS=1 .
|
||||
make
|
||||
fi
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv=refresh content=0;url=smithay/index.html />
|
||||
</head>
|
||||
</html>
|
||||
+501
@@ -0,0 +1,501 @@
|
||||
use std::{fmt, fs::File, os::unix::io::OwnedFd, path::PathBuf};
|
||||
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
use smithay::{
|
||||
backend::{
|
||||
allocator::{
|
||||
Allocator, Fourcc, Modifier,
|
||||
dmabuf::{AnyError, Dmabuf, DmabufAllocator},
|
||||
dumb::DumbAllocator,
|
||||
gbm::{GbmAllocator, GbmBufferFlags, GbmDevice},
|
||||
vulkan::{ImageUsageFlags, VulkanAllocator},
|
||||
},
|
||||
drm::{DrmDeviceFd, DrmNode},
|
||||
egl::{EGLContext, EGLDevice, EGLDisplay},
|
||||
renderer::{
|
||||
Bind, Color32F, ExportMem, Frame, ImportDma, Offscreen, Renderer,
|
||||
gles::{GlesRenderbuffer, GlesRenderer},
|
||||
},
|
||||
vulkan::{Instance, PhysicalDevice, version::Version},
|
||||
},
|
||||
utils::{DeviceFd, Rectangle, Transform},
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
subcommand: Subcommands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Subcommands {
|
||||
/// Intersect formats to find usable combinations
|
||||
#[command(arg_required_else_help = true)]
|
||||
Formats {
|
||||
/// Devices that should be able to render a format
|
||||
#[arg(short, long)]
|
||||
render: Vec<String>,
|
||||
/// Devices that should be able to sample from a format
|
||||
#[arg(short, long)]
|
||||
sample: Vec<String>,
|
||||
},
|
||||
/// Test exporting and importing buffers with various apis and devices
|
||||
Test(TestArgs),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
struct TestArgs {
|
||||
/// Allocator used for creating the buffer
|
||||
#[arg(short, long)]
|
||||
allocator: AllocatorType,
|
||||
/// Usage flags for allocating the image, when using the vulkan api
|
||||
#[arg(short, long, value_parser = usage_flags_from_string, required_if_eq("allocator", "vulkan"))]
|
||||
usage_flags: Option<ImageUsageFlags>,
|
||||
|
||||
/// Exporting device node
|
||||
#[arg(short, long, required_if_eq_any([("allocator", "dumb"), ("allocator", "gbm"), ("allocator", "vulkan")]))]
|
||||
export: Option<String>,
|
||||
/// Importing device node (may be equal to export)
|
||||
#[arg(short, long)]
|
||||
import: String,
|
||||
/// Renderer used to draw into buffer before exporting
|
||||
#[arg(short = 'r', long)]
|
||||
export_renderer: Option<RendererType>,
|
||||
#[arg(short = 't', long)]
|
||||
import_renderer: Option<RendererType>,
|
||||
#[arg(short, long, requires("export_renderer"), requires("import_renderer"))]
|
||||
dump: Option<String>,
|
||||
|
||||
#[arg(long, default_value_t = 256)]
|
||||
width: u32,
|
||||
#[arg(long, default_value_t = 256)]
|
||||
height: u32,
|
||||
#[arg(short, long, default_value = "Abgr8888", value_parser = fourcc_from_string)]
|
||||
fourcc: Fourcc,
|
||||
#[arg(short, long, default_value = "Linear", value_parser = modifier_from_string)]
|
||||
modifier: Modifier,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone, Copy)]
|
||||
pub enum AllocatorType {
|
||||
#[value(name = "dumb")]
|
||||
DumbBuffer,
|
||||
#[value(name = "gbm")]
|
||||
Gbm,
|
||||
#[value(name = "vulkan")]
|
||||
Vulkan,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone, Copy)]
|
||||
pub enum RendererType {
|
||||
#[value(name = "gles")]
|
||||
Gles,
|
||||
}
|
||||
|
||||
impl fmt::Display for RendererType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("Gles")
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Cli::parse();
|
||||
|
||||
if let Ok(env_filter) = tracing_subscriber::EnvFilter::try_from_default_env() {
|
||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||
} else {
|
||||
tracing_subscriber::fmt().init();
|
||||
}
|
||||
|
||||
match args.subcommand {
|
||||
Subcommands::Formats { render, sample } => format_test(render, sample),
|
||||
Subcommands::Test(args) => buffer_test(args),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_test(render: Vec<String>, sample: Vec<String>) {
|
||||
for format in render
|
||||
.iter()
|
||||
.map(|path| {
|
||||
let path = PathBuf::from(&path);
|
||||
let device = EGLDevice::enumerate()
|
||||
.expect("Failed to enumerate EGL devices")
|
||||
.find(|device| {
|
||||
device.drm_device_path().ok().as_ref() == Some(&path)
|
||||
|| device.render_device_path().ok().as_ref() == Some(&path)
|
||||
})
|
||||
.expect("Unable to find egl device");
|
||||
let display = unsafe { EGLDisplay::new(device).expect("Failed to create EGL display") };
|
||||
display.dmabuf_render_formats().clone()
|
||||
})
|
||||
.chain(sample.iter().map(|path| {
|
||||
let path = PathBuf::from(&path);
|
||||
let device = EGLDevice::enumerate()
|
||||
.expect("Failed to enumerate EGL devices")
|
||||
.find(|device| {
|
||||
device.drm_device_path().ok().as_ref() == Some(&path)
|
||||
|| device.render_device_path().ok().as_ref() == Some(&path)
|
||||
})
|
||||
.expect("Unable to find egl device");
|
||||
let display = unsafe { EGLDisplay::new(device).expect("Failed to create EGL display") };
|
||||
display.dmabuf_texture_formats().clone()
|
||||
}))
|
||||
.fold(None, |set, formats| match set {
|
||||
None => Some(formats),
|
||||
Some(set) => Some(set.intersection(&formats).copied().collect()),
|
||||
})
|
||||
.unwrap_or_default()
|
||||
{
|
||||
println!(
|
||||
"Format: Fourcc {} / Modifier {}",
|
||||
format.code,
|
||||
Into::<u64>::into(format.modifier)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn open_device(path: &str) -> DrmDeviceFd {
|
||||
let file = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(path)
|
||||
.expect("Failed to open device node");
|
||||
DrmDeviceFd::new(DeviceFd::from(Into::<OwnedFd>::into(file)))
|
||||
}
|
||||
|
||||
fn buffer_test(args: TestArgs) {
|
||||
// 1. create allocator
|
||||
let path = args
|
||||
.export
|
||||
.expect("Dumb buffer allocator requires an export device");
|
||||
info!("Export device: {}", path);
|
||||
let mut allocator = match args.allocator {
|
||||
AllocatorType::DumbBuffer => {
|
||||
let fd = open_device(&path);
|
||||
let dumb_allocator = DumbAllocator::new(fd);
|
||||
Box::new(DmabufAllocator(dumb_allocator)) as Box<dyn Allocator<Buffer = Dmabuf, Error = AnyError>>
|
||||
}
|
||||
AllocatorType::Gbm => {
|
||||
let fd = open_device(&path);
|
||||
let gbm = GbmDevice::new(fd).expect("Failed to init gbm device");
|
||||
let gbm_allocator = GbmAllocator::new(gbm, GbmBufferFlags::RENDERING);
|
||||
Box::new(DmabufAllocator(gbm_allocator)) as Box<_>
|
||||
}
|
||||
AllocatorType::Vulkan => {
|
||||
let node = DrmNode::from_path(&path).expect("Failed to find drm node");
|
||||
let instance =
|
||||
Instance::new(Version::VERSION_1_2, None).expect("Unable to create vulkan instance");
|
||||
let physical_device = PhysicalDevice::enumerate(&instance)
|
||||
.expect("Failed to enumerate physical devices")
|
||||
.filter(|phd| phd.has_device_extension(c"VK_EXT_physical_device_drm"))
|
||||
.find(|phd| {
|
||||
phd.primary_node().unwrap() == Some(node) || phd.render_node().unwrap() == Some(node)
|
||||
})
|
||||
.expect("Unable to find physical device");
|
||||
Box::new(DmabufAllocator(
|
||||
VulkanAllocator::new(&physical_device, args.usage_flags.unwrap())
|
||||
.expect("Failed to create vulkan allocator"),
|
||||
)) as Box<_>
|
||||
}
|
||||
};
|
||||
|
||||
// 2. alloc buffer
|
||||
let mut buffer = allocator
|
||||
.create_buffer(args.width, args.height, args.fourcc, &[args.modifier])
|
||||
.expect("Failed to allocate buffer");
|
||||
|
||||
// 3. render into buffer on src device
|
||||
match args.export_renderer {
|
||||
None => {}
|
||||
Some(RendererType::Gles) => {
|
||||
let path = PathBuf::from(&path);
|
||||
let device = EGLDevice::enumerate()
|
||||
.expect("Failed to enumerate EGL devices")
|
||||
.find(|device| {
|
||||
device.drm_device_path().ok().as_ref() == Some(&path)
|
||||
|| device.render_device_path().ok().as_ref() == Some(&path)
|
||||
})
|
||||
.expect("Unable to find egl device");
|
||||
let display = unsafe { EGLDisplay::new(device).expect("Failed to create EGL display") };
|
||||
|
||||
let context = EGLContext::new(&display).expect("Failed to create EGL context");
|
||||
let mut renderer = unsafe { GlesRenderer::new(context).expect("Failed to init GL ES renderer") };
|
||||
|
||||
render_into(&mut renderer, &mut buffer, args.width as i32, args.height as i32);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. import / render buffer on dst device
|
||||
let path = PathBuf::from(args.import);
|
||||
info!("Import device: {}", path.display());
|
||||
match args.import_renderer {
|
||||
None => {
|
||||
let device = EGLDevice::enumerate()
|
||||
.expect("Failed to enumerate EGL devices")
|
||||
.find(|device| {
|
||||
device.drm_device_path().ok().as_ref() == Some(&path)
|
||||
|| device.render_device_path().ok().as_ref() == Some(&path)
|
||||
})
|
||||
.expect("Unable to find egl device");
|
||||
let display = unsafe { EGLDisplay::new(device).expect("Failed to create EGL display") };
|
||||
|
||||
display
|
||||
.create_image_from_dmabuf(&buffer)
|
||||
.expect("Failed to import dmabuf");
|
||||
}
|
||||
Some(RendererType::Gles) => {
|
||||
let device = EGLDevice::enumerate()
|
||||
.expect("Failed to enumerate EGL devices")
|
||||
.find(|device| {
|
||||
device.drm_device_path().ok().as_ref() == Some(&path)
|
||||
|| device.render_device_path().ok().as_ref() == Some(&path)
|
||||
})
|
||||
.expect("Unable to find egl device");
|
||||
let display = unsafe { EGLDisplay::new(device).expect("Failed to create EGL display") };
|
||||
|
||||
let context = EGLContext::new(&display).expect("Failed to create EGL context");
|
||||
let mut renderer = unsafe { GlesRenderer::new(context).expect("Failed to init GL ES renderer") };
|
||||
|
||||
render_from::<_, GlesRenderbuffer>(
|
||||
&mut renderer,
|
||||
buffer,
|
||||
args.width as i32,
|
||||
args.height as i32,
|
||||
args.dump,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_into<R, T>(renderer: &mut R, buffer: &mut T, w: i32, h: i32)
|
||||
where
|
||||
R: Renderer + Bind<T>,
|
||||
{
|
||||
// Bind it as a framebuffer
|
||||
let mut framebuffer = renderer.bind(buffer).expect("Failed to bind dmabuf");
|
||||
|
||||
let mut frame = renderer
|
||||
.render(&mut framebuffer, (w, h).into(), Transform::Normal)
|
||||
.expect("Failed to create render frame");
|
||||
frame
|
||||
.clear(
|
||||
Color32F::new(1.0, 0.0, 0.0, 1.0),
|
||||
&[Rectangle::from_size((w / 2, h / 2).into())],
|
||||
)
|
||||
.expect("Render error");
|
||||
frame
|
||||
.clear(
|
||||
Color32F::new(0.0, 1.0, 0.0, 1.0),
|
||||
&[Rectangle::new((w / 2, 0).into(), (w / 2, h / 2).into())],
|
||||
)
|
||||
.expect("Render error");
|
||||
frame
|
||||
.clear(
|
||||
Color32F::new(0.0, 0.0, 1.0, 1.0),
|
||||
&[Rectangle::new((0, h / 2).into(), (w / 2, h / 2).into())],
|
||||
)
|
||||
.expect("Render error");
|
||||
frame
|
||||
.clear(
|
||||
Color32F::new(1.0, 1.0, 0.0, 1.0),
|
||||
&[Rectangle::new((w / 2, h / 2).into(), (w / 2, h / 2).into())],
|
||||
)
|
||||
.expect("Render error");
|
||||
frame
|
||||
.finish()
|
||||
.expect("Failed to finish render frame")
|
||||
.wait()
|
||||
.expect("Synchronization error");
|
||||
}
|
||||
|
||||
fn render_from<R, T>(renderer: &mut R, buffer: Dmabuf, w: i32, h: i32, dump: Option<String>)
|
||||
where
|
||||
R: Renderer + ImportDma + ExportMem + Offscreen<T>,
|
||||
{
|
||||
let texture = renderer
|
||||
.import_dmabuf(&buffer, None)
|
||||
.expect("Failed to import dmabuf");
|
||||
let mut offscreen = Offscreen::<T>::create_buffer(renderer, Fourcc::Abgr8888, (w, h).into())
|
||||
.expect("Failed to create offscreen buffer");
|
||||
let mut framebuffer = renderer
|
||||
.bind(&mut offscreen)
|
||||
.expect("Failed to bind offscreen buffer");
|
||||
let mut frame = renderer
|
||||
.render(&mut framebuffer, (w, h).into(), Transform::Normal)
|
||||
.expect("Failed to create render frame");
|
||||
frame
|
||||
.render_texture_at(
|
||||
&texture,
|
||||
(0, 0).into(),
|
||||
1,
|
||||
1.,
|
||||
Transform::Normal,
|
||||
&[Rectangle::from_size((w, h).into())],
|
||||
&[],
|
||||
1.0,
|
||||
)
|
||||
.expect("Failed to sample dmabuf");
|
||||
frame
|
||||
.finish()
|
||||
.expect("Failed to finish render frame")
|
||||
.wait()
|
||||
.expect("Synchronization error");
|
||||
|
||||
if let Some(path) = dump {
|
||||
let mapping = renderer
|
||||
.copy_framebuffer(
|
||||
&framebuffer,
|
||||
Rectangle::from_size((w, h).into()),
|
||||
Fourcc::Abgr8888,
|
||||
)
|
||||
.expect("Failed to map framebuffer");
|
||||
let copy = renderer.map_texture(&mapping).expect("Failed to read mapping");
|
||||
image::save_buffer(path, copy, w as u32, h as u32, image::ColorType::Rgba8)
|
||||
.expect("Failed to save image");
|
||||
}
|
||||
}
|
||||
|
||||
fn fourcc_from_string(name: &str) -> Result<Fourcc, &'static str> {
|
||||
Ok(match name {
|
||||
"Abgr1555" => Fourcc::Abgr1555,
|
||||
"Abgr16161616f" => Fourcc::Abgr16161616f,
|
||||
"Abgr2101010" => Fourcc::Abgr2101010,
|
||||
"Abgr4444" => Fourcc::Abgr4444,
|
||||
"Abgr8888" => Fourcc::Abgr8888,
|
||||
"Argb1555" => Fourcc::Argb1555,
|
||||
"Argb16161616f" => Fourcc::Argb16161616f,
|
||||
"Argb2101010" => Fourcc::Argb2101010,
|
||||
"Argb4444" => Fourcc::Argb4444,
|
||||
"Argb8888" => Fourcc::Argb8888,
|
||||
"Axbxgxrx106106106106" => Fourcc::Axbxgxrx106106106106,
|
||||
"Ayuv" => Fourcc::Ayuv,
|
||||
"Bgr233" => Fourcc::Bgr233,
|
||||
"Bgr565" => Fourcc::Bgr565,
|
||||
"Bgr565_a8" => Fourcc::Bgr565_a8,
|
||||
"Bgr888" => Fourcc::Bgr888,
|
||||
"Bgr888_a8" => Fourcc::Bgr888_a8,
|
||||
"Bgra1010102" => Fourcc::Bgra1010102,
|
||||
"Bgra4444" => Fourcc::Bgra4444,
|
||||
"Bgra5551" => Fourcc::Bgra5551,
|
||||
"Bgra8888" => Fourcc::Bgra8888,
|
||||
"Bgrx1010102" => Fourcc::Bgrx1010102,
|
||||
"Bgrx4444" => Fourcc::Bgrx4444,
|
||||
"Bgrx5551" => Fourcc::Bgrx5551,
|
||||
"Bgrx8888" => Fourcc::Bgrx8888,
|
||||
"Bgrx8888_a8" => Fourcc::Bgrx8888_a8,
|
||||
"Big_endian" => Fourcc::Big_endian,
|
||||
"C8" => Fourcc::C8,
|
||||
"Gr1616" => Fourcc::Gr1616,
|
||||
"Gr88" => Fourcc::Gr88,
|
||||
"Nv12" => Fourcc::Nv12,
|
||||
"Nv15" => Fourcc::Nv15,
|
||||
"Nv16" => Fourcc::Nv16,
|
||||
"Nv21" => Fourcc::Nv21,
|
||||
"Nv24" => Fourcc::Nv24,
|
||||
"Nv42" => Fourcc::Nv42,
|
||||
"Nv61" => Fourcc::Nv61,
|
||||
"P010" => Fourcc::P010,
|
||||
"P012" => Fourcc::P012,
|
||||
"P016" => Fourcc::P016,
|
||||
"P210" => Fourcc::P210,
|
||||
"Q401" => Fourcc::Q401,
|
||||
"Q410" => Fourcc::Q410,
|
||||
"R16" => Fourcc::R16,
|
||||
"R8" => Fourcc::R8,
|
||||
"Rg1616" => Fourcc::Rg1616,
|
||||
"Rg88" => Fourcc::Rg88,
|
||||
"Rgb332" => Fourcc::Rgb332,
|
||||
"Rgb565" => Fourcc::Rgb565,
|
||||
"Rgb565_a8" => Fourcc::Rgb565_a8,
|
||||
"Rgb888" => Fourcc::Rgb888,
|
||||
"Rgb888_a8" => Fourcc::Rgb888_a8,
|
||||
"Rgba1010102" => Fourcc::Rgba1010102,
|
||||
"Rgba4444" => Fourcc::Rgba4444,
|
||||
"Rgba5551" => Fourcc::Rgba5551,
|
||||
"Rgba8888" => Fourcc::Rgba8888,
|
||||
"Rgbx1010102" => Fourcc::Rgbx1010102,
|
||||
"Rgbx4444" => Fourcc::Rgbx4444,
|
||||
"Rgbx5551" => Fourcc::Rgbx5551,
|
||||
"Rgbx8888" => Fourcc::Rgbx8888,
|
||||
"Rgbx8888_a8" => Fourcc::Rgbx8888_a8,
|
||||
"Uyvy" => Fourcc::Uyvy,
|
||||
"Vuy101010" => Fourcc::Vuy101010,
|
||||
"Vuy888" => Fourcc::Vuy888,
|
||||
"Vyuy" => Fourcc::Vyuy,
|
||||
"X0l0" => Fourcc::X0l0,
|
||||
"X0l2" => Fourcc::X0l2,
|
||||
"Xbgr1555" => Fourcc::Xbgr1555,
|
||||
"Xbgr16161616f" => Fourcc::Xbgr16161616f,
|
||||
"Xbgr2101010" => Fourcc::Xbgr2101010,
|
||||
"Xbgr4444" => Fourcc::Xbgr4444,
|
||||
"Xbgr8888" => Fourcc::Xbgr8888,
|
||||
"Xbgr8888_a8" => Fourcc::Xbgr8888_a8,
|
||||
"Xrgb1555" => Fourcc::Xrgb1555,
|
||||
"Xrgb16161616f" => Fourcc::Xrgb16161616f,
|
||||
"Xrgb2101010" => Fourcc::Xrgb2101010,
|
||||
"Xrgb4444" => Fourcc::Xrgb4444,
|
||||
"Xrgb8888" => Fourcc::Xrgb8888,
|
||||
"Xrgb8888_a8" => Fourcc::Xrgb8888_a8,
|
||||
"Xvyu12_16161616" => Fourcc::Xvyu12_16161616,
|
||||
"Xvyu16161616" => Fourcc::Xvyu16161616,
|
||||
"Xvyu2101010" => Fourcc::Xvyu2101010,
|
||||
"Xyuv8888" => Fourcc::Xyuv8888,
|
||||
"Y0l0" => Fourcc::Y0l0,
|
||||
"Y0l2" => Fourcc::Y0l2,
|
||||
"Y210" => Fourcc::Y210,
|
||||
"Y212" => Fourcc::Y212,
|
||||
"Y216" => Fourcc::Y216,
|
||||
"Y410" => Fourcc::Y410,
|
||||
"Y412" => Fourcc::Y412,
|
||||
"Y416" => Fourcc::Y416,
|
||||
"Yuv410" => Fourcc::Yuv410,
|
||||
"Yuv411" => Fourcc::Yuv411,
|
||||
"Yuv420" => Fourcc::Yuv420,
|
||||
"Yuv420_10bit" => Fourcc::Yuv420_10bit,
|
||||
"Yuv420_8bit" => Fourcc::Yuv420_8bit,
|
||||
"Yuv422" => Fourcc::Yuv422,
|
||||
"Yuv444" => Fourcc::Yuv444,
|
||||
"Yuyv" => Fourcc::Yuyv,
|
||||
"Yvu410" => Fourcc::Yvu410,
|
||||
"Yvu411" => Fourcc::Yvu411,
|
||||
"Yvu420" => Fourcc::Yvu420,
|
||||
"Yvu422" => Fourcc::Yvu422,
|
||||
"Yvu444" => Fourcc::Yvu444,
|
||||
"Yvyu" => Fourcc::Yvyu,
|
||||
_ => {
|
||||
return Err("Unknown pixel format");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn modifier_from_string(modifier: &str) -> Result<Modifier, std::num::ParseIntError> {
|
||||
Ok(match modifier {
|
||||
"Invalid" => Modifier::Invalid,
|
||||
"Linear" => Modifier::Linear,
|
||||
x => Modifier::from(str::parse::<u64>(x)?),
|
||||
})
|
||||
}
|
||||
|
||||
fn usage_flags_from_string(flags: &str) -> Result<ImageUsageFlags, String> {
|
||||
let flags = flags
|
||||
.split('+')
|
||||
.map(|f| {
|
||||
Ok(match f.to_lowercase().trim() {
|
||||
"all" => ImageUsageFlags::all(),
|
||||
"empty" | "none" => ImageUsageFlags::empty(),
|
||||
"color_attachment" => ImageUsageFlags::COLOR_ATTACHMENT,
|
||||
"sampled" => ImageUsageFlags::SAMPLED,
|
||||
"transfer_src" => ImageUsageFlags::TRANSFER_SRC,
|
||||
"transfer_dst" => ImageUsageFlags::TRANSFER_DST,
|
||||
x => {
|
||||
return Err(format!("Unknown Usage Flag: {}", x));
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<ImageUsageFlags>, String>>()?;
|
||||
Ok(flags
|
||||
.into_iter()
|
||||
.fold(ImageUsageFlags::empty(), |akk, val| akk | val))
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use smithay::delegate_dispatch2;
|
||||
use smithay::reexports::wayland_server::Display;
|
||||
|
||||
use smithay::wayland::compositor::{CompositorClientState, CompositorHandler, CompositorState};
|
||||
|
||||
use wayland_server::backend::{ClientData, ClientId, DisconnectReason};
|
||||
use wayland_server::protocol::wl_surface::WlSurface;
|
||||
use wayland_server::{Client, ListeningSocket};
|
||||
|
||||
struct App {
|
||||
compositor_state: CompositorState,
|
||||
}
|
||||
|
||||
impl CompositorHandler for App {
|
||||
fn compositor_state(&mut self) -> &mut CompositorState {
|
||||
&mut self.compositor_state
|
||||
}
|
||||
|
||||
fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
|
||||
&client.get_data::<ClientState>().unwrap().compositor_state
|
||||
}
|
||||
|
||||
fn commit(&mut self, surface: &WlSurface) {
|
||||
dbg!("Commit", surface);
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut display: Display<App> = Display::new()?;
|
||||
let dh = display.handle();
|
||||
|
||||
let compositor_state = CompositorState::new::<App>(&dh);
|
||||
|
||||
let mut state = App { compositor_state };
|
||||
|
||||
let listener = ListeningSocket::bind("wayland-5").unwrap();
|
||||
|
||||
let mut clients = Vec::new();
|
||||
|
||||
loop {
|
||||
if let Some(stream) = listener.accept().unwrap() {
|
||||
println!("Got a client: {:?}", stream);
|
||||
|
||||
let client = display
|
||||
.handle()
|
||||
.insert_client(stream, Arc::new(ClientState::default()))
|
||||
.unwrap();
|
||||
clients.push(client);
|
||||
}
|
||||
|
||||
display.dispatch_clients(&mut state)?;
|
||||
display.flush_clients()?;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClientState {
|
||||
compositor_state: CompositorClientState,
|
||||
}
|
||||
impl ClientData for ClientState {
|
||||
fn initialized(&self, _client_id: ClientId) {
|
||||
println!("initialized");
|
||||
}
|
||||
|
||||
fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {
|
||||
println!("disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<CompositorState> for App {
|
||||
fn as_mut(&mut self) -> &mut CompositorState {
|
||||
&mut self.compositor_state
|
||||
}
|
||||
}
|
||||
|
||||
delegate_dispatch2!(App);
|
||||
Vendored
+295
@@ -0,0 +1,295 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ::winit::event_loop::pump_events::PumpStatus;
|
||||
use smithay::{
|
||||
backend::{
|
||||
input::{InputEvent, InputTime, KeyboardKeyEvent},
|
||||
renderer::{
|
||||
Color32F, Frame, Renderer,
|
||||
element::{
|
||||
Kind,
|
||||
surface::{WaylandSurfaceRenderElement, render_elements_from_surface_tree},
|
||||
},
|
||||
gles::GlesRenderer,
|
||||
utils::{draw_render_elements, on_commit_buffer_handler},
|
||||
},
|
||||
winit::{self, WinitEvent},
|
||||
},
|
||||
input::{Seat, SeatHandler, SeatState, keyboard::FilterResult},
|
||||
reexports::wayland_server::{Display, protocol::wl_seat},
|
||||
utils::{Rectangle, Serial, Transform},
|
||||
wayland::{
|
||||
buffer::BufferHandler,
|
||||
compositor::{
|
||||
CompositorClientState, CompositorHandler, CompositorState, SurfaceAttributes, TraversalAction,
|
||||
with_surface_tree_downward,
|
||||
},
|
||||
pointer_constraints::PointerConstraintsHandler,
|
||||
selection::{
|
||||
SelectionHandler,
|
||||
data_device::{DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler},
|
||||
},
|
||||
shell::xdg::{PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState},
|
||||
shm::{ShmHandler, ShmState},
|
||||
},
|
||||
};
|
||||
use wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use wayland_server::{
|
||||
Client, ListeningSocket,
|
||||
backend::{ClientData, ClientId, DisconnectReason},
|
||||
protocol::{
|
||||
wl_buffer,
|
||||
wl_surface::{self, WlSurface},
|
||||
},
|
||||
};
|
||||
|
||||
impl BufferHandler for App {
|
||||
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
|
||||
}
|
||||
|
||||
impl XdgShellHandler for App {
|
||||
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
|
||||
&mut self.xdg_shell_state
|
||||
}
|
||||
|
||||
fn new_toplevel(&mut self, surface: ToplevelSurface) {
|
||||
surface.with_pending_state(|state| {
|
||||
state.states.set(xdg_toplevel::State::Activated);
|
||||
});
|
||||
surface.send_configure();
|
||||
}
|
||||
|
||||
fn new_popup(&mut self, _surface: PopupSurface, _positioner: PositionerState) {
|
||||
// Handle popup creation here
|
||||
}
|
||||
|
||||
fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {
|
||||
// Handle popup grab here
|
||||
}
|
||||
|
||||
fn reposition_request(&mut self, _surface: PopupSurface, _positioner: PositionerState, _token: u32) {
|
||||
// Handle popup reposition here
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectionHandler for App {
|
||||
type SelectionUserData = ();
|
||||
}
|
||||
|
||||
impl DataDeviceHandler for App {
|
||||
fn data_device_state(&mut self) -> &mut DataDeviceState {
|
||||
&mut self.data_device_state
|
||||
}
|
||||
}
|
||||
|
||||
impl WaylandDndGrabHandler for App {}
|
||||
|
||||
impl CompositorHandler for App {
|
||||
fn compositor_state(&mut self) -> &mut CompositorState {
|
||||
&mut self.compositor_state
|
||||
}
|
||||
|
||||
fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
|
||||
&client.get_data::<ClientState>().unwrap().compositor_state
|
||||
}
|
||||
|
||||
fn commit(&mut self, surface: &WlSurface) {
|
||||
on_commit_buffer_handler::<Self>(surface);
|
||||
}
|
||||
}
|
||||
|
||||
impl ShmHandler for App {
|
||||
fn shm_state(&self) -> &ShmState {
|
||||
&self.shm_state
|
||||
}
|
||||
}
|
||||
|
||||
impl SeatHandler for App {
|
||||
type KeyboardFocus = WlSurface;
|
||||
type PointerFocus = WlSurface;
|
||||
type TouchFocus = WlSurface;
|
||||
|
||||
fn seat_state(&mut self) -> &mut SeatState<Self> {
|
||||
&mut self.seat_state
|
||||
}
|
||||
|
||||
fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
|
||||
fn cursor_image(&mut self, _seat: &Seat<Self>, _image: smithay::input::pointer::CursorImageStatus) {}
|
||||
}
|
||||
|
||||
impl PointerConstraintsHandler for App {}
|
||||
|
||||
struct App {
|
||||
compositor_state: CompositorState,
|
||||
xdg_shell_state: XdgShellState,
|
||||
shm_state: ShmState,
|
||||
seat_state: SeatState<Self>,
|
||||
data_device_state: DataDeviceState,
|
||||
|
||||
seat: Seat<Self>,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if let Ok(env_filter) = tracing_subscriber::EnvFilter::try_from_default_env() {
|
||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||
} else {
|
||||
tracing_subscriber::fmt().init();
|
||||
}
|
||||
|
||||
run_winit()
|
||||
}
|
||||
|
||||
pub fn run_winit() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut display: Display<App> = Display::new()?;
|
||||
let dh = display.handle();
|
||||
|
||||
let compositor_state = CompositorState::new::<App>(&dh);
|
||||
let shm_state = ShmState::new::<App>(&dh, vec![]);
|
||||
let mut seat_state = SeatState::new();
|
||||
let seat = seat_state.new_wl_seat(&dh, "winit");
|
||||
|
||||
let mut state = {
|
||||
App {
|
||||
compositor_state,
|
||||
xdg_shell_state: XdgShellState::new::<App>(&dh),
|
||||
shm_state,
|
||||
seat_state,
|
||||
data_device_state: DataDeviceState::new::<App>(&dh),
|
||||
seat,
|
||||
}
|
||||
};
|
||||
|
||||
let listener = ListeningSocket::bind("wayland-5").unwrap();
|
||||
let mut clients = Vec::new();
|
||||
|
||||
let (mut backend, mut winit) = winit::init::<GlesRenderer>()?;
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
let keyboard = state.seat.add_keyboard(Default::default(), 200, 200).unwrap();
|
||||
|
||||
unsafe { std::env::set_var("WAYLAND_DISPLAY", "wayland-5") };
|
||||
std::process::Command::new("weston-terminal").spawn().ok();
|
||||
|
||||
loop {
|
||||
let status = winit.dispatch_new_events(|event| match event {
|
||||
WinitEvent::Resized { .. } => {}
|
||||
WinitEvent::Input(event) => match event {
|
||||
InputEvent::Keyboard { event } => {
|
||||
keyboard.input::<(), _>(
|
||||
&mut state,
|
||||
event.key_code(),
|
||||
event.state(),
|
||||
0.into(),
|
||||
InputTime::now(),
|
||||
|_, _, _| {
|
||||
//
|
||||
FilterResult::Forward
|
||||
},
|
||||
);
|
||||
}
|
||||
InputEvent::PointerMotionAbsolute { .. } => {
|
||||
if let Some(surface) = state.xdg_shell_state.toplevel_surfaces().iter().next().cloned() {
|
||||
let surface = surface.wl_surface().clone();
|
||||
keyboard.set_focus(&mut state, Some(surface), 0.into());
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => (),
|
||||
});
|
||||
|
||||
match status {
|
||||
PumpStatus::Continue => (),
|
||||
PumpStatus::Exit(_) => return Ok(()),
|
||||
};
|
||||
|
||||
let size = backend.window_size();
|
||||
let damage = Rectangle::from_size(size);
|
||||
{
|
||||
let (renderer, mut framebuffer) = backend.bind().unwrap();
|
||||
let elements = state
|
||||
.xdg_shell_state
|
||||
.toplevel_surfaces()
|
||||
.iter()
|
||||
.flat_map(|surface| {
|
||||
render_elements_from_surface_tree(
|
||||
renderer,
|
||||
surface.wl_surface(),
|
||||
(0, 0),
|
||||
1.0,
|
||||
1.0,
|
||||
Kind::Unspecified,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<WaylandSurfaceRenderElement<GlesRenderer>>>();
|
||||
|
||||
let mut frame = renderer
|
||||
.render(&mut framebuffer, size, Transform::Flipped180)
|
||||
.unwrap();
|
||||
frame.clear(Color32F::new(0.1, 0.0, 0.0, 1.0), &[damage]).unwrap();
|
||||
draw_render_elements(&mut frame, 1.0, &elements, &[damage]).unwrap();
|
||||
// We rely on the nested compositor to do the sync for us
|
||||
let _ = frame.finish().unwrap();
|
||||
|
||||
for surface in state.xdg_shell_state.toplevel_surfaces() {
|
||||
send_frames_surface_tree(surface.wl_surface(), start_time.elapsed().as_millis() as u32);
|
||||
}
|
||||
|
||||
if let Some(stream) = listener.accept()? {
|
||||
println!("Got a client: {:?}", stream);
|
||||
|
||||
let client = display
|
||||
.handle()
|
||||
.insert_client(stream, Arc::new(ClientState::default()))
|
||||
.unwrap();
|
||||
clients.push(client);
|
||||
}
|
||||
|
||||
display.dispatch_clients(&mut state)?;
|
||||
display.flush_clients()?;
|
||||
}
|
||||
|
||||
// It is important that all events on the display have been dispatched and flushed to clients before
|
||||
// swapping buffers because this operation may block.
|
||||
backend.submit(Some(&[damage])).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_frames_surface_tree(surface: &wl_surface::WlSurface, time: u32) {
|
||||
with_surface_tree_downward(
|
||||
surface,
|
||||
(),
|
||||
|_, _, &()| TraversalAction::DoChildren(()),
|
||||
|_surf, states, &()| {
|
||||
// the surface may not have any user_data if it is a subsurface and has not
|
||||
// yet been committed
|
||||
for callback in states
|
||||
.cached_state
|
||||
.get::<SurfaceAttributes>()
|
||||
.current()
|
||||
.frame_callbacks
|
||||
.drain(..)
|
||||
{
|
||||
callback.done(time);
|
||||
}
|
||||
},
|
||||
|_, _, &()| true,
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClientState {
|
||||
compositor_state: CompositorClientState,
|
||||
}
|
||||
impl ClientData for ClientState {
|
||||
fn initialized(&self, _client_id: ClientId) {
|
||||
println!("initialized");
|
||||
}
|
||||
|
||||
fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {
|
||||
println!("disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
smithay::delegate_dispatch2!(App);
|
||||
Vendored
+110
@@ -0,0 +1,110 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use smithay::backend::input::InputTime;
|
||||
use smithay::input::{Seat, SeatHandler, SeatState, keyboard::FilterResult};
|
||||
use smithay::reexports::wayland_server::{
|
||||
Display, ListeningSocket,
|
||||
backend::{ClientData, ClientId, DisconnectReason},
|
||||
protocol::wl_surface::WlSurface,
|
||||
};
|
||||
use smithay::wayland::compositor::{CompositorClientState, CompositorHandler, CompositorState};
|
||||
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
|
||||
|
||||
struct App {
|
||||
compositor_state: CompositorState,
|
||||
seat_state: SeatState<Self>,
|
||||
seat: Seat<Self>,
|
||||
}
|
||||
|
||||
impl SeatHandler for App {
|
||||
type KeyboardFocus = WlSurface;
|
||||
type PointerFocus = WlSurface;
|
||||
type TouchFocus = WlSurface;
|
||||
|
||||
fn seat_state(&mut self) -> &mut SeatState<Self> {
|
||||
&mut self.seat_state
|
||||
}
|
||||
|
||||
fn focus_changed(&mut self, _seat: &Seat<Self>, _focused: Option<&WlSurface>) {}
|
||||
fn cursor_image(&mut self, _seat: &Seat<Self>, _image: smithay::input::pointer::CursorImageStatus) {}
|
||||
}
|
||||
|
||||
impl PointerConstraintsHandler for App {}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut display: Display<App> = Display::new()?;
|
||||
let dh = display.handle();
|
||||
|
||||
let compositor_state = CompositorState::new::<App>(&dh);
|
||||
let mut seat_state = SeatState::new();
|
||||
let seat = seat_state.new_wl_seat(&dh, "Example");
|
||||
|
||||
let mut state = App {
|
||||
compositor_state,
|
||||
seat_state,
|
||||
seat,
|
||||
};
|
||||
|
||||
let keyboard = state.seat.add_keyboard(Default::default(), 25, 600)?;
|
||||
|
||||
let listener = ListeningSocket::bind("wayland-5").unwrap();
|
||||
|
||||
let mut clients = Vec::new();
|
||||
|
||||
loop {
|
||||
if let Some(stream) = listener.accept().unwrap() {
|
||||
println!("Got a client: {:?}", stream);
|
||||
|
||||
let client = display
|
||||
.handle()
|
||||
.insert_client(stream, Arc::new(ClientState(CompositorClientState::default())))
|
||||
.unwrap();
|
||||
clients.push(client);
|
||||
}
|
||||
|
||||
keyboard.input(
|
||||
&mut state,
|
||||
smithay::backend::input::Keycode::from(9u32),
|
||||
smithay::backend::input::KeyState::Pressed,
|
||||
0.into(),
|
||||
InputTime::now(),
|
||||
|_, _, _| {
|
||||
if false {
|
||||
FilterResult::Intercept(0)
|
||||
} else {
|
||||
FilterResult::Forward
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
keyboard.set_focus(&mut state, Option::<WlSurface>::None, 0.into());
|
||||
|
||||
display.dispatch_clients(&mut state)?;
|
||||
display.flush_clients()?;
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientState(CompositorClientState);
|
||||
impl ClientData for ClientState {
|
||||
fn initialized(&self, _client_id: ClientId) {
|
||||
println!("initialized");
|
||||
}
|
||||
|
||||
fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {
|
||||
println!("disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
impl CompositorHandler for App {
|
||||
fn compositor_state(&mut self) -> &mut CompositorState {
|
||||
&mut self.compositor_state
|
||||
}
|
||||
|
||||
fn client_compositor_state<'a>(&self, client: &'a wayland_server::Client) -> &'a CompositorClientState {
|
||||
&client.get_data::<ClientState>().unwrap().0
|
||||
}
|
||||
|
||||
fn commit(&mut self, _surface: &WlSurface) {}
|
||||
}
|
||||
|
||||
smithay::delegate_dispatch2!(App);
|
||||
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
use drm_fourcc::{DrmFourcc, DrmModifier};
|
||||
use smithay::backend::{
|
||||
allocator::{
|
||||
Allocator, Buffer,
|
||||
dmabuf::AsDmabuf,
|
||||
vulkan::{ImageUsageFlags, VulkanAllocator},
|
||||
},
|
||||
vulkan::{Instance, PhysicalDevice, version::Version},
|
||||
};
|
||||
|
||||
fn main() {
|
||||
if let Ok(env_filter) = tracing_subscriber::EnvFilter::try_from_default_env() {
|
||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||
} else {
|
||||
tracing_subscriber::fmt().init();
|
||||
}
|
||||
|
||||
println!(
|
||||
"Available instance extensions: {:?}",
|
||||
Instance::enumerate_extensions().unwrap().collect::<Vec<_>>()
|
||||
);
|
||||
println!();
|
||||
|
||||
let instance = Instance::new(Version::VERSION_1_3, None).unwrap();
|
||||
|
||||
for (idx, phy) in PhysicalDevice::enumerate(&instance).unwrap().enumerate() {
|
||||
println!(
|
||||
"Device #{}: {} v{}, {:?}",
|
||||
idx,
|
||||
phy.name(),
|
||||
phy.api_version(),
|
||||
phy.driver()
|
||||
);
|
||||
}
|
||||
|
||||
let physical_device = PhysicalDevice::enumerate(&instance)
|
||||
.unwrap()
|
||||
.next()
|
||||
.expect("No physical devices");
|
||||
|
||||
// The allocator should create buffers that are suitable as render targets.
|
||||
let mut allocator = VulkanAllocator::new(&physical_device, ImageUsageFlags::COLOR_ATTACHMENT).unwrap();
|
||||
|
||||
let image = allocator
|
||||
.create_buffer(100, 200, DrmFourcc::Argb8888, &[DrmModifier::Linear])
|
||||
.expect("create");
|
||||
|
||||
assert_eq!(image.width(), 100);
|
||||
assert_eq!(image.height(), 200);
|
||||
|
||||
let image_dmabuf = image.export().expect("Export dmabuf");
|
||||
|
||||
drop(image);
|
||||
|
||||
let _image2 = allocator
|
||||
.create_buffer(200, 200, DrmFourcc::Argb8888, &[DrmModifier::Linear])
|
||||
.expect("create");
|
||||
|
||||
drop(allocator);
|
||||
drop(image_dmabuf);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
--- a/src/backend/renderer/gles/mod.rs
|
||||
+++ b/src/backend/renderer/gles/mod.rs
|
||||
@@ -883,6 +883,8 @@
|
||||
// why not store a `GlesTexture`? because the user might do so.
|
||||
// this is guaranteed a non-public internal type, so we are good.
|
||||
type CacheMap = HashMap<ContextId<GlesTexture>, Arc<GlesTextureInternal>>;
|
||||
+ static SHM_FORMAT_CHANGE_LOGS: std::sync::atomic::AtomicUsize =
|
||||
+ std::sync::atomic::AtomicUsize::new(0);
|
||||
|
||||
let mut surface_lock = surface.as_ref().map(|surface_data| {
|
||||
surface_data
|
||||
@@ -937,7 +939,33 @@
|
||||
surface_lock
|
||||
.as_ref()
|
||||
.and_then(|cache| cache.get(&id).cloned())
|
||||
- .filter(|texture| texture.size == (width, height).into())
|
||||
+ .filter(|texture| {
|
||||
+ let format_matches =
|
||||
+ texture.format == Some(internal_format) && texture.has_alpha == has_alpha;
|
||||
+ if !format_matches
|
||||
+ && tracing::enabled!(tracing::Level::DEBUG)
|
||||
+ && SHM_FORMAT_CHANGE_LOGS
|
||||
+ .fetch_update(
|
||||
+ std::sync::atomic::Ordering::Relaxed,
|
||||
+ std::sync::atomic::Ordering::Relaxed,
|
||||
+ |count| (count < 16).then_some(count + 1),
|
||||
+ )
|
||||
+ .is_ok()
|
||||
+ {
|
||||
+ tracing::debug!(
|
||||
+ old_width = texture.size.w,
|
||||
+ old_height = texture.size.h,
|
||||
+ new_width = width,
|
||||
+ new_height = height,
|
||||
+ old_internal_format = texture.format.unwrap_or(0),
|
||||
+ new_internal_format = internal_format,
|
||||
+ old_has_alpha = texture.has_alpha,
|
||||
+ new_has_alpha = has_alpha,
|
||||
+ "SHM texture cache format changed"
|
||||
+ );
|
||||
+ }
|
||||
+ texture.size == (width, height).into() && format_matches
|
||||
+ })
|
||||
.unwrap_or_else(|| {
|
||||
let mut tex = 0;
|
||||
unsafe { self.gl.GenTextures(1, &mut tex) };
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "smallvil"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||
bitflags = "2.2.1"
|
||||
|
||||
[dependencies.smithay]
|
||||
path = "../"
|
||||
default-features = false
|
||||
features = [
|
||||
"backend_winit",
|
||||
"wayland_frontend",
|
||||
"desktop",
|
||||
]
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
pub mod move_grab;
|
||||
pub use move_grab::MoveSurfaceGrab;
|
||||
|
||||
pub mod resize_grab;
|
||||
pub use resize_grab::ResizeSurfaceGrab;
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
//! Move grab is the state of a composer during which the client window is being dragged around.
|
||||
//!
|
||||
//! eg. Usually whenever a user clicks on the app's titlebar and starts dragging, the compositors
|
||||
//! enters a MoveSurfaceGrab state.
|
||||
|
||||
use crate::Smallvil;
|
||||
use smithay::{
|
||||
desktop::Window,
|
||||
input::pointer::{
|
||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
|
||||
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent,
|
||||
GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab,
|
||||
PointerInnerHandle, RelativeMotionEvent,
|
||||
},
|
||||
reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||
utils::{Logical, Point},
|
||||
};
|
||||
|
||||
pub struct MoveSurfaceGrab {
|
||||
pub start_data: PointerGrabStartData<Smallvil>,
|
||||
pub window: Window,
|
||||
pub initial_window_location: Point<i32, Logical>,
|
||||
}
|
||||
|
||||
impl PointerGrab<Smallvil> for MoveSurfaceGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
_focus: Option<(WlSurface, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus
|
||||
handle.motion(data, None, event);
|
||||
|
||||
let delta = event.location - self.start_data.location;
|
||||
let new_location = self.initial_window_location.to_f64() + delta;
|
||||
data.space
|
||||
.map_element(self.window.clone(), new_location.to_i32_round(), true);
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
focus: Option<(WlSurface, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
handle.relative_motion(data, focus, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
handle.button(data, event);
|
||||
|
||||
// The button is a button code as defined in the
|
||||
// Linux kernel's linux/input-event-codes.h header file, e.g. BTN_LEFT.
|
||||
const BTN_LEFT: u32 = 0x110;
|
||||
|
||||
if !handle.current_pressed().contains(&BTN_LEFT) {
|
||||
// No more buttons are pressed, release the grab.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
details: AxisFrame,
|
||||
) {
|
||||
handle.axis(data, details)
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut Smallvil, handle: &mut PointerInnerHandle<'_, Smallvil>) {
|
||||
handle.frame(data);
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
handle.gesture_swipe_begin(data, event)
|
||||
}
|
||||
|
||||
fn gesture_swipe_update(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
handle.gesture_swipe_update(data, event)
|
||||
}
|
||||
|
||||
fn gesture_swipe_end(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
handle.gesture_swipe_end(data, event)
|
||||
}
|
||||
|
||||
fn gesture_pinch_begin(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
handle.gesture_pinch_begin(data, event)
|
||||
}
|
||||
|
||||
fn gesture_pinch_update(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
handle.gesture_pinch_update(data, event)
|
||||
}
|
||||
|
||||
fn gesture_pinch_end(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
handle.gesture_pinch_end(data, event)
|
||||
}
|
||||
|
||||
fn gesture_hold_begin(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
handle.gesture_hold_begin(data, event)
|
||||
}
|
||||
|
||||
fn gesture_hold_end(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
handle.gesture_hold_end(data, event)
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &PointerGrabStartData<Smallvil> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, _data: &mut Smallvil) {}
|
||||
}
|
||||
+365
@@ -0,0 +1,365 @@
|
||||
//! Resize grab is the state of a composer during which the client window is being resized.
|
||||
//!
|
||||
//! eg. Usually whenever a user clicks on the app's border and starts dragging, the compositors
|
||||
//! enters a ResizeSurfaceGrab state.
|
||||
|
||||
use crate::Smallvil;
|
||||
use smithay::{
|
||||
desktop::{Space, Window},
|
||||
input::pointer::{
|
||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
|
||||
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent,
|
||||
GestureSwipeUpdateEvent, GrabStartData as PointerGrabStartData, MotionEvent, PointerGrab,
|
||||
PointerInnerHandle, RelativeMotionEvent,
|
||||
},
|
||||
reexports::{
|
||||
wayland_protocols::xdg::shell::server::xdg_toplevel, wayland_server::protocol::wl_surface::WlSurface,
|
||||
},
|
||||
utils::{Logical, Point, Rectangle, Size},
|
||||
wayland::{compositor, shell::xdg::SurfaceCachedState},
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
|
||||
bitflags::bitflags! {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ResizeEdge: u32 {
|
||||
const TOP = 0b0001;
|
||||
const BOTTOM = 0b0010;
|
||||
const LEFT = 0b0100;
|
||||
const RIGHT = 0b1000;
|
||||
|
||||
const TOP_LEFT = Self::TOP.bits() | Self::LEFT.bits();
|
||||
const BOTTOM_LEFT = Self::BOTTOM.bits() | Self::LEFT.bits();
|
||||
|
||||
const TOP_RIGHT = Self::TOP.bits() | Self::RIGHT.bits();
|
||||
const BOTTOM_RIGHT = Self::BOTTOM.bits() | Self::RIGHT.bits();
|
||||
}
|
||||
}
|
||||
|
||||
impl From<xdg_toplevel::ResizeEdge> for ResizeEdge {
|
||||
#[inline]
|
||||
fn from(x: xdg_toplevel::ResizeEdge) -> Self {
|
||||
Self::from_bits(x as u32).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ResizeSurfaceGrab {
|
||||
start_data: PointerGrabStartData<Smallvil>,
|
||||
window: Window,
|
||||
|
||||
edges: ResizeEdge,
|
||||
|
||||
initial_rect: Rectangle<i32, Logical>,
|
||||
last_window_size: Size<i32, Logical>,
|
||||
}
|
||||
|
||||
impl ResizeSurfaceGrab {
|
||||
pub fn start(
|
||||
start_data: PointerGrabStartData<Smallvil>,
|
||||
window: Window,
|
||||
edges: ResizeEdge,
|
||||
initial_window_rect: Rectangle<i32, Logical>,
|
||||
) -> Self {
|
||||
let initial_rect = initial_window_rect;
|
||||
|
||||
ResizeSurfaceState::with(window.toplevel().unwrap().wl_surface(), |state| {
|
||||
*state = ResizeSurfaceState::Resizing { edges, initial_rect };
|
||||
});
|
||||
|
||||
Self {
|
||||
start_data,
|
||||
window,
|
||||
edges,
|
||||
initial_rect,
|
||||
last_window_size: initial_rect.size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerGrab<Smallvil> for ResizeSurfaceGrab {
|
||||
fn motion(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
_focus: Option<(WlSurface, Point<f64, Logical>)>,
|
||||
event: &MotionEvent,
|
||||
) {
|
||||
// While the grab is active, no client has pointer focus
|
||||
handle.motion(data, None, event);
|
||||
|
||||
let mut delta = event.location - self.start_data.location;
|
||||
|
||||
let mut new_window_width = self.initial_rect.size.w;
|
||||
let mut new_window_height = self.initial_rect.size.h;
|
||||
|
||||
if self.edges.intersects(ResizeEdge::LEFT | ResizeEdge::RIGHT) {
|
||||
if self.edges.intersects(ResizeEdge::LEFT) {
|
||||
delta.x = -delta.x;
|
||||
}
|
||||
|
||||
new_window_width = (self.initial_rect.size.w as f64 + delta.x) as i32;
|
||||
}
|
||||
|
||||
if self.edges.intersects(ResizeEdge::TOP | ResizeEdge::BOTTOM) {
|
||||
if self.edges.intersects(ResizeEdge::TOP) {
|
||||
delta.y = -delta.y;
|
||||
}
|
||||
|
||||
new_window_height = (self.initial_rect.size.h as f64 + delta.y) as i32;
|
||||
}
|
||||
|
||||
let (min_size, max_size) =
|
||||
compositor::with_states(self.window.toplevel().unwrap().wl_surface(), |states| {
|
||||
let mut guard = states.cached_state.get::<SurfaceCachedState>();
|
||||
let data = guard.current();
|
||||
(data.min_size, data.max_size)
|
||||
});
|
||||
|
||||
let min_width = min_size.w.max(1);
|
||||
let min_height = min_size.h.max(1);
|
||||
|
||||
let max_width = if max_size.w == 0 { i32::MAX } else { max_size.w };
|
||||
let max_height = if max_size.h == 0 { i32::MAX } else { max_size.h };
|
||||
|
||||
self.last_window_size = Size::from((
|
||||
new_window_width.max(min_width).min(max_width),
|
||||
new_window_height.max(min_height).min(max_height),
|
||||
));
|
||||
|
||||
let xdg = self.window.toplevel().unwrap();
|
||||
xdg.with_pending_state(|state| {
|
||||
state.states.set(xdg_toplevel::State::Resizing);
|
||||
state.size = Some(self.last_window_size);
|
||||
});
|
||||
|
||||
xdg.send_pending_configure();
|
||||
}
|
||||
|
||||
fn relative_motion(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
focus: Option<(WlSurface, Point<f64, Logical>)>,
|
||||
event: &RelativeMotionEvent,
|
||||
) {
|
||||
handle.relative_motion(data, focus, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
handle.button(data, event);
|
||||
|
||||
// The button is a button code as defined in the
|
||||
// Linux kernel's linux/input-event-codes.h header file, e.g. BTN_LEFT.
|
||||
const BTN_LEFT: u32 = 0x110;
|
||||
|
||||
if !handle.current_pressed().contains(&BTN_LEFT) {
|
||||
// No more buttons are pressed, release the grab.
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
|
||||
let xdg = self.window.toplevel().unwrap();
|
||||
xdg.with_pending_state(|state| {
|
||||
state.states.unset(xdg_toplevel::State::Resizing);
|
||||
state.size = Some(self.last_window_size);
|
||||
});
|
||||
|
||||
xdg.send_pending_configure();
|
||||
|
||||
ResizeSurfaceState::with(xdg.wl_surface(), |state| {
|
||||
*state = ResizeSurfaceState::WaitingForLastCommit {
|
||||
edges: self.edges,
|
||||
initial_rect: self.initial_rect,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
details: AxisFrame,
|
||||
) {
|
||||
handle.axis(data, details)
|
||||
}
|
||||
|
||||
fn frame(&mut self, data: &mut Smallvil, handle: &mut PointerInnerHandle<'_, Smallvil>) {
|
||||
handle.frame(data);
|
||||
}
|
||||
|
||||
fn gesture_swipe_begin(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &GestureSwipeBeginEvent,
|
||||
) {
|
||||
handle.gesture_swipe_begin(data, event)
|
||||
}
|
||||
|
||||
fn gesture_swipe_update(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &GestureSwipeUpdateEvent,
|
||||
) {
|
||||
handle.gesture_swipe_update(data, event)
|
||||
}
|
||||
|
||||
fn gesture_swipe_end(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &GestureSwipeEndEvent,
|
||||
) {
|
||||
handle.gesture_swipe_end(data, event)
|
||||
}
|
||||
|
||||
fn gesture_pinch_begin(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &GesturePinchBeginEvent,
|
||||
) {
|
||||
handle.gesture_pinch_begin(data, event)
|
||||
}
|
||||
|
||||
fn gesture_pinch_update(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &GesturePinchUpdateEvent,
|
||||
) {
|
||||
handle.gesture_pinch_update(data, event)
|
||||
}
|
||||
|
||||
fn gesture_pinch_end(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &GesturePinchEndEvent,
|
||||
) {
|
||||
handle.gesture_pinch_end(data, event)
|
||||
}
|
||||
|
||||
fn gesture_hold_begin(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &GestureHoldBeginEvent,
|
||||
) {
|
||||
handle.gesture_hold_begin(data, event)
|
||||
}
|
||||
|
||||
fn gesture_hold_end(
|
||||
&mut self,
|
||||
data: &mut Smallvil,
|
||||
handle: &mut PointerInnerHandle<'_, Smallvil>,
|
||||
event: &GestureHoldEndEvent,
|
||||
) {
|
||||
handle.gesture_hold_end(data, event)
|
||||
}
|
||||
|
||||
fn start_data(&self) -> &PointerGrabStartData<Smallvil> {
|
||||
&self.start_data
|
||||
}
|
||||
|
||||
fn unset(&mut self, _data: &mut Smallvil) {}
|
||||
}
|
||||
|
||||
/// State of the resize operation.
|
||||
///
|
||||
/// It is stored inside of WlSurface,
|
||||
/// and can be accessed using [`ResizeSurfaceState::with`]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
|
||||
enum ResizeSurfaceState {
|
||||
#[default]
|
||||
Idle,
|
||||
Resizing {
|
||||
edges: ResizeEdge,
|
||||
/// The initial window size and location.
|
||||
initial_rect: Rectangle<i32, Logical>,
|
||||
},
|
||||
/// Resize is done, we are now waiting for last commit, to do the final move
|
||||
WaitingForLastCommit {
|
||||
edges: ResizeEdge,
|
||||
/// The initial window size and location.
|
||||
initial_rect: Rectangle<i32, Logical>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ResizeSurfaceState {
|
||||
fn with<F, T>(surface: &WlSurface, cb: F) -> T
|
||||
where
|
||||
F: FnOnce(&mut Self) -> T,
|
||||
{
|
||||
compositor::with_states(surface, |states| {
|
||||
states.data_map.insert_if_missing(RefCell::<Self>::default);
|
||||
let state = states.data_map.get::<RefCell<Self>>().unwrap();
|
||||
|
||||
cb(&mut state.borrow_mut())
|
||||
})
|
||||
}
|
||||
|
||||
fn commit(&mut self) -> Option<(ResizeEdge, Rectangle<i32, Logical>)> {
|
||||
match *self {
|
||||
Self::Resizing { edges, initial_rect } => Some((edges, initial_rect)),
|
||||
Self::WaitingForLastCommit { edges, initial_rect } => {
|
||||
// The resize is done, let's go back to idle
|
||||
*self = Self::Idle;
|
||||
|
||||
Some((edges, initial_rect))
|
||||
}
|
||||
Self::Idle => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Should be called on `WlSurface::commit`
|
||||
pub fn handle_commit(space: &mut Space<Window>, surface: &WlSurface) -> Option<()> {
|
||||
let window = space
|
||||
.elements()
|
||||
.find(|w| w.toplevel().unwrap().wl_surface() == surface)
|
||||
.cloned()?;
|
||||
|
||||
let mut window_loc = space.element_location(&window)?;
|
||||
let geometry = window.geometry();
|
||||
|
||||
let new_loc: Point<Option<i32>, Logical> = ResizeSurfaceState::with(surface, |state| {
|
||||
state
|
||||
.commit()
|
||||
.and_then(|(edges, initial_rect)| {
|
||||
// If the window is being resized by top or left, its location must be adjusted
|
||||
// accordingly.
|
||||
edges.intersects(ResizeEdge::TOP_LEFT).then(|| {
|
||||
let new_x = edges
|
||||
.intersects(ResizeEdge::LEFT)
|
||||
.then_some(initial_rect.loc.x + (initial_rect.size.w - geometry.size.w));
|
||||
|
||||
let new_y = edges
|
||||
.intersects(ResizeEdge::TOP)
|
||||
.then_some(initial_rect.loc.y + (initial_rect.size.h - geometry.size.h));
|
||||
|
||||
(new_x, new_y).into()
|
||||
})
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
if let Some(new_x) = new_loc.x {
|
||||
window_loc.x = new_x;
|
||||
}
|
||||
if let Some(new_y) = new_loc.y {
|
||||
window_loc.y = new_y;
|
||||
}
|
||||
|
||||
if new_loc.x.is_some() || new_loc.y.is_some() {
|
||||
// If TOP or LEFT side of the window got resized, we have to move it
|
||||
space.map_element(window, window_loc, false);
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use crate::{Smallvil, grabs::resize_grab, state::ClientState};
|
||||
use smithay::{
|
||||
backend::renderer::utils::on_commit_buffer_handler,
|
||||
reexports::wayland_server::{
|
||||
Client,
|
||||
protocol::{wl_buffer, wl_surface::WlSurface},
|
||||
},
|
||||
wayland::{
|
||||
buffer::BufferHandler,
|
||||
compositor::{
|
||||
CompositorClientState, CompositorHandler, CompositorState, get_parent, is_sync_subsurface,
|
||||
},
|
||||
shm::{ShmHandler, ShmState},
|
||||
},
|
||||
};
|
||||
|
||||
use super::xdg_shell;
|
||||
|
||||
impl CompositorHandler for Smallvil {
|
||||
fn compositor_state(&mut self) -> &mut CompositorState {
|
||||
&mut self.compositor_state
|
||||
}
|
||||
|
||||
fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState {
|
||||
&client.get_data::<ClientState>().unwrap().compositor_state
|
||||
}
|
||||
|
||||
fn commit(&mut self, surface: &WlSurface) {
|
||||
on_commit_buffer_handler::<Self>(surface);
|
||||
if !is_sync_subsurface(surface) {
|
||||
let mut root = surface.clone();
|
||||
while let Some(parent) = get_parent(&root) {
|
||||
root = parent;
|
||||
}
|
||||
if let Some(window) = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|w| w.toplevel().unwrap().wl_surface() == &root)
|
||||
{
|
||||
window.on_commit();
|
||||
}
|
||||
};
|
||||
|
||||
xdg_shell::handle_commit(&mut self.popups, &self.space, surface);
|
||||
resize_grab::handle_commit(&mut self.space, surface);
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferHandler for Smallvil {
|
||||
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
|
||||
}
|
||||
|
||||
impl ShmHandler for Smallvil {
|
||||
fn shm_state(&self) -> &ShmState {
|
||||
&self.shm_state
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
mod compositor;
|
||||
mod xdg_shell;
|
||||
|
||||
use crate::Smallvil;
|
||||
|
||||
//
|
||||
// Wl Seat
|
||||
//
|
||||
|
||||
use smithay::input::dnd::{DnDGrab, DndGrabHandler, GrabType, Source};
|
||||
use smithay::input::pointer::Focus;
|
||||
use smithay::input::{Seat, SeatHandler, SeatState};
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::Serial;
|
||||
use smithay::wayland::output::OutputHandler;
|
||||
use smithay::wayland::pointer_constraints::PointerConstraintsHandler;
|
||||
use smithay::wayland::selection::SelectionHandler;
|
||||
use smithay::wayland::selection::data_device::{
|
||||
DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus,
|
||||
};
|
||||
|
||||
impl SeatHandler for Smallvil {
|
||||
type KeyboardFocus = WlSurface;
|
||||
type PointerFocus = WlSurface;
|
||||
type TouchFocus = WlSurface;
|
||||
|
||||
fn seat_state(&mut self) -> &mut SeatState<Smallvil> {
|
||||
&mut self.seat_state
|
||||
}
|
||||
|
||||
fn cursor_image(&mut self, _seat: &Seat<Self>, _image: smithay::input::pointer::CursorImageStatus) {}
|
||||
|
||||
fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&WlSurface>) {
|
||||
let dh = &self.display_handle;
|
||||
let client = focused.and_then(|s| dh.get_client(s.id()).ok());
|
||||
set_data_device_focus(dh, seat, client);
|
||||
}
|
||||
}
|
||||
|
||||
impl PointerConstraintsHandler for Smallvil {}
|
||||
|
||||
//
|
||||
// Wl Data Device
|
||||
//
|
||||
|
||||
impl SelectionHandler for Smallvil {
|
||||
type SelectionUserData = ();
|
||||
}
|
||||
|
||||
impl DataDeviceHandler for Smallvil {
|
||||
fn data_device_state(&mut self) -> &mut DataDeviceState {
|
||||
&mut self.data_device_state
|
||||
}
|
||||
}
|
||||
|
||||
impl DndGrabHandler for Smallvil {}
|
||||
impl WaylandDndGrabHandler for Smallvil {
|
||||
fn dnd_requested<S: Source>(
|
||||
&mut self,
|
||||
source: S,
|
||||
_icon: Option<WlSurface>,
|
||||
seat: Seat<Self>,
|
||||
serial: Serial,
|
||||
type_: GrabType,
|
||||
) {
|
||||
match type_ {
|
||||
GrabType::Pointer => {
|
||||
let ptr = seat.get_pointer().unwrap();
|
||||
let start_data = ptr.grab_start_data().unwrap();
|
||||
|
||||
// create a dnd grab to start the operation
|
||||
let grab = DnDGrab::new_pointer(&self.display_handle, start_data, source, seat);
|
||||
ptr.set_grab(self, grab, serial, Focus::Keep);
|
||||
}
|
||||
GrabType::Touch => {
|
||||
// smallvil lacks touch handling
|
||||
source.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Wl Output & Xdg Output
|
||||
//
|
||||
|
||||
impl OutputHandler for Smallvil {}
|
||||
|
||||
smithay::delegate_dispatch2!(Smallvil);
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
use smithay::{
|
||||
desktop::{PopupKind, PopupManager, Space, Window, find_popup_root_surface, get_popup_toplevel_coords},
|
||||
input::{
|
||||
Seat,
|
||||
pointer::{Focus, GrabStartData as PointerGrabStartData},
|
||||
},
|
||||
reexports::{
|
||||
wayland_protocols::xdg::shell::server::xdg_toplevel,
|
||||
wayland_server::{
|
||||
Resource,
|
||||
protocol::{wl_seat, wl_surface::WlSurface},
|
||||
},
|
||||
},
|
||||
utils::{Rectangle, Serial},
|
||||
wayland::{
|
||||
compositor::with_states,
|
||||
shell::xdg::{
|
||||
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
|
||||
XdgToplevelSurfaceData,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Smallvil,
|
||||
grabs::{MoveSurfaceGrab, ResizeSurfaceGrab},
|
||||
};
|
||||
|
||||
impl XdgShellHandler for Smallvil {
|
||||
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
|
||||
&mut self.xdg_shell_state
|
||||
}
|
||||
|
||||
fn new_toplevel(&mut self, surface: ToplevelSurface) {
|
||||
let window = Window::new_wayland_window(surface);
|
||||
self.space.map_element(window, (0, 0), false);
|
||||
}
|
||||
|
||||
fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) {
|
||||
self.unconstrain_popup(&surface);
|
||||
let _ = self.popups.track_popup(PopupKind::Xdg(surface));
|
||||
}
|
||||
|
||||
fn reposition_request(&mut self, surface: PopupSurface, positioner: PositionerState, token: u32) {
|
||||
surface.with_pending_state(|state| {
|
||||
let geometry = positioner.get_geometry();
|
||||
state.geometry = geometry;
|
||||
state.positioner = positioner;
|
||||
});
|
||||
self.unconstrain_popup(&surface);
|
||||
surface.send_repositioned(token);
|
||||
}
|
||||
|
||||
fn move_request(&mut self, surface: ToplevelSurface, seat: wl_seat::WlSeat, serial: Serial) {
|
||||
let seat = Seat::from_resource(&seat).unwrap();
|
||||
|
||||
let wl_surface = surface.wl_surface();
|
||||
|
||||
if let Some(start_data) = check_grab(&seat, wl_surface, serial) {
|
||||
let pointer = seat.get_pointer().unwrap();
|
||||
|
||||
let window = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|w| w.toplevel().unwrap().wl_surface() == wl_surface)
|
||||
.unwrap()
|
||||
.clone();
|
||||
let initial_window_location = self.space.element_location(&window).unwrap();
|
||||
|
||||
let grab = MoveSurfaceGrab {
|
||||
start_data,
|
||||
window,
|
||||
initial_window_location,
|
||||
};
|
||||
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_request(
|
||||
&mut self,
|
||||
surface: ToplevelSurface,
|
||||
seat: wl_seat::WlSeat,
|
||||
serial: Serial,
|
||||
edges: xdg_toplevel::ResizeEdge,
|
||||
) {
|
||||
let seat = Seat::from_resource(&seat).unwrap();
|
||||
|
||||
let wl_surface = surface.wl_surface();
|
||||
|
||||
if let Some(start_data) = check_grab(&seat, wl_surface, serial) {
|
||||
let pointer = seat.get_pointer().unwrap();
|
||||
|
||||
let window = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|w| w.toplevel().unwrap().wl_surface() == wl_surface)
|
||||
.unwrap()
|
||||
.clone();
|
||||
let initial_window_location = self.space.element_location(&window).unwrap();
|
||||
let initial_window_size = window.geometry().size;
|
||||
|
||||
surface.with_pending_state(|state| {
|
||||
state.states.set(xdg_toplevel::State::Resizing);
|
||||
});
|
||||
|
||||
surface.send_pending_configure();
|
||||
|
||||
let grab = ResizeSurfaceGrab::start(
|
||||
start_data,
|
||||
window,
|
||||
edges.into(),
|
||||
Rectangle::new(initial_window_location, initial_window_size),
|
||||
);
|
||||
|
||||
pointer.set_grab(self, grab, serial, Focus::Clear);
|
||||
}
|
||||
}
|
||||
|
||||
fn grab(&mut self, _surface: PopupSurface, _seat: wl_seat::WlSeat, _serial: Serial) {
|
||||
// TODO popup grabs
|
||||
}
|
||||
}
|
||||
|
||||
fn check_grab(
|
||||
seat: &Seat<Smallvil>,
|
||||
surface: &WlSurface,
|
||||
serial: Serial,
|
||||
) -> Option<PointerGrabStartData<Smallvil>> {
|
||||
let pointer = seat.get_pointer()?;
|
||||
|
||||
// Check that this surface has a click grab.
|
||||
if !pointer.has_grab(serial) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start_data = pointer.grab_start_data()?;
|
||||
|
||||
let (focus, _) = start_data.focus.as_ref()?;
|
||||
// If the focus was for a different surface, ignore the request.
|
||||
if !focus.id().same_client_as(&surface.id()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(start_data)
|
||||
}
|
||||
|
||||
/// Should be called on `WlSurface::commit`
|
||||
pub fn handle_commit(popups: &mut PopupManager, space: &Space<Window>, surface: &WlSurface) {
|
||||
// Handle toplevel commits.
|
||||
if let Some(window) = space
|
||||
.elements()
|
||||
.find(|w| w.toplevel().unwrap().wl_surface() == surface)
|
||||
.cloned()
|
||||
{
|
||||
let initial_configure_sent = with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.initial_configure_sent
|
||||
});
|
||||
|
||||
if !initial_configure_sent {
|
||||
window.toplevel().unwrap().send_configure();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle popup commits.
|
||||
popups.commit(surface);
|
||||
if let Some(popup) = popups.find_popup(surface) {
|
||||
match popup {
|
||||
PopupKind::Xdg(ref xdg) => {
|
||||
if !xdg.is_initial_configure_sent() {
|
||||
// NOTE: This should never fail as the initial configure is always
|
||||
// allowed.
|
||||
xdg.send_configure().expect("initial configure failed");
|
||||
}
|
||||
}
|
||||
PopupKind::InputMethod(ref _input_method) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Smallvil {
|
||||
fn unconstrain_popup(&self, popup: &PopupSurface) {
|
||||
let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else {
|
||||
return;
|
||||
};
|
||||
let Some(window) = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|w| w.toplevel().unwrap().wl_surface() == &root)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let output = self.space.outputs().next().unwrap();
|
||||
let output_geo = self.space.output_geometry(output).unwrap();
|
||||
let window_geo = self.space.element_geometry(window).unwrap();
|
||||
|
||||
// The target geometry for the positioner should be relative to its parent's geometry, so
|
||||
// we will compute that here.
|
||||
let mut target = output_geo;
|
||||
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
|
||||
target.loc -= window_geo.loc;
|
||||
|
||||
popup.with_pending_state(|state| {
|
||||
state.geometry = state.positioner.get_unconstrained_geometry(target);
|
||||
});
|
||||
}
|
||||
}
|
||||
Loaded 100 of 391 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user