LVGL needs "toys"

LVGL needs to have some additional features added. I am not talking widgets. I am talking about low level rendering functions. Things like rendering an ellipsis, helper functions for getting the angle of a point in a circle/ellipsis, getting the distance between 2 points. Those kinds of things.

rendering a bezier is another.
gradients, (already has been discussed)

The more rendering ability there is the user will be able to design widgets of their own. what I call “bling” is why a user would pick one GUI framework over another. The more it has to offer is the one the user will usually pick to use.

make a repository for widgets so a user would be able to pick and choose what they want to add to LVGL. Kind of like the component system in the ESPIDF. the extras don’t have to be packaged with LVGL but an easy way to add things into LVGL would need to be available.

I mostly agree, but we need to structure these features carefully

  • utilities: (get angle, distance, etc) fine, 1-2 files with well-named functions.
  • rendering: we will utilize an SVG lib in v9 so the not super common things (ellipsis, bezier, etc) can be draw with it. Defining and SVG API is part of rendering update roadmap (see here).

and SVG should be rendered using beziers. That is what makes the curves. if you render an SVG using polygons you are going to need to use a hell of a lot of points to make a curved shape look curved. rendering an arc can also be done using a bezier with 4 points.

as far as utility functions go the ability to set a “Pen” or “Brush” could be really useful. a Pen would be an outline and the brush would be the background. the idea behind that is the ability to set a gradient as the brush or an image as a brush. the pen can have it’s width changed and also the style. dots, dashes, dots and dashes. A Pen can also have its ends changed to Rounded, squared, mitered (when meeting another line) that kind of thing.

a pen and a brush would each be a structure and the structures would get passed to the CTX and every time something is drawn they are used. If the user wants to change the pen or brush they are able to do so.

It is actually a better way to do it from a performance standpoint. Right now if I want to render a rectangle I have to initialize a lv_draw_rect_dsc_t structure which is where all the color information is stored. If I wanted to render an arc after that rectangle but using the same colors I am not able to just pass the information to the CTX in order to render the arc. I have to create a lv_draw_arc_dsc_t structure and pass that to the draw CTX with the color information replicated in that structure. Even tho I want to sue the same colors. So an additional bunch of bytes needs to be used for no real purpose.

Rendering a shadow would work under the same idea except a function would be called to render it. The brush would be used to render the shadow.

Here is the rect structure. There is a lot stuffed into that one structure that may or may not even get used by a user when rendering.

typedef struct {
    lv_coord_t radius;
    lv_blend_mode_t blend_mode;

    /*Background*/
    lv_opa_t bg_opa;
    lv_color_t bg_color;        /**< First element of a gradient is a color, so it maps well here*/
    lv_grad_dsc_t bg_grad;

    /*Background img*/
    const void * bg_img_src;
    const void * bg_img_symbol_font;
    lv_color_t bg_img_recolor;
    lv_opa_t bg_img_opa;
    lv_opa_t bg_img_recolor_opa;
    uint8_t bg_img_tiled;

    /*Border*/
    lv_color_t border_color;
    lv_coord_t border_width;
    lv_opa_t border_opa;
    uint8_t border_post : 1;        /*There is a border it will be drawn later.*/
    lv_border_side_t border_side : 5;

    /*Outline*/
    lv_color_t outline_color;
    lv_coord_t outline_width;
    lv_coord_t outline_pad;
    lv_opa_t outline_opa;

    /*Shadow*/
    lv_color_t shadow_color;
    lv_coord_t shadow_width;
    lv_coord_t shadow_ofs_x;
    lv_coord_t shadow_ofs_y;
    lv_coord_t shadow_spread;
    lv_opa_t shadow_opa;
} lv_draw_rect_dsc_t;

rendering a shadow should be a function. rendering the border would be done at the time the rect is rendered using the set Pen. image rendering is a function call, background get rendered at the time the rectangle is rendered.

This is pseudo code so as an example. Say the screen size is 480 x 320. the screen color is set to 0xFFFFFF

ctx = get_ctx();
lv_pen_t pen;
lv_brush_t brush;

lv_area_t rect;
rect.x1 = 156;
rect.y1 = 114;
rect.x2 = 321;
rect.y2 = 203;

pen.color = lv_color_hex(0x00FF00);
pen.opa = 125;
pen.style= LV_PEN_MITERED | LV_PEN_STYLE_SOLID
pen.width=4;

brush.color = lv_color_hex(0x0000FF);
brush.opa = 255;

lv_ctx_set_pen(ctx, pen);
lv_ctx_set_brush(ctx, brush);

// now we render the rectangle
lv_draw_rect(ctx, rect);

That produces this
image

Note how the border looks beveled. That is because the rect is rendered at the coords given and then the border is rendered using those coords so 1/2 of the pen width is over the background and the other 1/2 is not. Since you used an alpha value of 125 you end up with what looks like a beveled edge

we extend that code further to include

rect.x1 = rect.x1 + 2;
rect.y1 = rect.y1 + 2;
rect.x2 = rect.x2 - 2;
rect.y2 = rect.y2 - 2;


// rendering an image would use the brush as the mechanism to do a recolor., we do not want to recolor the image so set it to a transparent brush

lv_set_brush(ctx, LV_TRANSPARENT_BRUSH);

// render the image
lv_draw_img(ctx, rect, img_data);

and now you end up with this.

image

a function for rendering a rectangle with rounded corners

lv_draw_rounded_rect(ctx, rect, 10);

image

an arc in lvgl is really a partial circle so lets just call it a circle. a circle can be rendered 2 ways, using a center point and a radius or it can be rendered using a bounding box (rect) where it is going to use the smaller of the width or height to determine the radius and center. easiest way to go about this is going to be using a rect or an lv_area_t structure because that is a common thing to use. no need to make it more complex or add anything additional to render an arc. an “arc” that makes a complete circle would use the brush to fill in the circle otherwise only the pen is used so to

ctx = get_ctx();
lv_pen_t pen;

pen.color = lv_color_hex(0x0000FF);
pen.opa = 255;
pen.style= LV_PEN_ROUNDED_ENDS | LV_PEN_STYLE_SOLID
pen.width=20;

lv_ctx_set_pen(ctx, pen);
lv_set_brush(ctx, LV_TRANSPARENT_BRUSH);

lv_area_t rect;
rect.x1 = 140;
rect.y1 = 60;
rect.x2 = 340;
rect.y2 = 260;

lv_draw_arc(ctx, rect, 1350, 4050);

and the result is this.

image

allowing the objects to be reused by placing common parts into a structure that is able to be shared between all rendering functions is going to save resources. with manipulating how things are rendered like the rectangle you can actually kill 2 birds with one stone, the outline and the border is a good example if they are after making a beveled edge. even still by being able to set the background to a transparent brush a small change to the size of the rectangle being drawn they are able to render the outline pretty easily. You can also eliminate the repeated function calls for rendering by adding functions that render arrays. so for rendering rectangles an array of rectangles get passed and optionally an array of pens and/or an array of brushes.

To be honest I never really understood the point of the pen/brush concept. Why dashing is the pen’s property? Why isn’t the property of the guy who draws with the pen? I think it’s a confusing analogy.

Besides the naming, I have some technical comments too:

  • How to handle e.g. border with gradient with a pen? It’s not supported in LVGL now, but we can easily do it by adding a few fields to the descriptors.
  • In v9 we will separate rectangle drawing to: rectangle + border + shadow. The same border draw routing can be used for outline and border too.
  • Pen/Brush is different philosophy than what we have now, but I can’t see their advantages. Getting all the style parameters really takes some time, but we need to this anyway. The writing/reading this structs is clearly not a bottleneck.

In summary think pen/brush a limitation with no clear advantage.

I was using the pen and brush as an example.

the brush is a flood fill tool in reality. you can use whatever terms you like, call it a paint brush and a paint can. One gets used for large areas and the other for small detail work. A gradient should be able to be rendered as a single line of pixels (pen) or an entire display (brush). can you use a pen to fill an entire screen like a brush does… yes you can. can you use a brush to make a single line of pixels, yes you can. It’s more about code organization and providing a way to accomplish 2 things at the same time with less work for the user to do. it is simpler for a bordered around a rectangle to be rendered at the same time as the background of the rectangle then for them to be done separately. Right now if I want to render a rectangle that has red as the background and blue as the border and then right afterwards I want to render a line that is blue I have to create a new type of descriptor and load the color I want into it. that takes time and memory to do. were as with the pen and brush style of rendering I can set the pen color a single time and the brush color a single time and render both, rendering a line doesn’t use the color that is set for the brush it only uses the color set for the pen.

Now it’s funny that you specifically mentioned dashing being the property of a pen. Guess what lv_draw_line_dsc_t is? it’s a Pen. and it is the only drawing structure that has the ability to to “dash”. dash is not an option for the border of a rectangle using lv_draw_rect_dsc_t.

LVGL is partially set up on the pen and brush kind of a system.

typedef struct {
    lv_coord_t radius;
    lv_blend_mode_t blend_mode;       *****BRUSH*****

    /*Background*/
    lv_opa_t bg_opa;       *****BRUSH*****
    lv_color_t bg_color;        *****BRUSH*****
    lv_grad_dsc_t bg_grad;      *****BRUSH*****

    /*Background img*/
    const void * bg_img_src;       *****BRUSH*****
    const void * bg_img_symbol_font;       *****BRUSH*****
    lv_color_t bg_img_recolor;       *****BRUSH*****
    lv_opa_t bg_img_opa;       *****BRUSH*****
    lv_opa_t bg_img_recolor_opa;       *****BRUSH*****
    uint8_t bg_img_tiled;       *****BRUSH*****

    /*Border*/
    lv_color_t border_color;    *****PEN*****
    lv_coord_t border_width;    *****PEN*****
    lv_opa_t border_opa;    *****PEN*****
    uint8_t border_post : 1;
    lv_border_side_t border_side : 5;

    /*Outline*/
    lv_color_t outline_color;    *****PEN*****
    lv_coord_t outline_width;    *****PEN*****
    lv_coord_t outline_pad;
    lv_opa_t outline_opa;    *****PEN*****

    /*Shadow*/
    lv_color_t shadow_color;    *****BRUSH*****
    lv_coord_t shadow_width;     *****BRUSH*****
    lv_coord_t shadow_ofs_x; 
    lv_coord_t shadow_ofs_y;
    lv_coord_t shadow_spread; 
    lv_opa_t shadow_opa;         *****BRUSH****
} lv_draw_rect_dsc_t;


typedef struct {
    lv_color_t color;    *****PEN****
    lv_coord_t width;    *****PEN****
    lv_coord_t dash_width;    *****PEN****
    lv_coord_t dash_gap;    *****PEN****
    lv_opa_t opa;    *****PEN****
    lv_blend_mode_t blend_mode  : 2;        *****PEN****
    uint8_t round_start : 1;    *****PEN****
    uint8_t round_end   : 1;    *****PEN****
    uint8_t raw_end     : 1;        *****PEN****
} lv_draw_line_dsc_t;


typedef struct {
    lv_color_t color;        *****PEN****
    lv_coord_t width;        *****PEN****
    uint16_t start_angle;
    uint16_t end_angle; 
    const void * img_src;    *****BRUSH****
    lv_opa_t opa;        *****PEN****
    lv_blend_mode_t blend_mode  : 2;        *****PEN****
    uint8_t rounded : 1;        *****PEN****
} lv_draw_arc_dsc_t;


after a simple reordering into this

typedef struct {
    lv_color_t color; 
    lv_coord_t width;
    lv_coord_t dash_width;
    lv_coord_t dash_gap; 
    lv_opa_t opa;
    lv_blend_mode_t blend_mode  : 2;
    uint8_t round_start : 1; 
    uint8_t round_end   : 1; 
    uint8_t raw_end     : 1;
} lv_draw_line_dsc_t;


typedef struct {
    lv_blend_mode_t blend_mode;
    lv_opa_t opa;
    lv_color_t color;
    lv_grad_dsc_t grad;
    const void * img_src; 
    const void * img_symbol_font;
    lv_color_t img_recolor; 
    lv_opa_t img_opa;
    lv_opa_t img_recolor_opa;
    uint8_t img_tiled; 
} lv_draw_fill_dsc_t


typedef struct {
    lv_coord_t radius;

    /*Background / Background img*/
    lv_draw_fill_dsc_t *background;

    /*Border*/
    lv_draw_line_dsc_t* border;
    uint8_t border_post : 1;
    lv_border_side_t border_side : 5;

    /*Outline*/
    lv_draw_line_dsc_t *outline;
    lv_coord_t outline_pad;

    /*Shadow*/
    lv_draw_fill_dsc_t *shadow;
    lv_coord_t shadow_ofs_x;
    lv_coord_t shadow_ofs_y;
    lv_coord_t shadow_spread;
} lv_draw_rect_dsc_t;


typedef struct {
    lv_draw_line_dsc_t *arc;
    uint16_t start_angle;
    uint16_t end_angle; 
    lv_draw_fill_dsc_t background;
} lv_draw_arc_dsc_t;

and now what do you have? the ability to create a single “brush” and a single “pen” and use it on more than a single item being rendered. Nothing has been removed. only a reordering and that reordering can save memory and also processor time if a user is rendering to the screen or to a canvas/buffer.

say I wanted to render a rectangle that has blue fill a green outline and a blue shadow. what is the memory use using the existing system and what would be the memory use using what is outline above?

You can call the bits anything that you want it doesn’t matter but to me it looks like the pen and brush kind of a system it’s just that the pens and brushed are defined in the same container instead of separately.

and in reality the code actually should read

typedef struct {
    uint16_t radius;
    lv_blend_mode_t blend_mode;       *****BRUSH*****

    /*Background*/
    lv_opa_t bg_opa;       *****BRUSH*****
    lv_color_t bg_color;        *****BRUSH*****
    lv_grad_dsc_t bg_grad;      *****BRUSH*****

    /*Background img*/
    const void * bg_img_src;       *****BRUSH*****
    const void * bg_img_symbol_font;       *****BRUSH*****
    lv_color_t bg_img_recolor;       *****BRUSH*****
    lv_opa_t bg_img_opa;       *****BRUSH*****
    lv_opa_t bg_img_recolor_opa;       *****BRUSH*****
    uint8_t bg_img_tiled;       *****BRUSH*****

    /*Border*/
    lv_color_t border_color;    *****PEN*****
    uint8_t border_width;    *****PEN*****
    lv_opa_t border_opa;    *****PEN*****
    uint8_t border_post : 1;
    lv_border_side_t border_side : 5;

    /*Outline*/
    lv_color_t outline_color;    *****PEN*****
    uint8_t  outline_width;    *****PEN*****
    uint16_t outline_pad;
    lv_opa_t outline_opa;    *****PEN*****

    /*Shadow*/
    lv_color_t shadow_color;    *****BRUSH*****
    uint16_t shadow_width;     *****BRUSH*****
    uint16_t shadow_ofs_x; 
    uint16_t shadow_ofs_y;
    uint16_t shadow_spread; 
    lv_opa_t shadow_opa;         *****BRUSH****
} lv_draw_rect_dsc_t;


typedef struct {
    lv_color_t color;    *****PEN****
    uint16_t width;    *****PEN****
    uint16_t dash_width;    *****PEN****
    uint16_t dash_gap;    *****PEN****
    lv_opa_t opa;    *****PEN****
    lv_blend_mode_t blend_mode  : 2;        *****PEN****
    uint8_t round_start : 1;    *****PEN****
    uint8_t round_end   : 1;    *****PEN****
    uint8_t raw_end     : 1;        *****PEN****
} lv_draw_line_dsc_t;


typedef struct {
    lv_color_t color;        *****PEN****
    uint16_t width;        *****PEN****
    uint16_t start_angle;
    uint16_t end_angle; 
    const void * img_src;    *****BRUSH****
    lv_opa_t opa;        *****PEN****
    lv_blend_mode_t blend_mode  : 2;        *****PEN****
    uint8_t rounded : 1;        *****PEN****
} lv_draw_arc_dsc_t;

the use of lv_coord_t for the widths and things of that nature is wasteful if large coords needs to be used because of say the roller having a lot of items in it. You end up with a lot of bytes that will never get used considering the highest definition displays that are on the market right now don’t come anywhere near 65535 so having the ability to set a line to 4294967295 pixels wide just seems nuts

You asked how to handle a pen being able to draw a gradient. That is a fairly simple answer because after all what is a gradient? it is a series of colors. a pen has the capability to handle colors yes? you could give the ability to give a brush or a pen an array of colors, if it has a single color in that array then only that color gets used. if there is more than one color in the array then it would be rendered as a gradient.

So to phrase it in a generic way what we should have is a generic lv_line_style_dsc_t and an lv_fill_style_dsc_t and use these in the draw_dscs, right? I think it’s doable but there are to many moving parts in my head regarding the parallel rendering which completely changes how LVGL approaches rendering. Let’s get back to this topic what we have something more stable.

It’s an idea. It also makes sense to have it that way. You mentioned the sharing of common colors when using styles so why not take it another step further and apply it to the rendering as well. It also would give the ability to set say a dashed border for a rectangle.

it’s an idea that you should keep in mind as it could simplify something that you are doing now with the parallel rendering. rendering multiple things at the same time would be easier to do if they are the same color no?

I have been putting a lot of thought to your parallel rendering and because of the memory limitations there is an inability to create actual “layers” which would be multiple buffers of the same size and assign a task to handle each layer. that leaves you with having to either delegate sections of the buffer a task would be responsible for rendering to. don’t know how that would really work. the other option is to find all of components on the same layer that use the same color and set tasks to render each of the things that need to be rendered. since they are on the same layer rendering the same color writing to a buffer that is static in size if 2 of the items cross each other it’s not going to cause any kind of an oddity in the color when they overlap.

I think the greater importance thing is the wide use of lv_coord_t and it being used in places it really shouldn’t be used in. there is a need to have the position coordinates be a larger value because of issue that can arise from large scrolling type lists. there is no need to change everything to use a 32bit number when an 8 or a 16 would be more than large enough. wasted resources. You can make the memory footprint even smaller in LVGL. by a default lv_coord_t is a 16bit unsigned integer. for a border of a rectangle on a display that has say an 800 x 600 resolution to have a width of 255 is not something you are going to see being done, ever… having the ability to set the border width to 65535 is one byte wasted. and if large coords are used you are now up to 3 bytes wasted. Remember that is for each place the border width gets used.

1 Like