cloud storage client
git clone https://git.lucas.co/cce-cloud.git
fix: edge rows render cut, not culled; sliver clicks select them
The ScrollRegion copy is deleted for the toolkit's
cce_ui::widget::ScrollRegion (cce-ui@764d9e7): get_draw_y now returns
partially visible rows, and the list content (selection chip, icons,
row labels) paints under the list-viewport clip so edge rows render
cut. Row labels split out of own_labels so the chrome (query line,
empty-state) stays outside the clip. walk_text_labels now carries each
prim's merged clip bounds and prepare_text turns them into the span's
physical clip — without that, a cut row's text drew whole through the
glyph pass.
The 1063422 click gate keeps its shape — paint and click share the
get_draw_y predicate — but the predicate's new contract makes an edge
sliver legitimately selectable: visible => clickable, culled => not.
Verified in a shadow session (40-item dmenu): a half-cut row renders
clipped mid-glyph, and clicking its sliver prints that row and closes.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/main.rs | 147 +++++++++++++++---------
src/scroll_region.rs | 317 ---------------------------------------------------
2 files changed, 96 insertions(+), 368 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index dc75284..56a8350 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,4 @@
-mod scroll_region;
-use scroll_region::ScrollRegion;
+use cce_ui::widget::ScrollRegion;
use std::sync::{Arc, Mutex};
use std::io::{self, BufRead, IsTerminal};
@@ -123,15 +122,23 @@ fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buff
/// A widget subtree's text via the paint walk (not the legacy text_labels getter),
/// reduced to the plain labels this renderer shapes: the buffer font and window bounds
/// stay exactly as before (make_text_buffer applies the control font to every label).
-fn walk_text_labels(ui: &cce_ui::context::UiContext, w: &dyn WidgetHost) -> Vec<TextLabel> {
+/// Each label rides with its merged clip bounds (logical `[l, t, r, b]`, from
+/// the paint walk's clip ∩ the prim's own bounds — `append_widget_text` merges
+/// them): prepare_text turns them into the span's physical clip so text cut by
+/// a clip (a partially visible list row) is cut at the glyph pass too, not
+/// drawn whole.
+fn walk_text_labels(
+ ui: &cce_ui::context::UiContext,
+ w: &dyn WidgetHost,
+) -> Vec<(TextLabel, Option<[f32; 4]>)> {
let mut pc = cce_ui::scene::paint::PaintCtx::new();
cce_ui::scene::painter::append_widget_text(ui, w, &mut pc);
pc.finish()
.items
.into_iter()
.filter_map(|item| match item.prim {
- cce_ui::scene::paint::Prim::Text { text, x, y, font_size, color, .. } => {
- Some(TextLabel { text, x, y, font_size, color })
+ cce_ui::scene::paint::Prim::Text { text, x, y, font_size, color, bounds, .. } => {
+ Some((TextLabel { text, x, y, font_size, color }, bounds))
}
_ => None,
})
@@ -743,38 +750,57 @@ impl cce_ui::widget::Paint for FuzzelWidget {
ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
}
- // Selected Item Highlight
- let item_h = 25.0;
- if !self.filtered_items.is_empty() {
- let virtual_selected_y = self.selected as f32 * item_h;
- if let Some(draw_y) = self.scroll_box.get_draw_y(virtual_selected_y, item_h) {
- let scrollbar_w = if self.scroll_box.content_h > self.scroll_box.viewport_h { 10.0 } else { 0.0 };
- // A raised beveled chip, not a flat tint: the selection reads
- // as sitting proud of the list the way focused panes do.
- let sel = Rect {
- x: self.x + pad + 2.0,
- y: draw_y,
- width: self.w - pad * 2.0 - 4.0 - scrollbar_w,
- height: item_h - 2.0,
- };
- let depth = cce_ui::color::plate_bevel_width().min(sel.height * 0.2);
- ctx.bevel(sel, (4.0, 4.0, 4.0, 4.0), [0.20, 0.35, 0.65, 0.9], depth);
+ // The list content — selection chip, icons, row labels — under the
+ // list-viewport clip: `get_draw_y` returns PARTIALLY visible rows (the
+ // toolkit ScrollRegion's intersection contract), so an edge row
+ // renders cut by the clip instead of vanishing. Row text carries the
+ // clip as bounds through walk_text_labels → prepare_text.
+ let viewport = Rect {
+ x: self.scroll_box.x,
+ y: self.scroll_box.viewport_y,
+ width: self.scroll_box.w,
+ height: self.scroll_box.viewport_h,
+ };
+ ctx.clip(viewport, |ctx| {
+ // Selected Item Highlight
+ let item_h = 25.0;
+ if !self.filtered_items.is_empty() {
+ let virtual_selected_y = self.selected as f32 * item_h;
+ if let Some(draw_y) = self.scroll_box.get_draw_y(virtual_selected_y, item_h) {
+ let scrollbar_w = if self.scroll_box.content_h > self.scroll_box.viewport_h { 10.0 } else { 0.0 };
+ // A raised beveled chip, not a flat tint: the selection reads
+ // as sitting proud of the list the way focused panes do.
+ let sel = Rect {
+ x: self.x + pad + 2.0,
+ y: draw_y,
+ width: self.w - pad * 2.0 - 4.0 - scrollbar_w,
+ height: item_h - 2.0,
+ };
+ let depth = cce_ui::color::plate_bevel_width().min(sel.height * 0.2);
+ ctx.bevel(sel, (4.0, 4.0, 4.0, 4.0), [0.20, 0.35, 0.65, 0.9], depth);
+ }
}
- }
- // App icons, on the same virtualization predicate as the labels below:
- // only rows `get_draw_y` places inside the viewport are emitted, so a
- // 300-app list still costs one image quad per visible row.
- if self.icon_gutter > 0.0 {
- for (idx, item_text) in self.filtered_items.iter().enumerate() {
- let Some((image, iw, ih)) = self.icons.get(item_text).copied() else { continue };
- if let Some(draw_y) = self.scroll_box.get_draw_y(idx as f32 * item_h, item_h) {
- ctx.image(image, self.icon_rect(draw_y, item_h, iw, ih), 1.0);
+ // App icons, on the same virtualization predicate as the labels:
+ // only rows `get_draw_y` places in the viewport are emitted, so a
+ // 300-app list still costs one image quad per visible row.
+ if self.icon_gutter > 0.0 {
+ let item_h = 25.0;
+ for (idx, item_text) in self.filtered_items.iter().enumerate() {
+ let Some((image, iw, ih)) = self.icons.get(item_text).copied() else { continue };
+ if let Some(draw_y) = self.scroll_box.get_draw_y(idx as f32 * item_h, item_h) {
+ ctx.image(image, self.icon_rect(draw_y, item_h, iw, ih), 1.0);
+ }
}
}
- }
- // Own labels: prompt/query line, visible items, empty-state notice.
+ // Visible item labels.
+ for l in self.row_labels() {
+ ctx.text(l.text, l.x, l.y, l.font_size, l.color);
+ }
+ });
+
+ // Prompt/query line and the empty-state notice — outside the list clip.
for l in self.own_labels() {
ctx.text(l.text, l.x, l.y, l.font_size, l.color);
}
@@ -794,9 +820,11 @@ impl cce_ui::widget::Input for FuzzelWidget {
let item_h = 25.0;
// `hit()` spans the whole region, scrollbar strip included, and the row math
// below accepts any y inside it — so without these two gates a press on the
- // scrollbar, or on the partially-clipped sliver at the viewport edge, resolved
- // to a row. In Dmenu/switcher mode a press commits and closes, so that emitted
- // an item the user never clicked (and, on the sliver, never even saw).
+ // scrollbar resolved to a row. In Dmenu/switcher mode a press commits and
+ // closes, so that emitted an item the user never clicked. The `get_draw_y`
+ // gate keeps click and paint on the SAME predicate: it now returns partially
+ // visible rows too (drawn cut by the viewport clip), so a press on an edge
+ // sliver selects the row the user can see — visible ⇒ clickable, culled ⇒ not.
if self.scroll_box.hit(*px, *py) && !self.scroll_box.hit_scrollbar(*px, *py) {
let click_virtual_y = *py - self.scroll_box.viewport_y + self.scroll_box.scroll_y;
let clicked_idx = (click_virtual_y / item_h).floor() as usize;
@@ -1594,7 +1622,7 @@ impl State {
fn prepare_text(&mut self) {
let scale_f32 = self.scale as f32;
- let mut widget_labels: Vec<TextLabel> = Vec::new();
+ let mut widget_labels: Vec<(TextLabel, Option<[f32; 4]>)> = Vec::new();
if self.mode == LauncherMode::Json {
if let Some(jl) = &self.json_layout {
widget_labels.extend(walk_text_labels(&self.ui_context, jl));
@@ -1604,7 +1632,7 @@ impl State {
}
let mut buffers: Vec<Buffer> = Vec::with_capacity(widget_labels.len());
- for label in &widget_labels {
+ for (label, _) in &widget_labels {
buffers.push(make_text_buffer(&mut self.font_system, &label.text, label.font_size));
}
@@ -1612,13 +1640,22 @@ impl State {
let spans: Vec<TextSpan> = buffers
.iter()
.zip(widget_labels.iter())
- .map(|(buf, label)| TextSpan {
+ .map(|(buf, (label, bounds))| TextSpan {
buffer: buf,
left: (label.x * scale_f32).round(),
top: (label.y * scale_f32).round(),
// Buffers are shaped at logical size; the span scales to physical.
scale: scale_f32,
- bounds: None,
+ // Logical merged clip (walk clip ∩ prim bounds) → physical px,
+ // so a partially visible row's text is cut at the viewport.
+ bounds: bounds.map(|b| {
+ [
+ (b[0] * scale_f32).floor() as i32,
+ (b[1] * scale_f32).floor() as i32,
+ (b[2] * scale_f32).ceil() as i32,
+ (b[3] * scale_f32).ceil() as i32,
+ ]
+ }),
default_color: [
label.color[0] as f32 / 255.0,
label.color[1] as f32 / 255.0,
@@ -3930,7 +3967,27 @@ impl FuzzelWidget {
color: query_color,
});
+ if self.filtered_items.is_empty() {
+ let list_y = self.y + pad + search_h + 10.0;
+ labels.push(TextLabel {
+ text: "No matches found".to_string(),
+ x: self.x + pad + 10.0,
+ y: list_y + 4.0,
+ font_size: 13.0,
+ color: [0x88, 0x88, 0x99],
+ });
+ }
+
+ labels
+ }
+
+ /// Visible item labels — one per row `get_draw_y` places in (or partially
+ /// in) the viewport. Emitted under the paint walk's list clip, separately
+ /// from [`Self::own_labels`], which draws chrome outside it.
+ fn row_labels(&self) -> Vec<TextLabel> {
+ let pad = 15.0;
let item_h = 25.0;
+ let mut labels = Vec::new();
for (idx, item_text) in self.filtered_items.iter().enumerate() {
let virtual_y = idx as f32 * item_h;
if let Some(draw_y) = self.scroll_box.get_draw_y(virtual_y, item_h) {
@@ -3951,18 +4008,6 @@ impl FuzzelWidget {
});
}
}
-
- if self.filtered_items.is_empty() {
- let list_y = self.y + pad + search_h + 10.0;
- labels.push(TextLabel {
- text: "No matches found".to_string(),
- x: self.x + pad + 10.0,
- y: list_y + 4.0,
- font_size: 13.0,
- color: [0x88, 0x88, 0x99],
- });
- }
-
labels
}
}
diff --git a/src/scroll_region.rs b/src/scroll_region.rs
deleted file mode 100644
index 8e42e25..0000000
--- a/src/scroll_region.rs
+++ /dev/null
@@ -1,317 +0,0 @@
-//! App-owned scroll region replacing the dissolved `ScrollBox` embedded base (the Phase
-//! 6q ScrollRegion, ported via cce-mail). The fuzzel list drew its own rows at
-//! `get_draw_y` positions; the ScrollBox contributed only the flat background, the
-//! scrollbar, the scroll math, and wheel input — `push_quads` and the ScrollBox-shaped
-//! shims below reproduce those verbatim.
-
-use cce_ui::widget::{ElementState, Key, KeyEvent, MouseScrollDelta, NamedKey};
-
-#[derive(Debug, Clone)]
-pub struct ScrollRegion {
- pub x: f32,
- pub y: f32,
- pub w: f32,
- pub h: f32,
- /// Row height, with `List::new`'s silent adjustment to `max(item_height, list_font + 14)`.
- pub item_height: f32,
- pub item_gap: f32,
- pub scroll_y: f32,
- pub content_h: f32,
- pub viewport_y: f32,
- pub viewport_h: f32,
- pub dragging: bool,
- drag_offset_y: f32,
- pub hovered: bool,
- /// Local stand-in for the legacy global focus flag (`ScrollBox::focus()` on any press
- /// inside the frame): set on a press that hits the region, cleared on one that misses.
- pub focused: bool,
-}
-
-impl ScrollRegion {
- pub fn new(item_height: f32, item_gap: f32) -> Self {
- let (_, font_size) = cce_ui::layout::list_font_parsed();
- Self {
- x: 0.0,
- y: 0.0,
- w: 0.0,
- h: 0.0,
- item_height: item_height.max(font_size + 14.0),
- item_gap,
- scroll_y: 0.0,
- content_h: 0.0,
- viewport_y: 0.0,
- viewport_h: 0.0,
- dragging: false,
- drag_offset_y: 0.0,
- hovered: false,
- focused: false,
- }
- }
-
- pub fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
- self.x = x;
- self.y = y;
- self.w = w;
- self.h = h;
- }
-
- /// The `List::update_bounds` count math: `content_h = count * (item_height + gap) + 4`.
- pub fn update_bounds(&mut self, count: usize, viewport_y: f32, viewport_h: f32) {
- self.content_h = count as f32 * (self.item_height + self.item_gap) + 4.0;
- self.viewport_y = viewport_y;
- self.viewport_h = viewport_h;
- self.scroll_y = self.scroll_y.clamp(0.0, self.max_scroll());
- }
-
- pub fn set_scroll_y(&mut self, val: f32) {
- self.scroll_y = val;
- }
-
- fn max_scroll(&self) -> f32 {
- (self.content_h - self.viewport_h).max(0.0)
- }
-
- pub fn hit(&self, px: f32, py: f32) -> bool {
- px >= self.x && px < self.x + self.w && py >= self.y && py < self.y + self.h
- }
-
- /// Row virtualization (`List::get_item_draw_y`): screen y for row `idx`, or `None`
- /// when the row isn't fully inside the viewport.
- /// `ScrollBox::update_bounds` shape: raw content height, not a row count.
- pub fn update_bounds_raw(&mut self, content_h: f32, viewport_y: f32, viewport_h: f32) {
- self.content_h = content_h;
- self.viewport_y = viewport_y;
- self.viewport_h = viewport_h;
- let max_scroll = (content_h - viewport_h).max(0.0);
- self.scroll_y = self.scroll_y.clamp(0.0, max_scroll);
- }
-
- /// `ScrollBox::get_item_draw_y` shape: a precomputed virtual y + item height.
- pub fn get_draw_y(&self, virtual_y: f32, item_h: f32) -> Option<f32> {
- let draw_y = self.viewport_y + virtual_y - self.scroll_y;
- if draw_y >= self.viewport_y - 1.0 && draw_y + item_h <= self.viewport_y + self.viewport_h + 1.0 {
- Some(draw_y)
- } else {
- None
- }
- }
-
- pub fn get_item_draw_y(&self, idx: usize, offset: f32) -> Option<f32> {
- let virtual_y = idx as f32 * (self.item_height + self.item_gap) + offset;
- let draw_y = self.viewport_y + virtual_y - self.scroll_y;
- if draw_y >= self.viewport_y - 1.0
- && draw_y + self.item_height <= self.viewport_y + self.viewport_h + 1.0
- {
- Some(draw_y)
- } else {
- None
- }
- }
-
- /// Scrollbar geometry (`ScrollBox::extra_quads`): (sb_x, track_y, sb_w, track_h, thumb_y, thumb_h).
- fn scrollbar_geom(&self) -> (f32, f32, f32, f32, f32, f32) {
- let sb_w = cce_ui::layout::scrollbar_width();
- let sb_x = self.x + self.w - sb_w - 4.0;
- let track_h = self.viewport_h - 8.0;
- let track_y = self.viewport_y + 4.0;
- let visible_ratio = self.viewport_h / self.content_h.max(1.0);
- let thumb_h = if track_h <= 20.0 {
- track_h
- } else {
- (track_h * visible_ratio).clamp(20.0, track_h)
- };
- let scroll_ratio = if self.max_scroll() > 0.0 { self.scroll_y / self.max_scroll() } else { 0.0 };
- let thumb_y = track_y + scroll_ratio * (track_h - thumb_h);
- (sb_x, track_y, sb_w, track_h, thumb_y, thumb_h)
- }
-
- pub fn hit_scrollbar(&self, px: f32, py: f32) -> bool {
- if self.content_h <= self.viewport_h {
- return false;
- }
- let (sb_x, track_y, sb_w, track_h, _, _) = self.scrollbar_geom();
- px >= sb_x - 4.0 && px <= sb_x + sb_w + 4.0 && py >= track_y && py <= track_y + track_h
- }
-
- /// Left press: scrollbar thumb grab or track jump (`ScrollBox::mouse_input`), plus the
- /// press-inside focus / press-outside unfocus bookkeeping. Returns true only when the
- /// scrollbar consumed the press — a press on the rows falls through to them.
- pub fn press(&mut self, px: f32, py: f32) -> bool {
- self.focused = self.hit(px, py);
- if !self.hit_scrollbar(px, py) {
- self.dragging = false;
- return false;
- }
- self.dragging = true;
- let (_, track_y, _, track_h, thumb_y, thumb_h) = self.scrollbar_geom();
- let click_offset = py - thumb_y;
- if click_offset >= 0.0 && click_offset <= thumb_h {
- self.drag_offset_y = click_offset;
- } else {
- self.drag_offset_y = thumb_h / 2.0;
- let target = py - self.drag_offset_y;
- let ratio = if track_h - thumb_h > 0.0 {
- ((target - track_y) / (track_h - thumb_h)).clamp(0.0, 1.0)
- } else {
- 0.0
- };
- self.scroll_y = ratio * self.max_scroll();
- }
- true
- }
-
- /// Returns whether a thumb drag was in progress (the caller's redraw signal).
- pub fn release(&mut self) -> bool {
- std::mem::take(&mut self.dragging)
- }
-
- fn drag_move(&mut self, py: f32) -> bool {
- let (_, track_y, _, track_h, _, thumb_h) = self.scrollbar_geom();
- let target = py - self.drag_offset_y;
- let ratio = if track_h - thumb_h > 0.0 {
- ((target - track_y) / (track_h - thumb_h)).clamp(0.0, 1.0)
- } else {
- 0.0
- };
- let old = self.scroll_y;
- self.scroll_y = ratio * self.max_scroll();
- (self.scroll_y - old).abs() > 0.01
- }
-
- /// Pointer-move bookkeeping: forwards to an active thumb drag (returns true so the host
- /// treats it as a high-priority drag override), else just tracks hover for the border
- /// tint and the keyboard scope.
- pub fn cursor_moved(&mut self, px: f32, py: f32) -> bool {
- self.hovered = self.hit(px, py);
- if self.dragging {
- self.drag_move(py);
- return true;
- }
- false
- }
-
- pub fn wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32) -> bool {
- if !self.hit(px, py) {
- return false;
- }
- let dy = match delta {
- MouseScrollDelta::LineDelta(_, y) => -y * 24.0,
- MouseScrollDelta::PixelDelta(pos) => -pos.y as f32,
- };
- let old = self.scroll_y;
- self.scroll_y = (self.scroll_y + dy).clamp(0.0, self.max_scroll());
- (self.scroll_y - old).abs() > 0.01
- }
-
- /// Hover/focus-scoped keyboard scrolling (`ScrollBox::keyboard_input` reached the boxes
- /// when focused or hovered; the dissolved region keeps both via its local flags).
- pub fn keyboard(&mut self, event: &KeyEvent) -> bool {
- if (!self.hovered && !self.focused) || event.state != ElementState::Pressed {
- return false;
- }
- let max = self.max_scroll();
- let old = self.scroll_y;
- if event.ctrl {
- match &event.logical_key {
- Key::Character(c) if c == "n" || c == "N" => self.scroll_y = (self.scroll_y + 24.0).clamp(0.0, max),
- Key::Character(c) if c == "p" || c == "P" => self.scroll_y = (self.scroll_y - 24.0).clamp(0.0, max),
- _ => return false,
- }
- } else {
- match &event.logical_key {
- Key::Named(NamedKey::ArrowDown) => self.scroll_y = (self.scroll_y + 24.0).clamp(0.0, max),
- Key::Named(NamedKey::ArrowUp) => self.scroll_y = (self.scroll_y - 24.0).clamp(0.0, max),
- Key::Named(NamedKey::PageDown) => self.scroll_y = (self.scroll_y + self.viewport_h).clamp(0.0, max),
- Key::Named(NamedKey::PageUp) => self.scroll_y = (self.scroll_y - self.viewport_h).clamp(0.0, max),
- Key::Named(NamedKey::Home) => self.scroll_y = 0.0,
- Key::Named(NamedKey::End) => self.scroll_y = max,
- _ => return false,
- }
- }
- (self.scroll_y - old).abs() > 0.01
- }
-
- /// The legacy frame, single-drawn: 1px rounded border (focus/hover tinted, from
- /// `List::solid_border`), inset rounded bg, then the scrollbar track and thumb ON TOP.
- /// The legacy `ScrollBox::extra_quads` emission: flat background, then the
- /// scrollbar track + thumb when the content overflows.
- pub fn push_quads(&self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>) {
- quads.push((self.x, self.y, self.w, self.h, cce_ui::color::list_bg_color()));
- if self.content_h > self.viewport_h {
- let (sb_x, track_y, sb_w, track_h, thumb_y, thumb_h) = self.scrollbar_geom();
- quads.push((sb_x, track_y, sb_w, track_h, cce_ui::color::scrollbar_track_color()));
- quads.push((sb_x, thumb_y, sb_w, thumb_h, cce_ui::color::scrollbar_thumb_color()));
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- fn region() -> ScrollRegion {
- // item_height clamps to list_font + 14, so pick one comfortably above any config.
- let mut r = ScrollRegion::new(40.0, 4.0);
- r.set_rect(10.0, 20.0, 200.0, 100.0);
- r
- }
-
- #[test]
- fn wheel_scrolls_and_clamps() {
- let mut r = region();
- r.update_bounds(10, 20.0, 100.0); // content_h = 444 > 100
- assert!(r.wheel(&MouseScrollDelta::LineDelta(0.0, -2.0), 50.0, 50.0));
- assert_eq!(r.scroll_y, 48.0);
- assert!(!r.wheel(&MouseScrollDelta::LineDelta(0.0, -2.0), 500.0, 50.0)); // miss
- r.wheel(&MouseScrollDelta::LineDelta(0.0, -100.0), 50.0, 50.0);
- assert_eq!(r.scroll_y, 344.0); // clamped to max_scroll
- }
-
- #[test]
- fn virtualization_matches_list_math() {
- let mut r = region();
- r.update_bounds(10, 20.0, 100.0);
- r.set_scroll_y(0.0);
- // Row 0 at viewport_y + 0*(44) + 4 = 24; fits (24 + 40 <= 121).
- assert_eq!(r.get_item_draw_y(0, 4.0), Some(24.0));
- // Row 2 at 20 + 92 - 0 = 112; 112 + 40 > 121 → culled.
- assert!(r.get_item_draw_y(2, 4.0).is_none());
- }
-
- #[test]
- fn press_focuses_and_grabs_only_scrollbar() {
- let mut r = region();
- r.update_bounds(10, 20.0, 100.0);
- // Press in the rows area: focused, not dragging, falls through.
- assert!(!r.press(50.0, 50.0));
- assert!(r.focused && !r.dragging);
- // Press on the scrollbar strip (x + w - sb_w - 4 ± 4): consumed.
- let sb_x = 10.0 + 200.0 - cce_ui::layout::scrollbar_width() - 4.0;
- assert!(r.press(sb_x + 1.0, 50.0));
- assert!(r.dragging);
- assert!(r.release());
- // Press outside: unfocuses.
- assert!(!r.press(500.0, 500.0));
- assert!(!r.focused);
- }
-
- #[test]
- fn keyboard_is_hover_or_focus_scoped() {
- let mut r = region();
- r.update_bounds(10, 20.0, 100.0);
- let down = KeyEvent {
- state: ElementState::Pressed,
- logical_key: Key::Named(NamedKey::ArrowDown),
- text: None,
- repeat: false,
- ctrl: false,
- shift: false,
- alt: false,
- };
- assert!(!r.keyboard(&down)); // neither hovered nor focused
- r.cursor_moved(50.0, 50.0);
- assert!(r.hovered);
- assert!(r.keyboard(&down));
- assert_eq!(r.scroll_y, 24.0);
- }
-}