thyme/
lib.rs

1#![allow(clippy::upper_case_acronyms)]
2#![allow(clippy::uninlined_format_args)]
3/*!
4Thyme is a highly customizable, themable immediate mode GUI toolkit for Rust.
5
6It is designed to be performant and flexible enough for use both in prototyping and production games and applications.
7Requiring a theme and image sources adds some additional development cost compared to many other immediate mode toolkits,
8however the advantage is full flexibility and control over the ultimate appearance of your UI.
9
10To use Thyme, you need to choose a renderer and event handling support.
11There are currently three renderers built in - one using [Glium](https://github.com/glium/glium),
12one using [wgpu](https://github.com/gfx-rs/wgpu-rs)), and one using raw OpenGL (https://github.com/brendanzab/gl-rs/)).
13[winit](https://github.com/rust-windowing/winit) is currently supported for event handling.
14You also need a theme definition  with associated images and fonts.  Thyme logs errors using the
15[`log`](https://github.com/rust-lang/log) crate.  A very simple logger that sends messages to stdout
16is included to help you get started.
17
18All thyme widgets are drawn using images, with the image data registered with the renderer, and then individual
19widget components defined within that image within the theme file.  Likewise, `ttf` fonts are registered with
20the renderer and then individual fonts for use in your UI are defined in the theme file.
21Widgets themselves can be defined fully in source code, with only some basic templates in the theme file, or
22you can largely leave only logic in the source, with layout, alignment, etc defined in the theme file.
23
24# Example
25
26A quick snippet showing how the UI code looks:
27```
28// This method would be called in your main loop.  Each frame, you would call
29// `create_frame` on your context and the typically pass it into a function
30// like this one to construct the UI.
31fn create_ui(ui: &mut Frame) {
32    // all widgets need a theme, which is the first argument to widget builder methods
33    ui.label("label", "My Title");
34
35    // when a widget has children, the "ui" object is passed through a closure.
36    // All of the widget types such as window, scrollpane, etc are built using
37    // the Public API - meaning you can build your own custom versions if you wish.
38    ui.window("data_window", |ui| {
39      ui.label("label", "Data Points");
40
41      // many widgets return state data.  Here, clicked will only
42      // return true on the frame the button was clicked
43      if ui.button("button", "Calculate").clicked {
44        // do some expensive calculation
45      }
46    });
47
48    // You can either specify layout and alignment in the theme, or directly in code.
49    // If you specify your theme as a file read from disk (see the demo examples), you
50    // can tweak these aspects live using Thyme's built in live-reload.
51
52    // Here, we hardcode some layout
53    ui.start("custom_widget")
54    .align(Align::BotRight)
55    .layout(Layout::Vertical)
56    .children(|ui| {
57      for i in 1..10 {
58        ui.label("label", format!("Row #{}", i));
59      }
60    });
61}
62```
63
64# Overview
65
66For common use cases, the [`AppBuilder`](struct.AppBuilder.html) struct is available to allow you to create a simple
67application with just a few lines of code.
68
69In more general cases, you first create the [`ContextBuilder`](struct.ContextBuilder.html) and register resources
70with it. Once done, you [`build`](struct.ContextBuilder.html#method.build) the associated [`Context`](struct.Context.html).
71At each frame of your app, you [`create a Thyme frame`](struct.Context.html#method.create_frame).  The
72[`Frame`](struct.Frame.html) is then passed along through your UI building routines, and is used to create
73[`WidgetBuilders`](struct.WidgetBuilder.html) and populate your Widget tree.
74
75See the examples for details on how to use both of the above methods.
76
77# Theme Definition
78When creating a [`ContextBuilder`](struct.ContextBuilder.html), you need to specify a theme.  You can keep the
79theme fairly small with just a base set of widgets, defining most things in code, or go the other way around.
80
81The theme can be defined from any [`serde`](https://serde.rs/)
82compatible source, with the examples in this project using [`YAML`](https://yaml.org/).
83The theme has several sections: `fonts`, `image_sets`, and `widgets`.
84
85## Fonts
86The `fonts` section consists of a mapping, with `IDs` mapped
87to font data.  The font IDs are used elsewhere in the widgets section and in code when specifying
88a [`font`](struct.WidgetBuilder.html#method.font).
89
90The data consists of a `source`, which is a string which must match one of the fonts registered
91with the [`ContextBuilder`](struct.ContextBuilder.html#method.register_font_source), and a `size`
92in logical pixels.  Fonts may optionally specify one or more (inclusive) ranges of characters to display,
93subject to those characters being present in the actual font TTF data.  By default, printable
94characters from U+0000 to U+00FF are added.  In the future, once this is supported by RustType,
95 the default should change to automatically support all characters present in the source font data.
96```yaml
97fonts:
98  medium:
99    source: roboto
100    size: 20
101    # only support ASCII printable characters for this font
102    characters:
103      - lower: 0x0020
104        upper: 0x007e
105  small:
106    source: roboto
107    size: 16
108```
109
110## Image Sets
111Images are defined as a series of `image_sets`.  Each image_set has an `id`, used as the first
112part of the ID of each image in the set.  The complete image ID is equal to `image_set_id/image_id`.
113Each image_set may be `source`d from a different image file.  If you leave the `source` out of the image definition,
114all images will be treated as sourced from a 1x1 pixel.  This can be useful to create simple, minimal themes
115without requiring an image source.
116Each image file must be registered with [`ContextBuilder`](struct.ContextBuilder.html#method.register_image),
117under an ID matching the `source` id.
118```yaml
119image_sets:
120  source: gui
121  scale: 1
122  images:
123    ...
124```
125
126The image_set `scale` is used to pre-scale all images in that set by a given factor.  With a scale of 1 (the default),
127all images will be drawn at 1 image pixel to 1 physical screen pixel when the display has a scale factor of 1, but 1 image pixel to
1282 physical screen pixels on a hi-dpi display with a scale factor of 2.  By setting the scale factor of the image set to 0.5, you
129can use the full resolution on hi-dpi displays, but you will need twice the image resolution to get the same UI size.
130
131### Image Sampling
132Building images as sub-images of a larger spritesheet is convenient, but you need to be aware of texture sampling issues.  Because of
133floating point rounding, graphics cards will sometimes partially sample pixels just outside the defined area of your images.  To avoid
134unsightly lines and other graphical glitches, it is safest to have a 1 pixel wide border around all images, so that none of them
135are touching.  For images that need to seamlessly repeat or stretch many times (i.e. Simple Images below), the border pixels should
136maintain the same color as the nearby sub-image.  Otherwise you may not always get a seamless effect.
137
138### Images
139Each image set can contain many `images`, which are defined as subsets of the overall image file in various ways.  The type of
140image for each image within the set is determined based on the parameters specified.  Each image may optionally have a `color`
141attribute.  Color is specified via a `#` character followed by a hex code - See [`Color`](struct.Color.html).
142
143#### Solid Images
144Solid images are a single solid color, normally specified with the `color` field.  You will need to specify `solid: true`
145to help the Deserializer parse these definitions.  These are especially useful when defining a theme without an image file source.
146```yaml
147  bg_grey:
148    solid: true
149    color: "#888888"
150```
151
152#### Simple Images
153Simple images are defined by a position and size, in pixels, within the overall image.  The `fill` field is optional, with valid
154values of `None` (default) - image is drawn at fixed size, `Stretch` - image is stretched to fill an area, `Repeat` - image repeats
155over an area.
156```yaml
157  progress_bar:
158    position: [100, 100]
159    size: [16, 16]
160    fill: Stretch
161```
162
163#### Image Groups
164You can create an image group as a shorthand for multiple simple images.  You specify an overall scale factor and fill, then for each image,
165x, y, width, and height.  These four values are multipled by the scale factor.  All simple images in a group are immediately expanded
166as if they were specified as individual images for purposes of being referenced by other image types.
167```yaml
168  icons_set:
169    fill: Stretch
170    group_scale: [64, 64]
171    images:
172      up_arrow: [0, 0, 1, 1]
173      down_arrow: [1, 0, 1, 1]
174```
175
176#### Collected Images
177Collected images allow you to define an image that consists of one or more sub images, fairly arbitrarily.  Each sub image includes
178the image it references, a position, and a size.  Both position and size may be positive or negative.  When drawing, the size of the
179sub image is calculated as the main size of the image being drawn plus the sub image size for a negative or zero component, while a
180positive component indicates to just use the sub image size directly.  For the position, the calculation is similar
181except that for each x and y position component, a negative position means to add the main image position plus main image size plus
182sub image position.  This allows you to offset sub-images with respect to any of the top, bottom, left, or right of the main image.
183
184In this example, `window_bg_base` is a composed image.  Assuming it is transparent in the center, the collected image `window_bg` will draw
185the `window_bg_base` frame around a repeating tile of the `window_fill` image.
186```yaml
187  window_bg:
188    sub_images:
189      window_bg_base:
190        position: [0, 0]
191        size: [0, 0]
192      window_fill:
193        position: [5, 5]
194        size: [-10, -10]
195  window_bg_base:
196    position: [0, 0]
197    grid_size: [32, 32]
198  window_fill:
199    position: [128, 0]
200    size: [128, 128]
201    fill: Repeat
202```
203
204#### Composed Images
205Composed images are a common special case of collected images., consisting of an even 3 by 3 grid.  The corners are drawn at a fixed
206size, while the middle sections stretch along one axis.  The center grid image stretches to fill in the inner area of the image.
207These images allow you to easily draw widgets with almost any size that maintain the same look.  The `grid_size` specifies the size
208of one of the 9 cells, with each cell having the same size.
209```yaml
210  button_normal:
211    position: [100, 100]
212    grid_size: [16, 16]
213```
214
215#### Composed Horizontal and Vertical
216There are also composed horizontal and composed vertical images, that consist of a 3x1 and 1x3 grid, respectively.  These
217are defined and used in the same manner as regular composed images, but use `grid_size_horiz` and `grid_size_vert` to
218differentiate the different types.
219
220#### Timed Images
221Timed images display one out of several frames, on a timer.  Timed images can repeat continuously (the default), or only display once,
222based on the value of the optional `once` parameter.  `frame_time_millis` is how long each frame is shown for, in milliseconds.  Each
223`frame` is the `id` of an image within the current image set.  It can be any of the other types of images in the current set.
224
225In this example, each frame is displayed for 500 milliseconds in an endless cycle.
226```yaml
227  button_flash:
228    frame_time_millis: 500
229    once: false
230    frames:
231      - button_normal
232      - button_bright
233```
234
235#### Animated Images
236Animated images display one of several sub images based on the [`AnimState`](struct.AnimState.html). of the parent widget.
237The referenced images are specified by `id`, and can include Simple, Composed, or Collected images.
238```yaml
239  button:
240    states:
241      Normal: button_normal
242      Hover: button_hover
243      Pressed: button_pressed
244      Active: button_active
245      Active + Hover: button_hover_active
246      Active + Pressed: button_pressed_active
247```
248
249Images which contain references to other images are parsed in a particular order - `Collected`, then `Animated`, then
250`Timed`.  This means an `Animated` image may reference a `Collected` image, but not the other way around.  All of these
251image types may contain references to the basic image types - `Solid`, `Simple`, `Composed`, `ComposedHorizontal`, and
252`ComposedVertical`.  In addition, `Collected` images may refer to other `Collected` images.
253
254### Aliases
255For convenience, you can create an image ID which is an alias to another image.  For example, you may want a particular
256type of button to be easily changable to its own unique image in the future.
257```yaml
258  scroll_button:
259    from: button
260```
261
262## Widgets
263The widgets section defines themes for all widgets you will use in your UI.  Whenever you create a widget, such as through
264[`Frame.start`](struct.Frame.html#method.start), you specify a `theme_id`.  This `theme_id` must match one
265of the keys defined in this section.
266
267### Recursive definition
268Widget themes are defined recursively, and Thyme will first look for the exact recursive match, before falling back to the top level match.
269Each widget entry may have one or more `children`, with each child being a full widget definition in its own right.  The ID of each widget in the
270tree is computed as `{parent_id}/{child_id}`, recursively.
271
272For example, if you specified a `button` that is a child of a `content` that is in turn a child of `window`, the theme ID will be `window/content/button`.
273Thyme will first look for a theme at the full ID, i.e.
274```yaml
275  window:
276    children:
277      content:
278        children:
279          button
280```
281If that is not found, it will look for `button` at the top level.
282
283### Widget `from` attribute
284Each widget entry in the `widgets` section may optionally have a `from` attribute, which instructs Thyme to copy the specified widget theme into this theme.
285This is resolved fully recursively and will copy all children, merging  where appropriate.  `from` attributes may also be defined recursively.
286Specifically defined attributes within a widget theme will override the `from` theme.  Thyme first looks for the `from` theme at the specified absolute path.
287If no theme is found there, it then looks in the path relative to the current widget.
288
289For example, this definition:
290```yaml
291  button:
292    background: gui/button
293    size: [100, 25]
294  titlebar:
295    from: button
296    children:
297      label:
298        font: medium
299      close_button:
300        from: button
301        foreground: gui/close
302        size: [25, 25]
303  main_window_titlebar:
304    from: titlebar
305    children:
306      label:
307        text: "Main Window"
308```
309
310will interpret `main_window_titlebar` into the equivalent of this:
311```yaml
312  main_window_titlebar:
313    background: gui/button
314    size: [100, 25]
315    children:
316      label:
317        font: medium
318        text: "Main Window"
319      close_button:
320        background: gui/button
321        foregorund: gui/close
322        size: [25, 25]
323```
324
325### Overriding images
326`background` and `foreground` image attributes may be overridden as normal.  If you want to remove this attribute, you can use
327the special ID `empty`, which draws nothing.
328
329### Widget Attributes
330Each widget theme has many optional attributes that may be defined in the theme file, UI building source code, or both.  Source code
331methods on [`WidgetBuilder`](struct.WidgetBuilder.html) will take precedence over items defined in the theme file.  The
332[`child_align`](struct.WidgetBuilder.html#method.child_align), [`layout`](struct.WidgetBuilder.html#method.layout), and
333[`layout_spacing`](struct.WidgetBuilder.html#method.layout_spacing) fields deal specifically with how
334the widget will layout its children.
335
336```yaml
337   complicated_button:
338     text: Hello
339     text_color: "#FFAA00"
340     text_align: Center
341     font: medium
342     image_color: "#FFFFFF"
343     background: gui/button
344     foreground: gui/button_icon
345     tooltip: "This is a button!"
346     wants_mouse: true
347     wants_scroll: false
348     pos: [10, 10]
349     size: [100, 0]
350     width_from: Normal
351     height_from: FontLine
352     # OR size_from: [Normal, FontLine]
353     border: { all: 5 }
354     align: TopLeft
355     child_align: Top
356     layout: Vertical
357     layout_spacing: 5
358```
359
360### Custom fields
361You may optionally specify custom values in the `custom` mapping of the theme.  This allows more specialized widgets to
362obtain neccessary parameters from the theme itself, rather than relying on another external source.  Allowed data types
363include floats, integers, and strings.
364
365```yaml
366  my_custom_widget:
367    custom:
368      min_width: 0.0
369      min_height: 25.0
370      secondary_font: "Bold"
371```
372!*/
373
374#![deny(missing_docs)]
375
376pub mod bench;
377pub mod log;
378
379mod app_builder;
380mod context;
381mod context_builder;
382mod font;
383mod frame;
384mod image;
385mod theme;
386mod recipes;
387mod render;
388mod resource;
389mod theme_definition;
390mod point;
391mod scrollpane;
392mod text_area;
393mod widget;
394mod window;
395mod winit_io;
396
397#[cfg(feature = "glium_backend")]
398mod glium_backend;
399
400#[cfg(feature = "glium_backend")]
401pub use glium_backend::{GliumRenderer, GliumError};
402
403#[cfg(feature = "wgpu_backend")]
404mod wgpu_backend;
405
406#[cfg(feature = "wgpu_backend")]
407pub use wgpu_backend::WgpuRenderer;
408
409pub use app_builder::AppBuilder;
410
411#[cfg(feature="glium_backend")]
412pub use app_builder::GliumApp;
413
414#[cfg(feature = "gl_backend")]
415mod gl_backend;
416
417#[cfg(feature = "gl_backend")]
418pub use gl_backend::{GLRenderer, GlError};
419
420pub use frame::{Frame, MouseButton};
421pub use point::{Rect, Point, Border};
422pub use widget::{WidgetBuilder, WidgetState};
423pub use context_builder::{BuildOptions, ContextBuilder};
424pub use context::{Context, PersistentState, InputModifiers, SavedContext};
425pub use scrollpane::{ScrollpaneBuilder, ShowElement};
426pub use theme_definition::{AnimStateKey, AnimState, Align, Color, Layout, WidthRelative, HeightRelative};
427pub use window::WindowBuilder;
428pub use winit_io::WinitIo;
429
430pub use render::{IO, Renderer};
431
432/// A generic error that can come from a variety of internal sources.
433#[derive(Debug)]
434pub enum Error {
435    /// An error originating from a passed in serde deserializer
436    Serde(String),
437
438    /// An error originating from an invalid theme reference or theme parsing
439    Theme(String),
440
441    /// An error originating from an invalid font source
442    FontSource(String),
443
444    /// An error that occurred attempting to use the filesystem
445    IO(std::io::Error),
446
447    /// An error creating the display
448    DisplayCreation(String),
449
450    /// An error originating from Winit
451    Winit(crate::winit_io::WinitError),
452
453    /// An error that occurred reading an image using the `image` crate.
454    #[cfg(feature="image")]
455    Image(::image::error::ImageError),
456
457    /// An error originating from Glium
458    #[cfg(feature="glium_backend")]
459    Glium(crate::glium_backend::GliumError),
460
461    /// An error originating from OpenGl
462    #[cfg(feature="gl_backend")]
463    Gl(crate::gl_backend::GlError),
464
465    /// An error originating from Wgpu's create surface routine
466    #[cfg(feature="wgpu_backend")]
467    WgpuSurface(wgpu::CreateSurfaceError),
468}
469
470impl std::fmt::Display for Error {
471    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
472        use self::Error::*;
473        match self {
474            Serde(e) => write!(f, "Error deserializing theme: {}", e),
475            Theme(msg) => write!(f, "Error creating theme from theme definition: {}", msg),
476            FontSource(msg) => write!(f, "Error reading font source: {}", msg),
477            IO(error) => write!(f, "IO Error: {}", error),
478            DisplayCreation(msg) => write!(f, "Error creating display: {}", msg),
479            Winit(error) => write!(f, "Winit error: {}", error),
480
481            #[cfg(feature="image")]
482            Image(error) => write!(f, "Image Error: {}", error),
483
484            #[cfg(feature="glium_backend")]
485            Glium(error) => write!(f, "Glium Error: {}", error),
486
487            #[cfg(feature="gl_backend")]
488            Gl(error) => write!(f, "OpenGL Error: {}", error),
489
490            #[cfg(feature="wgpu_backend")]
491            WgpuSurface(error) => write!(f, "Wgpu surface error: {}", error),
492        }
493    }
494}
495
496impl std::error::Error for Error {
497    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
498        use self::Error::*;
499        match self {
500            Serde(..) => None,
501            Theme(..) => None,
502            FontSource(..) => None,
503            IO(error) => Some(error),
504            DisplayCreation(..) => None,
505            Winit(error) => Some(error),
506
507            #[cfg(feature="image")]
508            Image(error) => Some(error),
509
510            #[cfg(feature="glium_backend")]
511            Glium(error) => Some(error),
512
513            #[cfg(feature="gl_backend")]
514            Gl(error) => Some(error),
515
516            #[cfg(feature="wgpu_backend")]
517            WgpuSurface(error) => Some(error),
518        }
519    }
520}