From f353accf4b5d506e2d673d5134004e5b0aca809c Mon Sep 17 00:00:00 2001 From: Ryan Breen Date: Wed, 18 Mar 2026 14:03:37 -0400 Subject: [PATCH] feat: add scroll wheel support with ScrollBar widget Full-stack scroll wheel implementation from HID driver to app UI. Kernel: - Remove mouse click/position diagnostic logging from HID driver - Read scroll wheel byte from boot protocol mouse reports - Add MOUSE_WHEEL atomic with consume-on-read accessor - Add scroll_y field to WindowInputEvent (replaces _pad) - Inject wheel delta into window input events (MOUSE_SCROLL type 9) Breengel: - Add Event::Scroll { delta_y } variant - Parse MOUSE_SCROLL events in Window::poll_events() BWM: - Forward scroll events to focused client window - Use mouse_state_with_scroll() for wheel-aware input libbui: - New ScrollBar widget with proportional thumb, drag support, wheel integration (40px per scroll unit) bcheck: - Integrate ScrollBar on right edge for scrollable test results - Handle Event::Scroll and arrow key scrolling - ScrollBar updates on window resize Co-Authored-By: Claude Opus 4.6 (1M context) --- kernel/src/drivers/usb/hid.rs | 60 +++---- kernel/src/syscall/graphics.rs | 19 ++- libs/breengel/src/event.rs | 13 +- libs/libbreenix/src/graphics.rs | 22 ++- libs/libbui/src/widget/mod.rs | 1 + libs/libbui/src/widget/scroll_bar.rs | 226 +++++++++++++++++++++++++++ userspace/programs/src/bcheck.rs | 35 +++-- userspace/programs/src/bwm.rs | 51 ++++-- 8 files changed, 364 insertions(+), 63 deletions(-) create mode 100644 libs/libbui/src/widget/scroll_bar.rs diff --git a/kernel/src/drivers/usb/hid.rs b/kernel/src/drivers/usb/hid.rs index 41caa3ef..6aedc413 100644 --- a/kernel/src/drivers/usb/hid.rs +++ b/kernel/src/drivers/usb/hid.rs @@ -9,7 +9,7 @@ //! - Keyboard: 8 bytes (1 modifier + 1 reserved + 6 keycodes) //! - Mouse: 3-4 bytes (1 buttons + 1 dx + 1 dy + optional wheel) -use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; +use core::sync::atomic::{AtomicBool, AtomicI32, AtomicU32, AtomicU64, Ordering}; // ============================================================================= // Diagnostic counters (read by heartbeat in timer_interrupt.rs) @@ -46,6 +46,11 @@ static MOUSE_X: AtomicU32 = AtomicU32::new(0); static MOUSE_Y: AtomicU32 = AtomicU32::new(0); static MOUSE_BUTTONS: AtomicU32 = AtomicU32::new(0); +/// Accumulated scroll wheel delta since last read. +/// Positive = scroll up, negative = scroll down. +/// Consumed (reset to 0) when read by the window input event syscall. +static MOUSE_WHEEL: AtomicI32 = AtomicI32::new(0); + /// Per-endpoint button state for multi-endpoint devices (e.g., VMware dual HID). /// When multiple USB HID endpoints report button state independently, one endpoint /// reporting buttons=0 must not cancel the other's press. We track each endpoint's @@ -302,19 +307,15 @@ fn screen_dimensions() -> (u32, u32) { .unwrap_or((1280, 960)) } -/// Process a USB boot protocol mouse report (3-4 bytes). +/// Process a mouse HID report from a specific endpoint. /// -/// Report format: +/// Report format (boot protocol relative mouse): /// - Byte 0: Button flags (bit 0=left, bit 1=right, bit 2=middle) /// - Byte 1: X displacement (signed i8) /// - Byte 2: Y displacement (signed i8) /// - Byte 3: Wheel displacement (optional, signed i8) /// /// Updates the global mouse position atomics with clamping to screen bounds. -/// Counter for diagnostic logging of first few mouse reports. -static MOUSE_LOG_COUNT: AtomicU64 = AtomicU64::new(0); - -/// Process a mouse HID report from a specific endpoint. /// /// `ep_idx` identifies the USB endpoint (0-3) so that multi-endpoint devices /// (like VMware's dual HID mouse) don't race on button state. Each endpoint's @@ -324,25 +325,6 @@ pub fn process_mouse_report(report: &[u8], ep_idx: u8) { return; } - // Log first few non-zero mouse reports for debugging coordinate mapping - let log_n = MOUSE_LOG_COUNT.load(Ordering::Relaxed); - if log_n < 10 && report.iter().take(8).any(|&b| b != 0 && b != 0xDE) { - MOUSE_LOG_COUNT.fetch_add(1, Ordering::Relaxed); - crate::serial_println!( - "[mouse-report] #{}: [{:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x} {:02x}] len={}", - log_n, - report.get(0).copied().unwrap_or(0), - report.get(1).copied().unwrap_or(0), - report.get(2).copied().unwrap_or(0), - report.get(3).copied().unwrap_or(0), - report.get(4).copied().unwrap_or(0), - report.get(5).copied().unwrap_or(0), - report.get(6).copied().unwrap_or(0), - report.get(7).copied().unwrap_or(0), - report.len(), - ); - } - let (sw, sh) = screen_dimensions(); // Determine actual report length: the XHCI driver fills the 64-byte buffer @@ -364,7 +346,6 @@ pub fn process_mouse_report(report: &[u8], ep_idx: u8) { let is_tablet = IS_ABSOLUTE_TABLET.load(Ordering::Relaxed); if !is_tablet && actual_len >= 6 && report[1] == 0 { IS_ABSOLUTE_TABLET.store(true, Ordering::Relaxed); - crate::serial_println!("[mouse] Latched into absolute tablet mode (first 6-byte report with byte[1]=0)"); } // Absolute tablet path: coordinates are always at bytes 2-5. @@ -375,7 +356,6 @@ pub fn process_mouse_report(report: &[u8], ep_idx: u8) { if IS_ABSOLUTE_TABLET.load(Ordering::Relaxed) && report.len() >= 6 { if !TABLET_NO_REPORT_ID.load(Ordering::Relaxed) && report[0] == 0x00 { TABLET_NO_REPORT_ID.store(true, Ordering::Relaxed); - crate::serial_println!("[mouse] Detected tablet format: no report ID prefix (buttons in byte[0])"); } let buttons = if TABLET_NO_REPORT_ID.load(Ordering::Relaxed) { report[0] as u32 @@ -395,10 +375,6 @@ pub fn process_mouse_report(report: &[u8], ep_idx: u8) { if pressed != 0 { MOUSE_BUTTONS_PRESSED.fetch_or(pressed, Ordering::Relaxed); } - static BTN_LOG: AtomicU64 = AtomicU64::new(0); - if BTN_LOG.fetch_add(1, Ordering::Relaxed) < 50 { - crate::serial_println!("[mouse-click] {} -> {} (ep{})", prev, merged, ep_idx); - } } let abs_x = u16::from_le_bytes([report[2], report[3]]) as u32; @@ -406,12 +382,6 @@ pub fn process_mouse_report(report: &[u8], ep_idx: u8) { let new_x = (abs_x * sw / 32768).min(sw - 1); let new_y = (abs_y * sh / 32768).min(sh - 1); - if log_n < 10 { - crate::serial_println!( - "[mouse-pos] abs=({},{}) btn={} screen={}x{} -> ({},{})", - abs_x, abs_y, buttons, sw, sh, new_x, new_y - ); - } MOUSE_X.store(new_x, Ordering::Relaxed); MOUSE_Y.store(new_y, Ordering::Relaxed); crate::syscall::graphics::wake_compositor_if_waiting(); @@ -434,6 +404,11 @@ pub fn process_mouse_report(report: &[u8], ep_idx: u8) { } let dx = report[1] as i8 as i32; let dy = report[2] as i8 as i32; + let wheel = if report.len() >= 4 { + report[3] as i8 as i32 + } else { + 0 + }; let old_x = MOUSE_X.load(Ordering::Relaxed) as i32; let new_x = (old_x + dx).clamp(0, sw as i32 - 1) as u32; @@ -442,6 +417,10 @@ pub fn process_mouse_report(report: &[u8], ep_idx: u8) { let old_y = MOUSE_Y.load(Ordering::Relaxed) as i32; let new_y = (old_y + dy).clamp(0, sh as i32 - 1) as u32; MOUSE_Y.store(new_y, Ordering::Relaxed); + + if wheel != 0 { + MOUSE_WHEEL.fetch_add(wheel, Ordering::Relaxed); + } crate::syscall::graphics::wake_compositor_if_waiting(); } @@ -472,6 +451,11 @@ pub fn mouse_position() -> (u32, u32) { (MOUSE_X.load(Ordering::Relaxed), MOUSE_Y.load(Ordering::Relaxed)) } +/// Consume accumulated scroll wheel delta (resets to 0 after read). +pub fn mouse_wheel_consume() -> i32 { + MOUSE_WHEEL.swap(0, Ordering::Relaxed) +} + /// Get current mouse position and raw button state (non-consuming peek). /// /// Returns instantaneous hardware state (no latch). Used by compositor_wait for diff --git a/kernel/src/syscall/graphics.rs b/kernel/src/syscall/graphics.rs index 8d022d9b..7112e56a 100644 --- a/kernel/src/syscall/graphics.rs +++ b/kernel/src/syscall/graphics.rs @@ -162,7 +162,8 @@ pub struct WindowInputEvent { pub mouse_y: i16, /// Modifier bitmask (bit 0=shift, bit 1=ctrl, bit 2=alt) pub modifiers: u16, - pub _pad: u16, + /// Scroll wheel delta (positive = scroll up, negative = scroll down) + pub scroll_y: i16, } #[cfg(target_arch = "aarch64")] @@ -965,7 +966,21 @@ fn handle_virgl_op(cmd: &FbDrawCmd) -> SyscallResult { if event_ptr == 0 || event_ptr >= USER_SPACE_MAX { return SyscallResult::Err(super::ErrorCode::Fault as u64); } - let event: WindowInputEvent = unsafe { core::ptr::read(event_ptr as *const WindowInputEvent) }; + let mut event: WindowInputEvent = unsafe { core::ptr::read(event_ptr as *const WindowInputEvent) }; + + // Inject accumulated scroll wheel delta into mouse events. + // MOUSE_MOVE=3, MOUSE_BUTTON=4, MOUSE_SCROLL=9 + if event.event_type == 3 || event.event_type == 4 || event.event_type == 9 { + let wheel = crate::drivers::usb::hid::mouse_wheel_consume(); + if wheel != 0 { + event.scroll_y = event.scroll_y.saturating_add(wheel.clamp(-32768, 32767) as i16); + // If this was a plain MOUSE_MOVE (3) with wheel data, upgrade to MOUSE_SCROLL (9) + // so clients that filter on event type receive it correctly. + if event.event_type == 3 && event.scroll_y != 0 { + event.event_type = 9; + } + } + } let wake_tid = { let mut reg = WINDOW_REGISTRY.lock(); diff --git a/libs/breengel/src/event.rs b/libs/breengel/src/event.rs index 95df6bfb..b99bb205 100644 --- a/libs/breengel/src/event.rs +++ b/libs/breengel/src/event.rs @@ -32,6 +32,11 @@ pub enum Event { MouseMove { x: i32, y: i32 }, /// Mouse button pressed or released. MouseButton { button: u8, pressed: bool, x: i32, y: i32 }, + /// Mouse scroll wheel event. + /// + /// `delta_y` > 0 means scroll up (content moves down / offset decreases). + /// `delta_y` < 0 means scroll down (content moves up / offset increases). + Scroll { delta_y: i32 }, /// This window gained keyboard focus. FocusGained, /// This window lost keyboard focus. @@ -49,6 +54,9 @@ pub enum Event { impl Event { /// Convert a raw kernel input event to a high-level Event. + /// + /// Unknown event types fall back to a `KeyPress` with ascii=0 and + /// the raw keycode, so they are not silently dropped. pub fn from_raw(raw: &WindowInputEvent) -> Self { match raw.event_type { input_event_type::KEY_PRESS => Event::KeyPress { @@ -66,10 +74,13 @@ impl Event { }, input_event_type::MOUSE_BUTTON => Event::MouseButton { button: raw.keycode as u8, - pressed: raw._pad != 0, + pressed: raw.scroll_y != 0, x: raw.mouse_x as i32, y: raw.mouse_y as i32, }, + input_event_type::MOUSE_SCROLL => Event::Scroll { + delta_y: raw.scroll_y as i32, + }, input_event_type::FOCUS_GAINED => Event::FocusGained, input_event_type::FOCUS_LOST => Event::FocusLost, input_event_type::CLOSE_REQUESTED => Event::CloseRequested, diff --git a/libs/libbreenix/src/graphics.rs b/libs/libbreenix/src/graphics.rs index ea0b2396..c611bdf5 100644 --- a/libs/libbreenix/src/graphics.rs +++ b/libs/libbreenix/src/graphics.rs @@ -633,7 +633,10 @@ pub struct WindowInputEvent { pub mouse_y: i16, /// Modifier bitmask (bit 0=shift, bit 1=ctrl, bit 2=alt) pub modifiers: u16, - pub _pad: u16, + /// Scroll wheel delta. Positive = scroll up, negative = scroll down. + /// Set on MOUSE_MOVE and MOUSE_BUTTON events when the wheel moved this frame. + /// Also the sole payload for MOUSE_SCROLL events. + pub scroll_y: i16, } /// Cursor shape constants for `set_cursor_shape`. @@ -696,6 +699,8 @@ pub mod input_event_type { pub const FOCUS_LOST: u16 = 6; pub const CLOSE_REQUESTED: u16 = 7; pub const WINDOW_RESIZED: u16 = 8; + /// Scroll wheel event. `scroll_y` > 0 = scroll up, < 0 = scroll down. + pub const MOUSE_SCROLL: u16 = 9; } /// Write an input event to a window's kernel ring buffer. @@ -947,6 +952,21 @@ pub fn mouse_state() -> Result<(u32, u32, u32), Error> { Ok((state[0], state[1], state[2])) } +/// Get the current mouse cursor position, button state, and scroll wheel delta. +/// +/// The scroll delta is an accumulated signed value: positive = scroll up, +/// negative = scroll down. The kernel resets the accumulator on each read. +/// +/// # Returns +/// * Ok((x, y, buttons, scroll_y)) - Mouse position, buttons, and wheel delta +/// * Err(Error) - Error (ENODEV if no pointer device) +pub fn mouse_state_with_scroll() -> Result<(u32, u32, u32, i32), Error> { + let mut state: [u32; 4] = [0, 0, 0, 0]; + let ret = unsafe { raw::syscall1(nr::GET_MOUSE_POS, &mut state as *mut [u32; 4] as u64) as i64 }; + Error::from_syscall(ret)?; + Ok((state[0], state[1], state[2], state[3] as i32)) +} + // ============================================================================ // RAII Framebuffer Wrapper // ============================================================================ diff --git a/libs/libbui/src/widget/mod.rs b/libs/libbui/src/widget/mod.rs index 22dda39e..7c6bc866 100644 --- a/libs/libbui/src/widget/mod.rs +++ b/libs/libbui/src/widget/mod.rs @@ -4,6 +4,7 @@ pub mod file_picker; pub mod label; pub mod menu_bar; pub mod panel; +pub mod scroll_bar; pub mod slider; pub mod tab_bar; diff --git a/libs/libbui/src/widget/scroll_bar.rs b/libs/libbui/src/widget/scroll_bar.rs new file mode 100644 index 00000000..8d08b0fa --- /dev/null +++ b/libs/libbui/src/widget/scroll_bar.rs @@ -0,0 +1,226 @@ +//! Vertical scroll bar widget. +//! +//! Tracks a scroll offset for content that exceeds the visible area. +//! Renders a track with a draggable thumb. + +use libgfx::color::Color; +use libgfx::framebuf::FrameBuf; +use libgfx::shapes; + +use crate::input::InputState; +use crate::rect::Rect; +use crate::theme::Theme; + +/// Vertical scroll bar widget. +/// +/// Tracks a scroll offset for content taller than the visible area. +/// Renders a track with a proportional draggable thumb. +/// +/// # Usage +/// +/// ```no_run +/// let mut bar = ScrollBar::new( +/// Rect::new(win_w - ScrollBar::DEFAULT_WIDTH, 0, ScrollBar::DEFAULT_WIDTH, win_h), +/// content_height, +/// visible_height, +/// ); +/// +/// // In the event loop: +/// if let Event::Scroll { delta_y } = event { +/// bar.scroll(delta_y); +/// } +/// bar.update(&input); +/// +/// // When rendering content, subtract bar.offset() from all y positions. +/// // Draw the bar last so it renders on top of content. +/// bar.draw(fb, &theme); +/// ``` +pub struct ScrollBar { + /// Position and size of the scroll bar track. + rect: Rect, + /// Total content height in pixels. + content_height: i32, + /// Visible area height in pixels. + view_height: i32, + /// Current scroll offset (0 = top, max = content_height - view_height). + offset: i32, + /// Whether the thumb is currently being dragged. + dragging: bool, + /// Y offset within the thumb where the drag began. + drag_anchor: i32, +} + +impl ScrollBar { + /// Default scrollbar track width in pixels. + pub const DEFAULT_WIDTH: i32 = 12; + + /// Create a new vertical scroll bar. + /// + /// `rect` is the bounding box of the track (typically a narrow vertical strip + /// along the right edge of the content area). + /// + /// `content_height` is the total rendered content height in pixels. + /// `view_height` is how many pixels are actually visible. + pub fn new(rect: Rect, content_height: i32, view_height: i32) -> Self { + Self { + rect, + content_height, + view_height, + offset: 0, + dragging: false, + drag_anchor: 0, + } + } + + /// The current scroll offset in pixels (0 = top). + pub fn offset(&self) -> i32 { + self.offset + } + + /// Update content and view heights after content changes or window resizes. + /// + /// The offset is clamped to the new valid range automatically. + pub fn set_dimensions(&mut self, content_height: i32, view_height: i32) { + self.content_height = content_height; + self.view_height = view_height; + self.clamp_offset(); + } + + /// Update the scrollbar's bounding rect (e.g., after a window resize). + pub fn set_rect(&mut self, rect: Rect) { + self.rect = rect; + } + + /// Scroll by `delta_y` units. + /// + /// Positive `delta_y` scrolls up (offset decreases toward 0). + /// Negative `delta_y` scrolls down (offset increases toward max). + /// + /// Each unit corresponds to approximately 3 rows of 13px text (about 40px). + pub fn scroll(&mut self, delta_y: i32) { + self.offset -= delta_y * 40; + self.clamp_offset(); + } + + /// Process mouse input for drag interaction. + /// + /// Returns `true` if the scrollbar consumed the event (the caller should + /// not pass this input to widgets behind the scrollbar). + pub fn update(&mut self, input: &InputState) -> bool { + let max_scroll = self.max_scroll(); + if max_scroll <= 0 { + self.dragging = false; + return false; + } + + let thumb = self.thumb_rect(); + + if input.mouse_pressed && self.rect.contains(input.mouse_x, input.mouse_y) { + if thumb.contains(input.mouse_x, input.mouse_y) { + // Start drag from within the thumb + self.dragging = true; + self.drag_anchor = input.mouse_y - thumb.y; + } else { + // Click on track: jump to the clicked position + let track_y = input.mouse_y - self.rect.y; + let ratio = track_y as f32 / self.rect.h as f32; + self.offset = (ratio * max_scroll as f32) as i32; + self.clamp_offset(); + } + return true; + } + + if self.dragging { + if input.mouse_down { + // Continue dragging — map mouse Y to scroll offset + let thumb_top = input.mouse_y - self.drag_anchor - self.rect.y; + let track_range = self.rect.h - thumb.h; + if track_range > 0 { + let ratio = thumb_top as f32 / track_range as f32; + self.offset = (ratio * max_scroll as f32) as i32; + self.clamp_offset(); + } + return true; + } else { + self.dragging = false; + } + } + + false + } + + /// Draw the scrollbar track and thumb. + /// + /// When `can_scroll()` is false (content fits in the view), nothing is drawn. + pub fn draw(&self, fb: &mut FrameBuf, _theme: &Theme) { + if !self.can_scroll() { + return; + } + + // Track background + let track_color = Color::rgb(40, 42, 48); + shapes::fill_rect(fb, self.rect.x, self.rect.y, self.rect.w, self.rect.h, track_color); + + // Thumb — inset by 2px on sides, 1px top/bottom for a rounded appearance + let thumb = self.thumb_rect(); + let thumb_color = if self.dragging { + Color::rgb(140, 150, 170) + } else { + Color::rgb(80, 85, 100) + }; + shapes::fill_rect( + fb, + thumb.x + 2, + thumb.y + 1, + (thumb.w - 4).max(2), + (thumb.h - 2).max(2), + thumb_color, + ); + } + + /// Whether the content exceeds the view height (scrollbar is needed). + pub fn can_scroll(&self) -> bool { + self.content_height > self.view_height + } + + /// Maximum scroll offset = content_height - view_height. + pub fn max_scroll(&self) -> i32 { + (self.content_height - self.view_height).max(0) + } + + // ── Private helpers ────────────────────────────────────────────────────── + + fn clamp_offset(&mut self) { + let max = self.max_scroll(); + if self.offset < 0 { + self.offset = 0; + } + if self.offset > max { + self.offset = max; + } + } + + fn thumb_rect(&self) -> Rect { + let max_scroll = self.max_scroll(); + if max_scroll <= 0 || self.content_height <= 0 { + return Rect::new(self.rect.x, self.rect.y, self.rect.w, self.rect.h); + } + + // Thumb height proportional to visible fraction of total content + let ratio = self.view_height as f32 / self.content_height as f32; + let thumb_h = ((ratio * self.rect.h as f32) as i32) + .max(20) // minimum 20px so it's always clickable + .min(self.rect.h); + + // Thumb Y position proportional to scroll offset + let track_range = self.rect.h - thumb_h; + let thumb_y = if max_scroll > 0 { + self.rect.y + + (self.offset as f32 / max_scroll as f32 * track_range as f32) as i32 + } else { + self.rect.y + }; + + Rect::new(self.rect.x, thumb_y, self.rect.w, thumb_h) + } +} diff --git a/userspace/programs/src/bcheck.rs b/userspace/programs/src/bcheck.rs index bd394b58..32714767 100644 --- a/userspace/programs/src/bcheck.rs +++ b/userspace/programs/src/bcheck.rs @@ -14,6 +14,9 @@ use libgfx::framebuf::FrameBuf; use libgfx::shapes; use libgfx::ttf_font; +use libbui::Rect; +use libbui::widget::scroll_bar::ScrollBar; + // --------------------------------------------------------------------------- // Test definitions // --------------------------------------------------------------------------- @@ -468,29 +471,39 @@ fn main() { let failed = tests.iter().filter(|t| t.status == TestStatus::Fail).count(); println!("[bcheck] Complete: {}/{} passed, {} failed", passed, total, failed); - // Keep displaying results — support scrolling with arrow keys + // Keep displaying results — support scrolling with arrow keys and scroll wheel let visible_h = WIN_H as i32 - LIST_START_Y - FOOTER_H; let total_h = content_height(&tests); - let mut max_scroll = (total_h - visible_h).max(0); - let mut scroll_offset: i32 = 0; let sleep_ts = libbreenix::types::Timespec { tv_sec: 0, tv_nsec: 50_000_000 }; // 50ms + // Scrollbar positioned along the right edge of the content area + let bar_w = ScrollBar::DEFAULT_WIDTH; + let bar_rect = Rect::new(WIN_W as i32 - bar_w, LIST_START_Y, bar_w, visible_h); + let mut scroll_bar = ScrollBar::new(bar_rect, total_h, visible_h); + let theme = libbui::Theme::dark(); + loop { let mut need_redraw = false; for event in win.poll_events() { match event { Event::KeyPress { keycode, .. } => { match keycode { - 0x52 => { scroll_offset = (scroll_offset - ROW_H).max(0); need_redraw = true; } - 0x51 => { scroll_offset = (scroll_offset + ROW_H).min(max_scroll); need_redraw = true; } + 0x52 => { scroll_bar.scroll(1); need_redraw = true; } + 0x51 => { scroll_bar.scroll(-1); need_redraw = true; } _ => {} } } - Event::Resized { width: _, height: h } => { + Event::Scroll { delta_y } => { + scroll_bar.scroll(delta_y); + need_redraw = true; + } + Event::Resized { width: w, height: h } => { let new_visible_h = h as i32 - LIST_START_Y - FOOTER_H; - let new_max = (total_h - new_visible_h).max(0); - max_scroll = new_max; - scroll_offset = scroll_offset.min(max_scroll); + let new_bar_rect = Rect::new( + w as i32 - bar_w, LIST_START_Y, bar_w, new_visible_h, + ); + scroll_bar.set_rect(new_bar_rect); + scroll_bar.set_dimensions(total_h, new_visible_h); need_redraw = true; } Event::CloseRequested => std::process::exit(0), @@ -498,7 +511,9 @@ fn main() { } } if need_redraw { - render(win.framebuf(), &tests, scroll_offset, &mut ttf_font, font_size); + render(win.framebuf(), &tests, scroll_bar.offset(), &mut ttf_font, font_size); + // Draw the scrollbar on top of the content + scroll_bar.draw(win.framebuf(), &theme); let _ = win.present(); } else { let _ = time::nanosleep(&sleep_ts); diff --git a/userspace/programs/src/bwm.rs b/userspace/programs/src/bwm.rs index aaa94e5a..cd479af3 100644 --- a/userspace/programs/src/bwm.rs +++ b/userspace/programs/src/bwm.rs @@ -948,7 +948,7 @@ fn send_focus_event(windows: &[Window], win_idx: usize, event_type: u16) { if win_idx < windows.len() && windows[win_idx].window_id != 0 { let event = WindowInputEvent { event_type, - keycode: 0, mouse_x: 0, mouse_y: 0, modifiers: 0, _pad: 0, + keycode: 0, mouse_x: 0, mouse_y: 0, modifiers: 0, scroll_y: 0, }; let _ = graphics::write_window_input(windows[win_idx].window_id, &event); } @@ -965,7 +965,7 @@ fn route_mouse_button_to_focused( mouse_x: win_local_x, mouse_y: win_local_y, modifiers: 0, - _pad: if pressed { 1 } else { 0 }, + scroll_y: if pressed { 1 } else { 0 }, }; let _ = graphics::write_window_input(windows[focused_win].window_id, &event); } @@ -980,7 +980,24 @@ fn route_mouse_move_to_focused( event_type: input_event_type::MOUSE_MOVE, keycode: 0, mouse_x: win_local_x, mouse_y: win_local_y, - modifiers: 0, _pad: 0, + modifiers: 0, scroll_y: 0, + }; + let _ = graphics::write_window_input(windows[focused_win].window_id, &event); + } +} + +fn route_mouse_scroll_to_focused( + windows: &[Window], focused_win: usize, + win_local_x: i16, win_local_y: i16, scroll_delta: i16, +) { + if focused_win < windows.len() && windows[focused_win].window_id != 0 { + let event = WindowInputEvent { + event_type: input_event_type::MOUSE_SCROLL, + keycode: 0, + mouse_x: win_local_x, + mouse_y: win_local_y, + modifiers: 0, + scroll_y: scroll_delta, }; let _ = graphics::write_window_input(windows[focused_win].window_id, &event); } @@ -1088,7 +1105,7 @@ fn discover_windows( mouse_x: default_ch as i16, mouse_y: 0, modifiers: 0, - _pad: 0, + scroll_y: 0, }; let _ = graphics::write_window_input(info.buffer_id, &resize_event); } @@ -1249,7 +1266,7 @@ fn execute_hotkey_action(action: &HotkeyAction, windows: &mut Vec, focus if let Some(idx) = launcher_idx { let event = WindowInputEvent { event_type: input_event_type::CLOSE_REQUESTED, - keycode: 0, mouse_x: 0, mouse_y: 0, modifiers: 0, _pad: 0, + keycode: 0, mouse_x: 0, mouse_y: 0, modifiers: 0, scroll_y: 0, }; let _ = graphics::write_window_input(windows[idx].window_id, &event); return; @@ -1266,7 +1283,7 @@ fn execute_hotkey_action(action: &HotkeyAction, windows: &mut Vec, focus if !windows.is_empty() && focused_win < windows.len() { let event = WindowInputEvent { event_type: input_event_type::CLOSE_REQUESTED, - keycode: 0, mouse_x: 0, mouse_y: 0, modifiers: 0, _pad: 0, + keycode: 0, mouse_x: 0, mouse_y: 0, modifiers: 0, scroll_y: 0, }; let _ = graphics::write_window_input(windows[focused_win].window_id, &event); } @@ -1525,7 +1542,7 @@ fn main() { mouse_x: ascii as i16, mouse_y: 0, modifiers, - _pad: 0, + scroll_y: 0, }; route_keyboard_to_focused(&windows, focused_win, &win_event); } @@ -1547,7 +1564,7 @@ fn main() { // ── 4. Process mouse input (only when mouse changed) ── let mut mouse_moved_this_frame = false; if ready & graphics::COMPOSITOR_READY_MOUSE != 0 { - if let Ok((mx, my, buttons)) = graphics::mouse_state() { + if let Ok((mx, my, buttons, scroll_delta)) = graphics::mouse_state_with_scroll() { let new_mx = mx as i32; let new_my = my as i32; let mouse_moved = new_mx != mouse_x || new_my != mouse_y; @@ -1648,7 +1665,7 @@ fn main() { mouse_x: new_ch as i16, mouse_y: 0, modifiers: 0, - _pad: 0, + scroll_y: 0, }; let _ = graphics::write_window_input( windows[win_idx].window_id, &resize_event, @@ -1723,7 +1740,7 @@ fn main() { mouse_x: new_ch as i16, mouse_y: 0, modifiers: 0, - _pad: 0, + scroll_y: 0, }; let _ = graphics::write_window_input( windows[win_idx].window_id, &resize_event, @@ -1744,6 +1761,18 @@ fn main() { } } + // Scroll wheel: forward to focused window content area + if scroll_delta != 0 && !windows.is_empty() && focused_win < windows.len() + && !windows[focused_win].minimized + && windows[focused_win].window_id != 0 + { + let local_x = (mouse_x - windows[focused_win].content_x()) as i16; + let local_y = (mouse_y - windows[focused_win].content_y()) as i16; + route_mouse_scroll_to_focused( + &windows, focused_win, local_x, local_y, scroll_delta as i16, + ); + } + // Click: focus change + drag or route click to client let new_click = (buttons & 1) != 0 && (prev_buttons & 1) == 0; prev_buttons = buttons; @@ -1843,7 +1872,7 @@ fn main() { if windows[top].hit_close_button(mouse_x, mouse_y) { let close_event = WindowInputEvent { event_type: input_event_type::CLOSE_REQUESTED, - keycode: 0, mouse_x: 0, mouse_y: 0, modifiers: 0, _pad: 0, + keycode: 0, mouse_x: 0, mouse_y: 0, modifiers: 0, scroll_y: 0, }; let _ = graphics::write_window_input(windows[top].window_id, &close_event); if windows[top].owner_pid > 0 {