From eebeed1f2774146c3d7df90c7a982f58428d4cff Mon Sep 17 00:00:00 2001 From: MaxVerevkin Date: Mon, 2 Jan 2023 20:23:35 +0200 Subject: rewrite using wayrs --- src/state.rs | 721 ++++++++++++++++++++++++++++++++--------------------------- 1 file changed, 389 insertions(+), 332 deletions(-) (limited to 'src/state.rs') diff --git a/src/state.rs b/src/state.rs index e2e57ee..dbe73fd 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,103 +1,76 @@ -use std::{future::pending, io}; - -use smithay_client_toolkit::{ - compositor::{CompositorHandler, CompositorState}, - delegate_compositor, delegate_layer, delegate_output, delegate_pointer, delegate_registry, - delegate_seat, delegate_shm, - output::{OutputHandler, OutputState}, - reexports::client::{ - protocol::{wl_output, wl_pointer, wl_seat, wl_surface}, - Connection, EventQueue, QueueHandle, - }, - registry::{ProvidesRegistryState, RegistryState}, - registry_handlers, - seat::{ - pointer::{PointerEvent, PointerEventKind, PointerHandler, ThemeSpec, ThemedPointer}, - Capability, SeatHandler, SeatState, - }, - shell::layer::{ - Anchor, Layer, LayerShell, LayerShellHandler, LayerSurface, LayerSurfaceConfigure, - }, - shm::{slot::SlotPool, ShmHandler, ShmState}, -}; -use tokio::io::unix::AsyncFdReadyGuard; -use wayland_client::globals::GlobalList; +use crate::cursor::Cursor; +use crate::protocol::*; + +use std::convert::Infallible; +use std::future::pending; + +use wayrs_client::connection::Connection; +use wayrs_client::global::{Global, GlobalExt, Globals, GlobalsExt}; +use wayrs_client::proxy::{Dispatch, Dispatcher, Proxy}; +use wayrs_shm_alloc::ShmAlloc; use crate::{ - bar::Bar, - config::Config, - delegate_river_control, delegate_river_status, - i3bar_protocol::Block, - pointer_btn::PointerBtn, - river_protocols::{ - control::{RiverControlHandler, RiverControlState}, - status::{RiverStatusHandler, RiverStatusState}, - }, - shared_state::SharedState, - status_cmd::StatusCmd, - text::ComputedText, + bar::Bar, config::Config, i3bar_protocol::Block, pointer_btn::PointerBtn, + shared_state::SharedState, status_cmd::StatusCmd, text::ComputedText, }; pub struct State { - registry_state: RegistryState, - shm_state: ShmState, - seat_state: SeatState, - output_state: OutputState, - compositor_state: CompositorState, - layer_state: LayerShell, - river_status_state: Option, - river_control_state: Option, + wl_compositor: WlCompositor, + layer_shell: ZwlrLayerShellV1, + + river_status_manager: Option, + river_control: Option, seats: Vec, bars: Vec, + pub shared_state: SharedState, } struct Seat { - seat: wl_seat::WlSeat, - pointer: wl_pointer::WlPointer, - themed_pointer: ThemedPointer, - pointer_surface: wl_surface::WlSurface, - finger_scroll: f64, + seat: WlSeat, + reg_name: u32, + pointer: Option, + cursor: Cursor, + cur_surface: Option, + x: f64, + y: f64, + pending_button: Option, + pending_scroll: f64, + scroll_frame: ScrollFrame, } impl State { - pub fn new(event_queue: &mut EventQueue, globals: &GlobalList) -> Self { + pub fn new(conn: &mut Connection, globals: &Globals) -> Self { let mut error = Ok(()); - let qh = event_queue.handle(); - let config = Config::new() .map_err(|e| error = Err(e)) .unwrap_or_default(); - let status_cmd = match &error { - Err(_) => None, - Ok(()) => config.command.as_ref().and_then(|cmd| { - StatusCmd::new(cmd) - .map_err(|e| error = Err(anyhow!(e))) - .ok() - }), - }; + let status_cmd = config + .command + .as_ref() + .and_then(|cmd| StatusCmd::new(cmd).map_err(|e| error = Err(e)).ok()); - let shm_state = ShmState::bind(globals, &qh).unwrap(); - let pool = SlotPool::new(1024, &shm_state).expect("Failed to create pool"); + let shm = globals.bind(conn, 1..=1).expect("could not bind wl_shm"); let mut this = Self { - registry_state: RegistryState::new(globals), - shm_state, - seat_state: SeatState::new(globals, &qh), - output_state: OutputState::new(globals, &qh), - compositor_state: CompositorState::bind(globals, &qh).unwrap(), - layer_state: LayerShell::bind(globals, &qh).unwrap(), - river_status_state: RiverStatusState::new(globals, &qh).ok(), - river_control_state: RiverControlState::new(globals, &qh).ok(), + wl_compositor: globals + .bind(conn, 4..=5) + .expect("could not bind wl_compositor"), + layer_shell: globals + .bind(conn, 1..=4) + .expect("could not bind layer_shell"), + + river_status_manager: globals.bind(conn, 1..=4).ok(), + river_control: globals.bind(conn, 1..=1).ok(), seats: Vec::new(), bars: Vec::new(), + shared_state: SharedState { - qh: event_queue.handle(), - pool, + shm: ShmAlloc::new(conn, shm, 1024), config, status_cmd, blocks: Vec::new(), @@ -105,113 +78,102 @@ impl State { }, }; + globals + .iter() + .filter(|g| g.is::()) + .for_each(|g| this.bind_seat(conn, g)); + globals + .iter() + .filter(|g| g.is::()) + .for_each(|g| this.bind_output(conn, g)); + if let Err(e) = error { - this.set_error(e.to_string()); + this.set_error(conn, e.to_string()); } this } - pub fn set_blocks(&mut self, blocks: Vec) { + pub fn set_blocks(&mut self, conn: &mut Connection, blocks: Vec) { self.shared_state.blocks = blocks; - self.draw_all(); - } - - pub fn set_error(&mut self, error: impl Into) { - self.set_blocks(vec![Block { - full_text: error.into(), - ..Default::default() - }]); + self.draw_all(conn); } - pub fn notify_available(&mut self) -> anyhow::Result<()> { - if let Some(cmd) = &mut self.shared_state.status_cmd { - if let Some(blocks) = cmd.notify_available()? { - self.set_blocks(blocks); - } - } - Ok(()) + pub fn set_error(&mut self, conn: &mut Connection, error: impl Into) { + self.set_blocks( + conn, + vec![Block { + full_text: error.into(), + ..Default::default() + }], + ); } - pub fn draw_all(&mut self) { + pub fn draw_all(&mut self, conn: &mut Connection) { for bar in &mut self.bars { - bar.draw(&mut self.shared_state); + bar.request_frame(conn); } } - pub async fn wait_for_status_cmd(&self) -> io::Result> { - match &self.shared_state.status_cmd { - Some(cmd) => cmd.async_fd.readable().await, + pub async fn status_cmd_read(&mut self) -> anyhow::Result<()> { + match &mut self.shared_state.status_cmd { + Some(cmd) => cmd.read().await, None => { pending::<()>().await; unreachable!() } } } -} -impl CompositorHandler for State { - fn scale_factor_changed( + pub fn status_cmd_notify_available( &mut self, - _conn: &Connection, - _qh: &QueueHandle, - surface: &wl_surface::WlSurface, - scale: i32, - ) { - if let Some(bar) = self - .bars - .iter_mut() - .find(|bar| bar.layer.wl_surface() == surface) - { - bar.scale = scale; - bar.draw(&mut self.shared_state); + conn: &mut Connection, + ) -> anyhow::Result<()> { + if let Some(cmd) = &mut self.shared_state.status_cmd { + if let Some(blocks) = cmd.notify_available()? { + self.set_blocks(conn, blocks); + } } + Ok(()) } - fn frame( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _surface: &wl_surface::WlSurface, - _time: u32, - ) { - } -} + fn bind_output(&mut self, conn: &mut Connection, global: &Global) { + let output = global.bind(conn, 2..=4).expect("could not bind wl_output"); + let surface = self.wl_compositor.create_surface(conn); -impl OutputHandler for State { - fn output_state(&mut self) -> &mut OutputState { - &mut self.output_state - } + use zwlr_layer_shell_v1::Layer; + use zwlr_layer_surface_v1::Anchor; + let layer_surface = self.layer_shell.get_layer_surface( + conn, + surface, + output, + Layer::Top, + wayrs_client::cstr!("i3bar-river").into(), + ); + layer_surface.set_size(conn, 0, self.shared_state.config.height); + layer_surface.set_anchor(conn, Anchor::Top | Anchor::Left | Anchor::Right); // Top + Left + Right + layer_surface.set_exclusive_zone(conn, self.shared_state.config.height as i32 + 5); // TODO: make the margin configurable + + // Note: layer_surface is commited when we receive the scale factor of this output - fn new_output( - &mut self, - _conn: &Connection, - qh: &QueueHandle, - output: wl_output::WlOutput, - ) { - let height = self.shared_state.config.height; - let surface = self.compositor_state.create_surface(qh); - let layer = LayerSurface::builder() - .output(&output) - .size((0, height)) - .anchor(Anchor::TOP | Anchor::LEFT | Anchor::RIGHT) - .namespace("i3bar-river") - .map(qh, &self.layer_state, surface, Layer::Top) - .expect("layer surface creation"); let river_output_status = self - .river_status_state - .as_mut() - .map(|s| s.new_output_status(qh, &output)); + .river_status_manager + .as_ref() + .map(|s| s.get_river_output_status(conn, output)); + self.bars.push(Bar { output, + output_reg_name: global.name, configured: false, + frame_cb: None, width: 0, - height, + height: self.shared_state.config.height, scale: 1, - layer, + surface, + layer_surface, blocks_btns: Default::default(), river_output_status, - river_control: self.river_control_state.clone(), + river_control: self.river_control, layout_name: None, layout_name_computed: None, tags_btns: Default::default(), @@ -220,239 +182,319 @@ impl OutputHandler for State { }); } - fn update_output( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _output: wl_output::WlOutput, - ) { + fn bind_seat(&mut self, conn: &mut Connection, global: &Global) { + let seat: WlSeat = global.bind(conn, 5..=8).unwrap(); + let cursor = Cursor::new(conn, self.wl_compositor).unwrap(); + self.seats.push(Seat { + seat, + reg_name: global.name, + pointer: None, + cursor, + cur_surface: None, + x: 0.0, + y: 0.0, + pending_button: None, + pending_scroll: 0.0, + scroll_frame: ScrollFrame::default(), + }); } - fn output_destroyed( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - _output: wl_output::WlOutput, - ) { + fn drop_bar(&mut self, conn: &mut Connection, bar_index: usize) { + let bar = self.bars.swap_remove(bar_index); + bar.surface.destroy(conn); + bar.layer_surface.destroy(conn); + if let Some(output_status) = bar.river_output_status { + output_status.destroy(conn); + } + if bar.output.version() >= 3 { + bar.output.release(conn); + } } -} -impl LayerShellHandler for State { - fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle, layer: &LayerSurface) { - self.bars.retain(|b| &b.layer != layer) + fn drop_seat(&mut self, conn: &mut Connection, seat_index: usize) { + let seat = self.seats.swap_remove(seat_index); + if let Some(pointer) = seat.pointer { + if pointer.version() >= 3 { + pointer.release(conn); + } + } + if seat.seat.version() >= 5 { + seat.seat.release(conn); + } } +} - fn configure( - &mut self, - _conn: &Connection, - _qh: &QueueHandle, - layer: &LayerSurface, - configure: LayerSurfaceConfigure, - _serial: u32, - ) { - self.bars - .iter_mut() - .find(|b| &b.layer == layer) - .unwrap() - .configure(&mut self.shared_state, configure.new_size.0); - } +impl Dispatcher for State { + type Error = Infallible; } -impl SeatHandler for State { - fn seat_state(&mut self) -> &mut SeatState { - &mut self.seat_state +impl Dispatch for State { + fn event(&mut self, conn: &mut Connection, _: WlRegistry, event: wl_registry::Event) { + match event { + wl_registry::Event::Global(global) if global.is::() => { + self.bind_output(conn, &global); + } + wl_registry::Event::Global(global) if global.is::() => { + self.bind_seat(conn, &global); + } + wl_registry::Event::GlobalRemove(name) => { + if let Some(bar_index) = + self.bars.iter().position(|bar| bar.output_reg_name == name) + { + self.drop_bar(conn, bar_index); + } else if let Some(seat_index) = + self.seats.iter().position(|seat| seat.reg_name == name) + { + self.drop_seat(conn, seat_index); + } + } + _ => (), + } } +} - fn new_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} +impl Dispatch for State { + fn event(&mut self, conn: &mut Connection, cb: WlCallback, event: wl_callback::Event) { + let wl_callback::Event::Done(_) = event; + if let Some(bar) = self.bars.iter_mut().find(|bar| bar.frame_cb == Some(cb)) { + bar.frame_cb = None; + bar.frame(conn, &mut self.shared_state); + } + } +} - fn new_capability( - &mut self, - _conn: &Connection, - qh: &QueueHandle, - seat: wl_seat::WlSeat, - capability: Capability, - ) { - if capability == Capability::Pointer { - let (pointer, themed_pointer) = self - .seat_state - .get_pointer_with_theme(qh, &seat, ThemeSpec::System, 1) - .expect("Failed to create pointer"); - self.seats.push(Seat { - seat, - pointer, - themed_pointer, - pointer_surface: self.compositor_state.create_surface(qh), - finger_scroll: 0.0, - }); +impl Dispatch for State { + fn event(&mut self, conn: &mut Connection, output: WlOutput, event: wl_output::Event) { + if let wl_output::Event::Scale(scale) = event { + let bar = self + .bars + .iter_mut() + .find(|bar| bar.output == output) + .unwrap(); + bar.scale = scale; + // If bar is not configured yet, it is because we were waiting for the "scale" event + // before commiting the surface. Otherwise, there is no need to do any redrawing (we'll + // do that after "configure" event). + if !bar.configured { + bar.surface.commit(conn); + } } } +} - fn remove_capability( +impl Dispatch for State { + fn event( &mut self, - _conn: &Connection, - _qh: &QueueHandle, - seat: wl_seat::WlSeat, - capability: Capability, + conn: &mut Connection, + layer_surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, + event: zwlr_layer_surface_v1::Event, ) { - if capability == Capability::Pointer { - self.seats.retain(|p| p.seat == seat); + match event { + zwlr_layer_surface_v1::Event::Configure(args) => { + let bar = self + .bars + .iter_mut() + .find(|bar| bar.layer_surface == layer_surface) + .unwrap(); + assert_ne!(args.width, 0); + bar.width = args.width; + bar.configured = true; + bar.layer_surface.ack_configure(conn, args.serial); + bar.frame(conn, &mut self.shared_state); + } + zwlr_layer_surface_v1::Event::Closed => { + let bar_index = self + .bars + .iter() + .position(|bar| bar.layer_surface == layer_surface) + .unwrap(); + self.drop_bar(conn, bar_index); + } } } +} - fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} +impl Dispatch for State { + fn event(&mut self, _: &mut Connection, buffer: WlBuffer, event: wl_buffer::Event) { + let wl_buffer::Event::Release = event; + self.shared_state.shm.free_buffer(buffer); + } } -impl PointerHandler for State { - fn pointer_frame( - &mut self, - conn: &Connection, - _qh: &QueueHandle, - pointer: &wl_pointer::WlPointer, - events: &[PointerEvent], - ) { - let seat = self - .seats - .iter_mut() - .find(|p| &p.pointer == pointer) - .unwrap(); - for event in events { - if let Some(bar) = self - .bars - .iter_mut() - .find(|b| b.layer.wl_surface() == &event.surface) - { - match event.kind { - PointerEventKind::Enter { .. } => { - seat.themed_pointer - .set_cursor( - conn, - "default", - self.shm_state.wl_shm(), - &seat.pointer_surface, - ) - .unwrap(); - } - PointerEventKind::Press { button, .. } => { - bar.click( - &mut self.shared_state, - button.into(), - &seat.seat, - event.position.0, - event.position.1, - ) - .unwrap(); - } - PointerEventKind::Axis { - vertical, source, .. - } => { - if source == Some(wl_pointer::AxisSource::Finger) { - if self.shared_state.config.invert_touchpad_scrolling { - seat.finger_scroll -= vertical.absolute; - } else { - seat.finger_scroll += vertical.absolute; - } - if vertical.stop { - seat.finger_scroll = 0.0; - } - } - - let btn = if vertical.discrete > 0 { - PointerBtn::WheelDown - } else if vertical.discrete < 0 { - PointerBtn::WheelUp - } else if seat.finger_scroll >= 15.0 { - seat.finger_scroll = 0.0; - PointerBtn::WheelDown - } else if seat.finger_scroll <= -15.0 { - seat.finger_scroll = 0.0; - PointerBtn::WheelUp - } else { - continue; - }; - - bar.click( - &mut self.shared_state, - btn, - &seat.seat, - event.position.0, - event.position.1, - ) - .unwrap(); +impl Dispatch for State { + fn event(&mut self, conn: &mut Connection, seat: WlSeat, event: wl_seat::Event) { + if let wl_seat::Event::Capabilities(capabilities) = event { + let seat = self.seats.iter_mut().find(|s| s.seat == seat).unwrap(); + match &seat.pointer { + Some(pointer) if !capabilities.contains(wl_seat::Capability::Pointer) => { + if pointer.version() >= 3 { + pointer.release(conn); } - _ => (), + seat.pointer = None; + } + None if capabilities.contains(wl_seat::Capability::Pointer) => { + let pointer = seat.seat.get_pointer(conn); + seat.pointer = Some(pointer); } + _ => (), } } } } -impl ShmHandler for State { - fn shm_state(&mut self) -> &mut ShmState { - &mut self.shm_state - } -} +impl Dispatch for State { + fn event(&mut self, conn: &mut Connection, pointer: WlPointer, event: wl_pointer::Event) { + let seat = self + .seats + .iter_mut() + .find(|s| s.pointer == Some(pointer)) + .unwrap(); -impl RiverStatusHandler for State { - fn river_status_state(&mut self) -> &mut RiverStatusState { - self.river_status_state.as_mut().unwrap() - } + use wl_pointer::Event; + match event { + Event::Frame => { + let btn = seat.pending_button.take(); + let scroll = seat.scroll_frame.finalize(); + if let Some(surface) = seat.cur_surface { + let bar = self + .bars + .iter_mut() + .find(|bar| bar.surface == surface) + .unwrap(); - fn focused_tags_updated(&mut self, output: &wl_output::WlOutput, focused: u32) { - let bar = self.bars.iter_mut().find(|b| &b.output == output).unwrap(); - bar.tags_info.focused = focused; - bar.draw(&mut self.shared_state); - } + if let Some(btn) = btn { + bar.click(conn, &mut self.shared_state, btn, seat.seat, seat.x, seat.y) + .unwrap(); + } - fn urgent_tags_updated(&mut self, output: &wl_output::WlOutput, urgent: u32) { - let bar = self.bars.iter_mut().find(|b| &b.output == output).unwrap(); - bar.tags_info.urgent = urgent; - bar.draw(&mut self.shared_state); - } + if scroll.is_finder && self.shared_state.config.invert_touchpad_scrolling { + seat.pending_scroll -= scroll.absolute; + } else { + seat.pending_scroll += scroll.absolute; + } - fn views_tags_updated(&mut self, output: &wl_output::WlOutput, tags: Vec) { - let bar = self.bars.iter_mut().find(|b| &b.output == output).unwrap(); - bar.tags_info.active = tags.into_iter().fold(0, |a, b| a | b); - bar.draw(&mut self.shared_state); - } + if scroll.stop { + seat.pending_scroll = 0.0; + } - fn layout_name_updated(&mut self, output: &wl_output::WlOutput, layout_name: Option) { - let bar = self.bars.iter_mut().find(|b| &b.output == output).unwrap(); - bar.set_layout_name(layout_name); - bar.draw(&mut self.shared_state); + let btn = if seat.pending_scroll >= 15.0 { + seat.pending_scroll = 0.0; + Some(PointerBtn::WheelDown) + } else if seat.pending_scroll <= -15.0 { + seat.pending_scroll = 0.0; + Some(PointerBtn::WheelUp) + } else { + None + }; + + if let Some(btn) = btn { + bar.click(conn, &mut self.shared_state, btn, seat.seat, seat.x, seat.y) + .unwrap(); + } + } + } + Event::Enter(args) => { + let bar = self + .bars + .iter() + .find(|bar| bar.surface.id() == args.surface) + .unwrap(); + seat.cur_surface = Some(bar.surface); + seat.x = args.surface_x.as_f64(); + seat.y = args.surface_y.as_f64(); + seat.cursor.set( + conn, + args.serial, + pointer, + bar.scale as u32, + &mut self.shared_state.shm, + ); + } + Event::Leave(_) => seat.cur_surface = None, + Event::Motion(args) => { + seat.x = args.surface_x.as_f64(); + seat.y = args.surface_y.as_f64(); + } + Event::Button(args) => { + if args.state == wl_pointer::ButtonState::Pressed { + seat.pending_button = Some(args.button.into()); + } + } + Event::Axis(args) => { + if args.axis == wl_pointer::Axis::VerticalScroll { + seat.scroll_frame.absolute += args.value.as_f64(); + } + } + Event::AxisSource(source) => { + seat.scroll_frame.is_finder = source == wl_pointer::AxisSource::Finger; + } + Event::AxisStop(args) => { + if args.axis == wl_pointer::Axis::VerticalScroll { + seat.scroll_frame.stop = true; + } + } + Event::AxisDiscrete(_) | Event::AxisValue120(_) => (), + } } } -impl RiverControlHandler for State { - fn river_control_state(&mut self) -> &mut RiverControlState { - self.river_control_state.as_mut().unwrap() - } +impl Dispatch for State { + fn event( + &mut self, + conn: &mut Connection, + status: ZriverOutputStatusV1, + event: zriver_output_status_v1::Event, + ) { + let bar = self + .bars + .iter_mut() + .find(|b| b.river_output_status == Some(status)) + .unwrap(); - fn command_failure(&mut self, _: &Connection, _: &QueueHandle, message: String) { - self.set_error(format!("[river_control] {message}")); - } + use zriver_output_status_v1::Event; + match event { + Event::FocusedTags(tags) => bar.tags_info.focused = tags, + Event::UrgentTags(tags) => bar.tags_info.urgent = tags, + Event::LayoutName(name) => bar.set_layout_name(Some(name.into_string().unwrap())), + Event::LayoutNameClear => bar.set_layout_name(None), + Event::ViewTags(vt) => { + bar.tags_info.active = vt + .chunks_exact(4) + .map(|bytes| u32::from_ne_bytes(bytes.try_into().unwrap())) + .fold(0, |a, b| a | b); + } + } - fn command_success(&mut self, _: &Connection, _: &QueueHandle, message: String) { - info!("river_control: {message}"); + bar.request_frame(conn); } } -delegate_compositor!(State); -delegate_output!(State); -delegate_shm!(State); -delegate_seat!(State); -delegate_pointer!(State); -delegate_layer!(State); -delegate_registry!(State); -delegate_river_status!(State); -delegate_river_control!(State); - -impl ProvidesRegistryState for State { - fn registry(&mut self) -> &mut RegistryState { - &mut self.registry_state +impl Dispatch for State { + fn event( + &mut self, + conn: &mut Connection, + _: ZriverCommandCallbackV1, + event: zriver_command_callback_v1::Event, + ) { + use zriver_command_callback_v1::Event; + match event { + Event::Success(msg) => info!("river_control: {msg:?}"), + Event::Failure(msg) => self.set_error(conn, msg.into_string().unwrap()), + } } - registry_handlers![OutputState,]; } +// Interfaces with no events +impl Dispatch for State {} +impl Dispatch for State {} +impl Dispatch for State {} +impl Dispatch for State {} +impl Dispatch for State {} + +// Dont care +impl Dispatch for State {} +impl Dispatch for State {} + #[derive(Debug)] pub struct ComputedBlock { pub block: Block, @@ -460,3 +502,18 @@ pub struct ComputedBlock { pub short: Option, pub min_width: Option, } + +#[derive(Debug, Default, Clone, Copy)] +pub struct ScrollFrame { + stop: bool, + absolute: f64, + is_finder: bool, +} + +impl ScrollFrame { + fn finalize(&mut self) -> Self { + let copy = *self; + *self = Self::default(); + copy + } +} -- cgit v1.3.1