leptodon 0.1.0

your Leptos UI toolkit for data science
Documentation
// Leptodon
//
// Copyright (C) 2025-2026 Open Analytics NV
//
// ===========================================================================
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the Apache License as published by The Apache Software
// Foundation, either version 2 of the License, or (at your option) any later
// version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the Apache License for more details.
//
// You should have received a copy of the Apache License along with this program.
// If not, see <http://www.apache.org/licenses/>
use leptos::prelude::{ClassAttribute, For, Get, IntoAny, OnAttribute, Show};
use leptos::prelude::{ElementChild, RwSignal, Signal};
use std::fmt::Debug;
use std::{hash::Hash, sync::Arc};

use leptos::{IntoView, view};
use leptos_struct_table::*;

/// Rows which are groupable need to implement this trait in order to use the [GroupTableRowRenderer]
pub trait GroupRow<Column> {
    fn group_info(&self) -> &GroupingInfo<Column>;
}

/// Custom row renderer that handles rows groups
#[allow(unused_variables, non_snake_case)]
pub fn GroupTableRowRenderer<Row, Column>(
    // The class attribute for the row element. Generated by the classes provider.
    class: Signal<String>,
    // The row to render.
    row: RwSignal<Row>,
    // The index of the row. Starts at 0 for the first body row.
    index: usize,
    // The selected state of the row. True, when the row is selected.
    selected: Signal<bool>,
    // Event handler callback when this row is selected
    on_select: EventHandler<web_sys::MouseEvent>,
    // Columns to show and their order.
    columns: RwSignal<Vec<Column>>,
) -> impl IntoView
where
    Row: TableRow<Column> + Debug + Clone + Send + Sync + GroupRow<Column> + 'static,
    Column: Eq + Hash + Copy + Clone + Send + Sync + 'static,
{
    // debug_log!("Index of row: {}", index);
    // debug_log!("{:?}", row.get());
    leptos::view! {
        <tr class=class on:click=move |mouse_event| on_select.run(mouse_event)>
            <For
                each=move || columns.get().into_iter()
                key=|column| *column
                children=move |column| {
                    let temp_row = row.get();
                    let grouping_info = temp_row.group_info();
                    if !grouping_info.grouped_by.is_empty() {
                        let class = if grouping_info.grouped_by.contains(&column) && grouping_info.row_index == 0 {
                            "font-bold".into()
                        } else {
                            String::new()
                        };
                        if grouping_info.nb_entries > 1 {
                            if grouping_info.row_index == 0 {
                                // Group-heading-row
                                // Split rendering into 2 rows, first one below

                                if grouping_info.grouped_by.contains(&column) {
                                    return Row::cell_renderer_for_column(row, column, class).into_any()
                                }
                            } else {
                                // Content-row
                                // Skip rendering of grouped columns
                                if !grouping_info.grouped_by.contains(&column) ||
                                    grouping_info.grouped_by.len() == Row::columns().len() {
                                    return Row::cell_renderer_for_column(row, column, class).into_any()
                                }
                            }
                        } else {
                            // Single row in the group
                            return Row::cell_renderer_for_column(row, column, class).into_any()
                        }
                    } else {
                        return Row::cell_renderer_for_column(row, column, String::new()).into_any()
                    }

                    view!{ <td/> }.into_any()
                }>
            </For>
        </tr>
        <Show
            when=move || {
                let temp_row = row.get();
                let grouping_info = temp_row.group_info();

                grouping_info.row_index == 0 &&
                    grouping_info.nb_entries > 1 &&
                    !grouping_info.grouped_by.is_empty() &&
                    // Edge-case: when grouping on all columns, don't render content rows.
                    grouping_info.grouped_by.len() != Row::columns().len()
            }
            fallback=|| ()
        >
            // TODO: Fix on_click listener here
            <tr class=class>
                <For
                    each=move || columns.get().into_iter()
                    key=|column| *column
                    children=move |column| {
                        let temp_row = row.get();
                        let grouping_info = temp_row.group_info();
                        if !grouping_info.grouped_by.contains(&column) {
                            return Row::cell_renderer_for_column(row, column, String::new()).into_any()
                        }
                        view!{ <td/> }.into_any()
                    }>
                </For>
            </tr>
        </Show>
    }
}

#[derive(Debug, Clone)]
pub struct GroupingInfo<Column> {
    // Row index with respect to the group
    pub row_index: u32,
    // Number of entries in this group
    pub nb_entries: u32,
    // Grouping by certain columns
    pub grouped_by: Arc<Vec<Column>>,
}

impl<T> Default for GroupingInfo<T> {
    fn default() -> Self {
        Self {
            row_index: Default::default(),
            nb_entries: Default::default(),
            grouped_by: Default::default(),
        }
    }
}