public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Christoph Heiss <c.heiss@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH installer 3/4] tui: views: add new TabbedView component
Date: Thu, 13 Jun 2024 13:53:12 +0200	[thread overview]
Message-ID: <20240613115318.842583-4-c.heiss@proxmox.com> (raw)
In-Reply-To: <20240613115318.842583-1-c.heiss@proxmox.com>

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 <c.heiss@proxmox.com>
---
 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<dyn View>)>,
+    /// 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<V>(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<EventResult, ViewNotFound> {
+        if let Some(view) = self.get_mut(self.current) {
+            view.focus_view(selector)
+        } else {
+            Err(ViewNotFound)
+        }
+    }
+
+    fn take_focus(&mut self, direction: Direction) -> Result<EventResult, CannotFocus> {
+        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


  parent reply	other threads:[~2024-06-13 11:53 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2024-06-13 11:53 [pve-devel] [PATCH installer 0/4] tui: make disk options view tabbed on small screens Christoph Heiss
2024-06-13 11:53 ` [pve-devel] [PATCH installer 1/4] tui: fix some comment typos Christoph Heiss
2024-06-13 11:53 ` [pve-devel] [PATCH installer 2/4] tui: bootdisk: align btrfs dialog interface with zfs equivalent Christoph Heiss
2024-06-13 11:53 ` Christoph Heiss [this message]
2024-06-13 11:53 ` [pve-devel] [PATCH installer 4/4] tui: bootdisk: use tabbed view for disk options on small screens Christoph Heiss
2024-07-02 16:49 ` [pve-devel] [PATCH installer 0/4] tui: make disk options view tabbed " Max Carrara
2024-07-04  9:05   ` Christoph Heiss
2024-07-04  9:28 ` [pve-devel] applied-series: " Thomas Lamprecht

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20240613115318.842583-4-c.heiss@proxmox.com \
    --to=c.heiss@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal