use std::{collections::HashMap, marker::PhantomData, ops::Range};
use masonry::core::{Widget, WidgetPod};
use masonry::util::debug_panic;
use masonry::widgets::{self, VirtualScrollAction};
use private::VirtualScrollState;
use crate::core::{MessageContext, MessageResult, Mut, View, ViewId, ViewMarker, ViewPathTracker};
use crate::{Pod, ViewCtx, WidgetView};
pub struct VirtualScroll<State, Action, ChildrenViews, F> {
phantom: PhantomData<fn() -> (WidgetPod<dyn Widget>, State, Action, ChildrenViews)>,
func: F,
valid_range: Range<i64>,
}
pub fn virtual_scroll<State, Action, ChildrenViews, F>(
valid_range: Range<i64>,
func: F,
) -> VirtualScroll<State, Action, ChildrenViews, F>
where
ChildrenViews: WidgetView<State, Action>,
F: Fn(&mut State, i64) -> ChildrenViews + 'static,
{
VirtualScroll {
phantom: PhantomData,
func,
valid_range,
}
}
pub fn unlimited_virtual_scroll<State, Action, ChildrenViews, F>(
func: F,
) -> VirtualScroll<State, Action, ChildrenViews, F>
where
ChildrenViews: WidgetView<State, Action>,
F: Fn(&mut State, i64) -> ChildrenViews + 'static,
{
VirtualScroll {
phantom: PhantomData,
func,
valid_range: i64::MIN..i64::MAX,
}
}
mod private {
use masonry::widgets::VirtualScrollAction;
use std::collections::HashMap;
#[expect(
unnameable_types,
reason = "Not meaningful public API; required to be public due to design of View trait"
)]
pub struct VirtualScrollState<View, State> {
pub(super) pending_action: Option<VirtualScrollAction>,
pub(super) children: HashMap<i64, ChildState<View, State>>,
}
pub(super) struct ChildState<View, State> {
pub(super) view: View,
pub(super) state: State,
}
}
const fn view_id_for_index(idx: i64) -> ViewId {
ViewId::new(idx.cast_unsigned())
}
const fn index_for_view_id(id: ViewId) -> i64 {
id.routing_id().cast_signed()
}
impl<State, Action, ChildrenViews, F> ViewMarker
for VirtualScroll<State, Action, ChildrenViews, F>
{
}
impl<State, Action, ChildrenViews, F> View<State, Action, ViewCtx>
for VirtualScroll<State, Action, ChildrenViews, F>
where
State: 'static,
Action: 'static,
ChildrenViews: WidgetView<State, Action>,
F: Fn(&mut State, i64) -> ChildrenViews + 'static,
{
type Element = Pod<widgets::VirtualScroll>;
type ViewState = VirtualScrollState<ChildrenViews, ChildrenViews::ViewState>;
fn build(&self, ctx: &mut ViewCtx, _: &mut State) -> (Self::Element, Self::ViewState) {
let pod =
Pod::new(widgets::VirtualScroll::new(0).with_valid_range(self.valid_range.clone()));
ctx.record_action(pod.new_widget.id());
(
pod,
VirtualScrollState {
pending_action: None,
children: HashMap::default(),
},
)
}
fn rebuild(
&self,
prev: &Self,
view_state: &mut Self::ViewState,
ctx: &mut ViewCtx,
mut element: Mut<'_, Self::Element>,
app_state: &mut State,
) {
let valid_range_changed = self.valid_range != prev.valid_range;
if valid_range_changed {
widgets::VirtualScroll::set_valid_range(&mut element, self.valid_range.clone());
}
if let Some(pending_action) = view_state.pending_action.take() {
widgets::VirtualScroll::will_handle_action(&mut element, &pending_action);
for idx in pending_action.old_active.clone() {
if !pending_action.target.contains(&idx) {
let Some(mut child_state) = view_state.children.remove(&idx) else {
debug_panic!(
"Tried to remove {idx} from virtual scroll {pending_action:?}, but it wasn't already present."
);
continue;
};
ctx.with_id(view_id_for_index(idx), |ctx| {
child_state.view.teardown(
&mut child_state.state,
ctx,
widgets::VirtualScroll::child_mut(&mut element, idx).downcast(),
);
widgets::VirtualScroll::remove_child(&mut element, idx);
});
}
}
for idx in pending_action.target.clone() {
if let Some(child) = view_state.children.get_mut(&idx) {
debug_assert!(
pending_action.old_active.contains(&idx),
"{idx} was asked to be removed in {pending_action:?}, but wasn't already present."
);
let next_child = (self.func)(app_state, idx);
ctx.with_id(view_id_for_index(idx), |ctx| {
next_child.rebuild(
&child.view,
&mut child.state,
ctx,
widgets::VirtualScroll::child_mut(&mut element, idx).downcast(),
app_state,
);
child.view = next_child;
});
} else {
let new_child = (self.func)(app_state, idx);
ctx.with_id(view_id_for_index(idx), |ctx| {
let (new_element, child_state) = new_child.build(ctx, app_state);
widgets::VirtualScroll::add_child(
&mut element,
idx,
new_element.new_widget.erased(),
);
view_state.children.insert(
idx,
private::ChildState {
view: new_child,
state: child_state,
},
)
});
}
}
} else {
for (&idx, child) in &mut view_state.children {
let next_child = (self.func)(app_state, idx);
ctx.with_id(view_id_for_index(idx), |ctx| {
next_child.rebuild(
&child.view,
&mut child.state,
ctx,
widgets::VirtualScroll::child_mut(&mut element, idx).downcast(),
app_state,
);
child.view = next_child;
});
}
}
debug_assert_eq!(
element.widget.len(),
view_state.children.len(),
"VirtualScroll: Child added outside of the control of Xilem."
);
}
fn teardown(
&self,
view_state: &mut Self::ViewState,
ctx: &mut ViewCtx,
mut element: Mut<'_, Self::Element>,
) {
for (&idx, child) in &mut view_state.children {
ctx.with_id(view_id_for_index(idx), |ctx| {
child.view.teardown(
&mut child.state,
ctx,
widgets::VirtualScroll::child_mut(&mut element, idx).downcast(),
);
});
}
ctx.teardown_leaf(element);
}
fn message(
&self,
view_state: &mut Self::ViewState,
message: &mut MessageContext,
mut element: Mut<'_, Self::Element>,
app_state: &mut State,
) -> MessageResult<Action> {
if let Some(first) = message.take_first() {
let child_idx = index_for_view_id(first);
let target = view_state.children.get_mut(&child_idx);
if let Some(target) = target {
let result = target.view.message(
&mut target.state,
message,
widgets::VirtualScroll::child_mut(&mut element, child_idx).downcast(),
app_state,
);
return result;
} else {
tracing::error!(
"Message sent to unloaded view in `VirtualScroll::message`: {message:?}"
);
return MessageResult::Stale;
}
}
if let Some(action) = message.take_message::<VirtualScrollAction>() {
view_state.pending_action = Some(*action);
MessageResult::RequestRebuild
} else {
tracing::error!(?message, "Wrong message type in VirtualScroll::message");
MessageResult::Stale
}
}
}