diff --git a/docs/widgets.md b/docs/widgets.md index 615e70b..e62e76f 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -905,7 +905,7 @@ ownership. ### `tab_strip` -The open documents of an app, as a browser or a terminal shows them. On a desktop, a strip of tabs with their icon, title and close button, sharing the width between a minimum and a fifth of the screen, scrolling sideways when even the minimum does not fit, and always leaving 7.5 % of the width clear; buttons go before the tabs (`leading`), right after the last one (`after_tabs`) and at the far right (`trailing`). On a phone (`TabLayout::Mobile`), a bar with the active tab, or the app's own `mobile_center`, and a counter that sends `on_overview`; `tab_overview` is the grid of cards it opens, as a whole page with `build` or as the cards alone with `cards`. `TabStripColors` sets every colour, the theme's by default. +The open documents of an app, as a browser or a terminal shows them. On a desktop, a strip of tabs with their icon, title and close button, sharing the width between a minimum and a fifth of the screen, scrolling sideways when even the minimum does not fit, to show the selected tab when it changes and while a tab is dragged against its edge, and always leaving 7.5 % of the width clear; buttons go before the tabs (`leading`), right after the last one (`after_tabs`) and at the far right (`trailing`). On a phone (`TabLayout::Mobile`), a bar with the active tab, or the app's own `mobile_center`, and a counter that sends `on_overview`; `tab_overview` is the grid of cards it opens, as a whole page with `build` or as the cards alone with `cards`. `TabStripColors` sets every colour, the theme's by default. Tabs grow from nothing as they open and the others narrow; a closed tab leaves a gap that shrinks away while the others keep their width for a second, so the close button that slides in lands under the pointer. The app keeps a `TabMotion`, hands it to every view and reports `TabMotion::is_animating` from `App::is_animating`. diff --git a/src/app.rs b/src/app.rs index c632374..9b9a99b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -377,6 +377,29 @@ pub struct RawKey pub text: Option, } +/// A mouse button, as [`App::on_mouse_button`] reports it. +#[ derive( Debug, Clone, Copy, PartialEq, Eq ) ] +pub enum MouseButton +{ + Left, + Middle, + Right, +} + +/// A mouse button pressed or released, as [`App::on_mouse_button`] reports it. +#[ derive( Debug, Clone, Copy ) ] +pub struct MouseButtonEvent +{ + pub button: MouseButton, + /// In physical pixels of the surface under the pointer, as [`App::on_pointer_move`] reports them. + pub x: f32, + pub y: f32, + pub pressed: bool, + pub ctrl: bool, + pub shift: bool, + pub alt: bool, +} + /// Which of the two selections an app reads, through [`App::take_paste_request`]. #[ derive( Debug, Clone, Copy, PartialEq, Eq ) ] pub enum SelectionKind @@ -1018,6 +1041,13 @@ pub trait App: 'static /// A middle-button press, in physical pixels. Default: no-op. fn on_pointer_middle( &mut self, _x: f32, _y: f32 ) {} + /// Every press and release of the left, middle and right buttons, with the modifiers held, before ltk handles it: for an + /// app that hands the mouse on, as a terminal does to a program that asks for it. Default: no-op. + fn on_mouse_button( &mut self, _event: &MouseButtonEvent ) {} + + /// The modifiers held changed, for an app that needs them with the wheel or a motion, which do not carry them. Default: no-op. + fn on_modifiers( &mut self, _ctrl: bool, _shift: bool, _alt: bool ) {} + /// Called on every keyboard event (press *and* release) before the /// regular focus-aware dispatch ([`Self::on_key`] / /// [`Self::on_key_with_modifiers`]) runs. Default: no-op. diff --git a/src/core.rs b/src/core.rs index 719727e..ef0ea59 100644 --- a/src/core.rs +++ b/src/core.rs @@ -413,6 +413,7 @@ impl UiSurface previous_widget_rects: self.widget_rects.clone(), accessible_extras: Vec::new(), live_depth: 0, + origin: ( 0.0, 0.0 ), }; draw::layout_and_draw( element, &mut self.canvas, options.bounds, &mut ctx, 0 ); diff --git a/src/draw/layout.rs b/src/draw/layout.rs index 06d9e04..147df45 100644 --- a/src/draw/layout.rs +++ b/src/draw/layout.rs @@ -96,11 +96,18 @@ pub( crate ) fn layout_and_draw( } Element::TabRow( r ) => { - let inner = r.arrange( rect, canvas ); + // The child is the scroll of the tabs, whose offset is keyed by this same index. + let inner = r.arrange( rect, canvas ); + let offset = ctx.scroll_offsets.get( &flat_idx ).copied().unwrap_or( ( 0.0, 0.0 ) ); + if let Some( x ) = r.scroll_to( offset.0, inner.width, canvas ) + { + ctx.scroll_offsets.insert( flat_idx, ( x, offset.1 ) ); + } layout_and_draw::( r.child.as_ref(), canvas, inner, ctx, flat_idx ) } Element::TabCells( cells ) => { + cells.motion.set_origin( ctx.origin ); let child_rects = cells.layout( rect, canvas ); let mut idx = flat_idx; for ( child_rect, child_i ) in child_rects @@ -111,6 +118,7 @@ pub( crate ) fn layout_and_draw( } Element::TabCards( cards ) => { + cards.motion.set_origin( ctx.origin ); let child_rects = cards.layout( rect, canvas ); let mut idx = flat_idx; for ( child_rect, child_i ) in child_rects @@ -300,7 +308,10 @@ pub( crate ) fn layout_and_draw( let rects_before = ctx.widget_rects.len(); let scroll_rects_before = ctx.scroll_rects.len(); + let origin = ctx.origin; + ctx.origin = ( origin.0 + rect.x, origin.1 + rect.y ); let next_idx = layout_and_draw::( s.child.as_ref(), &mut sub, child_rect, ctx, my_idx + 1 ); + ctx.origin = origin; // Translate widget_rects from sub-canvas space to global space; drop // clipped ones. Also clamp `paint_rect` to the scroll viewport so a @@ -452,7 +463,10 @@ pub( crate ) fn layout_and_draw( let child_rect = Rect { x: 0.0, y: 0.0, width: rect.width, height: effective_h }; let rects_before = ctx.widget_rects.len(); let scroll_rects_before = ctx.scroll_rects.len(); + let origin = ctx.origin; + ctx.origin = ( origin.0 + rect.x, origin.1 + rect.y ); let next_idx = layout_and_draw::( v.child.as_ref(), &mut sub, child_rect, ctx, flat_idx ); + ctx.origin = origin; let new_rects: Vec> = ctx.widget_rects.drain( rects_before.. ).collect(); for mut w in new_rects diff --git a/src/draw/mod.rs b/src/draw/mod.rs index 6a45898..2cebdf9 100644 --- a/src/draw/mod.rs +++ b/src/draw/mod.rs @@ -98,6 +98,8 @@ pub( crate ) struct DrawCtx pub accessible_extras: Vec, /// Depth counter for containers marked `a11y_live`. pub live_depth: u32, + /// Where the canvas being drawn on sits on the surface: a scroll or a viewport draws its child on a canvas of its own. + pub origin: ( f32, f32 ), } /// Build the per-frame [`DrawCtx`] from the surface's [`FrameState`], moving the @@ -133,6 +135,7 @@ pub( crate ) fn build_draw_ctx( previous_widget_rects: frame.widget_rects.clone(), accessible_extras: Vec::new(), live_depth: 0, + origin: ( 0.0, 0.0 ), } } diff --git a/src/event_loop/subsurface.rs b/src/event_loop/subsurface.rs index e6d184f..19a981a 100644 --- a/src/event_loop/subsurface.rs +++ b/src/event_loop/subsurface.rs @@ -329,6 +329,7 @@ fn empty_draw_ctx() -> DrawCtx previous_widget_rects: Vec::new(), accessible_extras: Vec::new(), live_depth: 0, + origin: ( 0.0, 0.0 ), } } diff --git a/src/input/keyboard/mod.rs b/src/input/keyboard/mod.rs index 8eba403..47a020a 100644 --- a/src/input/keyboard/mod.rs +++ b/src/input/keyboard/mod.rs @@ -153,6 +153,7 @@ impl KeyboardHandler for AppData self.shift_pressed = modifiers.shift; self.ctrl_pressed = modifiers.ctrl; self.alt_pressed = modifiers.alt; + self.app.on_modifiers( modifiers.ctrl, modifiers.shift, modifiers.alt ); } fn update_repeat_info( diff --git a/src/input/pointer/mod.rs b/src/input/pointer/mod.rs index d6e3a13..189c939 100644 --- a/src/input/pointer/mod.rs +++ b/src/input/pointer/mod.rs @@ -55,6 +55,7 @@ impl PointerHandler for AppData let focus = self.focus_for_surface( &event.surface ) .unwrap_or( SurfaceFocus::Main ); self.pointer_focus = focus; + self.report_mouse_button( focus, event ); match event.kind { PointerEventKind::Enter { .. } => self.on_pointer_enter( conn, qh, pointer, event ), @@ -71,3 +72,35 @@ impl PointerHandler for AppData } } } + +impl AppData +{ + /// Hand a press or release of the left, middle or right button to [`App::on_mouse_button`]. + fn report_mouse_button( &mut self, focus: SurfaceFocus, event: &PointerEvent ) + { + let ( code, pressed ) = match event.kind + { + PointerEventKind::Press { button, .. } => ( button, true ), + PointerEventKind::Release { button, .. } => ( button, false ), + _ => return, + }; + let button = match code + { + 0x110 => crate::app::MouseButton::Left, + 0x111 => crate::app::MouseButton::Right, + 0x112 => crate::app::MouseButton::Middle, + _ => return, + }; + let pos = self.surface( focus ).to_physical( event.position.0, event.position.1 ); + self.app.on_mouse_button( &crate::app::MouseButtonEvent + { + button, + x: pos.x, + y: pos.y, + pressed, + ctrl: self.ctrl_pressed, + shift: self.shift_pressed, + alt: self.alt_pressed, + } ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 9984e22..88f36c2 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -313,7 +313,7 @@ pub use app:: { Anchor, App, BackAtRoot, BackResponse, ChannelSender, ShellMode, Layer, OverlayId, OverlaySpec, SubsurfaceId, SubsurfaceSpec, SubsurfaceParent, - InvalidationScope, RawKey, SelectionKind, SurfaceTarget, ToplevelEvent, + InvalidationScope, MouseButton, MouseButtonEvent, RawKey, SelectionKind, SurfaceTarget, ToplevelEvent, }; pub use theme:: { diff --git a/src/widget/tab_strip/desktop.rs b/src/widget/tab_strip/desktop.rs index 082eba3..e104a68 100644 --- a/src/widget/tab_strip/desktop.rs +++ b/src/widget/tab_strip/desktop.rs @@ -18,11 +18,12 @@ pub( super ) fn build( strip: TabStrip ) -> Element bool { @@ -64,7 +71,14 @@ impl TabMotion /// Where each tab goes in `rect`, the dragged one last, and where the tabs end. pub( crate ) fn lay( &self, rect: Rect, gap: f32 ) -> ( Vec, f32 ) { - self.lock().lay( rect, gap ) + self.lock().lay( rect, gap, Instant::now() ) + } + + /// Where the strip, scrolled by `offset` in a `viewport` that wide, should scroll to: to show the `selected` tab when it + /// changed, or `step` further towards the edge a dragged tab is held against; `None` to leave it. + pub( crate ) fn scroll( &self, selected: Option, offset: f32, viewport: f32, step: f32 ) -> Option + { + self.lock().scroll( selected, offset, viewport, step, Instant::now() ) } /// Where each card of `keys` goes in `rect`, in `columns` of `cell`-sized cards, the dragged one last. @@ -216,6 +230,11 @@ struct Motion gap: f32, shape: Shape, laid: Vec, + /// The selected tab last scrolled into view, and whether it is still being followed while it grows. + revealed: Option, + revealing: bool, + /// Of the canvas the last layout was on, on the surface. + origin: ( f32, f32 ), } /// The left edge of every slot from the strip's, each gap as wide as the slot before it is present. @@ -359,11 +378,16 @@ impl Motion laid } - fn lay( &mut self, rect: Rect, gap: f32 ) -> ( Vec, f32 ) + fn lay( &mut self, rect: Rect, gap: f32, now: Instant ) -> ( Vec, f32 ) { self.rect = rect; self.gap = gap; self.shape = Shape::Strip; + // The strip may have scrolled under a pointer that held still. + if let Some( ( key, at ) ) = self.drag.as_ref().map( |drag| ( drag.key, ( drag.pointer.0 - drag.grab.0, drag.pointer.1 - drag.grab.1 ) ) ) + { + self.follow( key, at, now ); + } let end = offsets( &self.placed, gap ).last().zip( self.placed.last() ).map_or( 0.0, |( at, slot )| at + slot.width ); let laid = self.place( |slot| ( slot.width, rect.height ), Rect { width: end, ..rect } ); ( laid, rect.x + end ) @@ -389,8 +413,31 @@ impl Motion self.place( |_| cell, Rect { height, ..rect } ) } + fn scroll( &mut self, selected: Option, offset: f32, viewport: f32, step: f32, now: Instant ) -> Option + { + // The tabs are laid out on the scroll's own canvas, whose visible part starts at 0. + if let Some( drag ) = &self.drag + { + let lifted = self.laid.iter().find( |laid| laid.key == drag.key )?.rect; + if lifted.x + lifted.width >= viewport - 0.5 { return Some( offset + step ); } + if lifted.x <= 0.5 && offset > 0.0 { return Some( ( offset - step ).max( 0.0 ) ); } + return None; + } + if selected != self.revealed + { + self.revealed = selected; + self.revealing = true; + } + if !self.revealing { return None; } + if !self.is_animating( now ) { self.revealing = false; } + let at = self.placed.iter().position( |slot| slot.key.is_some() && slot.key == selected )?; + let x = offsets( &self.placed, self.gap )[ at ]; + Some( offset_into_view( offset, x, self.placed[ at ].width, viewport ) ) + } + fn drag_move( &mut self, x: f32, y: f32, now: Instant ) -> bool { + let ( x, y ) = ( x - self.origin.0, y - self.origin.1 ); if self.drag.is_none() { let hit = self.laid.iter().find( |laid| x >= laid.rect.x && x < laid.rect.x + laid.rect.width && y >= laid.rect.y && y < laid.rect.y + laid.rect.height ); @@ -557,11 +604,11 @@ mod tests let now = Instant::now(); let strip = Rect { x: 0.0, y: 0.0, width: 400.0, height: 30.0 }; motion.arrange( &[ 1, 2, 3 ], 100.0, now ); - motion.lay( strip, 0.0 ); + motion.lay( strip, 0.0, now ); assert!( !motion.drag_move( 50.0, 40.0, now ) ); assert!( motion.drag_move( 50.0, 10.0, now ) ); assert!( motion.drag_move( 260.0, 10.0, now ) ); - let order: Vec = motion.lay( strip, 0.0 ).0.iter().map( |laid| laid.key ).collect(); + let order: Vec = motion.lay( strip, 0.0, now ).0.iter().map( |laid| laid.key ).collect(); assert_eq!( order, [ 2, 3, 1 ] ); assert_eq!( motion.drag_end( now ), Some( ( 0, 2 ) ) ); assert!( motion.arrange( &[ 2, 3, 1 ], 100.0, now ).iter().all( |p| p.width == 100.0 ) ); @@ -589,4 +636,31 @@ mod tests assert_eq!( order, [ 2, 3, 4, 1 ] ); assert_eq!( motion.drag_end( now ), Some( ( 0, 3 ) ) ); } + + #[ test ] + fn the_selected_tab_is_scrolled_into_view_once() + { + let mut motion = Motion::default(); + let now = Instant::now(); + let keys: Vec = ( 1..=10 ).collect(); + motion.arrange( &keys, 100.0, now ); + motion.lay( Rect { x: 0.0, y: 0.0, width: 1000.0, height: 30.0 }, 0.0, now ); + assert_eq!( motion.scroll( Some( 8 ), 0.0, 300.0, 10.0, now ), Some( 500.0 ) ); + assert_eq!( motion.scroll( Some( 8 ), 0.0, 300.0, 10.0, now ), None ); + assert_eq!( motion.scroll( Some( 2 ), 500.0, 300.0, 10.0, now ), Some( 100.0 ) ); + } + + #[ test ] + fn a_tab_dragged_against_the_edge_scrolls_the_strip() + { + let mut motion = Motion::default(); + let now = Instant::now(); + let keys: Vec = ( 1..=10 ).collect(); + motion.arrange( &keys, 100.0, now ); + motion.lay( Rect { x: 0.0, y: 0.0, width: 1000.0, height: 30.0 }, 0.0, now ); + assert!( motion.drag_move( 250.0, 10.0, now ) ); + assert!( motion.drag_move( 290.0, 10.0, now ) ); + motion.lay( Rect { x: 0.0, y: 0.0, width: 1000.0, height: 30.0 }, 0.0, now ); + assert_eq!( motion.scroll( None, 0.0, 300.0, 10.0, now ), Some( 10.0 ) ); + } } diff --git a/src/widget/tab_strip/row.rs b/src/widget/tab_strip/row.rs index d8aac4e..bb8dc6c 100644 --- a/src/widget/tab_strip/row.rs +++ b/src/widget/tab_strip/row.rs @@ -10,14 +10,15 @@ use crate::types::{ Length, Rect }; use crate::widget::Element; use super::motion::{ Placed, TabMotion }; -use super::theme::{ GAP, MAX_TAB_FRACTION, MIN_TAB_WIDTH, RESERVE }; +use super::theme::{ EDGE_SCROLL, GAP, MAX_TAB_FRACTION, MIN_TAB_WIDTH, RESERVE }; pub struct TabRow { /// A horizontal scroll over a [`TabCells`]. - pub( crate ) child: Box>, - pub( crate ) keys: Vec, - pub( crate ) motion: TabMotion, + pub( crate ) child: Box>, + pub( crate ) keys: Vec, + pub( crate ) selected: Option, + pub( crate ) motion: TabMotion, } impl TabRow @@ -43,6 +44,12 @@ impl TabRow Rect { width: room.max( 0.0 ), ..rect } } + /// Where the scroll of the tabs, now at `offset` in a `viewport` that wide, should go; `None` to leave it. + pub( crate ) fn scroll_to( &self, offset: f32, viewport: f32, canvas: &Canvas ) -> Option + { + self.motion.scroll( self.selected, offset, viewport, canvas.resolve_geom( Length::widget( EDGE_SCROLL ) ) ) + } + fn cells( &self ) -> Option<&TabCells> { match self.child.as_ref() @@ -61,7 +68,7 @@ impl TabRow U: Clone + 'static, Msg: 'static, { - TabRow { child: Box::new( self.child.map_arc( f ) ), keys: self.keys, motion: self.motion } + TabRow { child: Box::new( self.child.map_arc( f ) ), keys: self.keys, selected: self.selected, motion: self.motion } } } diff --git a/src/widget/tab_strip/tests.rs b/src/widget/tab_strip/tests.rs index 34a6382..4079fb8 100644 --- a/src/widget/tab_strip/tests.rs +++ b/src/widget/tab_strip/tests.rs @@ -19,7 +19,7 @@ fn strip( count: u64, motion: &TabMotion, after: Vec> ) -> TabRow<() let tabs = ( 1..=count ).map( |key| ( key, spacer().into() ) ).collect::>(); let keys = tabs.iter().map( |( key, _ )| *key ).collect(); let cells = TabCells { tabs, after, motion: motion.clone() }; - TabRow { child: Box::new( scroll( cells ).horizontal().into() ), keys, motion: motion.clone() } + TabRow { child: Box::new( scroll( cells ).horizontal().into() ), keys, selected: None, motion: motion.clone() } } fn cells( row: &TabRow<()> ) -> &TabCells<()> diff --git a/src/widget/tab_strip/theme.rs b/src/widget/tab_strip/theme.rs index 3d8046e..7699fa9 100644 --- a/src/widget/tab_strip/theme.rs +++ b/src/widget/tab_strip/theme.rs @@ -9,6 +9,8 @@ pub( crate ) const RESERVE: f32 = 0.075; pub( crate ) const MAX_TAB_FRACTION: f32 = 0.20; pub( crate ) const MIN_TAB_WIDTH: f32 = 90.0; pub( crate ) const GAP: f32 = 4.0; +/// How far a strip scrolls on each frame a dragged tab is held against its edge. +pub( crate ) const EDGE_SCROLL: f32 = 8.0; pub( crate ) const SLIDE: Duration = Duration::from_millis( 180 ); /// How long the other tabs keep their width after one closes, so the next close button lands under the pointer.