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}