Skip to content
This repository has been archived by the owner on Jul 15, 2024. It is now read-only.

Commit

Permalink
Support timers on Wayland (#150)
Browse files Browse the repository at this point in the history
* Implement Timers on the Wayland backend

* Add a blinking cursor to edit_text

* Change panic message
  • Loading branch information
DJMcNab authored Aug 16, 2023
1 parent e89fe93 commit e7b4230
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 32 deletions.
99 changes: 74 additions & 25 deletions examples/edit_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::rc::Rc;

#[cfg(feature = "accesskit")]
use accesskit::TreeUpdate;
use instant::Duration;
use parley::FontContext;
use tracing_subscriber::EnvFilter;
use unicode_segmentation::GraphemeCursor;
Expand All @@ -25,7 +26,7 @@ use glazier::{
},
Application, KeyEvent, Region, Scalable, TextFieldToken, WinHandler, WindowHandle,
};
use glazier::{HotKey, SysMods};
use glazier::{HotKey, SysMods, TimerToken};

mod common;
use common::text::{self, ParleyBrush};
Expand All @@ -35,6 +36,8 @@ const HEIGHT: usize = 1536;
const FONT_SIZE: f32 = 36.0;
const TEXT_X: f64 = 100.0;
const TEXT_Y: f64 = 100.0;
// TODO: We need to get this from the user's system, as most (all?) desktops have a setting for this
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(600);

fn main() {
tracing_subscriber::fmt()
Expand Down Expand Up @@ -84,6 +87,9 @@ struct WindowState {
document: Rc<RefCell<DocumentState>>,
text_input_token: Option<TextFieldToken>,
hotkeys: HotKeys,

cursor_blink_token: Option<TimerToken>,
cursor_shown: bool,
}

struct DocumentState {
Expand Down Expand Up @@ -159,6 +165,8 @@ impl WindowState {
size: Size::new(800.0, 600.0),
text_input_token: None,
hotkeys: Default::default(),
cursor_blink_token: None,
cursor_shown: true,
}
}

Expand Down Expand Up @@ -227,28 +235,44 @@ impl WindowState {
);
let doc = self.document.borrow();
text::render_text(&mut sb, Affine::translate((TEXT_X, TEXT_Y)), &doc.layout);
let selection_start_x =
parley::layout::Cursor::from_position(&doc.layout, doc.selection.min(), true).offset()
as f64
+ TEXT_X;
let selection_end_x =
parley::layout::Cursor::from_position(&doc.layout, doc.selection.max(), true).offset()
as f64
+ TEXT_X;
let rect = Rect::from_points(
Point::new(selection_start_x.min(selection_end_x - 1.0), TEXT_Y),
Point::new(
selection_end_x.max(selection_start_x + 1.0),
TEXT_Y + FONT_SIZE as f64,
),
);
sb.fill(
Fill::NonZero,
Affine::IDENTITY,
&Brush::Solid(Color::rgba8(0, 0, 255, 100)),
None,
&rect,
);
if doc.selection.len() > 0 {
let selection_start_x =
parley::layout::Cursor::from_position(&doc.layout, doc.selection.min(), true)
.offset() as f64
+ TEXT_X;
let selection_end_x =
parley::layout::Cursor::from_position(&doc.layout, doc.selection.max(), true)
.offset() as f64
+ TEXT_X;
let rect = Rect::from_points(
Point::new(selection_start_x, TEXT_Y),
Point::new(selection_end_x, TEXT_Y + FONT_SIZE as f64),
);
sb.fill(
Fill::NonZero,
Affine::IDENTITY,
&Brush::Solid(Color::rgba8(0, 0, 255, 100)),
None,
&rect,
);
}
if self.cursor_shown {
let cursor_active_x =
parley::layout::Cursor::from_position(&doc.layout, doc.selection.active, true)
.offset() as f64
+ TEXT_X;
let rect = Rect::from_points(
Point::new(cursor_active_x - 1.0, TEXT_Y),
Point::new(cursor_active_x + 1.0, TEXT_Y + FONT_SIZE as f64),
);
sb.fill(
Fill::NonZero,
Affine::IDENTITY,
&Brush::Solid(Color::BLACK),
None,
&rect,
);
}
if let Some(composition) = &doc.composition {
let composition_start = parley::layout::Cursor::from_position(
&doc.layout,
Expand Down Expand Up @@ -331,8 +355,15 @@ impl WinHandler for WindowState {
}

fn release_input_lock(&mut self, _token: TextFieldToken) {
// no action required; this example is simple enough that this
// state is not actually shared.
// Applications appear to only hide the cursor when there has been no change
// to the text. As a best attempt version of this, show the cursor
// when an input lock finishes. TODO: Should this only be on a mutable lock?

// This doesn't work for keypresses which don't do anything to the text (such as pressing escape
// for example), but this is good enough
self.cursor_shown = true;
// Ignore the previous request
self.cursor_blink_token = Some(self.handle.request_timer(CURSOR_BLINK_INTERVAL));
}

fn key_down(&mut self, event: KeyEvent) -> bool {
Expand Down Expand Up @@ -389,6 +420,24 @@ impl WinHandler for WindowState {
fn as_any(&mut self) -> &mut dyn Any {
self
}
fn got_focus(&mut self) {
self.cursor_blink_token = Some(self.handle.request_timer(CURSOR_BLINK_INTERVAL));
// The text field is always focused in this example, so start with it shown
self.cursor_shown = true;
}

fn lost_focus(&mut self) {
self.cursor_shown = false;
// Don't
self.cursor_blink_token = None;
}
fn timer(&mut self, token: TimerToken) {
if self.cursor_blink_token.is_some_and(|it| it == token) {
self.cursor_shown = !self.cursor_shown;
self.cursor_blink_token = Some(self.handle.request_timer(CURSOR_BLINK_INTERVAL));
}
// Ignore the token otherwise, as it's been superceded
}
}

impl InputHandler for AppInputHandler {
Expand Down
6 changes: 4 additions & 2 deletions src/backend/wayland/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use smithay_client_toolkit::{
compositor::CompositorState,
output::OutputState,
reexports::{
calloop::{channel, EventLoop, LoopSignal},
calloop::{channel, EventLoop, LoopHandle, LoopSignal},
client::{
globals::{registry_queue_init, BindError},
protocol::wl_compositor,
Expand Down Expand Up @@ -57,6 +57,7 @@ pub struct Application {
pub(super) wayland_queue: QueueHandle<WaylandState>,
pub(super) xdg_shell: Weak<XdgShell>,
loop_signal: LoopSignal,
pub(super) loop_handle: LoopHandle<'static, WaylandState>,
pub(super) idle_sender: Sender<IdleAction>,
pub(super) loop_sender: channel::Sender<ActiveAction>,
pub(super) raw_display_handle: *mut c_void,
Expand Down Expand Up @@ -126,7 +127,7 @@ impl Application {
seats: SeatState::new(&globals, &qh),
xkb_context: Context::new(),
text_input: text_input_global,
loop_handle,
loop_handle: loop_handle.clone(),
};
state.initial_seats();
Ok(Application {
Expand All @@ -136,6 +137,7 @@ impl Application {
loop_signal,
idle_sender,
loop_sender,
loop_handle,
xdg_shell: shell_ref,
raw_display_handle: conn.backend().display_ptr().cast(),
})
Expand Down
33 changes: 28 additions & 5 deletions src/backend/wayland/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ use raw_window_handle::{
WaylandDisplayHandle, WaylandWindowHandle,
};
use smithay_client_toolkit::compositor::CompositorHandler;
use smithay_client_toolkit::reexports::calloop::channel;
use smithay_client_toolkit::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay_client_toolkit::reexports::calloop::{channel, LoopHandle};
use smithay_client_toolkit::reexports::client::protocol::wl_compositor::WlCompositor;
use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface;
use smithay_client_toolkit::reexports::client::{protocol, Connection, Proxy, QueueHandle};
Expand Down Expand Up @@ -245,12 +246,30 @@ impl WindowHandle {
}
}

pub fn request_timer(&self, _deadline: std::time::Instant) -> TimerToken {
todo!()
pub fn request_timer(&self, deadline: std::time::Instant) -> TimerToken {
let props = self.properties();
let props = props.borrow();
let window_id = WindowId::new(&props.wayland_window);
let token = TimerToken::next();
props
.loop_handle
.insert_source(
Timer::from_deadline(deadline),
move |_deadline, _, state| {
let window = state.windows.get_mut(&window_id);
if let Some(window) = window {
window.handler.timer(token);
}
// In theory, we could get the `timer` request to give us a new deadline
TimeoutAction::Drop
},
)
.expect("adding a Timer to the calloop event loop is infallible");
token
}

pub fn set_cursor(&mut self, _cursor: &Cursor) {
todo!()
tracing::warn!("unimplemented set_cursor called")
}

pub fn make_cursor(&self, _desc: &CursorDesc) -> Option<Cursor> {
Expand Down Expand Up @@ -414,6 +433,7 @@ pub(crate) struct WindowBuilder {
show_titlebar: bool,
compositor: WlCompositor,
wayland_queue: QueueHandle<WaylandState>,
loop_handle: LoopHandle<'static, WaylandState>,
xdg_state: Weak<XdgShell>,
idle_sender: Sender<IdleAction>,
loop_sender: channel::Sender<ActiveAction>,
Expand All @@ -435,6 +455,7 @@ impl WindowBuilder {
show_titlebar: true,
compositor: app.compositor,
wayland_queue: app.wayland_queue,
loop_handle: app.loop_handle,
xdg_state: app.xdg_shell,
idle_sender: app.idle_sender,
loop_sender: app.loop_sender,
Expand Down Expand Up @@ -526,7 +547,8 @@ impl WindowBuilder {
current_size: Size::new(600., 800.),
current_scale: Scale::new(1., 1.), // TODO: NaN? - these values should (must?) not be used
wayland_window,
wayland_queue: self.wayland_queue.clone(),
wayland_queue: self.wayland_queue,
loop_handle: self.loop_handle,
will_repaint: false,
pending_frame_callback: false,
configured: false,
Expand Down Expand Up @@ -607,6 +629,7 @@ struct WindowProperties {
// We make this the only handle, so we can definitely drop it
wayland_window: Window,
wayland_queue: QueueHandle<WaylandState>,
loop_handle: LoopHandle<'static, WaylandState>,

/// Wayland requires frame (throttling) callbacks be requested *before* running commit.
/// However, user code controls when commit is called (generally through wgpu's
Expand Down

0 comments on commit e7b4230

Please sign in to comment.