From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 21E7A1FF3A0 for ; Thu, 13 Jun 2024 13:53:01 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 0E78D1F884; Thu, 13 Jun 2024 13:53:28 +0200 (CEST) From: Christoph Heiss To: pve-devel@lists.proxmox.com Date: Thu, 13 Jun 2024 13:53:12 +0200 Message-ID: <20240613115318.842583-4-c.heiss@proxmox.com> X-Mailer: git-send-email 2.44.1 In-Reply-To: <20240613115318.842583-1-c.heiss@proxmox.com> References: <20240613115318.842583-1-c.heiss@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.013 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record T_SCC_BODY_TEXT_LINE -0.01 - Subject: [pve-devel] [PATCH installer 3/4] tui: views: add new TabbedView component X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox VE development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" Add a tabbed view component, for usage in the advanced disk options dialog when selecting ZFS or Btrfs (for now). Works pretty much the same as its GUI counterpart, as much as that is possible. Signed-off-by: Christoph Heiss --- proxmox-tui-installer/src/views/mod.rs | 3 + .../src/views/tabbed_view.rs | 196 ++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 proxmox-tui-installer/src/views/tabbed_view.rs diff --git a/proxmox-tui-installer/src/views/mod.rs b/proxmox-tui-installer/src/views/mod.rs index 3244e76..a098d1f 100644 --- a/proxmox-tui-installer/src/views/mod.rs +++ b/proxmox-tui-installer/src/views/mod.rs @@ -15,6 +15,9 @@ pub use bootdisk::*; mod install_progress; pub use install_progress::*; +mod tabbed_view; +pub use tabbed_view::*; + mod table_view; pub use table_view::*; diff --git a/proxmox-tui-installer/src/views/tabbed_view.rs b/proxmox-tui-installer/src/views/tabbed_view.rs new file mode 100644 index 0000000..2aeea6a --- /dev/null +++ b/proxmox-tui-installer/src/views/tabbed_view.rs @@ -0,0 +1,196 @@ +use std::borrow::{Borrow, BorrowMut}; + +use cursive::{ + direction::Direction, + event::{AnyCb, Event, EventResult, Key}, + theme::{ColorStyle, PaletteColor}, + utils::{markup::StyledString, span::SpannedStr}, + view::{CannotFocus, IntoBoxedView, Selector, ViewNotFound}, + Printer, Vec2, View, +}; + +pub struct TabbedView { + /// All tab views in format (name, view) + views: Vec<(String, Box)>, + /// Currently active tab index + current: usize, + /// Whether the tab bar has focus currently + bar_has_focus: bool, +} + +impl TabbedView { + /// Creates a view with multiple tabs. + pub fn new() -> Self { + Self { + views: vec![], + current: 0, + bar_has_focus: false, + } + } + + /// Adds a tab to the view. The `name` is the string displayed at the top. + /// + /// Chainable variant. + pub fn tab(mut self, name: &str, view: V) -> Self + where + V: 'static + IntoBoxedView, + { + assert!(!name.is_empty()); + self.views.push((name.to_owned(), view.into_boxed_view())); + self + } + + /// Returns a reference to the specified tab content view. + pub fn get(&self, index: usize) -> Option<&dyn View> { + self.views.get(index).map(|(_, view)| view.borrow()) + } + + /// Returns a mutable reference to the specified tab content view. + pub fn get_mut(&mut self, index: usize) -> Option<&mut dyn View> { + self.views.get_mut(index).map(|(_, view)| view.borrow_mut()) + } + + /// Draws the border around the tab view and the name header for each tab. + fn draw_border(&self, p: &Printer) { + let names_len: usize = self.views.iter().map(|(name, _)| name.len() + 2).sum(); + let tabbar_width = names_len + self.views.len() + 1; + + let top_border_width = (p.output_size.x - tabbar_width) / 2; + p.print_box((0, 0), p.output_size, false); + + self.print_tab_names(p.offset((top_border_width, 0))); + } + + /// Draws all tab names with appropriate highlighting, depending on its state, + /// to the specified printer `p` at `(0, 0)`. + fn print_tab_names(&self, p: Printer) { + let mut pos = Vec2::zero(); + for (index, name) in self.views.iter().map(|(name, _)| name).enumerate() { + p.print(pos, "| "); + pos.x += 2; + + if index == self.current { + self.print_active_tab_name(name, p.offset(pos)); + } else { + p.print(pos, name); + } + + pos.x += name.len(); + p.print(pos, " "); + pos.x += 1; + + p.print(pos, "|"); + } + } + + /// Draws the active tab name to the printer `p`, with its highlighting + /// additionally depending upon whether the tab bar currently has focus or not. + fn print_active_tab_name(&self, name: &str, p: Printer) { + let background = if self.bar_has_focus { + PaletteColor::Highlight + } else { + PaletteColor::HighlightInactive + }; + + p.print_styled( + (0, 0), + SpannedStr::from(&StyledString::styled( + name, + ColorStyle::new(PaletteColor::HighlightText, background), + )), + ) + } +} + +impl View for TabbedView { + fn draw(&self, printer: &Printer) { + self.draw_border(printer); + + if let Some(view) = self.get(self.current) { + view.draw(&printer.offset((1, 1)).focused(!self.bar_has_focus)); + } + } + + fn layout(&mut self, size: Vec2) { + for (_, view) in self.views.iter_mut() { + view.layout(size.saturating_sub((2, 2))); + } + } + + fn required_size(&mut self, constraint: Vec2) -> Vec2 { + if let Some(view) = self.get_mut(self.current) { + view.required_size(constraint) + (2, 2) + } else { + constraint + } + } + + fn on_event(&mut self, event: Event) -> EventResult { + match event { + Event::Key(Key::Right) if self.bar_has_focus => { + self.current = (self.current + 1) % self.views.len(); + EventResult::consumed() + } + Event::Key(Key::Left) if self.bar_has_focus => { + self.current = if self.current == 0 { + self.views.len() - 1 + } else { + self.current - 1 + }; + EventResult::consumed() + } + Event::Key(Key::Down) if self.bar_has_focus => { + self.bar_has_focus = false; + self.get_mut(self.current) + .and_then(|v| v.take_focus(Direction::up()).ok()) + .unwrap_or(EventResult::Ignored) + } + Event::Key(Key::Up) if self.bar_has_focus => EventResult::Ignored, + Event::FocusLost if self.bar_has_focus => { + self.bar_has_focus = false; + EventResult::consumed() + } + Event::Key(Key::Up) if !self.bar_has_focus => { + let result = self + .get_mut(self.current) + .map(|v| v.on_event(event)) + .unwrap_or(EventResult::Ignored); + + match result { + EventResult::Ignored => { + self.bar_has_focus = true; + if let Some(view) = self.get_mut(self.current) { + view.on_event(Event::FocusLost); + } + EventResult::consumed() + } + ev => ev, + } + } + _ if !self.bar_has_focus => self + .get_mut(self.current) + .map(|v| v.on_event(event)) + .unwrap_or_else(EventResult::consumed), + _ => EventResult::Ignored, + } + } + + fn call_on_any(&mut self, selector: &Selector, callback: AnyCb) { + for (_, view) in &mut self.views { + view.call_on_any(selector, callback); + } + } + + fn focus_view(&mut self, selector: &Selector) -> Result { + if let Some(view) = self.get_mut(self.current) { + view.focus_view(selector) + } else { + Err(ViewNotFound) + } + } + + fn take_focus(&mut self, direction: Direction) -> Result { + self.bar_has_focus = direction == Direction::up(); + Ok(EventResult::consumed()) + } +} -- 2.44.1 _______________________________________________ pve-devel mailing list pve-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel