1 use std
::{net::IpAddr, rc::Rc, str::FromStr}
;
4 event
::{Event, EventResult}
,
5 view
::{Resizable, ViewWrapper}
,
6 views
::{EditView, LinearLayout, NamedView, ResizedView, SelectView, TextView}
,
10 use proxmox_installer_common
::utils
::CidrAddress
;
16 pub use table_view
::*;
21 pub struct NumericEditView
<T
> {
24 max_content_width
: Option
<usize>,
28 impl<T
: Copy
+ ToString
+ FromStr
+ PartialOrd
> NumericEditView
<T
> {
29 /// Creates a new [`NumericEditView`], with the value set to `0`.
30 pub fn new() -> Self {
31 let view
= LinearLayout
::horizontal().child(EditView
::new().content("0").full_width());
36 max_content_width
: None
,
41 /// Creates a new [`NumericEditView`], with the value set to `0` and a label to the right of it
42 /// with the given content, separated by a space.
45 /// * `suffix` - Content for the label to the right of it.
46 pub fn new_with_suffix(suffix
: &str) -> Self {
47 let view
= LinearLayout
::horizontal()
48 .child(EditView
::new().content("0").full_width())
49 .child(TextView
::new(format
!(" {suffix}")));
54 max_content_width
: None
,
59 pub fn max_value(mut self, max
: T
) -> Self {
60 self.max_value
= Some(max
);
64 pub fn max_content_width(mut self, width
: usize) -> Self {
65 self.max_content_width
= Some(width
);
66 self.inner_mut().set_max_content_width(Some(width
));
70 pub fn allow_empty(mut self, value
: bool
) -> Self {
71 self.allow_empty
= value
;
74 *self.inner_mut() = EditView
::new();
76 *self.inner_mut() = EditView
::new().content("0");
79 let max_content_width
= self.max_content_width
;
80 self.inner_mut().set_max_content_width(max_content_width
);
84 pub fn get_content(&self) -> Result
<T
, <T
as FromStr
>::Err
> {
85 assert
!(!self.allow_empty
);
86 self.inner().get_content().parse()
89 pub fn get_content_maybe(&self) -> Option
<Result
<T
, <T
as FromStr
>::Err
>> {
90 let content
= self.inner().get_content();
91 if !content
.is_empty() {
92 Some(self.inner().get_content().parse())
98 pub fn set_max_value(&mut self, max
: T
) {
99 self.max_value
= Some(max
);
102 fn check_bounds(&mut self, original
: Rc
<String
>, result
: EventResult
) -> EventResult
{
103 // Check if the new value is actually valid according to the max value, if set
104 if let Some(max
) = self.max_value
{
105 if let Ok(val
) = self.get_content() {
106 if result
.is_consumed() && val
> max
{
107 // Restore the original value, before the insert
108 let cb
= self.inner_mut().set_content((*original
).clone());
109 return EventResult
::with_cb_once(move |siv
| {
120 /// Provides an immutable reference to the inner [`EditView`].
121 fn inner(&self) -> &EditView
{
122 // Safety: Invariant; first child must always exist and be a `EditView`
126 .downcast_ref
::<ResizedView
<EditView
>>()
131 /// Provides a mutable reference to the inner [`EditView`].
132 fn inner_mut(&mut self) -> &mut EditView
{
133 // Safety: Invariant; first child must always exist and be a `EditView`
137 .downcast_mut
::<ResizedView
<EditView
>>()
142 /// Sets the content of the inner [`EditView`]. This correctly swaps out the content without
143 /// modifying the [`EditView`] in any way.
145 /// Chainable variant.
148 /// * `content` - New, stringified content for the inner [`EditView`]. Must be a valid value
149 /// according to the containet type `T`.
150 fn content_inner(mut self, content
: &str) -> Self {
151 let mut inner
= EditView
::new();
152 std
::mem
::swap(self.inner_mut(), &mut inner
);
153 inner
= inner
.content(content
);
154 std
::mem
::swap(self.inner_mut(), &mut inner
);
159 pub type FloatEditView
= NumericEditView
<f64>;
160 pub type IntegerEditView
= NumericEditView
<usize>;
162 impl ViewWrapper
for FloatEditView
{
163 cursive
::wrap_impl
!(self.view
: LinearLayout
);
165 fn wrap_on_event(&mut self, event
: Event
) -> EventResult
{
166 let original
= self.inner_mut().get_content();
168 let has_decimal_place
= original
.find('
.'
).is_some();
170 let result
= match event
{
171 Event
::Char(c
) if !c
.is_numeric() && c
!= '
.'
=> return EventResult
::consumed(),
172 Event
::Char('
.'
) if has_decimal_place
=> return EventResult
::consumed(),
173 _
=> self.view
.on_event(event
),
176 let decimal_places
= self
180 .map(|(_
, s
)| s
.len())
181 .unwrap_or_default();
182 if decimal_places
> 2 {
183 let cb
= self.inner_mut().set_content((*original
).clone());
184 return EventResult
::with_cb_once(move |siv
| {
190 self.check_bounds(original
, result
)
195 /// Sets the value of the [`FloatEditView`].
196 pub fn content(self, content
: f64) -> Self {
197 self.content_inner(&format
!("{:.2}", content
))
201 impl ViewWrapper
for IntegerEditView
{
202 cursive
::wrap_impl
!(self.view
: LinearLayout
);
204 fn wrap_on_event(&mut self, event
: Event
) -> EventResult
{
205 let original
= self.inner_mut().get_content();
207 let result
= match event
{
208 // Drop all other characters than numbers; allow dots if not set to integer-only
209 Event
::Char(c
) if !c
.is_numeric() => EventResult
::consumed(),
210 _
=> self.view
.on_event(event
),
213 self.check_bounds(original
, result
)
217 impl IntegerEditView
{
218 /// Sets the value of the [`IntegerEditView`].
219 pub fn content(self, content
: usize) -> Self {
220 self.content_inner(&content
.to_string())
224 pub struct DiskSizeEditView
{
229 impl DiskSizeEditView
{
230 pub fn new() -> Self {
231 let view
= LinearLayout
::horizontal()
232 .child(FloatEditView
::new().full_width())
233 .child(TextView
::new(" GB"));
241 pub fn new_emptyable() -> Self {
242 let view
= LinearLayout
::horizontal()
243 .child(FloatEditView
::new().allow_empty(true).full_width())
244 .child(TextView
::new(" GB"));
252 pub fn content(mut self, content
: f64) -> Self {
253 if let Some(view
) = self.view
.get_child_mut(0).and_then(|v
| v
.downcast_mut()) {
254 *view
= FloatEditView
::new().content(content
).full_width();
260 pub fn content_maybe(self, content
: Option
<f64>) -> Self {
261 if let Some(value
) = content
{
268 pub fn max_value(mut self, max
: f64) -> Self {
269 if let Some(view
) = self
272 .and_then(|v
| v
.downcast_mut
::<ResizedView
<FloatEditView
>>())
274 view
.get_inner_mut().set_max_value(max
);
280 pub fn get_content(&self) -> Option
<f64> {
283 .downcast_ref
::<ResizedView
<FloatEditView
>>()?
285 if self.allow_empty
{
286 v
.get_content_maybe().and_then(Result
::ok
)
297 impl ViewWrapper
for DiskSizeEditView
{
298 cursive
::wrap_impl
!(self.view
: LinearLayout
);
301 pub trait FormViewGetValue
<R
> {
302 fn get_value(&self) -> Option
<R
>;
305 impl FormViewGetValue
<String
> for EditView
{
306 fn get_value(&self) -> Option
<String
> {
307 Some((*self.get_content()).clone())
311 impl<T
: '
static + Clone
> FormViewGetValue
<T
> for SelectView
<T
> {
312 fn get_value(&self) -> Option
<T
> {
313 self.selection().map(|v
| (*v
).clone())
317 impl<T
> FormViewGetValue
<T
> for NumericEditView
<T
>
319 T
: Copy
+ ToString
+ FromStr
+ PartialOrd
,
320 NumericEditView
<T
>: ViewWrapper
,
322 fn get_value(&self) -> Option
<T
> {
323 self.get_content().ok()
327 impl FormViewGetValue
<CidrAddress
> for CidrAddressEditView
{
328 fn get_value(&self) -> Option
<CidrAddress
> {
333 impl<T
, R
> FormViewGetValue
<R
> for NamedView
<T
>
335 T
: '
static + FormViewGetValue
<R
>,
336 NamedView
<T
>: ViewWrapper
,
337 <NamedView
<T
> as ViewWrapper
>::V
: FormViewGetValue
<R
>,
339 fn get_value(&self) -> Option
<R
> {
340 self.with_view(|v
| v
.get_value()).flatten()
344 impl FormViewGetValue
<f64> for DiskSizeEditView
{
345 fn get_value(&self) -> Option
<f64> {
350 pub struct FormView
{
355 pub fn new() -> Self {
356 let view
= LinearLayout
::horizontal()
357 .child(LinearLayout
::vertical().full_width())
358 .child(LinearLayout
::vertical().full_width());
363 pub fn add_child(&mut self, label
: &str, view
: impl View
) {
364 self.add_to_column(0, TextView
::new(format
!("{label}: ")).no_wrap());
365 self.add_to_column(1, view
);
368 pub fn child(mut self, label
: &str, view
: impl View
) -> Self {
369 self.add_child(label
, view
);
373 pub fn child_conditional(mut self, condition
: bool
, label
: &str, view
: impl View
) -> Self {
375 self.add_child(label
, view
);
380 pub fn get_child
<T
: View
>(&self, index
: usize) -> Option
<&T
> {
383 .downcast_ref
::<ResizedView
<LinearLayout
>>()?
389 pub fn get_child_mut
<T
: View
>(&mut self, index
: usize) -> Option
<&mut T
> {
392 .downcast_mut
::<ResizedView
<LinearLayout
>>()?
394 .get_child_mut(index
)?
398 pub fn get_value
<T
, R
>(&self, index
: usize) -> Option
<R
>
400 T
: View
+ FormViewGetValue
<R
>,
402 self.get_child
::<T
>(index
)?
.get_value()
405 pub fn replace_child(&mut self, index
: usize, view
: impl View
) {
409 .and_then(|v
| v
.downcast_mut())
410 .map(ResizedView
::<LinearLayout
>::get_inner_mut
);
412 if let Some(parent
) = parent
{
413 parent
.remove_child(index
);
414 parent
.insert_child(index
, view
);
418 pub fn call_on_childs
<T
: View
>(&mut self, callback
: &dyn Fn(&mut T
)) {
419 for i
in 0..self.len() {
420 if let Some(v
) = self.get_child_mut
::<T
>(i
) {
426 pub fn len(&self) -> usize {
429 .and_then(|v
| v
.downcast_ref
::<ResizedView
<LinearLayout
>>())
435 fn add_to_column(&mut self, index
: usize, view
: impl View
) {
437 .get_child_mut(index
)
438 .and_then(|v
| v
.downcast_mut
::<ResizedView
<LinearLayout
>>())
445 impl ViewWrapper
for FormView
{
446 cursive
::wrap_impl
!(self.view
: LinearLayout
);
448 fn wrap_important_area(&self, size
: Vec2
) -> Rect
{
449 // This fixes scrolling on small screen when many elements are present, e.g. bootdisk/RAID
450 // list. Without this, scrolling completely down and then back up would not properly
451 // display the currently selected form element.
452 // tl;dr: For whatever reason, the inner `LinearLayout` calculates the rect with a line
453 // height of 2. So e.g. if the first form element is selected, the y-coordinate is 2, if
454 // the second is selected it is 4 and so on. Knowing that, this can fortunately be quite
455 // easy fixed by just dividing the y-coordinate by 2 and adjusting the size of the area
458 let inner
= self.view
.important_area(size
);
459 let top_left
= inner
.top_left().map_y(|y
| y
/ 2);
461 Rect
::from_size(top_left
, (inner
.width(), 1))
465 pub struct CidrAddressEditView
{
469 impl CidrAddressEditView
{
470 pub fn new() -> Self {
471 let view
= LinearLayout
::horizontal()
472 .child(EditView
::new().full_width())
473 .child(TextView
::new(" / "))
474 .child(Self::mask_edit_view(0));
479 pub fn content(mut self, cidr
: CidrAddress
) -> Self {
480 if let Some(view
) = self
483 .and_then(|v
| v
.downcast_mut
::<ResizedView
<EditView
>>())
485 *view
= EditView
::new()
486 .content(cidr
.addr().to_string())
490 if let Some(view
) = self
493 .and_then(|v
| v
.downcast_mut
::<ResizedView
<IntegerEditView
>>())
495 *view
= Self::mask_edit_view(cidr
.mask());
501 fn mask_edit_view(content
: usize) -> ResizedView
<IntegerEditView
> {
502 IntegerEditView
::new()
504 .max_content_width(3)
509 fn get_values(&self) -> Option
<CidrAddress
> {
513 .downcast_ref
::<ResizedView
<EditView
>>()?
522 .downcast_ref
::<ResizedView
<IntegerEditView
>>()?
527 CidrAddress
::new(addr
, mask
).ok()
531 impl ViewWrapper
for CidrAddressEditView
{
532 cursive
::wrap_impl
!(self.view
: LinearLayout
);