ListItem

The ListItem trait is a trait design to provide controls like ListView or TreeView a way to undestand how a structure should be represented. The trait has to be implemented by the item type that is displayed in the listview. This trait has the following methods that have to be implemented:

pub trait ListItem { fn columns_count() -> u16 { 0 } fn column(index: u16) -> Column { Column::new("", 10, TextAlignament::Left) } fn paint(&self, column_index: u32, width: u16, surface: &mut Surface, theme: &Theme, attr: Option<CharAttribute>) { // paint the item in the surface } fn render_method(&self, column_index: u16) -> Option<RenderMethod>; fn compare(&self, other: &Self, column_index: u16) -> Ordering { Ordering::Equal } fn matches(&self, text: &str) -> bool { true } }

These methods have the following purpose:

  • columns_count() - the number of columns that are displayed in the listview. If let unspecfied, the default value is 0. Adding new columns to the listview will not be affected by this value (all of the new columns will be added after the last column defined by the item type).
  • column(index) - returns the column definition for the column with the specified index. This method has to be implemented by the item type. The column definition contains the name of the column, the width of the column, and the alignment of the column. This method is called once, when the listview is created, for indexes from 0 to columns_count()-1.
  • paint(column_index, width, surface, theme, attr) - paints the item in the surface. This method has to be implemented by the item type. This method is only called if the render_method(...) returns the value RenderMethod::Custom.
  • render_method(column_index) - returns the render method for the column with the specified index. This method has to be implemented by the item type.
  • compare(other, column_index) - compares the item with another item based on the column index. This method has to be implemented by the item type. This method is used to sort the items in the listview.
  • matches(text) - returns true if the item matches the text. This method needs to be implemented only if the flag CustomFilter is set. This method is used to filter the items in the listview based on the search text and a custom algorithm that interprets the search test and filters based on it.

The RenderMethod enum is defined as follows:

pub enum RenderMethod<'a> { Text(&'a str), Ascii(&'a str), DateTime(NaiveDateTime, DateTimeFormat), Time(NaiveTime, TimeFormat), Date(NaiveDate, DateFormat), Duration(Duration, DurationFormat), Int64(i64, NumericFormat), UInt64(u64, NumericFormat), Bool(bool, BoolFormat), Size(u64, SizeFormat), Percentage(f64, PercentageFormat), Float(f64, FloatFormat), Status(Status, StatusFormat), Temperature(f64, TemperatureFormat), Area(u64, AreaFormat), Rating(u32, RatingFormat), Currency(f64, CurrencyFormat), Distance(u64, DistanceFormat), Volume(u64, VolumeFormat), Weight(u64, WeightFormat), Speed(u64, SpeedFormat), Custom, }

with the following meanings:

RenderMethodFormat variantsDescription
TextN/ARenders the text as it is
AsciiN/ARenders the text as ASCII (this is usefull if you know the text is in Ascii format as some thins can be computed faster)
DateTimeFull
Normal
Short
Renders a date and time value
TimeShort
AMPM
Normal
Renders a time value
DateFull
YearMonthDay
DayMonthYear
Renders a date value
DurationAuto
Seconds
Details
Renders a duration value. The Auto value will attempt to find the best representation (e.g. 1:20 instead of 80 seconds)
Int64Normal
Separator
Hex
Hex16
Hex32
Hex64
Renders an integer value. Example:
- Normal -> 12345
- Separator -> 12,345
- Hex and derivate will format a number into various hex representations
UInt64Normal
Separator
Hex
Hex16
Hex32
Hex64
Renders an unsigned integer value. The format is simialr to the one from Int64 variant
BoolYesNo
TrueFalse
XMinus
CheckmarkMinus
Renders a boolean value. Example:
- YesNo -> Yes
- TrueFalse -> True
SizeAuto
AutoWithDecimals
Bytes
KiloBytes
MegaBytes
GigaBytes
TeraBytes
KiloBytesWithDecimals
MegaBytesWithDecimals
GigaBytesWithDecimals
TeraBytesWithDecimals
Renders a size value. The Auto and AutoWithDecimals variants will attempt to find the best representation (e.g. 1.20 MB instead of 1234567 bytes)
PercentageNormal
Decimals
Renders a percentage value. The Normal variant will display the percentage without any decimals, while the Decimals variant will display the percentage with two decimals. For example: PercentageFormat::Normal(0.5) will display 50%, while PercentageFormat::Decimals(0.525) will display 52.50%
FloatNormal
TwoDigits
ThreeDigits
FourDigits
Renders a float value. The Normal variant will display the float without any decimals, while the other ones will add 2,3 or 4 digits to the representation
StatusHashtag
Graphical
Arrow
Renders a a value of type listview::Status with th following potential variants: Running, Queued,Paused, Stopped, Error and Completed. For the variant Running a progress bar is drawn. For the rest of th possible Status valuesa strng is shown
TemperatureCelsius
Fahrenheit
Kelvin
Renders a temperature value. For example: TemperatureFormat::Celsius(20.5) will display 20.5°C, while TemperatureFormat::Fahrenheit(20.5) will display 20.5°F
AreaSquaredMillimeters
SquaredCentimeters
SquaredMeters
SquaredKilometers
Hectares
Ares
SquareFeet
SquareInches
SquareYards
SquareMiles
Renders an area value.
RatingNumerical
Stars
Circles
Asterix
Renders a rating value. The Numerical variant will display the rating as a report (e.g. 3/4) while the other variants will use a star based representation (for example: ★★★☆☆ )
CurrencyUSD
USDSymbol
EUR
EURSymbol
GBP
GBPSymbol
YEN
YENSymbol
Bitcoin
BitcoinSymbol
RON
Renders a currency value. The USD and EUR variants will display the currency value with the currency short name, while the USDSymbol and EURSymbol variants will display the currency value with the currency symbol. For example: CurrencyFormat::USD(20.5) will display USD 20.5, while CurrencyFormat::USDSymbol(20.5) will display $ 20.5. The symbol or short name are alwats displayed on the left side of the column while the value with 2 digits will be displayed on the right side.
DistanceKilometers
Meters
Centimeters
Millimeters
Inches
Feet
Yards
Miles
Renders a distance value
VolumeCubicMillimeters
CubicCentimeters
CubicMeters
CubicKilometers
Liters
Milliliters
Gallons
CubicFeet
CubicInches
CubicYards
CubicMiles
Renders a volume value
WeightGrams
Milligrams
Kilograms
Pounds
Tons
Renders a weight value
SpeedKilometersPerHour
MetersPerHour
KilometersPerSecond
MetersPerSecond
MilesPerHour
MilesPerSecond
Knots
FeetPerSecond
Mach
Renders a speed value

Example

Lets consider the following structure: Student with the following fields:

struct Student { name: String, grade: u8, stars: u8, }

In order to use this structure in a ListView, the minimum implementation of the ListItem trait would be:

use appcui::listview::{ListItem, RenderMethod, NumericFormat, RatingFormat}; impl ListItem for Student { fn render_method(&self, column_index: u16) -> Option<RenderMethod> { match column_index { 0 => Some(RenderMethod::Text(&self.name)), 1 => Some(RenderMethod::UInt64(self.grade as u64, NumericFormat::Normal)), 2 => Some(RenderMethod::Rating(self.stars as u32, RatingFormat::Stars(5))), _ => None, } } }

For this implementation to work, the columns would have to be added when the listview is created (e.g. listview!("class:Student, d:c, columns:[{&Name,20,left},{&Grade,5,center},{&Stars,5,center}]")). However, you can also add them programatically by using the add_column method or by overriding the column method from the ListItem trait, like in the following example:

impl ListItem for Student { fn columns_count() -> u16 { 3 } fn column(index: u16) -> Column { match index { 0 => Column::new("&Name", 20, TextAlignament::Left), 1 => Column::new("&Grade", 5, TextAlignament::Center), 2 => Column::new("&Stars", 5, TextAlignament::Center), _ => Column::new("", 10, TextAlignament::Left), } } fn render_method(&self, column_index: u16) -> Option<RenderMethod> {...} }

Notice that in this case, we have to specify the number of columns that are displayed in the listview by using the columns_count() method.

If you want all of the columns to be sortable, you will have to override the compare method from the ListItem trait. This method has to return an Ordering value that indicates the order of the two items.

impl ListItem for Student { fn columns_count() -> u16 { 3 } fn column(index: u16) -> Column {...} fn render_method(&self, column_index: u16) -> Option<RenderMethod> {...} fn compare(&self, other: &Self, column_index: u16) -> Ordering { match column_index { 0 => self.name.cmp(other.name), 1 => self.grade.cmp(&other.grade), 2 => self.stars.cmp(&other.stars), _ => Ordering::Equal, } } }

Alternatively, you can use the LisItem derive macro to automatically implement the ListItem trait for a structure. The macro has to be combined with the #[Column(...)] attribute that has to be added to each field of the structure that has to be displayed in the listview. The #[Column(...)] attribute has the following parameters:

ParameterTypeRequiredDefault valueDescription
name or textStringYesN/AThe name of the column. This name will be displayed in the header of the column.
width or wu16No10The width of the column.
align or aAlignNoLeftThe alignment of the column (one of Left (or l), Right (or r)) and Center (or c)
render or rRenderNoN/AThe render method for the column. If not provided it will be automatically identified based of the field type
format or fFormatNovarious ...The format of the render method. If not provided it will be defaulted to different variants based on the renderer type
index or idxu16NoN/AThe index of the column. This is used to determine the order of the columns. Indexes starts with value 1 or 0 and have o be unique. If not provided, the next free index will be allocated for the column.

If the render parameter is not provided, the render method will be automatically identified based on the field type. The following field types are supported:

Field typeRender methodDefault variant
&strText
StringText
i8, i16, i32, i64Int64Normal
u8, u16, u32, u64UInt64Normal
f32, f64FloatNormal
boolBoolCheckmarkMinus
NaiveDateTimeDateTimeNormal
NaiveTimeTimeNormal
NaiveDateDateFull
DurationDurationAuto
StatusStatusGraphical

This means that the previous Student structure can be rewritten as follows:

#[derive(ListItem)] struct Student { #[Column(name: "&Name", width: 20, align: Left)] name: String, #[Column(name: "&Grade", width: 5, align: Center)] grade: u8, #[Column(name: "&Stars", width: 5, align: Center, render: Rating, format: Stars)] stars: u8, }

Custom filtering

The filtering mechanism takes the string from the search bar and tries to see if any of the fields that are displayed contain that string (ignoring the case). While this method will be good enough for most cases, there might be scearious where you want to implement a custom filtering algorithm.

For example, lets consider that we want to filter the student based on the name that starts with the specified text written in the search bar. In this case, we have to implement the matches method from the ListItem trait:

impl ListItem for Student { fn matches(&self, text: &str) -> bool { self.name.starts_with(text) } }

We will also need to make sure that the CustomFilter flag is set when creating the listview:

let lv = listview!("class:Student, d:c, flags: CustomFilter");

Custom rendering

If you want to have a custom rendering for the items in the listview, you can use the RenderMethod::Custom variant. This variant will trigger the paint method from the ListItem trait. It is important to notice that you don't need to implement the paint method for all fields (only for the ones where the response from the render_method method is RenderMethod::Custom).

In the next example, we will atempt to print the grade differently based on the value of the grade. If the grade is greater than 5, we will print the grade in green, otherwise in red.

impl ListItem for Student { fn render_method(&self, column_index: u16) -> Option<RenderMethod> { match column_index { 0 => Some(RenderMethod::Text(&self.name)), 1 => Some(RenderMethod::Custom)), 2 => Some(RenderMethod::Rating(self.stars as u32, RatingFormat::Stars(5))), _ => None, } } fn paint(&self, column_index: u32, width: u16, surface: &mut Surface, theme: &Theme, attr: Option<CharAttribute>) { if column_index == 1 { // the grade column let color = if self.grade > 5 { Color::Green } else { Color::Red }; // if the attr is provided, we will use it, otherwise we // will use the color variable (Green or Red) let a = attr.unwrap_or(CharAttribute::with_fore_color(color)); // prepare a string with the grade // normally this is not indicated as it would allocate memory // everytime the paint method is called let t = format!("{}", self.grade); // print the string in the surface surface.write_string(0, 0, &t, a, false); } } }