[pve-devel] [PATCH installer 4/4] tui: bootdisk: use tabbed view for disk options on small screens
Christoph Heiss
c.heiss at proxmox.com
Thu Jun 13 13:53:13 CEST 2024
It's currently only activated for small (<=80 columns) displays, to make
disk selection a lot more usable in these cases. This mostly affects
serial console installation, but possibly also installations using a
virtual screen via IPMI/BMC.
Signed-off-by: Christoph Heiss <c.heiss at proxmox.com>
---
proxmox-tui-installer/src/views/bootdisk.rs | 239 ++++++++++++--------
1 file changed, 147 insertions(+), 92 deletions(-)
diff --git a/proxmox-tui-installer/src/views/bootdisk.rs b/proxmox-tui-installer/src/views/bootdisk.rs
index 29a2854..1e22105 100644
--- a/proxmox-tui-installer/src/views/bootdisk.rs
+++ b/proxmox-tui-installer/src/views/bootdisk.rs
@@ -4,12 +4,12 @@ use cursive::{
view::{Nameable, Resizable, ViewWrapper},
views::{
Button, Dialog, DummyView, LinearLayout, NamedView, PaddedView, Panel, ScrollView,
- SelectView, TextView,
+ SelectView, TextView, ViewRef,
},
- Cursive, View,
+ Cursive, Vec2, View,
};
-use super::{DiskSizeEditView, FormView, IntegerEditView};
+use super::{DiskSizeEditView, FormView, IntegerEditView, TabbedView};
use crate::options::FS_TYPES;
use crate::InstallerState;
@@ -67,11 +67,15 @@ impl BootdiskOptionsView {
let runinfo = runinfo.clone();
let options = advanced_options.clone();
move |siv| {
- siv.add_layer(advanced_options_view(
- &runinfo,
- options.clone(),
- product_conf.clone(),
- ));
+ let mut view =
+ advanced_options_view(&runinfo, options.clone(), product_conf.clone());
+
+ // Pre-compute the child's layout, since it might depend on the size. Without this,
+ // the view will be empty until focused.
+ // The screen size never changes in our case, so this is completely OK.
+ view.layout(siv.screen_size());
+
+ siv.add_layer(view);
}
}));
@@ -193,6 +197,7 @@ impl AdvancedBootdiskOptionsView {
.unwrap_or_else(|| runinfo.disks[0].clone());
// Update the (inner) options view
+ let screen_size = siv.screen_size();
siv.call_on_name("advanced-bootdisk-options-dialog", |view: &mut Dialog| {
if let Some(AdvancedBootdiskOptionsView { view }) =
view.get_content_mut().downcast_mut()
@@ -203,7 +208,7 @@ impl AdvancedBootdiskOptionsView {
view.add_child(LvmBootdiskOptionsView::new_with_defaults(
&selected_lvm_disk,
&product_conf,
- ));
+ ))
}
FsType::Zfs(_) => view.add_child(ZfsBootdiskOptionsView::new_with_defaults(
&runinfo,
@@ -213,6 +218,11 @@ impl AdvancedBootdiskOptionsView {
view.add_child(BtrfsBootdiskOptionsView::new_with_defaults(&runinfo))
}
}
+
+ // Pre-compute the child's layout, since it might depend on the size. Without this,
+ // the view will be empty until focused.
+ // The screen size never changes in our case, so this is completely OK.
+ view.layout(screen_size);
}
});
@@ -373,11 +383,98 @@ impl ViewWrapper for LvmBootdiskOptionsView {
struct MultiDiskOptionsView<T> {
view: LinearLayout,
+ layout_data: Option<(Vec<Disk>, Vec<usize>, T)>,
phantom: PhantomData<T>,
}
impl<T: View> MultiDiskOptionsView<T> {
+ const DISK_FORM_VIEW_ID: &'static str = "multidisk-disk-form";
+
fn new(avail_disks: &[Disk], selected_disks: &[usize], options_view: T) -> Self {
+ Self {
+ view: LinearLayout::vertical().child(DummyView).child(DummyView),
+ layout_data: Some((avail_disks.to_vec(), selected_disks.to_vec(), options_view)),
+ phantom: PhantomData,
+ }
+ }
+
+ fn top_panel(mut self, view: impl View) -> Self {
+ self.view.remove_child(0);
+ self.view.insert_child(0, Panel::new(view));
+ self
+ }
+
+ fn get_options_view(&mut self) -> Option<&T> {
+ let inner = self.view.get_child(1)?;
+
+ if let Some(view) = inner.downcast_ref::<LinearLayout>() {
+ view.get_child(2)?
+ .downcast_ref::<LinearLayout>()?
+ .get_child(2)?
+ .downcast_ref::<T>()
+ } else if let Some(view) = inner.downcast_ref::<TabbedView>() {
+ view.get(1)?
+ .downcast_ref::<PaddedView<T>>()
+ .map(|v| v.get_inner())
+ } else {
+ None
+ }
+ }
+
+ fn get_disk_form(&mut self) -> Option<ViewRef<FormView>> {
+ let inner = self.view.get_child_mut(1)?;
+
+ let view = if let Some(view) = inner.downcast_mut::<LinearLayout>() {
+ view.get_child_mut(0)?
+ .downcast_mut::<LinearLayout>()?
+ .get_child_mut(2)
+ } else if let Some(view) = inner.downcast_mut::<TabbedView>() {
+ view.get_mut(0)?
+ .downcast_mut::<PaddedView<LinearLayout>>()
+ .map(|v| v.get_inner_mut())?
+ .get_child_mut(0)
+ } else {
+ None
+ };
+
+ view?
+ .downcast_mut::<ScrollView<NamedView<FormView>>>()
+ .map(ScrollView::get_inner_mut)
+ .map(NamedView::get_mut)
+ }
+
+ /// This function returns a tuple of vectors. The first vector contains the currently selected
+ /// disks in order of their selection slot. Empty slots are filtered out. The second vector
+ /// contains indices of each slot's selection, which enables us to restore the selection even
+ /// for empty slots.
+ fn get_disks_and_selection(&mut self) -> Option<(Vec<Disk>, Vec<usize>)> {
+ let mut disks = vec![];
+ let mut selected_disks = Vec::new();
+ let disk_form = self.get_disk_form()?;
+
+ for i in 0..disk_form.len() {
+ let disk = disk_form.get_value::<SelectView<Option<Disk>>, _>(i)?;
+
+ // `None` means no disk was selected for this slot
+ if let Some(disk) = disk {
+ disks.push(disk);
+ }
+
+ selected_disks.push(
+ disk_form
+ .get_child::<SelectView<Option<Disk>>>(i)?
+ .selected_id()?,
+ );
+ }
+
+ Some((disks, selected_disks))
+ }
+
+ fn do_layout(&mut self, size: Vec2) {
+ let Some((avail_disks, selected_disks, options_view)) = self.layout_data.take() else {
+ panic!("cannot do layout without data!");
+ };
+
let mut selectable_disks = avail_disks
.iter()
.map(|d| (d.to_string(), Some(d.clone())))
@@ -396,15 +493,14 @@ impl<T: View> MultiDiskOptionsView<T> {
);
}
- let mut disk_select_view = LinearLayout::vertical()
- .child(TextView::new("Disk setup").center())
- .child(DummyView)
- .child(ScrollView::new(disk_form.with_name("multidisk-disk-form")));
+ let mut disk_select_view = LinearLayout::vertical().child(ScrollView::new(
+ disk_form.with_name(Self::DISK_FORM_VIEW_ID),
+ ));
if avail_disks.len() > 3 {
let do_not_use_index = selectable_disks.len() - 1;
let deselect_all_button = Button::new("Deselect all", move |siv| {
- siv.call_on_name("multidisk-disk-form", |view: &mut FormView| {
+ siv.call_on_name(Self::DISK_FORM_VIEW_ID, |view: &mut FormView| {
view.call_on_childs(&|v: &mut SelectView<Option<Disk>>| {
// As there is no .on_select() callback defined on the
// SelectView's, the returned callback here can be safely
@@ -425,96 +521,52 @@ impl<T: View> MultiDiskOptionsView<T> {
));
}
- let options_view = LinearLayout::vertical()
- .child(TextView::new("Advanced options").center())
- .child(DummyView)
- .child(options_view);
+ self.view.remove_child(1);
- let view = LinearLayout::horizontal()
- .child(disk_select_view)
- .child(DummyView.fixed_width(3))
- .child(options_view);
+ if size.x > 80 {
+ disk_select_view.insert_child(0, DummyView);
+ disk_select_view.insert_child(0, TextView::new("Disk setup").center());
- Self {
- view: LinearLayout::vertical().child(view),
- phantom: PhantomData,
- }
- }
+ let view = LinearLayout::horizontal()
+ .child(disk_select_view)
+ .child(DummyView.fixed_width(3))
+ .child(
+ LinearLayout::vertical()
+ .child(TextView::new("Advanced options").center())
+ .child(DummyView)
+ .child(options_view),
+ );
- fn top_panel(mut self, view: impl View) -> Self {
- if self.has_top_panel() {
- self.view.remove_child(0);
+ self.view.add_child(view);
+ } else {
+ let view = TabbedView::new()
+ .tab("Disk setup", PaddedView::lrtb(0, 0, 1, 0, disk_select_view))
+ .tab(
+ "Advanced options",
+ PaddedView::lrtb(0, 0, 1, 0, options_view),
+ );
+
+ self.view.add_child(view);
}
-
- self.view.insert_child(0, Panel::new(view));
- self
}
+}
- ///
- /// This function returns a tuple of vectors. The first vector contains the currently selected
- /// disks in order of their selection slot. Empty slots are filtered out. The second vector
- /// contains indices of each slot's selection, which enables us to restore the selection even
- /// for empty slots.
- ///
- fn get_disks_and_selection(&mut self) -> Option<(Vec<Disk>, Vec<usize>)> {
- let mut disks = vec![];
- let view_top_index = usize::from(self.has_top_panel());
-
- let disk_form = self
- .view
- .get_child_mut(view_top_index)?
- .downcast_mut::<LinearLayout>()?
- .get_child_mut(0)?
- .downcast_mut::<LinearLayout>()?
- .get_child_mut(2)?
- .downcast_mut::<ScrollView<NamedView<FormView>>>()?
- .get_inner_mut()
- .get_mut();
-
- let mut selected_disks = Vec::new();
-
- for i in 0..disk_form.len() {
- let disk = disk_form.get_value::<SelectView<Option<Disk>>, _>(i)?;
-
- // `None` means no disk was selected for this slot
- if let Some(disk) = disk {
- disks.push(disk);
- }
+impl<T: 'static + View> ViewWrapper for MultiDiskOptionsView<T> {
+ cursive::wrap_impl!(self.view: LinearLayout);
- selected_disks.push(
- disk_form
- .get_child::<SelectView<Option<Disk>>>(i)?
- .selected_id()?,
- );
+ fn wrap_layout(&mut self, size: Vec2) {
+ if self.layout_data.is_some() {
+ self.do_layout(size);
}
- Some((disks, selected_disks))
- }
-
- fn inner_mut(&mut self) -> Option<&mut T> {
- let view_top_index = usize::from(self.has_top_panel());
-
- self.view
- .get_child_mut(view_top_index)?
- .downcast_mut::<LinearLayout>()?
- .get_child_mut(2)?
- .downcast_mut::<LinearLayout>()?
- .get_child_mut(2)?
- .downcast_mut::<T>()
+ self.view.layout(size)
}
- fn has_top_panel(&self) -> bool {
- // The root view should only ever have one or two children
- assert!([1, 2].contains(&self.view.len()));
-
- self.view.len() == 2
+ fn wrap_needs_relayout(&self) -> bool {
+ self.layout_data.is_some() || self.view.needs_relayout()
}
}
-impl<T: 'static> ViewWrapper for MultiDiskOptionsView<T> {
- cursive::wrap_impl!(self.view: LinearLayout);
-}
-
struct BtrfsBootdiskOptionsView {
view: MultiDiskOptionsView<FormView>,
}
@@ -540,7 +592,10 @@ impl BtrfsBootdiskOptionsView {
fn get_values(&mut self) -> Option<(Vec<Disk>, BtrfsBootdiskOptions)> {
let (disks, selected_disks) = self.view.get_disks_and_selection()?;
- let disk_size = self.view.inner_mut()?.get_value::<DiskSizeEditView, _>(0)?;
+ let disk_size = self
+ .view
+ .get_options_view()?
+ .get_value::<DiskSizeEditView, _>(0)?;
Some((
disks,
@@ -626,7 +681,7 @@ impl ZfsBootdiskOptionsView {
fn get_values(&mut self) -> Option<(Vec<Disk>, ZfsBootdiskOptions)> {
let (disks, selected_disks) = self.view.get_disks_and_selection()?;
- let view = self.view.inner_mut()?;
+ let view = self.view.get_options_view()?;
let has_arc_max = view.len() >= 6;
let disk_size_index = if has_arc_max { 5 } else { 4 };
--
2.44.1
More information about the pve-devel
mailing list