termscp/src/ui/components/bytes.rs

311 lines
9.0 KiB
Rust

//! ## Bytes
//!
//! `Bytes` component extends an `Input` component in order to provide an input type for byte size.
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use crate::utils::fmt::fmt_bytes;
use crate::utils::parser::parse_bytesize;
// ext
use tui_realm_stdlib::{Input, InputPropsBuilder};
use tuirealm::event::Event;
use tuirealm::props::{Alignment, Props, PropsBuilder};
use tuirealm::tui::{
layout::Rect,
style::Color,
widgets::{BorderType, Borders},
};
use tuirealm::{Component, Frame, Msg, Payload, Value};
// -- props
/// ## BytesPropsBuilder
///
/// A wrapper around an `InputPropsBuilder`
pub struct BytesPropsBuilder {
puppet: InputPropsBuilder,
}
impl Default for BytesPropsBuilder {
fn default() -> Self {
Self {
puppet: InputPropsBuilder::default(),
}
}
}
impl PropsBuilder for BytesPropsBuilder {
fn build(&mut self) -> Props {
self.puppet.build()
}
fn hidden(&mut self) -> &mut Self {
self.puppet.hidden();
self
}
fn visible(&mut self) -> &mut Self {
self.puppet.visible();
self
}
}
impl From<Props> for BytesPropsBuilder {
fn from(props: Props) -> Self {
BytesPropsBuilder {
puppet: InputPropsBuilder::from(props),
}
}
}
impl BytesPropsBuilder {
/// ### with_borders
///
/// Set component borders style
pub fn with_borders(
&mut self,
borders: Borders,
variant: BorderType,
color: Color,
) -> &mut Self {
self.puppet.with_borders(borders, variant, color);
self
}
/// ### with_label
///
/// Set input label
pub fn with_label<S: AsRef<str>>(&mut self, label: S, alignment: Alignment) -> &mut Self {
self.puppet.with_label(label, alignment);
self
}
/// ### with_color
///
/// Set initial value for component
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
self.puppet.with_foreground(color);
self
}
/// ### with_color
///
/// Set initial value for component
pub fn with_value(&mut self, val: u64) -> &mut Self {
self.puppet.with_value(fmt_bytes(val));
self
}
}
// -- component
/// ## Bytes
///
/// a wrapper component of `Input` which adds a superset of rules to behave as a color picker
pub struct Bytes {
input: Input,
native_color: Color,
}
impl Bytes {
/// ### new
///
/// Instantiate a new `Bytes`
pub fn new(props: Props) -> Self {
// Instantiate a new color picker using input
Self {
native_color: props.foreground,
input: Input::new(props),
}
}
/// ### update_colors
///
/// Update colors to match selected color, with provided one
fn update_colors(&mut self, color: Color) {
let mut props = self.get_props();
props.foreground = color;
props.borders.color = color;
let _ = self.input.update(props);
}
}
impl Component for Bytes {
/// ### render
///
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
/// If focused, cursor is also set (if supported by widget)
#[cfg(not(tarpaulin_include))]
fn render(&self, render: &mut Frame, area: Rect) {
self.input.render(render, area);
}
/// ### update
///
/// Update component properties
/// Properties should first be retrieved through `get_props` which creates a builder from
/// existing properties and then edited before calling update.
/// Returns a Msg to the view
fn update(&mut self, props: Props) -> Msg {
let msg: Msg = self.input.update(props);
match msg {
Msg::OnChange(Payload::One(Value::Str(input))) => {
match parse_bytesize(input.as_str()) {
Some(bytes) => {
// return OK
self.update_colors(self.native_color);
Msg::OnChange(Payload::One(Value::U64(bytes.as_u64())))
}
None => {
// Invalid color
self.update_colors(Color::Red);
Msg::None
}
}
}
msg => msg,
}
}
/// ### get_props
///
/// Returns a props builder starting from component properties.
/// This returns a prop builder in order to make easier to create
/// new properties for the element.
fn get_props(&self) -> Props {
self.input.get_props()
}
/// ### on
///
/// Handle input event and update internal states.
/// Returns a Msg to the view
fn on(&mut self, ev: Event) -> Msg {
// Capture message from input
match self.input.on(ev) {
Msg::OnChange(Payload::One(Value::Str(input))) => {
// Capture color and validate
match parse_bytesize(input.as_str()) {
Some(bytes) => {
// Update color and return OK
self.update_colors(self.native_color);
Msg::OnChange(Payload::One(Value::U64(bytes.as_u64())))
}
None => {
// Invalid color
self.update_colors(Color::Red);
Msg::None
}
}
}
Msg::OnSubmit(_) => Msg::None,
msg => msg,
}
}
/// ### get_state
///
/// Get current state from component
/// For this component returns Unsigned if the input type is a number, otherwise a text
/// The value is always the current input.
fn get_state(&self) -> Payload {
match self.input.get_state() {
Payload::One(Value::Str(bytes)) => match parse_bytesize(bytes.as_str()) {
None => Payload::None,
Some(bytes) => Payload::One(Value::U64(bytes.as_u64())),
},
_ => Payload::None,
}
}
// -- events
/// ### blur
///
/// Blur component; basically remove focus
fn blur(&mut self) {
self.input.blur();
}
/// ### active
///
/// Active component; basically give focus
fn active(&mut self) {
self.input.active();
}
}
#[cfg(test)]
mod test {
use super::*;
use crossterm::event::{KeyCode, KeyEvent};
use pretty_assertions::assert_eq;
#[test]
fn bytes_input() {
let mut component: Bytes = Bytes::new(
BytesPropsBuilder::default()
.visible()
.with_value(1024)
.with_borders(Borders::ALL, BorderType::Double, Color::Rgb(204, 170, 0))
.with_label("omar", Alignment::Left)
.with_foreground(Color::Red)
.build(),
);
// Focus
component.blur();
component.active();
// Get value
assert_eq!(component.get_state(), Payload::One(Value::U64(1024)));
// Set an invalid color
let props = InputPropsBuilder::from(component.get_props())
.with_value(String::from("#pippo1"))
.hidden()
.build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.get_state(), Payload::None);
// Reset color
let props = BytesPropsBuilder::from(component.get_props())
.with_value(111)
.hidden()
.build();
assert_eq!(
component.update(props),
Msg::OnChange(Payload::One(Value::U64(111)))
);
// Backspace (invalid)
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
Msg::None
);
// Press '1'
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Char('B')))),
Msg::OnChange(Payload::One(Value::U64(111)))
);
}
}