use crate::compose::ComposeResult;
use crate::event::EventCtx;
use crate::widgets::{BindingDecl, Container, Widget};
use super::ScrollView;
use crate::widgets::delegate::{delegate_renderable, delegate_widget_method};
pub struct ScrollableContainer {
inner: ScrollView,
can_focus: bool,
can_focus_children: bool,
can_maximize: Option<bool>,
}
impl ScrollableContainer {
pub fn new() -> Self {
Self {
inner: ScrollView::new(Container::new()),
can_focus: true,
can_focus_children: true,
can_maximize: Some(false),
}
}
pub fn with_child(mut self, child: impl Widget + 'static) -> Self {
self.inner = self.inner.with_child(child);
self
}
pub fn with_compose(mut self, children: ComposeResult) -> Self {
self.inner = self.inner.with_compose(children);
self
}
pub fn push(&mut self, child: impl Widget + 'static) {
self.inner.push(child);
}
pub fn height(mut self, height: usize) -> Self {
self.inner = self.inner.height(height);
self
}
pub fn scroll_step(mut self, step: usize) -> Self {
self.inner = self.inner.scroll_step(step);
self
}
pub fn scroll_step_x(mut self, step: usize) -> Self {
self.inner = self.inner.scroll_step_x(step);
self
}
pub fn set_scroll_step(&mut self, step: usize) {
self.inner.set_scroll_step(step);
}
pub fn set_scroll_step_x(&mut self, step: usize) {
self.inner.set_scroll_step_x(step);
}
pub fn scroll_by(&mut self, delta: i32) {
self.inner.scroll_by(delta);
}
pub fn scroll_by_x(&mut self, delta: i32) {
self.inner.scroll_by_x(delta);
}
pub fn set_virtual_content_size(&self, width: usize, height: usize) {
self.inner.set_virtual_content_size(width, height);
}
pub fn scroll_to(&mut self, offset_y: usize) {
self.inner.scroll_to(offset_y);
}
pub fn scroll_home(&mut self) {
self.inner.scroll_home();
}
pub fn with_can_focus(mut self, can_focus: bool) -> Self {
self.can_focus = can_focus;
self
}
pub fn with_can_focus_children(mut self, can_focus_children: bool) -> Self {
self.can_focus_children = can_focus_children;
self
}
pub fn with_can_maximize(mut self, can_maximize: Option<bool>) -> Self {
self.can_maximize = can_maximize;
self
}
pub fn can_maximize(&self) -> bool {
self.can_maximize.unwrap_or(self.can_focus)
}
}
impl Default for ScrollableContainer {
fn default() -> Self {
Self::new()
}
}
impl Widget for ScrollableContainer {
fn take_composed_children(&mut self) -> Vec<Box<dyn Widget>> {
let extracted = self.inner.take_composed_children();
let mut out = Vec::new();
let mut flattened_container = false;
for mut child in extracted {
let is_scrollbar_lane = matches!(
child.style_id(),
Some(
super::SCROLL_VIEW_VSCROLLBAR_ID
| super::SCROLL_VIEW_HSCROLLBAR_ID
| super::SCROLL_VIEW_SCROLLBAR_CORNER_ID
)
);
if !flattened_container && !is_scrollbar_lane {
let any = &mut *child as &mut dyn std::any::Any;
if let Some(container) = any.downcast_mut::<Container>() {
out.extend(container.take_composed_children());
flattened_container = true;
continue;
}
}
out.push(child);
}
out
}
fn focusable(&self) -> bool {
self.can_focus
}
fn can_focus(&self) -> bool {
self.can_focus
}
fn can_focus_children(&self) -> bool {
self.can_focus_children
}
fn set_virtual_content_size(&mut self, width: usize, height: usize) {
ScrollableContainer::set_virtual_content_size(self, width, height);
}
fn bindings(&self) -> Vec<crate::widgets::BindingDecl> {
let mut bindings = self.inner.bindings();
bindings.push(BindingDecl::new("ctrl+pageup", "page_left", "Page left").hidden());
bindings.push(BindingDecl::new("ctrl+pagedown", "page_right", "Page right").hidden());
bindings
}
fn execute_action(&mut self, action: &crate::action::ParsedAction, ctx: &mut EventCtx) -> bool {
match action.name.as_str() {
"page_left" => {
let before = self.inner.offset_x();
let page = self.inner.layout_height().unwrap_or(1).max(1);
self.inner.scroll_by_x(-(page as i32));
if self.inner.offset_x() != before {
ctx.request_repaint();
}
ctx.set_handled();
true
}
"page_right" => {
let before = self.inner.offset_x();
let page = self.inner.layout_height().unwrap_or(1).max(1);
self.inner.scroll_by_x(page as i32);
if self.inner.offset_x() != before {
ctx.request_repaint();
}
ctx.set_handled();
true
}
_ => self.inner.execute_action(action, ctx),
}
}
delegate_widget_method!(
inner,
[
render,
render_with_debug,
render_line,
render_lines,
compose,
set_focus,
has_focus,
on_mount,
on_unmount,
on_tick,
on_resize,
on_layout,
on_event_capture,
on_event,
on_message,
on_mouse_scroll,
on_mouse_move,
on_app_key,
on_app_action,
on_app_message,
on_app_tick,
on_app_mount,
scroll_offset,
scroll_offset_f32,
scroll_viewport_size,
scroll_virtual_content_size,
clips_descendants_to_content,
child_display_for_tree,
tree_child_content_inset,
layout_height,
content_width,
layout_constraints,
preserve_underlay,
binding_hints,
action_namespace,
action_registry,
styles,
styles_mut,
style_type,
style_type_aliases,
style_id,
style_classes,
set_style_id,
border_title,
border_subtitle,
is_disabled,
set_disabled_state,
is_loading,
set_loading_state,
is_hovered,
set_hovered,
is_active,
mouse_interactive,
tooltip,
tooltip_anchor,
help_markup,
allow_select,
selection_at,
selection_word_range_at,
selection_all_range,
update_selection,
clear_selection,
get_selection,
selection_updated,
reactive_widget,
]
);
}
delegate_renderable!(ScrollableContainer);
#[cfg(test)]
mod tests {
use super::*;
use crate::message::{Message, MessageEvent, ScrollbarAxis, ScrollbarScrollTo};
use crate::prelude::Label;
#[test]
fn scrollable_container_defaults_match_python_policies() {
let sc = ScrollableContainer::new();
assert!(sc.focusable());
assert!(sc.can_focus_children());
assert!(!sc.can_maximize());
}
#[test]
fn scrollable_container_forwards_scroll_offset() {
let mut sc = ScrollableContainer::new().with_child(Label::new("a"));
let _ = sc.take_composed_children();
assert_eq!(sc.scroll_offset(), (0, 0));
assert!(sc.clips_descendants_to_content());
}
#[test]
fn scrollable_container_forwards_scrollbar_messages_to_inner_scrollview() {
let mut sc = ScrollableContainer::new().with_child(Label::new("line\n".repeat(20)));
sc.set_virtual_content_size(20, 100);
let mut ctx = EventCtx::default();
sc.on_message(
&MessageEvent {
sender: crate::node_id::NodeId::default(),
message: Message::ScrollbarScrollTo(ScrollbarScrollTo {
axis: ScrollbarAxis::Vertical,
offset: 6.0,
animate: false,
scroll_duration: None,
}),
control: None,
},
&mut ctx,
);
assert_eq!(sc.scroll_offset().1, 6);
assert!(
ctx.handled(),
"message should be handled by inner ScrollView"
);
}
}