ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

gpui-kit 滚动条 Scrollbar 深度指南:为 GPUI 滚动视图定制带动效的自绘滚动条

gpui-kit 滚动条 Scrollbar 深度指南:为 GPUI 滚动视图定制带动效的自绘滚动条 gpui-kit 滚动条 Scrollbar 深度指南为 GPUI 滚动视图定制带动效的自绘滚动条【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kitScrollbar是 gpui-kit 的gpui-base基础库中提供的一个自绘custom-painted滚动条组件它连接 GPUI 的滚动句柄scroll handle为滚动区域、列表以及自定义视口补充可交互、可定制、可动效化的滚动条体验。它支持垂直、水平与双轴视口、轨道点击跳转、滑块拖拽、可配置的显示模式、类型化画笔样式、减少动态效果reduced motion偏好以及可反向的可见性与宽度过渡。读完本文你将掌握在 GPUI 应用中接入Scrollbar、配置全局主题与单实例样式、接入自定义滚动容器以及理解其交互与过渡生命周期的完整方法。架构分工Base 拥有生命周期应用层拥有表现gpui-basecrates/base拥有Scrollbar的交互与过渡生命周期可见性状态机、轨道点击、滑块拖拽、滚动监听、idle 计时与动画采样都由 Base 内部实现。而颜色、几何、时序与入场编排entrance choreography属于你的应用层或设计系统层——通过全局ScrollbarTheme或单实例.styles(...)注入。Base 自身不携带任何产品动效ScrollbarMotion::default()的enter、exit、expand时长全部为零只有 2 秒的行为性 idle 保持未安装动效的应用会得到即时显现与即时变宽的滚动条。该分工在源码中体现得非常清晰ScrollbarMotion的文档注释明确写道Base installs no motion of its own: every transition duration defaults to zero而ScrollbarEntrance的注释则是The styled layer chooses the choreography; Base only plays it见 crates/base/src/scrollbar.rs。Base 只提供机制mechanism产品节奏timing与入场风格属于你的设计系统。运行示例原生 showcase 与 WASM 预览使用同一套实现可直接运行cargo run -p gpui-base-examples -- scrollbar完整的可运行示例源码位于 crates/base/examples/showcase/components/scrollbar.rs它渲染一个 20 行的活动列表并叠加一个ScrollbarMode::Always模式的双轴滚动条impl BaseShowcase { pub(in super::super) fn scrollbar(self) - impl IntoElement { div() .id(example-scroll-region) .relative() .w_72() .h_48() .text_xs() .border_1() .border_color(super::example_rgb(0x171717)) .overflow_scroll() .track_scroll(self.example_scroll) .child(div().children((1..20).map(|row| { div() .h_7() .px_2() .flex() .items_center() .border_b_1() .border_color(super::example_rgb(0xe5e7eb)) .justify_between() .child(format!(Activity {row})) .child(if row % 3 0 { Completed } else { Pending }) }))) .child(Scrollbar::new(self.example_scroll).mode(ScrollbarMode::Always)) } }导入与基础用法在组件中使用Scrollbar需要引入以下符号示例见 website/base/primitives/scrollbar.mduse std::time::Duration; use gpui_kit::{div, px, rgb, ScrollHandle, Styled as _}; use gpui_kit::base::{ Scrollbar, ScrollbarAxis, ScrollbarEntrance, ScrollbarMode, ScrollbarMotion, ScrollbarStyles, ScrollbarTheme, Theme, };核心使用模式有三步把ScrollHandle保存在持久的视图状态上例如结构体字段保证跨渲染保持稳定用track_scroll(self.scroll_handle)把句柄挂到可滚动内容上在同一相对容器内叠加一个Scrollbar。pub struct ActivityList { scroll_handle: ScrollHandle, } impl ActivityList { pub fn new() - Self { Self { scroll_handle: ScrollHandle::new(), } } fn render_list(self) - impl gpui_kit::IntoElement { div() .relative() .size_full() .overflow_scroll() .track_scroll(self.scroll_handle) .child(div().children((1..100).map(|row| { div().h_8().px_2().child(format!(Activity {row})) }))) .child(Scrollbar::new(self.scroll_handle)) } }Scrollbar::new默认启用双轴ScrollbarAxis::Both。当容器只在一个方向上滚动时应使用轴专用构造器或显式指定轴Scrollbar::vertical(scroll_handle); Scrollbar::horizontal(scroll_handle); Scrollbar::new(scroll_handle).axis(ScrollbarAxis::Vertical);在源码中Scrollbar::vertical与Scrollbar::horizontal只是Scrollbar::new加上.axis(...)的语法糖见 crates/base/src/scrollbar.rs。滚动条是绝对定位覆盖层。其布局与命中盒hitbox在布局阶段固定不动只有绘制的轨道和滑块在动画——因此入场动效不会移动内容也不会改变交互几何。这一点在request_layout中实现滚动条元素使用position: Absolute、flex_grow: 1、宽高均为relative(1.)填满容器见 crates/base/src/scrollbar.rs。可见性模式Visibility Modes可以给单个滚动条设置模式也可以省略.mode(...)以使用全局ScrollbarTheme中的模式Scrollbar::vertical(scroll_handle).mode(ScrollbarMode::Scrolling); Scrollbar::vertical(scroll_handle).mode(ScrollbarMode::Hover); Scrollbar::vertical(scroll_handle).mode(ScrollbarMode::Always);模式行为Scrolling滚动或拖拽后出现。已可见的滚动条在悬停期间保持可见离开后开始一次全新的 idle 保持。悬停无法唤出完全隐藏的滚动条。Hover指针进入滚动条轨道时出现。Always保持可见并跳过可见性过渡。三种模式对应的判断逻辑集中在wants_visible、tracks_thumb_hover与hover_keeps_visible三个纯函数中见 crates/base/src/scrollbar.rs并有对应的单元测试覆盖如hidden_scrolling_mode_does_not_track_thumb_hover、visible_scrolling_mode_stays_visible_while_hovered。Scrolling模式下隐藏的滑块不会保留潜在悬停状态因此不会在下次滚动时意外膨胀。宽度约定所有模式默认使用 6 px 的静息滑块宽度轨道悬停保持该宽度滑块悬停与激活拖拽瞄准 8 px 的活动宽度。宽度变化使用配置的expand时长。这些默认值来自源码常量THUMB_WIDTH px(6.)、THUMB_ACTIVE_WIDTH px(8.)、THUMB_INSET px(4.)而滚动条整体轨道宽度WIDTH THUMB_ACTIVE_INSET * 2 THUMB_ACTIVE_WIDTH px(16.)见 crates/base/src/scrollbar.rs。测试every_mode_expands_only_for_thumb_hover验证了三种模式都是正常/轨道悬停 6 px、滑块悬停 8 px的扩张规则。隐藏状态下忽略交互隐藏的轨道与滑块点击被忽略。prepaint阶段只有当滚动条可见is_visible时才注册MouseDownEvent处理见 crates/base/src/scrollbar.rs测试hidden_hover_scrollbar_ignores_track_click与hidden_hover_scrollbar_ignores_thumb_drag分别验证了这两种情况。配置全局主题ScrollbarTheme使用私有字段搭配消费型构建器consuming builder与读取器。在应用初始化时或设计系统主题切换时设置它fn install_scrollbar_theme(cx: mut gpui_kit::App) { let styles ScrollbarStyles::default() .track(|style| { style .width(px(16.)) .bg(rgb(0x000000).alpha(0.08)) }) .track_hover(|style| { style.bg(rgb(0x000000).alpha(0.12)) }) .track_active(|style| { style.bg(rgb(0x000000).alpha(0.16)) }) .thumb(|style| { style .width(px(6.)) .inset(px(4.)) .radius(px(3.)) .min_length(px(48.)) .bg(rgb(0x737373)) }) .thumb_hover(|style| { style.width(px(8.)).bg(rgb(0x525252)) }) .thumb_active(|style| { style.width(px(8.)).bg(rgb(0x404040)) }); let motion ScrollbarMotion::default() .with_idle(Duration::from_secs(2)) .with_enter(Duration::from_millis(300)) .with_exit(Duration::from_millis(500)) .with_expand(Duration::from_millis(300)) .with_entrance(ScrollbarEntrance::Fade) .with_thumb_hover_entrance(ScrollbarEntrance::SlideAndFade); Theme::global_mut(cx).scrollbar ScrollbarTheme::new() .with_mode(ScrollbarMode::Scrolling) .with_motion(motion) .with_styles(styles); }同样的值可以在不暴露主题字段的情况下读取let scrollbar Theme::global(cx).scrollbar; let mode scrollbar.mode(); let motion scrollbar.motion(); let styles scrollbar.styles();ScrollbarTheme定义在 crates/base/src/theme.rs它把mode、motion、styles三个维度打包为一个可整体换入的全局默认值并通过Theme::global_mut(cx).scrollbar ...挂到全局主题上。运动令牌ScrollbarMotionScrollbarMotion是滚动条的全部时序与入场配置字段与默认值如下见 crates/base/src/scrollbar.rs构建器方法含义默认值with_idle(Duration)最后一次滚动/拖拽/悬停后保持可见的时长2s行为性保持DEFAULT_IDLEwith_enter(Duration)变为完全可见所需时长Duration::ZEROwith_exit(Duration)idle 到期后淡出所需时长Duration::ZEROwith_expand(Duration)滑块到达新宽度所需时长Duration::ZEROwith_entrance(ScrollbarEntrance)整体入场编排Fadewith_thumb_hover_entrance(ScrollbarEntrance)滑块悬停唤起时的入场编排FadeBase 不安装任何产品动效ScrollbarMotion::default()使用 2 秒的行为性 idle 保持但enter、exit、expand都是零时长。未安装动效的应用因此获得即时的可见性与宽度变化。单元测试base_ships_no_motion_of_its_own专门锁定这一契约并注释说明 idle 是行为而非动效必须保留以保证Scrolling模式可用。动效行为上文示例主题会产出如下编排触发条件入场方式在Scrolling或Hover模式下滚动entrance原地淡入Hover模式下轨道悬停entrance原地淡入Hover模式下滑块悬停thumb_hover_entrance从最近边缘滑入并淡出Always模式立即显示跳过可见性动效entrance_for的逻辑是仅当模式为Hover且滑块被悬停时才使用thumb_hover_entrance否则一律使用entrance见 crates/base/src/scrollbar.rs并有测试hover_mode_slides_only_when_the_thumb_is_hovered验证。SlideAndFade 的方向垂直滚动条从右侧进入水平滚动条从底部进入。这由visibility_translation实现——垂直轴沿 x 方向平移轨道宽度水平轴沿 y 方向平移见 crates/base/src/scrollbar.rs。缓动曲线透明度在入场时使用线性进度在退出时使用ease_in_cubic位置在入场时使用ease_out_cubic退出时使用ease_in_cubic见VisibilityAnimation::samplecrates/base/src/scrollbar.rs。测试entrance_fades_linearly_while_position_eases_out验证了半程时透明度恰为 0.5 而位置大于 0.5的 ease-out 特征fade_entrance_snaps_position_and_animates_opacity则验证 Fade 入场位置立即到位、仅透明度动画。中断与方向反转被打断的过渡会先采样当前的透明度与位置再改变方向且过渡时长按剩余距离缩放保证速度不突变见set_visiblecrates/base/src/scrollbar.rs。测试visibility_animation_reverses_from_current_progress与idle_boundary_starts_the_exit_without_a_jump覆盖了这两种场景。零时长即到目标零时长直接采用目标值即使有过渡正在进行也一样ScalarTransition::settle与set_visible中的full_duration.is_zero()分支。测试a_zero_duration_settles_a_transition_already_in_flight模拟入场中途开启 reduced motion的切换场景。减少动态效果GPUI 的reduce_motion偏好会把可见性与宽度时长都置零因此无需单独的 reduced-motion 主题。在prepaint中let reduce_motion cx.reduce_motion(); let (enter, exit) if !mode.is_always() !reduce_motion { (motion.enter(), motion.exit()) } else { (Duration::ZERO, Duration::ZERO) }; let expand if reduce_motion { Duration::ZERO } else { motion.expand() };见 crates/base/src/scrollbar.rs。也就是说Always模式跳过可见性动效但保留宽度动画reduced motion 则把所有通道都变为立即生效。测试motionless_base_snaps_every_transition与reduced_motion_snaps_thumb_expansion分别锁定了这两种行为。单实例样式覆盖使用.styles(...)覆盖单个滚动条的全局样式。实例样式优先于主题默认值Scrollbar::vertical(scroll_handle).styles(|styles| { styles .track(|style| style.width(px(14.)).bg(rgb(0xf5f5f5))) .track_hover(|style| style.bg(rgb(0xe5e5e5))) .thumb(|style| { style .width(px(6.)) .inset(px(3.)) .radius(px(3.)) .min_length(px(40.)) .bg(rgb(0x737373)) }) .thumb_hover(|style| style.width(px(8.)).bg(rgb(0x525252))) .thumb_active(|style| style.width(px(8.)).bg(rgb(0x404040))) })两类样式结构体支持以下字段定义见 crates/base/src/scrollbar.rs样式结构支持字段说明ScrollbarTrackStylebg、border_color、width轨道背景、边框色与轨道宽度ScrollbarThumbStylebg、width、inset、radius、min_length滑块背景、宽度、内缩、圆角与最小长度样式级联顺序见resolve_track/resolve_thumbcrates/base/src/scrollbar.rs从高到低为当前状态样式active / hovered 等状态专用实例.styles(...)中对应状态的值全局ScrollbarTheme样式由主题派生或内置的默认值MIN_THUMB_SIZE px(48.)兜底min_length。主题派生的滑块底色未显式覆盖时滑块默认色取自当前主题的tokens.colors.foreground并按状态施加透明度normal 0.35、hover/active 0.55而不是写死的黑色——这样浅色/深色主题切换时滑块始终可见见thumb_default_background与style_for_normalcrates/base/src/scrollbar.rs。测试unstyled_thumb_follows_the_theme_rather_than_a_fixed_colour专门验证了浅色主题上foreground近乎黑色、深色主题上近乎白色的跟随行为a_styled_thumb_still_beats_the_theme_derived_default则确认显式样式仍然压过主题派生值。自定义视口几何视口viewport默认来自ScrollbarHandle::viewport_bounds。两个覆盖手段支持复合控件或自绘控件Scrollbar::vertical(scroll_handle) .viewport_bounds(editor_content_bounds); Scrollbar::vertical(scroll_handle) .viewport_from_layout();使用viewport_bounds当你的自绘视口与句柄的布局边界不一致时例如文本编辑器仅需高亮实际可见区域源码注释明确提到 custom-painted viewports, such as the text editor使用viewport_from_layout当定位的覆盖容器本身就精确代表视口时例如固定表头下方的表格主体。此时滚动条直接采用自身元素的布局边界。视口解析优先级是viewport_bounds覆盖 viewport_from_layout 句柄上报见resolved_viewport_boundscrates/base/src/scrollbar.rs。测试explicit_viewport_bounds_override_handle_bounds与layout_viewport_uses_current_element_bounds覆盖了这两种路径。覆盖内容尺寸仅当句柄无法报告完整可滚动范围时才需要Scrollbar::vertical(scroll_handle) .scroll_size(gpui_kit::size(px(800.), px(4_000.)));scroll_size默认取scroll_handle.content_size()见prepaint中的self.scroll_size.unwrap_or(self.scroll_handle.content_size())crates/base/src/scrollbar.rs。无溢出即隐藏当滚动内容尺寸小于等于容器尺寸时该轴直接跳过绘制与交互if scroll_area_size container_size { ... continue; }crates/base/src/scrollbar.rs。测试no_overflow_has_no_interactive_track验证了无溢出时点击轨道不会产生任何滚动偏移。双轴避让双轴模式下水平滚动条会自动为垂直滚动条让出margin_end track_width避免两者重叠若垂直条因无溢出被隐藏水平条则占满全宽has_both标记会在跳过时置回falsecrates/base/src/scrollbar.rs。自定义滚动句柄ScrollHandle、UniformListScrollHandle与ListState均已实现ScrollbarHandletrait见 crates/base/src/scrollbar.rs。自定义滚动容器可以实现同一个 trait 接入滚动条use gpui_kit::{Bounds, Pixels, Point, Size}; use gpui_kit::base::ScrollbarHandle; impl ScrollbarHandle for MyScrollState { fn viewport_bounds(self) - BoundsPixels { self.viewport_bounds() } fn offset(self) - PointPixels { self.offset() } fn set_offset(self, offset: PointPixels) { self.set_offset(offset); } fn content_size(self) - SizePixels { self.content_size() } fn start_drag(self) { self.set_scrollbar_dragging(true); } fn end_drag(self) { self.set_scrollbar_dragging(false); } }ScrollbarHandle的完整契约crates/base/src/scrollbar.rs方法必选说明viewport_bounds() - BoundsPixels是滚动条覆盖的视口边界offset() - PointPixels是当前滚动偏移set_offset(offset)是设置滚动偏移content_size() - SizePixels是内容完整尺寸含 paddingstart_drag()否开始拖拽滑块时回调end_drag()否结束拖拽滑块时回调start_drag与end_drag是可选的。当滚动容器需要在滑块拖拽期间挂起吸附snapping、选区或其他行为时使用它们。只有实际被拖拽的轴会在鼠标抬起时收到end_drag——松开鼠标时paint中的MouseUpEvent处理器会检查state.get().dragged_axis Some(axis)再调用end_dragcrates/base/src/scrollbar.rs。ListState的实现把这两个回调接到scrollbar_drag_started/scrollbar_drag_ended测试thumb_drag_notifies_handle_start_and_end验证了拖拽全程的 start/end 配对。稳定身份Stable IdentityScrollbar::new、vertical、horizontal会从其调用位置Location::caller()派生元素 ID见 crates/base/src/scrollbar.rs。当同一个调用点产生多个相互独立的滚动条时需要显式设置稳定 IDScrollbar::vertical(scroll_handle).id((activity-list, panel_id));稳定身份会在多次渲染之间保留可见性与宽度动画的续存状态retained state避免每次重绘都重置动画进度。交互实现细节轨道点击、滑块拖拽与帧率限制滚动条的交互逻辑全部在paint阶段注册crates/base/src/scrollbar.rs轨道点击跳转点击轨道空白处时滚动条以点击位置为滑块中心换算滚动百分比并 clamp 到合法范围后调用set_offset实现点击轨道跳转滑块拖拽鼠标按下命中滑块时调用start_drag并记录按下点与滑块的相对偏移拖拽移动时按比例换算偏移并set_offset同时stop_propagation避免触发文本选择等副作用帧率限制拖拽更新默认限制为120 FPSmax_fps: usize可用.max_fps(...)调整并被 clamp 在 30..120。在每次更新前检查距上次更新的间隔是否超过1000 / max_fps毫秒用于降低复杂交互场景下的 CPU 占用见 crates/base/src/scrollbar.rs。该 API 标注为#[doc(hidden)]属于高级调优项完整的轨道命中区即使绘制的滑块很窄完整轨道命中盒始终可交互bar_hitbox覆盖整个轨道区域保证窄滑块也容易点中。可访问性与交互检查清单在Scrolling/Hover/Always三种模式下接入滚动条后请对照以下清单验收原文见 website/base/primitives/scrollbar.md保持底层视口的滚轮、触控板与键盘滚动始终可用即使绘制的滑块很窄也要保留完整轨道的默认交互命中区滑块在 normal、hover、active 三种状态下都要有足够的对比度不要通过移动布局或命中盒来实现入场动画滚动条的布局与命中盒始终固定分别在开启与关闭 reduced motion 的情况下测试Scrolling、Hover、Always三种模式独立测试垂直、水平与双轴溢出场景。此外从源码契约看还有几条值得注意的默认行为内容不溢出时该轴滚动条自动隐藏且不可交互Scrolling模式下悬停无法唤出完全隐藏的滚动条但已可见的滚动条在悬停期间会续存可见性双轴模式自动为垂直条让位。理解这些默认行为有助于在复杂布局如表格固定表头 主体滚动中正确选用viewport_bounds/viewport_from_layout覆盖项。【免费下载链接】gpui-kitRust GUI components for building fantastic cross-platform desktop application by using GPUI.项目地址: https://gitcode.com/GitHub_Trending/gp/gpui-kit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表