* Add `Window::set_theme` * typo * fix linux build * fix wayland * review changes * update docs * update changelog * pin `image` dep * suppport falling back to system default * fix linux * default to dark on macOS and x11 * fix `setAppearance` definition * add macOS notes * update docs * Update CHANGELOG.md Co-authored-by: Markus Siglreithmaier <m.siglreith@gmail.com> * update doc * Revert "pin `image` dep" This reverts commit 7517f7c5065b4089ca146ce8799dab445ec32068. * Update theme example with Window::set_theme * Fix Window::theme getter on macOS Co-authored-by: Markus Siglreithmaier <m.siglreith@gmail.com> Co-authored-by: Mads Marquart <mads@marquart.dk>
564 lines
19 KiB
Rust
564 lines
19 KiB
Rust
use std::cell::Cell;
|
|
use std::mem::ManuallyDrop;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use sctk::reexports::client::protocol::wl_compositor::WlCompositor;
|
|
use sctk::reexports::client::protocol::wl_output::WlOutput;
|
|
use sctk::reexports::client::Attached;
|
|
use sctk::reexports::protocols::staging::xdg_activation::v1::client::xdg_activation_token_v1;
|
|
use sctk::reexports::protocols::staging::xdg_activation::v1::client::xdg_activation_v1::XdgActivationV1;
|
|
|
|
use sctk::environment::Environment;
|
|
use sctk::window::{Decorations, Window};
|
|
|
|
use crate::dpi::{LogicalPosition, LogicalSize};
|
|
|
|
use crate::event::{Ime, WindowEvent};
|
|
use crate::platform_impl::wayland;
|
|
use crate::platform_impl::wayland::env::WinitEnv;
|
|
use crate::platform_impl::wayland::event_loop::{EventSink, WinitState};
|
|
use crate::platform_impl::wayland::seat::pointer::WinitPointer;
|
|
use crate::platform_impl::wayland::seat::text_input::TextInputHandler;
|
|
use crate::platform_impl::wayland::WindowId;
|
|
use crate::window::{CursorGrabMode, CursorIcon, Theme, UserAttentionType};
|
|
|
|
use super::WinitFrame;
|
|
|
|
/// A request to SCTK window from Winit window.
|
|
#[derive(Debug, Clone)]
|
|
pub enum WindowRequest {
|
|
/// Set fullscreen.
|
|
///
|
|
/// Passing `None` will set it on the current monitor.
|
|
Fullscreen(Option<WlOutput>),
|
|
|
|
/// Unset fullscreen.
|
|
UnsetFullscreen,
|
|
|
|
/// Show cursor for the certain window or not.
|
|
ShowCursor(bool),
|
|
|
|
/// Change the cursor icon.
|
|
NewCursorIcon(CursorIcon),
|
|
|
|
/// Change cursor grabbing mode.
|
|
SetCursorGrabMode(CursorGrabMode),
|
|
|
|
/// Set cursor position.
|
|
SetLockedCursorPosition(LogicalPosition<u32>),
|
|
|
|
/// Drag window.
|
|
DragWindow,
|
|
|
|
/// Maximize the window.
|
|
Maximize(bool),
|
|
|
|
/// Minimize the window.
|
|
Minimize,
|
|
|
|
/// Request decorations change.
|
|
Decorate(bool),
|
|
|
|
/// Make the window resizeable.
|
|
Resizeable(bool),
|
|
|
|
/// Set the title for window.
|
|
Title(String),
|
|
|
|
/// Min size.
|
|
MinSize(Option<LogicalSize<u32>>),
|
|
|
|
/// Max size.
|
|
MaxSize(Option<LogicalSize<u32>>),
|
|
|
|
/// New frame size.
|
|
FrameSize(LogicalSize<u32>),
|
|
|
|
/// Set IME window position.
|
|
ImePosition(LogicalPosition<u32>),
|
|
|
|
/// Enable IME on the given window.
|
|
AllowIme(bool),
|
|
|
|
/// Request Attention.
|
|
///
|
|
/// `None` unsets the attention request.
|
|
Attention(Option<UserAttentionType>),
|
|
|
|
/// Passthrough mouse input to underlying windows.
|
|
PassthroughMouseInput(bool),
|
|
|
|
/// Redraw was requested.
|
|
Redraw,
|
|
|
|
/// Window should be closed.
|
|
Close,
|
|
|
|
/// Change window theme.
|
|
Theme(Option<Theme>),
|
|
}
|
|
|
|
// The window update comming from the compositor.
|
|
#[derive(Default, Debug, Clone, Copy)]
|
|
pub struct WindowCompositorUpdate {
|
|
/// New window size.
|
|
pub size: Option<LogicalSize<u32>>,
|
|
|
|
/// New scale factor.
|
|
pub scale_factor: Option<i32>,
|
|
|
|
/// Close the window.
|
|
pub close_window: bool,
|
|
}
|
|
|
|
impl WindowCompositorUpdate {
|
|
pub fn new() -> Self {
|
|
Default::default()
|
|
}
|
|
}
|
|
|
|
/// Pending update to a window requested by the user.
|
|
#[derive(Default, Debug, Clone, Copy)]
|
|
pub struct WindowUserRequest {
|
|
/// Whether `redraw` was requested.
|
|
pub redraw_requested: bool,
|
|
|
|
/// Wether the frame should be refreshed.
|
|
pub refresh_frame: bool,
|
|
}
|
|
|
|
impl WindowUserRequest {
|
|
pub fn new() -> Self {
|
|
Default::default()
|
|
}
|
|
}
|
|
|
|
/// A handle to perform operations on SCTK window
|
|
/// and react to events.
|
|
pub struct WindowHandle {
|
|
/// An actual window.
|
|
pub window: ManuallyDrop<Window<WinitFrame>>,
|
|
|
|
/// The current size of the window.
|
|
pub size: Arc<Mutex<LogicalSize<u32>>>,
|
|
|
|
/// A pending requests to SCTK window.
|
|
pub pending_window_requests: Arc<Mutex<Vec<WindowRequest>>>,
|
|
|
|
/// Current cursor icon.
|
|
pub cursor_icon: Cell<CursorIcon>,
|
|
|
|
/// Whether the window is resizable.
|
|
pub is_resizable: Cell<bool>,
|
|
|
|
/// Allow IME events for that window.
|
|
pub ime_allowed: Cell<bool>,
|
|
|
|
/// Visible cursor or not.
|
|
cursor_visible: Cell<bool>,
|
|
|
|
/// Cursor confined to the surface.
|
|
cursor_grab_mode: Cell<CursorGrabMode>,
|
|
|
|
/// Pointers over the current surface.
|
|
pointers: Vec<WinitPointer>,
|
|
|
|
/// Text inputs on the current surface.
|
|
text_inputs: Vec<TextInputHandler>,
|
|
|
|
/// XdgActivation object.
|
|
xdg_activation: Option<Attached<XdgActivationV1>>,
|
|
|
|
/// Indicator whether user attention is requested.
|
|
attention_requested: Cell<bool>,
|
|
|
|
/// Compositor
|
|
compositor: Attached<WlCompositor>,
|
|
}
|
|
|
|
impl WindowHandle {
|
|
pub fn new(
|
|
env: &Environment<WinitEnv>,
|
|
window: Window<WinitFrame>,
|
|
size: Arc<Mutex<LogicalSize<u32>>>,
|
|
pending_window_requests: Arc<Mutex<Vec<WindowRequest>>>,
|
|
) -> Self {
|
|
let xdg_activation = env.get_global::<XdgActivationV1>();
|
|
// Unwrap is safe, since we can't create window without compositor anyway and won't be
|
|
// here.
|
|
let compositor = env.get_global::<WlCompositor>().unwrap();
|
|
|
|
Self {
|
|
window: ManuallyDrop::new(window),
|
|
size,
|
|
pending_window_requests,
|
|
cursor_icon: Cell::new(CursorIcon::Default),
|
|
is_resizable: Cell::new(true),
|
|
cursor_grab_mode: Cell::new(CursorGrabMode::None),
|
|
cursor_visible: Cell::new(true),
|
|
pointers: Vec::new(),
|
|
text_inputs: Vec::new(),
|
|
xdg_activation,
|
|
attention_requested: Cell::new(false),
|
|
compositor,
|
|
ime_allowed: Cell::new(false),
|
|
}
|
|
}
|
|
|
|
pub fn set_cursor_grab(&self, mode: CursorGrabMode) {
|
|
// The new requested state matches the current confine status, return.
|
|
let old_mode = self.cursor_grab_mode.replace(mode);
|
|
if old_mode == mode {
|
|
return;
|
|
}
|
|
|
|
// Clear old pointer data.
|
|
match old_mode {
|
|
CursorGrabMode::None => (),
|
|
CursorGrabMode::Confined => self.pointers.iter().for_each(|p| p.unconfine()),
|
|
CursorGrabMode::Locked => self.pointers.iter().for_each(|p| p.unlock()),
|
|
}
|
|
|
|
let surface = self.window.surface();
|
|
match mode {
|
|
CursorGrabMode::Locked => self.pointers.iter().for_each(|p| p.lock(surface)),
|
|
CursorGrabMode::Confined => self.pointers.iter().for_each(|p| p.confine(surface)),
|
|
CursorGrabMode::None => {
|
|
// Current lock/confine was already removed.
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn set_locked_cursor_position(&self, position: LogicalPosition<u32>) {
|
|
// XXX the cursor locking is ensured inside `Window`.
|
|
self.pointers
|
|
.iter()
|
|
.for_each(|p| p.set_cursor_position(position.x, position.y));
|
|
}
|
|
|
|
pub fn set_user_attention(&self, request_type: Option<UserAttentionType>) {
|
|
let xdg_activation = match self.xdg_activation.as_ref() {
|
|
None => return,
|
|
Some(xdg_activation) => xdg_activation,
|
|
};
|
|
|
|
// Urgency is only removed by the compositor and there's no need to raise urgency when it
|
|
// was already raised.
|
|
if request_type.is_none() || self.attention_requested.get() {
|
|
return;
|
|
}
|
|
|
|
let xdg_activation_token = xdg_activation.get_activation_token();
|
|
let surface = self.window.surface();
|
|
let window_id = wayland::make_wid(surface);
|
|
let xdg_activation = xdg_activation.clone();
|
|
|
|
xdg_activation_token.quick_assign(move |xdg_token, event, mut dispatch_data| {
|
|
let token = match event {
|
|
xdg_activation_token_v1::Event::Done { token } => token,
|
|
_ => return,
|
|
};
|
|
|
|
let winit_state = dispatch_data.get::<WinitState>().unwrap();
|
|
let window_handle = match winit_state.window_map.get_mut(&window_id) {
|
|
Some(window_handle) => window_handle,
|
|
None => return,
|
|
};
|
|
|
|
let surface = window_handle.window.surface();
|
|
xdg_activation.activate(token, surface);
|
|
|
|
// Mark that attention request was done and drop the token.
|
|
window_handle.attention_requested.replace(false);
|
|
xdg_token.destroy();
|
|
});
|
|
|
|
xdg_activation_token.set_surface(surface);
|
|
xdg_activation_token.commit();
|
|
self.attention_requested.replace(true);
|
|
}
|
|
|
|
/// Pointer appeared over the window.
|
|
pub fn pointer_entered(&mut self, pointer: WinitPointer) {
|
|
let position = self.pointers.iter().position(|p| *p == pointer);
|
|
|
|
if position.is_none() {
|
|
let surface = self.window.surface();
|
|
match self.cursor_grab_mode.get() {
|
|
CursorGrabMode::None => (),
|
|
CursorGrabMode::Locked => pointer.lock(surface),
|
|
CursorGrabMode::Confined => pointer.confine(surface),
|
|
}
|
|
|
|
self.pointers.push(pointer);
|
|
}
|
|
|
|
// Apply the current cursor style.
|
|
self.set_cursor_visible(self.cursor_visible.get());
|
|
}
|
|
|
|
/// Pointer left the window.
|
|
pub fn pointer_left(&mut self, pointer: WinitPointer) {
|
|
let position = self.pointers.iter().position(|p| *p == pointer);
|
|
|
|
if let Some(position) = position {
|
|
let pointer = self.pointers.remove(position);
|
|
|
|
// Drop the grabbing mode.
|
|
match self.cursor_grab_mode.get() {
|
|
CursorGrabMode::None => (),
|
|
CursorGrabMode::Locked => pointer.unlock(),
|
|
CursorGrabMode::Confined => pointer.unconfine(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn text_input_entered(&mut self, text_input: TextInputHandler) {
|
|
if !self.text_inputs.iter().any(|t| *t == text_input) {
|
|
self.text_inputs.push(text_input);
|
|
}
|
|
}
|
|
|
|
pub fn text_input_left(&mut self, text_input: TextInputHandler) {
|
|
if let Some(position) = self.text_inputs.iter().position(|t| *t == text_input) {
|
|
self.text_inputs.remove(position);
|
|
}
|
|
}
|
|
|
|
pub fn set_ime_position(&self, position: LogicalPosition<u32>) {
|
|
// XXX This won't fly unless user will have a way to request IME window per seat, since
|
|
// the ime windows will be overlapping, but winit doesn't expose API to specify for
|
|
// which seat we're setting IME position.
|
|
let (x, y) = (position.x as i32, position.y as i32);
|
|
for text_input in self.text_inputs.iter() {
|
|
text_input.set_ime_position(x, y);
|
|
}
|
|
}
|
|
|
|
pub fn passthrough_mouse_input(&self, passthrough_mouse_input: bool) {
|
|
if passthrough_mouse_input {
|
|
let region = self.compositor.create_region();
|
|
region.add(0, 0, 0, 0);
|
|
self.window
|
|
.surface()
|
|
.set_input_region(Some(®ion.detach()));
|
|
region.destroy();
|
|
} else {
|
|
// Using `None` results in the entire window being clickable.
|
|
self.window.surface().set_input_region(None);
|
|
}
|
|
}
|
|
|
|
pub fn set_ime_allowed(&self, allowed: bool, event_sink: &mut EventSink) {
|
|
if self.ime_allowed.get() == allowed {
|
|
return;
|
|
}
|
|
|
|
self.ime_allowed.replace(allowed);
|
|
let window_id = wayland::make_wid(self.window.surface());
|
|
|
|
for text_input in self.text_inputs.iter() {
|
|
text_input.set_input_allowed(allowed);
|
|
}
|
|
|
|
let event = if allowed {
|
|
WindowEvent::Ime(Ime::Enabled)
|
|
} else {
|
|
WindowEvent::Ime(Ime::Disabled)
|
|
};
|
|
|
|
event_sink.push_window_event(event, window_id);
|
|
}
|
|
|
|
pub fn set_cursor_visible(&self, visible: bool) {
|
|
self.cursor_visible.replace(visible);
|
|
let cursor_icon = match visible {
|
|
true => Some(self.cursor_icon.get()),
|
|
false => None,
|
|
};
|
|
|
|
for pointer in self.pointers.iter() {
|
|
pointer.set_cursor(cursor_icon)
|
|
}
|
|
}
|
|
|
|
pub fn set_cursor_icon(&self, cursor_icon: CursorIcon) {
|
|
self.cursor_icon.replace(cursor_icon);
|
|
|
|
if !self.cursor_visible.get() {
|
|
return;
|
|
}
|
|
|
|
for pointer in self.pointers.iter() {
|
|
pointer.set_cursor(Some(cursor_icon));
|
|
}
|
|
}
|
|
|
|
pub fn drag_window(&self) {
|
|
for pointer in self.pointers.iter() {
|
|
pointer.drag_window(&self.window);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn handle_window_requests(winit_state: &mut WinitState) {
|
|
let window_map = &mut winit_state.window_map;
|
|
let window_user_requests = &mut winit_state.window_user_requests;
|
|
let window_compositor_updates = &mut winit_state.window_compositor_updates;
|
|
let mut windows_to_close: Vec<WindowId> = Vec::new();
|
|
|
|
// Process the rest of the events.
|
|
for (window_id, window_handle) in window_map.iter_mut() {
|
|
let mut requests = window_handle.pending_window_requests.lock().unwrap();
|
|
let requests = requests.drain(..);
|
|
for request in requests {
|
|
match request {
|
|
WindowRequest::Fullscreen(fullscreen) => {
|
|
window_handle.window.set_fullscreen(fullscreen.as_ref());
|
|
}
|
|
WindowRequest::UnsetFullscreen => {
|
|
window_handle.window.unset_fullscreen();
|
|
}
|
|
WindowRequest::ShowCursor(show_cursor) => {
|
|
window_handle.set_cursor_visible(show_cursor);
|
|
}
|
|
WindowRequest::NewCursorIcon(cursor_icon) => {
|
|
window_handle.set_cursor_icon(cursor_icon);
|
|
}
|
|
WindowRequest::ImePosition(position) => {
|
|
window_handle.set_ime_position(position);
|
|
}
|
|
WindowRequest::AllowIme(allow) => {
|
|
let event_sink = &mut winit_state.event_sink;
|
|
window_handle.set_ime_allowed(allow, event_sink);
|
|
}
|
|
WindowRequest::SetCursorGrabMode(mode) => {
|
|
window_handle.set_cursor_grab(mode);
|
|
}
|
|
WindowRequest::SetLockedCursorPosition(position) => {
|
|
window_handle.set_locked_cursor_position(position);
|
|
}
|
|
WindowRequest::DragWindow => {
|
|
window_handle.drag_window();
|
|
}
|
|
WindowRequest::Maximize(maximize) => {
|
|
if maximize {
|
|
window_handle.window.set_maximized();
|
|
} else {
|
|
window_handle.window.unset_maximized();
|
|
}
|
|
}
|
|
WindowRequest::Minimize => {
|
|
window_handle.window.set_minimized();
|
|
}
|
|
WindowRequest::Decorate(decorate) => {
|
|
let decorations = match decorate {
|
|
true => Decorations::FollowServer,
|
|
false => Decorations::None,
|
|
};
|
|
|
|
window_handle.window.set_decorate(decorations);
|
|
|
|
// We should refresh the frame to apply decorations change.
|
|
let window_request = window_user_requests.get_mut(window_id).unwrap();
|
|
window_request.refresh_frame = true;
|
|
}
|
|
WindowRequest::Resizeable(resizeable) => {
|
|
window_handle.window.set_resizable(resizeable);
|
|
|
|
// We should refresh the frame to update button state.
|
|
let window_request = window_user_requests.get_mut(window_id).unwrap();
|
|
window_request.refresh_frame = true;
|
|
}
|
|
WindowRequest::Title(title) => {
|
|
window_handle.window.set_title(title);
|
|
|
|
// We should refresh the frame to draw new title.
|
|
let window_request = window_user_requests.get_mut(window_id).unwrap();
|
|
window_request.refresh_frame = true;
|
|
}
|
|
WindowRequest::MinSize(size) => {
|
|
let size = size.map(|size| (size.width, size.height));
|
|
window_handle.window.set_min_size(size);
|
|
|
|
let window_request = window_user_requests.get_mut(window_id).unwrap();
|
|
window_request.refresh_frame = true;
|
|
}
|
|
WindowRequest::MaxSize(size) => {
|
|
let size = size.map(|size| (size.width, size.height));
|
|
window_handle.window.set_max_size(size);
|
|
|
|
let window_request = window_user_requests.get_mut(window_id).unwrap();
|
|
window_request.refresh_frame = true;
|
|
}
|
|
WindowRequest::FrameSize(size) => {
|
|
if !window_handle.is_resizable.get() {
|
|
// On Wayland non-resizable window is achieved by setting both min and max
|
|
// size of the window to the same value.
|
|
let size = Some((size.width, size.height));
|
|
window_handle.window.set_max_size(size);
|
|
window_handle.window.set_min_size(size);
|
|
}
|
|
|
|
window_handle.window.resize(size.width, size.height);
|
|
|
|
// We should refresh the frame after resize.
|
|
let window_request = window_user_requests.get_mut(window_id).unwrap();
|
|
window_request.refresh_frame = true;
|
|
}
|
|
WindowRequest::PassthroughMouseInput(passthrough) => {
|
|
window_handle.passthrough_mouse_input(passthrough);
|
|
|
|
let window_request = window_user_requests.get_mut(window_id).unwrap();
|
|
window_request.refresh_frame = true;
|
|
}
|
|
WindowRequest::Attention(request_type) => {
|
|
window_handle.set_user_attention(request_type);
|
|
}
|
|
WindowRequest::Redraw => {
|
|
let window_request = window_user_requests.get_mut(window_id).unwrap();
|
|
window_request.redraw_requested = true;
|
|
}
|
|
WindowRequest::Close => {
|
|
// The window was requested to be closed.
|
|
windows_to_close.push(*window_id);
|
|
|
|
// Send event that the window was destroyed.
|
|
let event_sink = &mut winit_state.event_sink;
|
|
event_sink.push_window_event(WindowEvent::Destroyed, *window_id);
|
|
}
|
|
WindowRequest::Theme(_theme) => {
|
|
#[cfg(feature = "sctk-adwaita")]
|
|
{
|
|
window_handle.window.set_frame_config(match _theme {
|
|
Some(theme) => theme.into(),
|
|
None => sctk_adwaita::FrameConfig::auto(),
|
|
});
|
|
|
|
let window_requst = window_user_requests.get_mut(window_id).unwrap();
|
|
window_requst.refresh_frame = true;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
// Close the windows.
|
|
for window in windows_to_close {
|
|
let _ = window_map.remove(&window);
|
|
let _ = window_user_requests.remove(&window);
|
|
let _ = window_compositor_updates.remove(&window);
|
|
}
|
|
}
|
|
|
|
impl Drop for WindowHandle {
|
|
fn drop(&mut self) {
|
|
unsafe {
|
|
let surface = self.window.surface().clone();
|
|
// The window must be destroyed before wl_surface.
|
|
ManuallyDrop::drop(&mut self.window);
|
|
surface.destroy();
|
|
}
|
|
}
|
|
}
|