// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! Notebook — paginated tabs with a content area. //! //! Different from `tab_bar` (which is just a segmented selector — the //! application is responsible for rendering whatever content //! corresponds to the active tab). A `Notebook` owns the //! pages: each page bundles a label *and* its content, and only the //! active page's content is laid out / drawn each frame. //! //! The widget itself is stateless: the application owns //! `self.tab: usize` and updates it from the `on_select` callback. The //! pages can be built fresh every frame (typical) or memoised on the //! application side. //! //! ```rust,no_run //! # use ltk::{ notebook, text, Element, Notebook }; //! # #[ derive( Clone ) ] enum Msg { SelectTab( usize ) } //! # struct App { tab: usize } //! # impl App { //! # fn general_view( &self ) -> Element { text( "g" ).into() } //! # fn network_view( &self ) -> Element { text( "n" ).into() } //! # fn audio_view( &self ) -> Element { text( "a" ).into() } //! # fn _ex( &self ) -> Notebook { //! notebook() //! .page( "General", self.general_view() ) //! .page( "Network", self.network_view() ) //! .page( "Audio", self.audio_view() ) //! .selected( self.tab ) //! .on_select( Msg::SelectTab ) //! # } //! # } //! ``` use std::sync::Arc; use crate::layout::column::column; use crate::layout::spacer::spacer; use crate::types::Length; use super::Element; mod theme; #[ cfg( test ) ] mod tests; /// One page of a [`Notebook`] — a label for the tab strip and the /// element to show when this page is active. pub struct NotebookPage { pub( crate ) label: String, pub( crate ) view: Element, } /// Paginated tab container. /// /// Renders a `tab_bar` strip at the top followed by the /// active page's content. Pages whose index is not [`Self::selected`] /// are dropped before draw so they do not consume layout time — a /// notebook with 50 pages costs the same to draw as one with 5. pub struct Notebook { pub( crate ) pages: Vec>, pub( crate ) selected: usize, pub( crate ) on_select: Option Msg>>, } impl Notebook { /// Create an empty notebook with no pages and no selection. pub fn new() -> Self { Self { pages: Vec::new(), selected: 0, on_select: None, } } /// Append a page. Returns `Self` for chaining. pub fn page( mut self, label: impl Into, view: impl Into>, ) -> Self { self.pages.push( NotebookPage { label: label.into(), view: view.into(), } ); self } /// Index of the currently active page. Out-of-range values fall /// back to page 0; a notebook with no pages renders an empty /// container. pub fn selected( mut self, idx: usize ) -> Self { self.selected = idx; self } /// Callback invoked with the index of the tab the user tapped. pub fn on_select( mut self, f: impl Fn( usize ) -> Msg + 'static ) -> Self { self.on_select = Some( Arc::new( f ) ); self } /// Build the `Element` tree representing this notebook. pub fn build( self ) -> Element { use super::tab_bar::tabs; let labels: Vec = self.pages.iter().map( |p| p.label.clone() ).collect(); let selected = if self.selected < self.pages.len() { self.selected } else { 0 }; let n_pages = self.pages.len(); // Build the tab strip; wire on_select if the caller provided one. let mut strip = tabs::( labels ).selected( selected ); if let Some( cb ) = self.on_select.clone() { strip = strip.on_select( move |i| cb( i ) ); } // Drain to extract the active page's view by index. let mut pages = self.pages; let active_view: Element = if pages.is_empty() { spacer().into() } else { let _ = n_pages; pages.swap_remove( selected ) .view }; column() .spacing( Length::widget( theme::SPACING ) ) .push( strip ) .push( active_view ) .into() } } impl Default for Notebook { fn default() -> Self { Self::new() } } impl From> for Element { fn from( n: Notebook ) -> Self { n.build() } } /// Create an empty [`Notebook`]. /// /// ```rust,no_run /// # use ltk::{ notebook, text, Element, Notebook }; /// # #[ derive( Clone ) ] enum Msg { SelectTab( usize ) } /// # struct App { tab: usize } /// # impl App { /// # fn inbox_view( &self ) -> Element { text( "i" ).into() } /// # fn sent_view( &self ) -> Element { text( "s" ).into() } /// # fn _ex( &self ) -> Notebook { /// notebook() /// .page( "Inbox", self.inbox_view() ) /// .page( "Sent", self.sent_view() ) /// .selected( self.tab ) /// .on_select( Msg::SelectTab ) /// # } /// # } /// ``` pub fn notebook() -> Notebook { Notebook::new() }