┌────────────────────────┐┌──────────────────────────┐
┌─┤ Unit Tests ││ Application │
│ └────────────────────────┘└───────────────────┬───┬──┘
Simulate │ ╔═══════════════════╗ ┌────────────────────┐ │ │
Events │ ║ tcw3::ui::views ║ │ Custom Widgets │ │ │
│ ║ ╔════════════╝ └────────────┐ │ │ Initialization
┌─│─╢ ║ ╔═══════════════════════╗ │ │ │ │
Drawing, │ │ ║ ║ ║ tcw3::ui::theming ║ │ │ │ │ Platform-
layers, │ │ ╚══════╝ ╚═══════════════════════╝ └───────┘ │ │ specific
etc. │ │ ╔═══════════════════════╗ ╔════════════════╗ │ │ operations
│ │ ║ tcw3::uicore ║ ║ tcw3::images ║ │ │ (e.g., creating
│ │ ╚═══════════════════════╝ ╚════════════════╝ ↓ │ a main menu on
│ │ ╔═══════════════════════════════════════════════╗ │ Cocoa)
└─│→║ tcw3::pal ║ │
│ ╟───────────────────────────────────────────────╢ │
└→║ testing (optional) ║ │
╟───────────────┬───────────────┬───────────┐ ║ │
Backends: ║ windows │ macos │ gtk │ ║ │
╚═══════════════╧═══════════════╧═══════════╧═══╝ ↓
┌────────────────────────────────────────────────────┐
│ Win32, WinRT, Cocoa, GTK, GLib, Pango, ... │
└────────────────────────────────────────────────────┘
tcw3::pal
abstracts the underlying window system and
graphics libraries. It defines the concept of main thread and a trait
Wm
for compile-time thread checking. Wm
also provides several entry
point functions for the clients to call. Several backends are provided.
During a build, one of them is chosen based on the target platform and given
feature flags, and its public types such as Wm
(type) and HWnd
are
reexported at the crate root. The clients are supposed to use these
reexported items.
The API surface of tcw3::pal
is carefully designed so that all backends
can implement the same interface, while ensuring the features of each target
platform are well utilized, the abstraction overhead is reasonably low, and
the engineering cost is acceptable.
For example, details of widget handling are vastly different between
platforms, so tcw3::pal
has no concept of widgets, and widgets are instead
realized by tcw3::uicore
. On the other hand, with regard to text input
handling, text input widgets are modeled as an abstract text storage and
the widgets don't have to deal with key strokes entered by the user.
tcw3::pal
has a headless backend named testing
, which is used for
testing various components built based on tcw3::pal
. testing
has a
dedicated interface to programmatically control the simulated window system,
which is exposed at tcw3::pal::testing
.
tcw3::images
provides a type HImg
, which represents a
scale-independent image that is rasterized on-demand. tcw3::ui::theming
uses this extensively to render non-rectangular shapes in widgets.
tcw3::images
also manages the list of DPI scaling factors currently in use
by the user's desktop. The cache of rasterized bitmaps are categorized by
DPI scaling factors, and bitmaps are automatically released when the
associated DPI scaling factor is no longer in use. To implement this cache
management policy, a signaling mechanism that notifies changes in the global
list of DPI scaling factors is needed, but tcw3::pal
doesn't provide
a facility for that.
It's tcw3::uicore
that tells tcw3::images
which DPI scaling factor is in
use and which one is no longer used. Other clients may do the same, but this
is usually unnecessary.
tcw3::uicore
is a widget toolkit built on top of the aforementioned
subsystems. It introduces the concept of views, which are nestable
rectangular regions inside a window that can receive user inputs and display
graphical contents. It provides a framework for laying out views using
layout objects. It's responsible for routing input events received by
windows to appropriate views.
uicore
is by no means meant to be a complete widget toolkit by itself. The
appearance of views is not defined at all by uicore
. Also, uicore
doesn't completely hide pal
, so TCW3 widgets and applications occasionally
have to use pal
directly.
tcw3::ui
is an assortment of libraries built on top of uicore
.
tcw3::ui::theming
is a styling framework that allows decoupling
between logic and style. theming
provides a view StyledBox
, which is
often used to define the appearance of widgets.
tcw3::ui::mixins
provides mix-ins, which handle input events
(through composition) to implement common GUI behaviors.
Other submodules provide useful premade views and layouts.
tcw3_designer
(Designer) is a declarative framework for writing
GUI components with less boilerplate code. It's meant to be invoked by a
build script (build.rs
). The generate code requires a runtime library
located in tcw3::designer_runtime
.
designer
stores the definition of components in meta crates. Build
scripts invoking designer
should import the definition of components they
use from meta crates. For the widgets defined by tcw3::ui
, the
meta crate is [tcw3_meta
].
TCW3 relies on the concept of main thread. A main thread is defined by the
possession of an instance of a non-Send
-able marker type tcw3::pal::Wm
.
A TCW3 application enters a main event loop by calling Wm::enter_main_loop
.
The event loop monitors for events sent by the target window system, processes
them, and calls application-provided event listeners (such as those in
tcw3::pal::iface::WndListener
) as needed.
You can use Wm::invoke
and similar methods to have a custom closure called
inside the main event loop. The following list summarizes the methods in this
category:
-
Wm::invoke
enqueues a closure to the event queue. This is a low-level method thatuicore
relies on, and application developers should preferuicore::WmExt::invoke_on_update
over this. -
Wm::invoke_on_main_thread
is similar to above, but can be called by any thread. -
Wm::invoke_after
enqueues a closure to be called after a given delay. -
uicore::WmExt::invoke_on_update
is similar toWm::invoke
, but ensures the closure is called beforeuicore
updates window contents. -
uicore::HWnd::invoke_on_next_frame
is similar toWm::invoke
, but calls are synchronized to the refresh rate of the display where the givenHWnd
is currently located. You should use this method to schedule screen updates for animation.
Wm::terminate
instructs the system to stop the main event loop, process all
remaining events, and exit the application.
Follow these steps to create a bitmap:
-
Use
tcw3::pal::BitmapBuilder
::new
to start constructing a bitmap. -
BitmapBuilder
implements the traittcw3::pal::iface::Canvas
. Use the methods from this trait to issue 2D drawing commands. -
Finally, call
tcw3::pal::BitmapBuilder::into_bitmap
to convert theBitmapBuilder
into an immutabletcw3::pal::Bitmap
.
To be filled
To be filled
To be filled
To be filled
To be filled
To be filled
A view hierarchy and a styling element tree are independent from each other.
However, there are some points at which there is a one-by-one relationship
between them. Such points are useful for connecting widgets and thus represented
by the trait Widget
. This trait provides two methods each returning the
root node of the corresponding type of subtree. The client of this trait can use
them to embed the widget in outer trees. For example, Split
has a method
named set_children
that receives two values of &dyn Widget
and puts them
on the respective sides of a splitter. Split
itself implements Widget
, so
the user of Split
can easily place a Split
inside something that exposes a
method equivalent to set_children
.
The following diagram illustrates this model.
view hierarchy styling tree
window
┌ ─ ─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ A_view A_elem ├ Widget A
└─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─│─ ─┘
┌──┴──┐ │
view view │
│ │
─│─ ─ ─ ─ ─ ─ ─│─ ─ Widget A may expose this "socket",
│ │ to which Widget B can be plugged in
│ │
┌─ ─ ┼ ─ ─ ─ ─ ─ ─ ┼ ─ ┐
│ B_view B_elem ├ Widget B
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┘
Some styling elements (only StyledBox
at the moment, actually) support
controlling the arrangement of their subviews through the styling framework.
Subviews are modeled by the styling framework as follows: Each styling element
is associated with a set of pairs of type (Role, HView)
. Role
is used
to identify subviews in stylesheets and to apply styling props to specific
subviews.
uicore
uses a two-phase layouting algorithm. The algoritm consists of
the following steps:
-
Up phase:
SizeTraits
(a triplet of min/max/preferred sizes) is calculated for each view in a top-down manner using the local properties and subviews'SizeTraits
. -
The window size is constrained based on the root view's
SizeTraits
. The root view's frame always matches the window size. -
Down phase: The final frame (a bounding rectangle in the superview coordinate space) is calculated for each view in a bottom-up manner.
To be filled
There are multiple ways for a TCW3 application to handle keyboard events, each having different goals and purposes:
-
Define one or more accelerator tables (precompiled mapping from key combinations to action IDs) and provide the list of them through
WndListener::interpret_event
to have the backend translate keyboard events to action IDs, which are delivered to the currently focused view or window. (See the section Actions for more). -
(macOS only) Create menu items with key equivalents set to the desired key combination and its
target
set tonil
. The Objective C messages generated by activating those menu items are converted to actions using accelerator tables if they are provided throughWndListener::interpret_event
. (See the section Actions for more). -
Define an accelerator table similarly, but use it in a window or view's keyboard event handler
*Listener::key_*
(pal::iface::WndListener
,uicore::WndListener
,uicore::ViewListener
) to interpret the event.uicore
delivers key events to a view or window. To determine the ultimate receiver, theuicore
implementation of these methods examines the currently focused view, and moves up until thekey_down
orkey_up
methods (WndListener
,ViewListener
) returnstrue
for the view or window. -
Use the text input API. The system processes input events and tells the application what to do by interfacing with the abstract text storage exposed via a text input context or by generating actions.
Upon receiving a keyboard event, the system tries each of these ways until the event is handled or there are no more ways to try. The exact order isn't strictly defined, but it usually looks like the following: 4 (modal only) → 2 → 1 → 3 → 4.
Keyboard Event
|
|
,-------------+----------------------+-----------------,
|(3) |(1) |(2) |(4)
| | v v
| | ,-----------------, ,------------,
| | | Key equivalents | | Text input |
| | '-----------------' '------------'
| | | SEL | |
| | ,----------------+ | |
System | | | | | |
- - - - - - | - - - - - - | - - | - - - , v , -|- -|- - -
App | v v , ,-----------------, , | |
| ,-----------------, , | Other UI (e.g., | , | |
| | WndListener:: | , | "open" dialog | , | |
| | interpret_event | , '-----------------' , | |
| '-----------------' - - - - - - - - - - - | |
| | Action ID | |
- - - - - - | - - - - - - | - - - - - - - - - - - - - - - - - - -|- -|- - -
Widgets | | ,--------------------------' |
| | | |
v v v v
,------------------, ,---------------------, ,----------------------,
| *Listener::key_* | | *Listener::*_action | | TextInputCtxListener |
'------------------' '---------------------' '----------------------'
Single-shot operations such as copying to clipboard are delivered to widgets as actions. Actions have global identifiers shared by all components, so they are suitable for common UI operations and application-wide operations, but not for widget-local operations.
Actions are generated through one of the following mechanisms:
- The application creates one or more accelerator tables, which are mappings
from key combinations to actions with a platform-specific representation.
When the backend needs to interpret an input event, it calls
WndListener::interpret_event
(pal
,uicore
), which calls a given callback function for each active accelerator table until it finds an applicable mapping.
- When a text input context is active, the system sends some commands as
actions. See
tcw3::pal::actions
for the list of the commands that can be generated through this mechanism.
-
(macOS only) When the user selects an application menu item (the creation of this is out of the scope of TCW3) or inputs its key equivalent, Cocoa sends an Objective C message down a responder chain. If an application object happens to receive it, the TCW3 backend will attempt to translate it to an action. On macOS, accelerator tables define mappings from Objective C selectors to actions in addition to the aforementioned key-to-action mappings.
Standard widgets from Cocoa use this responder chain as well. You can observe this by opening a standard file dialog and clicking the application's Edit menu, where you will find Cut/Copy/Paste are usable even in the dialog. Also, the user can customize the key equivalents of menu items in the user's system preference. This means you should prefer this mechanism over key-to-action mappings described in the previous bullet point.
Actions are identified by 16-bit integers (ActionId
). Some ranges are
reserved by TCW3 for common UI operations.
The pal
backend calls the following methods of pal::iface::WndListener
to
perform an action or to see if an action is valid in the current state:
validate_action
: Returns flags indicating such as whether the window can perform the action right now or not.perform_action
: Performs the action.
The uicore
implementation of this trait forwards the calls to these methods to
a view or window. To determine the ultimate receiver, the uicore
implementation of these methods examines the currently focused view, and moves
up until the validate_action
method (WndListener
, ViewListener
)
returns ActionStatus::VALID
for the view or window.
To be filled
The default tab order follows the pre-order of the view hierarchy. The order
for sibling views are defined by Layout::subviews
.
The default order can be overridden by HViewRef::override_tab_order_sibling
and HViewRef::override_tab_order_child
. These methods define a completely
independent subtree that determines the tab order. The client is
responsible for linking nodes correctly.
use tcw3::uicore::{HView, TabOrderSibling};
// root
// ├─ v1
// └─ v2
let root = HView::new(Default::default());
let v1 = HView::new(Default::default());
let v2 = HView::new(Default::default());
root.override_tab_order_child(Some([v1.clone(), v2.clone()]));
v1.override_tab_order_sibling(
TabOrderSibling::Parent(root.downgrade()),
TabOrderSibling::Sibling(v2.downgrade()),
);
v2.override_tab_order_sibling(
TabOrderSibling::Sibling(v1.downgrade()),
TabOrderSibling::Parent(root.downgrade()),
);
The testing
feature enables the testing
backend of tcw3::pal
,
which is a headless backend that simulates the behavior of a real window
system. This is mainly used for unit testing and is supposed to be enabled
only when running tests by passing a command-line option like
cargo test --workspace --all-features
. When the feature is not enabled,
the entry point function explained in the next paragraph will do nothing
except for outputting a warning message.
Enabling the feature alone doesn't activate the headless backend. You need
to call a specific entry point function and pass a closure. The closure
will receive &dyn TestingWm
that can be used to send simulated input
events to the backend.
See the documentation of tcw3::pal::testing
for more details.
Note: You need to enable the testing
feature to see the documentation.
tcw3::testing
provides an attribute macro useful for writing unit
tests using the testing
backend.
Color values are specified in the sRGB color space, unless otherwise specified.
Full color management support is yet to be implemented. Some backends are incapable of doing even a basic color management at the moment.