aboutsummaryrefslogtreecommitdiff
path: root/docs/quantum_painter.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/quantum_painter.md')
-rw-r--r--docs/quantum_painter.md215
1 files changed, 121 insertions, 94 deletions
diff --git a/docs/quantum_painter.md b/docs/quantum_painter.md
index 7f524b07ee..1d844d0f94 100644
--- a/docs/quantum_painter.md
+++ b/docs/quantum_painter.md
@@ -1,4 +1,4 @@
-# Quantum Painter :id=quantum-painter
+# Quantum Painter {#quantum-painter}
Quantum Painter is the standardised API for graphical displays. It currently includes support for basic drawing primitives, as well as custom images, animations, and fonts.
@@ -13,7 +13,9 @@ QUANTUM_PAINTER_DRIVERS += ......
You will also likely need to select an appropriate driver in `rules.mk`, which is listed below.
-!> Quantum Painter is not currently integrated with system-level operations such as when the keyboard goes into suspend. Users will need to handle this manually at the current time.
+::: warning
+Quantum Painter is not currently integrated with system-level operations such as when the keyboard goes into suspend. Users will need to handle this manually at the current time.
+:::
The QMK CLI can be used to convert from normal images such as PNG files or animated GIFs, as well as fonts from TTF files.
@@ -35,7 +37,7 @@ Supported devices:
| SSD1306 (I2C) | Monochrome OLED | 128x32 | I2C | `QUANTUM_PAINTER_DRIVERS += sh1106_i2c` |
| Surface | Virtual | User-defined | None | `QUANTUM_PAINTER_DRIVERS += surface` |
-## Quantum Painter Configuration :id=quantum-painter-config
+## Quantum Painter Configuration {#quantum-painter-config}
| Option | Default | Purpose |
|---------------------------------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
@@ -54,11 +56,11 @@ Supported devices:
Drivers have their own set of configurable options, and are described in their respective sections.
-## Quantum Painter CLI Commands :id=quantum-painter-cli
+## Quantum Painter CLI Commands {#quantum-painter-cli}
-<!-- tabs:start -->
+:::::tabs
-### ** `qmk painter-convert-graphics` **
+==== `qmk painter-convert-graphics`
This command converts images to a format usable by QMK, i.e. the QGF File Format.
@@ -109,7 +111,7 @@ Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/my_image.qgf.h...
Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/my_image.qgf.c...
```
-### ** `qmk painter-make-font-image` **
+==== `qmk painter-make-font-image`
This command converts a TTF font to an intermediate format for editing, before converting to the QFF File Format.
@@ -142,7 +144,7 @@ The `UNICODE_GLYPHS` argument allows for specifying extra unicode glyphs to gene
$ qmk painter-make-font-image --font NotoSans-ExtraCondensedBold.ttf --size 11 -o noto11.png --unicode-glyphs "ĄȽɂɻɣɈʣ"
```
-### ** `qmk painter-convert-font-image` **
+==== `qmk painter-convert-font-image`
This command converts an intermediate font image to the QFF File Format.
@@ -187,14 +189,13 @@ Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/noto11.qff.h...
Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/noto11.qff.c...
```
-<!-- tabs:end -->
+:::::
-## Quantum Painter Display Drivers :id=quantum-painter-drivers
+## Quantum Painter Display Drivers {#quantum-painter-drivers}
-<!-- tabs:start -->
+::::::tabs
-
-### ** LCD **
+===== LCD
Most TFT display panels use a 5-pin interface -- SPI SCK, SPI MOSI, SPI CS, D/C, and RST pins.
@@ -202,9 +203,9 @@ For these displays, QMK's `spi_master` must already be correctly configured for
The pin assignments for SPI CS, D/C, and RST are specified during device construction.
-<!-- tabs:start -->
+:::::tabs
-#### ** GC9A01 **
+==== GC9A01
Enabling support for the GC9A01 in Quantum Painter is done by adding the following to `rules.mk`:
@@ -230,7 +231,7 @@ The maximum number of displays can be configured by changing the following in yo
Native color format rgb565 is compatible with GC9A01
-#### ** ILI9163 **
+==== ILI9163
Enabling support for the ILI9163 in Quantum Painter is done by adding the following to `rules.mk`:
@@ -256,7 +257,7 @@ The maximum number of displays can be configured by changing the following in yo
Native color format rgb565 is compatible with ILI9163
-#### ** ILI9341 **
+==== ILI9341
Enabling support for the ILI9341 in Quantum Painter is done by adding the following to `rules.mk`:
@@ -282,7 +283,7 @@ The maximum number of displays can be configured by changing the following in yo
Native color format rgb565 is compatible with ILI9341
-#### ** ILI9486 **
+==== ILI9486
Enabling support for the ILI9486 in Quantum Painter is done by adding the following to `rules.mk`:
@@ -315,7 +316,7 @@ The maximum number of displays can be configured by changing the following in yo
Native color format rgb888 is compatible with ILI9486
Native color format rgb565 is compatible with ILI9486 Waveshare
-#### ** ILI9488 **
+==== ILI9488
Enabling support for the ILI9488 in Quantum Painter is done by adding the following to `rules.mk`:
@@ -341,7 +342,7 @@ The maximum number of displays can be configured by changing the following in yo
Native color format rgb888 is compatible with ILI9488
-#### ** ST7735 **
+==== ST7735
Enabling support for the ST7735 in Quantum Painter is done by adding the following to `rules.mk`:
@@ -367,9 +368,11 @@ The maximum number of displays can be configured by changing the following in yo
Native color format rgb565 is compatible with ST7735
-!> Some ST7735 devices are known to have different drawing offsets -- despite being a 132x162 pixel display controller internally, some display panels are only 80x160, or smaller. These may require an offset to be applied; see `qp_set_viewport_offsets` above for information on how to override the offsets if they aren't correctly rendered.
+::: warning
+Some ST7735 devices are known to have different drawing offsets -- despite being a 132x162 pixel display controller internally, some display panels are only 80x160, or smaller. These may require an offset to be applied; see `qp_set_viewport_offsets` above for information on how to override the offsets if they aren't correctly rendered.
+:::
-#### ** ST7789 **
+==== ST7789
Enabling support for the ST7789 in Quantum Painter is done by adding the following to `rules.mk`:
@@ -395,11 +398,13 @@ The maximum number of displays can be configured by changing the following in yo
Native color format rgb565 is compatible with ST7789
-!> Some ST7789 devices are known to have different drawing offsets -- despite being a 240x320 pixel display controller internally, some display panels are only 240x240, or smaller. These may require an offset to be applied; see `qp_set_viewport_offsets` above for information on how to override the offsets if they aren't correctly rendered.
+::: warning
+Some ST7789 devices are known to have different drawing offsets -- despite being a 240x320 pixel display controller internally, some display panels are only 240x240, or smaller. These may require an offset to be applied; see `qp_set_viewport_offsets` above for information on how to override the offsets if they aren't correctly rendered.
+:::
-<!-- tabs:end -->
+:::::
-### ** OLED **
+===== OLED
OLED displays tend to use 5-pin SPI when at larger resolutions, or when using color -- SPI SCK, SPI MOSI, SPI CS, D/C, and RST pins. Smaller OLEDs may use I2C instead.
@@ -407,9 +412,9 @@ When using these displays, either `spi_master` or `i2c_master` must already be c
For SPI, the pin assignments for SPI CS, D/C, and RST are specified during device construction -- for I2C the panel's address is specified instead.
-<!-- tabs:start -->
+:::::tabs
-#### ** SSD1351 **
+==== SSD1351
Enabling support for the SSD1351 in Quantum Painter is done by adding the following to `rules.mk`:
@@ -435,7 +440,7 @@ The maximum number of displays can be configured by changing the following in yo
Native color format rgb565 is compatible with SSD1351
-#### ** SH1106 **
+==== SH1106
Enabling support for the SH1106 in Quantum Painter is done by adding the following to `rules.mk`:
@@ -469,17 +474,19 @@ The maximum number of displays of each type can be configured by changing the fo
Native color format mono2 is compatible with SH1106
-#### ** SSD1306 **
+==== SSD1306
SSD1306 and SH1106 are almost entirely identical, to the point of being indisinguishable by Quantum Painter. Enable SH1106 support in Quantum Painter and create SH1106 devices in firmware to perform drawing operations on SSD1306 displays.
-<!-- tabs:end -->
+:::::
-### ** Surface **
+===== Surface
Quantum Painter has a surface driver which is able to target a buffer in RAM. In general, surfaces keep track of the "dirty" region -- the area that has been drawn to since the last flush -- so that when transferring to the display they can transfer the minimal amount of data to achieve the end result.
-!> These generally require significant amounts of RAM, so at large sizes and/or higher bit depths, they may not be usable on all MCUs.
+::: warning
+These generally require significant amounts of RAM, so at large sizes and/or higher bit depths, they may not be usable on all MCUs.
+:::
Enabling support for surfaces in Quantum Painter is done by adding the following to `rules.mk`:
@@ -533,13 +540,17 @@ bool qp_surface_draw(painter_device_t surface, painter_device_t display, uint16_
The `surface` is the surface to copy out from. The `display` is the target display to draw into. `x` and `y` are the target location to draw the surface pixel data. Under normal circumstances, the location should be consistent, as the dirty region is calculated with respect to the `x` and `y` coordinates -- changing those will result in partial, overlapping draws. `entire_surface` whether the entire surface should be drawn, instead of just the dirty region.
-!> The surface and display panel must have the same native pixel format.
+::: warning
+The surface and display panel must have the same native pixel format.
+:::
-?> Calling `qp_flush()` on the surface resets its dirty region. Copying the surface contents to the display also automatically resets the dirty region.
+::: tip
+Calling `qp_flush()` on the surface resets its dirty region. Copying the surface contents to the display also automatically resets the dirty region.
+:::
-<!-- tabs:end -->
+::::::
-## Quantum Painter Drawing API :id=quantum-painter-api
+## Quantum Painter Drawing API {#quantum-painter-api}
All APIs require a `painter_device_t` object as their first parameter -- this object comes from the specific device initialisation, and instructions on creating it can be found in each driver's respective section.
@@ -548,13 +559,15 @@ To use any of the APIs, you need to include `qp.h`:
#include <qp.h>
```
-<!-- tabs:start -->
+::::::tabs
-### ** General Notes **
+===== General Notes
The coordinate system used in Quantum Painter generally accepts `left`, `top`, `right`, and `bottom` instead of x/y/width/height, and each coordinate is inclusive of where pixels should be drawn. This is required as some datatypes used by display panels have a maximum value of `255` -- for any value or geometry extent that matches `256`, this would be represented as a `0`, instead.
-?> Drawing a horizontal line 8 pixels long, starting from 4 pixels inside the left side of the display, will need `left=4`, `right=11`.
+::: tip
+Drawing a horizontal line 8 pixels long, starting from 4 pixels inside the left side of the display, will need `left=4`, `right=11`.
+:::
All color data matches the standard QMK HSV triplet definitions:
@@ -562,13 +575,15 @@ All color data matches the standard QMK HSV triplet definitions:
* Saturation is of the range `0...255` and is internally mapped to 0...100% saturation.
* Value is of the range `0...255` and is internally mapped to 0...100% brightness.
-?> Colors used in Quantum Painter are not subject to the RGB lighting CIE curve, if it is enabled.
+::: tip
+Colors used in Quantum Painter are not subject to the RGB lighting CIE curve, if it is enabled.
+:::
-### ** Device Control **
+===== Device Control
-<!-- tabs:start -->
+:::::tabs
-#### ** Display Initialisation **
+==== Display Initialisation
```c
bool qp_init(painter_device_t device, painter_rotation_t rotation);
@@ -584,7 +599,7 @@ void keyboard_post_init_kb(void) {
}
```
-#### ** Display Power **
+==== Display Power
```c
bool qp_power(painter_device_t device, bool power_on);
@@ -592,7 +607,9 @@ bool qp_power(painter_device_t device, bool power_on);
The `qp_power` function instructs the display whether or not the display panel should be on or off.
-!> If there is a separate backlight controlled through the normal QMK backlight API, this is not controlled by the `qp_power` function and needs to be manually handled elsewhere.
+::: warning
+If there is a separate backlight controlled through the normal QMK backlight API, this is not controlled by the `qp_power` function and needs to be manually handled elsewhere.
+:::
```c
static uint8_t last_backlight = 255;
@@ -615,7 +632,7 @@ void suspend_wakeup_init_user(void) {
}
```
-#### ** Display Clear **
+==== Display Clear
```c
bool qp_clear(painter_device_t device);
@@ -623,7 +640,7 @@ bool qp_clear(painter_device_t device);
The `qp_clear` function clears the display's screen.
-#### ** Display Flush **
+==== Display Flush
```c
bool qp_flush(painter_device_t device);
@@ -631,7 +648,9 @@ bool qp_flush(painter_device_t device);
The `qp_flush` function ensures that all drawing operations are "pushed" to the display. This should be done as the last operation whenever a sequence of draws occur, and guarantees that any changes are applied.
-!> Some display panels may seem to work even without a call to `qp_flush` -- this may be because the driver cannot queue drawing operations and needs to display them immediately when invoked. In general, calling `qp_flush` at the end is still considered "best practice".
+::: warning
+Some display panels may seem to work even without a call to `qp_flush` -- this may be because the driver cannot queue drawing operations and needs to display them immediately when invoked. In general, calling `qp_flush` at the end is still considered "best practice".
+:::
```c
void housekeeping_task_user(void) {
@@ -645,13 +664,13 @@ void housekeeping_task_user(void) {
}
```
-<!-- tabs:end -->
+:::::
-### ** Drawing Primitives **
+===== Drawing Primitives
-<!-- tabs:start -->
+:::::tabs
-#### ** Set Pixel **
+==== Set Pixel
```c
bool qp_setpixel(painter_device_t device, uint16_t x, uint16_t y, uint8_t hue, uint8_t sat, uint8_t val);
@@ -659,7 +678,9 @@ bool qp_setpixel(painter_device_t device, uint16_t x, uint16_t y, uint8_t hue, u
The `qp_setpixel` can be used to set a specific pixel on the screen to the supplied color.
-?> Using `qp_setpixel` for large amounts of drawing operations is inefficient and should be avoided unless they cannot be achieved with other drawing APIs.
+::: tip
+Using `qp_setpixel` for large amounts of drawing operations is inefficient and should be avoided unless they cannot be achieved with other drawing APIs.
+:::
```c
void housekeeping_task_user(void) {
@@ -675,7 +696,7 @@ void housekeeping_task_user(void) {
}
```
-#### ** Draw Line **
+==== Draw Line
```c
bool qp_line(painter_device_t device, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t hue, uint8_t sat, uint8_t val);
@@ -697,7 +718,7 @@ void housekeeping_task_user(void) {
}
```
-#### ** Draw Rect **
+==== Draw Rect
```c
bool qp_rect(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
@@ -719,7 +740,7 @@ void housekeeping_task_user(void) {
}
```
-#### ** Draw Circle **
+==== Draw Circle
```c
bool qp_circle(painter_device_t device, uint16_t x, uint16_t y, uint16_t radius, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
@@ -741,7 +762,7 @@ void housekeeping_task_user(void) {
}
```
-#### ** Draw Ellipse **
+==== Draw Ellipse
```c
bool qp_ellipse(painter_device_t device, uint16_t x, uint16_t y, uint16_t sizex, uint16_t sizey, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
@@ -763,9 +784,9 @@ void housekeeping_task_user(void) {
}
```
-<!-- tabs:end -->
+:::::
-### ** Image Functions **
+===== Image Functions
Making an image available for use requires compiling it into your firmware. To do so, assuming you've created `my_image.qgf.c` and `my_image.qgf.h` as per the CLI examples above, you'd add the following to your `rules.mk`:
@@ -778,9 +799,9 @@ SRC += my_image.qgf.c
#include "my_image.qgf.h"
```
-<!-- tabs:start -->
+:::::tabs
-#### ** Load Image **
+==== Load Image
```c
painter_image_handle_t qp_load_image_mem(const void *buffer);
@@ -790,9 +811,11 @@ The `qp_load_image_mem` function loads a QGF image from memory or flash.
`qp_load_image_mem` returns a handle to the loaded image, which can then be used to draw to the screen using `qp_drawimage`, `qp_drawimage_recolor`, `qp_animate`, or `qp_animate_recolor`. If an image is no longer required, it can be unloaded by calling `qp_close_image` below.
-See the [CLI Commands](quantum_painter.md?id=quantum-painter-cli) for instructions on how to convert images to [QGF](quantum_painter_qgf.md).
+See the [CLI Commands](quantum_painter#quantum-painter-cli) for instructions on how to convert images to [QGF](quantum_painter_qgf).
-?> The total number of images available to load at any one time is controlled by the configurable option `QUANTUM_PAINTER_NUM_IMAGES` in the table above. If more images are required, the number should be increased in `config.h`.
+::: tip
+The total number of images available to load at any one time is controlled by the configurable option `QUANTUM_PAINTER_NUM_IMAGES` in the table above. If more images are required, the number should be increased in `config.h`.
+:::
Image information is available through accessing the handle:
@@ -802,7 +825,7 @@ Image information is available through accessing the handle:
| Height | `image->height` |
| Frame Count | `image->frame_count` |
-#### ** Unload Image **
+==== Unload Image
```c
bool qp_close_image(painter_image_handle_t image);
@@ -810,7 +833,7 @@ bool qp_close_image(painter_image_handle_t image);
The `qp_close_image` function releases resources related to the loading of the supplied image.
-#### ** Draw image **
+==== Draw image
```c
bool qp_drawimage(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image);
@@ -830,7 +853,7 @@ void keyboard_post_init_kb(void) {
}
```
-#### ** Animate Image **
+==== Animate Image
```c
deferred_token qp_animate(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image);
@@ -855,7 +878,7 @@ void keyboard_post_init_kb(void) {
}
```
-#### ** Stop Animation **
+==== Stop Animation
```c
void qp_stop_animation(deferred_token anim_token);
@@ -870,9 +893,9 @@ void housekeeping_task_user(void) {
}
```
-<!-- tabs:end -->
+:::::
-### ** Font Functions **
+===== Font Functions
Making a font available for use requires compiling it into your firmware. To do so, assuming you've created `my_font.qff.c` and `my_font.qff.h` as per the CLI examples above, you'd add the following to your `rules.mk`:
@@ -885,9 +908,9 @@ SRC += noto11.qff.c
#include "noto11.qff.h"
```
-<!-- tabs: start -->
+:::::tabs
-#### ** Load Font **
+==== Load Font
```c
painter_font_handle_t qp_load_font_mem(const void *buffer);
@@ -897,9 +920,11 @@ The `qp_load_font_mem` function loads a QFF font from memory or flash.
`qp_load_font_mem` returns a handle to the loaded font, which can then be measured using `qp_textwidth`, or drawn to the screen using `qp_drawtext`, or `qp_drawtext_recolor`. If a font is no longer required, it can be unloaded by calling `qp_close_font` below.
-See the [CLI Commands](quantum_painter.md?id=quantum-painter-cli) for instructions on how to convert TTF fonts to [QFF](quantum_painter_qff.md).
+See the [CLI Commands](quantum_painter#quantum-painter-cli) for instructions on how to convert TTF fonts to [QFF](quantum_painter_qff).
-?> The total number of fonts available to load at any one time is controlled by the configurable option `QUANTUM_PAINTER_NUM_FONTS` in the table above. If more fonts are required, the number should be increased in `config.h`.
+::: tip
+The total number of fonts available to load at any one time is controlled by the configurable option `QUANTUM_PAINTER_NUM_FONTS` in the table above. If more fonts are required, the number should be increased in `config.h`.
+:::
Font information is available through accessing the handle:
@@ -907,7 +932,7 @@ Font information is available through accessing the handle:
|-------------|----------------------|
| Line Height | `image->line_height` |
-#### ** Unload Font **
+==== Unload Font
```c
bool qp_close_font(painter_font_handle_t font);
@@ -915,7 +940,7 @@ bool qp_close_font(painter_font_handle_t font);
The `qp_close_font` function releases resources related to the loading of the supplied font.
-#### ** Measure Text **
+==== Measure Text
```c
int16_t qp_textwidth(painter_font_handle_t font, const char *str);
@@ -923,7 +948,7 @@ int16_t qp_textwidth(painter_font_handle_t font, const char *str);
The `qp_textwidth` function allows measurement of how many pixels wide the supplied string would result in, for the given font.
-#### ** Draw Text **
+==== Draw Text
```c
int16_t qp_drawtext(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str);
@@ -945,49 +970,49 @@ void keyboard_post_init_kb(void) {
}
```
-<!-- tabs:end -->
+:::::
-### ** Advanced Functions **
+===== Advanced Functions
-<!-- tabs:start -->
+:::::tabs
-#### ** Gettters **
+==== Getters
These functions allow external code to retrieve the current width, height, rotation, and drawing offsets.
-<!-- tabs:start -->
+::::tabs
-#### ** Width **
+=== Width
```c
uint16_t qp_get_width(painter_device_t device);
```
-#### ** Height **
+=== Height
```c
uint16_t qp_get_height(painter_device_t device);
```
-#### ** Rotation **
+=== Rotation
```c
painter_rotation_t qp_get_rotation(painter_device_t device);
```
-#### ** Offset X **
+=== Offset X
```c
uint16_t qp_get_offset_x(painter_device_t device);
```
-#### ** Offset Y **
+=== Offset Y
```c
uint16_t qp_get_offset_y(painter_device_t device);
```
-##### ** Everything **
+=== Everything
Convenience function to call all the previous ones at once.
Note: You can pass `NULL` for the values you are not interested in.
@@ -996,9 +1021,9 @@ Note: You can pass `NULL` for the values you are not interested in.
void qp_get_geometry(painter_device_t device, uint16_t *width, uint16_t *height, painter_rotation_t *rotation, uint16_t *offset_x, uint16_t *offset_y);
```
-<!-- tabs:end -->
+::::
-#### ** Set Viewport Offsets **
+==== Set Viewport Offsets
```c
void qp_set_viewport_offsets(painter_device_t device, uint16_t offset_x, uint16_t offset_y);
@@ -1006,7 +1031,7 @@ void qp_set_viewport_offsets(painter_device_t device, uint16_t offset_x, uint16_
The `qp_set_viewport_offsets` function can be used to offset all subsequent drawing operations. For example, if a display controller is internally 240x320, but the display panel is 240x240 and has a Y offset of 80 pixels, you could invoke `qp_set_viewport_offsets(display, 0, 80);` and the drawing positioning would be corrected.
-#### ** Set Viewport **
+==== Set Viewport
```c
bool qp_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom);
@@ -1014,7 +1039,7 @@ bool qp_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t
The `qp_viewport` function controls where raw pixel data is written to.
-#### ** Stream Pixel Data **
+==== Stream Pixel Data
```c
bool qp_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count);
@@ -1022,8 +1047,10 @@ bool qp_pixdata(painter_device_t device, const void *pixel_data, uint32_t native
The `qp_pixdata` function allows raw pixel data to be streamed to the display. It requires a native pixel count rather than the number of bytes to transfer, to ensure display panel data alignment is respected. E.g. for display panels using RGB565 internal format, sending 10 pixels will result in 20 bytes of transfer.
-!> Under normal circumstances, users will not need to manually call either `qp_viewport` or `qp_pixdata`. These allow for writing of raw pixel information, in the display panel's native format, to the area defined by the viewport.
+::: warning
+Under normal circumstances, users will not need to manually call either `qp_viewport` or `qp_pixdata`. These allow for writing of raw pixel information, in the display panel's native format, to the area defined by the viewport.
+:::
-<!-- tabs:end -->
+:::::
-<!-- tabs:end -->
+::::::