From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 30E661FF0BA for ; Thu, 20 Aug 2026 10:18:16 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 015E52159D; Thu, 20 Aug 2026 10:18:16 +0200 (CEST) From: Dominik Csapak To: pdm-devel@lists.proxmox.com Subject: [PATCH yew-widget-toolkit 2/3] widget: navigation drawer: allow persisting the expanded state Date: Thu, 20 Aug 2026 10:17:51 +0200 Message-ID: <20260820081803.991511-3-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260820081803.991511-1-d.csapak@proxmox.com> References: <20260820081803.991511-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.848 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: TT7S5NYWMMXHU4LIK62BKRHXM5BMLCCS X-Message-ID-Hash: TT7S5NYWMMXHU4LIK62BKRHXM5BMLCCS X-MailFrom: d.csapak@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: By exposing a `stateful_id` property that is used to save the `MenuStates` state in the browser local storage. This is loaded on component creation and when either the menu or the stateful id changes. Signed-off-by: Dominik Csapak --- src/widget/nav/navigation_drawer.rs | 60 +++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/src/widget/nav/navigation_drawer.rs b/src/widget/nav/navigation_drawer.rs index b2bcd1e..114e463 100644 --- a/src/widget/nav/navigation_drawer.rs +++ b/src/widget/nav/navigation_drawer.rs @@ -10,9 +10,9 @@ use pwt_macros::builder; use crate::css::{OverflowX, OverflowY}; use crate::props::{ AsClassesMut, AsCssStylesMut, ContainerBuilder, CssBorderBuilder, CssPaddingBuilder, CssStyles, - EventSubscriber, IntoOptionalKey, IntoVTag, WidgetBuilder, WidgetStyleBuilder, + EventSubscriber, IntoOptionalKey, IntoVTag, StorageLocation, WidgetBuilder, WidgetStyleBuilder, }; -use crate::state::{NavigationContext, NavigationContextExt, Selection}; +use crate::state::{NavigationContext, NavigationContextExt, PersistentState, Selection}; use crate::{impl_class_prop_builder, impl_yew_std_props_builder}; use crate::dom::focus::roving_tabindex_next_recursive; @@ -82,6 +82,14 @@ pub struct NavigationDrawer { #[builder] #[prop_or(true)] animated: bool, + + /// If set, saves the open/collapsed state in the browser local storage + /// + /// The value is used as the storage key, so it has to be unique within the application. Only + /// the menu items that deviate from their default state are stored. + #[builder(IntoPropValue, into_prop_value)] + #[prop_or_default] + stateful_id: Option, } impl AsClassesMut for NavigationDrawer { @@ -153,18 +161,25 @@ impl NavigationDrawer { struct MenuStates { default_collapsed: HashSet, // contains all keys that are collapsed by default changed: HashSet, // contains all keys that deviate from their default + id: Option, // local storage key, if the state should be persisted } impl MenuStates { /// Initializes the state from the menu config (default_collapsed). - fn new(menu: &Menu) -> Self { + /// + /// If an `id` is given, the state will be loaded from, and subsequently saved to, the browser + /// local storage with that identifier. + fn new(menu: &Menu, id: Option) -> Self { let mut default_collapsed = HashSet::new(); Self::collect_default_collapse_states(&mut default_collapsed, menu); - Self { + let mut this = Self { default_collapsed, changed: HashSet::new(), - } + id, + }; + this.load_state(); + this } // iterates over a menu and saves all submenus that are collapsed by default @@ -199,10 +214,33 @@ impl MenuStates { let key = key.to_string(); let default_open = !self.default_collapsed.contains(&key); // only record the keys that deviate from their default, so the set stays minimal - if open == default_open { - self.changed.remove(&key); + let modified = if open == default_open { + self.changed.remove(&key) } else { - self.changed.insert(key); + self.changed.insert(key) + }; + // do not rewrite the local storage if the state did not actually change + if modified { + self.save_state(); + } + } + + /// Loads the deviations from the default state from the local storage, if an `id` is set. + fn load_state(&mut self) { + if let Some(id) = self.id.clone() { + let state = PersistentState::>>::new(StorageLocation::Local(id)); + if let Some(state) = state.into_inner() { + self.changed = state; + } + } + } + + /// Saves the deviations from the default state to the local storage, if an `id` is set. + fn save_state(&self) { + if let Some(id) = self.id.clone() { + let mut state = + PersistentState::>>::new(StorageLocation::Local(id)); + state.update(Some(self.changed.clone())); } } } @@ -544,7 +582,7 @@ impl Component for PwtNavigationDrawer { node_ref: NodeRef::default(), active: active.clone(), selection, - menu_states: MenuStates::new(&props.menu), + menu_states: MenuStates::new(&props.menu, props.stateful_id.clone()), _nav_ctx_handle, }; @@ -663,8 +701,8 @@ impl Component for PwtNavigationDrawer { if props.selection != old_props.selection { self.selection = Self::init_selection(ctx, props.selection.clone(), &self.active); } - if props.menu != old_props.menu { - self.menu_states = MenuStates::new(&props.menu); + if props.menu != old_props.menu || props.stateful_id != old_props.stateful_id { + self.menu_states = MenuStates::new(&props.menu, props.stateful_id.clone()); } true } -- 2.47.3