forked from NotAShelf/beer
grid: scrollback with mouse-wheel and Shift+PageUp scrolling
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I12b2ed33a705eb3474a7d14a295e021d6a6a6964
This commit is contained in:
parent
ba8f8d7144
commit
3dd953b75a
4 changed files with 201 additions and 20 deletions
88
src/grid.rs
88
src/grid.rs
|
|
@ -1,8 +1,13 @@
|
||||||
//! The terminal screen: a grid of styled cells, a cursor, and the editing
|
//! The terminal screen: a grid of styled cells, a cursor, and the editing
|
||||||
//! operations the VT parser drives.
|
//! operations the VT parser drives.
|
||||||
|
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use unicode_width::UnicodeWidthChar;
|
use unicode_width::UnicodeWidthChar;
|
||||||
|
|
||||||
|
/// Maximum scrollback lines retained for the main screen.
|
||||||
|
const SCROLLBACK_CAP: usize = 10_000;
|
||||||
|
|
||||||
/// A cell colour: terminal default, a palette index, or direct RGB.
|
/// A cell colour: terminal default, a palette index, or direct RGB.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
||||||
pub enum Color {
|
pub enum Color {
|
||||||
|
|
@ -122,6 +127,10 @@ pub struct Grid {
|
||||||
tabs: Vec<bool>,
|
tabs: Vec<bool>,
|
||||||
/// Saved primary screen while the alternate screen is active.
|
/// Saved primary screen while the alternate screen is active.
|
||||||
alt_saved: Option<Vec<Vec<Cell>>>,
|
alt_saved: Option<Vec<Vec<Cell>>>,
|
||||||
|
/// Lines that have scrolled off the top of the main screen, newest last.
|
||||||
|
scrollback: VecDeque<Vec<Cell>>,
|
||||||
|
/// How many lines the viewport is scrolled back from the live bottom.
|
||||||
|
view_offset: usize,
|
||||||
cursor_shape: CursorShape,
|
cursor_shape: CursorShape,
|
||||||
cursor_visible: bool,
|
cursor_visible: bool,
|
||||||
/// Application cursor-keys mode (DECCKM): arrows send SS3 instead of CSI.
|
/// Application cursor-keys mode (DECCKM): arrows send SS3 instead of CSI.
|
||||||
|
|
@ -153,6 +162,8 @@ impl Grid {
|
||||||
wrap_pending: false,
|
wrap_pending: false,
|
||||||
tabs: default_tabs(cols),
|
tabs: default_tabs(cols),
|
||||||
alt_saved: None,
|
alt_saved: None,
|
||||||
|
scrollback: VecDeque::new(),
|
||||||
|
view_offset: 0,
|
||||||
cursor_shape: CursorShape::default(),
|
cursor_shape: CursorShape::default(),
|
||||||
cursor_visible: true,
|
cursor_visible: true,
|
||||||
cursor_color: None,
|
cursor_color: None,
|
||||||
|
|
@ -184,6 +195,9 @@ impl Grid {
|
||||||
self.cursor.x = self.cursor.x.min(cols - 1);
|
self.cursor.x = self.cursor.x.min(cols - 1);
|
||||||
self.cursor.y = self.cursor.y.min(rows - 1);
|
self.cursor.y = self.cursor.y.min(rows - 1);
|
||||||
self.wrap_pending = false;
|
self.wrap_pending = false;
|
||||||
|
// Scrollback lines keep their old width (no reflow); snap to the live
|
||||||
|
// screen so the viewport never indexes a stale-width line.
|
||||||
|
self.view_offset = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursor(&self) -> (usize, usize) {
|
pub fn cursor(&self) -> (usize, usize) {
|
||||||
|
|
@ -475,6 +489,21 @@ impl Grid {
|
||||||
|
|
||||||
pub fn scroll_up(&mut self, n: usize) {
|
pub fn scroll_up(&mut self, n: usize) {
|
||||||
let n = n.min(self.bottom - self.top + 1);
|
let n = n.min(self.bottom - self.top + 1);
|
||||||
|
// Lines leaving the top of the *whole* main screen become scrollback;
|
||||||
|
// a DECSTBM region scroll (top > 0) or the alt screen does not.
|
||||||
|
if self.top == 0 && self.alt_saved.is_none() {
|
||||||
|
for y in 0..n {
|
||||||
|
let line = std::mem::replace(&mut self.lines[y], vec![Cell::default(); self.cols]);
|
||||||
|
self.scrollback.push_back(line);
|
||||||
|
}
|
||||||
|
while self.scrollback.len() > SCROLLBACK_CAP {
|
||||||
|
self.scrollback.pop_front();
|
||||||
|
}
|
||||||
|
// Keep a scrolled-back viewport anchored to the same content.
|
||||||
|
if self.view_offset > 0 {
|
||||||
|
self.view_offset = (self.view_offset + n).min(self.scrollback.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
for y in self.top..=self.bottom {
|
for y in self.top..=self.bottom {
|
||||||
if y + n <= self.bottom {
|
if y + n <= self.bottom {
|
||||||
self.lines.swap(y, y + n);
|
self.lines.swap(y, y + n);
|
||||||
|
|
@ -618,6 +647,7 @@ impl Grid {
|
||||||
if self.alt_saved.is_some() {
|
if self.alt_saved.is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
self.view_offset = 0;
|
||||||
let blank = vec![vec![Cell::default(); self.cols]; self.rows];
|
let blank = vec![vec![Cell::default(); self.cols]; self.rows];
|
||||||
self.alt_saved = Some(std::mem::replace(&mut self.lines, blank));
|
self.alt_saved = Some(std::mem::replace(&mut self.lines, blank));
|
||||||
}
|
}
|
||||||
|
|
@ -628,6 +658,45 @@ impl Grid {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- scrollback viewport ---
|
||||||
|
|
||||||
|
/// Scroll the viewport by `delta` lines: positive = back into history,
|
||||||
|
/// negative = toward the live screen. No-op on the alternate screen.
|
||||||
|
pub fn scroll_view(&mut self, delta: isize) {
|
||||||
|
if self.alt_saved.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let max = self.scrollback.len() as isize;
|
||||||
|
self.view_offset = (self.view_offset as isize + delta).clamp(0, max) as usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_to_bottom(&mut self) {
|
||||||
|
self.view_offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether the viewport is showing the live screen (not scrolled back).
|
||||||
|
pub fn view_at_bottom(&self) -> bool {
|
||||||
|
self.view_offset == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One page (a screenful) of lines, for page-scroll bindings.
|
||||||
|
pub fn page(&self) -> usize {
|
||||||
|
self.rows.max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cells shown at viewport row `y` (0 = top of the window), accounting
|
||||||
|
/// for the scrollback offset. May differ from `cols` in width if the line
|
||||||
|
/// predates a resize (no reflow yet), so callers must not assume length.
|
||||||
|
pub fn view_row(&self, y: usize) -> &[Cell] {
|
||||||
|
let start = self.scrollback.len() - self.view_offset;
|
||||||
|
let idx = start + y;
|
||||||
|
if idx < self.scrollback.len() {
|
||||||
|
&self.scrollback[idx]
|
||||||
|
} else {
|
||||||
|
&self.lines[idx - self.scrollback.len()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- inspection (logging + tests) ---
|
// --- inspection (logging + tests) ---
|
||||||
|
|
||||||
/// The visible text of one row, trailing blanks trimmed.
|
/// The visible text of one row, trailing blanks trimmed.
|
||||||
|
|
@ -713,6 +782,25 @@ mod tests {
|
||||||
assert_eq!(g.row_text(0), "a def");
|
assert_eq!(g.row_text(0), "a def");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scrollback_captures_scrolled_lines() {
|
||||||
|
let mut g = Grid::new(8, 2);
|
||||||
|
for c in ['1', '2', '3'] {
|
||||||
|
g.print(c);
|
||||||
|
g.next_line();
|
||||||
|
}
|
||||||
|
// Live screen: newest line on top, cleared line below.
|
||||||
|
assert_eq!(g.view_row(0)[0].c, '3');
|
||||||
|
assert!(g.view_at_bottom());
|
||||||
|
// Scroll back to reveal the two captured lines.
|
||||||
|
g.scroll_view(2);
|
||||||
|
assert!(!g.view_at_bottom());
|
||||||
|
assert_eq!(g.view_row(0)[0].c, '1');
|
||||||
|
assert_eq!(g.view_row(1)[0].c, '2');
|
||||||
|
g.scroll_to_bottom();
|
||||||
|
assert_eq!(g.view_row(0)[0].c, '3');
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wide_char_occupies_two_columns() {
|
fn wide_char_occupies_two_columns() {
|
||||||
let mut g = Grid::new(6, 1);
|
let mut g = Grid::new(6, 1);
|
||||||
|
|
|
||||||
|
|
@ -126,12 +126,14 @@ impl Renderer {
|
||||||
canvas.clear(DEFAULT_BG);
|
canvas.clear(DEFAULT_BG);
|
||||||
|
|
||||||
let m = self.fonts.metrics();
|
let m = self.fonts.metrics();
|
||||||
|
let cols = grid.cols();
|
||||||
|
|
||||||
// Cell backgrounds: only paint cells that differ from the cleared
|
// Cell backgrounds: only paint cells that differ from the cleared
|
||||||
// default - most of a screen is default background.
|
// default - most of a screen is default background. Rows come through
|
||||||
|
// the scrollback viewport and may be shorter than `cols` after a resize.
|
||||||
for y in 0..grid.rows() {
|
for y in 0..grid.rows() {
|
||||||
for x in 0..grid.cols() {
|
for (x, cell) in grid.view_row(y).iter().take(cols).enumerate() {
|
||||||
let (_, bg) = cell_colors(grid.cell(x, y));
|
let (_, bg) = cell_colors(cell);
|
||||||
if bg != DEFAULT_BG {
|
if bg != DEFAULT_BG {
|
||||||
let (px, py) = (x as i32 * m.width as i32, y as i32 * m.height as i32);
|
let (px, py) = (x as i32 * m.width as i32, y as i32 * m.height as i32);
|
||||||
canvas.fill_rect(px, py, m.width, m.height, bg);
|
canvas.fill_rect(px, py, m.width, m.height, bg);
|
||||||
|
|
@ -140,8 +142,7 @@ impl Renderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
for y in 0..grid.rows() {
|
for y in 0..grid.rows() {
|
||||||
for x in 0..grid.cols() {
|
for (x, cell) in grid.view_row(y).iter().take(cols).enumerate() {
|
||||||
let cell = grid.cell(x, y);
|
|
||||||
if cell.flags.contains(Flags::WIDE_CONT) {
|
if cell.flags.contains(Flags::WIDE_CONT) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -162,7 +163,10 @@ impl Renderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.draw_cursor(&mut canvas, grid, m, focused);
|
// The cursor belongs to the live screen; hide it while scrolled back.
|
||||||
|
if grid.view_at_bottom() {
|
||||||
|
self.draw_cursor(&mut canvas, grid, m, focused);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the cursor: a solid block/underline/beam when focused, a hollow
|
/// Draw the cursor: a solid block/underline/beam when focused, a hollow
|
||||||
|
|
|
||||||
13
src/vt.rs
13
src/vt.rs
|
|
@ -123,6 +123,19 @@ impl Term {
|
||||||
self.grid.resize(cols, rows);
|
self.grid.resize(cols, rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scroll_view(&mut self, delta: isize) {
|
||||||
|
self.grid.scroll_view(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scroll_to_bottom(&mut self) {
|
||||||
|
self.grid.scroll_to_bottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lines per page, for page-scroll bindings.
|
||||||
|
pub fn page(&self) -> usize {
|
||||||
|
self.grid.page()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn title(&self) -> Option<&str> {
|
pub fn title(&self) -> Option<&str> {
|
||||||
self.title.as_deref()
|
self.title.as_deref()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
104
src/wayland.rs
104
src/wayland.rs
|
|
@ -18,14 +18,15 @@ use crate::render::Renderer;
|
||||||
use crate::vt::Term;
|
use crate::vt::Term;
|
||||||
use smithay_client_toolkit::{
|
use smithay_client_toolkit::{
|
||||||
compositor::{CompositorHandler, CompositorState},
|
compositor::{CompositorHandler, CompositorState},
|
||||||
delegate_compositor, delegate_keyboard, delegate_output, delegate_registry, delegate_seat,
|
delegate_compositor, delegate_keyboard, delegate_output, delegate_pointer, delegate_registry,
|
||||||
delegate_shm, delegate_xdg_shell, delegate_xdg_window,
|
delegate_seat, delegate_shm, delegate_xdg_shell, delegate_xdg_window,
|
||||||
output::{OutputHandler, OutputState},
|
output::{OutputHandler, OutputState},
|
||||||
registry::{ProvidesRegistryState, RegistryState},
|
registry::{ProvidesRegistryState, RegistryState},
|
||||||
registry_handlers,
|
registry_handlers,
|
||||||
seat::{
|
seat::{
|
||||||
Capability, SeatHandler, SeatState,
|
Capability, SeatHandler, SeatState,
|
||||||
keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers, RepeatInfo},
|
keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers, RepeatInfo},
|
||||||
|
pointer::{PointerEvent, PointerEventKind, PointerHandler},
|
||||||
},
|
},
|
||||||
shell::{
|
shell::{
|
||||||
WaylandSurface,
|
WaylandSurface,
|
||||||
|
|
@ -39,7 +40,7 @@ use smithay_client_toolkit::{
|
||||||
use wayland_client::{
|
use wayland_client::{
|
||||||
Connection, QueueHandle,
|
Connection, QueueHandle,
|
||||||
globals::registry_queue_init,
|
globals::registry_queue_init,
|
||||||
protocol::{wl_keyboard, wl_output, wl_seat, wl_shm, wl_surface},
|
protocol::{wl_keyboard, wl_output, wl_pointer, wl_seat, wl_shm, wl_surface},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Default window size in pixels before the compositor suggests one.
|
/// Default window size in pixels before the compositor suggests one.
|
||||||
|
|
@ -90,6 +91,7 @@ pub fn run() -> anyhow::Result<ExitCode> {
|
||||||
renderer,
|
renderer,
|
||||||
loop_handle: event_loop.handle(),
|
loop_handle: event_loop.handle(),
|
||||||
keyboard: None,
|
keyboard: None,
|
||||||
|
pointer: None,
|
||||||
modifiers: Modifiers::default(),
|
modifiers: Modifiers::default(),
|
||||||
// The PTY is spawned on the first configure, once the real window size
|
// The PTY is spawned on the first configure, once the real window size
|
||||||
// is known, so the shell starts at the final size and is not hit by a
|
// is known, so the shell starts at the final size and is not hit by a
|
||||||
|
|
@ -158,6 +160,7 @@ struct App {
|
||||||
renderer: Renderer,
|
renderer: Renderer,
|
||||||
loop_handle: LoopHandle<'static, App>,
|
loop_handle: LoopHandle<'static, App>,
|
||||||
keyboard: Option<wl_keyboard::WlKeyboard>,
|
keyboard: Option<wl_keyboard::WlKeyboard>,
|
||||||
|
pointer: Option<wl_pointer::WlPointer>,
|
||||||
modifiers: Modifiers,
|
modifiers: Modifiers,
|
||||||
/// `None` until the first configure spawns the shell.
|
/// `None` until the first configure spawns the shell.
|
||||||
session: Option<Session>,
|
session: Option<Session>,
|
||||||
|
|
@ -233,17 +236,36 @@ impl App {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode a key event and write it to the shell.
|
/// Handle a key (initial press or repeat): Shift+PageUp/PageDown scroll the
|
||||||
fn send_key(&self, event: &KeyEvent) {
|
/// viewport locally; anything else is encoded to the shell and snaps the
|
||||||
|
/// viewport back to the live screen.
|
||||||
|
fn handle_key(&mut self, event: &KeyEvent) {
|
||||||
|
if self.modifiers.shift && matches!(event.keysym, Keysym::Page_Up | Keysym::Page_Down) {
|
||||||
|
if let Some(session) = self.session.as_mut() {
|
||||||
|
let page = session.term.page() as isize;
|
||||||
|
let delta = if event.keysym == Keysym::Page_Up {
|
||||||
|
page
|
||||||
|
} else {
|
||||||
|
-page
|
||||||
|
};
|
||||||
|
session.term.scroll_view(delta);
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let app_cursor = self
|
let app_cursor = self
|
||||||
.session
|
.session
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|s| s.term.grid().app_cursor());
|
.is_some_and(|s| s.term.grid().app_cursor());
|
||||||
if let Some(bytes) = crate::input::encode(event, self.modifiers, app_cursor)
|
if let Some(bytes) = crate::input::encode(event, self.modifiers, app_cursor)
|
||||||
&& let Some(session) = self.session.as_ref()
|
&& let Some(session) = self.session.as_mut()
|
||||||
&& let Err(err) = write_all(session.pty.master(), &bytes)
|
|
||||||
{
|
{
|
||||||
tracing::warn!("write key to pty: {err}");
|
session.term.scroll_to_bottom();
|
||||||
|
self.dirty = true;
|
||||||
|
if let Err(err) = write_all(session.pty.master(), &bytes) {
|
||||||
|
tracing::warn!("write key to pty: {err}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -439,13 +461,19 @@ impl SeatHandler for App {
|
||||||
&seat,
|
&seat,
|
||||||
None,
|
None,
|
||||||
loop_handle,
|
loop_handle,
|
||||||
Box::new(|app: &mut App, _kbd, event| app.send_key(&event)),
|
Box::new(|app: &mut App, _kbd, event| app.handle_key(&event)),
|
||||||
);
|
);
|
||||||
match keyboard {
|
match keyboard {
|
||||||
Ok(keyboard) => self.keyboard = Some(keyboard),
|
Ok(keyboard) => self.keyboard = Some(keyboard),
|
||||||
Err(err) => tracing::warn!("get keyboard: {err}"),
|
Err(err) => tracing::warn!("get keyboard: {err}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if capability == Capability::Pointer && self.pointer.is_none() {
|
||||||
|
match self.seat_state.get_pointer(qh, &seat) {
|
||||||
|
Ok(pointer) => self.pointer = Some(pointer),
|
||||||
|
Err(err) => tracing::warn!("get pointer: {err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_capability(
|
fn remove_capability(
|
||||||
|
|
@ -455,10 +483,18 @@ impl SeatHandler for App {
|
||||||
_: wl_seat::WlSeat,
|
_: wl_seat::WlSeat,
|
||||||
capability: Capability,
|
capability: Capability,
|
||||||
) {
|
) {
|
||||||
if capability == Capability::Keyboard
|
match capability {
|
||||||
&& let Some(keyboard) = self.keyboard.take()
|
Capability::Keyboard => {
|
||||||
{
|
if let Some(keyboard) = self.keyboard.take() {
|
||||||
keyboard.release();
|
keyboard.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Capability::Pointer => {
|
||||||
|
if let Some(pointer) = self.pointer.take() {
|
||||||
|
pointer.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -500,7 +536,7 @@ impl KeyboardHandler for App {
|
||||||
_: u32,
|
_: u32,
|
||||||
event: KeyEvent,
|
event: KeyEvent,
|
||||||
) {
|
) {
|
||||||
self.send_key(&event);
|
self.handle_key(&event);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn repeat_key(
|
fn repeat_key(
|
||||||
|
|
@ -548,6 +584,45 @@ impl KeyboardHandler for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PointerHandler for App {
|
||||||
|
fn pointer_frame(
|
||||||
|
&mut self,
|
||||||
|
_: &Connection,
|
||||||
|
_: &QueueHandle<Self>,
|
||||||
|
_: &wl_pointer::WlPointer,
|
||||||
|
events: &[PointerEvent],
|
||||||
|
) {
|
||||||
|
let cell_h = f64::from(self.renderer.metrics().height);
|
||||||
|
for event in events {
|
||||||
|
let PointerEventKind::Axis { vertical, .. } = &event.kind else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
// Wheel notches arrive as value120 (÷120) or legacy discrete steps,
|
||||||
|
// ~3 lines each; touchpads send pixels, mapped per cell height.
|
||||||
|
let (raw, scale) = if vertical.value120 != 0 {
|
||||||
|
(f64::from(vertical.value120) / 120.0, 3.0)
|
||||||
|
} else if vertical.discrete != 0 {
|
||||||
|
(f64::from(vertical.discrete), 3.0)
|
||||||
|
} else if cell_h > 0.0 {
|
||||||
|
(vertical.absolute / cell_h, 1.0)
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if raw == 0.0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Positive axis = scroll down (toward live); we scroll the viewport
|
||||||
|
// the opposite way (negative offset delta).
|
||||||
|
let lines = (raw.abs() * scale).ceil().max(1.0) as isize;
|
||||||
|
let delta = if raw < 0.0 { lines } else { -lines };
|
||||||
|
if let Some(session) = self.session.as_mut() {
|
||||||
|
session.term.scroll_view(delta);
|
||||||
|
self.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl OutputHandler for App {
|
impl OutputHandler for App {
|
||||||
fn output_state(&mut self) -> &mut OutputState {
|
fn output_state(&mut self) -> &mut OutputState {
|
||||||
&mut self.output_state
|
&mut self.output_state
|
||||||
|
|
@ -572,6 +647,7 @@ delegate_output!(App);
|
||||||
delegate_shm!(App);
|
delegate_shm!(App);
|
||||||
delegate_seat!(App);
|
delegate_seat!(App);
|
||||||
delegate_keyboard!(App);
|
delegate_keyboard!(App);
|
||||||
|
delegate_pointer!(App);
|
||||||
delegate_xdg_shell!(App);
|
delegate_xdg_shell!(App);
|
||||||
delegate_xdg_window!(App);
|
delegate_xdg_window!(App);
|
||||||
delegate_registry!(App);
|
delegate_registry!(App);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue