Getting started

In this chapter we will introduce AppCUI framework and write a first (very simplistic) application in Rust.

What is AppCUI

AppCUI is a cross-platform TUI (Text User Interface / Terminal User Interface) / CUI (Console User Interface) framework designed to allow quick creation TUI/CUI based applications. AppCUI has a lot of out-of-the-box controls (such as buttons, checkboxes, radioboxes, window, tab cotrols, lists, comboboxes, etc), and can also provide quick macros to create custom controls.

The core of AppCUI is written completely in Rust and is designed to be fast and efficient. It is based on a handle-based system, where each control is represented by a handle. This allows for easy manipulation of controls and their properties.

Installation

To install AppCUI just link it directly from cargo.toml as follows:

[dependencies]
appcui = <version>

then you can use the following import in your code:

use appcui::prelude::*;

First Application

Let's start by building a simple window that prints Hello World on the screen.

Firts, make sure that you have the following dependency added in your project cargo.toml file:

[dependencies]
appcui = <version>

Then, replace your main.rs with the following snippet:

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    let mut win = Window::new("First Window", Layout::new("d:c,w:30,h:9"), window::Flags::None);
    win.add(Label::new("Hello World !",Layout::new("d:c,w:13,h:1")));
    app.add_window(win);
    app.run();
    Ok(())
}

or using macros to compact the code:

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    let mut win = window!("'First Window',d:c,w:30,h:9");
    win.add(label!("'Hello World !',d:c,w:13,h:1"));
    app.add_window(win);
    app.run();
    Ok(())
}

After compiling and executing this code you should see something like this:

Remarks: Keep in mind that depending on your terminal and other settings this image might look differently.

Basic Concepts

In this chapter we will discuss about the basic concepts of AppCUI

  • Application
  • Terminals
  • Screen area and sizes
  • Surface
  • Input

Application

An application in AppCUI is the context that holds all of the framework data together (it keeps all controls, passes messages between controls, manages terminals and system events). There can be only one application per program that uses AppCUI (this is enforced by the framework: subsequenct attempts to create an application will fail).

To create an application three APIs can be used:

  1. App::new(). This will create an application and chose the best fit terminal available on the current operating system. The result of new() method is a Builder object that can further be used to configure how the terminal looks like.

  2. App::with_backend(backend_type). This will create Builder object, but you will chose the backend to be used instead of having one chosed for you automatically. You can check more on backends availability and types on section Backends

  3. App::debug(width, height, script). This is designed to help you with unit testing (see more on this way of initializing AppCUI on section Debug scenarios)

Example (using the default backend):

let mut a = App::new().build().expect("Fail to create an AppCUI application");

Example (using the windows console backend):

let mut a = App::with_backend(apcui::backend::WindowsConsole)
                 .build()
                 .expect("Fail to create an AppCUI application with WindowsConsole backend");

Builder

Using App::new or App::with_backend creates a builder object that can further be used set up how the application will be constructed. For example, you can change the terminal size, colors, font, etc using this object. Keep in mind that not all settings apply for each terminal, and using the wrong configuration might led to an initialization error. Curently, the Builder supports the following methods:

  • .size(terminal_size) to set up a terminal size
  • .title(terminal_title) to set up a terminal title
  • .desktop(custom_desktop) if you want to use a custom desktop instead of the default one
  • .single_window() if you want a single window application
  • .menu_bar() to enable the application top menu bar
  • .command_bar() to enable the application command bar
  • .theme(custom_theme) to set up a custom theme or another predefined theme. Read more on themes in section Themes
  • .timers_count(count) to set up the number of timers that can be used in the application (if not specified the default value is 4)
  • .log_file(path,append) to set up a log file where logs will be displayed. This option will only be valid in debug mode. Once the file was specified, any call to log! macro will be recorded in that file.

After setting up the configuration for an application, just call the build() method to create an application. This methods returns a result of type Result<App,Error> from where the appcui application can be obtained via several methods such as:

  • unwrap() or expect(...) methods
  • ? opertor
  • if let construct

A typical example of using this settings is as follows:

let mut a = App::new().size(Size::new(80,40))       // size should be 80x25 chars
                      .menu_bar()                   // top menu bar should be enabled
                      .command_bar()                // command bar should be enabled
                      .log_file("debug.log", false) // log into debug.log
                      .build()
                      .expect("Fail to create an AppCUI application");

Errors

If the .build() method from the Builder object fails, an error is returned. You can use .kind member to identify the type of error. Curently, the following error class are provided:

  • ErrorKind::InitializationFailure a failure occured when initializing the backend API (this is usually due to some OS constranits).
  • ErrorKind::InvalidFeature an invalid feature (configuration option) that is not compatible with the current terminal was used. For example, an attemp to set up DirectX for NCurses backend will be invalid.
  • ErrorKind::InvalidParameter a valid feature but with invalid parameters was used. For example, an attempt to instantiate a terminal with the size of (0x0) will trigger such an error.

To get a more detailed description of the Error, use the description() method from class Error just like in the next code snipped:

let result = App::new().size(Size::new(0,0)).build();
if let Err(error) = result {
    // we have an Error - let's print it
    println!("Fail to instantiate AppCUI");
    println!("Error: {}",error.description());
}

Execution flows

Usually, each AppCUI program consists in the following steps:

  1. create an application
  2. add on or multiple windows to that application (to do this use the add_window method from struct App)
  3. run the application via method run. This method consumes the object and as such you can not use the application anymore after this method ends.

A typical main.rs file that uses AppCUI framework looks like this:

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    // 1. build an application
    let mut app = App::new().build()?;
    // 2. add one or several windows
    app.add_window(/* a window */);
    // 3. run the aplication
    app.run();
    Ok(())
}

Debug scenarios

When using AppCUI and needing to test the interface, it is recommended to write the unit tests using App::debug(...) method. This method allows one to write a succesion of system events (mouse clicks, keys being pressed, etc) and validate if the output is the expected one. This succesion of command is considered an event script - form out of multiple commands, each command written on a line. A command can have parameters. You can also use // to comment a command.

General format for a script

Command-1(param1,param2,param3)
Command-2()
// comment

Remarks:

  • App::debug(...) will panic if the script is incorect (a command is not valid, the number of parameters is incorect, etc).
  • AppCUI allows only one instance at one time (this is done via a mutex object). If you have multiple unit test and you try to run them with cargo test command, you might get an error as cargo might try to use multiple threads to do this and it is likely that one thread might try to start an AppCUI application while another one is already running on another thread. The solution in this case is to run the tests using a single thread:
cargo test -- --test-threads=1

Mouse related commands are a set of commands that simulate various mouse events

CommandPurpose
Mouse.Hold(x,y,button)simulates an event where the mouse button is being pressed while the mouse is located at a specific position on screen. The parameters x and y are a screen position, while the parameter button is one of left, right or center
Mouse.Release(x,y,button)simulates the release of the mouse buttons while the mouse is located at a specific screen position. The parameters x and y are a screen position, while the parameter button is one of left, right or center
Mouse.Click(x,y,button)simulates a click (hold and release). It is equivalent to
- Mouse.Hold(x,y,button)
- Mouse.Release(x,y)
Mouse.DoubleClick(x,y,button)simulates a double-click (for a specific button)
Mouse.Move(x,y)simulates the movement of a mouse to coordonates (x,y). No mouse button are being pressed.
Mouse.Drag(x1,y1,x2,y2)simulates the movement of a mouse from (x1,y1) to (x2,y2) while the left button is being pressed
Mouse.Wheel(x,y,direction,times)simulates the wheel mouse being rotated into a direction (one of up, down, left, right) for a number of times. The times parameter must be biggen than 0.
CommandPurpose
Key.Pressed(key,times)where key parameter can be a key name or any combination of control key and a regular key such as
- Z (for pressin the Z key)
- Enter (for pressing the Enter key)
-Alt+T (Alt + T combination)
-Ctrl+Alt+F1 (Ctrl+Alt+F1 keys). The times parameter can be omited. If present it has to be bigger than 1
Key.TypeText(text)where text parameter is a text that is being typed.
Example: Key.TypeText('Hello world') will trigger the following keys to be pressed: H, e, l, l, o, Space, w, o, r, l and d
Key.Modifier(modifier)Simulates the pressing of a modifier key (such as Shift, Ctrl or Alt). The modifier parameter can be a combination between Alt, Ctrl, Shift separate by + or None if no modifier is changed.
For example: Key.Modifier(Alt+Ctrl) will simulate the pressing of Alt and Ctrl keys at the same time.

Usually the key parameter can have several forms:

  • key
  • modifier-1+key
  • modifier-1+modifier-2+key
  • modifier-1+modifier-2+modifier-3+key

where the list of all keys supported by this command is:

  • F-commands (F1 to F12)
  • Letters (A to Z) - with upper case
  • Numbers (0 to 9)
  • Arrows (Up, Down, Left, Right)
  • Navigation keys (PageUp, PageDown, Home, End)
  • Deletion and Insertions (Delete , Backspace, Insert)
  • White-spaces (Space, Tab)
  • Other (Enter, Escape)

and the list of modifiers consists in Shift, Ctrl and Alt.

CommandPurpose
Paint(staet_name)
or
Paint()
paints the current virtual screen into the current screen using ANSI codes and colors. This command also computes a hash over the current virtual screen and prints it. The state_name is a name can be used to reflect the current execution state. This is useful if multipl Paint command are being used and you need to differentiate between them.
Paint.Enable(value)enables or disables painting. value is a boolean value (true or false). If set to false all subsequent calls to command Paint will be ignored. By default, all paints are enabled.

System events

CommandPurpose
Resize(width,height)simulates a resize of the virtual terminal to the size represented by width and height parameters

Clipboard commands

CommandPurpose
Clipboard.SetText(text)sets a new text into a simulated clipboard. That text will be available to all controls if they want to paste it
Clipboard.Clear()clears the text from the simulated clipboard.

Validation commands

CommandPurpose
CheckHash(hash)checks if the hash computer over the current virtual screen is as expected. If not it will panic. This is useful for unit testing.
CheckCursor(x,y)checks if the cursor (caret) is at a specify position
CheckCursor(hidden)checks is the cursor (caret) is hidden (not visible). You cal also check this by using false instead of hidden
CheckClipboardText(text)checks to see if the clipboard if the clipboard contains a specific text. This method is used to validate if the Copy/Cut to clipboard command from a control worked properly
Error.Disable(value)enables or disables errors when testing the the hashes or cursor position. value is a boolean value (true or false). By default, errors are NOT disabled

Example

Let's consider a scenario where we want to test if moving a window with a mouse works as expected. For this we will create a test function, with the following code:

#[test]
fn check_if_window_can_be_moved() {
    let script = "
        Paint('initial state')
        CheckHash(0xB1471A30B30F5C6C)
        Mouse.Drag(30,3,35,5)
        Paint('window was moved')
        CheckHash(0x419533D4BBEFE538)
    ";
    let mut a = App::debug(60, 10, script).build().unwrap();
    let w = Window::new("Title", Layout::new("d:c,w:20,h:5"), window::Flags::None);
    a.add_window(w);
    a.run();
}

Let's break the event script in pieces and see exactly what is supposed to happen:

  1. Paint('initial state') - this will print the virtual screen. It should look like the following (but with colors):
    +===================================================================+
    | Name  : initial state                                             |
    | Hash  : 0xB1471A30B30F5C6C                                        |
    | Cursor: Hidden                                                      |
    | ------------------------------------------------------------------- |
    |                                                                     | 11111111112222222222333333333344444444445555555555           |
    |                                                                     | 012345678901234567890123456789012345678901234567890123456789 |
    | ------------------------------------------------------------------- |
    | 0                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 1                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 2                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 3                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒╔════ Title ════[x]╗▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 4                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║                  ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 5                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║                  ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 6                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║                  ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 7                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒╚══════════════════╝▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 8                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 9                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | ------------------------------------------------------------------- |

We can inspect inspect if the position of the window is correct. We can also notice the hash compited for the entire virtual screen: 0xB1471A30B30F5C6C (this could help us do further checks).

  1. CheckHash(0xB1471A30B30F5C6C) - this compute the hash for the entire virtual screen and then check it againts the expected one. The usual scenario here is that we firs apply a Paint command, validate it, and them write the CheckHash command with the hash obtained from the Paint command. This way, if something changes to the logic/code of the program, the new hash will be different. If the hash for the virtual screen is not as expected the application will panic. If used in a test, this behavior will fail the test.

  2. Mouse.Drag(30,3,35,5) this command does the following:

    • moves the mouse to the (30,3) coordonate (over the title of the window)
    • click and hold the left mouse button
    • moves the mouse to a new position (35,5) (since we hold the mouse button, we expect the window to move as well)
    • releases the left mouse button
  3. Paint('window was moved') now we should see something like the following. Notice that indeed, the window was moved to a new position. We also have a new hash for the virtual screen: 0x419533D4BBEFE538

    +===================================================================+
    | Name  : window was moved                                          |
    | Hash  : 0x419533D4BBEFE538                                        |
    | Cursor: Hidden                                                      |
    | ------------------------------------------------------------------- |
    |                                                                     | 11111111112222222222333333333344444444445555555555           |
    |                                                                     | 012345678901234567890123456789012345678901234567890123456789 |
    | ------------------------------------------------------------------- |
    | 0                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 1                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 2                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 3                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 4                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 5                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒╔════ Title ════[x]╗▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 6                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║                  ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 7                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║                  ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 8                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║                  ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | 9                                                                   | ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒╚══════════════════╝▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ |
    | ------------------------------------------------------------------- |
  1. CheckHash(0x419533D4BBEFE538) - finally we check the new hash to see if it maches the one we expect.

Remark: using unit tests (while it works with the Paint command activated) might look strange on the actual screen (especially if all you need is to validate an example). As such, it is best that after one example such as the previous one was validated, to add another command at the begining of the script: Paint.Enable(false). This will not change the logic of the script, instead it will not print anything on the screen. As such, the final test function should look like this:

use appcui::prelude::*;

#[test]
fn check_if_window_can_be_moved() {
    let script = "
        Paint.Enable(false)
        Paint('initial state')
        CheckHash(0xB1471A30B30F5C6C)
        Mouse.Drag(30,3,35,5)
        Paint('window was moved')
        CheckHash(0x419533D4BBEFE538)
    ";
    let mut a = App::debug(60, 10, script).build().unwrap();
    let w = Window::new("Title", Layout::new("d:c,w:20,h:5"), window::Flags::None);
    a.add_window(w);
    a.run();
}

and its execution should produce an output similar to the next one:

running 1 test
test check_if_window_can_be_moved ... ok

Recording Events

Writing complex debug or unit-test scenarios might be a tedious task. However, it can be automated with the record events feature from AppCUI.

The first thing is to enable this feature (via cargo.toml) where you need to enable the feature EVENT_RECORDER for default building, like in the following snipped.

[features]
default = ["EVENT_RECORDER"]
DEBUG_SHOW_WINDOW_TITLE_BOUNDERIES = []
EVENT_RECORDER = []

Once you do this, any program that uses AppCUI will enable a special hotkey Ctrl+Alt+Space that will allow you to open a special configuration window similar to the one from the next image:

You can use this window to perform the following action:

  1. Add a new state (by typeing its name and pressing Enter) - this wil efectivelly add a new Paint and ChackHash commands
  2. Enable automated mode (via shortkey F9). Enabling auto record mode will efectively check whenever the screen changes because of the action performed and automatiicaly add a Paint and CheckHash commands. It will also filter out all other raw events (related to key strokes and mouse).
  3. Clear all events recorded up to this moment (via hotket F8)

The tipical way of using this feature is as follows:

  • enable the feature from cargo.toml
  • run you application
  • if you prefer to do this manually, perform certain action that change the state of the application, then press Ctrl+Alt+Space and in the configuration menu type the name of the new state and hit Enter.
  • if you prefer automated mode, press Ctrl+Alt+Space and enable automatic mode via F9 short key.
  • Once you finish doing your scenario, exit the application. At that point a file named events.txt will be dropped near your application. You can use its content as part of a unit test or for debug purposes.

Logging

AppCUI supports an internal logging mechanism that can be used to log messages to a file. The logging mechanism is available only in debug mode and can be used by calling the log! macro. The macro has the following syntax:

log!(TAG, format, ...)

To enable the logging mechanism, you need to specify a log file when creating the application. This can be done by calling the log_file method when AppCUI is initialized This method has two parameters: the path to the log file and a boolean value that specifies if the log file should be appended or overwritten.

AppCUI::new().log_file("debug.log", false)
             .build()
             .expect("Fail to create an AppCUI application");

Example

let x = 10;
log!("INFO", "The value of x is: {}", x);

Logging mechanism has zero overhead when the application is compiled in release mode. The logging mechanism is disabled in release mode and the log! macro will not generate any code.

Screen area and sizes

The screen in AppCUI is a 2D matrix of characters, with different widths (w) and heights (h).

It is important to note that each character is going to have the same size. For each character we have the following attributes:

  • Forenground color (the color of the character that we are printing)
  • Background color (the color of the character background)
  • Attributes: Bold, Italic, Underline, Boxed

The following collors are supported by AppCUI via Color enum from AppCUI::graphics module:

ColorEnum variantRGBColor
BlackColor::BlackRed=0, Green=0, Blue=0
Dark BlueColor::DarkBlueRed=0, Green=0, Blue=128
Dark GreenColor::DarkGreenRed=0, Green=128, Blue=0
Dark RedColor::DarkRedRed=128, Green=0, Blue=0
TealColor::TealRed=0, Green=128, Blue=128
MagentaColor::MagentaRed=128, Green=0, Blue=128
OliveColor::OliveRed=128, Green=128, Blue=0
SilverColor::SilverRed=192, Green=192, Blue=192
GrayColor::GrayRed=128, Green=128, Blue=128
BlueColor::BlueRed=0, Green=0, Blue=255
GreenColor::GreenRed=0, Green=255, Blue=0
RedColor::RedRed=255, Green=0, Blue=0
AquaColor::AquaRed=0, Green=255, Blue=255
PinkColor::PinkRed=255, Green=0, Blue=255
YellowColor::YellowRed=255, Green=255, Blue=0
WhiteColor::WhiteRed=255, Green=255, Blue=255

Besides this list, a special enuma variant Color::Transparent can be used to draw without a color (or in simple terms to keep the existing color). For example, if the current character has a forenground color Red writing another character on the same position with color Transparent will keep the color Red for the character.

Additionally, if the TRUE_COLORS feature is enabled, the following variant is supported:

  • Color::RGB(r, g, b) - this is a custom color that is defined by the RGB values.

REMARKS:

  1. Not all terminals support this exact set of colors. Further more, some terminals might allow changing the RGB color for certain colors in the pallete.
  2. Enabling TRUE_COLORS feature does not mean that the terminal supports 24-bit colors. It only means that the AppCUI framework will use 24-bit colors for the screen, but the terminal might still need to convert them to the terminal's color pallete.
  3. Enabling TRUE_COLORS feature will make the size of the Color enum to be 4 bytes (instead of 1 byte without this feature). If memory is a concern and you don't need true colors, it is recommended to NOT enable this feature.

The list of attributes available in AppCUI are described by CharFlags enum from AppCUI::graphics module and include the following flags:

  • Bold - bolded character
  • Underline - underlined character
  • Italic - italic character

These flags can be used with | operator if you want to combine them. For example: CharFlags::Bold | CharFlags::Underline means a character that is both bolded and underlined.

Character

As previously explained, a character is the basic unit of AppCUI (we can say that it is similar to what a pixel is for a regular UX system). The following method can be used to build a character:

#![allow(unused)]
fn main() {
pub fn new<T>(code: T, fore: Color, back: Color, flags: CharFlags) -> Character
}

where:

  • fore and back are characters colors (foreground and background)
  • code can be a character (like 'a' or 'b') or a value of type SpecialCharacter that can be used to quickly access special characters (like arrows). Any type of UTF-8 character is allowed.
  • flags are a set of flags (like Bold, Underline, ...) that can be used.

The list of all special characters that are supported by AppCUI (as described in the SpacialCharacter enum) are:

Box lines and corners

Variant
(appcui::graphics::SpecialCharacter enum)
Unicode codeVisual Representation
SpecialCharacter::BoxTopLeftCornerDoubleLine0x2554
SpecialCharacter::BoxTopRightCornerDoubleLine0x2557
SpecialCharacter::BoxBottomRightCornerDoubleLine0x255D
SpecialCharacter::BoxBottomLeftCornerDoubleLine0x255A
SpecialCharacter::BoxHorizontalDoubleLine0x2550
SpecialCharacter::BoxVerticalDoubleLine0x2551
SpecialCharacter::BoxCrossDoubleLine0x256C
SpecialCharacter::BoxTopLeftCornerSingleLine0x250C
SpecialCharacter::BoxTopRightCornerSingleLine0x2510
SpecialCharacter::BoxBottomRightCornerSingleLine0x2518
SpecialCharacter::BoxBottomLeftCornerSingleLine0x2514
SpecialCharacter::BoxHorizontalSingleLine0x2500
SpecialCharacter::BoxVerticalSingleLine0x2502
SpecialCharacter::BoxCrossSingleLine0x253C

Arrows

Variant
(appcui::graphics::SpecialCharacter enum)
Unicode codeVisual Representation
SpecialCharacter::ArrowUp0x2191
SpecialCharacter::ArrowDown0x2193
SpecialCharacter::ArrowLeft0x2190
SpecialCharacter::ArrowRight0x2192
SpecialCharacter::ArrowUpDown0x2195
SpecialCharacter::ArrowLeftRight0x2194
SpecialCharacter::TriangleUp0x25B2
SpecialCharacter::TriangleDown0x25BC
SpecialCharacter::TriangleLeft0x25C4
SpecialCharacter::TriangleRight0x25BA

Blocks

Variant
(appcui::graphics::SpecialCharacter enum)
Unicode codeVisual Representation
SpecialCharacter::Block00x20
SpecialCharacter::Block250x2591
SpecialCharacter::Block500x2592
SpecialCharacter::Block750x2593
SpecialCharacter::Block1000x2588
SpecialCharacter::BlockUpperHalf0x2580
SpecialCharacter::BlockLowerHalf0x2584
SpecialCharacter::BlockLeftHalf0x258C
SpecialCharacter::BlockRightHalf0x2590
SpecialCharacter::BlockCentered0x25A0
SpecialCharacter::LineOnTop0x2594
SpecialCharacter::LineOnBottom0x2581
SpecialCharacter::LineOnLeft0x258F
SpecialCharacter::LineOnRight0x2595

Other

Variant
(appcui::graphics::SpecialCharacter enum)
Unicode codeVisual Representation
SpecialCharacter::CircleFilled0x25CF
SpecialCharacter::CircleEmpty0x25CB
SpecialCharacter::CheckMark0x221A
SpecialCharacter::MenuSign0x2261
SpecialCharacter::FourPoints0x205E
SpecialCharacter::ThreePointsHorizontal0x2026

Other character constructors

Besides Character::new(...) the following constructors are also available:

  1. #![allow(unused)]
    fn main() {
     pub fn with_char<T>(code: T) -> Character
    }

    this is the same as calling:

    #![allow(unused)]
    fn main() {
    Character::new(code, Color::Transparent, Color::Transparent, CharFlags::None)
    }
  2. #![allow(unused)]
    fn main() {
     pub fn with_color(fore: Color, back: Color) -> Character
    }

    this is the same as calling:

    #![allow(unused)]
    fn main() {
    Character::new(0, fore, fore, CharFlags::None)
    }

    Note: Using the character with code 0 means keeping the existing character but chainging the colors and attributes.

Macro builds

You can also use char! macro to quickly create a character. The macro supports tha following positional and named parameters:

PositionParameterType
#1 (first)charactercharacter or string (for special chars)
#2 (second)foreground colorColor for foreground (special constants are accepted in this case - see below)
#3 (third)background colorColor for background (special constants are accepted in this case - see below)

and the named parameters:

NameTypeOptionalDescription
value or char or chStringYesThe character or the name or representation of a special character. If string characters ' or " are being used, the content of the string is analyzed. This is useful for when the character is a special token such as : or = or ,. If not specified a special character with value 0 is being used that translates as an invariant character (meaning that it will not modify the existing character, but only its color and attributes.)
code or unicodeHex valueYesThe unicode value of a character. Using this parameter will invalidate the previous parameter
fore or foreground or forecolor or colorColorYesThe foreground color of the character. If not specified it is defaulted to Transparent.
back or background or backcolorColorYesThe background color of the character. If not specified it is defaulted to Transparent.
attr or attributesFlagsYesOne of the following combination: Bold, Italic, Underline

The following values can be used as color parameters for foreground and background parameters:

ValuesColorEnum variantColor
blackBlackColor::Black
DarkBlue or dbDark BlueColor::DarkBlue
DarkGreen or dgDark GreenColor::DarkGreen
DarkRed or drDark RedColor::DarkRed
TealTealColor::Teal
MagentaMagentaColor::Magenta
OliveOliveColor::Olive
Silver or Gray75SilverColor::Silver
Gray or gray50GrayColor::Yellow
Blue or bBlueColor::Blue
Green or gGreenColor::Green
Red or rRedColor::Red
Aqua or aAquaColor::Aqua
PinkPinkColor::Pink
Yellow or yYellowColor::Yellow
White or wWhiteColor::White

For Transparent color you can use the following values: transparent, invisible or ?.

You can also specify special characters by either using their specific name from the enum SpecialChars or by using certaing adnotations as presented in the following table:

ValueVariant
(appcui::graphics::SpecialCharacter enum)
Visual Representation
up or /|\SpecialCharacter::ArrowUp
down or \|/SpecialCharacter::ArrowDown
left or <-SpecialCharacter::ArrowLeft
right or ->SpecialCharacter::ArrowRight
updown or up-downSpecialCharacter::ArrowUpDown
leftright or left-right or
<->
SpecialCharacter::ArrowLeftRight
/\SpecialCharacter::TriangleUp
\/SpecialCharacter::TriangleDown
<|SpecialCharacter::TriangleLeft
|>SpecialCharacter::TriangleRight
...SpecialCharacter::ThreePointsHorizontal

Character attributes

Sometimes, you might want to use a character with a specific color and attributes. For example, you might want to use a bolded character with a red color on a yellow background. This is in particular useful when building a theme where you just select the attributes and colors and then apply them to the characters. AppCUI provides a specific structure called CharAttribute that allows you to define colors and attributes for a character. To create a CharAttribute you can use the following methods:

#![allow(unused)]
fn main() {
impl CharAttribute {
    pub fn new(fore: Color, back: Color, flags: CharFlags) -> CharAttribute {...}
    pub fn with_color(fore: Color, back: Color) -> CharAttribute {...}
    pub fn with_fore_color(fore: Color) -> CharAttribute {...}
    pub fn with_back_color(back: Color) -> CharAttribute {...}
}
}

or the macro charattr! that works similar to char! but it returns a CharAttribute object. The macro supports tha following positional and named parameters:

PositionParameterType
#1 (second)foreground colorColor for foreground (special constants are accepted in this case - see below)
#2 (third)background colorColor for background (special constants are accepted in this case - see below)

and the named parameters:

NameTypeOptionalDescription
fore or foreground or forecolor or colorColorYesThe foreground color of the character. If not specified it is defaulted to Transparent.
back or background or backcolorColorYesThe background color of the character. If not specified it is defaulted to Transparent.
attr or attributesFlagsYesOne of the following combination: Bold, Italic, Underline

Examples

Example 1: Letter A with a Red color on an Yellow background:

#![allow(unused)]
fn main() {
Character::new('A',Color::Red,Color::Yellow,CharFlags::None)
}

or

char!("A,red,yellow")

or

char!("A,r,y")

Example 2: Letter A (bolded and underlined) with a White color on a Dark blue background:

#![allow(unused)]
fn main() {
Character::new('A',Color::White,Color::DarkBlue,CharFlags::Bold | CharFlags::Underline)
}

or

char!("A,fore=White,back=DarkBlue,attr=[Bold,Underline]")

or

char!("A,w,db,attr=Bold+Underline")

Example 3: An arrow towards left a Red color while keeping the current background:

#![allow(unused)]
fn main() {
Character::new(SpecialCharacter::ArrowLeft,Color::Red,Color::Transparent,CharFlags::None)
}

or

char!("ArrowLeft,fore=Red,back=Transparent")

or

char!("<-,red")

or

char!("<-,r")

Example 4: An arrow towards left a DarkGreen color, Bolded and Underlined while keeping the current background. We will use a CharAttribute for this example:

#![allow(unused)]
fn main() {
let attr = CharAttribute::new(Color::DarkGreen,Color::Transparent,CharFlags::Bold | CharFlags::Underline);  
let c = Character::with_attr(SpecialCharacter::ArrowLeft,attr);
}

or

let attr = charattr!("DarkGreen,Transparent,attr:Bold+Underline");
let c = Character::with_attr(SpecialCharacter::ArrowLeft,attr));

or

let attr = charattr!("dg,?,attr:Bold+Underline");
let c = Character::with_attr(SpecialCharacter::ArrowLeft,attr);

Surface

A surface is a two-dimensional array of Characters that can be displayed on the screen. It is the basic building block of the UI system. Surfaces can be created and manipulated using the Surface class.

A surface has the following properties:

  • a clipper area that restricts the drawing operations to a specific region of the surface
  • an origin point that is used as the reference point for all drawing operations
  • a cursor (that can be moved, enabled or disabled)
  • an array (vector) of characters that represent the content of the surface

Remarks: A screen is in fact a surface that covers the entire console visible space and it is created automatically when the application starts.

Creating a Surface

To create a new surface, you can use the method Surface::new() - with two parameters, width and height - that returns a new surface with the specified dimensions. Both width and height must be greater than zero and smaller than 10000. Any value outside this range will be clamped to the nearest valid value.

The surface will be filled with the space character ' ' with a White foreground and Black background. The surface will have the origin set to (0,0) and the clip area will be the entire surface. The cursor associated with the surface will be disabled.

#![allow(unused)]
fn main() {
use appcui::graphics::{Surface};
let mut surface = Surface::new(100, 50);
}

Remarks: Creating a surface is rarely needed, as the library will create the main screen surface automatically when the application starts and will provide a mutable reference to that surface whenever the on_paint event is called for a control.

Clip Area and Origin point

Every surface has a clip area and an origin point. The clip area restricts the drawing operations to a specific region of the surface. The origin point is used as the reference point for all drawing operations.

For example, if the clip area is set to (10,10,20,20) and the origin point is set to (5,5), then the drawing operations will be restricted to the area (15,15,25,25) of the surface.

The following methods can be used to manipulate the clip area and the origin point of a surface:

MethodDescription
set_origin(...)Sets the origin point of the surface
reset_origin()Resets the origin point
set_clip(...)Sets the clip area of the surface. This methods take 4 parameters (left, top, right and bottom)
set_relative_clip(...)Sets the clip area of the surface relative to the current clip area. This methods take 4 parameters (left, top, right and bottom)
reduce_clip_by(...)Reduces the clip area of the surface. This methods take 4 parameter (left margin, top margin, right margin and bottom margin)
reset_clip()Resets the clip area of the surface

Example:

#![allow(unused)]
fn main() {
use appcui::graphics::*;

let mut surface = Surface::new(100, 50);
// Set the origin point to (10,10)
surface.set_origin(10, 10);
// Set the clip area to (10,10,20,20)
surface.set_clip(10, 10, 20, 20);
// draw a border around the clip area
surface.draw_rect(
    Rect::new(0,0,9,9), // left,top,right,bottom relativ to origin
    LineType::Single,
    CharAttribute::with_color(Color::White, Color::DarkRed)
);
// reduce the clip area by 1 character on each side
// so that we will not draw over the border
surface.reduce_clip_by(1, 1, 1, 1);
// draw something else
// ...

/// finally, reset the clip area and origin point
/// to the entire surface
surface.reset_clip();
surface.reset_origin();
}

Cursor

Every surface has an associated cursor that can be moved, enabled or disabled. The cursor is used to indicate the current position where the next character will be drawn. Depending on the terminal, the cursor can be a blinking rectangle, a blinking underline or a blinking vertical line.

The following methods can be used to manipulate the clip area and the origin point of a surface:

MethodDescription
set_cursor(...)Sets the position of the cursor relativ to the origin point. If the cursor is within the clip area, it will be visible. Otherwise it will be hidden.
hide_cursor()Hides the cursor

Example:

#![allow(unused)]
fn main() {
use appcui::graphics::{Surface};

let mut surface = Surface::new(100, 50);
surface.set_cursor(10, 10);
}

Drawing characters on a Surface

The most basic operation that can be performed on a surface is drawing a character at a specific position. This allows for more complex operations like drawing text, lines, rectangles, etc. to be built on top of it.

A surface has the following methods that can be used to manipulate characters and how they are drown on the surface:

MethodDescription
write_char(...)Writes a character at the specified position. If the position is outside the clip area, the character will not be drawn.
char(...)Returns the current character at the specified position or None if the position is outside the clip area or invalid.
clear(...)Clears/Fills the entire clip area with the specified character. If the clip area is not visible, the surface will not be cleared.

Example:

#![allow(unused)]
fn main() {
use appcui::graphics::*;

let mut surface = Surface::new(100, 50);
// Set the origin point to (10,10)
surface.set_origin(10, 10);
// Set the clip area to (10,10,20,20)
surface.set_clip(10, 10, 20, 20);
// Clear the clip area
surface.clear(Character::new('*', Color::Silver, Color::Black, CharFlags::None))
// write a character at position (5,5) relativ to the origin
// point (10,10) => the character will be drawn at position (15,15)
surface.write_char(5, 5, Character::new('A', Color::Yellow, Color::DarkBlue, CharFlags::None));
}

Lines

Drawing lines is a common operation when building a UI. In AppCUI there are two methods that cen be used to draw lines (vertical and horizontal) on a surface.

  • use special characters to draw the line (like single lines, double lines, etc) that are designed to be used in this context
  • use a generic character to draw the line

Using special characters to draw lines

The following methods can be used to draw lines on a surface using special characters:

MethodDescription
draw_horizontal_line(...)Draws a horizontal line on the surface. The line will be drawn from left to right.
draw_vertical_line(...)Draws a vertical line on the surface. The line will be drawn from top to bottom.
draw_horizontal_line_with_size(...)Draws a horizontal line on the surface with a specific length. The line will be drawn from left to right, starting from a given point and a width.
draw_vertical_line_with_size(...)Draws a vertical line on the surface with a specific length. The line will be drawn from top to bottom, starting from a given point and a width.

These methods take a parameter line_type that specifies the type of line that will be drawn. The line type can be one of the following values:

ValueCharacters being used
Single, , , , , , , , , ,
Double, , , , , , , , , ,
SingleThick, , , , , , , , , ,
Border, ,
Ascii\|, -, +
AsciiRound\|, -, +, \\ , \/
SingleRound, , , , ,

Example:

#![allow(unused)]
fn main() {
use appcui::graphics::{Surface, LineType, CharAttribute, Color};

let mut surface = Surface::new(100, 50);
surface.draw_vertical_line(10, 10, 20, 
                            LineType::Single, 
                            CharAttribute::with_color(Color::White, Color::Black));
}

Using a generic character to draw lines

The following methods can be used to draw lines on a surface using a generic character:

MethodDescription
fill_horizontal_line(...)Fills a horizontal line on the surface. The line will be filled from left to right with a provided Character
fill_vertical_line(...)Fills a vertical line on the surface. The line will be filled from top to bottom with a provided Character
fill_horizontal_line_with_size(...)Fills a horizontal line on the surface with a specific length. The line will be filled from left to right with a provided Character
fill_vertical_line_with_size(...)Fills a vertical line on the surface with a specific length. The line will be filled from top to bottom with a provided Character

Example:

#![allow(unused)]
fn main() {
use appcui::graphics::{Surface, CharAttribute, Color, Character};

let mut surface = Surface::new(100, 50);
let c = Character::new('=', Color::White, Color::Black, CharFlags::None);
surface.fill_horizontal_line(10, 10, 20, c);
}

Rectangles

Rectangles are the most basic shape you can draw on a surface. They are defined by a position and a size. The position is the top-left corner of the rectangle, and the size is the width and height of the rectangle.

In AppCUI a rectangle is defined based on the following structure:

#![allow(unused)]
fn main() {
#[derive(Copy, Clone, Debug)]
pub struct Rect {
    left: i32,
    top: i32,
    right: i32,
    bottom: i32,
}
}

A rectangle can be created using the following methods:

  1. Rect::new(left, top, right, bottom) - creates a new rectangle based on the provided coordinates.
  2. Rect::with_size(left, top, width, height) - creates a new rectangle based on the provided position and size.
  3. Rect::with_alignament(x, y, width, height, align) - creates a new rectangle based on the provided position, size and alignment.
  4. Rect::with_point_and_size(point, size) - creates a new rectangle based on the provided point and size.

The alignament in the third method is defined as follows:

#![allow(unused)]
fn main() {
#[repr(u8)]
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum Alignament {
    TopLeft = 0,
    Top,
    TopRight,
    Right,
    BottomRight,
    Bottom,
    BottomLeft,
    Left,
    Center,
}
}
AlignamentDecriptionPreview
TopLeft(X,Y) represents the top-left corner of the rectangleTopLeft
Top(X,Y) represents the top-center of the rectangleTop
TopRight(X,Y) represents the top-right corner of the rectangleTopRight
Right(X,Y) represents the right-center of the rectangleRight
BottomRight(X,Y) represents the bottom-right corner of the rectangleBottomRight
Bottom(X,Y) represents the bottom-center of the rectangleBottom
BottomLeft(X,Y) represents the bottom-left corner of the rectangleBottomLeft
Left(X,Y) represents the left-center of the rectangleLeft
Center(X,Y) represents the center of the rectangleCenter

To draw a rectangle on a surface, you can use the following methods:

MethodDescription
draw_rect(...)Draws a rectangle on the surface by providing a Rect object, a line type and a character attribute.
fill_rect(...)Fills a rectangle on the surface by providing a Rect object and a character attribute.

Example:

#![allow(unused)]
fn main() {
use appcui::graphics::*;

let mut surface = Surface::new(100, 50);
let r = Rect::new(10, 10, 20, 20);
// fill the rectangel with spaces (dark blue background)
surface.fill_rect(r, Character::new(' ', Color::White, Color::DarkBlue, CharFlags::None));
// draw a border around the rectangle (white on black)
surface.draw_rect(r, LineType::Single, CharAttribute::with_color(Color::White, Color::Black));
}

Text

Writing text on a surface is a common task in GUI programming, that can be achieved using the following methods:

  1. write_string(...) - writes a string (String or &str) on the surface starting from a specific position, color and character attribute.
  2. write_ascii(...) - similar to write_string, but it writes only ASCII characters.
  3. write_text(...) - a more complex method that allows alignament, wrapping and text formatting.

Write a string

The write_string(...) method writes a string on the surface starting from a specific position. The method has the following signature:

#![allow(unused)]
fn main() {
pub fn write_string(&mut self, 
                    x: i32, 
                    y: i32, 
                    text: &str, 
                    attr: CharAttribute, 
                    multi_line: bool)
}

The multi-line parameter specifices if the text should interpret new line characters as a new line or not. if set to false the code of this method is optimized to write the text faster. The text will be written from left to right, starting from the specified position (x,y).

Example:

#![allow(unused)]
fn main() {
use appcui::graphics::{Surface, CharAttribute, Color};

let mut surface = Surface::new(100, 50);
surface.write_string(10, 10, 
                    "Hello World!", 
                    CharAttribute::with_color(Color::White, Color::Black), 
                    false);
}

Write an ASCII string

The write_ascii(...) method writes an ASCII string on the surface starting from a specific position. The method has the following signature:

#![allow(unused)]
fn main() {
pub fn write_ascii(&mut self, 
                   x:i32, 
                   y:i32, 
                   ascii_buffer: &[u8], 
                   attr: CharAttribute, 
                   multi_line: bool)
}

The multi-line parameter specifices if the text should interpret new line characters as a new line or not. if set to false the code of this method is optimized to write the text faster. The text will be written from left to right, starting from the specified position (x,y).

Example:

#![allow(unused)]
fn main() {
use appcui::graphics::{Surface, CharAttribute, Color};

let mut surface = Surface::new(100, 50);
surface.write_ascii(10, 10,
                   b"Hello World!",
                   CharAttribute::with_color(Color::White, Color::Black),
                   false);
}

Write a formatted text

In some cases, you may need to write a text that is formatted in a specific way (like alignament, wrapping, etc). The write_text(...) method allows you to do this. The method has the following signature:

#![allow(unused)]
fn main() {
pub fn write_text(&mut self, text: &str, format: &TextFormat)
}

where the TextFormat structure can be created using the TextFormatBuilder in the following way:

MethodDescription
new()Creates a new TextFormatBuilder object
position(...)Sets the position where the text will be written (X and Y axes)
attribute(...)Sets the character attribute for the text (forecolor, backcolor and other attributes)
hotkey(...)Sets the hotkey attribute and position for the text (if any)
align(...)Sets the text alignament (left, right, center)
wrap_type(...)Sets the text wrapping type of the code (WrapType enum)
chars_count(...)Sets the number of characters in the text (this is useful to optimize several operations especially if this value is aready known)
build()Builds the TextFormat object

Example:

#![allow(unused)]
fn main() {
use appcui::graphics::{Surface, CharAttribute, Color, TextFormatBuilder, WrapType};
let format = TextFormatBuilder::new()
    .position(10, 10)
    .attribute(CharAttribute::with_color(Color::White, Color::Black))
    .align(Alignment::Center)
    .wrap_type(WrapType::Word(20))
    .build();
surface.write_text("Hello World!", &format);
}

Once a TextFormat object is created, you can modify it and use it using the following methods:

MethodDescription
set_position(...)Sets the position where the text will be written (X and Y axes)
set_attribute(...)Sets the character attribute for the text (forecolor, backcolor and other attributes)
set_hotkey(...)Sets the hotkey attribute and position for the text (if any)
clear_hotkey()Clears the hotkey attribute from the text
set_align(...)Sets the text alignament (left, right, center)
set_wrap_type(...)Sets the text wrapping type of the code (WrapType enum)
set_chars_count(...)Sets the number of characters in the text (this is useful to optimize several operations especially if this value is aready known)

The WrapType enum is defined as follows:

#![allow(unused)]
fn main() {
pub enum WrapType {
    WordWrap(u16),
    CharacterWrap(u16),
    MultiLine,
    SingleLine,
    SingleLineWrap(u16),
}
}

with the following meaning:

MethodMulti-lineDescription
WordWrap(width)YesWraps the text around a specific width not separating words. The text will be printed on the next line if a new line character (CR or LF or combinations) is encountered or if the current word if printed will be outside the specfied width.
CharacterWrap(width)YesWraps the text around a specific width separating words. The text will be printed on the next line if a new line character (CR or LF or combinations) is encountered or when the position of the current character is outside the specified width.
MultiLineYesThe text will be printed on the next line only if a new line character (CR or LF or combinations) is encountered.
SingleLineNoThe text will be printed on the same line, ignoring any new line characters.
SingleLineWrap(width)NoThe text will be printed on the same line, but it will be wrapped around a specific width. One the width is reach, the printing stops.

Let's consider the following string Hello World!\nFrom AppCUI. This text will be printed as follows:

WrapTypeResult
WrapType::WordWrap(10)Hello
World!

From
AppCUI
WrapType::WordWrap(20)Hello World!
From AppCUI
WrapType::CharacterWrap(10)Hello Worl
d!
From AppC
UI
WrapType::CharacterWrap(20)Hello World!
From AppCUI
WrapType::CharacterWrap(5)Hello
Worl
d!
From
AppCU
I
WrapType::MultiLineHello World!
From AppCUI
WrapType::SingleLineHello World!\nFrom AppCUI
WrapType::SingleLineWrap(5)Hello
WrapType::SingleLineWrap(10)Hello Worl
WrapType::SingleLineWrap(20)Hello World!\nFrom Ap

Images

While AppCUI is developed for CLI usage, it can still use images to some degree, meaning that it can store an images as an array of pixels and it has various methods to represent it using characters and combination of existing colors.

To create an image, use the class Image with the following construction methods:

  1. Image::new(width,height) creates an image with a specific size. That image will be filled with a transparent pixel that you can later change
  2. Image::with_str(...) creates a 16 color image based on a string representation.

Methods

Once an image is create you can use the following methods to manipulate it:

MethodPurpose
clear(...)Fills the entire image with a specific pixel
pixel(...)Provide the pixel from a specific coordonate in the image or None otherwise
set_pixel(...)Sets the pixel from a specific coordonate from the image
width()The width of the image in pixels
height()The height of the image in pixels
size()The size (width and height) of te image in pixels
render_size(...)The size (in characters) needed for a surface object to allow the entire image to be painted. It requires both a scale and a rendering method to compute

Pixel

A pixel is a simple structure defined as follows:

#[derive(Copy, Clone, Debug, PartialEq, Default)]
pub struct Pixel {
    pub red: u8,
    pub green: u8,
    pub blue: u8,
    pub alpha: u8,
}

You can create a pixel in the following way:

  1. using direct construction:
    let px = Pixel { red:10, green: 20, blue:30, alpha: 255 };
    
  2. using the .new(...) constructor:
    let px = Pixel::new(10, 20, 30, 255);
    
  3. using the .with_rgb(...) constructor:
    let px = Pixel::with_rgb(10, 20, 30);
    
  4. using the .with_color(...) constructor:
    let px = Pixel::with_color(Color::Aqua);
    
  5. using the From implementation from an u32 (in an ARGB format - Alpha Red Green Blue).
    let px = Pixel::from(0xFF005090u32);
    
  6. using the Default implementation (this will create a trnsparent pixel where Red=0, Green=0, Blue=0 and Alpha=0)
    let px = Pixel::default();
    

Usage

A typical way to create an image is as follows:

  1. create a new Image object
  2. optionally, fill the entire image with a differnt pixel than the default one
  3. use .set_pixel(...) method to fill the image. At this point aditional crates that can load an image from a file can be used to transfer the content of that image into this object.

The following example draws a horizontal Red line on a Blue background image of size 32x32:

let mut img = Image::new(32,32);
img.clear(Pixel::with_color(Color::Blue));
for i in 5..30 {
    img.set_pixel(i,10,Pixel::with_color(Color::Red));
}

Building from a string

A more common usage is to build a small image from a string that specifies colors for each pixel. The format in this case is as follows:

  • each line is enclosed between two characters |
  • outside of these characters any other character is being ignored (usually you add spaces or new lines to align the text)
  • each line must have the same with (in terms of the number of characters that are located between | characters )

for example, a 5x5 image will be represented as follows:

let string_representation = r#"
      |.....|
      |.....|
      |.....|
      |.....|
      |.....|
"#;

Within the space between the characters | the following characters have the a color association:

CharacterEnum variantRGBColor
0
(space)
.(point)
Color::BlackRed=0, Green=0, Blue=0
1
B(capital B)
Color::DarkBlueRed=0, Green=0, Blue=128
2
G(capital G)
Color::DarkGreenRed=0, Green=128, Blue=0
3
T(capital T)
Color::TealRed=0, Green=128, Blue=128
4
R(capital R)
Color::DarkRedRed=128, Green=0, Blue=0
5
M or m
Color::MagentaRed=128, Green=0, Blue=128
6
O or o
Color::OliveRed=128, Green=128, Blue=0
7
S(capital S)
Color::SilverRed=192, Green=192, Blue=192
8
s(lower s)
Color::GrayRed=128, Green=128, Blue=128
9
b(lower b)
Color::BlueRed=0, Green=0, Blue=255
g(lower g)Color::GreenRed=0, Green=255, Blue=0
r(lower r)Color::RedRed=255, Green=0, Blue=0
A or a
t(lower t)
Color::AquaRed=0, Green=255, Blue=255
P or pColor::PinkRed=255, Green=0, Blue=255
Y or yColor::YellowRed=255, Green=255, Blue=0
W or wColor::WhiteRed=255, Green=255, Blue=255

So ... to create an image of a red heart ♥ you will need to create the folowing string:

let heart = r#"
    |..rr.rr..|
    |.rrrrrrr.|
    |.rrrrrrr.|
    |..rrrrr..|
    |...rrr...|
    |....r....|
"#;
let img = Image::with_str(heart);

Rendering images

AppCUI framework relies on characters. As such, an image can not be displayes as it is. However, there is one method in the Surface object that be used to aproximate an image:

impl Surface {
    // other methods
    pub fn draw_image(&mut self, x: i32, 
                                y: i32, 
                                image: &Image, 
                                rendering_method: image::RendererType, 
                                scale_method: image::Scale
                    ) { ... }
}

This method attempts to draw an image using characters and the available colors. The following rendering methods are available:

  • SmallBlocks
  • LargeBlocks64Colors
  • GrayScale
  • AsciiArt

Let's consider an image of Cuddly Ferris and see how it will be displayed using different rendering methods:

MethodsResult
SmallBlocks
LargeBlocks64Colors
GrayScale
AsciiArt

The supported scales (from the enume image::Scale):

  • Scale::NoScale => 100%
  • Scale::Scale50 => 50%
  • Scale::Scale33 => 33%
  • Scale::Scale25 => 25%
  • Scale::Scale20 => 20%
  • Scale::Scale10 => 10%
  • Scale::Scale5 => 5%

Input

AppCUI allows input from:

  • Keyboard
  • Mouse

The input is received via OnKeyPressed and OnMouseEvents traits and they are usually used when designing a Custom Control.

Mouse

Mouse events are received through the trait OnMouseEvent defined as follows:

pub trait OnMouseEvent {
    fn on_mouse_event(&mut self, event: &MouseEvent) -> EventProcessStatus {
        EventProcessStatus::Ignored
    }
}

where MouseEvent is defined as follows:

#[derive(Copy, Clone, Debug, PartialEq)]
pub enum MouseEvent {
    Enter,
    Leave,
    Over(Point),
    Pressed(MouseEventData),
    Released(MouseEventData),
    DoubleClick(MouseEventData),
    Drag(MouseEventData),
    Wheel(MouseWheelDirection)
}

where MouseEventData is defined as follows:

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct MouseEventData {
    pub x: i32,
    pub y: i32,
    pub button: MouseButton,
    pub modifier: KeyModifier
}

and MouseButton is an enum with the following values:

  • Left - indicates that the left mouse button was pressed
  • Right - indicates that the right mouse button was pressed
  • Center - indicates that the center mouse button was pressed
  • None - indicates that no button was pressed

MouseWheelDirection is an enum with the following values:

  • Up - indicates that the mouse wheel was rotated up
  • Down - indicates that the mouse wheel was rotated down
  • Left - indicates that the mouse wheel was rotated left
  • Right - indicates that the mouse wheel was rotated right
  • None - indicates that the mouse wheel was not rotated

These events are reflect the following actions:

  • MouseEvent::Enter - the mouse cursor entered the control
  • MouseEvent::Leave - the mouse cursor left the control
  • MouseEvent::Over(Point) - the mouse cursor was moved over the control and it is now at a the specified point
  • MouseEvent::Pressed(MouseEventData) - a mouse button was pressed over the control
  • MouseEvent::Released(MouseEventData) - a mouse button was released. If a mouse button was pressed over the control and the control can receive input, then all of the following mouse events will be send to the control (even if the mouse cursor is outside the control) until the mouse button is released.
  • MouseEvent::DoubleClick(MouseEventData) - a mouse button was double clicked over the control
  • MouseEvent::Drag(MouseEventData) - a mouse button was pressed over the control and the mouse cursor was moved while keeping the button pressed
  • MouseEvent::Wheel(MouseWheelDirection) - the mouse wheel was rotated

Usage

The events are generated by the system and are sent to the control that has the focus. The control can choose to ignore the event or to process it. The OnMouseEvent trait is usually used when designing a Custom Control.

A typical implementation of the OnMouseEvent trait looks like this:

impl OnMouseEvent for <MyControl> {
    fn on_mouse_event(&mut self, event: &MouseEvent) -> EventProcessStatus {
        // check the key
        match event {
            MouseEvent::Enter => {
                // the mouse cursor entered the control
            },
            MouseEvent::Leave => {
                // the mouse cursor left the control
            },
            MouseEvent::Over(point) => {
                // the mouse cursor is over the control
            },
            MouseEvent::Pressed(data) => {
                // a mouse button was pressed over the control
            },
            MouseEvent::Released(data) => {
                // a mouse button was released over the control
            },
            MouseEvent::DoubleClick(data) => {
                // a mouse button was double clicked over the control
            },
            MouseEvent::Drag(data) => {
                // a mouse button was pressed over the control and the mouse cursor was moved while keeping the button pressed
            },
            MouseEvent::Wheel(direction) => {
                // the mouse wheel was rotated
            }
        }
    }
}

Key modifiers

The MouseEventData structure contains a modifier field that indicates the key modifiers that were pressed when the mouse event occurred. This can be used to perform different actions based on the key modifiers (e.g., pressing Ctrl while clicking the mouse button can have a different effect than just clicking the mouse button).

impl OnMouseEvent for <MyControl> {
    fn on_mouse_event(&mut self, event: &MouseEvent) -> EventProcessStatus {
        match event {
            MouseEvent::Drag(data) => {
                if data.modifier.contains(KeyModifier::Ctrl) {
                    // the mouse button was pressed while the Ctrl key was also pressed
                } else {
                    // the mouse button was pressed without the Ctrl key being pressed 
                }
            },
            _ => {
                EventProcessStatus::Ignored
            }
        }
    }
}

Keyboard

Keyboard events are received through the trait OnKeyPressed defined as follows:

pub trait OnKeyPressed {
    fn on_key_pressed(&mut self, key: Key, character: char) -> EventProcessStatus {
        // do something depending on the key pressed
    }
}

This method has two parameters:

  1. the key parameter (that provides information about the code of the key that was pressed and its modifiers)
  2. the character (when this is the case). This is usually when you want insert text intro a control (for example in case of a TextField)

Key

A key in AppCUI is defined as follows:

#[derive(Copy, Clone, PartialEq, Debug)]
pub struct Key {
    pub code: KeyCode,
    pub modifier: KeyModifier,
}

where:

  • code is an enum that indicates a code for the key that was pressed and it includes:
    • F-commands (F1 to F12)
    • Letters (A to Z) - with apper case
    • Numbers (0 to 9)
    • Arrows (Up, Down, Left, Right)
    • Navigation keys (PageUp, PageDown, Home, End)
    • Deletion and Insertions (Delete , Backspace, Insert)
    • White-spaces (Space, Tab)
    • Other (Enter, Escape)
  • modifier can be one of the following (including combination between them):
    • Shift
    • Ctrl
    • Alt

The crete a key use:

  1. Key::new(code, modifier) - for example:
    let k = Key::new(KeyCode::F1,KeyModifier::Alt | KeyModifier::Ctrl);
    let k2 = Key::new(KeyCode::Enter, KeyModifier::None);
    
  2. using From implementation:
    let k = Key::from(KeyCode::F1);
    // this is equivalent to Key::new(KeyCode::F1, KeyModifier::None);
    
  3. key! macro - this can be used to create a key:
    let k1 = key!("F2");
    let k2 = key!("Enter")
    let k3 = key!("Alt+F4")
    let k4 = key!("Ctrl+Alt+F")
    let k5 = key!("Ctrl+Shift+Alt+Tab")
    

Usage

The usual usage is via OnKeyPressed::on_key_pressed(...) method as follows:

impl OnKeyPressed for <MyControl> {
    fn on_key_pressed(&mut self, key: Key, character: char) -> EventProcessStatus {
        // check the key
        match key.value() {
            // check various key combinations
        }
        // check the character
        match character {
            // do something with the character
        }
        
        EventProcessStatus::Ignored
    }
}

The following example checks the arrow keys for movement and ignores the rest of the keys:

impl OnKeyPressed for <MyControl> {
    fn on_key_pressed(&mut self, key: Key, character: char) -> EventProcessStatus {
        match key.value() {
            key!("Left") => { 
                /* move left */ 
                return EventProcessStatus::Processed;
            }
            key!("Right") => { 
                /* move right */ 
                return EventProcessStatus::Processed;
            }
            key!("Ctrl+Left") => { 
                /* Move to begining */ 
                return EventProcessStatus::Processed;
            }
            key!("Ctrl+Right") => { 
                /* Move to end */ 
                return EventProcessStatus::Processed;
            }
            _ => {
                return EventProcessStatus::Ignored;
            }
        }
    }
}

Clipboard

Access to clipboard can be done via a special non-instantiable class called Clipboard. This class provides the basic functionality to work with the clipboard, as follows:

MethodPurpose
Clipboard::clear()Cleans the content of the clipboard
Clipboard::set_text(...)Sets a new text to the clipboard
Clipboard::has_text()Returns true if the clipboard contains a text or false otherwise
Clipboard::text()Returns an option with a String that contains the text that is stored in the clipboard

Access to clipboard depends on the type of backend you are using (e.g. WindowsConsole backend relies on low level APIs like OpenClipboard, GetClipboardData, EmptyClipboard, SetClipboardData and CloseClipboard). As such, you will only be able to use this class after the application has been initialized (e.g. after a call to App::new()). Calling static methods from this class before that moment will have no action.

Example

A typical example on how to use the clipboard looks like the following:

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    // fist initialize the application
    let mut a = App::new().build()?;
    // now use the clipboard
    if let Some(text) = Clipboard::text() {
        // do something with the text from the clipboard
    }
    // now set a new text into the clipboard:
    Clipboard::set_text("Hello world");
    Ok(())
}

Remarks: Keep in mind that calling Clipboard::text() will always create a String object containing the content of the clipboad. If you just want to check if something exists in a clipboard (for example to enable/disable some menu items - use Clipboard::has_text() method instead).

Limitations

Depending on the type of terminal, the clipboard comes with some limitations (for example in case of WindowsConsole backend, the clipboard can not store unicode characters that are not in WTF-16 format - within the range 0..0xFFFF).

Backends

AppCUI supports various backends (but each one comes with advantages and drawbacks). A backend is the terminal that takes the information (characters) from the virtual screen of AppCUI and displays them.

Each backend supported by AppCUI has the following properties:

  • Output rendering - each character from the AppCUI surface is display on the screen
  • Input reading - the backend is capable of identifying keyboard and mouse events and convert them to internal AppCUI events
  • Clipboard support - the backend interacts with the OS and provides functionality for Copy / Cut / Paste based on OS-es API

The following backends are supported:

  1. Windows Console
  2. Windows VT (Virtual Terminal)
  3. NCurses
  4. Termios
  5. Web Terminal

Remarks: These types are available via appcui::backend::Type and can be used to initialize an application

#![allow(unused)]
fn main() {
let mut a = App::with_backend(apcui::backend::/*type*/).build()?;
}

where the appcui::backend::Type enum is defined as follows:

#![allow(unused)]
fn main() {
pub enum Type {
    #[cfg(target_os = "windows")]
    WindowsConsole,
    #[cfg(target_os = "windows")]
    WindowsVT,
    #[cfg(target_family = "unix")]
    Termios,
    #[cfg(target_os = "linux")]
    NcursesTerminal,
    #[cfg(target_arch = "wasm32")]
    WebTerminal,
}
}

OS Support

OSWindows ConsoleWindows VTNCursesTermiosWeb Terminal
WindowsYesYes---
Linux--YesYes-
Mac/OSX--YesYes-
Web----Yes

Display

DisplayWindows ConsoleWindows VTNCursesTermiosWeb Terminal
Colors16 (fore),16 (back)True colors16 (fore),16 (back)16 (fore),16 (back)16 (fore),16 (back)
Bold--Yes--
UnderlineYes-Yes-Yes
Italic-----
Character SetAscii,WTF-16Ascii,UTF-8Ascii,UTF-8Ascii,UTF-8Ascii,UTF-8
CursorYesYesYes-Yes

Keyboard

KeysWindows ConsoleWindows VTNCursesTermiosWeb Terminal
Alt+KeyYesYesWip-Yes
Shift+KeyYesYesYes-Yes
Ctrl+KeyYesYesYes-Yes
Alt+Shift+KeyYesYes---
Ctrl+Shift+KeyYesYes---
Ctrl+Alt+KeyYesYes---
Ctrl+Alt+Shift+KeyYesYes---
Alt pressedYesYes---
Shift pressedYesYes---
Ctrl pressedYesYes---

Mouse

Mouse eventsWindows ConsoleWindows VTNCursesTermiosWeb Terminal
ClickYesYesYesYesYes
Move & DragYesYesYesYesYes
WheelYesYesYes-Yes

System events

EventsWindows ConsoleWindows VTNCursesTermiosWeb Terminal
Console ResizeYesYesYes-Yes
Console closedYesYes--Yes

Other capabilities

CapabilitiesWindows ConsoleWindows VTNCursesTermiosWeb Terminal
Set dimensionYesYes--Yes
Set titleYesYes--Yes

Clipboard

AppCUI provides clipboard support for copying and pasting text. The clipboard functionality is available on the following backends:

BackendClipboard SupportAPI Used
Windows ConsoleYesWindows API
Windows VTYesWindows API
NCursesYesvia copypasta crate
Termios--
Web TerminalYesBrowser API

Defaults

By default, when using initializing a backend, the folowing will be used:

OSDefault backend
WindowsWindows Console
LinuxNCurses
Mac/OSXTermios

Windows Console

This backend replies on the following Windows API for various console related tasks:

APITask(s)
GetStdHandle(...)To gain access to stdin and stdout
GetConsoleScreenBufferInfo(...)To get information about console size and position
GetConsoleMode(...)To get information about the current mode of the console
WriteConsoleOutputW(...)To write a buffer of characters directly into the console
ReadConsoleInputW(...)To read input events (keys, mouse, resizing, console closing)
SetConsoleTitleW(...)To set the title (caption) of the console
SetConsoleScreenBufferSize(...)To resize the console to a specific width and heighr
SetConsoleCursorInfo(...)To move the caret (cursor) into a specific position of the console

For clipboard based operations, it relies on the following APIs:

  • OpenClipboard
  • EmptyClipboard
  • CloseClipboard
  • SetClipboardData
  • GetClipboardData
  • IsClipboardFormatAvailable

Remarks: For this type of backend to work, there is no need for a 3rd party crate (everything is done via FFI and direct API calls).

Limitations

Windows uses WTF-16 (that does not encode the full range of unicode characters). While unicode surrogates are supported, depending on the version of windows some characters (usually with a code higher than 0xFFFF) might not be disply accurtely or my move the line they are down into to the left.

Windows VT (Virtual Terminal)

This backend is based on both Windows API and VT100 escape sequences.

For clipboard based operations, it relies on the following APIs:

  • OpenClipboard
  • EmptyClipboard
  • CloseClipboard
  • SetClipboardData
  • GetClipboardData
  • IsClipboardFormatAvailable

Input (mouse / keyboard / console resize) is handled by the following APIs:

APITask(s)
GetStdHandle(...)To gain access to stdin and stdout
GetConsoleScreenBufferInfo(...)To get information about console size and position
GetConsoleMode(...)To get information about the current mode of the console
ReadConsoleInputW(...)To read input events (keys, mouse, resizing, console closing)
SetConsoleTitleW(...)To set the title (caption) of the console
SetConsoleScreenBufferSize(...)To resize the console to a specific width and heighr

The output is done via VT100 escape sequences (please refer to Wikipedia for more information). This backend supports true colors (24 bits per pixel) and wide characters (2 bytes per character) but it depends on the Windows version to support them.

Limitations:

Because of the way VT100 escape sequences work, the backend is much slower than a regular Windows Console backend (that renders the output directly into the console). If speed is a priority, it is recommended to use the Windows Console backend instead.

Keep in mind that the speed limitation can be mitigated by using a 3rd party terminal (that use the GPU to render the output)such as:

Usage

Windows VT is not the default backend on Windows. To use it, you need to specify the WindowsVT backend type when creating the application:

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    let app = App::with_backend(appcui::backend::Type::WindowsVT).build()?;
    // build your application here
    Ok(())
}

Further more, if you also want to use true colors you will need to enable the TRUE_COLORS feature when building the application:

[dependencies]
appcui = { version = "*", features = ["TRUE_COLORS"] }

Ncurses Terminal

Installing ncurses Library

To use the ncurses terminal library in your Rust project, you need to install the ncurses library on your system. Here's how you can install it on different platforms:

Ubuntu/Debian

sudo apt-get install libncurses5-dev libncursesw5-dev

macOS

brew install ncurses

Limitations of ncurses

While ncurses is a powerful terminal library, it does have some limitations to be aware of:

  • ncurses is primarily designed for ASCII characters and may not handle wide characters properly.
  • Wide characters, such as Unicode characters, may not be displayed correctly or may cause unexpected behavior.
  • Some terminal emulators may not fully support all ncurses features, leading to inconsistent behavior across different terminals.

It's important to consider these limitations when using ncurses in your Rust project, especially if you need to work with wide characters or require consistent behavior across different terminals.

AppCUI uses ncursesw for wide character support, in order to render multiple Unicode chars.

Termios

Summary

Termios is a low-level library for terminal manipulation on UNIX systems. It is utilized in AppCUI to handle terminal input and output on macOS.

Implementation Details

When a TermiosTerminal instance is initialized, the terminal is configured to operate in raw mode. This mode allows the application to capture individual key press events directly, bypassing line buffering and other default terminal behaviors. To facilitate advanced input handling, a specific byte sequence is sent to stdout to enable mouse events, allowing AppCUI to handle mouse interactions.

To adapt to dynamic terminal conditions, a signal handler is set up to monitor window resize events (SIGWINCH). This ensures that the terminal layout is updated appropriately when the terminal window's dimensions change.

At the lowest level, the implementation involves reading one or more bytes directly from stdin. This is performed using a blocking read operation on a separate thread to avoid interfering with the application's main execution flow. These bytes are then interpreted into a SystemEvent, which is dispatched to the runtime manager for further processing.

Limitations

  • Screen flickering: Screen updates may cause flickering because the screen content is not flushed all at once. We have not yet found a solution for this issue.
  • Key Combination Limitations: Certain key combinations on macOS cannot be uniquely identified due to limitations in the terminal's input byte encoding. For example:
    • Enter, Command + Enter, Option + Enter, and Control + Enter all produce the same byte sequence
    • Control + H and Control + Backspace produce conflicting byte sequences

TODO

The following features are missing from the Termios Terminal implementation:

  • Text Styling: Support for CharFlags (bold, italic, underlined characters)
  • Key Mapping: Some keys and key combinations are either unmapped or incorrectly mapped
  • Cursor Visibility: Hiding the cursor is not supported

Web Terminal

Summary

The Web Terminal allows AppCUI applications to run in a web browser using WebAssembly, WebGL for rendering, and JavaScript for event handling.

Prerequisites

Before you begin, make sure you have:

  • Rust Toolchain:

    [!IMPORTANT] Use the nightly toolchain, as this project requires unstable features.

  • wasm-bindgen: Add the following dependency in your Cargo.toml:
    wasm-bindgen = { version = "0.2" }
    
  • wasm-pack: Install wasm-pack for building your WebAssembly package.
  • A Web Server: Use the provided server.py below or any static server to serve your files.

    [!WARNING] If using threads, make sure to serve all your files in browser with these headers:

    Cross-Origin-Opener-Policy: "same-origin"
    Cross-Origin-Embedder-Policy: "require-corp"
    

Setup

1. Configure Rust for WebAssembly

Create or update your .cargo/config.toml to include the following target configuration:

[target.wasm32-unknown-unknown]
rustflags = [
    "-C", "target-feature=+atomics,+bulk-memory,+mutable-globals"
]

[unstable]
build-std = ["panic_abort", "std"]

This configuration enables atomic operations, bulk memory, and mutable globals on the wasm32-unknown-unknown target, and ensures that the build uses the required unstable std features.

2. Create a Library Package

Ensure your Rust project is set up as a library. In your library entry point, add the wasm-bindgen start macro to export your start function:

#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::wasm_bindgen;

#[wasm_bindgen(start)]
pub fn start() {
    // your code
}
}

Make sure that your library depends on the appcui crate and that you use its features for rendering and input handling.

Building the Package

Use wasm-pack to compile the project for the web target:

wasm-pack build --target web

Ensure that your Cargo project has the target wasm32-unknown-unknown installed. You can do so with:

rustup target add wasm32-unknown-unknown

Example HTML File

Below is an example index.html that sets up the canvases and loads the compiled WebAssembly package.

Index.html Example
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Web Terminal Test</title>
  <style>
    html, body {
      margin: 0;
      padding: 0;
      overflow: hidden;
    }
    #canvas, #textCanvas {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: block;
      background: transparent;
    }
    #textCanvas {
      pointer-events: none;
    }
    .config {
      display: none;
    }
  </style>
</head>
<body>
  <canvas id="canvas"></canvas>
  <canvas id="textCanvas"></canvas>

  <div class="config">
    <span id="terminal-cols">211</span>
    <span id="terminal-rows">56</span>
    <span id="terminal-font">Consolas Mono, monospace</span>
    <span id="terminal-font-size">20</span>
  </div>

  <script type="module">
    console.log("SharedArrayBuffer available:", typeof SharedArrayBuffer !== "undefined");
    import init, * as wasm from "./pkg/your_application.js"; // Replace 'your_application' with your package name

    init({
      module: new URL("./pkg/your_application.wasm", import.meta.url), // Replace 'your_application'
      memory: new WebAssembly.Memory({ initial: 200, maximum: 16384, shared: true })
    }).then(async () => {
      console.log("WASM module initialized");
      // Example: Initialize a thread pool if your application uses threads
      // await wasm.initThreadPool(navigator.hardwareConcurrency); 

      if (wasm.start) { // Ensure your exported start function is called
        wasm.start();
        console.log("WASM start function called");
      }
    });
  </script>
</body>
</html>

This file:

  • Creates two canvases: one for WebGL background rendering (canvas) and one for text rendering (textCanvas).
  • Includes a hidden configuration section for terminal settings (cols, rows, font, font size). These values are read by the WebTerminal in appcui.
  • Imports the WebAssembly package and initializes it. Make sure to replace your_application with the actual name of your wasm package.

Running the Server

A simple Python server for hosting the application:

Python Server Example
import http.server
import socketserver
import os

class CustomHandler(http.server.SimpleHTTPRequestHandler):
    def send_head(self):
        path = self.translate_path(self.path)
        if os.path.isfile(path):
            f = open(path, 'rb')
            fs = os.fstat(f.fileno())
            self.send_response(200)
            if path.endswith('.js'):
                mime_type = "application/javascript"
            elif path.endswith('.wasm'):
                mime_type = "application/wasm"
            else:
                mime_type = "text/html"
            self.send_header("Content-Type", mime_type)
            self.send_header("Content-Length", str(fs.st_size))
            self.send_header("Cross-Origin-Opener-Policy", "same-origin")
            self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
            self.end_headers()
            return f
        return super().send_head()

    def do_GET(self):
        f = self.send_head()
        if f:
            try:
                self.wfile.write(f.read())
            finally:
                f.close()

PORT = 4000
with socketserver.TCPServer(("", PORT), CustomHandler) as httpd:
    print(f"Serving on port {PORT}")
    httpd.serve_forever()

To run the example Python server (assuming you are in the directory containing server.py and your index.html and pkg folder):

python server.py

Then navigate to http://localhost:4000/index.html (or the appropriate address and port for your server) in your browser.

Implementation Details

The WebTerminal uses two HTML canvas elements:

  • One canvas (canvas) is used for WebGL rendering of cell backgrounds. This allows for efficient rendering of colored backgrounds.
  • A second canvas (textCanvas) is overlaid on top for rendering text characters.
  • Event handling (keyboard, mouse) is done via JavaScript event listeners attached to the document, which then forward events to the Rust/WASM module.
  • Configuration for terminal dimensions, font, etc., is typically read from hidden HTML elements on the page.

Limitations

  • Performance can vary depending on the browser and the complexity of the UI.
  • Threading support relies on SharedArrayBuffer, which requires specific HTTP headers (Cross-Origin-Opener-Policy: "same-origin" and Cross-Origin-Embedder-Policy: "require-corp") to be set by the web server.
  • Clipboard integration uses the browser's asynchronous clipboard API.

Controls

All controls from AppCUI follow a tree-like organization (a control has a parent control and may have multiple children controls).

Remarks

  • There is only one Desktop control. AppCUI provides a default one, but costom desktop can be created as well
  • A Desktop control can have one or multiple Windows.
  • All events emitted by any control are process at window level
  • A control may contain other children controls

Every control has a set of common characteristics:

  1. Layout (how it is position relative to its parent). The only exception in this case is the Desktop control that always takes the entire terminal space. More details on Layout section.
  2. Visibility (a control can be visible or not). The only exception is the Desktop control that will always be visible. A hidden control does not receive any input events and it is not drawn.
  3. Enabled (a control can be enabled or not). The only exception are the Desktop and Window controls that are always enabled. If a control is not enabled, it will not receive any input events (key pressed or mouse events) but it will still be drawn.
  4. HotKey (a combination of keys usually at the following form: Alt+<Letter|Number> that will automatically change the focus to the current control and execute a default action for it - for example for a checkbox, pressing that combination will check or uncheck the checkbox)

Besides this, a set of commonly available methods are available for all controls. These methods allow changing / accessing some attributes like visibility, loyout, hotkeys, etc. More deails can be found on Common methods for all Controls section.

Layout

Each control in AppCUI is created based on a layout rule that can be described as an ascii string that respects the following format:

"key:value , key:value , ... key:value"

Where key can be one of the following:

KeyAlias
(short)
Value typeDescription
xnumerical or percentage"X" coordonate
ynumerical or percentage"Y" coordonate
leftlnumerical or percentageleft anchor for the control
(the space between parent left margin and control)
rightrnumerical or percentageright anchor for the control
(the space between parent right margin and control)
toptnumerical or percentagetop anchor for the control
(the space between parent top margin and control)
bottombnumerical or percentagebottom anchor for the control
(the space between parent bottom margin and control)
widthwnumerical or percentagethe width of the control
heighthnumerical or percentagethe height of the control
dockddocking valuethe way the entire control is docked on its parent
alignaalignament valuethe way the entire control is aligne against a fix point

Remarks

  • Key aliases can be use to provide a shorter format for a layout. In other words, the following two formats are identical: x:10,y:10,width:30,height:30 and x:10,y:10,w:30,h:30
  • A numerical value is represented by an integer (positive and negative) number between -30000 and 30000. Example: x:100 --> X will be 100. Using a value outside accepted interval ([-30000..30000]) will reject the layout.
  • A percentage value is represented by a floating value (positive and negative) succeded by the character % between -300% and 300%. Example: x:12.75% --> X will be converted to a numerical value that is equal to the width of its parent multiplied by 0.1275. Using a value outside accepted interval ([-300%..300%]) will reject the layout. Percentage values can be use to ensure that if a parent size is changed, its children change their size with it.
  • All layout keys are case insensitive (meaning that 'left=10' and 'LEFT=10' have the same meaning)

Dock values can be one of the following:

Alt text for image

ValueAlias
topleftlefttop, tl, lt
topt
toprightrighttop, tr, rt
rightr
bottomrightrightbottom, br, rb
bottomb
bottomleftleftbottom, lb, bl
leftl
centerc

Remarks:

  • Dock value aliases can be use to provide a shorter format for a layout. In other words: dock:topleft is the same with dock:tl or dock:lt or d:tl

Align values have the same name as the docking ones, but they refer to the direction of width and height from a specific point (denoted by "X" and "Y" keys). Align parameter is used to compute top-left and bottom-right corner of a control that is described using a (X,Y) coordonate. The following table ilustrate how this values are computed:

ValueAliasTop-Left cornerBottom-Right corner
topleftlefttop, tl, lt(x,y)(x+width,y+height)
topt(x-width/2,y)(x+width/2,y+height)
toprightrighttop, tr, rt(x-width,y)(x,y+height)
rightr(x-width,y-height/2)(x,y+height/2)
bottomrightrightbottom, br, rb(x-width,y-height)(x,y)
bottomb(x-width/2,y-height)(x+width/2,y)
bottomleftleftbottom, lb, bl(x,y-height)(x+width,y)
leftl(x,y-height/2)(x+width,y+height/2)
centerc(x-width/2,y-height/2)(x+width/2,y+height/2)

Remarks:

  • Align value aliases can be use to provide a shorter format for a layout. In other words: align:center is the same with align:c or a:c

Absolute position

In this mode parameters x and y must be used to specify a point from where the control will be constructed. When using this mode, parameters dock, left, right, top, bottom can not be used. If width or height are not specified , they will be defaulted to 1 character (unless there is a minimum width or minumum height specified for that controls - in which case that limit will be applied). If align is not specified, it will be defaulted to topleft

If x, y, width or height are provided using percentages, the control will automatically adjust its size if its parent size changes.

LayoutResult
x:8,y:5,w:33%,h:6
or
x:8,y:5,w:33%,h:6,a:tl
If no alignament is provided, top-left will be considered as a default.

x:30,y:20,w:33%,h:6,a:br
x:50%,y:50%,w:25,h:8,a:c

Docking

To dock a control inside its parent use d or dock key. When docking a control, the following keys can not be used: align, x, y, left, right, top, bottom. Width and height should be used to specify the size of control. If not specified, they are defaulted to 100%.

LayoutOutcome
d:c,w:30,h:50%
d:bl,w:50%As height is not specified, it will be defaulted to 100%

d:c or d:tl or d:br
or any dock without width and height parameter
As both width and height parameters are missing, they will be defaulted to 100%. This means that current control will ocupy its entire parent surface. This is the easyest way to make a control fill all of its parent surface.

Anchors

Anchors (left, right, top and bottom) represent the distance between the object and its parent margins. When one of the anchors is present, the dock key can not be used. Depending on the combination of anchors used, other keys may be unusable.

Corner anchors

Corner anchors are cases when the following combinations of anchors are used toghether (left and top), (left and bottom), (right and top) and (right and bottom). When this combinations are used, x and y keys can not be used. Using them will reject the layout. If width or height are not specified , they will be defaulted to 1 character (unless there is a minimum width or minumum height specified for that controls - in which case that limit will be applied).

The combination of anchors also decides how (top,left) and (right,bottom) corners of a control are computed, as follows:

CombinationTop-Left cornerBottom-Right corner
top and left(left, top)(left+width, top+height)
top and right(parentWidth-right-width, top)(parentWidth-right, top+height)
bottom and left(left, parentHeight-bottom-height)(left+width, parentHeight-bottom)
bottom and right(parentWidth-right-width, parentHeight-bottom-height)(parentWidth-right, parentHeight-bottom)

where:

  • parentWidth is the width of the parent control
  • parentHeight the height of the parent control

Examples

LayoutResult
t:10,r:20,w:50,h:20
b:10,r:20,w:33%,h:10
b:10%,l:50%,w:25%,h:10

Using Left-Right anchors

When Left and right anchors are used together, there are several restrictions. First of all, width and x parameters can not be specified. Width is deduced as the difference between parents width and the sum of left and right anchors. Left anchor will also be considered as the "x" coordonate. However, height parameter should be specified (if not specified it will be defaulted to 1 character (unless a minimum height is specified for that controls - in which case that limit will be applied). align paramter can also be specified , but only with the following values: top, center or bottom. If not specified it will be defaulted to center.

Examples

LayoutResult
l:10,r:20,h:20,y:80%,a:b
l:10,r:20,h:100%,y:50%,a:c
l:10,r:20,h:50%,y:0,a:t

Using Top-Bottom anchors

When top and bottom anchors are used together, there are several restrictions. First of all, height and y parameters can not be specified. Height is deduced as the difference between parents height and the sum of top and bottom anchors. Top anchor will also be considered as the "y" coordonate. However, width parameter should be specified (if not specified it will be defaulted to 1 character (unless a minimum width is specified for that controls - in which case that limit will be applied). align paramter can also be specified , but only with the following values: left, center or right. If not specified it will be defaulted to center.

Examples

LayoutResult
t:10,b:20,w:90,x:80%,a:r
t:10,b:20,w:100%,x:50%,a:c
t:10,b:20,w:50%,x:0,a:l

3-margin anchors

When using 3 of the 4 anchors, the following keys can not be used: x, y, align and dock. Using them will reject the layout. The following table reflects these dependencies:

CombinationResult
left and top and right
or
left and bottom and right
height optional (see remarks)

width = parentWidth - (left+right)
top and left and bottom
or
top and right and bottom
width optional (see remarks)

height = parentHeight - (top+bottom)

Remarks

  • if height or width are not present and can not be computed as a difference between two margins, they are defaulted to value 1. If limits are present (min Width or min Height) those limits are applied. This is usually usefull for controls that have a fixed width or height (e.g. a button, a combobox).

The position of the control is also computed based on the combination of the 3 anchors selectd, as shown in the next table:

CombinationTop-Left cornerBottom-Right corner
left and top and right(left, top)(parentWidth-right, top+height)
left and bottom and right(left, parentHeight-bottom-height)(parentWidth-right, parentHeight-bottom)
top and left and bottom(left, top)(left+width, parentHeight-bottom)
top and right and bottom(parentWidth-right-width, top)(parentWidth-right, parentHeight-bottom)

where:

  • parentWidth is the width of the parent control
  • parentHeight the height of the parent control

Examples

LayoutResult
l:10,t:8,r:20,h:33%
l:20,b:5,r:10,h:33%
l:10,t:8,b:15,w:80%
r:30,t:8,b:20%,w:50%%

4-margin anchors

When all of the 4 anchors, the rest of the keys ( x, y, width, height, align and dock) can not be used. Using them will reject the layout.

Example

LayoutResult
l:20,t:7,r:10,b:10

Instantiate a control using macros

All controls can be build via their constructors, but also via some specialized macros that are meant to ease setting up a controller. The general format is as follows: controller!("<parameter_list>"), where the controller! is a specialized macro (for example button!) and the parameter_list is a json-like/python-like format, form out of:

  • positional parameters - for example in this case: "10,20,30" has three positional parameters 10, 20 and 30
  • named parameters - are parameters described using a name and a value. The usual format is name:value but name=value is also supported.

All parameters are separated one from onether using , or ;. The overall format of a parameter list is:

PostionalParam-1, ... PostionalParam-n, NamedParam-1,  ... NamedParam-m

Both positional parameters and named parameters are optionally. Their order however it is not. Positional parameters (if used) should always be placed before named parameters.

Values

The values used as parameters can be:

  • regula word (ex: align:center)
  • numerical values (ex: x=10or y=-2)
  • percentages (ex: width:10% or height=25%)
  • strings - a string can be separated between douple quotes or single quotes and can contain new lines (ex: "..." or '...'). If both a single quote and a double quote has to be used in a string, three consecuitev double or single quotes can be used (ex: """...""" or '''...''')
  • list of values - obtained using [ and ] characters. The general format is [value-1, value-2, ... value-n] - ex: [10,20,30] is a list with three values 10,20 and 30.
  • another parameter list - obtained using [ and ] characters. The general format is {parameter list} - for example the following syntax point={x:10,y:20} translates into parameter point being defined as a set of two parameters x with value 10 and y with value 20.
  • flags - flags are a meta interpretation for the previously described parameters. It can be a regular word / string or a list of values. If using a word you can separate flags using one of the following characters: | and +. Aditionally when using a strings, spaces, , and ; cand also be used as a separator. When using a list of values, each one of the values is a separate flag. For example the following declarations are equivalent:
    • flags=[flag1,flag2]
    • flags=flag1+flag2
    • flags=flag1|flag2
    • flags="flags1,flags2"
    • flags="flags1;flags2"
    • flags="flags1 flags2"

Common parameters

All controls have a set of common parameters that are required for layout or to change some of their states (such as visibility or if a control is enabled - can receive input).

Parameter namesTypePuspose
x, y, width, height, left, right, top, bottom and their aliasesNumerical or percentageUsed for control layout
align , dock and their aliasesAlignament value (left, topleft, top, center, ...)Used for control layout
enabled or enablebool (true or false)Use to set up the enable state of a control
visiblebool (true or false)Use to set up the visibility of a control

Common methods for all Controls

All controls (including Window and Desktop) have a set of common methods obtaing via Deref trait over a common base object.

MethodPurpose
set_visible(...)Shows or hides a controls (will have no change over a Desktop)
is_visible()Returns true if the current control is visible, or false otherwise
set_enable(...)Changes the enable/disable status of a control (has no effect on a Window or a Desktop control
is_enabled()Returns true if the current control is enabled, or false otherwise
has_focus()Returns true if the current control has focus, or false otherwise
can_receive_input()Returns true if the current control could receive mouse or keyboard events if focused or false otherwise
is_mouse_over()Returns true if the mouse cursor is over the current control
MethodPurpose
size()Returns the size (widthxheight) for the current control
client_size()Returns the client size (the size minus the margins) for the current control
set_size(...)Sets the new size for a control (to a specified size given by parameters width and height). Keep in mind that this method will change the existing layout to an a layout based on top-left corner (given by controls x and y coordonates) and the new provided size. Any dock or alignament properties will be removed.
This method has no effect on a Desktop control.
position()Returns the relatove position (x,y) of the current control to its parent.
set_position(...)Sets the new position for a control (to a specified coordonate given by parameters x and y). Keep in mind that this method will change the existing layout to an a layout based on top-left corner (given by coordonates x and y) and the controls current width and height. Any dock or alignament properties will be removed.
This method has no effect on a Desktop control.
set_components_toolbar_margins(...)Sets the left and top components margins - for scrollbars, filters, etc
MethodPurpose
hotkey()Returns the hotkey associated witha control or Key::None otherwise
set_hotkey()Sets the hotkey for a control. To clear the hotkey call this function like this: .set_hotkey(Key::None)

Update methods

MethodPurpose
request_focus()Request the framework to assign the focus to the current control
request_update()Request the framework to update itself. This actian will update the commandbar, menus and the position of the controls.
MethodPurpose
register_menu(...)Register a menu into AppCUI framework and returns a Handle for it
show_menu(...)Show a popup menu that was registered by the current control
menuitem(...)Returns an immutable reference to a menu item based on two handles: one for the menu, and one for the menu item
menuitem_mut(...)Returns an mutable reference to a menu item based on two handles: one for the menu, and one for the menu item
MethodPurpose
theme()Returns a reference to the theme object that is being used by the application. This is the same reference an objectr receives when OnPaint method is being called.

Event loop

AppCUI is an event driven framework, meaning that each control can emit events to reflect various acions or changes that occur. For example, whenever you push a button, an event will be raise. All events are process at Window level by implementing various traits. To build a Window that supports event handling, you must use a special procedural macro call Window, defined in the the following way:

#[Window(events=..., )]
struct MyWindow {
    // specific fields
}

where the attribute events has the following form:

  • events=EventTrait-1 + EventTrait-2 + EventTrait-3 + ... EventTrait-n

and an event trait can be one of the following:

  • ButtonEvents
  • CheckBoxEvents
  • RadioBoxEvents
  • WindowEvents
  • MenuEvents
  • CommandBarEvents
  • ToolBarEvents
  • ColorPickerEvents
  • ThreeStateBoxEvents
  • PasswordEvents
  • KeySelectorEvents
  • TextFieldEvents

These events can be implemented to receive notification on various actions that children controls are performing.

When creating a window that supports event loop in this manner, you will need to instantiate it. A common approach is the following:

#[Window(events=..., )]
struct MyWindow {
    // specific fields
}
impl MyWindow {
    fn new(/* extra parameters */) -> Self {
        let mut obj = MyWindow {
            base: Window::new(title, layout, flags);
            // initialization other fileds from MyWindow i
        }
        // other initialization (such as creating children)
        return obj;
    }
}

The initializaton base: Window::new(title, layout, flags); is mandatory. As for the title, layout and flags you can provide them as parameters in the new method or you can infer them / or hardcode them in a different way. More on how a Window can be created on Window page.

Once you create an event loop you can add it to your application using add_window(...) method.

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    app.add_window(MyWindow::new(/* parameters */));
    app.run();
    Ok(())
}

A simple example

Let's start with a simple example that creates such a window that has a fixed sized of 40x20 characters and two internal i32 values.

use appcui::prelude::*;

#[Window()]
struct MyWindow {
    value1: i32,
    value2: i32
}
impl MyWindow {
    fn new(title: &str) -> Self {
        MyWindow {
            base: Window::new(title, Layout::new("d:c,w:40,h:20"), window::Flags::None);
            value1: 0,
            value2: 1
        }
    }
}
fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    app.add_window(MyWindow::new("Some title"));
    app.run();
    Ok(())
}

Intercepting events from a child control

Usually, a window that processes events mentains a handle to various controls and enable event processing in the #[Window(...)] declaration.

use appcui::prelude::*;

#[Window(events = /*Events specific to a control */)]
struct MyWindow {
    value1: i32,
    control: Handle</*control type*/>
}
impl MyWindow {
    fn new(/* parameters */) -> Self {
        let mut mywin = MyWindow {
            base: Window::new(/*...*/);
            control: Handle::None
        }
        // now we create the control
        mywin.control =  mywin.add(/* Code that creates a control */);

        return mywin;
    }
}
impl /*Control event*/ for MyWindow {
    // add logic for event
}
fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    app.add_window(MyWindow::new(/* parameters */));
    app.run();
    Ok(())
}

For every control described in Stock Controls an example on how that control can be used with the event loop and the type of events it emits will be presented.

Window

A window is the core component of an application and it is the object where all events from children controls are being processed.

To create a Window use:

  • Window::new method (with 3 parameters: a title, a layout and initialization flags)
  • Window::with_type method (with 4 parameters: a title, a layout , initialization flags and a type)
  • macro window!.
let w = Window::new("Title", Layout::new("x:10,y:5,w:15,h:9"),window::Flags::None);
let w2 = window!("Title,d:c,w:10,h:10");
let w3 = window!("title='Some Title',d:c,w:30,h:10,flags=[Sizeable])");
let w4 = window!("title='WithTag',d:c,w:30,h:10,tag:MyTag)");
let w5 = window!("Title,d:c,w:10,h:10,key:Alt+F10");
let w6 = window!("Title,d:c,w:10,h:10,key:auto");

Keep in mind that window will NOT handle any events from its children.

A window supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
title or text or captionStringYes (first postional parameter)The title (text) of the window
flagsString or ListNoWindow initialization flags
typeStringNoWindow type
tagStringNoThe tag of the window
hotkey or hot-key or keyKeyNoThe hotkey associated with a window. You can also use the auto value to ask the framework to find the first available key (from Alt+1 to Alt+9)

To create a window that will handle events from its children, use #[Window(...)] method:

#[Window(events=..., )]
struct MyWindow {
    // specific fields
}

A window supports the following initialization flags:

  • window::Flags::None - regular window (with a close button)
  • window::Flags::Sizeable or Sizeable (for window! macro) - a window that has the resize grip and the maximize button
  • window::Flags::NoCloseButton or NoCloseButton (for window! macro) - a window without a close button
  • window::Flags::FixedPosition or FixedPosition (for window! macro) - a window that can not be moved

and the following types:

  • window::Type::Normal or Normal (for window! macro) - a regular window
  • window::Type::Error or Error (for window! macro) - a window with a red background to indicate an error message
  • window::Type::Notification or Notification (for window! macro) - a window with a different background designed for notification messages
  • window::Type::Warning or Warning (for window! macro) - a window with a different background designed for Warning messages

Methods

Besides the Common methods for all Controls a button also has the following aditional methods:

MethodPurpose
add(...)Adds a new control as a child control for current window
control(...)Returns an immutable reference to a control based on its handle
control_mut(...)Returns a mutable reference to a control based on its handle
request_focus_for_control(...)Requests the focus for a specific control given a specfic handle
toolbar()Returns a mutable reference to current window toolbar
set_title(...)Sets the title of Window.
Example: win.set_title("Title") - this will set the title of the window to Title
title()Returns the title of the current window
set_tag(...)Sets the tag of Window.
Example: win.set_tag("ABC") - this will set the tag of the window to ABC
tag()Returns the tag of the current window
clear_tag()Clears the current tag. Its equivalent to set_tag("")
set_auto_hotkey()Automatically selects a free hotkey (in a format Alt+{number} where {number} is between 1 and 9)
enter_resize_mode()Enters the resize mode programatically
closeCloses current window

Key association

In terms of key association, a Window has two modes:

  • Normal mode (for whem a window is has focus)
  • Resize/Move mode (in this mode you can use arrows and various combinations to move the window)

For normal mode

KeyPurpose
Ctrl+Alt+M or
Ctrl+Alt+R
Switch the window to resize/move mode
EscapeTrigers a call cu on_cancel(...) method. By default this will close the window. Howeverm you can change the behavior and return ActionRequest::Deny from the on_cancel callback
Up or
Alt+Up or
Ctrl+Up
Moves to the closes control on upper side the curent one.
Down or
Alt+Down or
Ctrl+Down
Moves to the closest control on the bottom side of the curent one
Left or
Alt+Left or
Ctrl+Left
Moves to the closest control on the left side of the curent one
Right or
Alt+Right or
Ctrl+Right
Moves to the closest control on the right side of the curent one

OBS: Keep in mind that if any of these keys (in particular Left, Right, Up and Down) are capture by one of the children of a window, they will not pe process by the window.

For resize/move mode

KeyPurpose
Escape or Enter or Tab or SpaceSwitch back to the normal mode
Left, Up, Right, DownArrow keys can be used to move the window
CCenters the current window to the Desktop
M or RMaximizes or Restores the size of the current Windows
Alt+{Left, Up, Right, Down}Moves the window towards one of the margins of the Desktop. For example Alt+Up will move current window to the top margin of the client space of the Desktop
Ctrl+{Left, Up, Right, Down}Increases or decreases the Width or Height of the current Window

Events

Window related events can be intercepted via WindowEvents trait. You will need to add WindowEvents in the list of events like in the following example:

#![allow(unused)]
fn main() {
#[Window(events=WindowEvents)]
struct MyWindow { ... }
impl WindowEvents for MyWindow { ... }
}

WindowEvents is defined in the following way:

#![allow(unused)]
fn main() {
pub trait WindowEvents {
    fn on_close(&mut self) -> EventProcessStatus {
        EventProcessStatus::Ignored
    }
    fn on_layout_changed(&mut self, old_layout: Rect, new_layout: Rect) {}
    fn on_activate(&mut self) {}
    fn on_deactivate(&mut self) {}
    fn on_accept(&mut self) {}
    fn on_cancel(&mut self) -> ActionRequest {
        ActionRequest::Allow
    }
}
}

These methods are called under the following scenarious:

MethodCalled when
on_layout_changed(...)Called whenever the size or position of a window changes.
on_activate(...)Called whenever a window or a modal window receives the focus
on_deactivate(...)Called whenever a window or a modal window loses the focus
on_accept(...)Called only for modal windows when you hit the Enter key
on_cancel(...)For a modal window this method is called when you press Escape. You can use this method to disable closing via Escape key and for an exit with a value (via method exit_with(...)

For a regular window (non-modal) this method can be called when you pressed Esc key or when you pressed the close button from a window.

Window Tags

For every window, a tag can be set for a window (a tag is a string associated with a Window that reflects its purpose). To set a tag use .set_tag("<name") method. For example, the following code:

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    let mut win = window!("Title,d:c,w:40,h:9");
    win.set_tag("TAG");
    app.add_window(win);
    app.run();
    Ok(())
}

should generate a window that looks like the following:

Window Hot Key

You can also associate a hot key to a window. A hot key allows you to quickly switch between windows. In the next example, we set up Alt+7 as a hot key for a windows.

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    let mut win = window!("Title,d:c,w:40,h:9");
    win.set_hotkey(key!("Alt+7"));
    app.add_window(win);
    app.run();
    Ok(())
}

should generate a window that looks like the following:

Toolbar

A toolbar is a generic concept of an area over the margins of a window (top or bottom margins) where several controls (or toolbar items) reside. A toolbar items are simplier controls that convey their events directly to the window via a spcial trait: ToolBarEvents.

All toolbar items are organized in groups. A group is similar to a flow (all toolbar items are aligned one after another, depending on the direction of the flow). It is important to notice that a toolbar item does not have a layout of itself (its position is decided by the group). The following direction are supported by a toolbar group (via enum toolbar::GroupPosition):

pub enum GroupPosition {
    TopLeft,
    BottomLeft,
    TopRight,
    BottomRight,
}

Constructing a toolbar item

To add a toolbar item into a window, you need to perform the following steps:

  1. create a group
  2. create a toolbar item
  3. add the previously created toolbar item to the group created on step 1.

Typically, a toolbar initialization mechanims is done when creating a window, in a similar manner as with the next snippet:

#[Window(...)]
struct MyWin {
    // data members
}
impl MyWin {
    fn new() -> Self {
        let mut me = Self {
            base: window!("..."),
        };
        let a_group = me.toolbar().create_group(toolbar::GroupPosition::<Value>);
        let item_handle = me.toolbar().add(a_group, toolbar::<Type>::new("..."));
        // other initializations
        me
    }
}

To create a toolbar item, there are two options:

  • use toolbar::<Item>::new(...) method
  • use toolbaritem! macro

Curenly AppCUI supports the following toolbar item types:

Common methods

All toolbar items have a set of common methods that can be used to modify their behavior:

MethodPurpose
set_toooltip(...)Set the tooltip for the current item. Using this method with an empty string clears the tooltip
get_tooltip()Returns the current tooltip associated with an toolbar item
set_visible()Changes the visibility settings for a selected toolbar item
is_visible()Returns true if the current toolbar item is visible, or false otherwise

Button toolbar item

A toolbar button is a item that can be positioned on the top or bottom part of a windows (like in the following image).

To create a button within a toolbar use the toolbar::Button::new(...) method:

#![allow(unused)]
fn main() {
let toolbar_button = toolbar::Button::new("content");
}

or the toolbaritem! macro:

#![allow(unused)]
fn main() {
let toolbar_button_1 = toolbaritem!("content,type=button");
let toolbal_button_2 = toolbaritem!("content='Start',type:button");
let toolbal_button_3 = toolbaritem!("content='&Stop',type:button,tooltip:'a tooltip'");
let toolbal_button_4 = toolbaritem!("content='hidden button',type:button,visible:false");
}

Using the character & as part of the button caption will associate the next character (if it is a letter or number) as a hot-key for the button. For example, the following caption St&art will set Alt+A as a hot-key for the button.

The following parameters are supported for a toolbar button:

Parameter nameTypePositional parameterPurpose
text or captionStringYes (first postional parameter)The caption (text) written on the button
typeStringNoFor a button use: type:Button
tooltipStringNoThe tooltip associated with the button
visibleBoolNotrue if the toolbar item is visible (this is also the default setting) or false otherwise

Besides the default methods that every toolbar item has (as described here), the following methods are available for a toolbar label:

MethodPurpose
set_caption(...)Set the new caption for a button. The width (in characters) of the button is the considered to be the number of characters in its content
caption()Returns the current caption of a button.

Events

To intercept button clicks, implement ToolBarEvents for the current window, as presented in the following example:

#![allow(unused)]
fn main() {
#[Window(events=ToolBarEvents)]
struct MyWin { /* data members */ }

impl ToolBarEvents for MyWin {
    fn on_button_clicked(&mut self, handle: Handle<toolbar::Button>) -> EventProcessStatus {
        // process click events from a toolbar button
    }
}
}

Example

The following example creates two buttons on the bottom right part of a window toolbar that can be used to increase the value of a number.

#[Window(events = ToolBarEvents)]
struct MyWin {
    increase_button: Handle<toolbar::Button>,
    decrease_button: Handle<toolbar::Button>,
    text: Handle<Label>,
    number: u32,
}

impl MyWin {
    fn new() -> Self {
        let mut win = MyWin {
            base: window!("'My Win',d:c,w:40,h:6"),
            increase_button: Handle::None,
            decrease_button: Handle::None,
            text: Handle::None,
            number: 10,
        };
        // create a group
        let g = win.toolbar().create_group(toolbar::GroupPosition::BottomRight);
        // add buttons
        win.increase_button = win.toolbar().add(g, toolbar::Button::new("+"));
        win.decrease_button = win.toolbar().add(g, toolbar::Button::new("-"));
        // add a label
        win.text = win.add(label!("10,d:c,w:2,h:1"));
        win
    }
}
impl ToolBarEvents for MyWin {
    fn on_button_clicked(&mut self, handle: Handle<toolbar::Button>) -> EventProcessStatus {
        match () {
            _ if handle == self.increase_button => self.number += 1,
            _ if handle == self.decrease_button => self.number -= 1,
            _ => {}
        }
        let h = self.text;
        let n = self.number;
        if let Some(label) = self.control_mut(h) {            
            label.set_caption(format!("{}", n).as_str());
        }
        EventProcessStatus::Processed
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

CheckBox toolbar item

A toolbar checkbox is a item that can be positioned on the top or bottom part of a windows (like in the following image) that can have two states (checked or un-checked).

To create a checkbox within a toolbar use the toolbar::CheckBox::new(...) method:

#![allow(unused)]
fn main() {
let toolbar_checkbox = toolbar::CheckBox::new("content", true);
}

or the toolbaritem! macro:

#![allow(unused)]
fn main() {
let toolbar_checkbox_1 = toolbaritem!("content,type=checkbox");
let toolbal_checkbox_2 = toolbaritem!("content='Start',type:checkbox,checked: true");
let toolbal_checkbox_3 = toolbaritem!("content='&Stop',type:checkbox,tooltip:'a tooltip'");
let toolbal_checkbox_4 = toolbaritem!("content='hidden checkbox',type:checkbox,visible:false");
}

Using the character & as part of the button caption will associate the next character (if it is a letter or number) as a hot-key for the checkbox. For example, the following caption &Option one will set Alt+O as a hot-key for the checkbox.

The following parameters are supported for a toolbar checkbox:

Parameter nameTypePositional parameterPurpose
text or captionStringYes (first postional parameter)The caption (text) written on the checkbox
typeStringNoFor a checkbox use: type:Checkbox
tooltipStringNoThe tooltip associated with the button
visibleBoolNotrue if the toolbar item is visible (this is also the default setting) or false otherwise
check or checkedBoolNotrue if the checkbox is checked or false otherwise

Besides the default methods that every toolbar item has (as described here), the following methods are available for a toolbar label:

MethodPurpose
set_caption(...)Sets the new caption for a checkbox. The width (in characters) of the checkbox is the considered to be the number of characters in its content
caption()Returns the current caption of a checkbox.
set_checked()Sets the new checked stated of the checkbox.
is_checked()true if the toolbar checkbox is checked or false otherwise

Events

To intercept a change in a checkbox checked state, you need to implement ToolBarEvents for the current window, as presented in the following example:

#![allow(unused)]
fn main() {
#[Window(events=ToolBarEvents)]
struct MyWin { /* data members */ }

impl ToolBarEvens for MyWin {
    fn on_checkbox_clicked(&mut self, handle: Handle<toolbar::CheckBox>, checked: bool) -> EventProcessStatus {
        // do an action based on the new state of the checkbox
        // parameter `checked` is true if the toolbar checkbox is checked or false otherwise 
    }
}
}

Example

The following example creates a window with two checkboxes toolbar items and a label. Clicking on each one of the checkboxes will show a message on the label that states the check state of that checkbox.

#[Window(events = ToolBarEvents)]
struct MyWin {
    cb1: Handle<toolbar::CheckBox>,
    cb2: Handle<toolbar::CheckBox>,
    text: Handle<Label>,
}

impl MyWin {
    fn new() -> Self {
        let mut win = MyWin {
            base: window!("'My Win',d:c,w:40,h:6"),
            cb1: Handle::None,
            cb2: Handle::None,
            text: Handle::None,
        };
        // create a group
        let g = win.toolbar().create_group(toolbar::GroupPosition::BottomRight);
        // add checkboxes
        win.cb1 = win.toolbar().add(g, toolbar::CheckBox::new("Opt-1",false));
        win.cb2 = win.toolbar().add(g, toolbar::CheckBox::new("Opt-2",false));
        // add a label
        win.text = win.add(label!("'',d:c,w:20,h:1"));
        win
    }
}
impl ToolBarEvents for MyWin {
    fn on_checkbox_clicked(&mut self, handle: Handle<toolbar::CheckBox>, checked: bool) -> EventProcessStatus {
        let txt = match () {
            _ if handle == self.cb1 => format!("Opt-1 is {}",checked),
            _ if handle == self.cb2 => format!("Opt-2 is {}",checked),
            _ => String::new(),
        };
        let h = self.text;
        if let Some(label) = self.control_mut(h) {
            label.set_caption(&txt);
        }
        EventProcessStatus::Processed
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

Label toolbar item

A toolbar label is a text that can be written on the top or bottom part of a windows (like in the following image).

To create a label toolbar use the toolbar::Label::new(...) method:

#![allow(unused)]
fn main() {
let toolbar_label = toolbar::Label::new("content");
}

or the toolbaritem! macro:

#![allow(unused)]
fn main() {
let toolbar_label_1 = toolbaritem!("content,type=label");
let toolbal_label_2 = toolbaritem!("content='label text',type:label");
let toolbal_label_3 = toolbaritem!("content='label text',type:label,tooltip:'a tooltip'");
let toolbal_label_4 = toolbaritem!("content='hidden label',type:label,visible:false");
}

The following parameters are supported for a toolbar label:

Parameter nameTypePositional parameterPurpose
text or captionStringYes (first postional parameter)The caption (text) written on the label
typeStringNoFor a label use: type:Label
tooltipStringNoThe tooltip associated with the label
visibleBoolNotrue if the toolbar item is visible (this is also the default setting) or false otherwise

Besides the default methods that every toolbar item has (as described here), the following methods are available for a toolbar label:

MethodPurpose
set_caption(...)Set the new caption for a label. The size of the label is the considered the number of characters in its content
caption()Returns the current caption of a label.

Example

The following example shows 3 lables that show a number written in base 10, 16 and 2.

  • the first two labels (for base 10 and base 16 are part of one group located on the bottom left part of the window);
  • the last label is part of a separate group located on the top-right side of the window.
  • at the left of the window, there is a button that if clicked increases the number and updates the values in each label;
  • at the right of the window, 3 checkboxes can change the visibility state for the labels
#[Window(events = ButtonEvents+CheckBoxEvents)]
struct MyWin {
    increase_button: Handle<Button>,
    dec: Handle<toolbar::Label>,
    hex: Handle<toolbar::Label>,
    bin: Handle<toolbar::Label>,
    show_dec: Handle<CheckBox>,
    show_hex: Handle<CheckBox>,
    show_bin: Handle<CheckBox>,
    number: u32,
}

impl MyWin {
    fn new() -> Self {
        let mut win = MyWin {
            base: window!("'My Win',d:c,w:40,h:6"),
            increase_button: Handle::None,
            dec: Handle::None,
            hex: Handle::None,
            bin: Handle::None,
            show_dec: Handle::None,
            show_hex: Handle::None,
            show_bin: Handle::None,
            number: 24,
        };
        // add the increase button
        win.increase_button = win.add(button!("Increase,w:15,d:l"));
        // add checkboxes
        win.show_dec = win.add(checkbox!("'Show decimal',x:20,y:1,w:16,checked:true"));
        win.show_hex = win.add(checkbox!("'Show hex',x:20,y:2,w:16,checked:true"));
        win.show_bin = win.add(checkbox!("'Show binary',x:20,y:3,w:16,checked:true"));
        // add toolbar labels
        let first_group = win.toolbar().create_group(toolbar::GroupPosition::BottomLeft);
        let second_group = win.toolbar().create_group(toolbar::GroupPosition::TopRight);
        win.dec = win.toolbar().add(first_group, toolbar::Label::new(""));
        win.hex = win.toolbar().add(first_group, toolbar::Label::new(""));
        win.bin = win.toolbar().add(second_group, toolbar::Label::new(""));
        win.update_toolbar_labels();
        win
    }
    fn update_toolbar_label(&mut self, handle: Handle<toolbar::Label>, text: String) {
        if let Some(label) = self.toolbar().get_mut(handle) {
            label.set_content(text.as_str());
        }
    }
    fn update_visibility_status_for_label(&mut self, handle: Handle<toolbar::Label>, visible: bool) {
        if let Some(label) = self.toolbar().get_mut(handle) {
            label.set_visible(visible);
        }        
    }
    fn update_toolbar_labels(&mut self) {
        self.update_toolbar_label(self.dec, format!("Dec:{}", self.number));
        self.update_toolbar_label(self.hex, format!("Hex:{:X}", self.number));
        self.update_toolbar_label(self.bin, format!("Bin:{:b}", self.number));
    }
}

impl ButtonEvents for MyWin {
    fn on_pressed(&mut self, _handle: Handle<Button>) -> EventProcessStatus {
        self.number += 1;
        self.update_toolbar_labels();
        return EventProcessStatus::Processed;
    }
}
impl CheckBoxEvents for MyWin {
    fn on_status_changed(&mut self, handle: Handle<CheckBox>, checked: bool) -> EventProcessStatus {
        match () {
            _ if handle == self.show_bin => self.update_visibility_status_for_label(self.bin, checked),
            _ if handle == self.show_hex => self.update_visibility_status_for_label(self.hex, checked),
            _ if handle == self.show_dec => self.update_visibility_status_for_label(self.dec, checked),
            _ => {}
        }
        EventProcessStatus::Processed
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

Upon execution you should see something that looks like the following image:

SingleChoice toolbar item

A toolbar singlechoice item is a item that can be positioned on the top or bottom part of a windows (like in the following image) that can have two states (selected or un-selected).

Within a group, only one singlechoice item can be selected.

To create a checkbox within a toolbar use the toolbar::SingleChoice::new(...) method:

#![allow(unused)]
fn main() {
let toolbar_singlechoice = toolbar::SingleChoice::new("SingleChoice");
}

or the toolbaritem! macro:

#![allow(unused)]
fn main() {
let toolbar_sc_1 = toolbaritem!("content,type=singlechoice");
let toolbal_sc_2 = toolbaritem!("content='Choice One',type:singlechoice");
let toolbal_sc_3 = toolbaritem!("content='&Second choice',type:singlechoice,tooltip:'a tooltip'");
let toolbal_sc_4 = toolbaritem!("content='hidden choice',type:singlechoice,visible:false");
}

Using the character & as part of the button caption will associate the next character (if it is a letter or number) as a hot-key for the singlechoice item. For example, the following caption First &choice will set Alt+C as a hot-key for the singlechoice item.

The following parameters are supported for a toolbar singlechoice:

Parameter nameTypePositional parameterPurpose
text or captionStringYes (first postional parameter)The caption (text) written on the single choice
typeStringNoFor a singlechoince use: type:SingleChoince
tooltipStringNoThe tooltip associated with the singlechoice
visibleBoolNotrue if the toolbar item is visible (this is also the default setting) or false otherwise

Besides the default methods that every toolbar item has (as described here), the following methods are available for a toolbar label:

MethodPurpose
set_caption(...)Sets the new caption for a singlechoince. The width (in characters) of the singlechoince is the considered to be the number of characters in its content
caption()Returns the current caption of a singlechoice item.
select()Sets the current single choice as the selected single choince for the current group.
is_selected()true if the toolbar single choice is selected or false otherwise

OBS: Keep in mind that using select() method only works if the single choice has already been added to a toolbar. Using this methid without adding the item to a toolbar will result in a panic.

Events

To intercept if the current choice has change, you need to implement ToolBarEvents for the current window, as presented in the following example:

#![allow(unused)]
fn main() {
#[Window(events=ToolBarEvents)]
struct MyWin { /* data members */ }

impl ToolBarEvens for MyWin {
    fn on_choice_selected(&mut self, _handle: Handle<toolbar::SingleChoice>) -> EventProcessStatus {
        // do an action based on the new selection
    }
}
}

Example

The following example creates a window with two single choice toolbar items and a label. Clicking on each one of the singlechoice items will show a message on the label that states the selected singlechoice.

#[Window(events = ToolBarEvents)]
struct MyWin {
    opt1: Handle<toolbar::SingleChoice>,
    opt2: Handle<toolbar::SingleChoice>,
    text: Handle<Label>,
}

impl MyWin {
    fn new() -> Self {
        let mut win = MyWin {
            base: window!("'My Win',d:c,w:40,h:6"),
            opt1: Handle::None,
            opt2: Handle::None,
            text: Handle::None,
        };
        // create a group
        let g = win.toolbar().create_group(toolbar::GroupPosition::BottomLeft);
        // add buttons
        win.opt1 = win.toolbar().add(g, toolbar::SingleChoice::new("First Choice"));
        win.opt2 = win.toolbar().add(g, toolbar::SingleChoice::new("Second Choice"));
        // add a label
        win.text = win.add(label!("'',d:c,w:22,h:1"));
        win
    }
}
impl ToolBarEvents for MyWin {
    fn on_choice_selected(&mut self, handle: Handle<toolbar::SingleChoice>) -> EventProcessStatus {
        let txt = match () {
            _ if handle == self.opt1 => "First choice selected",
            _ if handle == self.opt2 => "Second choice selected",
            _ => "",
        };
        let h = self.text;
        if let Some(label) = self.control_mut(h) {
            label.set_caption(txt);
        }
        EventProcessStatus::Processed
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

Modal Window

A modal window is a window that captures the entire focus for the duration of its existance. In other word, when a modal window is started, it will be on top of everything else and the entire input (mouse and keyboard will be treated by it).

When a modal window is opened the rest of the windows or other modal windows will be disabled:

A modal window is in fact just like a regular window (you can add other controls, you can resize and move it) and you can intercept events just like in a regular window case. However, since a modal window can not lose the focus, it has another property (response) that implies that it will provide a response once its execution ends. The response can be any kind of type (including a void type).

To create a modal window that will handle events from its children, use #[ModalWindow(...)] method:

#[ModalWindow(events=..., response=...)]
struct MyModalWindow {
    // specific fields
}

Besides the normal methods that a regular Window has, the following extra methods are available:

MethodPurpose
exit() or close()Exits the current modal window without returning anything. This translates into returning None from the call of method show(...)
exit_with(...)Exits and returns a value of the same type as the parameter reponse from the #[ModalWindo(...)] definition. This translates into returning Some(value) from the call of method show(...)
show()Shows the modal window and capture the entire input. The execution flow is blocked until method show returns.

Besides the keys that a regular (non-modal) window supports, the following keys have a different purpose:

KeyPurpose
EscapeTrigers a call to on_cancel(...) method. By default this will close the modal window and will return None to the caller. The behavior can be changed by returning ActionRequest::Deny from the on_cancel callback
EnterCalls the on_accept(...) method. This will not close the window unless you call exit() or exit_with(...) from within the callback

To disable this behavior, you can add WindowEvents to the list of events and then return ActionRequest::Deny when implementing on_cancel.

#![allow(unused)]
fn main() {
#[ModalWindow(events=WindowEvents, response=...)]
struct MyModalWindow {
    // specific fields
}
impl WindowEvents for MyWindow {
    fn on_cancel(&mut self) -> ActionRequest {
        ActionRequest::Deny
    }
}

}

Execution flow

Normaly, a modal window looks like the following template:

#![allow(unused)]
fn main() {
// ResponseType is a type of data that you want to return
// it could be anything like: u32, String, something user-defined
#[ModalWindow(events=..., response=ResponseType)]
struct MyWindow {
    // specific fields
}
impl MyWindow {
    fn new(...) -> MyWindow { /* constructor */ }
    // other methods
}
// SomeEvent in this context could be any event supported by a Window
// such as: ButtonEvents, ToolBarEvents, ...
impl SomeEvent for MyWindow {
    fn event_methods(&mut self...) {
        // some operations / checks
        self.exit_with(ResponseType::new(...)); // ResponseType::new(...) something that creates a new object of type ResponseType
    }
}
}

Once all of this is in place, you can start the modal window in the following way:

#![allow(unused)]
fn main() {
let r: ResponseType = MyWindow::new(...).show();
}

Example

The following example creates a window with a button that starts a modal window that doubles a value received from the first window.

#[ModalWindow(events=ButtonEvents,response=i32)]
struct MyModalWin {
    value: i32,
}
impl MyModalWin {
    fn new(value: i32) -> Self {
        let mut w = MyModalWin {
            base: ModalWindow::new("Calc", Layout::new("d:c,w:40,h:12"), window::Flags::None),
            value: value * 2,
        };
        w.add(Label::new(format!("{} x 2 = {}", value, value * 2).as_str(), Layout::new("d:c,w:16,h:1")));
        w.add(button!("Close,d:b,w:15"));
        w
    }
}
impl ButtonEvents for MyModalWin {
    fn on_pressed(&mut self, _handle: Handle<Button>) -> EventProcessStatus {
        self.exit_with(self.value);
        EventProcessStatus::Processed
    }
}

#[Window(events = ButtonEvents)]
struct MyWin {
    text: Handle<Label>,
    value: i32,
}

impl MyWin {
    fn new() -> Self {
        let mut win = MyWin {
            base: window!("'My Win',d:c,w:40,h:16"),
            text: Handle::None,
            value: 1,
        };
        win.text = win.add(label!("'Value=10',d:c,w:24,h:1"));
        win.add(button!("Double,d:b,w:15"));
        win
    }
}
impl ButtonEvents for MyWin {
    fn on_pressed(&mut self, _handle: Handle<Button>) -> EventProcessStatus {
        // first run the modal window
        if let Some(response) = MyModalWin::new(self.value).show() {
            // set the new value
            self.value = response;
            let h = self.text;
            if let Some(label) = self.control_mut(h) {
                label.set_caption(format!("Value={}", response).as_str());
            }
        }
        EventProcessStatus::Processed
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

Single Window Apps

A single window app is an AppCUI application where you only have one window that ocupies the entire desktop. Usually, when you create a AppCUI app, you can add multiple windows to a desktop object. In this mode you can only add one window, and terminating that window will close the app.

To do this you need to use the .single_window() method from the App builder as follows:

let mut app = App::new().single_window().build()?;
// add one and only one window
app.add_window(...);
// run the application
app.run()

Remarks

  • in a Single Window mode you can not set a custom desktop as there is only one window and it covers the entire visible size of a desktop. Using a .desktop(...) method with a .single_window() method will result in a panic:
    // the following code wil panic
    App::new().single_window().desktop(...).build()?
    
  • Since in a Single Window mode there is only one window , you can not use the .add_window(...) method twice. Using it wll result in a panic.
    let mut a = App::new().single_window()..build()?;
    a.add_window(...);
    // the following line will panic as there is alreay a window added
    a.add_window(...); // panic
    a.run();
    
  • Since in a Single Window mode the window ocupies the entire visible size of a desktop, you can not resize or move it. As such, window flag attributes like Sizeable are not allowed. If used, the code will panic. The layout (regardless on how you set it up) will be changed to make sure that the window ocupies the entire visible desktop space.
    let mut a = App::new().single_window()..build()?;
    // the following line will panic as Sizeable flag is not allow on windows in Single Window mode
    a.add_window(window!("Test,d:c,flags: Sizeable"));
    a.run();
    
  • In a Single Window mode, the event loop will be associated with the single window. As such, not adding a window will result in a panic.
    let mut a = App::new().single_window()..build()?;
    // the following line will panic no window was added
    a.run();
    

Stock controls

AppCUI comes with a set of out-of-the-box controls that can be used:

ControlClassMacroImage
Accordionui::Accordionaccordion!
Buttonui::Buttonbutton!
Canvasui::Canvascanvas!
CheckBoxui::CheckBoxcheckbox!
ColorPickerui::ColorPickercolorpicker!
ComboBoxui::ComboBoxcombobox!
DatePickerui::DatePickerdatepicker!
DropDownListui::DropDownList<T>dropdownlist!
HLineui::HLinehline!
HSplitterui::HSplitterhsplitter!
ImageViewerui::ImageViewerimageviewer!
KeySelectorui::KeySelectorkeyselector!
Labelui::Labellabel!
ListBoxui::ListBoxlistbox!
ListViewui::ListView<T>listview!
Markdownui::Markdownmarkdown!
NumericSelectorui::NumericSelector<T>numericselector!
Panelui::Panelpanel!
Passwordui::Passwordpassword!
PathFinderui::PathFinderpathfinder!
ProgressBarui::ProgressBarprogressbar!
RadioBoxui::RadioBoxradiobox!
Selectorui::Selector<T>selector!
Tabui::Tabtab!
TextAreaui::TextAreatextarea!
TextFieldui::TextFieldtextfield!
ThreeStateBoxui::ThreeStateBoxthreestatebox!
ToggleButtonui::ToggleButtontogglebutton!
TreeViewui::TreeView<T>treeview!
VLineui::VLinevline!
VSplitterui::VSplittervsplitter!

Accordion

An accordion control is a graphical user interface element that consists of a vertically stacked list of panels. Only one panel can be "expanded" to reveal its associated content.

To create an accordion use Accordion::new methods:

let a1 = Accordion::new(Layout::new("d:c,w:15,h:10"),accordion::Flags::None);

or the macro accordion!

let a2 = accordion!("d:c,w:15,h:10,panels:[First,Second,Third]");
let a3 = accordion!("d:c,w:15,h:10,panels:[A,B,C],flags:TransparentBackground");

The caption of each accordion may contain the special character & that indicates that the next character is a hot-key. For example, constructing a accordion panel with the following caption &Start will set up the text of the accordion to Start and will set up character S as the hot key to activate that accordion panel.

A accordion supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
flagsListNoAccordion initialization flags (available list include: TransparentBackground)
panelsListNoA list of accordion panels

A accordion supports the following initialization flags:

  • accordion::Flags::TransparentBackground or TransparentBackground (for macro initialization) - this will not draw the background of the accordion

Some examples that uses these paramateres:

let t1 = accordion!("panels:[Tab1,Tab2,Accordion&3],d:c,w:100%,h:100%");
let t2 = accordion!("panels:[A,B,C],flags:TransparentBackground,d:c,w:100%,h:100%");

Events

This control does not emits any events.

Methods

Besides the Common methods for all Controls a accordion also has the following aditional methods:

MethodPurpose
add_panel(...)Adds a new accordion panel
add(...)Add a new control into the accordion (the index of the accordion where the control has to be added must be provided)
current_panel()Provides the index of the current accordion panel
set_current_panel(...)Sets the current accordion panel (this method will also change the focus to the accordion cotrol)
panel_caption(...)Returns the caption (name) or a accordion panel based on its index
set_panel_caption(...)Sets the caption (name) of a accordion panel

Key association

The following keys are processed by a Accordion control if it has focus:

KeyPurpose
Ctrl+TabSelect the next accordion. If the current accordion is the last one, the first one will be selected.
Ctrl+Shift+TabSelect the previous accordion. If the current accordion is the first one, the last one will be selected

Aditionally, Alt+letter or number will automatically select the accordion with that particular hotkey combination.

Example

The following code creates an accordion with 3 panels and adds two buttons on each accordion panel.

use appcui::prelude::*;


fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    let mut w = window!("Test,d:c,w:100%,h:100%");
    let mut t = accordion!("l:1,t:1,r:1,b:3,panels:['Panel &1','Panel &2','Panel &3']");
    t.add(0, button!("T1-1-A,r:1,b:0,w:10,type:flat"));
    t.add(0, button!("T1-1-B,d:c,w:10,type:flat"));      
    t.add(1, button!("T1-2-A,r:1,b:0,w:14,type:flat"));
    t.add(1, button!("T1-2-B,d:c,w:14,type:flat")); 
    t.add(2, button!("T1-3-A,r:1,b:0,w:20,type:flat"));
    t.add(2, button!("T1-3-B,d:l,w:20,type:flat"));  
    w.add(t); 

    w.add(button!("OK,r:0,b:0,w:10, type: flat"));
    w.add(button!("Cancel,r:12,b:0,w:10, type: flat"));

    a.add_window(w);
    a.run();
    Ok(())
}

Button

Represent a clickable button control:

To create a button use Button::new method (with 3 parameters: a caption, a layout and initialization flags).

let b = Button::new("&Start", Layout::new("x:10,y:5,w:15"),button::Flags::None);

or the macro button!

let b1 = button!("caption=&Start,x:10,y:5,w:15");
let b2 = button!("&Start,x:10,y:5,w:15");

The caption of a button may contain the special character & that indicates that the next character is a hot-key. For example, constructing a button with the following caption &Start will set up the text of the button to Start and will set up character S as the hot key for that button (pressing Alt+S will be equivalent to pressing that button).

A button supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
name or text or captionStringYes (first postional parameter)The caption (text) written on a button
typeStringNoButton type

A button supports the following initialization types:

  • button::Type::Flat or flat (for macro initialization) - thils will hide the shaddow of the button makeing it flat.

Some examples that uses these paramateres:

let disabled_button = button!("caption=&Disabled,x:10,y:5,w:15,enable=false");
let hidden_button = button!("text='&Hidden',x=9,y:1,align:center,w:9,visible=false");
let flat_button = button!("&flat,x:1,y:1,w:10,type:flat");

Events

To intercept events from a button, the following trait has to be implemented to the Window that processes the event loop:

pub trait ButtonEvents {
    fn on_pressed(&mut self, button: Handle<Button>) -> EventProcessStatus {...}
}

Methods

Besides the Common methods for all Controls a button also has the following aditional methods:

MethodPurpose
set_caption(...)Set the new caption for a button. If the string provided contains the special character &, this method also sets the hotkey associated with a control. If the string provided does not contain the & character, this method will clear the current hotkey (if any).
Example: button.set_caption("&Start") - this will set the caption of the button cu Start and the hotket to Alt+S
caption()Returns the current caption of a button

Key association

The following keys are processed by a Button control if it has focus:

KeyPurpose
SpaceClicks / pushes the button and emits ButtonEvents::on_pressed(...) event. It has the same action clicking the button with the mouse.
EnterClicks / pushes the button and emits ButtonEvents::on_pressed(...) event. It has the same action clicking the button with the mouse.

Aditionally, Alt+letter or number will have the same action (even if the Button does not have a focus) if that letter or nunber was set as a hot-key for a button via its caption. For example, creating a value with the following caption: "My b&utton" (notice the & character before letter u) will enable Alt+U to be a hot-key associated with this button. Pressing this combination while the button is enabled and part of the current focused window, will change the focus to that button and will emit the ButtonEvents::on_pressed(...) event.

Example

The following code creates a window with two buttons (Add and Reset). When Add button is pressed a number is incremented and set as the text of the Add button. When Reset button is being pressed, the counter is reset to 0.

use appcui::prelude::*;

#[Window(events = ButtonEvents)]
struct MyWin {
    add: Handle<Button>,
    reset: Handle<Button>,
    counter: i32,
}

impl MyWin {
    fn new() -> Self {
        let mut win = MyWin {
            base: Window::new("My Win", Layout::new("d:c,w:40,h:6"), window::Flags::None),
            add: Handle::None,
            reset: Handle::None,
            counter: 0,
        };
        win.add = win.add(Button::new("Add (0)", Layout::new("x:25%,y:2,w:13,a:c"), button::Type::Normal));
        win.reset = win.add(Button::new("&Reset", Layout::new("x:75%,y:2,w:13,a:c",), button::Type::Normal));
        win
    }
    fn update_add_button_caption(&mut self) {
        let h = self.add;
        let new_text = format!("Add ({})",self.counter);
        if let Some(button) = self.control_mut(h) {
            button.set_caption(new_text.as_str());
        }
    }
}

impl ButtonEvents for MyWin {
    fn on_pressed(&mut self, button_handle: Handle<Button>) -> EventProcessStatus {
        if button_handle == self.add {
            // 'Add' button was pressed - lets increment the counter
            self.counter += 1;
            self.update_add_button_caption();
            return EventProcessStatus::Processed;
        }
        if button_handle == self.reset {
            // 'Reset` button was pressed - counter will become 0
            self.counter = 0;
            self.update_add_button_caption();
            return EventProcessStatus::Processed;
        }
        // unknown handle - we'll ignore this event
        EventProcessStatus::Ignored
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    app.add_window(MyWin::new());
    app.run();
    Ok(())
}

Canvas

Represent a surface that can be drawn under a view-port:

To create a canvas use Canvas::new method (with 3 parameters: a size, a layout and initialization flags).

let b = Canvas::new(Size::new(30,10), Layout::new("x:10,y:5,w:15"),canvas::Flags::None);

or the macro canvas!

let b1 = canvas!("30x10,x:10,y:5,w:15");
let b2 = canvas!("'30,10',x:10,y:5,w:15");

A canvas supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
size or sz or surfaceSizeYes (first postional parameter)The size of the surface within the canvas
flagsStringNocanvas initialization flags
back or backgroudchar! formatNoA character as describes in Macro Builds - the same as with the char! macro format
lsm or left-scroll-marginNumericNoThe left margin of the bottom scroll bar in characters. If not provided the default value is 0. This should be a positive number and it only has an effect if the flag Scrollbars was set up.
tsm or top-scroll-marginNumericNoThe top margin of the right scroll bar in characters. If not provided the default value is 0. This should be a positive number and it only has an effect if the flag Scrollbars was set up.

A canvas supports the following initialization flags:

  • canvas::Flags::ScrollBars or ScrollBars (for macro initialization) - thils enable a set of scrollbars that can be used to change the view of the inner surface, but only when the control has focus, as described in Components section.

Some examples that uses these paramateres:

  1. A canvas with a backgroud that consists in the character X in with Aqua and DarkBlue colors.
    let c = canvas!("size:10x5,x:10,y:5,w:15,back={X,fore:aqua,back:darkblue}");
    
  2. A canvas with scrollbars with different margins
    let c = canvas!("sz:'10 x 5',x:10,y:5,w:15,flags:Scrollbars,lsm:5,tsm:1");
    

Events

A canvas emits no events.

Methods

Besides the Common methods for all Controls a canvas also has the following aditional methods:

MethodPurpose
drawing_surface_mut(...)Returns the inner surface that can be dranw into the canvas
resize_surface(...)Resizes the inner surface of the canvas
set_backgound(...)Sets the character used for background
clear_background()Remove the background character making the background transparent.

Key association

The following keys are processed by a canvas control if it has focus:

KeyPurpose
Left,Right,Up,DownMove the view port to a specified direction by one character.
Shift+LeftMoves the horizontal view port coordonate to 0
Shift+UpMoves the vertical view port coordonate to 0
Shift+RightMoves the horizontal view port coordonate so that the right side of the inner surface is displayed
Shift+DownMoves the vertical view port coordonate so that the bottom side of the inner surface is displayed
Ctrl+{Left,Right,Up,Down}Move the view port to a specified direction by a number of characters that is equal to the width for Left/Right or height for Up/Down.
PageUp, PageDownhas the same effect as Ctrl+{Up or Down}
HomeMoves the view port to the coordonates (0,0)
EndMoves the view port so that the bottom-right part of the inner surface is visible

Example

The following code uses a canvas to create a viewer over the Rust language definition from wikipedia:

use appcui::prelude::*;

static text: &str = r"--- From Wiki ----
Rust is a multi-paradigm, general-purpose 
programming language that emphasizes performance, 
type safety, and concurrency. It enforces memory 
safety—meaning that all references point to valid 
memory—without a garbage collector. To 
simultaneously enforce memory safety and prevent 
data races, its 'borrow checker' tracks the object 
lifetime of all references in a program during 
compilation. Rust was influenced by ideas from 
functional programming, including immutability, 
higher-order functions, and algebraic data types. 
It is popular for systems programming.

From: https://en.wikipedia.org/wiki/Rust_(programming_language)
";
fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().size(Size::new(60,20)).build()?;
    let mut w = window!("Title,d:c,w:40,h:8,flags:Sizeable");
    let mut c = canvas!("'60x15',d:c,w:100%,h:100%,flags=ScrollBars,lsm:3,tsm:1");
    let s = c.drawing_surface_mut();
    s.write_string(0, 0, text, CharAttribute::with_color(Color::White, Color::Black), true);
    w.add(c);
    a.add_window(w);
    a.run();
    Ok(())
}

CheckBox

Represent a control with two states (checked and unckehed):

To create a checkbox use CheckBox::new method (with 3 parameters: a caption, a layout and checked status (true or false)) or method CheckBox::with_type (with one aditional parameter - the type of the checblx).

let b1 = CheckBox::new("A checkbox", 
                       Layout::new("x:10,y:5,w:15"),
                       true);
let b2 = CheckBox::with_type("Another checkbox", 
                             Layout::new("x:10,y:5,w:15"),
                             false,
                             checkbox::Type::YesNo);

or the macro checkbox!

let c1 = checkbox!("caption='Some option',x:10,y:5,w:15,h:1");
let c2 = checkbox!("'Another &option',x:10,y:5,w:15,h:1,checked:true");
let c3 = checkbox!("'&Multi-line option\nthis a hot-key',x:10,y:5,w:15,h:3,checked:false");
let c4 = checkbox!("'&YesNo checkbox',x:10,y:5,w:15,h:3,checked:false,type: YesNo");

The caption of a checkbox may contain the special character & that indicates that the next character is a hot-key. For example, constructing a checkbox with the following caption &Option number 1 will set up the text of the checkbox to Option number 1 and will set up character O as the hot key for that checkbox (pressing Alt+O will be equivalent to changing the status for that checkbox from checked to unchecked or vice-versa).

A checkbox can contain a multi-line text but you will have to set the height parameter large enough to a larger value (bigger than 1).

A checkbox supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
text or captionStringYes (first postional parameter)The caption (text) written on a checkbox
checked or checkBoolNoCheckbox checked status: true for false
typeStringNoThe type of the checkbox (see below)

Some examples that uses these paramateres:

let disabled_checkbox = checkbox!("caption=&Disabled,x:10,y:5,w:15,enable=false");
let hidden_checkbox = checkbox!("text='&Hidden',x=9,y:1,align:center,w:9,visible=false");
let multi_line_checkbox = checkbox!("'&Multi line\nLine2\nLine3',x:1,y:1,w:10,h:3");

The type of a checkbox is described by the checkbox::Type enum:

#![allow(unused)]
fn main() {
#[derive(Copy,Clone,PartialEq,Eq)]
pub enum Type {
    Standard, // Default value
    Ascii,
    CheckBox,
    CheckMark,
    FilledBox,
    YesNo,
    PlusMinus,
}
}

The type of the checkbox describes how the checkbox state (checked or unchecked) will be represented on the screen.

TypeCheck StateUncheck State
Standard[✓] Checked[ ] Unchecked
Ascii[X] Checked[ ] Unchecked
CheckBox☑ Checked☐ Unchecked
CheckMark✔ Checked✖ Unchecked
FilledBox▣ Checked▢ Unchecked
YesNo[Y] Checked[N] Unchecked
PlusMinus➕ Checked➖ Unchecked

Events

To intercept events from a checkbox, the following trait has to be implemented to the Window that processes the event loop:

pub trait CheckBoxEvents {
    fn on_status_changed(&mut self, handle: Handle<CheckBox>, checked: bool) -> EventProcessStatus {...}
}

Methods

Besides the Common methods for all Controls a checkbox also has the following aditional methods:

MethodPurpose
set_caption(...)Set the new caption for a checkbox. If the string provided contains the special character &, this method also sets the hotkey associated with a control. If the string provided does not contain the & character, this method will clear the current hotkey (if any).
Example: checkbox.set_caption("&Option") - this will set the caption of the checkbox cu Option and the hotkey to Alt+O
caption()Returns the current caption of a checbox
is_checked()true if the checkbox is checked, false otherwise
set_checked(...)Sets the new checked status for the checkbox

Key association

The following keys are processed by a Checkbox control if it has focus:

KeyPurpose
Space or EnterChanges the checked state (checked to un-checked and vice-versa). It also emits CheckBoxEvents::on_status_changed(...) event with the checked parameter the current chcked status of the checkbox. It has the same action clicking the checkbox with the mouse.

Aditionally, Alt+letter or number will have the same action (even if the checkbox does not have a focus) if that letter or nunber was set as a hot-key for a checkbox via its caption.

Example

The following code creates a window with a checkbox and a label. Whenever the checkbox status is being change, the label will print the new status (checked or not-checked).

#[Window(events = CheckBoxEvents)]
struct MyWin {
    c: Handle<CheckBox>,
    l: Handle<Label>,
}

impl MyWin {
    fn new() -> Self {
        let mut win = MyWin {
            base: window!("'My Win',d:c,w:40,h:6"),
            c: Handle::None,
            l: Handle::None,
        };
        win.c = win.add(checkbox!("'My option',l:1,r:1,b:1"));
        win.l = win.add(label!("'<no status>',l:1,r:1,t:1"));
        win
    }
}

impl CheckBoxEvents for MyWin {
    fn on_status_changed(&mut self, _handle: Handle<CheckBox>, checked: bool) -> EventProcessStatus {
        let handle = self.l;
        let l = self.control_mut(handle).unwrap();
        if checked {
            l.set_caption("Status: Checked");
        } else {
            l.set_caption("Status: Not-checked");
        }
        EventProcessStatus::Processed
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    app.add_window(MyWin::new());
    app.run();
    Ok(())
}

ColorPicker

Represent a control from where you can choose a color:

To create a color picker use ColorPicker::new method (with 2 parameters: a color and a layout).

let c = ColorPicker::new(Color::Green, Layout::new("x:10,y:5,w:15"));

or the macro colorpicker!

let c1 = colorpicker!("color=Red,x:10,y:5,w:15");
let c2 = colorpicker!("Darkgreen,x:10,y:5,w:15");
let c3 = colorpicker!("Yellow,x:10,y:5,w:15,visible:false");

A ColorPicker control supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
colorStringYes (first postional parameter)The color of the ColorPicker control (should be one of the following values: Black, DarkBlue, DarkGreen, Teal, DarkRed, Magenta, Olive, Silver, Gray, Blue, Green, Aqua, Red, Pink, Yellow, White or Transparent)

Events

To intercept events from a ColorPicker control, the following trait has to be implemented to the Window that processes the event loop:

pub trait ColorPickerEvents {
    fn on_color_changed(&mut self, handle: Handle<ColorPicker>, color: Color) -> EventProcessStatus {...}
}

Methods

Besides the Common methods for all Controls a ColorPicker control also has the following aditional methods:

MethodPurpose
set_color(...)Manually sets the color of the ColorPicker control. It receives an object of type Color.
color()Returns the current color selected in the ColorPicker control

Key association

The following keys are processed by a ColorPicker control if it has focus:

KeyPurpose
Space or EnterExpands or packs (collapses) the ColorPicker control.
Up, Down, Left, RightChanges the current selected color from the ColorPicker. Using this keys will trigger a call to ColorPickerEvents::on_color_changed(...)
EscapeOnly when the ColorPicker is expanded, it collapses the control. If the ColorPicker is already colapsed, this key will not be captured (meaning that one of the ColorPicker ancestors will be responsable with treating this key)

Example

The following example creates a Window with a ColorPicker and a label. Every time the color from the ColorPicker is being changed, the label caption will be modified with the name of the new color.

#[Window(events = ColorPickerEvents)]
struct MyWin {
    c: Handle<ColorPicker>,
    l: Handle<Label>,
}

impl MyWin {
    fn new() -> Self {
        let mut win = MyWin {
            base: Window::new("Test", Layout::new("d:c,w:40,h:10"), window::Flags::None),
            c: Handle::None,
            l: Handle::None,
        };
        win.l = win.add(label!("'',x:1,y:1,w:30,h:1"));
        win.c = win.add(colorpicker!("Black,x:1,y:3,w:30"));
        win
    }
}

impl ColorPickerEvents for MyWin {
    fn on_color_changed(&mut self, _handle: Handle<ColorPicker>, color: Color) -> EventProcessStatus {
        let h = self.l;
        if let Some(label) = self.control_mut(h) {
            label.set_caption(color.get_name());
            return EventProcessStatus::Processed;
        }
        return EventProcessStatus::Ignored;
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    app.add_window(MyWin::new());
    app.run();
    Ok(())
}

ComboBox

A combobox is a drop down list control that allows you to select a variant from a list os strings.

It can be created using ComboBox::new(...) or the combobox! macro.

let c1 = ComboBox::new(Layout::new("..."),combobox::Flags::None);
let c2 = ComboBox::new(Layout::new("..."),combobox::Flags::ShowDescription);
let c3 = combobox!("x:1,y:1,w:20,items=['Red','Greem','Blue']");
let c3 = combobox!("x:1,y:1,w:20,items=['Red','Greem','Blue'],index:2");

A combobox supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
flagsStringNoComboBox initialization flags
itemsListNoA list of string items to populate the combobox with.
index or selected_indexNumericNoThe index of the selected item (it should be a value between 0 and number if items - 1)

A combobox supports the following initialization flags:

  • combobox::Flags::ShowDescription or ShowDescription (for macro initialization) - thils will allow a combobox show the description of each item (if exists) when expanded

Events

To intercept events from a combobox, the following trait has to be implemented to the Window that processes the event loop:

pub trait ComboBoxEvents {
    fn on_selection_changed(&mut self, handle: Handle<ComboBox>) -> EventProcessStatus {
        EventProcessStatus::Ignored
    }
}

Methods

Besides the Common methods for all Controls a combobox also has the following aditional methods:

MethodPurpose
value()Returns the selected value of the ComboBox. If the current value is None a panic will occur
try_value()Returns an Option<&str> containint the current selected of the ComboBox.
index()Returns the selected index from the ComboBox list
set_index(...)Selects a new element from the ComboBox based on its index
add(...)Adss a new string to the list of items in the ComboBox
add_item(...)Adds a new item (value and descrition) to the list of items in the ComboBox
clear()Clears the list of items available in the ComboBox
selected_item(...)Provides immutable (read-only) access to the selected item from the ComboBox
selected_item_mut(...)Provides mutable access to the selected item from the ComboBox
has_selection()True if an item is selected, False otherwise
count()Returns the number of items available in the combo box

Remarks: Methods selected_item and seletec_item_mut return an Option over the type combobox::Item that is defined as follows:

pub struct Item { ...}

impl Item {
    pub fn new(value: &str, description: &str) -> Self {...}
    pub fn from_string(value: String, description: String) -> Self {...}
    pub fn set_value(&mut self, value: &str) {...}
    pub fn value(&self) -> &str {...}
    pub fn set_description(&mut self, description: &str) {...}
    pub fn description(&self) -> &str {...}
}

Key association

The following keys are processed by a ComboBox control if it has focus:

KeyPurpose
Space or EnterExpands or packs (collapses) the ComboBox control.
Up, Down, Left, RightChanges the current selected color from the ComboBox.
PageUp, PageDownNavigates through the list of variants page by page. If the control is not expanded, their behavior is similar to the keys Up and Down
HomeMove the selection to the first variant
EndMove the selection to the last variant or to None if AllowNoneVariant flag was set upon initialization

Besides this using any one of the following keys: A to Z and/or 0 to 9 will move the selection to the fist variant that starts with that letter (case is ignored). The search starts from the next variant after the current one. This means that if you have multiple variants that starts with letter G, pressing G multiple times will efectively switch between all of the variants that starts with letter G.

When the combobox is expanded the following additional keys can be used:

KeyPurpose
Ctrl+UpScroll the view to top. If the new view can not show the current selection, move the selection to the previous value so that it would be visible
Ctrl+DownScroll the view to bottom. If the new view can not show the current selection, move the selection to the next value so that it would be visible
EscapeCollapses the control. If the ComboBox is already colapsed, this key will not be captured (meaning that one of the ComboBox ancestors will be responsable with treating this key)

Example

The following example creates a Window with a ComboBox that was populated with various animals and their speed. Selecting one animal from the list changes the title of the window to the name of that animal.

use appcui::prelude::*;

#[Window(events = ComboBoxEvents)]
struct MyWin {}
impl MyWin {
    fn new() -> Self {
        let mut w = Self {
            base: window!("x:1,y:1,w:34,h:6,caption:Win"),
        };
        w.add(label!("'Select animal',x:1,y:1,w:30"));
        let mut c = ComboBox::new(Layout::new("x:1,y:2,w:30"), combobox::Flags::ShowDescription);
        // data from https://en.wikipedia.org/wiki/Fastest_animals
        c.add_item(combobox::Item::new("Cheetah","(120 km/h)"));
        c.add_item(combobox::Item::new("Swordfish","(97 km/h)"));
        c.add_item(combobox::Item::new("Iguana","(35 km/h)"));
        c.add_item(combobox::Item::new("Gazelle","(81 km/h)"));
        c.add_item(combobox::Item::new("Lion","(80 km/h)"));
        c.add_item(combobox::Item::new("Dog","(60 km/h)"));
        c.add_item(combobox::Item::new("Zebra","(56 km/h)"));
        w.add(c);
        w
    }
}
impl ComboBoxEvents for MyWin {
    fn on_selection_changed(&mut self, handle: Handle<ComboBox>) -> EventProcessStatus {
        let title = if let Some(cb) = self.control_mut(handle) {
            if let Some(item) = cb.selected_item() {
                item.value().to_string()
            } else {
                String::from("[None]")
            }
        } else {
            String::from("?")
        };
        self.set_title(&title);
        EventProcessStatus::Processed
    }
}


fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

DatePicker

Represent a control from where you can choose a color:

To create a color picker use DatePicker::new method (with 2 parameters: a date and a layout), or it can be created with DatePicker::with_date method (with 2 parameters: a NaiveDate object and a layout).

let d = DatePicker::new("2024-06-13", Layout::new("d:c,w:19"));
let d = DatePicker::with_date(NaiveDate::from_ymd_opt(2000, 10, 1).unwrap(), Layout::new("d:c,w:19"));

or the macro datepicker!

let d1 = datepicker!("2024-06-13,x:1,y:1,w:19");
let d2 = datepicker!("date:2024-06-13,x:1,y:1,w:19");

A DatePicker control supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
dateStringYes (first postional parameter)The initial date of the DatePicker in YYYY-MM-DD format or any other format supported by NaiveDate in chrono crate

Events

To intercept events from a DatePicker control, the following trait has to be implemented to the Window that processes the event loop:

pub trait DatePickerEvents {
    fn on_date_change(&mut self, _handle: Handle<DatePicker>, date: chrono::prelude::NaiveDate) -> EventProcessStatus {...}
}

Methods

Besides the Common methods for all Controls a DatePicker control also has the following aditional methods:

MethodPurpose
set_date(...)Manually sets the date of the DatePicker control. It receives an object of type NaiveDate.
set_date_str(...)Manually sets the date of the DatePicker control. It receives an string slice.
date()Returns the current date selected in the DatePicker control

Key association

The following keys are processed by a DatePicker control if it has focus:

On unexpanded calendar:

KeyPurpose
Space or EnterExtends(unpacks) the DatePicker control.
Up, DownChanges the date's day with 1 day.
Shift+Up, Shift+DownChanges the date's month by 1.
Ctrl+Up, Ctrl+DownChanges the date's year by 1.
Ctrl+Shift+Up, Ctrl+Shift+DownChanges the date's year by 10.

On expanded calendar:

KeyPurpose
EnterPacks (collapses) the DatePicker control, saving the date and triggering a call to DatePickerEvents::on_date_change(...).
EscapeIt collapses the control without saving the new date. If the DatePicker is already colapsed, this key will not be captured (meaning that one of the DatePicker ancestors will be responsable with treating this key)
Up, Down, Left, RightChanges the date's day with 1 (left, right) or 7(up, down) days.
Shift+Left, Shift+RightChanges the date's month by 1.
Ctrl+Left, Ctrl+RightChanges the date's year by 1.
Ctrl+Shift+Left, Ctrl+Shift+RightChanges the date's year by 10.

On both calendar types:

KeyPurpose
Letter (ex. D)Changes the date's month to the next month starting with that letter.
Shift+Letter (ex. D)Changes the date's month to the previous month starting with that letter. (Working for letters for which there are multiple months starting with it (ex. A))

Example

The following example creates a Window with a DatePicker. The window implements the DatePickerEvents to intercept DatePicker events.

use appcui::prelude::*;

use appcui::prelude::*;

#[Window(events=DatePickerEvents)]
struct MyWin {
    dp: Handle<DatePicker>,
}

impl MyWin{
    fn new() -> Self{
        let mut win = MyWin{
            base: window!("Dates,d:c,w:25,h:6"),
            dp: Handle::None,
        };
        win.dp = win.add(datepicker!("2024-06-13,x:1,y:1,w:19"));
        win
    }

}

impl DatePickerEvents for MyWin{
    fn on_date_change(&mut self, _handle: Handle<DatePicker>, date: chrono::prelude::NaiveDate) -> EventProcessStatus {
        self.base.set_title(&format!("Date: {}", date));
        EventProcessStatus::Processed                                                                        
    }
}

fn main(){
    let mut a =  App::new().build().unwrap();
    a.add_window(MyWin::new());
    a.run();
}

DropDownList

A drop down list is a templetize (generics based) control that allows you to select a variant of a list of variants of type T.

It can be create using DropDownList::new(...) , DropDownList::with_symbol(...) or the dropdownlist! macro. Using DropDownList::new(...) and DropDownList::with_symbol(...) can be done in two ways:

  1. by specifying the type for a variable:

    let s: DropDownList<T> = DropDownList::new(...);
    
  2. by using turbo-fish notation (usually when you don't want to create a separate variable for the control):

    let s = DropDownList::<T>::with_symbol(...);
    

Remarks: It is important to notice that the T type must implement a special trait DropDownListType that is defined as follows:

pub trait DropDownListType {
    fn name(&self) -> &str;
    fn description(&self) -> &str {
        ""
    }
    fn symbol(&self) -> &str {
        ""
    }
}

where:

  • name() is a method that provides a string representation (name) for a specific variant
  • description() is a method that provides a detailed description for a specific variant
  • symbol() is a method that returns a suggested symbol for a specific variant

Examples

Assuming we have the following struct: MyData thet implements the required traits as follows:

struct MyData { ... }
impl DropDownListType for MyData { ... }

then we can create a dropdown list object based on this type as follows:

let d1: DropDownList<MyData> = DropDownList::new(Layout::new("..."),dropdownlist::Flags::None);

let d2: DropDownList<MyData> = DropDownList::with_symbol(1,Layout::new("..."),dropdownlist::Flags::AllowNoneSelection);

let d3 = dropdownlist!("class:MyData,x:1,y:1,w:20");

let d4 = dropdownlist!("class:MyData,x:1,y:1,w:20, flags: AllowNoneSelection, symbolsize:1");

let d5 = dropdownlist!("MyData,x:1,y:1,w:20");

A dropdown list supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
class or typeStringYes (first postional parameter)The name of a templetized type to be used when creating the dropdown list
flagsStringNoDropDownList initialization flags
symbolsizeNumericNoThe size (width) of the symbol in characters. It can be one of the following 0 (symbol will not be displayed), 1, 2 or 3
noneStringNoThe display name for the None variant that will be displayed in the dropdown list. If not specified, the None variant will not be used

A dropdown list supports the following initialization flags:

  • dropdownlist::Flags::AllowNoneSelection or AllowNoneSelection (for macro initialization) - thils will allow a dropdown list to hold a None value (meaning that the user can select no variant)
  • dropdownlist::Flags::ShowDescription or ShowDescription (for macro initialization) - this will show the description of the selected variant in the dropdown list

Events

To intercept events from a dropdown list, the following trait has to be implemented to the Window that processes the event loop:

pub trait DropDownListEvents<T> {
    fn on_selection_changed(&mut self, handle: Handle<DropDownList<T>>) -> EventProcessStatus {...}
}

Methods

Besides the Common methods for all Controls a dropdown list also has the following aditional methods:

MethodPurpose
index()Returns the selected index from the DropDownList list
set_index(...)Selects a new element from the DropDownList based on its index
add(...)Adss a new string to the list of items in the DropDownList
clear()Clears the list of items available in the DropDownList
selected_item(...)Provides immutable (read-only) access to the selected item from the DropDownList
selected_item_mut(...)Provides mutable access to the selected item from the DropDownList
item(...)Returns a immutable reference to the item at the specified index. If the index is invalid, the code will return None
item_mut(...)Returns a mutable reference to the item at the specified index. If the index is invalid, the code will return None
has_selection()True if an item is selected, False otherwise
count()Returns the number of items available in the combo box
set_none_string(...)Sets the display name for the None variant that will be displayed in the dropdown list

Key association

The following keys are processed by a DropDownList control if it has focus:

KeyPurpose
Space or EnterExpands or packs (collapses) the DropDownList control.
Up, Down, Left, RightChanges the current selected color from the DropDownList.
PageUp, PageDownNavigates through the list of variants page by page. If the control is not expanded, their behavior is similar to the keys Up and Down
HomeMove the selection to the first variant
EndMove the selection to the last variant or to None if AllowNoneSelection flag was set upon initialization

Besides this using any one of the following keys: A to Z and/or 0 to 9 will move the selection to the fist variant that starts with that letter (case is ignored). The search starts from the next variant after the current one. This means that if you have multiple variants that starts with letter G, pressing G multiple times will efectively switch between all of the variants that starts with letter G.

When the dropdown list is expanded the following additional keys can be used:

KeyPurpose
Ctrl+UpScroll the view to top. If the new view can not show the current selection, move the selection to the previous value so that it would be visible
Ctrl+DownScroll the view to bottom. If the new view can not show the current selection, move the selection to the next value so that it would be visible
EscapeCollapses the control. If the DropDownList is already colapsed, this key will not be captured (meaning that one of the DropDownList ancestors will be responsable with treating this key)

Example

The following example creates a Window with a DropDownList from where we can select a symbol: or .

use appcui::prelude::*;

struct MyObject {
    name: String,
    description: String,
    symbol: String,
}

impl MyObject {
    fn new(name: &str, description: &str, symbol: &str) -> MyObject {
        MyObject {
            name: name.to_string(),
            description: description.to_string(),
            symbol: symbol.to_string(),
        }
    }
}

impl DropDownListType for MyObject {
    fn name(&self) -> &str {
        &self.name
    }
    fn description(&self) -> &str {
        &self.description
    }
    fn symbol(&self) -> &str {
        &self.symbol
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    let mut w = window!("x:1,y:1,w:60,h:20,title:Win");
    let mut db = DropDownList::<MyObject>::with_symbol(1, Layout::new("x:1,y:1,w:56"), dropdownlist::Flags::ShowDescription);
    db.add(MyObject::new("Heart", "(symbol of love)", "♥"));
    db.add(MyObject::new("Spade", "(used in a deck of cards)", "♠"));
    w.add(db);
    a.add_window(w);
    a.run();
    Ok(())
}

Label

Represent a label (a text):

To create a label use Label::new method (with 2 parameters: a caption and a layout).

let b = Label::new("My label", Layout::new("x:10,y:5,w:15"));

or the macro label!

let l1 = label!("caption='a caption for the label',x:10,y:5,w:15");
let l2 = label!("MyLabel,x:10,y:5,w:15");

The caption of a label may contain the special character & that indicates that the next character is a hot-key. However, as a label can not receive any input, the hotkey is meant to be for display only.

A label supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
name or text or captionStringYes (first postional parameter)The caption (text) written on the label

Events

A label emits no events.

Methods

Besides the Common methods for all Controls a label also has the following aditional methods:

MethodPurpose
set_caption(...)Set the new caption for a label. If the string provided contains the special character &, this method will highlight the next character just like a hotkey does.
Example: label.set_caption("&Start") - this will set the caption of the label to Start and highlight the first letter (S)
caption()Returns the current caption of a label

Key association

A label does not receive any input and as such it has no key associated with it.

Example

The following code creates a window with a label that contains the text Hello world !.

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    let mut w = Window::new("Title", Layout::new("d:c,w:40,h:9"), window::Flags::None);
    w.add(Label::new("Hello world !", Layout::new("d:c,w:14,h:1")));
    app.add_window(w);
    app.run();
    Ok(())
}

ListBox

A listbox is a control that displays a list of items.

It can be created using ListBox::new(...) and ListBox::with_capacity(...) methods or with the listbox! macro.

let l1 = ListBox::new(Layout::new("..."),listbox::Flags::None);
let l2 = ListBox::with_capacity(10,Layout::new("..."),listbox::Flags::ScrollBars);
let l3 = listbox!("x:1,y:1,w:20,h:10,items=['Red','Greem','Blue']");
let l4 = listbox!("x:1,y:1,w:20,h:10,items=['Red','Greem','Blue'],index:2");
let l5 = listbox!("x:1,y:1,w:20,h:10,items=['Red','Greem','Blue'],index:2, flags: ScrollBars+SearchBar");

A listbox supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
flagsStringNoListbox initialization flags
itemsListNoA list of string items to populate the listbox with.
index or selected_indexNumericNoThe index of the selected item (it should be a value between 0 and number if items - 1)
lsm or left-scroll-marginNumericNoThe left margin of the bottom scroll bar in characters. If not provided the default value is 0. This should be a positive number and it only has an effect if the flag ScrollBars or SearchBar flags were specified.
tsm or top-scroll-marginNumericNoThe top margin of the right scroll bar in characters. If not provided the default value is 0. This should be a positive number and it only has an effect if the flag ScrollBars flags was used to create the control.
em or empty-messageStringNoA message that will be displayed when the listbox is empty. This message will be centered in the listbox.

A listbox supports the following initialization flags:

  • listbox::Flags::ScrollBars or ScrollBars (for macro initialization) - this enables a set of scrollbars that can be used to navigate through the list of items. The scrollbars are visible only when the control has focus
  • listbox::Flags::SearchBar or SearchBar (for macro initialization) - this enables a search bar that can be used to filter the list of items. The search bar is visible only when the control has focus
  • listbox::Flags::CheckBoxes or CheckBoxes (for macro initialization) - this enable a set of checkboxes that can be used to select multiple items from the list.
  • listbox::Flags::AutoScroll or AutoScroll (for macro initialization) - this will automatically scroll the listbox to the last item whenever a new item is being added. This flag is usefull for scenarios where the listbox is used as a log/event viewer.
  • listbox::Flags::HighlightSelectedItemWhenInactive or HighlightSelectedItemWhenInactive (for macro initialization) - this will highlight the selected item even when the listbox does not have focus. This flag is usefull when the listbox is used as a navigation menu.

Events

To intercept events from a listbox, the following trait has to be implemented to the Window that processes the event loop:

pub trait ListBoxEvents {
    fn on_current_item_changed(&mut self, handle: Handle<ListBox>, index: usize) -> EventProcessStatus {
        EventProcessStatus::Ignored
    }
    fn on_item_checked(&mut self, handle: Handle<ListBox>, index: usize, checked: bool) -> EventProcessStatus {
        EventProcessStatus::Ignored
    }
}

Methods

Besides the Common methods for all Controls a listbox also has the following aditional methods:

MethodPurpose
add(...)Adds a new string to the list of items in the ListBox
add_item(...)Adds a new item (text and check status) to the list of items in the ListBox
clear()Clears the list of items available in the ListBox
index()Returns the selected index from the ListBox list
item(...)Returns the item from a specified index from the ListBox. If the index is invalid, None will be returned
set_index(...)Selects a new element from the ListBox based on its index
count()Returns the number of items from the ListBox
count_checked()Returns the number of checked items from the ListBox. This method will always return 0 if the flags CheckBoxes was NOT set when creating a ListBox
set_empty_message(...)Sets the message that will be displayed when the ListBox is empty
sort()Sorts the items from the ListBox alphabetically. The sorting is done based on the text of the items.
sort_by(...)Sorts the items from the ListBox based on a custom comparison function. The function should have the following signature: fn(&Item, &Item) -> Ordering

An item from the ListBox is represented by the following structure:

pub struct Item { ...}

impl Item {
    pub fn new(text: &str, checked: bool) -> Self {...}
    pub fn text(&self) -> &str {...}
    pub fn is_checked(&self) -> bool {...}
}

Key association

The following keys are processed by a ListBox control if it has focus:

KeyPurpose
Up, DownChanges the current selected item from the ListBox.
Left, RightScrolls the view to the left or to the right.
Space or EnterChecks or unchecks the current selected item from the ListBox. If the CheckBoxes flag was not set, this key will have no effect.
HomeMove the selection to the first item
EndMove the selection to the last item
PageUp, PageDownNavigates through the list of items page by page.
Ctrl+Alt+LeftScrolls the view to the left-most position
Ctrl+Alt+RightScrolls the view to the right-most position
Ctrl+Alt+UpScrolls the view to the top with one position
Ctrl+Alt+DownScrolls the view to the bottom with one position

When pressing an ascii key, the ListBox will start a search in the list of items. All items that are matched (ignoring case) will be highlighted while the rest of them will be dimmed. While in search mode, the following keys can be used to navigate through the list of items:

KeyPurpose
EnterGo to the next item that matches the search criteria. If the search criteria is not met, the search will start from the beginning.
EscapeExit the search mode.
BackspaceRemove the last character from the search criteria

Any other key used while in search mode (such as arrow keys, page up, page down, etc) will exit the search mode and will be processed as a normal key press.

Example

The following example creates a Window with a ListBox that was populated with various animals.

use appcui::prelude::*;


fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    let mut w = window!("Animals,d:c,w:50,h:11,flags: Sizeable");
    let mut p = panel!("l:1,t:1,b:1,r:1");
    let mut l = listbox!("d:c,w:100%,h:100%,flags: ScrollBars+CheckBoxes+SearchBar, lsm:2");
    l.add_item(listbox::Item::new("Dog (man best friend)", false));
    l.add_item(listbox::Item::new("Cat (independent)", true));
    l.add_item(listbox::Item::new("Elephant (the largest land animal)", false));
    l.add_item(listbox::Item::new("Giraffe (the tallest animal, can reach 5.5m)", true));
    l.add_item(listbox::Item::new("Lion (the king of the jungle)", false));
    l.add_item(listbox::Item::new("Tiger (the largest cat species)", false));
    l.add_item(listbox::Item::new("Zebra (black and white stripes)", false));
    l.add_item(listbox::Item::new("Donkey (related to horses)", false));
    l.add_item(listbox::Item::new("Cow (provides milk)", false));
    p.add(l);
    w.add(p);
    a.add_window(w);
    a.run();
    Ok(())
}

ListView

A ListView is a templetize (generics based) control that allows you to view a list of objects.

It can be created using ListView::new(...) and ListView::with_capacity(...) methods or with the listview! macro.

let l1: ListView<T> = ListView::new(Layout::new("..."),listview::Flags::None);
let l2: ListView<T> = ListView::with_capacity(10,Layout::new("..."),listview::Flags::ScrollBars);
let l3 = listview!("class: T, flags: Scrollbar, d:c, w:100%, h:100%");
let l4 = listview!("type: T, flags: Scrollbar, d:c, view:Columns(3)");
let l5 = listview!("T, d:c, view:Details, columns:[{Name,10,left},{Age,5,right},{City,20,center}]");

where type T is the type of the elements that are shown in the list view and has to implement ListItem trait.

A listview supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
class or typeStringYes, first positional parameterThe type of items that are being displayed in the ListView control.
flagsStringNoListView initialization flags
lsm or left-scroll-marginNumericNoThe left margin of the bottom scroll bar in characters. If not provided the default value is 0. This should be a positive number and it only has an effect if the flag ScrollBars or SearchBar flags were specified.
tsm or top-scroll-marginNumericNoThe top margin of the right scroll bar in characters. If not provided the default value is 0. This should be a positive number and it only has an effect if the flag ScrollBars flags was used to create the control.
view or viewmode or vmStringNoThe view mode of the ListView control (Details or Columns).
columnsListNoThe list of columns for the the ListView control.

The field columns is a list of columns that are displayed in the ListView control. Each column is a tuple with three elements: the name of the column, the width of the column in characters, and the alignment of the column (left, right, or center). The column field accespts the following parameters:

Parameter nameTypePositional parameterPurpose
caption or name or textStringYes, first positional parameterThe name of the column. If a character in the name is precedeed by the & character, that column will have a hot key associated that will allow clicking on a column via Ctrl+
width or wNumericYes, second positional parameterThe width of the column in characters.
align or alignment or aStringYes, third positional parameterThe alignment of the column (left or l, right or r, and center or c).

To create a column with the name Test, that hast Ctrl+E assigned as a hot key, with a width of 10 characters, and aligned to the right, you can use the following formats:

  • {caption: "T&est", width: 10, align: right}
  • {name: "T&est", w: 10, a: right}
  • {T&est, 10, right}
  • {T&est,10,r}

Similary, to create a listview with 3 columns (Name, Age, and City) with the widths of 10, 5, and 20 characters, respectively, and aligned to the left, right, and center, you can use the following format:

let l = listview!("T, d:c, view:Details, columns:[{Name,10,left},{Age,5,right},{City,20,center}]");

A listview supports the following initialization flags:

  • listview::Flags::ScrollBars or ScrollBars (for macro initialization) - this enables a set of scrollbars that can be used to navigate through the list of items. The scrollbars are visible only when the control has focus
  • listview::Flags::SearchBar or SearchBar (for macro initialization) - this enables a search bar that can be used to filter the list of items. The search bar is visible only when the control has focus
  • listview::Flags::CheckBoxes or CheckBoxes (for macro initialization) - this enable a set of checkboxes that can be used to select multiple items from the list.
  • listview::Flags::ShowGroups or ShowGroups (for macro initialization) - this enables the grouping of items in the list view.
  • listview::Flags::SmallIcons or SmallIcons (for macro initialization) - this enables the small icons (one character) view mode for the list view.
  • listview::Flags::LargeIcons or LargeIcons (for macro initialization) - this enables the large icons (two characters or unicode surrogates) view mode for the list view.
  • listview::Flags::CustomFilter or CustomFilter (for macro initialization) - this enables the custom filter that can be used to filter the list of items. The custom filter should be provided by the user in the ListItem implementation.
  • listview::Flags::NoSelection or NoSelection (for macro initialization) - this disables the selection of items from the list view. This flag is useful when the list view is used only for displaying information and the selection is not needed (such as a Save or Open file dialog). Using this flag together with the CheckBoxes flag will result in a panic.

Events

To intercept events from a listview, the following trait has to be implemented to the Window that processes the event loop:

pub trait ListViewEvents<T: ListItem + 'static> {
    // called when the current item is changed
    fn on_current_item_changed(&mut self, handle: Handle<ListView<T>>) -> EventProcessStatus {
        EventProcessStatus::Ignored
    }
    
    // called when a group (if groups are enabled) is collapsed
    fn on_group_collapsed(&mut self, handle: Handle<ListView<T>>, group: listview::Group) -> EventProcessStatus {
        EventProcessStatus::Ignored
    }

    // called when a group (if groups are enabled) is expanded
    fn on_group_expanded(&mut self, handle: Handle<ListView<T>>, group: listview::Group) -> EventProcessStatus {
        EventProcessStatus::Ignored
    }

    // called when the selection is changed
    fn on_selection_changed(&mut self, handle: Handle<ListView<T>>) -> EventProcessStatus {
        EventProcessStatus::Ignored
    }

    // called when you double click on an item (or press Enter)
    fn on_item_action(&mut self, handle: Handle<ListView<T>>, item_index: usize) -> EventProcessStatus {
        EventProcessStatus::Ignored
    }
}

Methods

Besides the Common methods for all Controls a list view also has the following aditional methods:

Adding items and groups

MethodPurpose
add_group(...)Creates a new group with a specified name and return a group identifier. You can further used the group identified to add an item to a group.
add(...)Adds a new item to the ListView control.
add_item(...)Adds a new item to the ListView control. This methods allows you to specify the color, icon, group and selection state for that item.
add_items(...)Adds a vector of items to the ListView control.
add_to_group(...)Adds a vector if items to the ListView control and associate all of them to a group
add_batch(...)Adds multiple items to the listview. When an item is added to a listview, it is imediatly filtered based on the current search text. If you want to add multiple items (using various methods) and then filter them, you can use the add_batch method.
clear()Clears all items from the listview

Item manipulation

MethodPurpose
current_item()Returns an immutable reference to the current item or None if the listview is empty or the cursor is over a group.
current_item_mut()Returns a mutable reference to the current item or None if the listview is empty or the cursor is over a group.
current_item_index()Returns the index of the current item or None if the listview is empty or the cursor is over a group.
item(...)Returns an immutable reference to an item based on its index or None if the index is out of bounds.
item_mut(...)Returns a mutable reference to an item based on its index or None if the index is out of bounds.
items_count()Returns the number of items in the listview.
is_item_selected(...)Returns true if the item is selected or false otherwise. If the index is invalid, false will be returned.
selected_items_count()Returns the number of selected items in the listview.
select_item(...)Selects or deselects an item based on its index.

Group manipulation

MethodPurpose
current_group()Returns the current group or None if the cursor is not on a group or the current item does not have an associated group
group_name(...)Returns the name of a group based on its identifier or None if the identifier is invalid

Column manipulation

MethodPurpose
column(...)Returns the column based on its index or None if the index is out of bounds.
column_mut()Returns a mutable reference to the column based on its index or None if the index is out of bounds.
add_column(...)Adds a new column to the ListView control. This method is in particular usefull when you need to create a custom listview.

Miscellaneous

MethodPurpose
set_frozen_columns(...)Sets the number of frozen columns. Frozen columns are columns that are not scrolled when the listview is scrolled horizontally.
set_view_mode(...)Sets the view mode of the ListView control.
sort(...)Sorts the items in the ListView control based on a column index.
clear_search()Clears the content of the search box of the listview.

Key association

The following keys are processed by a ListView control if it has focus:

KeyPurpose
Up, DownChanges the current item from the ListView.
Left, RightScrolls the view to the left or to the right (when the view is Details or changes the current item if the view is Columns)
PageUp, PageDownNavigates through the list of items page by page.
HomeMoves the current item to the first element in the list
EndMoves the current item to the last element in the list
Shift+{Up, Down, Left, Right, PageUp, PageDown, Home, End}Selects multiple items in the list. If the flag CheckBoxes is prezent, the items will be checked, otherwise will be colored with a different color to indicare that they are selected.
InsertIf the flag CheckBoxes is present, this will toggle the selection state of the current item. Once the selection is toggled, the cursor will me moved to the next item in the list.
SpaceIf the flag CheckBoxes is present, this will toggle the selection state of the current item. If the flag CheckBoxes is not present, and the current item is a group, this will expand or collapse the group.
Ctrl+ASelects all items in the list. If the flag CheckBoxes is present, all items will be checked, otherwise will be colored with a different color to indicare that they are selected.
Ctrl+Alt+{Up, Down}Moves the scroll up or down
Enterif the current item is a group, this will expand or collapse the group. If the current item is an element from the list, this will trigger the ListViewEvents::on_item_action event.
Ctrl+{A..Z, 0..9}If a column has a hot key associated (by using the & character in the column name), this will sort all items bsed on that column. If that column is already selected, this will reverse the order of the sort items (ascendent or descendent)
Ctrl+{Left, Right}Enter in the column resize mode.

Aditionally, typing any character will trigger the search bar (if the flag SearchBar is present) and will filter the items based on the search text. While the search bar is active, the following keys are processed:

  • Backspace - removes the last character from the search text
  • Escape - clears the search text and closes the search bar
  • Movement keys (such as Up, Down, Left, Right, PageUp, PageDown, Home, End) - will disable the search bar, but will keep the search text

While in the column resize mode, the following keys are processed:

  • Left, Right - increases or decreases the width of the current column
  • Ctrl+Left, Ctrl+Right - moves the focus to the previous or next column
  • Escape or movement keys - exits the column resize mode

Groups

Groups are a way to organize items in a listview. A group is a special item that can contain other items. If the flag ShowGroups is set, the items that are part of a group are displayed below the group item and are indented to the right. The group item can be expanded or collapsed by using the Space key or by double clicking on the group item.

Sorting is done within the group (even if the flag ShowGroups is not set) and the items are sorted based on the current sorting column. The groups are always displayed at the top of the listview and are not sorted.

If the CheckBoxes flag is set, the group item will have a checkbox that can be used to select all items from the group. The selection state of the group item is updated based on the selection state of the items from the group.

To create a group use the add_group method. The method returns a group identifier that can be used to add items to that group.

A group is displayed in the following way:

that contains the following characteristics:

  • name - the name of the group
  • expanded - a button that can be used to expand or collapse the group
  • selected - a checkbox that can be used to select or deselecte all items from the group.
  • count - the right most number that indicates the number of items from the group that are visible (if the search bar is being used, this number will indicate the number of items in the group that have been filtered out)

Populating a list view

Whenever an element is being added to a listview, the listview will try to assigned to a group and if the search bar contains a filter expression, will try to filter the item based on that expression. After this steps, the listview will also try to sort the items based on the current sorting column. Aditionally, there might be cases where you want to add an item with a specific icon, selection status or color. As such, there are several methods that can be used to add items to a listview (each one design for a different scenario).

These operations can be expensive if the listview contains a large number of items. To avoid this, you can use the add_batch method that allows you to add multiple items to the listview and then filter and sort them.

Let's consder the following structure Student that represents the type of items that are displayed in the listview:

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

To add an item to the listview (assuming the listview is named lv), you can use the following methods:

  1. add - adds a new item to the listview

    lv.add(Student { name: "John", age: 20, grade: 10 });
    lv.add(Student { name: "Alice", age: 21, grade: 9 });
    
  2. add_item - adds a new item to the listview with a specific color, icon, group, and selection status

    lv.add_item(listview::Item::new(
        Student { name: "John", age: 20, grade: 10 }, 
        false,                  // not selected
        None,                   // no color
        [0 as char,0 as char],  // no icon
        listview::Group::None   // no group
    ));
    let g = lv.add_group("Group 1");
    lv.add_item(listview::Item::new(
        Student { name: "Alice", age: 22, grade: 9 }, 
        true,                                               // selected
        Some(CharAttribute::with_fore_color(Color::Red)),   // Red
        ['🐧', 0 as char],                                  // Penguin icon
        g                                                   // link to group g
    ));
    
  3. add_items - adds a vector of items to the listview. You can use this method when you don't use groups and you need to process all items after being added (this will speed up the process for large lists)

    lv.add_items(vec![
        Student { name: "John", age: 20, grade: 10 },
        Student { name: "Alice", age: 21, grade: 9 },
        Student { name: "Bob", age: 21, grade: 8 },
        Student { name: "Mike", age: 21, grade: 7 },
    ]);
    
  4. add_to_group - adds a vector of items to the listview and associates all of them to a group. You can use this method when you want to add multiple items to a group and you need to process all items after being added (this will speed up the process for large lists)

    let g = lv.add_group("Group 1");
    lv.add_to_group(vec![
        Student { name: "John", age: 20, grade: 10 },
        Student { name: "Alice", age: 21, grade: 9 },
        Student { name: "Bob", age: 21, grade: 8 },
        Student { name: "Mike", age: 21, grade: 7 },
    ], g);
    
  5. add_batch - adds multiple items to the listview. This is the most generic way to add items using the preivous methods to a listview and to filter and sort them after the adding process ends.

    lv.add_batch(|lv| {
        lv.add(Student { name: "John", age: 20, grade: 10 });
        lv.add_item(listview::Item::new(
            Student { name: "Ana", age: 21, grade: 10 }, 
            false,                  // not selected
            None,                   // no color
            [0 as char,0 as char],  // no icon
            listview::Group::None   // no group
        ));
    });
    
    

To add an item to a listview, the item type has to implement the ListItem trait. Based on the implementation of this trait, the listview will:

  • display an item based on a specification
  • filter the item based on the search text or a specific filtering algorithm
  • sort the items based on a column index
  • get a list of columns and their specifications (name, width, alignment)

View modes

The listview control has two ways to display the items:

  • Details - the items are displayed in a table with multiple columns. Each column can have a different width and alignment and contain information about various fields of the item.
  • Columns - the items are displayed in a table with multiple columns. Each column has one item and represents the fist content of the first column in the Details view mode.

You can change the view mode of the listview by using the view parameter in the listview! macro or by using the set_view_mode method programatically. The default view mode is Details.

View modeEnumExample
Detailslistview::ViewMode::Details
Columns (three)listview::ViewMode::Columns(3)

Example

The following example shows how to create a listview with a custom item type and how to add items to it. The item used in the example is a DownloadItem that has the following fields:

  • name - the name of the item
  • age - the age of the item
  • server - the server from which the item is downloaded
  • stars - the rating of the item
  • download - the download status of the item
  • created - the creation date of the item
  • enabled - a flag that indicates if the item is enabled or not
use appcui::prelude::*;

#[derive(ListItem)]
struct DownloadItem {
    #[Column(name: "&Name", width: 12, align: Left)]
    name: &'static str,
    #[Column(name: "&Age", width: 10, align: Center)]
    age: u32,
    #[Column(name: "&Server")]
    server: &'static str,
    #[Column(name: "&Stars", width: 10, align: Center, render: Rating, format:Stars)]
    stars: u8,
    #[Column(name: "Download", width:15)]
    download: listview::Status,
    #[Column(name: "Created", w: 20, align: Center, render: DateTime, format: Short)]
    created: chrono::NaiveDateTime,
    #[Column(name: "Enabled", align: Center)]
    enabled: bool,
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    let mut w = window!("Download,d:c,w:100%,h:100%,flags: Sizeable");
    let mut l = listview!("DownloadItem,d:c,view:Details,flags: ScrollBars+CheckBoxes");
    l.add(DownloadItem {
        name: "music.mp3",
        age: 21,
        server: "London",
        stars: 4,
        download: listview::Status::Running(0.5),
        created: chrono::NaiveDate::from_ymd_opt(2016, 7, 8).unwrap().and_hms_opt(9, 10, 11).unwrap(),
        enabled: true,
    });
    l.add(DownloadItem {
        name: "picture.png",
        age: 30,
        server: "Bucharest",
        stars: 3,
        download: listview::Status::Paused(0.25),
        created: chrono::NaiveDate::from_ymd_opt(2016, 7, 8).unwrap().and_hms_opt(9, 10, 11).unwrap(),
        enabled: false,
    });
    l.add(DownloadItem {
        name: "game.exe",
        age: 40,
        server: "Bucharest",
        stars: 5,
        download: listview::Status::Completed,
        created: chrono::NaiveDate::from_ymd_opt(2016, 7, 8).unwrap().and_hms_opt(9, 10, 11).unwrap(),
        enabled: true,
    });
    w.add(l);
    a.add_window(w);
    a.run();
    Ok(())
}

HLine

Represent a horizontal line:

To create a horizontal line use HLine::new method (with 3 parameters: a title, a layout and a set of flags). The flags let you choose if the line has text or if it is a double line.

let a = HLine::new("TestLine", Layout::new("x:1,y:3,w:30"), Flags::None);
let b = HLine::new("TestLine", Layout::new("x:1,y:3,w:30"), Flags::DoubleLine | Flags::HasTitle);

or the macro hline!

let hl1 = hline!("x:1,y:1,w:10");
let hl2 = hline!("TestLine,x:1,y:3,w:30,flags:DoubleLine+HasTitle");

A horizontal line supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
text or titleStringYes (first postional parameter)The title (text) written on the line
flagsEnumNoFlags to specify how the horizontal line should be drawn

Where the flags are defined as follows:

  • hline::Flags::DoubleLine or DoubleLine (for macro initialization) - this will draw a double line instead of a single one.
  • hline::Flags::HasTitle or HasTitle (for macro initialization) - this will draw a title (a text) centered on the line.

Events

A horizontal line emits no events.

Methods

Besides the Common methods for all Controls a horizontal line also has the following aditional methods:

MethodPurpose
set_title(...)Set the new title for a horizontal line.
title()Returns the current title of a label

Key association

A horizontal line does not receive any input and as such it has no key associated with it.

Example

The following code creates a window with a horizontal line that contains the text Hello world !.

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    let mut w = Window::new("Title", Layout::new("d:c,w:40,h:9"), window::Flags::None);
    
    w.add(HLine::new("Hello world !", Layout::new("x:1,y:3,w:30"), 
                                      hline::Flags::DoubleLine | hline::Flags::HasTitle));
    app.add_window(w);
    app.run();
    Ok(())
}

Horizontal Splitter

Renders a horizontal splitter that allows the user to resize the two panels it separates.

To create a horizontal splitter use HSplitter::new method or the hsplitter! macro.

#![allow(unused)]
fn main() {
let vs_1 = HSplitter::new(0.5,Layout::new("x:1,y:1,w:20,h:10"),hsplitter::ResizeBehavior::PreserveBottomPanelSize);
let vs_2 = HSplitter::new(20,Layout::new("x:1,y:1,w:20,h:10"),hsplitter::ResizeBehavior::PreserveBottomPanelSize);
}

or

#![allow(unused)]
fn main() {
let vs_3 = hsplitter!("x:1,y:1,w:20,h:10,pos:50%");
let vs_4 = hsplitter!("x:1,y:1,w:20,h:10,pos:20,resize:PreserveBottomPanelSize");
}

A horizontal splitter supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
posCoordonateYes (first postional parameter)The position of the splitter (can be an abosolute value - like 10 or a percentage like 50% )
resize or resize-behavior or on-resize or rbStringNoThe resize behavior of the splitter. Can be one of the following: PreserveTopPanelSize, PreserveBottomPanelSize or PreserveAspectRatio
min-top-height or mintopheight or mthDimensionNoThe minimum height of the top panel (in characters - e.g. 5) or as a percentage (e.g. 10%)
min-bottom-height or minbottomheight or mbhDimensionNoThe minimum height of the bottom panel (in characters - e.g. 5) or as a percentage (e.g. 10%)

A vertial splitters supports the following resize modes:

  • hsplitter::ResizeBehavior::PreserveTopPanelSize or PreserveTopPanelSize (for macro initialization) - this will keep the size of the top panel constant when resizing the splitter
  • hsplitter::ResizeBehavior::PreserveBottomPanelSize or PreserveBottomPanelSize (for macro initialization) - this will keep the size of the bottom panel constant when resizing the splitter
  • hsplitter::ResizeBehavior::PreserveAspectRatio or PreserveAspectRatio (for macro initialization) - this will keep the aspect ratio of the two panels constant when resizing the splitter

Events

A horizontal splitter emits no events.

Methods

Besides the Common methods for all Controls a horizontal splitter also has the following aditional methods:

MethodPurpose
add(...)Adds an element to the top or bottom panel of the splitter.
set_min_height(...)Sets the minimum height of the top or bottom panel.
set_position(...)Sets the position of the splitter. If an integer value is being used, the position will be considered in characters. If a flotant value (f32 or f64) is being used, the position will be considered as a percentage.
position()Returns the current position of the splitter (in characters).

Key association

The following keys are processed by a HSplitter control if it has focus:

KeyPurpose
Ctrl+UpMoves the splitter one character up
Ctrl+DownMoves the splitter one character down
Ctrl+Shift+UpMove the splitter to its top most position
Ctrl+Shift+DownMove the splitter to its bottom most position

Example

The following code creates a window with a horizontal splitter that separates two panels. The upper panel contains a panel with the text Top and the bottom panel contains a panel with the text Bottom.

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    let mut w = window!("'Horizontal Splitter',d:c,w:50,h:11,flags: Sizeable");
    let mut hs = hsplitter!("50%,d:c,w:100%,h:100%,resize:PreserveBottomPanelSize");
    hs.add(hsplitter::Panel::Top,panel!("Top,l:1,r:1,t:1,b:1"));
    hs.add(hsplitter::Panel::Bottom,panel!("Bottom,l:1,r:1,t:1,b:1"));
    w.add(hs);
    a.add_window(w);
    a.run();
    Ok(())
}

Image Viewer

Represent a image that is being rendered under a view-port:

To create a image viewer use ImageViewer::new method (with 5 parameters: an image, a layout, a rendering method, scaling and initialization flags). To undestand more on how an image is being renedered or constructed read the Images chapter.

let i = ImageViewer::new(Image::with_str(...).unwrap(), 
                         Layout::new("x:10,y:5,w:15"),
                         image::RendererType::SmallBlocks, 
                         image::Scale::NoScale, 
                         imageviewer::Flags::None);

or the macro imageviewer!

let i1 = imageviewer!("x:10,y:5,w:15,scale:50%,render:AsciiArt");
let i2 = imageviewer!("image:'|R..|,|.R.|,|..R|',x:10,y:5,w:15");

A image viewer supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
imageStringNoA string representation of an image as described in Images (Building from a string) chapter
scalePercentageNoThe scaling percentage. Acceptable values are: 100%, 50%, 33%, 25%, 20%, 10% and 5%
render or RendererType or rmEnum valuesNoThe rendering method as described in Images (Rendering) chapter
flagsStringNoimage viewer initialization flags
back or backgroudchar! formatNoA character as describes in Macro Builds - the same as with the char! macro format
lsm or left-scroll-marginNumericNoThe left margin of the bottom scroll bar in characters. If not provided the default value is 0. This should be a positive number and it only has an effect if the flag Scrollbars was set up.
tsm or top-scroll-marginNumericNoThe top margin of the right scroll bar in characters. If not provided the default value is 0. This should be a positive number and it only has an effect if the flag Scrollbars was set up.

A image viewer supports the following initialization flags:

  • image viewer::Flags::ScrollBars or ScrollBars (for macro initialization) - thils enable a set of scrollbars that can be used to change the view of the inner surface, but only when the control has focus, as described in Components section.

Some examples that uses these paramateres:

  1. A image viewer with a backgroud that consists in the character X in with Aqua and DarkBlue colors.
    let img = imageviewer!("x:10,y:5,w:15,back={X,fore:aqua,back:darkblue}");
    
  2. A image viewer with scrollbars with different margins
    let img = imageviewer!("x:10,y:5,w:15,flags:Scrollbars,lsm:5,tsm:1");
    
  3. Am ascii art image with scrollbars with different margins and 50% scaling:
    let img = imageviewer!("image:'...',x:10,y:5,w:15,flags:Scrollbars,lsm:5,tsm:1,scale:50%,render:AsciArt");
    

Events

An image viewer control emits no events.

Methods

Besides the Common methods for all Controls a image viewer also has the following aditional methods:

MethodPurpose
set_image(...)Sets a new image to be displayed in the image viewer
set_scale(...)Sets the new scale for the current image
scale()Returns the current scale of the current image
set_render_method(...)Sets the new render_method of the current image
render_method()Returns the render method (SmallBlocks, AsciiArt, ...) used to described the paint the current image
set_backgound(...)Sets the character used for background
clear_background()Remove the background character making the background transparent.

Key association

The following keys are processed by a image viewer control if it has focus:

KeyPurpose
Left,Right,Up,DownMove the view port to a specified direction by one character.
Shift+LeftMoves the horizontal view port coordonate to 0
Shift+UpMoves the vertical view port coordonate to 0
Shift+RightMoves the horizontal view port coordonate so that the right side of the inner surface is displayed
Shift+DownMoves the vertical view port coordonate so that the bottom side of the inner surface is displayed
Ctrl+{Left,Right,Up,Down}Move the view port to a specified direction by a number of characters that is equal to the width for Left/Right or height for Up/Down.
PageUp, PageDownhas the same effect as Ctrl+{Up or Down}
HomeMoves the view port to the coordonates (0,0)
EndMoves the view port so that the bottom-right part of the inner surface is visible

Example

The following code draws a heart with differemt colors using an ImageView:

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    let mut w = window!("Heart,d:c,w:15,h:7");
    let heart = Image::with_str(r#"
        |.............|
        |...rr...rr...|
        |..rrrr.rrrr..|
        |.rrrrrrrrrrr.|
        |.raaaaaaaaar.|
        |..ryyyyyyyr..|
        |   rwwwwwr   |
        |....rwwwr....|
        |.....rwr.....|
        |......r......|
    "#).unwrap();
    w.add(ImageViewer::new(
        heart,
        Layout::new("d:c"),
        image::RendererType::SmallBlocks,
        image::Scale::NoScale,
        imageviewer::Flags::None,
    ));
    a.add_window(w);
    a.run();
    Ok(())
}

KeySelector

Represent a control that can be used to select a key (including modifiers such as Alt, Shift, ...)

To create a keyselector use KeySelector::new method (with 3 parameters: a key, a layout and initialization flags).

let k = KeySelector::new(Key::from(KeyCode::F1), Layout::new("x:10,y:5,w:15"),keyselector::Flags::None);

or the macro keyselector!

let b1 = keyselector!("F1,x:10,y:5,w:15");
let b2 = keyselector!("key:Ctrl+Alt+F1,x:10,y:5,w:15,flags:ReadOnly");

A keyselector supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
keyStringYes (first postional parameter)The key (including modifiers such as Alt,Ctrl or Shift)
flagsStringNoInitialization flags that describe the behavior of the control

A keyselector supports the following initialization flags:

  • keyselector::Flags::AcceptEnter or AcceptEnter (for macro initialization) - this will intercept the Enter key (with or without modifiers) if pressed while the control has focus
  • keyselector::Flags::AcceptEscape or AcceptEscape (for macro initialization) - this will intercept the Escape key (with or without modifiers) if pressed while the control has focus. Enabling this feture must be done carefully, as Escape key is used by window or desktop to exit and intercepting it might change this behavior.
  • keyselector::Flags::AcceptTab or AcceptTab (for macro initialization) - this will intercept the Tab key (with or without modifiers) if pressed while the control has focus. Be carefull when intercepting this key, as it is being used to switch between controls.
  • keyselector::Flags::ReadOnly or ReadOnly (for macro initialization) - this will set the internal state of the control to a read-only state (meaning that keys are being intercepted, but the selection of the new key will not be possible).

Some examples that uses these paramateres:

let intercept_enter = keyselector!("Enter,x:10,y:5,w:15,flags=AcceptEnter");
let readonly_all_keys = keyselector!("x:1,y:1,w:10,flags:[AcceptEnter,AcceptTab,AcceptEscape,ReadOnly]");

Events

To intercept events from a keyselector, the following trait has to be implemented to the Window that processes the event loop:

pub trait KeySelectorEvents {
    fn on_key_changed(&mut self, handle: Handle<KeySelector>, new_key: Key, old_key: Key) -> EventProcessStatus { ... }
}

Methods

Besides the Common methods for all Controls a keyselector also has the following aditional methods:

MethodPurpose
set_key(...)Set the new key for a keyselector. You can also use Key::None here to infer no selection
key()Returns the current key of a keyselector

Key association

There are no specific key associations (all keys are intercepted expect for Enter, Escape and Tab that can be intercepted if some flags are set).

Example

The following code creates a window with a keyselector and a button that can be used to reset the keyselector key to None.

use appcui::prelude::*;
#[Window(events = ButtonEvents+KeySelectorEvents)]
struct MyWin {
    reset: Handle<Button>,
    ks: Handle<KeySelector>,
    lb: Handle<Label>,
}

impl MyWin {
    fn new() -> Self {
        let mut win = MyWin {
            base: window!("'Key Selector example',d:c,w:40,h:9"),
            reset: Handle::None,
            ks: Handle::None,
            lb: Handle::None,
        };
        win.reset = win.add(button!("&Reset,x:50%,y:6,a:c,w:15"));
        win.ks = win.add(keyselector!("x:1,y:3,w:36"));
        win.lb = win.add(label!("<none>,x:1,y:1,w:35"));
        win
    }
    fn update_info(&mut self) {
        let key = self.control(self.ks).map(|obj| obj.key()).unwrap_or(Key::None);
        let s = if key == Key::None {
            "<none>".to_string()
        } else {
            format!("New key is: {}{}", key.modifier.name(), key.code.name())
        };
        let h = self.lb;
        if let Some(label) = self.control_mut(h) {
            label.set_caption(&s);
        }
    }
}

impl ButtonEvents for MyWin {
    fn on_pressed(&mut self, _: Handle<Button>) -> EventProcessStatus {
        // reset button was pressed
        let h = self.ks;
        if let Some(k) = self.control_mut(h) {
            k.set_key(Key::None);
        }
        self.update_info();
        EventProcessStatus::Processed
    }
}
impl KeySelectorEvents for MyWin {
    fn on_key_changed(&mut self, _handle: Handle<KeySelector>, _new_key: Key, _old_key: Key) -> EventProcessStatus {
        self.update_info();
        EventProcessStatus::Processed
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    app.add_window(MyWin::new());
    app.run();
    Ok(())
}

Markdown

Represent a control that can properly display .md (Markdown) text.

To create a canvas use Markdown::new method (with 3 parameters: content, layout and initialization flags).

let m = Markdown::new(&content,Layout::new("d: c"),markdown::Flags::ScrollBars);

or the macro canvas!

let mut m = markdown!(
        "'\r\n\
        # Markdown Control\r\n\r\n\
        ## Code Blocks  \r\n\r\n\
        Inline code: `let x = 10;`\r\n\r\n\
        Block code:\r\n\r\n\
        ```\r\n\
        fn main() {\r\n\
            println!(\"Hello, world!\");\r\n\
        }\r\n\
        ```\r\n\r\n\
        **etc.**\r\n', 
        d: c, 
        flags: ScrollBars"
    );

A markdown control supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
content or textStringYes (first positional parameter)The content of the markdown control
flagsFlagsNoCanvas initialization flags
left-scroll-margin or lsmIntegerNoDefines the left margin of the bottom scroll bar in characters. If not provided, the default value is 0. Only applies if ScrollBars is set.
top-scroll-margin or tsmIntegerNoDefines the top margin of the right scroll bar in characters. If not provided, the default value is 0. Only applies if ScrollBars is set.

A markdown supports the following initialization flags:

  • markdown::Flags::ScrollBars or ScrollBars (for macro initialization) - thils enable a set of scrollbars that can be used to change the view of the inner surface, but only when the control has focus, as described in Components section.

Some examples that uses these paramateres:

  1. A markdown control with two headers, inline code, a code block and the text etc. marcked as bold.
    let mut m = markdown!(
        "'\r\n\
        # Markdown Control\r\n\r\n\
        ## Code Blocks  \r\n\r\n\
        Inline code: `let x = 10;`\r\n\r\n\
        Block code:\r\n\r\n\
        ```\r\n\
        fn main() {\r\n\
            println!(\"Hello, world!\");\r\n\
        }\r\n\
        ```\r\n\r\n\
        **etc.**\r\n', 
        d: c, 
        flags: ScrollBars"
    );
    
  2. A markdown control containging a table and having scrollbars with different margins.
    let mut m = markdown!(
        "'\r\n\
        # Markdown Control\r\n\r\n\
        | Column 1 | Column 2|\r\n\
        | - | --- |\r\n\
        | Cell 1, Row 1| Cell 2, Row 1 |\r\n\
        | Cell 1, Row 2 | Cell 1, Row 2 |\r\n\
        ', 
        d: c, 
        flags: ScrollBars,
        lsm:10,tsm:1"
    );
    

Events

To intercept events from a markdown control, the following trait has to be implemented to the Window that processes the event loop:

pub trait MarkdownEvents {
    fn on_external_link(&mut self, markdown: Handle<Markdown>, link: &str) -> 
    EventProcessStatus {...}
}

Methods

Besides the Common methods for all Controls a canvas also has the following aditional methods:

MethodPurpose
set_content(...)Replaces the canvas content with new data. It resets the drawing coordinates (x, y) and re-parses the content.

Key association

The following keys are processed by a markdown control if it has focus:

KeyPurpose
Left,Right,Up,DownMove the view port to a specified direction by one character.

Example

The following code uses a markdown control to create a documentation viewer for the Rust programming language:

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;

    let mut w = window!("Markdown,d:c,w:50,h:15,flags:sizeable");
    let m = markdown!(
        "'''
        \r\n\
        # Rust Programming Language\r\n\
        \r\n\
        Rust is a modern systems programming language that emphasizes memory safety, concurrency, and performance.\r\n\
        \r\n\
        ## Table of Contents\r\n\
        - [Introduction](#introduction)\r\n\
            - [What Makes Rust Unique?](#what-makes-rust-unique)\r\n\
        - [Features](#features)\r\n\
            - [Ownership and Borrowing](#ownership-and-borrowing)\r\n\
            - [Concurrency](#concurrency)\r\n\
        \r\n\
        ## Introduction\r\n\
        \r\n\
        Rust is a statically typed language designed to eliminate common programming errors at compile time while delivering high performance.\r\n\
        \r\n\
        ### What Makes Rust Unique?\r\n\
        \r\n\
        - **Memory Safety**: Rust's ownership model prevents null pointer dereferences and data races.\r\n\
        - **Concurrency**: Built-in support for safe, concurrent programming.\r\n\
        - **Performance**: Delivers speed comparable to C/C++.\r\n\
        - **Modern Syntax**: Offers clear, expressive code that is easy to maintain.\r\n\
        \r\n\
        ## Features\r\n\
        \r\n\
        Rust provides several advanced features that set it apart:\r\n\
        \r\n\
        ### Ownership and Borrowing\r\n\
        \r\n\
        Rust enforces strict rules for how memory is accessed and managed, ensuring that bugs like use-after-free and data races are caught at compile time.\r\n\
        \r\n\
        ### Concurrency\r\n\
        \r\n\
        Rust's design promotes safe concurrency, enabling multithreaded programming without the typical pitfalls of shared mutable state.\r\n\
        \r\n\
        Inline code example: `let x = 10;`\r\n\
        \r\n\
        Block code example:\r\n\
        \r\n\
        ```\r\n\
        fn main() {\r\n\
            println!(\"Hello, world!\");\r\n\
        }\r\n\
        ```\r\n\
        \r\n\
        | Feature           | Description                                                          |\r\n\
        | ----------------- | -------------------------------------------------------------------- |\r\n\
        | Memory Safety     | Prevents null pointers and data races through ownership rules.       |\r\n\
        | Concurrency       | Enables safe multithreading with minimal runtime overhead.           |\r\n\
        | Performance       | Optimized for high-performance, low-level systems programming.       |\r\n\
        | Expressive Syntax | Modern syntax that enhances code clarity and maintainability.         |\r\n\
        ''',d: c,flags: ScrollBars,lsm:10,tsm:1"
    );
    w.add(m);
    a.add_window(w);
    a.run();
    Ok(())
}

NumericSelector

The NumericSelector control is a simple control that allows the user to select a number from a range of numbers. The control is made up of a text field and two buttons, one to increase the number and one to decrease it. The control can be used to select a number from a range of numbers.

It can be create using NumericSelector::new(...), NumericSelector::with_format(...) or the numericselector! macro. Using NumericSelector::new(...) can be done in two ways:

  1. by specifying the type for a variable:

    let s: NumericSelector<T> = NumericSelector::new(...);
    
  2. by using turbo-fish notation (usually when you don't want to create a separate variable for the control):

    let s = NumericSelector::<T>::new(...);
    

Remarks: The type T can be one of the following: i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, usize, isize, f32, f64.

Examples

Assuming we want to create a NumeicSelector for i32 type, we can do it as follows:

let n1: NumericSelector<i32> = NumericSelector::new(Layout::new("..."),numericselector::Flags::None);
let n2: NumericSelector<i32> = NumericSelector::with_format(1,Layout::new("..."),numericselector::Flags::None, numericselector::Format::Percentage);
let n3 = numericselector!("class:i32,value:5,min:0,max:10,step:1,x:1,y:1,w:20");
let n4 = numericselector!("u32,5,0,10,step:1,x:1,y:1,w:20,format:Percentage");
let n5 = numericselector!("i32,5,0,10,step:1,x:1,y:1,w:20,flags:ReadOnly,format:Percentage");

A numeric selector supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
class or typeStringYes (first postional parameter)The name of a templetized type to be used when creating the numeric selector
valueStringYes (second positional parameter)The initial value of the numeric selector. If it is not within the bounds (min and max parameters) it will be adjusted to the closest limit.
minStringYes (third positional parameter)The minimum value that the numeric selector can have. If the initial value is less than this, it will be adjusted to this value.
maxStringYes (fourth positional parameter)The maximum value that the numeric selector can have. If the initial value is greater than this, it will be adjusted to this value.
stepStringYes (fifth positional parameter)The step by which the value of the numeric selector will be increased or decreased.
flagsStringNoNumeric selector initialization flags
format or nf or numericformatStringNoThe format in which the value of the numeric selector will be displayed.

A numeric selector supports the following initialization flags:

  • numericselector::Flags::ReadOnly or ReadOnly (for macro initialization) - this will make the numeric selector read-only
  • numericselector::Flags::HideButtons or HideButtons (for macro initialization) - this will hide the buttons that allow the user to increase or decrease the value

The format parameter can be one of the following:

  • numericselector::Format::Decimal or Decimal (for macro initialization) - this will display the value as it is
  • numericselector::Format::Percentage or Percentage (for macro initialization) - this will display the value as a percentage
  • numericselector::Format::Hex or Hex (for macro initialization) - this will display the value as a hexadecimal number
  • numericselector::Format::DigitGrouping or DigitGrouping (for macro initialization) - this will display the value with digit grouping (for example: 1,000,000)
  • numericselector::Format::Size or Size (for macro initialization) - this will display the value as a size (for example: 1.5 KB, 1.5 MB, 1.5 GB, 1.5 TB)

Events

To intercept events from a numeric selector, the following trait has to be implemented to the Window that processes the event loop:

pub trait NumericSelectorEvents<T> {
    fn on_value_changed(&mut self, handle: Handle<NumericSelector<T>>, value: T) -> EventProcessStatus {...}
}

Methods

Besides the Common methods for all Controls a numeric selector also has the following aditional methods:

MethodPurpose
set_value(...)Sets the new value associated with the selector. The value will be adjusted to the min / max parameters. This method will do nothing if the control was created in a read-only mode
value()Returns the current value of the control.

Key association

The following keys are processed by a NumericSelector control if it has focus:

KeyPurpose
EnterEither enters the edit mode, or if already in edit mode validates the new value and sets it up
Up, LeftDecreases the value using the step parameter. If the new value is less than the min parameter, it will be adjusted to this value
Down, RightIncreases the value using the step parameter. If the new value is greater than the max parameter, it will be adjusted to this value
HomeSets the value to the min parameter
EndSets the value to the max parameter
BackspaceDeletes the last digit from the value
EscapeCancels the edit mode and restores the previous value

Besides this using any one of the following keys: A to F and/or 0 to 9 will move enter the edit mode and will allow the user to enter a new value.

Example

The following example shows how to create a simple application that converts a temperature from Celsius to Fahrenheit and vice versa. The application uses two numeric selectors, one for Celsius and one for Fahrenheit. When the value of one of the numeric selectors is changed, the other numeric selector is updated with the converted value.

use appcui::prelude::*;

#[Window(events = NumericSelectorEvents<f64>)]
struct MyWin {
    celsius: Handle<NumericSelector<f64>>,
    fahrenheit: Handle<NumericSelector<f64>>,
}

impl MyWin {
    fn new() -> Self {
        let mut win = MyWin {
            base: window!("'Convert',d:c,w:40,h:8"),
            celsius: Handle::None,
            fahrenheit: Handle::None,
        };
        win.add(label!("'Celsius:',x:1,y:1,w:12,h:1"));
        win.celsius = win.add(numericselector!("f64,0.0,x:14,y:1,w:25,min:-100.0,max:100.0,step:1.0"));
        win.add(label!("'Fahrenheit:',x:1,y:3,w:12,h:1"));
        win.fahrenheit = win.add(numericselector!("f64,32.0,x:14,y:3,w:25,min:-213.0,max:213.0,step:0.1"));
        win
    }
    fn convert_celsius_to_feherenheit(&mut self) {
        let celsius = self.control(self.celsius).unwrap().value();
        let fahrenheit = celsius * 9.0 / 5.0 + 32.0;
        let h = self.fahrenheit;
        self.control_mut(h).unwrap().set_value(fahrenheit);
    }
    fn convert_fahrenheit_to_celsius(&mut self) {
        let fahrenheit = self.control(self.fahrenheit).unwrap().value();
        let celsius = (fahrenheit - 32.0) * 5.0 / 9.0;
        let h = self.celsius;
        self.control_mut(h).unwrap().set_value(celsius);
    }
}

impl NumericSelectorEvents<f64> for MyWin {
    fn on_value_changed(&mut self, handle: Handle<NumericSelector<f64>>, _value: f64) -> EventProcessStatus {
        match () {
            _ if handle == self.celsius => {
                self.convert_celsius_to_feherenheit();
                EventProcessStatus::Processed
            }
            _ if handle == self.fahrenheit => {
                self.convert_fahrenheit_to_celsius();
                EventProcessStatus::Processed
            }
            _ => EventProcessStatus::Ignored,
        }
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

Panel

Represent a panel (a container that can have multiple children):

To create a panel use Panel::new method (with 3 parameters: a title, a layout and a type).

let b = Panel::new("My panel", Layout::new("x:10,y:5,w:15"), panel::Type::Border);

or the macro panel!

let p1 = panel!("caption='a panel',x:10,y:5,w:15");
let p2 = panel!("MyPanel,x:10,y:5,w:15,type:Border");

A panel supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
title or text or captionStringYes (first postional parameter)The title of the panel
typeStringNoPanel type. If not provided, Border type is considered as default

A pabel supports the following types:

  • panel::Type::Border or border (for macro initialization) - this will create a panel surrounded by a border (with the title left allined).
  • panel::Type::Window or window (for macro initialization) - this will create a panel surrounded by a border (with the title centered allined).
  • panel::Type::Page or page (for macro initialization) - this will create a panel without any border or title
  • panel::Type::TopBar or topbar (for macro initialization) - this will create a panel with a top bar and centered titled

Events

A panel emits no events.

Methods

Besides the Common methods for all Controls a button also has the following aditional methods:

MethodPurpose
set_title(...)Set the new title of the panel
title()Returns the current title of the panel
panel_type()Returns type of the panel
add(...)Adds a new control as a child for the panel. It returns a handle for the new control or Handle::None if the control was not added

Key association

A panel does not receive any input and as such it has no key associated with it.

Example

The following code creates a panel with the title Options.

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    let mut w = Window::new("Title", Layout::new("d:c,w:40,h:10"), window::Flags::None);
    w.add(Panel::new("Options", Layout::new("l:1,t:1,r:1,b:2"),panel::Type::Border));
    app.add_window(w);
    app.run();
    Ok(())
}

Password

Represent a clickable password control:

To create a password use Password::new method (with one parameter - the layout).

let p = Password::new(Layout::new("x:10,y:5,w:15"));

or use the macro password!

let p1 = password!("pass=1234,x:10,y:5,w:15");
let p2 = password!("password='MyP@ssw0rd',x:10,y:5,w:15");

A password control supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
pass or passwordStringNoA password to be used

Some examples that uses these paramateres:

let disabled_password = password!("x:10,y:5,w:15,enable=false");
let hidden_password = password!("pass='admin',x=9,y:1,align:center,w:9,visible=false");

Events

To intercept events from a password, the following trait has to be implemented to the Window that processes the event loop:

pub trait PasswordEvents {
    fn on_accept(&mut self, handle: Handle<Password>) -> EventProcessStatus {
        // called when you hit the ENTER key (to accept a passowrd)
    }
    fn on_cancel(&mut self, handle: Handle<Password>) -> EventProcessStatus {
        // called when you hit the ESCAPE key
    }
}

Methods

Besides the Common methods for all Controls a password also has the following aditional methods:

MethodPurpose
set_password(...)Programatically sets a new password .
Example: password.set_password("1234")
password()Returns the current password

Key association

The following keys are processed by a password control if it has focus:

KeyPurpose
EnterAttempts to accept password by emitting passwordEvents::on_accept(...) event. This is where a password can be validated.
EscapeCancel the password validation and emits passwordEvents::on_cancel(...).

Example

The following code creates a login window where you need to type the password admin to continue.

use appcui::prelude::*;

#[Window(events = ButtonEvents+PasswordEvents)]
struct MyWin {
    p: Handle<Password>,
    b_ok: Handle<Button>,
    b_cancel: Handle<Button>,
}

impl MyWin {
    fn new() -> Self {
        let mut win = MyWin {
            base: window!("'Login',d:c,w:40,h:8"),
            p: Handle::None,
            b_ok: Handle::None,
            b_cancel: Handle::None
        };
        win.add(label!("'Enter the password:',x:1,y:1,w:36,h:1"));
        win.b_ok = win.add(button!("&Ok,x:5,y:4,w:11"));
        win.b_cancel = win.add(button!("&Cancel,x:22,y:4,w:11"));
        win.p = win.add(password!("x:1,y:2,w:36"));

        win
    }
    fn check_password(&mut self) {
        let p = self.p;
        if let Some(pass) = self.control(p) {
            if pass.password() == "admin" {
                dialogs::message("Login", "Correct password. Let's start !");
            } else {
                if !dialogs::retry("Login", "Invalid password. Try again ?") {
                    self.close();
                }
            }
        }
    }
}

impl ButtonEvents for MyWin {
    fn on_pressed(&mut self, handle: Handle<Button>) -> EventProcessStatus {
        match () {
            _ if handle == self.b_cancel => {
                self.close();
                EventProcessStatus::Processed
            }
            _ if handle == self.b_ok => {
                self.check_password();
                EventProcessStatus::Processed
            }
            _ => { EventProcessStatus::Ignored }
        }
    }
}
impl PasswordEvents for MyWin {
    fn on_accept(&mut self, _: Handle<Password>) -> EventProcessStatus {
        self.check_password();
        EventProcessStatus::Processed
    }

    fn on_cancel(&mut self, _: Handle<Password>) -> EventProcessStatus {
        self.close();
        EventProcessStatus::Processed
    }
}
fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    app.add_window(MyWin::new());
    app.run();
    Ok(())
}

PathFinder

Represents a control where you can navigate through the file system and select a path:

To create a path finder control use the PathFinder::new method with the 3 parameteres: a starting file path, a layout and initialization flags:

let mut control = PathFinder::new("C:\\Program Files", Layout::new("x:1 , y:1 , width:40"), pathfinder::Flags::CaseSensitive)

or the macro pathfinder!

let mut control = pathfinder!(" x: 1, y:1,  path: 'C:\\Program Files', w:40"));

A pathfinder supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
pathStringYes (first postional parameter)The file path used as a starting point when navigating through the file system.
flagsListNoPathFinder initialization flags that control if the path finder is case-sensitive, readonly, etc

A pathfinder supports the following initialization flags:

  • pathfinder::Type::Readonly or Readonly (for macro initialization) - thils will allow you to view or copy the text but not to modify it
  • pathfinder::Type::CaseSensitive or CaseSensitive (for macro initialization) - by default the control is case insensitive, set this if you want it to be case sensitive. Some examples that use these parameters:
let cb = pathfinder!(" x: 1, y:1,  path: 'C:\\Program Files', w:40, flags:ReadOnly|CaseSensitive");
let cb = pathfinder!(" x: 1, y:1,  path: 'C:\\Program Files', w:40, enabled: false);

Events

To intercept events from a pathfinder, the following trait has to be implemented to the Window that processes the event loop:

pub trait PathFinderEvents {
    fn on_path_updated(&mut self, handle: Handle<PathFinder>) -> EventProcessStatus {...}
}

Methods

Besides the Common methods for all Controls a pathfinder also has the following aditional methods:

MethodPurpose
set_path(...)Set the path for a pathfinder.
path()Returns the current path from a pathfinder.

Key association

The following keys are processed by a PathFinder control if it has focus:

KeyPurpose
Left, RightNavigate through the text from the pathfinder
Shift+{Left,Right}Selects part of the text pathfinder
HomeMove to the begining of the text
Shift+HomeSelects the text from the beging of the text until the current position
EndMoves to the end of the text
Shift + EndSelects the text from current position until the end of the text
DeleteDeletes the current character. If a selection exists, it deletes it first
BackspaceDeletes the previous charactr. If a selection exists, it deletes it first
Ctrl+ASelects the entire text
Ctrl+C or Ctrl+InsertCopy the current selection to clipboard
Ctrl+V or Shift+InsertPaste the text from the clipboard (if any) to current position
Ctrl+X or Shift+DeleteIf a selection is present, it copies it into the clipboard and then delets it (acts like a Cut command)

Aditionally, all printable characters can be used to insert / modify or edit the current text.

Mouse actions

Mouse cursor can be used to select the text. Aditionally, a double click over the control will select all the text.

Example

The following code creates multiple path finders with both unicode and regular text.

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    let mut w = Window::new("Title", Layout::new("d:c,w:40,h:11"), window::Flags::None);
    w.add(pathfinder!("path: 'C:\\Program Files',x:1,y:1,w:36,h:1"));
    w.add(pathfinder!("'C:\\Program Files',x:1,y:3,w:36,h:1, flags: ReadOnly"));
    w.add(pathfinder!("path:'C:\\Program Files\\Țambal.exe',x:1,y:5,w:36,h:1,enable: false"));
    a.add_window(w);
    a.run();
    Ok(())
}

ProgressBar

Represent a progress bar that can be used to show the progress of a task.

To create a label use ProgressBar::new method (with 2 parameters: a caption and a layout).

let pg1 = ProgressBar::new(1000, Layout::new("x:10,y:5,w:15"), progressbar::Flags::None);

or the macro progressbar!

let pg1 = progressbar!("total: 1000, x:10,y:5,w:15");
let pg2 = progressbar!("count: 125 ,x:10,y:5,w:15, text: 'Copying ...'");

A progress supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
text or captionStringNoThe caption (text) written on the progress bar
total or count or cIntegerNoThe total number of steps that the progress bar will show
progress or value or vIntegerNoThe current value of the progress bar
pause or pausedBooleanNoIf true, the progress bar will be paused
flagsFlagsNoAdditional flags for the progress bar that can be used to control how the progress bar is being displayed

A progress bar supports the following initialization types:

  • progressbar::Type::HidePercentage or HidePercentage (for macro initialization) - thils will hide the percentage displayed on the progress bar.

Events

A progress bar emits no events.

Methods

Besides the Common methods for all Controls a label also has the following aditional methods:

MethodPurpose
update_text(...)Updates the text display on the progress bar
update_progress(...)Updates the progress of the progress bar. If the progress bar is paused this method also resume its activity.
processed()Returns the current progress of the progress bar
count()Returns the total number of steps of the progress bar
pause()Pauses the progress bar
resume()Resumes the progress bar
is_paused()Returns true if the progress bar is paused and false otherwise
reset(...)Resets the progress bar to its initial state. This method should be use to also set up a new count (total items) value for the progress bar. This method is often used when a progress bar is being reused.

Key association

A progress bar does not receive any input and as such it has no key associated with it.

Example

The following code creates a window with a progress bar that shows the progress of a task that copies 100 files from one location to another.

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    let mut w = window!("Test,d:c");
    let mut p = ProgressBar::new(100,Layout::new("x:1,y:1,w:30,h:2"), progressbar::Flags::None);
    p.update_text("Copying ...");
    w.add(p);
    a.add_window(w);
    a.run();
    Ok(())
}

RadioBox

A RadioBox is a control that allows selecting one option from a group of options. When a radio box is selected, it will notify its parent control to update the selection state of its siblings to unselected.

To create a radiobox use RadioBox::new method (with 3 parameters: a caption, a layout and selected status (true or false)) or method RadioBox::with_type (with one additional parameter - the type of the radiobox).

let b1 = RadioBox::new("A radiobox", 
                       Layout::new("x:10,y:5,w:15"),
                       true);
let b2 = RadioBox::with_type("Another radiobox", 
                             Layout::new("x:10,y:5,w:15"),
                             false,
                             radiobox::Type::Circle);

or the macro radiobox!

let r1 = radiobox!("caption='Some option',x:10,y:5,w:15,h:1");
let r2 = radiobox!("'Another &option',x:10,y:5,w:15,h:1,selected:true");
let r3 = radiobox!("'&Multi-line option\nthis a hot-key',x:10,y:5,w:15,h:3,selected:false");
let r4 = radiobox!("'&Circle radiobox',x:10,y:5,w:15,h:3,selected:false,type: Circle");

The caption of a radiobox may contain the special character & that indicates that the next character is a hot-key. For example, constructing a radiobox with the following caption &Option number 1 will set up the text of the radiobox to Option number 1 and will set up character O as the hot key for that radiobox (pressing Alt+O will be equivalent to selecting that radiobox).

A radiobox can contain a multi-line text but you will have to set the height parameter large enough to a larger value (bigger than 1).

A radiobox supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
text or captionStringYes (first postional parameter)The caption (text) written on a radiobox
selected or selectBoolNoRadiobox selected status: true for false
typeStringNoThe type of the radiobox (see below)

Some examples that uses these paramateres:

let disabled_radiobox = radiobox!("caption=&Disabled,x:10,y:5,w:15,enable=false");
let hidden_radiobox = radiobox!("text='&Hidden',x=9,y:1,align:center,w:9,visible=false");
let multi_line_radiobox = radiobox!("'&Multi line\nLine2\nLine3',x:1,y:1,w:10,h:3");

The type of a radiobox is described by the radiobox::Type enum:

#![allow(unused)]
fn main() {
#[derive(Copy,Clone,PartialEq,Eq)]
pub enum Type {
    Standard, // Default value
    Circle,
    Diamond,
    Square,
    Star,
    Dot,
}
}

The type of the radiobox describes how the radiobox state (selected or unselected) will be represented on the screen.

TypeSelected StateUnselected State
Standard(●) Selected( ) Unselected
Ascii(*) Selected( ) Unselected
Circle◉ Selected○ Unselected
Diamond◆ Selected◇ Unselected

Events

To intercept events from a radiobox, the following trait has to be implemented to the Window that processes the event loop:

pub trait RadioBoxEvents {
    fn on_status_changed(&mut self, handle: Handle<RadioBox>) -> EventProcessStatus {...}
}

Methods

Besides the Common methods for all Controls a radiobox also has the following aditional methods:

MethodPurpose
set_caption(...)Set the new caption for a radiobox. If the string provided contains the special character &, this method also sets the hotkey associated with a control. If the string provided does not contain the & character, this method will clear the current hotkey (if any).
Example: radiobox.set_caption("&Option") - this will set the caption of the radiobox to Option and the hotkey to Alt+O
caption()Returns the current caption of a radiobox
is_selected()true if the radiobox is selected, false otherwise
set_selected()Sets the radiobox to selected state and notifies its parent control to update the selection state of its siblings to unselected

Key association

The following keys are processed by a RadioBox control if it has focus:

KeyPurpose
Space or EnterChanges the selected state. It also emits RadioBoxEvents::on_status_changed(...) event. It has the same action clicking the radiobox with the mouse.

Additionally, Alt+letter or number will have the same action (even if the radiobox does not have a focus) if that letter or number was set as a hot-key for a radiobox via its caption.

Grouping

Implicetelly, all radiboxes withing a control (that have the same parent) are considered as part of one group. This means that when you select one radiobox, all other radioboxes from the same group will be unselected.

To create multiple groups, one need to create panels and add radioboxes as their children, like in the following example:

// group 1
let mut panel_1 = Panel::new(...);
panel_1.add(RadioBox::new(...));
panel_1.add(RadioBox::new(...));
panel_1.add(RadioBox::new(...));

// group 2
let mut panel_2 = Panel::new(...);
panel_2.add(RadioBox::new(...));
panel_2.add(RadioBox::new(...));
panel_2.add(RadioBox::new(...));

Example

The following code creates a window with two groups (panels), each group containing 3 radioboxes. When a radiobox is selected, its content will display on a label.

use appcui::prelude::*;

#[Window(events = RadioBoxEvents)]
struct MyWin {
    g1_r1: Handle<RadioBox>,
    g1_r2: Handle<RadioBox>,
    g1_r3: Handle<RadioBox>,
    g2_r1: Handle<RadioBox>,
    g2_r2: Handle<RadioBox>,
    g2_r3: Handle<RadioBox>,
    l: Handle<Label>,
}

impl MyWin {
    fn new() -> Self {
        let mut win = MyWin {
            base: window!("'My Win',d:c,w:60,h:14"),
            g1_r1: Handle::None,
            g1_r2: Handle::None,
            g1_r3: Handle::None,
            g2_r1: Handle::None,
            g2_r2: Handle::None,
            g2_r3: Handle::None,
            l: Handle::None,
        };
        win.l = win.add(label!("'<no status>',l:1,r:1,t:1"));
        let mut group_1 = panel!("'Group 1',x:1,y:3,w:26,h:7");
        win.g1_r1 = group_1.add(radiobox!("Meters,x:1,y:1,w:20,select:true"));
        win.g1_r2 = group_1.add(radiobox!("Centimeters,x:1,y:2,w:20"));
        win.g1_r3 = group_1.add(radiobox!("Kilometers,x:1,y:3,w:20"));
        
        let mut group_2 = panel!("'Group 2',x:30,y:3,w:26,h:7");
        win.g2_r1 = group_2.add(radiobox!("Red,x:1,y:1,w:20,select:true"));
        win.g2_r2 = group_2.add(radiobox!("Green,x:1,y:2,w:20"));
        win.g2_r3 = group_2.add(radiobox!("Blue,x:1,y:3,w:20"));

        win.add(group_1);
        win.add(group_2);
        win
    }
}

impl RadioBoxEvents for MyWin {
    fn on_selected(&mut self, handle: Handle<RadioBox>) -> EventProcessStatus {
        let mut s = String::new();
        if let Some(r) = self.control(handle) {
            s += r.caption();
        }
        if s.len()>0 {
            let h = self.l;
            if let Some(l) = self.control_mut(h) {
                l.set_caption(&s);
            }
        }
        EventProcessStatus::Ignored
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

Selector

A selector is a templetize (generics based) control that allows you to select a variant of an enum.

It can be create using Selector::new(...) or the selector! macro. Using Selector::new(...) can be done in two ways:

  1. by specifying the type for a variable:

    let s: Selector<T> = Selector::new(...);
    
  2. by using turbo-fish notation (usually when you don't want to create a separate variable for the control):

    let s = Selector::<T>::new(...);
    

Remarks: It is important to notice that the T type must implement a special trait EnumSelector as well as Copy, Clone, Eq and PartialEq.

Examples

Assuming we have the following enum: Animal thet implements the required traits as follows:

#[derive(Copy,Clone,PartialEq,Eq)]
enum Animal { Cat,Mouse,Dog }

impl EnumSelector for Animal { ... }

then we can create a selector object based on this type as follows:

let s1: Selector<Animal> = Selector::new(Some(Animal::Dog),Layout::new("..."),selector::Flags::None);
let s2: Selector<Animal> = Selector::new(None,Layout::new("..."),selector::Flags::AllowNoneVariant);
let s3 = selector!("Animal,value:Dog,x:1,y:1,w:20");
let s4 = selector!("enum: Animal,x:1,y:1,w:20, flags: AllowNoneVariant");

A selector supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
enum or typeStringYes (first postional parameter)The name of a templetized type to be used when creating the selector
flagsStringNoSelector initialization flags
valueStringNoThe initial value of the selector (should be one of the variants of the enum). If not specified, None is assume (and you MUST also set the AllowNoneVariant flag on initalization). If you don't a panic will occur !

A selector supports the following initialization flags:

  • selector::Flags::AllowNoneVariant or AllowNoneVariant (for macro initialization) - thils will allow a selector to hold a None value as well. If it is not specified, the value from a selector will always be one of the variants from the templetized type.

Events

To intercept events from a selector, the following trait has to be implemented to the Window that processes the event loop:

pub trait SelectorEvents<T> {
    fn on_selection_changed(&mut self, handle: Handle<Selector<T>>, value: Option<T>) -> EventProcessStatus {...}
}

Methods

Besides the Common methods for all Controls a selector also has the following aditional methods:

MethodPurpose
set_value(...)Sets the new value associated with the selector. The value will be of type T (that was used to templetized the selector control)
clear_value()Sets the new value of the control to None. If the flag AllowNoneVariant was not set when the control was create, a panic will occur
value()Returns the current value of the control. If the current value is None a panic will occur
try_value()Returns an Option<T> containint the current value of the control.

Remarks: If the flag AllowNoneVariant was set, it is recommended to use try_value() method. If not, you can safely use the value() method.

Key association

The following keys are processed by a Selector control if it has focus:

KeyPurpose
Space or EnterExpands or packs (collapses) the Selector control.
Up, Down, Left, RightChanges the current selected color from the Selector.
PageUp, PageDownNavigates through the list of variants page by page. If the control is not expanded, their behavior is similar to the keys Up and Down
HomeMove the selection to the first variant
EndMove the selection to the last variant or to None if AllowNoneVariant flag was set upon initialization

Besides this using any one of the following keys: A to Z and/or 0 to 9 will move the selection to the fist variant that starts with that letter (case is ignored). The search starts from the next variant after the current one. This means that if you have multiple variants that starts with letter G, pressing G multiple times will efectively switch between all of the variants that starts with letter G.

When the selector is expanded the following additional keys can be used:

KeyPurpose
Ctrl+UpScroll the view to top. If the new view can not show the current selection, move the selection to the previous value so that it would be visible
Ctrl+DownScroll the view to bottom. If the new view can not show the current selection, move the selection to the next value so that it would be visible
EscapeCollapses the control. If the Selector is already colapsed, this key will not be captured (meaning that one of the Selector ancestors will be responsable with treating this key)

Example

The following example creates a Window with a Selector that can chose between 4 animals: a cat, a dog, a horse and a mouse. When an animal is being selected the title of the window changes to reflect this.

use appcui::prelude::*;

#[derive(Copy, Clone, Eq, PartialEq)]
enum Animals {
    Cat,
    Dog,
    Horse,
    Mouse,
}
impl EnumSelector for Animals {
    const COUNT: u32 = 4;

    fn from_index(index: u32) -> Option<Self>
    where
        Self: Sized,
    {
        match index {
            0 => Some(Animals::Cat),
            1 => Some(Animals::Dog),
            2 => Some(Animals::Horse),
            3 => Some(Animals::Mouse),
            _ => None,
        }
    }

    fn name(&self) -> &'static str {
        match self {
            Animals::Cat => "Cat",
            Animals::Dog => "Dog",
            Animals::Horse => "Horse",
            Animals::Mouse => "Mouse",
        }
    }

    fn description(&self) -> &'static str {
        match self {
            Animals::Cat => "A cat is a ...",
            Animals::Dog => "A dog is a ...",
            Animals::Horse => "A horse is a ...",
            Animals::Mouse => "A mouse is a ...",
        }
    }
}

#[Window(events = SelectorEvents<Animals>)]
struct MyWin {}
impl MyWin {
    fn new() -> Self {
        let mut w = Self {
            base: window!("x:1,y:1,w:30,h:8,caption:Win"),
        };
        w.add(selector!("Animals,value:Cat,x:1,y:1,w:26"));
        w
    }
}
impl SelectorEvents<Animals> for MyWin {
    fn on_selection_changed(&mut self, _handle: Handle<Selector<Animals>>, 
                                       value: Option<Animals>) -> EventProcessStatus 
    {
        self.set_title(value.unwrap().name());
        EventProcessStatus::Processed
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

Tab

Represent tabulator (tab control) where you can select the visible page:

To create a tab use Tab::new , Tab::with_type methods:

let t1 = Tab::new(Layout::new("d:c,w:15,h:10"),tab::Flags::None);
let t2 = Tab::with_type(Layout::new("d:c,w:15,h:10"),tab::Flags::None, tab::Type::OnLeft);

or the macro tab!

let t3 = tab!("d:c,w:15,h:10,tabs:[First,Second,Third],type:OnBottom");
let t4 = tab!("d:c,w:15,h:10,tabs:[A,B,C],flags:TabsBar");

The caption of each tab may contain the special character & that indicates that the next character is a hot-key. For example, constructing a tab with the following caption &Start will set up the text of the tab to Start and will set up character S as the hot key to activate that tab.

A tab supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
typeStringNoTab type (one of OnLeft, OnBottom, OnTop, HiddenTabs)
flagsListNoTab initialization flags
tabsListNoA list of tab pages
tabwidth or tab-width or twNumericNoThe size of one tab. Should be between 3 and 32

A tab supports the following initialization types:

  • tab::Type::OnTop or OnTop (for macro initialization) - this will position all tabs on top (this is also the default mode if this parameter is not specified)
  • tab::Type::OnBottom or OnBottom (for macro initialization) - this will position all tabs on the bottom the the control
  • tab::Type::OnLeft or OnLeft (for macro initialization) - this will position all tabs on the left side of the control
  • tab::Type::HiddenTabs or HiddentTabs (for macro initialization) - this will hide all tabs. You can use this mode if you plan to change the tabs manually (via .set_current_tab(...)) method

and the following flags:

  • tab::Flags::TransparentBackground or TransparentBackground (for macro initialization) - this will not draw the background of the tab
  • tab::Flags::TabsBar or TabsBar (for macro initialization) - this will position all tabs over a bar

Some examples that uses these paramateres:

let t1 = tab!("type:OnBottom,tabs:[Tab1,Tab2,Tab&3],tw:10,flags:TabsBar,d:c,w:100%,h:100%");
let t2 = tab!("type:OnLeft,tabs:[A,B,C],flags:TabsBar+TransparentBackground,d:c,w:100%,h:100%");

Events

This control does not emits any events.

Methods

Besides the Common methods for all Controls a tab also has the following aditional methods:

MethodPurpose
add_tab(...)Adds a new tab
add(...)Add a new control into the tab (the index of the tab where the control has to be added must be provided)
current_tab()Provides the index of the current tab
set_current_tab(...)Sets the current tab (this method will also change the focus to the tab cotrol)
tab_width()Returns the width of a tab
set_tab_width(...)Sets the width of a tab (must be a value between 3 and 32)
tab_caption(...)Returns the caption (name) or a tab based on its index
set_tab_caption(...)Sets the caption (name) of a tab

Key association

The following keys are processed by a Tab control if it has focus:

KeyPurpose
Ctrl+TabSelect the next tab. If the current tab is the last one, the first one will be selected.
Ctrl+Shift+TabSelect the previous tab. If the current tab is the first one, the last one will be selected

Aditionally, Alt+letter or number will automatically select the tab with that particular hotkey combination.

Example

The following code creates a tab with 3 tabs pages and adds two buttons on each tab page.

use appcui::prelude::*;


fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    let mut w = window!("Test,d:c,w:100%,h:100%");
    let mut t = tab!("l:1,t:1,r:1,b:3,tabs:['Tab &1','Tab &2','Tab &3']");
    t.add(0, button!("T1-1-A,r:1,b:0,w:10,type:flat"));
    t.add(0, button!("T1-1-B,d:c,w:10,type:flat"));      
    t.add(1, button!("T1-2-A,r:1,b:0,w:14,type:flat"));
    t.add(1, button!("T1-2-B,d:c,w:14,type:flat")); 
    t.add(2, button!("T1-3-A,r:1,b:0,w:20,type:flat"));
    t.add(2, button!("T1-3-B,d:l,w:20,type:flat"));  
    w.add(t); 

    w.add(button!("OK,r:0,b:0,w:10, type: flat"));
    w.add(button!("Cancel,r:12,b:0,w:10, type: flat"));

    a.add_window(w);
    a.run();
    Ok(())
}

TextArea

Represent a control where you can add/modify a text:

To create a textarea use TextArea::new method (with 3 parameters: a caption, a layout and initialization flags).

let tx = TextArea::new("Some text", Layout::new("x:10,y:5,w:15"), textarea::Flags::None);

or use the macro textarea!()

let textarea1 = textarea!("text='some text to edit',d:c,h:100%");
let textarea2 = textarea!("'some text to print',d:c,h:100%,flags:ReadOnly");

A textarea supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
textStringYes (first postional parameter)The text from a text area. If ommited an empty string will be considered as the caption of the textarea.
flagsListNoTextArea initialization flags that control how the TextArea should look and behave(ReadOnly, having line numbers)

Text Area supports the following initialization flags:

  • textarea::Flags::ShowLineNumber or ShowLineNumber (for macro initialization) - This flag enables the display of line numbers in the text area, typically in a gutter on the left side. It helps users keep track of their position within the text, making navigation and debugging easier. This feature is especially useful for programming and document editing, where line references are important.
  • textarea::Flags::ReadOnly or ReadOnly (for macro initialization) - When this flag is set, the text area becomes non-editable, meaning users can view but not modify the text. This is useful for displaying logs, reference documents, or any content where accidental modifications should be prevented. Although users cannot change the text, they may still be able to select and copy it.
  • textarea::Flags::ScrollBars or ScrollBars (for macro initialization)- This flag enables scrollbars in the text area when the content exceeds the visible space. It ensures smooth navigation by allowing users to scroll horizontally or vertically as needed.
  • textarea::Flags::HighlightCursor or HughlightCursor (for macro initialization) - When enabled, this flag highlights the current cursor position within the text. It can be useful for visually tracking the insertion point while typing or editing. The highlight will appear as a different background color.

Methods

Besides the Common methods for all Controls a textfield also has the following aditional methods:

MethodPurpose
set_textReplaces the current content with the specified text.
insert_textInserts the given text at the specified cursor position.
remove_textRemoves a portion of the text between specified positions.
textReturns the full content of the text editor.
select_textSelects a range of text with the given start position and size.
clear_selectionClears any active text selection.
has_selectionReturns true if there is an active text selection.
selectionReturns the currently selected text, if any.
delete_selectionDeletes the currently selected text.
is_read_onlyReturns true if the text editor is in read-only mode.
set_cursor_positionMoves the cursor to the specified position.
cursor_positionReturns the current position of the cursor.

Key association

The following keys are processed by a TextField control if it has focus:

KeyPurpose
Arrow KeysMove the cursor left, right, up, or down by one character or line.
Shift + ArrowsExtends the text selection in the direction of the arrow key.
Ctrl + RightMoves the cursor to the beginning of the next word.
Ctrl + LeftMoves the cursor to the beginning of the previous word.
Ctrl + Shift + RightExtends the selection to the beginning of the next word.
Ctrl + Shift + LeftExtends the selection to the beginning of the previous word.
Ctrl + CCopies the selected text to the clipboard.
Ctrl + VPastes the clipboard content at the cursor position.
BackspaceDeletes the character before the cursor.
DeleteDeletes the character after the cursor.
Ctrl + BackspaceDeletes the entire previous word.
Ctrl + DeleteDeletes the entire next word.
EnterInserts a new line at the cursor position.
Page UpMoves the view up by one page, scrolling the text accordingly.
Page DownMoves the view down by one page, scrolling the text accordingly.

Aditionally, all printable characters can be used to insert / modify or edit the current text.

Mouse actions

Mouse cursor can be used to select the text.

Example

The following code creates multiple text areas with both unicode and regular text.

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    let mut w = Window::new("Title", Layout::new("d:c,w:40,h:11"), window::Flags::None);
    w.add(TextArea::new("I ❤️ Rust Language", Layout::new("d:c,h:100%"), textarea::Flags::None));
    w.add(TextArea::new("Read only text", Layout::new("d:c,h:100%"), textarea::Flags::ReadOnly));
    w.add(TextArea::new("Line Numbers tab functional", Layout::new("d:c,h:100%"), textarea::Flags::ShowLineNumber | textarea::Flags::ReadOnly));
    w.add(TextArea::new("I also have scrollbars ❤️", Layout::new("d:c,h:100%"), textarea::Flags::ScrollBars));
    a.add_window(w);
    a.run();
    Ok(())
}

TextField

Represent a control where you can add/modify a text:

To create a textfield use TextField::new method (with 3 parameters: a caption, a layout and initialization flags).

let tx = TextField::new("some text", Layout::new("x:10,y:5,w:15"),textfield::Flags::None);

or the macro textfield!

let tx1 = textfield!("text='some text',x:10,y:5,w:15");
let tx2 = textfield!("some_text,x:10,y:5,w:15");

A textfield supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
text or captionStringYes (first postional parameter)The caption (text) from a text field. If ommited an empty string will be considered as the caption of the textfield.
flagsListNoTextField initialization flags that control how Enter is process, if the textfield is readonly, etc

A textfield supports the following initialization flags:

  • textfield::Type::Readonly or Readonly (for macro initialization) - thils will allow you to view or copy the text but not to modify it
  • textfield::Type::ProcessEnter or ProcessEnter (for macro initialization) - by default the Enter key is not processed by this control. However, if this flag is being used, Enter key is being captured and when pressed the TextFieldEvents::on_validate(...) method is being called.
  • textfield::Type::DisableAutoSelectOnFocus or DisableAutoSelectOnFocus (for macro initialization) - by default, a textfield will automatically select its content when it receives the focus. This behavior can be disabled by adding this flag to the initialization flags.

Some examples that uses these paramateres:

let no_auto_focus = textfield!("caption='no auto focus',x:10,y:5,w:15,flags:DisableAutoSelectOnFocus");
let read_only = textfield!("text='a read only text',x=9,y:1,align:center,w:9,flags: ReadOnly");
let expty_text = textfield!("x:1,y:1,w:10");

Events

To intercept events from a textfield, the following trait has to be implemented to the Window that processes the event loop:

pub trait TextFieldEvents {
    fn on_validate(&mut self, handle: Handle<TextField>, text: &str) -> EventProcessStatus {...}
}

Methods

Besides the Common methods for all Controls a textfield also has the following aditional methods:

MethodPurpose
set_text(...)Set the new text for a textfield.
text()Returns the current text from a textfield
is_readonly()Returns true if the current textfield is in a readonly state (was created with the readonlu flag) or false otherwise

Key association

The following keys are processed by a TextField control if it has focus:

KeyPurpose
Left, Right, Up, DownNavigate through the text from the textfield
Shift+{Left,Right,Up,Down}Selects part of the text textfield
Ctrl+LeftMoves to the begining of the previous word
Shift+Ctrl+LeftSelects the text from the begining of the previous word until the current position
Ctrl+RightMoves to the begining of the next word
Shift+Ctrl+RightSelects the text from current postion until the start of the next word
HomeMove to the begining of the text
Shift+HomeSelects the text from the beging of the text until the current position
EndMoves to the end of the text
Shift + EndSelects the text from current position until the end of the text
DeleteDeletes the current character. If a selection exists, it deletes it first
BackspaceDeletes the previous charactr. If a selection exists, it deletes it first
Ctrl+ASelects the entire text
Ctrl+UConverts the current selection to lower case. If no selection is present the curent word will be selected and then converted to lowercase
Ctrl+Shift+UConverts the current selection to upper case. If no selection is present the curent word will be selected and then converted to uppercase
Ctrl+C or Ctrl+InsertCopy the current selection to clipboard
Ctrl+V or Shift+InsertPaste the text from the clipboard (if any) to current position
Ctrl+X or Shift+DeleteIf a selection is present, it copies it into the clipboard and then delets it (acts like a Cut command)
EnterOnly if the flag textfield::Type::ProcessEnter is present will trigger a call to TextFieldEvents::on_validate(...)

Aditionally, al printable characters can be used to insert / modify or edit the current text.

Mouse actions

Mouse cursor can be used to select the text. Aditionally, a double click over a word will select it.

Example

The following code creates multiple text fields with both unicode and regular text.

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    let mut w = Window::new("Title", Layout::new("d:c,w:40,h:11"), window::Flags::None);
    w.add(textfield!("text:'I ❤️ Rust Language',x:1,y:1,w:36,h:1"));
    w.add(textfield!("'Read only text',x:1,y:3,w:36,h:1, flags: Readonly"));
    w.add(textfield!("Inactive,x:1,y:5,w:36,h:1,enable: false"));
    w.add(textfield!("'No auto selection',x:1,y:7,w:36,h:1, flags: DisableAutoSelectOnFocus"));
    a.add_window(w);
    a.run();
    Ok(())
}

ThreeStateBox

Represent a control with three states (checked, unckehed or unknown):

To create a threestatebox use ThreeStateBox::new method (with 3 parameters: a caption, a layout and a state (checked, unchecked or unknown)).

let b = ThreeStateBox::new("A ThreeStateBox", Layout::new("x:10,y:5,w:15"),threestatebox::State::Checked);

or the macro threestatebox!

let c1 = threestatebox!("caption='Some option',x:10,y:5,w:15,h:1");
let c2 = threestatebox!("'Another &option',x:10,y:5,w:15,h:1,state:checked");
let c3 = threestatebox!("'&Multi-line option\nthis a hot-key',x:10,y:5,w:15,h:3,state:unknown");
let c4 = threestatebox!("'&Unchecked threestatebox',x:10,y:5,w:15,h:3,state:unchecked");

The caption of a threestatebox may contain the special character & that indicates that the next character is a hot-key. For example, constructing a threestatebox with the following caption &Option number 1 will set up the text of the button to Option number 1 and will set up character O as the hot key for that threestatebox (pressing Alt+O will be equivalent to changing the status for that threestatebox from checked to unchecked to unkown).

A threestatebox can contain a multi-line text but you will have to set the height parameter large enough to a larger value (bigger than 1).

A threestatebox supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
text or captionStringYes (first postional parameter)The caption (text) written on a threestatebox
stateStringNothreestatebox state: checked, unchecked or unknown. If the parameter is not provided, it will be defaulted to unknown state
typeStringNothreestatebox type: Standard, Ascii, CheckBox, CheckMark, FilledBox, YesNo or PlusMinus. If the parameter is not provided, it will be defaulted to Standard type

Some examples that uses these paramateres:

let disabled_threestatebox = threestatebox!("caption=&Disabled,x:10,y:5,w:15,enable=false");
let hidden_threestatebox = threestatebox!("text='&Hidden',x=9,y:1,align:center,w:9,visible=false");
let multi_line_threestatebox = threestatebox!("'&Multi line\nLine2\nLine3',x:1,y:1,w:10,h:3");
let custom_type_threestatebox = threestatebox!("'&Custom type',x:1,y:1,w:10,h:1,type=YesNo");

The type of the ThreeStateBox describes how the ThreeStateBox state (checked , unchecked or unknown) will be represented on the screen.

TypeChecked StateUnchecked StateUnknown State
Standard[✓] Checked[ ] Unchecked[?] Unknown
Ascii[X] Checked[ ] Unchecked[?] Unknown
CheckBox☑ Checked☐ Unchecked⍰ Unknown
CheckMark✔ Checked✖ Unchecked? Unknown
FilledBox▣ Checked▢ Unchecked◪ Unknown
YesNo[Y] Checked[N] Unchecked[?] Unknown
PlusMinus➕ Checked➖ Unchecked± Unknown

Events

To intercept events from a threestatebox, the following trait has to be implemented to the Window that processes the event loop:

pub trait ThreeStateBoxEvents {
    fn on_status_changed(&mut self, handle: Handle<ThreeStateBoxEvents>, state: threestatebox::State) -> EventProcessStatus {...}
}

Methods

Besides the Common methods for all Controls a checkbox also has the following aditional methods:

MethodPurpose
set_caption(...)Set the new caption. If the string provided contains the special character &, this method also sets the hotkey associated with a control. If the string provided does not contain the & character, this method will clear the current hotkey (if any).
Example: threestatebox.set_caption("&Option") - this will set the caption of the threestatebox with Option and the hotkey to Alt+O
caption()Returns the current caption
state()Returns the current state of the threestatebox (checked, unchecked or unknown)
set_state(...)Sets the new state for the threestatebox (checked, unchecked or unknown)

Key association

The following keys are processed by the control if it has focus:

KeyPurpose
Space or EnterCycle throght the states (checked to un-checked and vice-versa). It also emits ThreeStateBoxEvents::on_status_changed(...) event with the state parameter, the current state of the threestatebox. It has the same action clicking the threestatebox with the mouse.

Aditionally, Alt+letter or number will have the same action (even if the threestatebox does not have a focus) if that letter or nunber was set as a hot-key for a threestatebox via its caption.

Example

The following code creates a window a threestatebox and a label. Whenever the threestatebox status is being changed, the label will print the new status (checked, unchecked or unkown).

#[Window(events = ThreeStateBoxEvents)]
struct MyWin {
    c: Handle<ThreeStateBox>,
    l: Handle<Label>,
}

impl MyWin {
    fn new() -> Self {
        let mut win = MyWin {
            base: window!("'My Win',d:c,w:40,h:6"),
            c: Handle::None,
            l: Handle::None,
        };
        win.c = win.add(threestatebox!("'My option',l:1,r:1,b:1"));
        win.l = win.add(label!("'State: Unknown',l:1,r:1,t:1"));
        win
    }
}

impl ThreeStateBoxEvents for MyWin {
    fn on_status_changed(&mut self, _handle: Handle<ThreeStateBox>, state: State) -> EventProcessStatus {
        let handle = self.l;
        let l = self.control_mut(handle).unwrap();
        match state {
            State::Checked => l.set_caption("State: Checked"),
            State::Unchecked => l.set_caption("State: Unchecked"),
            State::Unknown => l.set_caption("State: Unknown"),
        }
        EventProcessStatus::Processed
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    app.add_window(MyWin::new());
    app.run();
    Ok(())
}

ToggleButton

A toggle button is a button that can be toggled on (selected) and off (unselected).

A toggle button is created using the ToggleButton::new and ToggleButotn::with_single_selection methods:

let tb1 = ToggleButton::new( 
        "Aa",                         // caption
        "Enable case sensitive",      // tooltip
        Layout::new(...),             // layout
        false,                        // initial state (on/off)
        togglebutton::Flags::Normal); // type

let tb2 = ToggleButton::with_single_selection( 
        "Aa",                         // caption
        "Enable case sensitive",      // tooltip
        Layout::new(...),             // layout
        false,                        // initial state (on/off)
        togglebutton::Flags::Normal); // type        

or the macro togglebutton!:

let tb1 = togglebutton!("Aa,'Enable case sensitive',x:10,y:5,w:15");
let tb2 = togglebutton!("Aa,'Enable case sensitive',
                         x:10,y:5,w:15,selected: false, group: true");                         

A toggle button supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
name or text or captionStringYes (first postional parameter)The caption (text) written on a button
tooltip or desc or descriptionStringYes (second positional parameter)The tool tip that will be showed if the mouse is hovered over the control. Since the text within this control is usually small (2-3 character - such as a pictogram), this is the way to convey more information on the purpose of the control
typeStringNoThe type of the toggle button
state or selected or selectboolNoThe initial state of the toggle button (on/off)
group or single_selectionboolNoIf true the toggle button will be part of a group of toggle buttons. Only one button in the group can be selected at a time.

A toggle button supports the following types:

  • button::Type::Normal or Normal (for macro initialization) - this is the default type of a toggle button.
  • button::Type::Underlined or Underlined (for macro initialization) - this will underline the caption of the button is it is selected.

Events

To intercept events from a toggle button, the following trait has to be implemented to the Window that processes the event loop:

pub trait ToggleButtonEvents {
    fn on_selection_changed(&mut self, handle: Handle<ToggleButton>, selected: bool) -> EventProcessStatus {
        EventProcessStatus::Ignored
    }
}

Methods

Besides the Common methods for all Controls a button also has the following aditional methods:

MethodPurpose
set_caption(...)Set the new caption for a toggle button.
caption()Returns the current caption of a toggle button
set_selected(...)Set the state of the toggle button (on or off)
is_selected()Returns the current state of the toggle button (selected or not)

Key association

The following keys are processed by a Button control if it has focus:

KeyPurpose
SpaceClicks / pushes the button and emits ToggleButtonEvents::on_selection_changed(...) event. It has the same action clicking the toggle button with the mouse.
EnterClicks / pushes the button and emits ToggleButtonEvents::on_selection_changed(...) event. It has the same action clicking the toggle button with the mouse.

Grouping

If a toggle button is created with the group parameter set to true or via the api ToggleButton::with_single_selection it will be part of a group of toggle buttons. Only one button in the group can be selected at a time.

To create multiple groups, one need to create panels and add toggle buttons as their children, like in the following example:

// group 1
let mut panel_1 = Panel::new(...);
panel_1.add(ToggleButton::with_single_selection(...));
panel_1.add(ToggleButton::with_single_selection(...));
panel_1.add(ToggleButton::with_single_selection(...));

// group 2
let mut panel_2 = Panel::new(...);
panel_2.add(togglebutton!("....,group:true"));
panel_2.add(togglebutton!("....,group:true"));
panel_2.add(togglebutton!("....,group:true"));

Example

The following code creates a window with three toggle buttons (Case sensitive, Match whole word and RegExp search).

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    let mut w = window!("Test,d:c,w:60,h:10");
    let tg1 = ToggleButton::new(
        "Aa", 
        "Case sensitive", 
        Layout::new("x:1,y:1,w:2,h:1"), 
        true, 
        togglebutton::Type::Underlined);
    let tg2 = ToggleButton::new(
        "..",
        "Match whole word",
        Layout::new("x:4,y:1,w:2,h:1"),
        false,
        togglebutton::Type::Underlined,
    );
    let tg3 = ToggleButton::new(
        ".*", 
        "RegExp search", 
        Layout::new("x:7,y:1,w:2,h:1"), 
        true, 
        togglebutton::Type::Underlined);
    w.add(tg1);
    w.add(tg2);
    w.add(tg3);
    a.add_window(w);
    a.run();
    Ok(())
}

TreeView

A TreeView is a templetize (generics based) control that allows you to view a list of objects as a tree.

It can be created using TreeView::new(...) and TreeView::with_capacity(...) methods or with the treeview! macro.

let l1: TreeView<T> = TreeView::new(Layout::new("..."),treeview::Flags::None);
let l2: TreeView<T> = TreeView::with_capacity(10,Layout::new("..."),treeview::Flags::ScrollBars);
let l3 = treeview!("class: T, flags: Scrollbar, d:c, w:100%, h:100%");
let l4 = treeview!("type: T, flags: Scrollbar, d:c, view:Columns(3)");
let l5 = treeview!("T, d:c, columns:[{Name,10,left},{Age,5,right},{City,20,center}]");

where type T is the type of the elements that are shown in the tree view and has to implement ListItem trait.

A treeview supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
class or typeStringYes, first positional parameterThe type of items that are being displayed in the TreeView control.
flagsStringNoTreeView initialization flags
lsm or left-scroll-marginNumericNoThe left margin of the bottom scroll bar in characters. If not provided the default value is 0. This should be a positive number and it only has an effect if the flag ScrollBars or SearchBar flags were specified.
tsm or top-scroll-marginNumericNoThe top margin of the right scroll bar in characters. If not provided the default value is 0. This should be a positive number and it only has an effect if the flag ScrollBars flags was used to create the control.
columnsListNoThe list of columns for the the TreeView control.

The field columns is a list of columns that are displayed in the TreeView control. Each column is a tuple with three elements: the name of the column, the width of the column in characters, and the alignment of the column (left, right, or center). The column field accespts the following parameters:

Parameter nameTypePositional parameterPurpose
caption or name or textStringYes, first positional parameterThe name of the column. If a character in the name is precedeed by the & character, that column will have a hot key associated that will allow clicking on a column via Ctrl+
width or wNumericYes, second positional parameterThe width of the column in characters.
align or alignment or aStringYes, third positional parameterThe alignment of the column (left or l, right or r, and center or c).

To create a column with the name Test, that hast Ctrl+E assigned as a hot key, with a width of 10 characters, and aligned to the right, you can use the following formats:

  • {caption: "T&est", width: 10, align: right}
  • {name: "T&est", w: 10, a: right}
  • {T&est, 10, right}
  • {T&est,10,r}

Similary, to create a treeview with 3 columns (Name, Age, and City) with the widths of 10, 5, and 20 characters, respectively, and aligned to the left, right, and center, you can use the following format:

let l = treeview!("T, d:c, columns:[{Name,10,left},{Age,5,right},{City,20,center}]");

A treeview supports the following initialization flags:

  • treeview::Flags::ScrollBars or ScrollBars (for macro initialization) - this enables a set of scrollbars that can be used to navigate through the list of items. The scrollbars are visible only when the control has focus
  • treeview::Flags::SearchBar or SearchBar (for macro initialization) - this enables a search bar that can be used to filter the list of items. The search bar is visible only when the control has focus
  • treeview::Flags::SmallIcons or SmallIcons (for macro initialization) - this enables the small icons (one character) view mode for the tree view.
  • treeview::Flags::LargeIcons or LargeIcons (for macro initialization) - this enables the large icons (two characters or unicode surrogates) view mode for the tree view.
  • treeview::Flags::CustomFilter or CustomFilter (for macro initialization) - this enables the custom filter that can be used to filter the list of items. The custom filter should be provided by the user in the ListItem implementation.
  • treeview::Flags::NoSelection or NoSelection (for macro initialization) - this disables the selection of items from the tree view. This flag is useful when the tree view is used only for displaying information and the selection is not needed (such as a Save or Open file dialog).
  • treeview::Flags::HideHeader or HideHeader (for macro initialization) - this hides the header of the tree view. This flag is useful when the tree view is used only for displaying information and the header is not needed.

Events

To intercept events from a treeview, the following trait has to be implemented to the Window that processes the event loop:

pub trait TreeViewEvents<T: ListItem + 'static> {
    // called when the current item is changed
    fn on_current_item_changed(&mut self, 
                               handle: Handle<TreeView<T>>, 
                               item:   Handle<treeview::Item<T>>) -> EventProcessStatus 
    {
        EventProcessStatus::Ignored
    }
    
    // called whenever an item was collapes.
    // the recursive parameter indicates that all children and their children 
    // were collapesed as well
    fn on_item_collapsed(&mut self, 
                         handle:    Handle<TreeView<T>>, 
                         item:      Handle<treeview::Item<T>>, 
                         recursive: bool) -> EventProcessStatus 
    {
        EventProcessStatus::Ignored
    }

    // called whenever an item was expanded.
    // the recursive parameter indicates that all children and their children 
    // were expanded as well
    fn on_item_expanded(&mut self, 
                        handle:    Handle<TreeView<T>>, 
                        item:      Handle<treeview::Item<T>>, 
                        recursive: bool) -> EventProcessStatus 
    {
        EventProcessStatus::Ignored
    }


    // called when the selection is changed
    fn on_selection_changed(&mut self, handle: Handle<TreeView<T>>) -> EventProcessStatus 
    {
        EventProcessStatus::Ignored
    }

    // called when you double click on an item (or press Enter)
    fn on_item_action(&mut self, 
                      handle: Handle<TreeView<T>>, 
                      item:   Handle<treeview::Item<T>>) -> EventProcessStatus 
    {
        EventProcessStatus::Ignored
    }
}

Methods

Besides the Common methods for all Controls a tree view also has the following aditional methods:

Adding items

MethodPurpose
add_column(...)Adds a new column to the TreeView control. This method is in particular usefull when you need to create a custom treeview.
add(...)Adds a new item to the root of the TreeView control.
add_to_parent(...)Adds a new item as a child for another item that exists in TreeView control.
add_item(...)Adds a new item to the root of the TreeView control. This methods allows you to specify the color, icon and selection state for that item.
add_item_to_parent(...)Adds a new item as a child for another item that exists in TreeView control. This methods allows you to specify the color, icon and selection state for that item.
add_batch(...)Adds multiple items to the treeview. When an item is added to a treeview, it is imediatly filtered based on the current search text. If you want to add multiple items (using various methods) and then filter them, you can use the add_batch method.
items_count()Returns the number of items in the treeview.

Deleting items

MethodPurpose
delete_item(...)Deletes an item from the treeview. If the item has children, they will be deleted as well.
delete_item_children(...)Deletes all children of an item from the treeview.
clear()Clears all items from the treeview

Item access

MethodPurpose
current_item_handle()Returns a handle to the current item or None if the treeview is empty
current_item()Returns a immutable reference to the current item or None if the treeview is empty
current_item_mut()Returns a mutable reference to the current item or None if the treeview is empty
item(...)Returns an immutable reference to an item based on its handle or None if the handle is invalid (e.g. item was deleted)
item_mut(...)Returns a mutable reference to an item based on its handle or None if the handle is invalid (e.g. item was deleted)
root_items()Returns a list of handles to the root items in the treeview.
root_item_mut(...)Returns a mutable reference to the root item based on its index or None if the index is invalid
root_item(...)Returns an immutable reference to the root item based on its index or None if the index is invalid

Selection & Folding

MethodPurpose
select_item(...)Selects or deselects an item based on its handle.
selected_items_count()Returns the number of selected items in the treeview.
collapse_item(...)Collapses an item based on its handle. This methods takes a recursive parameter that if true will also collapse all of the item children
expand_item(...)Expands an item based on its handle. This methods takes a recursive parameter that if true will also expand all of the item children
collapse_all()Collapses all items in the treeview.
expand_all()Expands all items in the treeview.

Miscellaneous

MethodPurpose
set_frozen_columns(...)Sets the number of frozen columns. Frozen columns are columns that are not scrolled when the treeview is scrolled horizontally.
sort(...)Sorts the items in the TreeView control based on a column index.
clear_search()Clears the content of the search box of the treeview.
move_cursor_to(...)Moves the cursor to a specific item in the treeview.

Key association

The following keys are processed by a TreeView control if it has focus:

KeyPurpose
Up, DownChanges the current item from the TreeView.
Left, RightScrolls the view to the left or to the right
PageUp, PageDownNavigates through the list of items page by page.
HomeMoves the current item to the first element in the tree view
EndMoves the current item to the last element in the tree view
Shift+{Up, Down,PageUp, PageDown, Home, End}Selects multiple items in the tree view.
InsertToggle the selection state of the current item. Once the selection is toggled, the cursor will me moved to the next item in the tree view.
SpaceFolds or un-foldes an item in the tree view
Ctrl+Alt+{Up, Down}Moves the scroll up or down
EnterTriggers the TreeViewEvents::on_item_action event for the current item
Ctrl+{A..Z, 0..9}If a column has a hot key associated (by using the & character in the column name), this will sort all items bsed on that column. If that column is already selected, this will reverse the order of the sort items (ascendent or descendent)
Ctrl+{Left, Right}Enter in the column resize mode.

Aditionally, typing any character will trigger the search bar (if the flag SearchBar is present) and will filter the items based on the search text. While the search bar is active, the following keys are processed:

  • Backspace - removes the last character from the search text
  • Escape - clears the search text and closes the search bar
  • Enter - moves to the next match
  • Movement keys (such as Up, Down, Left, Right, PageUp, PageDown, Home, End) - will disable the search bar, but will keep the search text

While in the column resize mode, the following keys are processed:

  • Left, Right - increases or decreases the width of the current column
  • Ctrl+Left, Ctrl+Right - moves the focus to the previous or next column
  • Escape or movement keys - exits the column resize mode

Populating a tree view

To add items to a tree view, you can use the add and add_to_parent methods. The add method adds an item to the root of the tree view, while the add_to_parent method adds an item as a child to another item. Both of them return a handle to the newly added item.

The following example shows how to add items to a tree view:

let mut treeview = TreeView::new(Layout::new("d:c"),treeview::Flags::ScrollBars);
// add two items to the root of the tree view
let handle_item_1 = treeview.add(...);
let handle_item_2 = treeview.add(...);
// add a child item to the first item
let handle_item_3 = treeview.add_to_parent(...,handle_item_1);
// add a child to the child of the first item
let handle_item_4 = treeview.add_to_parent(...,handle_item_3);

Whenever an element is being added to a TreeView, the TreeView will try to filter and sort the item based on its content. These operations are expensive so if you need to add multiple items to a TreeView, you can use the add_batch method. This method will add all items to the TreeView and will filter and sort the items only once, after all items were added.

To add an item to a tree view, the item type has to implement the ListItem trait. Based on the implementation of this trait, the TreeView will:

  • display an item based on a specification
  • filter the item based on the search text or a specific filtering algorithm
  • sort the items based on a column index
  • get a list of columns and their specifications (name, width, alignment)

Example

The following example shows how to create a tree view with a custom item type that implements the ListItem trait:

use appcui::prelude::*;

#[derive(ListItem)]
struct MyItem {
    #[Column(name="Text", width=100)]
    text: String,
}
impl MyItem {
    pub fn new(text: &str) -> Self {
        Self {
            text: text.to_string(),
        }
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    let mut w = window!("Tree,d:c");
    let mut tv = treeview!("MyItem,d:c,flags: ScrollBars+SearchBar+HideHeader");
    let h1 = tv.add(MyItem::new("Root Item 1"));    
    let h2 = tv.add(MyItem::new("Root Item 2"));
    let h1_1 = tv.add_to_parent(MyItem::new("First Child of Root Item 1"), h1);
    let h1_2 = tv.add_to_parent(MyItem::new("Second Child of Root Item 1"), h1);
    let h1_3 = tv.add_to_parent(MyItem::new("Third Child of Root Item 1"), h1);
    let h1_1_1 = tv.add_to_parent(MyItem::new("First Child of First Child of Root Item 1"), h1_1);
    let h2_1 = tv.add_to_parent(MyItem::new("First Child of Root Item 1"), h2);
    let h2_2 = tv.add_to_parent(MyItem::new("Second Child of Root Item 1"), h2);

    w.add(tv);
    a.add_window(w);
    a.run();
    Ok(())
}

VLine

Represent a vertical line:

To create a vertical line use VLine::new method (with 2 parameters: a layout and a set of flags). The flags let you choose if it is a double line.

let a = VLine::new(Layout::new("x:1,y:1,h:10"), Flags::None);
let b = VLine::new(Layout::new("x:3,y:1,h:20"), Flags::DoubleLine);

or the macro vline!

let hl1 = vline!("x:1,y:1,h:10");
let hl2 = vline!("x:3,y:1,h:20,flags:DoubleLine");

A vertical line supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
flagsEnumNoFlags to specify how the line should be drown

Where the flags are defined as follows:

  • vline::Flags::DoubleLine or DoubleLine (for macro initialization) - this will draw a double line instead of a single one.

Events

A vertical line emits no events.

Methods

A vertical line has no aditional methods.

Key association

A vertical line does not receive any input and as such it has no key associated with it.

Example

The following code creates a window with a vertical line.

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    let mut app = App::new().build()?;
    let mut w = Window::new("Title", Layout::new("d:c,w:40,h:20"), window::Flags::None);
    
    w.add(VLine::new(Layout::new("x:3,y:1,h:15"), vline::Flags::DoubleLine));
    app.add_window(w);
    app.run();
    Ok(())
}

Vertical Splitter

Renders a vertical splitter that allows the user to resize the two panes it separates.

To create a vertical splitter use VSplitter::new method or the vsplitter! macro.

#![allow(unused)]
fn main() {
let vs_1 = VSplitter::new(0.5,Layout::new("x:1,y:1,w:20,h:10"),vsplitter::ResizeBehavior::PreserveRightPanelSize);
let vs_2 = VSplitter::new(20,Layout::new("x:1,y:1,w:20,h:10"),vsplitter::ResizeBehavior::PreserveRightPanelSize);
}

or

#![allow(unused)]
fn main() {
let vs_3 = vsplitter!("x:1,y:1,w:20,h:10,pos:50%");
let vs_4 = vsplitter!("x:1,y:1,w:20,h:10,pos:20,resize:PreserveRightPanelSize");
}

A vertical splitter supports all common parameters (as they are described in Instantiate via Macros section). Besides them, the following named parameters are also accepted:

Parameter nameTypePositional parameterPurpose
posCoordonateYes (first postional parameter)The position of the splitter (can be an abosolute value - like 10 or a percentage like 50% )
resize or resize-behavior or on-resize or rbStringNoThe resize behavior of the splitter. Can be one of the following: PreserveLeftPanelSize, PreserveRightPanelSize or PreserveAspectRatio
min-left-width or minleftwidth or mlwDimensionNoThe minimum width of the left panel (in characters - e.g. 5) or as a percentage (e.g. 10%)
min-right-width or minrightwidth or mrwDimensionNoThe minimum width of the right panel (in characters - e.g. 5) or as a percentage (e.g. 10%)

A vertial splitters supports the following resize modes:

  • vsplitter::ResizeBehavior::PreserveLeftPanelSize or PreserveLeftPanelSize (for macro initialization) - this will keep the size of the left panel constant when resizing the splitter
  • vsplitter::ResizeBehavior::PreserveRightPanelSize or PreserveRightPanelSize (for macro initialization) - this will keep the size of the right panel constant when resizing the splitter
  • vsplitter::ResizeBehavior::PreserveAspectRatio or PreserveAspectRatio (for macro initialization) - this will keep the aspect ratio of the two panels constant when resizing the splitter

Events

A vertical splitter emits no events.

Methods

Besides the Common methods for all Controls a vertical splitter also has the following aditional methods:

MethodPurpose
add(...)Adds an element to the left or right panel of the splitter.
set_min_width(...)Sets the minimum width of the left or right panel.
set_position(...)Sets the position of the splitter. If an integer value is being used, the position will be considered in characters. If a flotant value (f32 or f64) is being used, the position will be considered as a percentage.
position()Returns the current position of the splitter (in characters).

Key association

The following keys are processed by a VSplitter control if it has focus:

KeyPurpose
Ctrl+LeftMoves the splitter one character to the left
Ctrl+RightMoves the splitter one character to the right
Ctrl+Shift+LeftMove the splitter to its left most position
Ctrl+Shift+RightMove the splitter to its right most position

Example

The following code creates a window with a vertical splitter that separates two panels. The left panel contains a panel with the text Left and the right panel contains a panel with the text Right.

use appcui::prelude::*;

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    let mut w = window!("'Vertical Splitter',d:c,w:50,h:10,flags: Sizeable");
    let mut vs = vsplitter!("50%,d:c,w:100%,h:100%,resize:PreserveRightPanelSize");
    vs.add(vsplitter::Panel::Left,panel!("Left,l:1,r:1,t:1,b:1"));
    vs.add(vsplitter::Panel::Right,panel!("Right,l:1,r:1,t:1,b:1"));
    w.add(vs);
    a.add_window(w);
    a.run();
    Ok(())
}

Custom controls

While the existing stock controls should suffice for most apps, there is sometines a need to create a custom control. This can be done using a special macro: `#[CustomControl(...)] as follows:

#[CustomControl(...)]
struct MyCustomControl {
    // aditional fields
}

A custom control accepts the following atributes (via #[CustomControl(...)] macro):

  • events with two possible values or combinations: MenuEvents and/or CommandBarEvents:
    #[CustomControl(events = MenuEvent+CommandBarEvent)]
    struct MyCustomControl {
        // aditional fields
    }
    
  • overwrite to allow one to overwrite certain traits (for painting or resizing):
    #[CustomControl(overwrite = OnPaint+OnReisze)]
    struct MyCustomControl {
        // aditional fields
    }
    
  • emit to describe a list of events that the current control can emit towards the event loop:
    #[CustomControl(emit = Playe1Wins+Playe2Wins+GameOver)]
    struct MyCustomControl {
        // aditional fields
    }
    
  • commands (as they are described in Commands section)

A simple example

The following example creates a simple custom control with the X character written in Yellow over Red background and a White double border.

use appcui::prelude::*;

#[CustomControl(overwrite = OnPaint)]
struct MyControl {}
impl MyControl {
    fn new(layout: Layout) -> Self {
        Self { base: ControlBase::new(layout, true) }
    }
}

impl OnPaint for MyControl {
    fn on_paint(&self, surface: &mut Surface, _theme: &Theme) {
        surface.clear(char!("'X',Yellow,DarkRed"));
        let size = self.size();
        surface.draw_rect(
            Rect::with_point_and_size(Point::ORIGIN, size),
            LineType::Double,
            CharAttribute::with_fore_color(Color::White),
        );
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    let mut w = window!("caption:'Custom Control',d:c,w:30,h:10");
    w.add(MyControl::new(Layout::new("l:1,t:1,r:1,b:1")));
    a.add_window(w);
    a.run();
    Ok(())
}

Remarks: Notice that a new data member base has been create by the #[CustomControl] macro. This data member provides all standard methods that every control has (related to visibility, enablement, etc). This data meber must be instantiated in one of the following two ways:

ControlBase::new(layout: Layout, accept_into: bool)

or

ControlBase::with_focus_overlay(layout: Layout)

where:

  • layout is the Layout of the control
  • accept_input is either true if we want the new custom control to receive events from mouse and/or keyboard or false otherwise (the last case is usually when a control similar to a label is being create).

The second method (ControlBase::with_focus_overlay) is used when we want to create a custom control that will extend its size one character to the bottom and one character to the right.

Overwriteable traits

The following traits can be overwritten in a custom control:

  • OnPaint
  • OnResize
  • OnFocus
  • OnExpand
  • OnDefaultAction
  • OnKeyPressed
  • OnMouseEvent

OnPaint

OnPaint trait methods are called whenever a control is being painted:

pub trait OnPaint {
    fn on_paint(&self, surface: &mut Surface, theme: &Theme) {

    }
}

The surface object will be clipped to the visible space ocupied by the control and the coordonates will be translated to corespond to the top-left corner of the control(this means that surface.write_char(0,0,...) will draw a character to the top-left corner of the control).

OnResize

OnResize trait methods are called whenever the control is being resized:

pub trait OnResize {
    fn on_resize(&mut self, old_size: Size, new_size: Size) {

    }
}

if old_size parameter has the size of (0x0) then this is the first time this method is being called.

OnFocus

OnFocus trait methods are called whenever the control either receives the focus or it loses it:

pub trait OnFocus {
    fn on_focus(&mut self) {}
    fn on_lose_focus(&mut self) {}
}

OnExpand

OnExpand methods are called whenever a control is being expanded or packed. An expanded control is a control that increases its size when it has the focus amd packs back to its original size when the control loses its focus. One such example is the ColorPicker.

pub trait OnExpand {
    fn on_expand(&mut self, direction: ExpandedDirection) { }
    fn on_pack(&mut self) { }
}

OnDefaultAction

OnDefaultAction methods a default action is balled for a control. The default action is different from one control to another (for example in case of a button - the default action is similar to clicking the button, for a checkbox is similar to checking or unchecking the control, etc).

This method is also associated with the control hot key. Assuming we have a hot key associated with a control, pressing that hot key is equivalent to:

  1. changing the focus of the control (if it does not have the focus)
  2. calling OnDefaultAction::on_default_action() for that control.

For example, if a button has a hot key, pressinhg that hot-key is similar to clicking the button.

pub trait OnDefaultAction {
    fn on_default_action(&mut self) {        
    }
}

OnKeyPressed

OnKeyPressed methods are called whenever a key is pressed. The control must have the focus at that point.

pub trait OnKeyPressed {
    fn on_key_pressed(&mut self, key: Key, character: char) -> EventProcessStatus {
        EventProcessStatus::Ignored
    }
}

if OnKeyPressed::on_key_pressed(...) returns EventProcessStatus::Ignored the key is being send to the parent of the current control. If the method returns EventProcessStatus::Processed the cotrol will ne redrawn and the event will not be passed to its parent anymore.

The following custom control uses arrow keys to move a rectangle within the control:

use appcui::prelude::*;

#[CustomControl(overwrite = OnPaint+OnKeyPressed)]
struct MyControl {
    p: Point,
}
impl MyControl {
    fn new(layout: Layout) -> Self {
        Self {
            base: ControlBase::new(layout, true),
            p: Point::ORIGIN,
        }
    }
}

impl OnPaint for MyControl {
    fn on_paint(&self, surface: &mut Surface, _theme: &Theme) {
        surface.clear(char!("' ',black,black"));
        surface.draw_rect(
            Rect::with_point_and_size(self.p, Size::new(2, 2)),
            LineType::Double,
            CharAttribute::with_fore_color(Color::White),
        );
    }
}
impl OnKeyPressed for MyControl {
    fn on_key_pressed(&mut self, key: Key, _character: char) -> EventProcessStatus {
        match key.value() {
            key!("Left")  => { self.p.x -= 1; EventProcessStatus::Processed }
            key!("Right") => { self.p.x += 1; EventProcessStatus::Processed }
            key!("Up")    => { self.p.y -= 1; EventProcessStatus::Processed }
            key!("Down")  => { self.p.y += 1; EventProcessStatus::Processed }
            _             => EventProcessStatus::Ignored
        }        
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    let mut w = window!("caption:'Custom Control',d:c,w:30,h:10");
    w.add(MyControl::new(Layout::new("l:1,t:1,r:1,b:1")));
    a.add_window(w);
    a.run();
    Ok(())
}

OnMouseEvent

OnMouseEvent trait methods can be use to react to mouse events such as clicks, drag, wheel movement, etc.

pub trait OnMouseEvent {
    fn on_mouse_event(&mut self, event: &MouseEvent) -> EventProcessStatus {
        EventProcessStatus::Ignored
    }
}

if OnMouseEvent::on_mouse_event(...) returns EventProcessStatus::Processed the control is going to be repainted, otherwise nothing happens.

A tipical implementation for this trait looks like the following one:

impl OnMouseEvent for /* control name */ {
    fn on_mouse_event(&mut self, event: &MouseEvent) -> EventProcessStatus {
        match event {
            MouseEvent::Enter => todo!(),
            MouseEvent::Leave => todo!(),
            MouseEvent::Over(_) => todo!(),
            MouseEvent::Pressed(_) => todo!(),
            MouseEvent::Released(_) => todo!(),
            MouseEvent::DoubleClick(_) => todo!(),
            MouseEvent::Drag(_) => todo!(),
            MouseEvent::Wheel(_) => todo!(),
        }
    }
}

The following example intercepts the mouse movement while the mouse is over the control and prints it.

use std::fmt::Write;
use appcui::prelude::*;

#[CustomControl(overwrite = OnPaint+OnMouseEvent)]
struct MyControl {
    text: String
}
impl MyControl {
    fn new(layout: Layout) -> Self {
        Self {
            base: ControlBase::new(layout, true),
            text: String::new()
        }
    }
}

impl OnPaint for MyControl {
    fn on_paint(&self, surface: &mut Surface, _theme: &Theme) {
        surface.clear(char!("' ',black,black"));
        surface.write_string(0, 0, &self.text, CharAttribute::with_fore_color(Color::White), false);
    }
}

impl OnMouseEvent for MyControl {
    fn on_mouse_event(&mut self, event: &MouseEvent) -> EventProcessStatus {
        match event {
            MouseEvent::Enter | MouseEvent::Leave => EventProcessStatus::Processed,
            MouseEvent::Over(data) => {
                self.text.clear();
                write!(&mut self.text,"Mouse at: ({}x{})", data.x, data.y).unwrap();
                EventProcessStatus::Processed
            },
            _ => EventProcessStatus::Ignored
        }
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    let mut w = window!("caption:'Custom Control',d:c,w:30,h:10");
    w.add(MyControl::new(Layout::new("l:1,t:1,r:1,b:1")));
    a.add_window(w);
    a.run();
    Ok(())
}

Emitting custom events

There are scenarios where a custom control needs to emit some events that will be used in the event loop window. For this the following steps need to be performed:

  1. Create a list of events in the custom control using emit attribute. For example:

    #[CustomControl(emit:Event1+Event2+Event3)]
    pub struct MyCustomControl {
        // data members
    }
    

    Make sure that the custom control is defined with appropriate visibility as you will need to use it in another structure (e.g. where you define the event loop via #[Window(...)] or #[ModalWindow(...)]).

  2. When you create an event loop via #[Window(...)] add the following attribute custom_events that has as value a list of all custom controls from where you want to intercept custom events. For example:

    #[Window(custom_events: CustomControl1Events+CustomControl2Events+...)]
    struct MyWin {
        // data members
    }
    

    Remarks: The name of the custom event MUST be in the following format: CustomControlName followed by Events.

  3. Implement the custom event trait in the following way:

    impl MyCustomControlEvents for MyWin {
        fn on_event(&mut self, handle: Handle<CustomControl>, 
                               event: customcontrl::Events) -> EventProcessStatus 
        {
            // add your code here
        }
    }
    

    Remarks: Returning EventProcessStatus::Processed will repaint the entire custom control.

  4. Make sure that you import the custom control in you window file. The #[CustomControl(...)] creates an internal module with the same name (but with lowercase) as the custom control where an enum (named Events) will store all defined custom events. As such, the file where you define the window (event loop) should have the following:

    use <path to custom control>::MyCustomControl;
    use <path to custom control>::mycustomcontrol::*;
    

Example

Let's consider the following example:

  • we want to create a custom control for a Chess game. That control will display all pieces, will allow movement and will notify the main window when the game is over.
  • we will also need a main window from where we can start the game, make configuration changes, etc.

Let's start by designing the Chess custom control. We will create a separate file (chess.rs) where we will define the control in the following way:

use appcui::prelude::*;

#[CustomControl(overwrite: OnPaint+OnKeyPressed+OnMouseEvent,
                emit     : DrawGame+Player1Wins+Player2Wins)]
pub struct Chess {
    // private data
}
impl OnPaint for Chess { ... }
impl OnMouseEvent for Chess { ... }
impl OnKeyPressed for Chess { ...}

This code will also create an inner module that contains an Events enum with the following variants:

pub mod chess {
    #[repr(u32)]
    #[derive(Copy, Clone, Eq, PartialEq, Debug)]
    pub enum Events {
        DrawGame,
        Player1Wins,
        Player2Wins
    }
}

Notice that we define the structure Chess as public. The visibility attribute is copied into the inner module and as such, the module chess is public as well.

Also, a method raise_event(event: chess::Events) was added to the Chess struct.

Now lets see how we can define the event loop component (the window where the custom control will be added):

use appcui::prelude::*;
use <path_to_cheese_file>::Chess;
use <path_to_cheese_file>::chess::*;

#[Window(custom_events: ChessEvents)]
struct MyWin {
    // data members
    table: Handle<Chess>
}

impl ChessEvents for MyWin {
        fn on_event(&mut self, handle: Handle<Chess>, 
                               event: board::Events) -> EventProcessStatus 
        {
            match event {
                chess::Events::DrawGame    => { /* in case of a draw game */ }
                chess::Events::Player1Wins => { /* Player one has won     */ }
                chess::Events::Player2Wins => { /* Player two has won     */ }
            }
        }
    }
}

Overlay on Focus support

Sometimes, you may want to have aditional space in a control when it has a focus (e.g. to draw some extract components that make sense only when the control has a focus). Example of such behavior include:

  • scrollbars - when a control has scrollbars, the scrollbars are drawn only when the control has a focus.
  • searchbar - when a control has a focus, a search bar is drawn that the control can use to filter/find inner items.
  • aditional information - controls that operates over a container (e.g. a list) can use the overlay to show some aditional information about the current item or the current state of the control (e.g. number of items selected, etc).

Constructor

To create a custom control that has a focus overlay, you need to use the ControlBase::with_focus_overlay method.

use appcui::prelude::*;

#[CustomControl(overwrite = OnPaint)]
struct MyControl {
    // aditional fields
}
impl MyControl {
    fn new(layout: Layout) -> Self {
        Self { 
            base: ControlBase::with_focus_overlay(layout) 
            // initialization of aditional fields
        }
    }
}

Remember: Using ControlBase::with_focus_overlay implies that the control would receive imput events (e.g. keyboard, mouse).

Behavior

When a control has a focus overlay, the control will extend its size by one character to the right and one character to the bottom when if has focus. This will allow it to be drawn over its parent if needed (e.g. if a control is within a window, it will extend over the window's border).

The control will also receive all mouse events if they are triggered over the overlay area. This means that the control has to handle them or pass them to the parent control (to allow a normal behavior).

OnPaint trait

As previously mentioned, q custom control (if created with ControlBase::with_focus_overlay) will increase its size by one character to the right and one character to the bottom only if it has the focus. This means that the control will have to handle the drawing of the overlay area (if needed) in two cases:

  1. when the control has the focus and it is being drawn
  2. when the control does not have the focus and it is being drawn

A tipical implementation of the OnPaint trait for a custom control that has a focus overlay would do the following steps in the on_paint method:

  1. Check if the control has the focus
  2. If it has the focus:
    • draw (if needed) the overlay area (the area that is one character to the right and one character to the bottom of the control)
    • reduce the clip area of the surface to the size of the control (without the overlay area). This can be done by calling surface.reduce_clip_by(0, 0, 1, 1).
  3. Draw the normal area of the control

A typical implementation of the OnPaint trait for a custom control that has a focus overlay would look like this:

use appcui::prelude::*;

#[CustomControl(overwrite = OnPaint)]
struct MyControl {
    // aditional fields
}
impl MyControl {
    fn new(layout: Layout) -> Self {
        Self { 
            base: ControlBase::with_focus_overlay(layout) 
            // initialization of aditional fields
        }
    }
}
impl OnPaint for MyControl {
    fn on_paint(&self, surface: &mut Surface, theme: &Theme) {
        if self.has_focus() {
            // draw the overlay area
            surface.reduce_clip_by(0, 0, 1, 1);
            // now the surface has the exact same size 
            // as if it would not have the focus
        }   
        // draw the normal area
    }
}

Final remarks

Keep in mind that if focus ovelay is being used you may still want to convay mouse events to the parent control. Lets consider a custom control that fills up the entire window. If the window is sizeable, it means that the bottom-right corner of the window has a grip that allows the user to resize the window. If the custom control is not passing the mouse events to the parent, the user will not be able to resize the window anymore. This means that you need to be carefull where you draw when in the overlay area as well as what events you pass to the parent.

You can however use the ControlBase::set_components_toolbar_margins(...) method to set the left and top margins for the overlay components (this will allow the AppCUI framework to avoid sending mouse events if they originated from the overlay area, but are outside the margins).

For example:

use appcui::prelude::*;

#[CustomControl(overwrite = OnPaint)]
struct MyControl {
    // aditional fields
}
impl MyControl {
    fn new(layout: Layout) -> Self {
        let mut me = Self { 
            base: ControlBase::with_focus_overlay(layout) 
            // initialization of aditional fields
        };
        me.set_components_toolbar_margins(5,4);
        me
    }
}

Assuming the control size is 20 x 10 characters. Then, with this setup , when it has the focus, the overlay area will be 21 x 11 characters but the mouse event will be send only in the following cases:

  • if the mouse coordonate (relative to the control) is between (0,0) and (20,10) - the normal area
  • if the mouse coordonate (relative to the control) is between (5,10) and (21,10) - the bottom side of the overlay area (but only from the 5th character as it was specified in the me.set_components_toolbar_margins(5,4); command)
  • if the mouse coordonate (relative to the control) is between (20,4) and (21,11) - the right side of the overlay area (but only from the 4th character as it was specified in the me.set_components_toolbar_margins(5,4); command)

ScrollBars

Scrollbars are a set of components that are used to navigate through a control that has a large content. They are typically used in controls that handle a large amount of data, such as text editors, image viewers, and other controls that display a lot of information. Scrollbars are usually displayed only when the control has the focus and they are not visible when the control does not have the focus.

AppCUI provides a special structure (called ScrollBars) that can be used to create a set of scrollbars for a control. The ScrollBars structure is created by calling the ScrollBars::new method and it takes two parameters (the width and the height of the content to be displayed).

Scrollbars can only be used with controls that have a focus overlay. This means that the control must be created with the ControlBase::with_focus_overlay method.

Methods

The ScrollBars structure has the following methods:

MethodPurpose
paint(...)Paints the scrollbars on the given surface and theme.
process_mouse_event(...)Processes a mouse event if it is triggered over the scrollbars and returns true in this case or false otherwise. It is a useful method to filter our scenarios where a mouse event should be pass to the parent control.
should_repaint(...)Returns true if the scrollbars should be repainted.
horizontal_index()Returns the current horizontal index of the scrollbars.
vertical_index()Returns the current vertical index of the scrollbars.
offset()Returns a point that represents from where in the current control with its current clippint the content should be drawn to the surface. This is useful when the control has a large content and it needs to be drawn in a specific area of the surface.
set_indexes(...)Sets the horizontal and vertical indexes of the scrollbars.
resize(...)Resizes the scrollbars to the given width and height. This method is often called when on_resize(...) trait from a custom control is being called.

Usage

A typical usage of the ScrollBars structure would look like this:

  1. Create a custom control (make sure it is created with ControlBase::with_focus_overlay method) and add a scrollbars field to it:

    use appcui::prelude::*;
    
    #[CustomControl(overwrite = OnPaint+OnResize+OnMouseEvents+OnKeyPressed)]
    struct MyControl {
        sb: ScrollBars
    }
    impl MyControl {
        fn new(layout: Layout) -> Self {
            Self { 
                base: ControlBase::with_focus_overlay(layout) 
                sb: ScrollBars::new(/* content width */, 
                                    /* contront height */),
            }
        }
    }
    
  2. The on_paint method should look like this:

    impl OnPaint for MyControl {
        fn on_paint(&self, surface: &mut Surface, theme: &Theme) {
            if self.base.has_focus() {
                // draw the scrollbars
                self.sb.paint(surface, theme, self);
                // reduce the clip area to the size of the 
                // control (without the overlay area)
                surface.reduce_clip_by(0, 0, 1, 1);
            }
            // draw the content of the control
            // you can use at this point the `self.sb.offset()` method to 
            // get the offset from where the content should be drawn or 
            // the self.sb.horizontal_index() and self.sb.vertical_index() 
            // methods to get the current indexes and customize the drawing 
            // of the content
        }
    }
    
  3. The on_resize method should look like this:

    impl OnResize for MyControl {
        fn on_resize(&mut self, old_size: Size, new_size: Size) {
            self.sb.resize(/* content width  */, 
                           /* content height */, 
                           &self.base);
        }
    }
    

    It is important to call the resize method of the scrollbars in the on_resize method of the control. This will ensure that the scrollbars are resized to fit the new size of the control.

  4. The on_mouse_event method should look like this:

    impl OnMouseEvent for MyControl {
        fn on_mouse_event(&mut self, event: &MouseEvent) -> EventProcessStatus {
            // if the event can be processed by the scrollbars
            // then return
            if self.sb.process_mouse_event(event) {
                return EventProcessStatus::Processed;
            }
            match event {
                // process the event for the control
            }
        }
    }
    
    
    
  5. Aditionally, you can change the indexes manually (for example via an OnKeyPressed event) by using the self.sb.set_indexes(...) method. This is useful when you want to change the indexes of the scrollbars based on a specific event (e.g. a key pressed event).

    impl OnKeyPressed for Canvas {
        fn on_key_pressed(&mut self, key: Key, _character: char) -> EventProcessStatus {
            match key.value() {
                key!("Up") => {
                    self.sb.set_indexes(self.sb.horizontal_index(), 
                                        self.sb.vertical_index().saturating_sub(1));
                    return EventProcessStatus::Processed;
                }
                key!("Down") => {
                    self.sb.set_indexes(self.sb.horizontal_index(), 
                                        self.sb.vertical_index() + 1);
                    return EventProcessStatus::Processed;
                }
                key!("Left") => {
                    self.sb.set_indexes(self.sb.horizontal_index().saturating_sub(1), 
                                        self.sb.vertical_index());
                    return EventProcessStatus::Processed;
                }
                key!("Right") => {
                    self.sb.set_indexes(self.sb.horizontal_index() + 1, 
                                        self.sb.vertical_index());
                    return EventProcessStatus::Processed;
                }
                // process other keys
                _ => {}
            }
            EventProcessStatus::Ignored
        }
    }
    

Scrollbar size

You can change the scrollbars size by using the ControlBase::set_components_toolbar_margins(left,top) method to specify:

  • where on the X-axes (left parameter) the horizontal scrollbars should be drawn (relative to the control)
  • where on the Y-axes (top parameter) the vertical scrollbars should be drawn (relative to the control)

Object Traits

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);
        }
    }      
}

EnumSelector

The EnumSelector trait is a trait design to provide controls like Selector. The trait has to be implemented by the enum types you wish to associate with a selection mechanism.

pub trait EnumSelector {
    const COUNT: u32;
    fn from_index(index: u32) -> Option<Self> where Self: Sized;
    fn name(&self) -> &'static str;
    fn description(&self) -> &'static str {
        ""
    }
}

These methods have the following purpose:

  • COUNT
    This constant defines the total number of variants in the enum. It is used to assist in selection logic and must be set appropriately.

  • from_index(index) This method is responsible for mapping a provided index to a specific variant of the enum. It should return Some(variant) if the index is valid, and None if the index is out of bounds.

  • name()
    This method returns the name of the enum variant as a static string. It can be used to display the name of the variant in user interfaces or for documentation purposes.

  • description() This method provides a description of the enum variant. By default, it returns an empty string, but it can be overridden to provide more detailed information about the variant.

    Example

Lets consider the following enum: Shape with the following structure:

enum Shape {
    Square,
    Rectangle,
    Triangle,
    Circle,
}

In order to use this enum in a EnumSelector, the minimum implementation of the EnumSelector trait would be:

impl EnumSelector for Shape {
    const COUNT: u32 = 4;

    fn from_index(index: u32) -> Option<Self> where Self: Sized {
        match index {
            0 => Some(Shape::Square),
            1 => Some(Shape::Rectangle),
            2 => Some(Shape::Triangle),
            3 => Some(Shape::Circle),
            _ => None
        }
    }

    fn name(&self) -> &'static str {
        match self {
            Shape::Square => "Square",
            Shape::Rectangle => "Rectangle",
            Shape::Triangle => "Triangle",
            Shape::Circle => "Circle",
        }
    }

    fn description(&self) -> &'static str {
        match self {
            Shape::Square => "a red square",
            Shape::Rectangle => "a green rectangle",
            Shape::Triangle => "a blue triangle",
            Shape::Circle => "a white circle",
        }        
    }
}

Alternatively, you can use the EnumSelector derive macro to automatically implement the EnumSelector trait for a enum This means that the previous Shape enum can be rewritten as follows:

#[derive(EnumSelector, Eq, PartialEq, Copy, Clone)]
enum Shape {
    #[VariantInfo(name = "Square", description = "a red square")]
    Square,

    #[VariantInfo(name = "Rectangle", description = "a green rectangle")]
    Rectangle,

    #[VariantInfo(name = "Triangle", description = "a blue triangle")]
    Triangle,

    #[VariantInfo(name = "Circle", description = "a white circle")]
    Circle,
}

Make sure that you also add the following derives: Eq, PartialEq, Copy and Clone to the enum. This is required for the EnumSelector derive macro to work properly.

Desktop

The desktop is the root control for all controls in an AppCUI application. There is only onde such object created and it is always created during the AppCUI framework initialization. Creating another desktop object after this point will result in a panic.

The desktop will always have the same size as the terminal. Resizing the terminal implicetelly resizes the desktop as well.

The desktop object is created by default when the AppCUI framework is initiated (via App::new(...) command). However, if needed a custom desktop can be provided.

Custom Desktop

A custom desktop is an user defined desktop where various method can be overwritten and system events can be processed.

To build a custom Desktop that supports event handling, you must use a special procedural macro call Desktop, defined in the the following way:

#[Desktop(events=..., overwrite=... )]
struct MyDesktop {
    // specific fields
}

where the attribute events has the following form: events=EventTrait-1 + EventTrait-2 + EventTrait-3 + ... EventTrait-n

and an event trait can be one of the following:

  • MenuEvents
  • CommandBarEvents
  • DesktopEvents

and the overwrite atribute allows you to overwrite the following traits:

  • OnPaint
  • OnResize

In other words, a custom Desktop object can have specific logic for paint and for scenarious where it gets resized and can process its internal events as well as events from menus and command bar.

Events

The most important event trait for a desktop is DesktopEvents (that allows you to intercept desktop specific events):

pub trait DesktopEvents {
    fn on_start(&mut self) { }
    fn on_close(&mut self) -> ActionRequest {...}
    fn on_update_window_count(&mut self, count: usize) {...}
}

These methods have the following purpose:

  • on_start is being called once (after the AppCUI framework was started). A desktop object is being constructed before AppCUI framework starts. As such, you can not instantiate other objects such as menus, windows, etc in its constructor. However, you can do this by overwriting the on_start method.
  • on_close is called whenever a desktop is being closed (usually when you pressed Escape key on a desktop). You can use this to performa some aditional validations (such sa saving all files, closing various handles, etc)
  • on_update_window_count is being called whenever a new window is being added or removed from the desktop. You can use this method to re-arange the remaining methods.

Using the custom desktop

To use the custom desktop, use the .desktop(...) method from the App like in the following example:

#[Desktop(events=..., overwrite=...)]
struct MyDesktop {
    // aditional fields
}
impl MyDesktop {
    fn new()->Self {...}
    // aditional methods
}
// aditional implementation for events and overwritten traits

fn main() -> Result<(), appcui::system::Error> {
    let a = App::new().desktop(MyDesktop::new()).build()?;
    // do aditional stuff with the application
    // such as add some windows into it
    a.run();
    Ok(())
}

It is important to notice that usually it is prefered that the entire logic to instantiate a desktop and add windows / menus or other settings to be done via on_start method. From this point of view, the code from main becomes quite simple:

fn main() -> Result<(), appcui::system::Error> {
    App::new().desktop(MyDesktop::new()).build()?.run();
    Ok(())
}

Methods

Besides the Common methods for all Controls a desktop also has the following aditional methods:

MethodPurpose
terminal_size()Returns the size of the current terminal
desktop_rect()Returns the actual rectangle for the desktop. If menu bar and command bar are prezent, the desktop rectangle provides the visible side of the desktop. For example, if the terminal size is 80x20 and we also have a coomand bar and a menu bar, then the desktop rectangle will be [Left:0, Top:1, Right:79, bottom:18]
add_window(...)Adds a new window to the desktop
arrange_windows(...)Arranges windows on the desktop. 4 methods are provided: Cascade, Verical, Horizontal and Grid
close()Closes the desktop and the entire app

Key associations

A desktop intercepts the following keys (if they are not process at window level):

KeyPurpose
Tab or Ctrl+TabChanges the focus to the next window
Shift+Tab or Ctrl+Shift+TabChanges the focus to the previous window
EscapeCalls the on_close method and if the result is ActionRequest::Allow closes the desktop and the entire application.

If hotkeys are present for window, Alt+{hotkey} is checked by the desktop window and the focused is moved to that window that has that specific hotkey association.

Example

The following example created a custom desktop that that prints My desktop on the top-left side of the screen with white color on a red background. The desktop has one command (AddWindow) to add new windows via key Insert.

At the same time, DesktopEvents::on_update_window_count(...) is intercepted and whenever a new window is being added, it reorganize all windows in a grid.

use appcui::prelude::*;

#[Desktop(events: CommandBarEvents+DesktopEvents, overwrite:OnPaint, commands:AddWindow)]
struct MyDesktop {
    index: u32,
}
impl MyDesktop {
    fn new() -> Self {
        Self {
            base: Desktop::new(),
            index: 1,
        }
    }
}
impl OnPaint for MyDesktop {
    fn on_paint(&self, surface: &mut Surface, theme: &Theme) {
        surface.clear(theme.desktop.character);
        surface.write_string(1, 1, "My desktop", CharAttribute::with_color(Color::White, Color::Red), false);
    }
}
impl DesktopEvents for MyDesktop {
    fn on_update_window_count(&mut self, _count: usize) {
        self.arrange_windows(desktop::ArrangeWindowsMethod::Grid);
    }   
}
impl CommandBarEvents for MyDesktop {
    fn on_update_commandbar(&self, commandbar: &mut CommandBar) {
        commandbar.set(key!("Insert"), "Add new_window", mydesktop::Commands::AddWindow);
    }

    fn on_event(&mut self, command_id: mydesktop::Commands) {
        match command_id {
            mydesktop::Commands::AddWindow => {
                let name = format!("Win─{}", self.index);
                self.index += 1;
                self.add_window(Window::new(&name, Layout::new("d:c,w:20,h:10"), window::Flags::None));
            }
        }
    }
}

fn main() -> Result<(), appcui::system::Error> {
    App::new().size(Size::new(80,20)).desktop(MyDesktop::new()).command_bar().build()?.run();
    Ok(())
}

Commands

All custom controls, windows (including modal) and desktop supports a set of commands that are associated with their functionality.

To define such commands, use the attribute commands when define a window, modal window, desktop or custom control:

#[Window(commands = <list of commands>)]

The list of commands can be added in two ways:

  • use [command1, command2, ... command-n] format
  • use command1+command2+...command-n format

Example

#[Window(commands = [Save,Load,New]])]

and

#[Window(commands = Save+Load+New)]

are identical and create 3 commands that are supported by the new windows (Save, Load and New).

Enum

Including a command attribute implicitly generates an enum within a module. The module's name is derived from the desktop, window, or custom control for which the commands are created, using its name in lowercase.

For example, the following definition:

#[Window(commands = Save+Load+New)]
struct MyWindow { /* data memebers */ }

will create the following:

mod mywindow {
    #[repr(u32)]
    #[derive(Copy, Clone, Eq, PartialEq, Debug)]
    pub enum Commands {
        Save = 0,
        Open = 1,
        New = 2
    }
}

Notice that the module name mywindow is the lowercase name of the window MyWindow.

You can further use these commands for menus or the command bar (via mywindow::Commands::Save, or mywindow::Commands::Open, ...).

You can also use them for parameter cmd in the macro definition for menus or menu items. If the macro parameter defines the name of the class via the parameter class (e.g. class = MyWindow) you don't have to write the full qualifier in the macro, you can write the name of the command alone.

For example, the following are equivalent:

cmd="mywin::Command::Save"

and

cmd="Save", class="MyWin"

Menu

A menu is a list of items (that represents commands, checkboxes and single choice elements) that can be display over the existing controls.

To create a menu, use Menu::new(...) method or the macro menu! (this can be used to quickly create complex static menus).

let m = Menu::new("&File")

The name of the menu might include the special character &. This designames the next character as the hot key needed to activate the menu (in the previous example this will be Alt+F).

Registration

Each menu, once create has to be registered into the AppCui framework (registration will provide a handle to that menu that can further be used to get access to it). To register a menu, use the .register_menu(...) method that is available on every control.

A common flow is to create menus when initializing a control (usually a window), just like in the following example:

#[Window(events = MenuEvents, commands=[<list of commands>)]
struct MyWin {
    menu_handle_1: Handle<Menu>,
    menu_handle_2: Handle<Menu>,
    // other haddles
}
impl MyWin {
    fn new() -> Self {
        let mut w = MyWin {
            base: window!(...),
            menu_handle_1: Handle::None,
            menu_handle_2: Handle::None,
            // other handle initialization,
        };
        // first menu
        let m1 = Menu::new(...);
        // add items to menu 'm1'
        w.menu_handle_1 = w.register_menu(m1);

        // second menu
        let m2 = Menu::new(...);
        // add items to menu 'm2'
        w.menu_handle_2 = w.register_menu(m2);

        w
    }
}

Events

Using a menu implies that you will need to implement MenuEvents into the desktop / window or a custom control to receive the associated action from a menu. MenuEvents trait is described as follows:


trait MenuEvents {
    fn on_menu_open(&self, menu: &mut Menu) {
        // called whenever a menu is being opened
        // by AppCUI framework
        // This method can be use to change 
        // certain menu related aspects, such as
        // - enable/disable menu items
        // - add new items
    }

    fn on_command(&mut self, menu: Handle<Menu>, item: Handle<menu::Command>, command: mywin::Commands) {
        // this is called whenever a Command menu 
        // item is being cliecked
    }

    fn on_check(&mut self, menu: Handle<Menu>, item: Handle<menu::CheckBox>, command: mywin::Commands, checked: bool) {
        // this is called whenever a CheckBox menu 
        // item is being cliecked

    }

    fn on_select(&mut self, menu: Handle<Menu>, item: Handle<menu::SingleChoice>, command: mywin::Commands) {
        // this is called whenever a SingleChoice menu 
        // item is being cliecked

    }

    fn on_update_menubar(&self, menubar: &mut MenuBar) {
        // this is called whenever the menu bar
        // needs to be update. This is where
        // registered menus can be add to the 
        // desktop menu bar.
    }
}

Methods

The following methods are available for every Menu object

MethodPurpose
add(...)Adds a new menu item to the existing menu and returns a Handle for it
get(...)Returns an immutable reference to a menu item
get_mut(...)Returns a mutable reference to a menu item

Besides this the following methods are available in each control and allow menu manipulation.

Menu Items

Each menu is form out of menu items. AppCUI supports the following menu items:

  • Command : a command (clicking this item will send a command)
  • CheckBox : a item that has two states (checked or unchecked). Clicking on this item will change the state (from checked to unchecked and vice-versa) and will send a command.
  • SingleChoice : a item that is part of a group of items from which only one can be selected at a moment of time. Clicking on this item will select it (an implicelely unselect any other selected SingleChoice item from the group) and send a command.
  • SubMenu : a item that contains another menu. Clicking on this item will open the sub-menu.
  • Separator: a item that has no input and is represented by a horizontal line that separates groups or commands withing a menu.

Macro

All menu items can be build via menuitem! macro. The following attributes are can be used:

AttributeCommandCheckBoxSingleChoiceSub-MenuSeparator
captionYesYesYesYesYes
shortcutYesYesYes
commandYesYesYes
checkedYes
selectedYes
itemsYes
enabledYes (opt)Yes (opt)Yes (opt)Yes (opt)
typeYes (opt)Yes (opt)Yes (opt)Yes (opt)Yes (opt)
classYes (opt)Yes (opt)Yes (opt)Yes (opt)Yes (opt)

The type attribute is not optional. If not present , the type of the menu item is determine as follows:

  • a menuitem with only one attribute of type caption that consists only in multiple characters - will be consider a Separator
  • a menuitem that has the attribute checked will be considered a CheckBox
  • a menuitem that has the attribute selected will be considered a SingleChoice
  • a menuitem that has the attribute items wil be considered a SubMenu
  • otherwise, the menuitem will be considered of type Command

Similarly, the attribute class can be used to simplify the command value. Typically, the command attribute must include a format that resambles the following form:

"command='<module-name>::Command::<Command-ID>'"

where <module-name> is the lowercase name of the struct that registers the menu. To simplify the previous form, one can use the following:

"command=<Command-ID>, class=<class-name>"

For example - assuming we have the following window:

#![allow(unused)]
fn main() {
#[Window(... commands=Save+Open+New)]
struct MyWindow { /* data members */ }
}

then we can define a menu item for command Save in one of the following ways:

#![allow(unused)]
fn main() {
let item = menuitem!("... command='mywindow::Commands::Save'");
}

or

#![allow(unused)]
fn main() {
let item = menuitem!("... command=Save, class=MyWindow");
}

It is also important to notice that class attribute will be inherit (meaning that if you specify it for a menu item that hase sub menus, the sub menu items will inherit it and as such you don't have to add it to their definition).

Command (menu item)

A command menu item is an equivalent of a button but for menus.

You can create it using either menu::Command::new(...) method or via the menuitem! macro.

let cmd = menu::Command::new("Content", Key::new(KeyCode::F1,KeyModifier::None), <module>::Command::Content);

or

let cmd = menu::Command::new("Content", key!("F1"), <module>::Command::Content);

or

let cmd = menuitem!("Content,F1,'<module>::Command::Content'");

or

let cmd = menuitem!("Content,F1,cmd:Content,class:<class-name>");

Macro build

The following parameters are accepted by menuitem! when building a command menu item:

Parameter nameTypePositional parameterPurpose
text or captionStringYes (first postional parameter)The caption (text) of the command. If the caption contains the special character & the next character after that will act as a short key (meaning that pressing that character while that menu is opened is equivalent to clicking on that item)
key or shortcut or shortcutketStringYes (second positional parameter)The shortcut associated with the command. If not specified it will be considered Key::None
cmd or cmd-id or command or command-idStringYes (third positional parameter)The associated command id for this item
typeStringNoThe type of the item (for a command item if this filed is being specified its value must be command)
classStringNoThe name of the class where the menu is being implemented
enable or enabledBoolNoUse this to disable or enable a menu item

Events

To intercept events this item, the following trait and method have to be implemented to the Window that processes the event loop:

trait MenuEvents {
    fn on_command(&mut self, menu: Handle<Menu>, item: Handle<menu::Command>, command: <module>::Commands) {
        // add logic here
    }
}

Methods

The following methods are availble for a menu::Command object:

MethodPurpose
set_caption(...)Set the new caption for the item. If the string provided contains the special character &, this method also sets the hotkey associated with an item. If the string provided does not contain the & character, this method will clear the current hotkey (if any).
caption()Returns the current caption of an item
set_enables(...)Enables or disables current item
is_enabled()true if the item is enables, false otherwise
set_shortcut(...)Sets a new shortcut for the current item
shortcut()Returns the shortcut for the current item

Example

The following code creates a menu with 3 menu items (of type command). Notice that we had to initialize the application with support for menus.

use appcui::prelude::*;

#[Window(events = MenuEvents, commands=Cmd1+Cmd2+Cmd3)]
struct MyWin {
    m_commands: Handle<Menu>,
}
impl MyWin {
    fn new() -> Self {
        let mut w = MyWin {
            base: window!("Test,d:c,w:40,h:8"),
            m_commands: Handle::None,
        };
        let mut m = Menu::new("Commands");
        m.add(menu::Command::new("Command-1", Key::None, mywin::Commands::Cmd1));
        m.add(menu::Command::new("Command-2", Key::None, mywin::Commands::Cmd2));
        m.add(menuitem!("Command-3,F1,cmd:Cmd3,class:MyWin"));
        w.m_commands = w.register_menu(m);

        w
    }
}
impl MenuEvents for MyWin {

    fn on_command(&mut self, menu: Handle<Menu>, item: Handle<menu::Command>, command: mywin::Commands) {
        match command {
            mywin::Commands::Cmd1 => { /* do something with command 1 */ },
            mywin::Commands::Cmd2 => { /* do something with command 2 */ },
            mywin::Commands::Cmd3 => { /* do something with command 3 */ },
        }
    }
    fn on_update_menubar(&self, menubar: &mut MenuBar) {
        menubar.add(self.m_commands);
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().menu_bar().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

Checkbox (menu item)

A checkbox menu item is an equivalent of a checkbo but for menus.

You can create it using either menu::CheckBox::new(...) method or via the menuitem! macro.

let cbox = menu::CheckBox::new("Option", Key::new(KeyCode::F1,KeyModifier::None), <module>::Command::Content, true);

or

let cbox = menu::CheckBox::new("Option", key!("F1"), <module>::Command::Content, false);

or

let cbox = menuitem!("Option,F1,'<module>::Command::Content',type:Checkbox");

or

let cbox = menuitem!("Option,F1,cmd:Content,class:<class-name>,checked:true");

Macro build

The following parameters are accepted by menuitem! when building a checkbox menu item:

Parameter nameTypePositional parameterPurpose
text or captionStringYes (first postional parameter)The caption (text) of the checkbox. If the caption contains the special character & the next character after that will act as a short key (meaning that pressing that character while that menu is opened is equivalent to clicking on that item)
key or shortcut or shortcutketStringYes (second positional parameter)The shortcut associated with the checkbox. If not specified it will be considered Key::None
cmd or cmd-id or command or command-idStringYes (third positional parameter)The associated command id for this item
check or checkedBoolNotrue if the item is checked, false otherwise
typeStringNoThe type of the item (for a checbox item if this filed is being specified its value must be checkbox)
classStringNoThe name of the class where the menu is being implemented
enable or enabledBoolNoUse this to disable or enable a menu item

Events

To intercept events this item, the following trait and method have to be implemented to the Window that processes the event loop:

trait MenuEvents {
    fn on_check(&mut self, menu: Handle<Menu>, item: Handle<menu::CheckBox>, command: <module>::Commands, checked: bool) {
        // this is called whenever a CheckBox menu 
        // item is being cliecked

    }
}

Methods

The following methods are availble for a menu::CheckBox object:

MethodPurpose
set_caption(...)Set the new caption for the item. If the string provided contains the special character &, this method also sets the hotkey associated with an item. If the string provided does not contain the & character, this method will clear the current hotkey (if any).
caption()Returns the current caption of an item
set_checked(...)Checkes or uncheckes current ite,
is_checked()true if the item is checked, false otherwise
set_enables(...)Enables or disables current item
is_enabled()true if the item is enables, false otherwise
set_shortcut(...)Sets a new shortcut for the current item
shortcut()Returns the shortcut for the current item

Example

The following code creates a menu with 3 menu items (of type checkbox). Notice that we had to initialize the application with support for menus.

use appcui::prelude::*;

#[Window(events = MenuEvents, commands=Cmd1+Cmd2+Cmd3)]
struct MyWin {
    m_commands: Handle<Menu>,
}
impl MyWin {
    fn new() -> Self {
        let mut w = MyWin {
            base: window!("Test,d:c,w:40,h:8"),
            m_commands: Handle::None,
        };
        let mut m = Menu::new("Checkboxes");
        m.add(menu::CheckBox::new("&Option-1", Key::None, mywin::Commands::Cmd1,true));
        m.add(menu::CheckBox::new("Option-2", Key::None, mywin::Commands::Cmd2,false));
        m.add(menuitem!("Option-3,F1,cmd:Cmd3,class:MyWin,checked:true"));
        w.m_commands = w.register_menu(m);

        w
    }
}
impl MenuEvents for MyWin {

    fn on_check(&mut self,menu:Handle<Menu>,item:Handle<menu::CheckBox>,command:mywin::Commands,checked:bool) {
        match command {
            mywin::Commands::Cmd1 => { /* do something with option 1 */ },
            mywin::Commands::Cmd2 => { /* do something with option 2 */ },
            mywin::Commands::Cmd3 => { /* do something with option 3 */ },
        }
    }
    fn on_update_menubar(&self, menubar: &mut MenuBar) {
        menubar.add(self.m_commands);
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().menu_bar().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

Single Choice (menu item)

A checkbox menu item is an equivalent of a checkbo but for menus.

You can create it using either menu::SingleChoice::new(...) method or via the menuitem! macro.

let sc = menu::SingleChoice::new("Choice", Key::new(KeyCode::F1,KeyModifier::None), <module>::Command::Content);

or

let sc = menu::SingleChoice::new("Choice", key!("F1"), <module>::Command::Content);

or

let sc = menuitem!("Choice,F1,'<module>::Command::Content',type:SinghleChoice");

or

let sc = menuitem!("Choice,F1,cmd:Content,class:<class-name>,selected:true");

Macro build

The following parameters are accepted by menuitem! when building a checkbox menu item:

Parameter nameTypePositional parameterPurpose
text or captionStringYes (first postional parameter)The caption (text) of the single choice item. If the caption contains the special character & the next character after that will act as a short key (meaning that pressing that character while that menu is opened is equivalent to clicking on that item)
key or shortcut or shortcutketStringYes (second positional parameter)The shortcut associated with the single choice item. If not specified it will be considered Key::None
cmd or cmd-id or command or command-idStringYes (third positional parameter)The associated command id for this item
select or selectedBoolNotrue if the choice is the selected one, false otherwise
typeStringNoThe type of the item (for a single choice item if this filed is being specified its value must be singlechoice)
classStringNoThe name of the class where the menu is being implemented
enable or enabledBoolNoUse this to disable or enable a menu item

Events

To intercept events this item, the following trait and method have to be implemented to the Window that processes the event loop:

trait MenuEvents {
    fn on_select(&mut self, menu: Handle<Menu>, item: Handle<menu::CheckBox>, command: <module>::Commands) {
        // this is whenever a single choice item is selected
    }
}

Methods

The following methods are availble for a menu::SingleChoice object:

MethodPurpose
set_caption(...)Set the new caption for the item. If the string provided contains the special character &, this method also sets the hotkey associated with an item. If the string provided does not contain the & character, this method will clear the current hotkey (if any).
caption()Returns the current caption of an item
set_checked(...)Checkes or uncheckes current ite,
is_selected()true if the item is checked, false otherwise
set_selected()Selects the current item
is_enabled()true if the item is enables, false otherwise
set_shortcut(...)Sets a new shortcut for the current item
shortcut()Returns the shortcut for the current item

Groups

All single choice items are implicetely gouped based on their index. A consequitive set of single choice items forms a group. Whenever a single choice item is selected, the rest of the items from the group will be unselected.

Let's consider the following example (menu):

Single choice A
Single choice B
Single choice C
---------------
Single choice D
Single choice E
Single choice F

This menu has 7 items (the first three are of type single choice, then we have a separator and then another three single choice items). As such, 2 groups will be create:

  • First group - created out of single choice items A, B and C
  • Second group - created out of single choice items D, E and F

Whenever an item from the first group is being selected, the rest of the items will be unselected (ex: if we slect item F, then item D and item E will be unselected by default).

Example

The following code creates a menu with 3 menu items (of type checkbox). Notice that we had to initialize the application with support for menus.

use appcui::prelude::*;

#[Window(events = MenuEvents, commands=Cmd1+Cmd2+Cmd3)]
struct MyWin {
    m_commands: Handle<Menu>,
}
impl MyWin {
    fn new() -> Self {
        let mut w = MyWin {
            base: window!("Test,d:c,w:40,h:8"),
            m_commands: Handle::None,
        };
        let mut m = Menu::new("Single choices");
        m.add(menu::SingleChoice::new("Choice &A", Key::None, mywin::Commands::Cmd1,false));
        m.add(menu::SingleChoice::new("Choice &B", Key::None, mywin::Commands::Cmd2,false));
        m.add(menuitem!("'Choice &C',F1,cmd:Cmd3,class:MyWin,selected:true"));
        w.m_commands = w.register_menu(m);

        w
    }
}
impl MenuEvents for MyWin {

    fn on_select(&mut self,menu:Handle<Menu>,item:Handle<menu::SingleChoice>,command:mywin::Commands) {
        match command {
            mywin::Commands::Cmd1 => { /* do something with option 1 */ },
            mywin::Commands::Cmd2 => { /* do something with option 2 */ },
            mywin::Commands::Cmd3 => { /* do something with option 3 */ },
        }
    }
    fn on_update_menubar(&self, menubar: &mut MenuBar) {
        menubar.add(self.m_commands);
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().menu_bar().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

Separator

A separator item is designed to provide a way to separate groups of menu items.

You can create it using either menu::Separator::new() method or via the menuitem! macro.

let sep = menu::Separator::new();

or

let sep = menuitem!("---");

or

let sep = menuitem!("type:separator");

Macro build

The following parameters are accepted by menuitem! when building a command menu item:

Parameter nameTypePositional parameterPurpose
typeStringNoThe type of the item (for a command item if this filed is being specified its value must be separator)

Events

There are no events associated with a separator.

Methods

There are no events associated with a separator.

Example

The following code creates a menu with multiple items separated between them.

use appcui::prelude::*;

#[Window(events = MenuEvents, commands=Cmd1+Cmd2+Cmd3)]
struct MyWin {
    m_commands: Handle<Menu>,
}
impl MyWin {
    fn new() -> Self {
        let mut w = MyWin {
            base: window!("Test,d:c,w:40,h:8"),
            m_commands: Handle::None,
        };
        let mut m = Menu::new("Separators");
        m.add(menu::Command::new("Fist command", Key::None, mywin::Commands::Cmd1));
        m.add(menu::Separator::new());
        m.add(menuitem!("'Choice &A',F1,cmd:Cmd3,class:MyWin,selected:true"));
        m.add(menuitem!("'Choice &B',F2,cmd:Cmd3,class:MyWin,selected:false"));
        m.add(menuitem!("'Choice &C',F3,cmd:Cmd3,class:MyWin,selected:false"));
        m.add(menuitem!("---"));
        m.add(menu::Command::new("Another command", Key::None, mywin::Commands::Cmd2));
        w.m_commands = w.register_menu(m);

        w
    }
}
impl MenuEvents for MyWin {
    fn on_update_menubar(&self, menubar: &mut MenuBar) {
        menubar.add(self.m_commands);
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().menu_bar().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

Sub Menus

A sub menu item is a container for another menu.

You can create it using either menu::SubMenu::new(...) method or via the menuitem! macro.

let cmd = menu::SubMenu::new(Menu::new(...));

or

let cmd = menuitem!("Content,items=[...]");

or

let cmd = menuitem!("Content,class:<class-name>,items=[...]");

Macro build

The following parameters are accepted by menuitem! when building a command menu item:

Parameter nameTypePositional parameterPurpose
text or captionStringYes (first postional parameter)The caption (text) of the submenu. If the caption contains the special character & the next character after that will act as a short key (meaning that pressing that character while that menu is opened is equivalent to clicking on that item)
typeStringNoThe type of the item (for a sub-menu item if this filed is being specified its value must be submenu)
classStringNoThe name of the class where the menu is being implemented
enable or enabledBoolNoUse this to disable or enable a menu item

Remarks: Using the class attribute in a sub-menu will trigger an inheritence of that attribute for all sub items and sub menus. Check out Build a menu with macros for more details.

Events

There are no command based events associated with a sub-menu. When clicked (or the Enter key is being pressed) the sub-menu will open and on_menu_open will be called (if needed to change the status of some of the sub-menu items):

trait MenuEvents {
    fn on_menu_open(&self, menu: &mut Menu) {
        // called whenever a menu is being opened
        // by AppCUI framework
        // This method can be use to change 
        // certain menu related aspects, such as
        // - enable/disable menu items
        // - add new items
    }

Methods

The following methods are availble for a menu::SubMenu object:

MethodPurpose
set_caption(...)Set the new caption for the item. If the string provided contains the special character &, this method also sets the hotkey associated with an item. If the string provided does not contain the & character, this method will clear the current hotkey (if any).
caption()Returns the current caption of an item
set_enables(...)Enables or disables current item
is_enabled()true if the item is enables, false otherwise

Example

The following code creates a menu with 3 menu items (of type command). Notice that we had to initialize the application with support for menus.

use appcui::prelude::*;

#[Window(events = MenuEvents, commands=Red+Green+Blue+Copy+Paste+Cut+PasteSpecial+Exit)]
struct MyWin {
    m_submenus: Handle<Menu>,
}
impl MyWin {
    fn new() -> Self {
        let mut w = MyWin {
            base: window!("Test,d:c,w:40,h:8"),
            m_submenus: Handle::None,
        };
        let mut m = Menu::new("Sub &Menus");
        let mut m_colors = Menu::new("Colors");
        m_colors.add(menuitem!("Red,selected:true,cmd:Red,class:MyWin"));
        m_colors.add(menuitem!("Green,selected:true,cmd:Green,class:MyWin"));
        m_colors.add(menuitem!("Blue,selected:true,cmd:Blue,class:MyWin"));
        m.add(menu::SubMenu::new(m_colors));

        let mut m_clipboard = Menu::new("&Clipboard");
        m_clipboard.add(menuitem!("Copy,Ctrl+C,cmd:Copy,class:MyWin"));
        m_clipboard.add(menuitem!("Paste,Ctrl+V,cmd:Paste,class:MyWin"));
        m_clipboard.add(menuitem!("Cut,Ctrl+X,cmd:Cut,class:MyWin"));
        m_clipboard.add(menuitem!("---"));
        m_clipboard.add(menuitem!("'Paste Special',None,cmd:PasteSpecial,class:MyWin"));
        m.add(menu::SubMenu::new(m_clipboard));

        m.add(menuitem!("---"));
        m.add(menu::Command::new("Exit", Key::None, mywin::Commands::Exit));
        w.m_submenus = w.register_menu(m);

        w
    }
}
impl MenuEvents for MyWin {
    fn on_update_menubar(&self, menubar: &mut MenuBar) {
        menubar.add(self.m_submenus);
    }
    fn on_command(&mut self, menu: Handle<Menu>, item: Handle<menu::Command>, command: mywin::Commands) {
        match command {
            mywin::Commands::Copy => { /* Copy command was called */ }
            mywin::Commands::Paste => { /* Paster command was called */ },
            mywin::Commands::Cut => { /* Cut command was called */ },
            mywin::Commands::PasteSpecial => { /* PasteSpecial command was called */ },
            mywin::Commands::Exit => { /* Exit command was called */ },
            _ => {}
        }
    }

    fn on_select(&mut self, menu: Handle<Menu>, item: Handle<menu::SingleChoice>, command: mywin::Commands) {
        match command {
            mywin::Commands::Red => { /* Red color was selected */ }
            mywin::Commands::Green => { /* Green color was selected */ }
            mywin::Commands::Blue => { /* Blue color was selected */ }
            _ => {}
        }
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().menu_bar().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

Build a menu with macros

Builing a menu is not a complicated task, but it envolves multiple operations over the menu items. Let's consider the folowing menu :

  • Menu name: Test
  • Items:
    • Colors -> a sub-menu that contains the following sub-items:
      • Red (a single choice sub-item)
      • Green (a single choice sub-item)
      • Blue (a single choice sub-item)
    • Clipboard -> another sub-menu that contains the following sub-items:
      • Copy (a command, with Ctrl+C shortcut associated )
      • Cut (a command, with Ctrl+X shortcut associated )
      • Paste ( a command with Ctrl+V shortcut associated )
      • a separator
      • Paste Special (also a command, with no shortcut associated )
    • a separator
    • Exit (a command with no shortcut associated)

We will also considered that the following commands were added via the command attribute:

#[Window(... commands=Red+Green+Blue+Copy+Paste+Cut+PasteSpecial+Exit)]
struct MyWindow { /* data memebers */ }

Let's see several ways this menu can be created.

Build this menu without any macros

let mut m = Menu::new("Test");
// build the color submenu
let mut m_colors = Menu::new("Colors");
m_colors.add(menu::SingleChoice::new("Red",
                                     Key::None,
                                     mywin::Commands::Red, 
                                     true));
m_colors.add(menu::SingleChoice::new("Green",
                                     Key::None,
                                     mywin::Commands::Green, 
                                     true));
m_colors.add(menu::SingleChoice::new("Blue",
                                     Key::None,
                                     mywin::Commands::Blue, 
                                     true));
m.add(menu::SubMenu::new(m_colors));

// build the clipboard submenu
let mut m_clipboard = Menu::new("&Clipboard");
m_clipboard.add(menu::Command::new("Copy",
                                   Key::new(KeyCode::C, KeyModifier::Ctrl),
                                   mywin::Commands::Copy));
m_clipboard.add(menu::Command::new("Cut",
                                   Key::new(KeyCode::X, KeyModifier::Ctrl),
                                   mywin::Commands::Cut));
m_clipboard.add(menu::Command::new("Paste",
                                   Key::new(KeyCode::V, KeyModifier::Ctrl),
                                   mywin::Commands::Paste));
m_clipboard.add(menu::Separator::new());
m_clipboard.add(menu::Command::new("Paste Special",
                                   Key::None,
                                   mywin::Commands::PasteSpecial));
m.add(menu::SubMenu::new(m_clipboard));

// add the last items
m.add(menu::Separator::new());
m.add(menu::Command::new("Exit", 
                         Key::None, 
                         mywin::Commands::Exit));

Notice that the code is correct but is quite bloated and hard to read.

Build this menu using menuitem! macro

let mut m = Menu::new("Test");
// build the color submenu
let mut m_colors = Menu::new("Colors");
m_colors.add(menuitem!("Red,selected:true,cmd:Red,class:MyWin"));
m_colors.add(menuitem!("Green,selected:true,cmd:Green,class:MyWin"));
m_colors.add(menuitem!("Blue,selected:true,cmd:Blue,class:MyWin"));
m.add(menu::SubMenu::new(m_colors));

// build the clipboard submenu
let mut m_clipboard = Menu::new("&Clipboard");
m_clipboard.add(menuitem!("Copy,Ctrl+C,cmd:Copy,class:MyWin"));
m_clipboard.add(menuitem!("Cut,Ctrl+X,cmd:Cut,class:MyWin"));
m_clipboard.add(menuitem!("Paste,Ctrl+V,cmd:Paste,class:MyWin"));
m_clipboard.add(menuitem!("---"));
m_clipboard.add(menuitem!("'Paste Special',None,cmd:PasteSpecial,class:MyWin"));
m.add(menu::SubMenu::new(m_clipboard));

// add the last items
m.add(menuitem!("---"));
m.add(menuitem!("Exit,cmd:Exit,class:MyWin"));

The code is more readable, but we can make it even more smaller.

Building a menu using the menu! macro

In this case we will use the menu! macro to condense the code even more:

let m = menu!("Test,items=[
    { Colors,items=[
        { Red,selected:true,cmd:Red,class:MyWin },
        { Green,selected:true,cmd:Green,class:MyWin },
        { Blue,selected:true,cmd:Blue,class:MyWin }
    ]},
    { &Clipboard,items=[
        { Copy,Ctrl+C,cmd:Copy,class:MyWin },
        { Cut,Ctrl+X,cmd:Cut,class:MyWin },
        { Paste,Ctrl+V,cmd:Paste,class:MyWin },
        { --- },
        { 'Paste Special',None,cmd:PasteSpecial,class:MyWin }
    ]},
    { --- },
    { Exit,cmd:Exit,class:MyWin }
]");

Notice that in this case, the description of a menu item looks is more condense (and easier to read) and it looks like a JSON files.

However, there are still some duplicate data in this form (for example: attribute class with value MyWin is present for each of the actionable items). In this case we can use the inherit properties of a menu, an specify this item only once and reduce the code even more by adding the class attribute to the top level menu description and we get the most compressed way of quickly creating a menu.

let m = menu!("Test,class:MyWin,items=[
    { Colors,items=[
        { Red,selected:true,cmd:Red },
        { Green,selected:true,cmd:Green },
        { Blue,selected:true,cmd:Blue }
    ]},
    { &Clipboard,items=[
        { Copy,Ctrl+C,cmd:Copy },
        { Cut,Ctrl+X,cmd:Cut },
        { Paste,Ctrl+V,cmd:Paste },
        { --- },
        { 'Paste Special',None,cmd:PasteSpecial }
    ]},
    { --- },
    { Exit,cmd:Exit }
]");

Remarks: Keep in mind that this method will not allow you obtain any menu item handle. If they are neccesary to change some attributes (like enable/disable status) you will not be able to do so. However, if your menu only has commands, or checboxes and assigning a command is enough for you to react to an event, this is the prefered way to create a menu.

Menu Bar

A menu bar is a bar (on the top part of a desktop and on top of every window) that contains all menus associated with an application.

The menu bar is unique per application. This means, that you need to enable it when a new application is created. A tipical way to do this is by using .menu_bar() method when building an application, like in the following snippet:

#![allow(unused)]
fn main() {
let mut app = App.App::new().menu_bar().build()?;
}

Once you enabled the menu bar, you will need to implement MenuEvents on your window or custom control, and you will also need to add a list of commands when you create your window and/or custom control. A typical template of these flows look like this:

#![allow(unused)]
fn main() {
#[Window(events = MenuEvents, commands=[Command_1, Command_2 ... Command_n])]
struct MyWin { /* data member */ }
impl MyWin { /* internal methods */ }
impl MenuEvents for MyWin {
    // other event related methods

    fn on_update_menubar(&self, menubar: &mut MenuBar) {
        // this is called whenever the menu bar
        // needs to be update. This is where
        // registered menus can be add to the 
        // desktop menu bar.
    }
}
}

More details on the MenuEvents trait can be found on Menu chapter.

Its also important to note that on_update_menubar is being called only if the current focus (or one of its children) has focus. This implies that except for the case where a modal window is opened, this method will always be called for the desktop object.

Whenever the focus changes, the menu bar is cleared and the method on_update_menubar is being recall for each control from the focused one to its oldest ancestor (in most cases, the desktop).

You can always request an update to the command bar if by calling the method .request_update() that every control should have. This method will force AppCUI to recall on_update_menubar from the focused control to its oldest ancestor. Keep in mind that this command will not neccesarely call the on_update_menubar for the control that calls request_update , unless that control has the focus.

Usage

A menu bar has only one method:

pub fn add(&mut self, handle: Handle<Menu>) { ... }

This method is typically used to link a menu handle to a menu bar. This also implies that you have to register a menu first, save its handle and only then add it to the menu bar. A typical template of these flows look like this:

#![allow(unused)]
fn main() {
#[Window(events = MenuEvents, ...)]
struct MyWin { 
    menu_handle_1: Handle<Menu>,
    menu_handle_2: Handle<Menu>,
    // other menu handles or data members
 }
impl MyWin { 
    fn new()->Self {
        let mut w = MyWin { /* code to instantiate the structure */ };
        w.menu_handle_1 = w.register_menu(Menu::new(...)); 
        w.menu_handle_2 = w.register_menu(Menu::new(...)); 
        // other initialization methods
        w
    }

 }
impl MenuEvents for MyWin {
    fn on_update_menubar(&self, menubar: &mut MenuBar) {
        menubar.add(self.menu_1); // add first menu to the menu bar
        menubar.add(self.menu_2); // add the second menu to the menu bar
    }
}
}

All menus from the menu bar will be displayed in the order they were added.

Example

The following example shows a window that creates 3 menus: File, Edit and Help and adds them in this order to the menu bar.

use appcui::prelude::*;

#[Window(events   = MenuEvents, 
         commands = New+Save+Open+Exit+Copy+Paste+Delete+Cut+CheckUpdate+Help+About)]
struct MyWin {
    m_file: Handle<Menu>,
    m_edit: Handle<Menu>,
    m_help: Handle<Menu>,
}
impl MyWin {
    fn new() -> Self {
        let mut w = MyWin {
            base: window!("Test,d:c,w:40,h:8"),
            m_file: Handle::None,
            m_edit: Handle::None,
            m_help: Handle::None,
        };
        w.m_file = w.register_menu(menu!("&File,class:MyWin,items=[
            { &New,cmd:New },
            { &Save,F2,cmd:Save },
            { &Open,F3,cmd:Open },
            { --- },
            { Exit,cmd:Exit }
        ]"));
        w.m_edit = w.register_menu(menu!("&Edit,class:MyWin,items=[
            { &Copy,Ctrl+Ins,cmd:Copy },
            { &Paste,Shift+Ins,cmd:Paste },
            { &Delete,cmd:Delete },
            { C&ut,Ctrl+X,cmd: Cut}
        ]"));
        w.m_help = w.register_menu(menu!("&Help,class:MyWin,items=[
            { 'Check for updates ...', cmd: CheckUpdate },
            { 'Show online help', cmd: Help },
            { --- },
            { &About,cmd:About }
        ]"));

        w
    }
}
impl MenuEvents for MyWin {
    fn on_update_menubar(&self, menubar: &mut MenuBar) {
        menubar.add(self.m_file);
        menubar.add(self.m_edit);
        menubar.add(self.m_help);
    }
    fn on_command(&mut self, menu: Handle<Menu>, item: Handle<menu::Command>, command: mywin::Commands) {
        match command {
            mywin::Commands::New => todo!(),
            mywin::Commands::Save => todo!(),
            mywin::Commands::Open => todo!(),
            mywin::Commands::Exit => todo!(),
            mywin::Commands::Copy => todo!(),
            mywin::Commands::Paste => todo!(),
            mywin::Commands::Delete => todo!(),
            mywin::Commands::Cut => todo!(),
            mywin::Commands::CheckUpdate => todo!(),
            mywin::Commands::Help => todo!(),
            mywin::Commands::About => todo!(),
        }
    }
}
fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().command_bar().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

Popup Menu

A popup menu is a meniu that is display outside of a menubar (for example a menu that appears when right click is being pressed):

Remarks: There is no need to enable the mouse bar when creating for a popup menu.

There is no special control for a popup menu. A popup menu is a menu that is being displayed differently. Usually a popup menu is associated with either a window, a custom control or a custom desktop and it implies using the method show_menu available on all controls:

fn show_menu(&self, handle: Handle<Menu>, x: i32, y: i32, max_size: Option<Size>) {
    ...
}

where:

  • handle is a handle to a menu that was registered via register_menu(...) method
  • x and y are coordonates within the current control where the menu should be displayed. Keep in mind that by default AppCUI will try to position the menu to the bottom-right side of the provided coordonates. However, if the menu does not fit in the available space, it will try to position the menu in a different way so that it is visible on the screen.
  • max_size an Option that allows one to control the maximum size of a menu. By default, a menu will attemp to increase its width and height to show all items while being visible on the screen. This behavior can be overwritten by providing a maximum width and height (keep in mind that the maximum width has to be at least 5 characters - to have at least 3 items visible)

Example

The following example creates a custom control that can display a popup menu when the use right click the mouse on it:

use appcui::prelude::*;


#[CustomControl(events    : MenuEvents, 
                overwrite : OnPaint+OnMouseEvent,
                commands  : A+B+C)]
struct MyCustomControl {
    popup_menu: Handle<Menu>,
}
impl MyCustomControl {
    fn new() -> Self {
        let mut w = MyCustomControl {
            base: ControlBase::new(Layout::new("d:c,w:8,h:2"), true),
            popup_menu: Handle::None,
        };
        // construct a popup menu
        let m = menu!("Popup,class: MyCustomControl, items=[
            {Command-1,cmd:A},
            {Command-2,cmd:B},
            {-},
            {'Option A',checked:true, cmd:C},
            {'Option B',checked:true, cmd:C},
            {'Option C',checked:false, cmd:C},
            {'Option D',checked:false, cmd:C},
        ]");
        w.popup_menu = w.register_menu(m);

        w
    }
}
impl MenuEvents for MyCustomControl {

    fn on_command(&mut self, _menu: Handle<Menu>, _item: Handle<menu::Command>, command: mycustomcontrol::Commands) {
        match command {
            mycustomcontrol::Commands::A => { /* do something */ },
            mycustomcontrol::Commands::B => { /* do something */ },
            mycustomcontrol::Commands::C => { /* do something */ },
        }
    }
}
impl OnPaint for MyCustomControl {
    fn on_paint(&self, surface: &mut Surface, _theme: &Theme) {
        surface.clear(char!("' ',White,DarkRed"));
    }
}
impl OnMouseEvent for MyCustomControl {
    fn on_mouse_event(&mut self, event: &MouseEvent) -> EventProcessStatus {
        // if the event is a mouse click
        if let MouseEvent::Pressed(ev) = event {
            // if the button is the right one
            if ev.button == MouseButton::Right {
                // show the popup menu at mouse coordinates
                self.show_menu(self.popup_menu, ev.x, ev.y, None);
            }
        }
        EventProcessStatus::Ignored
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    let mut w = window!("Title,d:c,w:40,h:10");
    w.add(MyCustomControl::new());
    a.add_window(w);
    a.run();
    Ok(())
}

Command Bar

A command bar is a bar (on the bottom part of a desktop and on top of every window) that contains key associations with commands. All associations are checked first - meaning that if you associate the key F1 with a command, when you press F1 you will not receive the key event, but the command associated with it.

The command bar is unique per application. This means, that you need to enable it when a new application is created. A tipical way to do this is by using .command_bar() method when building an application, like in the following snippet:

#![allow(unused)]
fn main() {
let mut app = App.App::new().command_bar().build()?;
}

Once you enabled the command bar, you will need to implement CommandBarEvents on your window or custom control, and you will also need to add a list of commands when you create your window and/or custom control. A tipical template of these flows look like this:

#![allow(unused)]
fn main() {
#[Window(events = CommandBarEvents, commands=[Command_1, Command_2 ... Command_n])]
struct MyWin { /* data member */ }
impl MyWin { /* internal methods */ }
impl CommandBarEvents for MyWin {
    fn on_update_commandbar(&self, commandbar: &mut CommandBar) {
        // this is where you add associations (key - command)
        // this can be done via `commandbar.set(...)` method
    }

    fn on_event(&mut self, command_id: mywin::Commands) {
        // this method is called whenever a key from the associated list is being pressed
    }
}
}

Its also important to note that on_update_commandbar is being called only if the current focus (or one of its children) has focus. This implies that except for the case where a modal window is opened, this method will always be called for the desktop object.

Whenever the focus changes, the command bar is clear and the method on_update_commandbar is being recall for each control from the focused one to its oldest ancestor (in most cases, the desktop).

You can always request an update to the command bar if by calling the method .request_update() that every control should have. This method will force AppCUI to recall on_update_commandbar from the focused control to its oldest ancestor. Keep in mind that this command will not neccesarely call the on_update_commandbar for the control that calls request_update , unless that control has the focus.

All of the command that you add via the commands attribute, will be automatically added in a module (with the same name as you window or control, but lowercased) under the enum Commands.

Example

The following example shows a window that associates three keys: F1, F2 and F3 to some commands:

use appcui::prelude::*;

#[Window(events = CommandBarEvents, commands=[Help, Save, Load])]
struct MyWin { }
impl MyWin {
    fn new() -> Self {
        Self {
            base: window!("Win,x:1,y:1,w:20,h:7"),
        }
    }
}
impl CommandBarEvents for MyWin {
    fn on_update_commandbar(&self, commandbar: &mut CommandBar) {
        commandbar.set(key!("F1"), "Help", mywin::Commands::Help);
        commandbar.set(key!("F2"), "Save", mywin::Commands::Save);
        commandbar.set(key!("F3"), "Load", mywin::Commands::Load);
    }

    fn on_event(&mut self, command_id: mywin::Commands) {
        match command_id {
            mywin::Commands::Help => {
                // show a help
            },
            mywin::Commands::Save => {
                // save current data
            },
            mywin::Commands::Load => {
                // load something
            },
        }
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().command_bar().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

Dialogs

Dialogs are a set of predefined modal windows that are common when using an UI system, such as:

  • notification (dialogs that show an error or a message)
  • save/open (dialogs that allows you to select the name of a file(s) that will be saved or opened)
  • folder selection (a dialog that allows you to select a folder)
  • window management (a dialog that provides window management through out the entire AppCUI system)

All dialogs are available via appcui::dialogs module.

Notification Dialogs

Notification dialogs are predefined modal window that can be used for various purposes such as:

  • show an error or a warning
  • provide a validation (where you need to acknoledge a certain action)
  • show a message
  • etc

Errors

You can show an error by using the following method:

fn dialogs::error(title: &str, caption: &str) {...}

This will create a modal window with the message provided to this method and one button (that contains the caption Ok). The following code:

dialogs::error("Error","An error has occured during the last operation");

will produce the following error modal window:

Error dialogs are often use in scenarios where an error has occured and a specific action need to pe stop because of it. There are however cases where you will also want a retry option (if an error occurs, retry the same operation in the hope of another result). If this is the case, the following method can be used:

fn dialogs::retry(title: &str, caption: &str) -> bool {...}

This method will create an error dialog but with two buttons (Retry and Cancel). If you click on Retry button the method will return true otherwise it will return false. For example, the following code:

if dialogs::retry("Error","An error occured while performn a copy operation.\nRetry again ?") {
    // retry the operation
}

will create a dialog that looks like the following picture:

Alerts

Alerts are dialogs where an error has occured, but it is not that relevant for the program exection flow (its an error from where we can recover). You can show an error by using the following method:

fn dialogs::alert(title: &str, caption: &str) {...}

This will create a modal window with the message (the content of variable caption) provided to this method and one button (that contains the caption Ok). The following code:

dialogs::alert("Error","An error has occured during the last operation");

will produce the following error modal window:

Just like in the case of errors, if the alert is something we can ignore and continue with the execution, the following method can be used:

fn dialogs::proceed(title: &str, caption: &str) -> bool {...}

This method will create an alert dialog but with two buttons (Yes and No). If you click on Yes button the method will return true otherwise it will return false. For example, the following code:

if dialogs::proceed("Alert","An error occured while performn a copy operation.\nContinue anyway ?") {
    // retry the operation
}

will create a dialog that looks like the following picture:

Popup messages are notification of success or generic information that are provided. To show a simple message use the following method:

fn dialogs::message(title: &str, caption: &str) {...}

This will create a modal window with the message (the content of variable caption) provided to this method and one button (that contains the caption Ok). The following code:

dialogs::message("Success","All files have been copied");

will produce the following modal window:

Validation messages

Validation messages are simple questions that determine how the execution flow should continue from that point. To show a validation message use the following method:

fn dialogs::validate(title: &str, caption: &str) -> bool {...}

This method will create a dialog with two buttons (Yes and No). If you click on Yes button the method will return true otherwise it will return false. This is used to create a simple validation message such Are you sure you want to proceed ?.

For example, the following code:

if dialogs::validate("Question","Are you sure you want to proceed ?") {
    // start the action
}

will create a dialog that looks like the following picture:

Aditionally a validate_or_cancel method is also available with the following definition:

fn dialogs::validate_or_cancel(title: &str, caption: &str) -> ValidateOrCancelResult {...}

This method will display three button (Yes, No and Cancel). The result of this dialog are described by the followin enum:

#[derive(Copy,Clone,PartialEq,Eq)]
pub enum ValidateOrCancelResult {
    Yes,
    No,
    Cancel
}

This type of dialog should be used for scenarios where you can do one action in two ways or you can stop doing that action. For example, when an application ends and you need to save the date, you can chose between:

  • saving the data (and close the application)
  • not saving the date (and still close the application)

or

  • cancel (meaning that you will not close the application)

The following code describes a similar scenario:

let result = dialogs::validate_or_cancel("Exit","Do you want to save your files ?"); 
match result {
    ValidateOrCancelResult::Yes => { /* save files and then exist application */ },
    ValidateOrCancelResult::No => { /* exit the application directly */ },
    ValidateOrCancelResult::Cancel => { /* don't exit the application */ }
}

and should create a dialog that looks like the following picture:

Open/Save dialogs

Open/Save dialogs are predefined dialogs that allows you to select a file path that will further be used to save or load content from/into. The following methods are available:

  • dialogs::save
  • dialogs::open

Save dialog

A save dialog is usually used whenever your application needs to save some data into a file and you need to select the location where the file will be saved.

A save dialog can be open using the following method:

fn dialogs::save(title: &str, 
                 file_name: &str, 
                 location: Location,
                 extension_mask: Option<&str>,
                 flags: SaveFileDialogFlags) -> Option<PathBuf> 
{
    ...
}

where:

  • title - the title of the dialog (usually "Save" or "Save as")
  • file_name - the default file name that will be displayed in the dialog
  • location - the location / path where the dialog will be opened (see Location for more details)
  • extension_mask - a mask that will be used to filter the files that can be selected (see Extension mask for more details). If None all files will be shown.
  • flags - additional flags that can be used to customize the dialog

The SaveFileDialogsFlags is defined as follows:

#[EnumBitFlags(bits = 8)]
pub enum SaveFileDialogFlags {
    Icons = 1,
    ValidateOverwrite = 2,
}

where:

  • Icons - show icons for files and folders
  • ValidateOverwrite - if the file already exists, a validation message will be shown to confirm the overwrite

Open Dialog

An open dialog is usually used whenever your application needs to load some data from a file and you need to select the location of the file.

An open dialog can be open using the following method:

fn dialogs::open(title: &str, 
                 file_name: &str, 
                 location: Location,
                 extension_mask: Option<&str>,
                 flags: OpenFileDialogFlags) -> Option<PathBuf> 
{
    ...
}

where:

  • title - the title of the dialog (usually "Open" or "Load")
  • file_name - the default file name that will be displayed in the dialog
  • location - the location / path where the dialog will be opened (see Location for more details)
  • extension_mask - a mask that will be used to filter the files that can be selected (see Extension mask for more details). If None all files will be shown.
  • flags - additional flags that can be used to customize the dialog

The OpenFileDialogsFlags is defined as follows:

#[EnumBitFlags(bits = 8)]
pub enum OpenFileDialogFlags {
    Icons = 1,
    CheckIfFileExists = 2,
}

where:

  • Icons - show icons for files and folders
  • CheckIfFileExists - if the file does not exist, a error message will be shown abd the dialog will remain open

Location

Whenever a save or open dialog is opened, you can specify the location where the dialog will be opened. The following locations are available:

  • Location::Current - the dialog will be opened in the current directory
  • Location::Last - the dialog will be opened on the last location where a file was saved or opened. If no file was saved or opened, the dialog will be opened in the current directory
  • Location::Path(...) - the dialog will be opened in the specified path

Extension mask

The extension mask is a string that contains multiple items in the format display-name = [extensions lists] separated by commas that will serve as a filter for the files that can be selected.

For example, if we want to filter only the images, we will create a string that looks like the following:

"Images = [jpg, jpeg, png, bmp, gif]"

If we want to have multiple options for filtering, we can create multiple strings like the previous one and separte them by commas. For example, we we want to have three options for filtering: images, documents and executables, we will create a string like the following:

"Images = [jpg, jpeg, png, bmp, gif],
 Documents = [doc, pdf, docx, txt],
 Executables = [exe, bat, sh]
"

Remarks:

  1. AppCUI will ALWAYS add an All Files options at the end of your options list.
  2. The first item from the provided list will be the default mask when opening the dialog.

Example

The following example shows how to use the save dialog:

if let Some(file_path) = dialogs::save(
            "Save As",
            "abc.exe",
            dialogs::Location::Current,
            Some("Images = [jpg,png,bmp], 
                  Documents = [txt,docx], 
                  Executable and scripts = [exe,dll,js,py]"),
            dialogs::SaveFileDialogFlags::Icons |
            dialogs::SaveFileDialogFlags::ValidateOverwrite 
        ) 
{
    // do something with the file_path
}

Folder Selection Dialog

A folder selection dialog is usually used whenever your application needs to select a folder where some data will be saved or loaded from.

To open a folder selection dialog, use the following method:

fn dialogs::select_folder(title: &str, 
                          location: Location,
                          flags: SelectFolderDialogFlags) -> Option<PathBuf> 
{
    ...
}

where:

  • title - the title of the dialog (usually "Save" or "Save as")
  • location - the location / path where the dialog will be opened (see Location for more details)
  • flags - additional flags that can be used to customize the dialog

The SelectFolderDialogFlags is defined as follows:

#[EnumBitFlags(bits = 8)]
pub enum SaveFileDialogFlags {
    Icons = 1,
}

where:

  • Icons - show icons for folders and root drives

Location

Whenever a save or open dialog is opened, you can specify the location where the dialog will be opened. The following locations are available:

  • Location::Current - the dialog will be opened in the current directory
  • Location::Last - the dialog will be opened on the last location where a file was saved or opened. If no file was saved or opened, the dialog will be opened in the current directory
  • Location::Path(...) - the dialog will be opened in the specified path

Example

The following example shows how to open a folder selection dialog:

if let Some(folder_path) = dialogs::select_folder(
            "Select Folder",
            dialogs::Location::Current,
            dialogs::SaveFileDialogFlags::None 
        ) 
{
    // do something with the folder_path    
}

Themes

Themes are a way to change the look and feel of the application. A theme is a collection of colors, fonts, and other settings that are used to draw the controls. AppCUI comes with a set of predefined themes that can be used out of the box. You can also create your own themes by modifying the predefined ones or by creating a new one from scratch.

To create a new theme, you need to create a new instance of the Theme structure and set the desired colors, fonts, and other settings. You can then apply the theme to the application by using the .theme(...) method on the application builder.

#![allow(unused)]
fn main() {
let mut my_theme = Theme::new(Themes::Default);
// modify my_theme (colors, characters, etc)
let mut app = App::new().theme(my_theme).build().expect("Fail to create an AppCUI application");
// add aditional windows
app.run();
}

or by calling the set_theme method on the App structure later on during the execution.

#![allow(unused)]
fn main() {
let mut my_theme = Theme::new(Themes::Default);
// modify my_theme (colors, characters, etc)
App::set_theme(my_theme);
}

If not specified, the default theme (Themes::Default) will be used.

Predefined Themes

AppCUI comes with a set of predefined themes that can be used out of the box via the Themes enum. The following themes are available:

  1. Default - the default theme used by the library.

  2. DarkGray - a dark gray theme.

  3. Light - a light theme.

Events

Normally, the theme of the application is set up when the application is created. However, you can change the theme at any time during the execution of the application. While, the stock controls will automatically update their appearance when the theme is changed, custom controls may need to be notified about the change. This is in particular the case when a double buffer is used to draw the control (e.g. you control has an inner Surface object that is updated based on a different logic and that will further be used in the on_paint method).

In these scenarios, the control must implement OnThemeChanged trait in order to receive notification when the theme is updated.

#![allow(unused)]
fn main() {
impl OnThemeChanged for MyControl {
    fn on_theme_changed(&mut self, theme: &Theme) {
        // update your inner state using the current theme
    }
}
}

Remarks: The on_theme_changed method is called only when the theme is changed and ONLY after the Application has been started (i.e. after the run method has been called). This means that you will never get notified about the theme change for the initial theme set up when the application is created.

Example

Let's create a simple custom control that uses an attribute from the theme to draw itself. Such a control will requre to be notified when the theme is changed in order to update its data members.

#![allow(unused)]
fn main() {
#[CustomControl(overwrite : OnThemeChanged+OnPaint)]
struct MyControl {
    attr: CharAttribute,
}
impl MyControl {
    fn new() -> Self {
        let mut obj = Self {
            base: ControlBase::new(Layout::new("l:1,r:1,t:1,b:1"), true),
            attr: CharAttribute::default(),
        };
        // we set up  the attribute based on the current theme
        obj.attr = obj.theme().window.normal;
        obj
    }
}
impl OnPaint for MyControl {
    fn on_paint(&self, surface: &mut crate::prelude::Surface, _theme: &Theme) {
        // this is where we use the self.attr to draw something
        surface.clear(Character::with_attributes('X', self.attr));
    }
}
impl OnThemeChanged for MyControl {
    fn on_theme_changed(&mut self, theme: &Theme) {
        // this is where we update the attribute to match the new theme
        self.attr = theme.window.normal;
    }
}
}

Multi Threading

While AppCUI works on a single thread, the multi-threading support is available in some scenarios such as:

  • Timers
  • Background tasks

Remarks: Multi-threading support relies heavely on channels and the way terminals are implemented on the current operating system. This means that some features might not work as expected on some terminals (in particular if they are not adapted to work with multiple threads).

Timers

Timers are internal AppCUI mechanisms that allow each control to receive a signal at a specified interval. This signal can be used to update the control's content, to animate it, or to perform other actions.

Each timer has its own thread that sends a signal to the control at a specified interval. Because of this, the total number of timers that can be used in an application is limited. By default, an application can use up to 4 timers. This number can be increased by using the .timers_count(count) method from the App builder, but it can not be more than 255.

When a control is destroyes, if it has a timer associated with it, that timer will also be closed and the slot associated with it released.

To use a timer, you will need the following things:

  1. access the control timer via the .timer() method
  2. implement TimerEvents trait for the control to get notification when the timer signal is received

The timer events is define as follows:

#![allow(unused)]
fn main() {
pub trait TimerEvents {
    fn on_start(&mut self) -> EventProcessStatus {
        // called when the timer is started
        EventProcessStatus::Ignored
    }
    fn on_resume(&mut self, ticks: u64) -> EventProcessStatus {
        // called when the timer is resumed
        EventProcessStatus::Ignored
    }
    fn on_pause(&mut self, ticks: u64) -> EventProcessStatus {

        EventProcessStatus::Ignored
    }    
    fn on_update(&mut self, ticks: u64) -> EventProcessStatus {
        // called when the timer updates itself
        // (e.g. the interval of the timer has passed)
        EventProcessStatus::Ignored
    }
}
}

The ticks variable represents the number of times the timer has been triggered. Whenever the timer starts this variable is set to 0, and them it is incremented each time the timer is triggered.

Remarks: It is important to note that if you must return EventProcessStatus::Process from these methods if you want AppCUI framework to redraw itself (this is usually the case if you are updating a control context in the timer event).

Timer object

The timer object is created by calling the .timer() method from the control. This method returns an Option<&mut Timer> object that can be used to start, stop, pause, or resume the timer. If a slot (from the list of maximum number of timers) is present, the method will return a Some(&mut Timer) object. If no slot is available, the method will return None. Once a slot is available, the timer will be created (e.g. the slot will be occupied) but no threads will be created until the .start() method is called.

The following methods are available for a timer:

MethodDescription
start(...)Starts the timer with the specified interval and reset the internal tick count to 0. If this is the first time that timer is started, a thread will also be created (otherwise the existing thread associated with the timer will be used
pause()Pauses the timer. The timer thread will also be paused (but not terminated)
resume()Resumes the timer after it was paused
stop()Stops the timer. The timer thread will be terminated and the slot will be freed (meaning that other object can use that slot for its own timer)
set_interval(...)Sets the interval for the timer. If the timer is already started, the new interval is applied imediately. Otherwise, the new interval will be use the moment that timer is being started or resumed
is_paused()Returns true if the timer is paused, false otherwise
is_running()Returns true if the timer is started, false otherwise

Typical a timer is being used like this:

#![allow(unused)]
fn main() {
// assuming that we run in a control context (e.g. self refers to a control)
if let Some(timer) = self.timer() {
    timer.start(Duration::with_seconds(1)); // start the timer with 1 second interval
}
}

Example

The following example starts a 1 second timer that updates a label control with the current time:

use std::time::Duration;
use appcui::prelude::*;

#[Window(events = TimerEvents)]
struct MyWin {
    lb: Handle<Label>,
}
impl MyWin {
    fn new() -> Self {
        let mut w = Self {
            base: window!("'Timer Example', d:c, w:30, h:5"),
            lb: Handle::None,
        };
        w.lb = w.add(label!("'',x:1,y:1,w:28"));
        if let Some(timer) = w.timer() {
            timer.start(Duration::from_secs(1));
        }
        w
    }
}
impl TimerEvents for MyWin {
    fn on_update(&mut self, ticks: u64) -> EventProcessStatus {
        let text = format!("Ticks: {}", ticks);
        let h = self.lb;
        if let Some(lb) = self.control_mut(h) {
            lb.set_caption(&text);
        }
        // return EventProcessStatus::Process to repaint controls
        EventProcessStatus::Processed
    }
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}

Background Tasks

A background task is a thread that can comunicate with the main AppCUI threat and send/receive information from it, just like in the following diagram:

To start a background task, use the following command:

pub struct BackgroundTask<T,T>
where: 
    T: Send + 'static, 
    R: Send + 'static 
{ ... }

impl<T: Send, R: Send> BackgroundTask<T, R> {
    pub fn run(task: fn(conector: &BackgroundTaskConector<T, R>), 
               receiver: Handle<Window>) -> Handle<BackgroundTask<T, R>> 
    {...}
}

where:

  • T represents the type that will be send from the background thread to the main thread (the thread where AppCUI runs). It is usually an enum that reflects a status (e.g. precentage of the task) or a query (information needed by the background thread)
  • R represent the response for a query (meaning that if the background thread ask the main thread using type T, the reply will be of type R)

A BackgroundTask object has the following methods:

MethodDescription
read(...)Reads the last object of type T sent by the background thread to the main thread
send(...)Sends an object of type R from the main thread to the background thread
stop()Requests the background thread to stop
pause()Requests the background thread to pause
resume()Requests the background thread to resume
update_control_handle(...)Updates the handle of the control that will receive events from the background thread. This is usually required if we want to close the window that shows the background thread

A BackgroundTaskConector<T, R> is an object that can be used to send/receive information between the background thread and the main thread. It has the following methods:

MethodDescription
notify(...)Sends an object of type T from the background thread to the main thread
query(...)Sends an object of type T from the main thread to the background thread and waits until the main thread responds with an object of type R
should_stop()Returns true if the background thread should stop (meaning that the main thread requested the background thread to stop) or false otherwise. This method also handle Pause and Resume requests (meaning that if the main thread has requested a pause this method will wait until the main thread will request a resume or will request a stop of the background thread)

Events

The BackgroundTaskEvents event handler must be implementd on a window to receive events from a background task. It has the following methods:

trait BackgroundTaskEvents<T: Send+'static, R: Send+'static> {
    fn on_start(&mut self, task: &BackgroundTask<T,R>) -> EventProcessStatus {
        // Called when the background task is started
        EventProcessStatus::Ignored
    }
    fn on_update(&mut self, value: T, task: &BackgroundTask<T,R>) -> EventProcessStatus {
        // Called when the background task sends information to the main thread
        // via the BackgroundTaskConector::notify(...) method
        // if the return value is EventProcessStatus::Processed, the main thread will
        // repaint the window
        EventProcessStatus::Ignored
    }
    fn on_finish(&mut self, task: &BackgroundTask<T,R>) -> EventProcessStatus {
        // Called when the background task is finished
        EventProcessStatus::Ignored
    }
    fn on_query(&mut self, value: T, task: &BackgroundTask<T,R>) -> R;
    // Called when the background task sends a query to the main thread
    // via the BackgroundTaskConector::query(...) method
    // The main thread must return a value of type R
}

Flow

An usual flow for a background task is the following:

  1. The main thread starts a background task using the BackgroundTask::run(...) method
  2. The window that receives this background task events should overwrite the BackgroundTaskEvents<T,R> event handler
  3. The background thread will start and will send information to the main thread using the BackgroundTaskConector::notify(...) method
  4. The background thread should check from time to time if the main thread requested a stop or a pause (via the BackgroundTaskConector::should_stop() method)
  5. The main thread will be called via BackgroundTaskEvents event handler with the information sent by the background thread

Example

  1. Create the coresponding type T and R that will be send between the main thread and the background thread:

    enum TaskStatus {
        StartWithTotaltems(u64),
        Progress(u64),
        AskToContinue,
    }
    enum Response {
        Continue,
        Stop,
    }
    
  2. Create a window that will receive events from a background task:

    #[Window(events = BackgroundTaskEvents<TaskStatus,Response>)]
    struct MyWindow { ... }
    
  3. Create a function / lambda that will execute the background task:

    fn background_task(conector: &BackgroundTaskConector<TaskStatus,Response>) {
        // send a start message and notify about the total items
        // that need to be processed by the background thread
        let total_items = 100;
        conector.notify(TaskStatus::StartWithTotaltems(total_items));
    
        // start processing items
        for crt in 0..total_items {
            // update the progress status to the main thread
            conector.notify(TaskStatus::Progress(crt));
            // check to see if the main thread has requested a stop
            if conector.should_stop() {
                break;
            }
            // if needed ask the main thread if it should continue
            // this part is optional
            if crt % 10 == 0 {
                if conector.query(TaskStatus::AskToContinue) == Response::Stop {
                    break;
                }
            }
            // do the actual work
        }
    }
    
  4. Start the background task:

    let task = BackgroundTask::<TaskStatus,Response>::run(background_task, window.handle());
    
  5. Implement the BackgroundTaskEvents event handler for the window:

    impl BackgroundTaskEvents<TaskStatus, Response> for MyWindow {
        fn on_start(&mut self, task: &BackgroundTask<TaskStatus, Response>) -> EventProcessStatus {
            // called when the background task is started
            EventProcessStatus::Processed
        }
        fn on_update(&mut self, value: TaskStatus, task: &BackgroundTask<TaskStatus, Response>) -> EventProcessStatus {
            match value {
                // process the task status value
            }
            EventProcessStatus::Processed
        }
        fn on_finish(&mut self, task: &BackgroundTask<TaskStatus, Response>) -> EventProcessStatus {
            // called when the background task is finished
            EventProcessStatus::Processed
        }
        fn on_query(&mut self, value: TaskStatus, task: &BackgroundTask<Status, Response>) -> Response {
            // return a response to the query
            Response::Continue
        }
    }
    

Example

The following example shows how to create a window that will receive events from a background task and update a progress bar:

use appcui::prelude::*;

enum Status {
    Start(u32),
    Progress(u32),
}

#[Window(events = ButtonEvents+BackgroundTaskEvents<Status,bool>)]
struct MyWin {
    p: Handle<ProgressBar>,
}

impl MyWin {
    fn new() -> Self {
        let mut w = Self {
            base: window!("'Background Task',d:c,w:50,h:8,flags:sizeable"),
            p: Handle::None,
        };
        w.p = w.add(progressbar!("l:1,t:1,r:1,h:2"));
        w.add(button!("&Start,l:1,b:0,w:10"));
        w
    }
}
impl ButtonEvents for MyWin {
    fn on_pressed(&mut self, handle: Handle<Button>) -> EventProcessStatus {
        BackgroundTask::<Status, bool>::run(my_task, self.handle());
        EventProcessStatus::Processed
    }
}
impl BackgroundTaskEvents<Status, bool> for MyWin {
    fn on_update(&mut self, value: Status, _: &BackgroundTask<Status, bool>) -> EventProcessStatus {
        let h = self.p;
        if let Some(p) = self.control_mut(h) {
            match value {
                Status::Start(value) => p.reset(value as u64),
                Status::Progress(value) => p.update_progress(value as u64),
            }
            EventProcessStatus::Processed
        } else {
            EventProcessStatus::Ignored
        }
    }
    fn on_query(&mut self, _: Status, _: &BackgroundTask<Status, bool>) -> bool {
        true
    }
}

fn my_task(conector: &BackgroundTaskConector<Status, bool>) {
    conector.notify(Status::Start(100));
    for i in 0..100 {
        if conector.should_stop() {
            return;
        }
        std::thread::sleep(std::time::Duration::from_millis(100));
        conector.notify(Status::Progress(i));
    }
    conector.notify(Status::Progress(100));
}

fn main() -> Result<(), appcui::system::Error> {
    let mut a = App::new().build()?;
    a.add_window(MyWin::new());
    a.run();
    Ok(())
}