use alloc::boxed::Box;
use core::ops::RangeBounds;
use nami::collection::Collection;
use nami::watcher::Context;
use nami::{Computed, signal::IntoComputed};
use crate::views::{AnyViews, ForEach, SharedAnyViews, Views, ViewsExt};
use waterui_core::id::SelfId;
use waterui_core::view::{ConfigurableView, Hook, ViewConfiguration};
use waterui_core::{
AnyView, Environment, Metadata, Native, NativeView, View,
handler::{AnyViewBuilder, Handler, shared_action},
id::Identifiable,
impl_extractor,
layout::StretchAxis,
};
use waterui_layout::scroll::ScrollController;
use waterui_text::{IntoText, Text};
mod content;
mod section;
pub use content::{ListContent, ListItemSink, Row, RowLayout, detail_row, row};
pub use section::Section;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Move {
from: usize,
to: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ListDelete(pub usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ListMove(pub Move);
impl_extractor!(ListDelete);
impl_extractor!(ListMove);
impl Move {
#[must_use]
pub const fn new(from: usize, to: usize) -> Self {
Self { from, to }
}
#[must_use]
pub const fn from(self) -> usize {
self.from
}
#[must_use]
pub const fn to(self) -> usize {
self.to
}
}
pub type OnDelete = Box<dyn Fn(&Environment, usize)>;
pub type OnMove = Box<dyn Fn(&Environment, Move)>;
pub struct ListConfig {
pub contents: SharedAnyViews<ListItem>,
pub editing: Computed<bool>,
pub on_delete: Option<OnDelete>,
pub on_move: Option<OnMove>,
pub scroll_controller: Option<ScrollController<usize>>,
pub uses_sections: bool,
}
impl_debug!(ListConfig);
impl NativeView for ListConfig {
fn stretch_axis(&self) -> StretchAxis {
StretchAxis::Both
}
}
#[derive(Debug)]
pub struct List<V: Views<View = ListItem> = AnyViews<ListItem>> {
contents: V,
uses_sections: bool,
}
impl<V> List<V>
where
V: Views<View = ListItem>,
{
pub const fn new(contents: V) -> Self {
Self {
contents,
uses_sections: true,
}
}
#[must_use]
pub fn editing(self, editing: impl IntoComputed<bool>) -> ListBuilder<V> {
ListBuilder {
contents: self.contents,
editing: editing.into_computed(),
on_delete: None,
on_move: None,
scroll_controller: None,
uses_sections: self.uses_sections,
}
}
#[must_use]
pub fn on_delete<H, Args>(self, on_delete: H) -> ListBuilder<V>
where
H: Handler<Args, ()>,
{
ListBuilder {
contents: self.contents,
editing: Computed::new(false),
on_delete: Some(list_delete_action(on_delete)),
on_move: None,
scroll_controller: None,
uses_sections: self.uses_sections,
}
}
#[must_use]
pub fn on_move<H, Args>(self, on_move: H) -> ListBuilder<V>
where
H: Handler<Args, ()>,
{
ListBuilder {
contents: self.contents,
editing: Computed::new(false),
on_delete: None,
on_move: Some(list_move_action(on_move)),
scroll_controller: None,
uses_sections: self.uses_sections,
}
}
#[must_use]
pub fn scroll_controller(self, controller: &ScrollController<usize>) -> ListBuilder<V> {
ListBuilder {
contents: self.contents,
editing: Computed::new(false),
on_delete: None,
on_move: None,
scroll_controller: Some(controller.clone()),
uses_sections: self.uses_sections,
}
}
}
impl<C, F> List<ForEach<C, F, ListItem>>
where
C: Collection + Clone,
C::Item: Identifiable,
F: 'static + Fn(C::Item) -> ListItem,
{
pub const fn for_each(data: C, generator: F) -> Self {
Self {
contents: ForEach::new(data, generator),
uses_sections: false,
}
}
}
impl List<BuiltViews> {
#[must_use]
pub fn content(content: impl ListContent) -> Self {
let mut sink = ListItemSink::new();
content.collect_items(&mut sink);
Self {
contents: BuiltViews::new(sink),
uses_sections: true,
}
}
}
pub struct BuiltViews {
entries: alloc::vec::Vec<(AnyViewBuilder<ListItem>, Option<ListSection>)>,
}
impl BuiltViews {
pub(crate) fn new(sink: ListItemSink) -> Self {
Self {
entries: sink.into_entries(),
}
}
}
impl core::fmt::Debug for BuiltViews {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("BuiltViews")
.field("len", &self.entries.len())
.finish()
}
}
impl Views for BuiltViews {
type Id = SelfId<usize>;
type Guard = ();
type View = ListItem;
fn len(&self) -> Computed<usize> {
Computed::constant(self.entries.len())
}
fn get_id(&self, index: usize) -> Option<Self::Id> {
(index < self.entries.len()).then(|| SelfId::new(index))
}
fn get_view(&self, index: usize) -> Option<Self::View> {
let (builder, section) = self.entries.get(index)?;
let mut item = builder.build();
item.section = section;
Some(item)
}
fn watch(
&self,
_range: impl RangeBounds<usize>,
_watcher: impl for<'a> Fn(Context<&'a [Self::Id]>) + 'static,
) -> Self::Guard {
}
}
impl<V> ConfigurableView for List<V>
where
V: Views<View = ListItem> + 'static,
{
type Config = ListConfig;
fn config(self) -> Self::Config {
ListConfig {
contents: SharedAnyViews::new(self.contents),
editing: Computed::new(false),
on_delete: None,
on_move: None,
scroll_controller: None,
uses_sections: self.uses_sections,
}
}
}
impl ViewConfiguration for ListConfig {
type View = List<SharedAnyViews<ListItem>>;
fn render(self) -> Self::View {
List {
contents: self.contents,
uses_sections: self.uses_sections,
}
}
}
impl From<ListConfig> for List<SharedAnyViews<ListItem>> {
fn from(value: ListConfig) -> Self {
value.render()
}
}
fn render_list_config(mut config: ListConfig, env: &Environment) -> impl View {
let section_env = env.clone();
config.contents = SharedAnyViews::new(
config
.contents
.clone()
.map(move |item| selection_themed(resolve_item_section(item, §ion_env))),
);
if let Some(hook) = env.get::<Hook<ListConfig>>() {
AnyView::new(hook.apply(env, config))
} else {
let fallback =
crate::component::lazy::Lazy::vstack(config.contents.clone().map(|item| item.content));
AnyView::new(Native::new(config).with_fallback(fallback))
}
}
fn resolve_item_section(mut item: ListItem, env: &Environment) -> ListItem {
item.section = item.section.map(|section| section.resolved(env));
item
}
fn selection_themed(mut item: ListItem) -> ListItem {
use crate::color::ResolvedColor;
use crate::theme::{color, install_color_signal};
use nami::SignalExt;
use waterui_core::env::use_env;
use waterui_core::resolve::Resolvable;
let selected = item.selected.clone();
let content = core::mem::take(&mut item.content);
item.content = AnyView::new(use_env(move |mut env: Environment| {
let on_selection = color::SelectionForeground.resolve(&env).computed();
let flip = |normal: Computed<ResolvedColor>| {
selected
.clone()
.zip(&normal)
.zip(&on_selection)
.map(|((selected, normal), selection)| if selected { selection } else { normal })
.computed()
};
let foreground = flip(color::Foreground.resolve(&env).computed());
let muted = flip(color::MutedForeground.resolve(&env).computed());
let accent = flip(color::Accent.resolve(&env).computed());
install_color_signal::<color::Foreground>(&mut env, foreground);
install_color_signal::<color::MutedForeground>(&mut env, muted);
install_color_signal::<color::Accent>(&mut env, accent);
Metadata::new(content, env)
}));
item
}
impl<V> View for List<V>
where
V: Views<View = ListItem> + 'static,
{
fn body(self, env: &Environment) -> impl View {
render_list_config(ConfigurableView::config(self), env)
}
}
pub struct ListBuilder<V: Views<View = ListItem>> {
contents: V,
editing: Computed<bool>,
on_delete: Option<OnDelete>,
on_move: Option<OnMove>,
scroll_controller: Option<ScrollController<usize>>,
uses_sections: bool,
}
impl<V: Views<View = ListItem>> core::fmt::Debug for ListBuilder<V> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("ListBuilder")
}
}
impl<V> ListBuilder<V>
where
V: Views<View = ListItem>,
{
#[must_use]
pub fn editing(mut self, editing: impl IntoComputed<bool>) -> Self {
self.editing = editing.into_computed();
self
}
#[must_use]
pub fn on_delete<H, Args>(mut self, on_delete: H) -> Self
where
H: Handler<Args, ()>,
{
self.on_delete = Some(list_delete_action(on_delete));
self
}
#[must_use]
pub fn on_move<H, Args>(mut self, on_move: H) -> Self
where
H: Handler<Args, ()>,
{
self.on_move = Some(list_move_action(on_move));
self
}
#[must_use]
pub fn scroll_controller(mut self, controller: &ScrollController<usize>) -> Self {
self.scroll_controller = Some(controller.clone());
self
}
}
impl<V> ConfigurableView for ListBuilder<V>
where
V: Views<View = ListItem> + 'static,
{
type Config = ListConfig;
fn config(self) -> Self::Config {
ListConfig {
contents: SharedAnyViews::new(self.contents),
editing: self.editing,
on_delete: self.on_delete,
on_move: self.on_move,
scroll_controller: self.scroll_controller,
uses_sections: self.uses_sections,
}
}
}
impl<V> View for ListBuilder<V>
where
V: Views<View = ListItem> + 'static,
{
fn body(self, env: &Environment) -> impl View {
render_list_config(ConfigurableView::config(self), env)
}
}
fn list_delete_action<H, Args>(handler: H) -> OnDelete
where
H: Handler<Args, ()>,
{
let action = shared_action(handler);
Box::new(move |env, index| action.call(&env.extending(ListDelete(index))))
}
fn list_move_action<H, Args>(handler: H) -> OnMove
where
H: Handler<Args, ()>,
{
let action = shared_action(handler);
Box::new(move |env, movement| action.call(&env.extending(ListMove(movement))))
}
#[derive(Debug, Clone, Default)]
pub struct ListSection {
pub label: Option<Text>,
pub footer: Option<Text>,
}
impl ListSection {
#[must_use]
pub fn new(label: impl IntoText) -> Self {
Self {
label: Some(label.into_text()),
footer: None,
}
}
#[must_use]
pub const fn unlabeled() -> Self {
Self {
label: None,
footer: None,
}
}
#[must_use]
pub fn footer(mut self, footer: impl IntoText) -> Self {
self.footer = Some(footer.into_text());
self
}
#[must_use]
fn resolved(self, env: &Environment) -> Self {
Self {
label: self.label.map(|label| Text::from(label.resolve(env))),
footer: self.footer.map(|footer| Text::from(footer.resolve(env))),
}
}
}
pub struct ListItem {
pub content: AnyView,
pub deletable: Computed<bool>,
pub section: Option<ListSection>,
pub selected: Computed<bool>,
}
impl NativeView for ListItem {}
impl View for ListItem {
fn body(self, _env: &Environment) -> impl View {
self.content
}
}
impl_debug!(ListItem);
impl ListItem {
pub fn new(content: impl View) -> Self {
Self {
content: AnyView::new(content),
deletable: Computed::new(true),
section: None,
selected: Computed::new(false),
}
}
#[must_use]
pub fn deletable(mut self, deletable: impl IntoComputed<bool>) -> Self {
self.deletable = deletable.into_computed();
self
}
#[must_use]
pub fn section(mut self, section: ListSection) -> Self {
self.section = Some(section);
self
}
#[must_use]
pub fn selected(mut self, selected: impl IntoComputed<bool>) -> Self {
self.selected = selected.into_computed();
self
}
}