forked from mbk-lab/rui_orig
2
0
Fork 0

Compare commits

...

50 Commits
0.10 ... main

Author SHA1 Message Date
Роман Бородин d8ab76da20 описание 2024-04-12 17:06:55 +03:00
Роман Бородин d4831bb68d upstream: Bug fixing 2024-04-12 16:17:56 +03:00
Роман Бородин 7b3caa64bd upstream: Bug fixing 2024-04-12 15:58:11 +03:00
Роман Бородин 08dc88ca86 upstream: Bug fixing 2024-04-12 15:52:58 +03:00
Роман Бородин 882e9bcce3 имя модуля и новый websocket 2024-04-06 23:21:54 +03:00
Роман Бородин e41e61966f обработчик http 2024-04-06 23:12:40 +03:00
Alexei Anoshenko d7f0dd4358 Update README.md 2023-11-12 17:25:49 +02:00
anoshenko d2204a8627 Changed the type of KeyEvent.Code field 2023-05-29 17:19:47 +03:00
Alexei Anoshenko c44093e6f4 Added key codes 2023-05-29 15:28:22 +03:00
anoshenko 600f56d8ea Added support for height and width range in media styles 2023-05-26 20:08:25 +03:00
anoshenko 3d44aa3ba3 Bug fixing 2023-05-18 12:26:54 +03:00
anoshenko dc2ea14cac Added SetHotKey function to Session interface 2023-05-15 16:19:33 +03:00
anoshenko ab421b4c32 Added PopupButtonType 2023-05-15 15:27:37 +03:00
anoshenko 904859ff6e Added ReloadCell and ReloadTableViewCell functions 2023-05-14 17:53:26 +03:00
anoshenko 3bf3b9c2ba Update theme.go 2023-05-10 15:52:56 +03:00
anoshenko 1cf0ee0bae Bug fixing 2023-05-07 20:58:51 +03:00
anoshenko b15305d727 Changed FocusView function 2023-05-07 19:44:28 +03:00
anoshenko 7c0449775c Bug fixing 2023-05-07 19:26:02 +03:00
anoshenko 4ec3fe2ff2 Optimisation 2023-05-04 16:45:03 +03:00
anoshenko b6b5183f21 Bug fixing 2023-05-04 13:37:26 +03:00
anoshenko f75435eb6c Bug fixing 2023-05-03 15:16:03 +03:00
anoshenko b7a0aa9a6d Bug fixing 2023-05-02 18:58:57 +03:00
anoshenko 43a8b9fe58 Bug fixing & optimisation 2023-05-02 17:20:01 +03:00
anoshenko 43cb889ab1 Bug fixing 2023-05-02 14:49:35 +03:00
anoshenko dec7e723ef Bug fixing 2023-04-25 17:33:08 +03:00
anoshenko 677e0e8692 Update CHANGELOG.md 2023-04-25 17:30:21 +03:00
anoshenko a83d634f54 Added "tooltip" property 2023-04-25 17:20:47 +03:00
anoshenko ac8bb47677 Added the ViewIndex function to the ViewsContainer interface 2023-04-24 21:13:45 +03:00
anoshenko f3c9bf7f56 Changed the main TimePicker listener 2023-04-23 18:54:53 +03:00
anoshenko 4e7dd37f6a Changed the main DatePicker listener 2023-04-23 18:47:07 +03:00
anoshenko 05fa725003 Changed the main NumberPicker listener 2023-04-23 18:39:25 +03:00
anoshenko 763de29698 Changed the main ColorPicker listener 2023-04-23 18:27:04 +03:00
anoshenko 2a480cc6ac Changed the main DropDownList and EditView listener 2023-04-23 18:07:01 +03:00
anoshenko eac3379fb1 Added "outline-offset" property 2023-04-23 13:41:26 +03:00
anoshenko 372f5971e8 Fixed SvgImageView 2023-04-23 12:49:32 +03:00
anoshenko d1b30c56da Optimisation 2023-04-23 12:32:03 +03:00
Alexei Anoshenko 62910bf41f Update resources.go 2023-04-20 09:16:16 +03:00
Alexei Anoshenko 644ec6d8c5 Added InlineImageFromResource function 2023-01-24 17:41:56 -05:00
Alexei Anoshenko 42b34ac4df Added SvgImageView 2023-01-24 17:06:36 -05:00
Alexei Anoshenko b99c39f947
Merge pull request #3 from anoshenko/0.11.0
0.11.0
2023-01-03 15:08:09 +03:00
anoshenko 01e2e2e00b Added "column-span-all" property 2023-01-03 14:56:57 +03:00
anoshenko c7a7b3ed1e Added "column-fill" property 2022-12-23 17:27:14 +03:00
anoshenko 3993dbad20 Updated readme 2022-12-20 18:55:54 +03:00
anoshenko 3c3271663d Added "tabindex" property 2022-12-20 18:38:39 +03:00
anoshenko c5485c27c2 Updated readme 2022-12-20 17:48:00 +03:00
anoshenko 60783f2f22 Updated readme 2022-12-19 19:18:35 +03:00
anoshenko e5842180ef Added mix-blend-mode and background-blend-mode 2022-12-19 18:31:35 +03:00
anoshenko 9fe570ec22 Added "order" property 2022-12-18 18:37:36 +03:00
anoshenko c31b2f9d8c PropertyWithTag method of DataObject renamed to PropertyByTag 2022-12-18 18:22:58 +03:00
anoshenko d3002ced0e Bug fixing 2022-11-23 15:10:29 +03:00
65 changed files with 3568 additions and 1133 deletions

View File

@ -1,6 +1,35 @@
# v.10.0 # v0.13.0
* Added SetHotKey function to Session interface
* Added ViewIndex function to ViewsContainer interface
* Added ReloadCell function to TableView interface
* Added ReloadTableViewCell function
* Added "tooltip" property and GetTooltip function
* Added "outline-offset" property and GetOutlineOffset function
* Changed the main event listener format for "drop-down-event", "edit-text-changed",
"color-changed", "number-changed", "date-changed", and "time-changed" events.
Old format is "<listener>(<view>, <new value>)", new format is "<listener>(<view>, <new value>, <old value>)"
* Changed FocusView function
* Added support for height and width range in media styles.
Changed MediaStyle, SetMediaStyle, and MediaStyles functions of Theme interface
* Bug fixing
# v0.12.0
* Added SvgImageView
* Added InlineImageFromResource function
# v0.11.0
* Added "tabindex", "order", "column-fill", "column-span-all", "background-blend-mode", and "mix-blend-mode" properties
* Added GetTabIndex, GetOrder, GetColumnFill, IsColumnSpanAll, GetBackgroundBlendMode, and GetMixBlendMode functions
* ClientItem, SetClientItem, and RemoveAllClientItems method added to Session interface
* PropertyWithTag method of DataObject interface renamed to PropertyByTag
# v0.10.0
* The Canvas.TextWidth method replaced by Canvas.TextMetrics * The Canvas.TextWidth method replaced by Canvas.TextMetrics
* Added support of WebAssembly
# v0.9.0 # v0.9.0
@ -72,7 +101,7 @@
# v0.2.0 # v0.2.0
* Added "animation" and "transition" properties, Animation interface, animation events * Added "animation" and "transition" properties, Animation interface, animation events
* Renamed ColorPropery constant to ColorTag * Renamed ColorProperty constant to ColorTag
* Updated readme * Updated readme
* Added the Animation example to the demo * Added the Animation example to the demo
* Bug fixing * Bug fixing

File diff suppressed because it is too large Load Diff

360
README.md
View File

@ -745,7 +745,7 @@ If all the lines of the frame are the same, then the following properties can be
|-----------|----------|----------|---------------------| |-----------|----------|----------|---------------------|
| "style" | Style | int | Border line style | | "style" | Style | int | Border line style |
| "width" | Width | SizeUnit | Border line width | | "width" | Width | SizeUnit | Border line width |
| "color" | Color | Color | Border line color | | "color" | ColorTag | Color | Border line color |
Example Example
@ -769,7 +769,7 @@ equivalent to
view.Set(rui.Border, NewBorder(rui.Params{ view.Set(rui.Border, NewBorder(rui.Params{
rui.Style : rui.SolidBorder, rui.Style : rui.SolidBorder,
rui.Width : rui.Px(1), rui.Width : rui.Px(1),
rui.ColorProperty: rui.Black, rui.ColorTag: rui.Black,
})) }))
The BorderProperty interface can be converted to a ViewBorders structure using the Border function. The BorderProperty interface can be converted to a ViewBorders structure using the Border function.
@ -833,9 +833,51 @@ equivalent to
view.Set(rui.Border, NewBorder(rui.Params{ view.Set(rui.Border, NewBorder(rui.Params{
rui.Style : rui.SolidBorder, rui.Style : rui.SolidBorder,
rui.Width : rui.Px(1), rui.Width : rui.Px(1),
rui.ColorProperty: rui.Black, rui.ColorTag: rui.Black,
})) }))
### "outline" and "outline-offset" properties
The "outline" property defines the border outside the View.
A frame line is described by three attributes: line style, thickness, and color.
The "outline" property is similar to the "border" property. The three main differences between an "outline" frame and a "border" frame are:
1) the "border" frame is always located inside the boundaries of the View, and the "outline" frame can be located both inside the View and outside it;
2) all sides of the "outline" frame are the same, while the sides of the "border" frame can have different color, style and thickness.
3) "border" thickness is added to "padding" and "outline" thickness does not affect "padding".
The value of the "border" property is stored as an OutlineProperty interface that implements the Properties interface (see above).
OutlineProperty can contain the following properties:
| Property | Constant | Type | Description |
|-----------|----------|----------|---------------------|
| "style" | Style | int | Border line style |
| "width" | Width | SizeUnit | Border line width |
| "color" | ColorTag | Color | Border line color |
Line style can take the following values:
| Value | Constant | Name | Description |
|:-----:|------------|----------|---------------------|
| 0 | NoneLine | "none" | No frame |
| 1 | SolidLine | "solid" | Solid line |
| 2 | DashedLine | "dashed" | Dashed line |
| 3 | DottedLine | "dotted" | Dotted line |
| 4 | DoubleLine | "double" | Double solid line |
All other style values are ignored.
The NewOutline function is used to create the OutlineProperty interface.
By default, the inner border of the "outline" border is the same as the border of the View (i.e. the border is drawn around the View).
To change this behavior, use the "outline-offset" SizeUnit property (OutlineOffset constant).
This property defines the distance between the inner border of the frame and the border of the View.
A positive value moves the frame away from the View's boundaries, while a negative value forces the frame to be inside the View
(in this case, the frame is drawn on top of the contents of the View).
### "radius" property ### "radius" property
The "radius" property sets the elliptical corner radius of the View. Radii are specified by the RadiusProperty The "radius" property sets the elliptical corner radius of the View. Radii are specified by the RadiusProperty
@ -970,7 +1012,7 @@ The shadow has the following properties:
| Property | Constant | Type | Description | | Property | Constant | Type | Description |
|-----------------|---------------|----------|-----------------------------------------------------------------------| |-----------------|---------------|----------|-----------------------------------------------------------------------|
| "color" | ColorProperty | Color | Shadow color | | "color" | ColorTag | Color | Shadow color |
| "inset" | Inset | bool | true - the shadow inside the View, false - outside | | "inset" | Inset | bool | true - the shadow inside the View, false - outside |
| "x-offset" | XOffset | SizeUnit | Offset the shadow along the X axis | | "x-offset" | XOffset | SizeUnit | Offset the shadow along the X axis |
| "y-offset" | YOffset | SizeUnit | Offset the shadow along the Y axis | | "y-offset" | YOffset | SizeUnit | Offset the shadow along the Y axis |
@ -989,7 +1031,7 @@ The NewShadowWithParams function is used when constants must be used as paramete
For example: For example:
shadow := NewShadowWithParams(rui.Params{ shadow := NewShadowWithParams(rui.Params{
rui.ColorProperty : "@shadowColor", rui.ColorTag : "@shadowColor",
rui.BlurRadius: 8.0, rui.BlurRadius: 8.0,
rui.Dilation : 16.0, rui.Dilation : 16.0,
}) })
@ -1195,6 +1237,47 @@ Can be one of the following int values:
* ImageVerticalAlign, * ImageVerticalAlign,
### "background-blend-mode" property
The "background-blend-mode" int property (BackgroundBlendMode constant)sets how an view's background images should blend
with each other and with the view's background color.
Can take one of the following values:
| Constant | Value | Name | Description |
|-----------------|:-----:|--------------|------------------------------------------------------------------|
| BlendNormal | 0 | "normal" | The final color is the top color, regardless of what the bottom color is. The effect is like two opaque pieces of paper overlapping. |
| BlendMultiply | 1 | "multiply" | The final color is the result of multiplying the top and bottom colors. A black layer leads to a black final layer, and a white layer leads to no change. The effect is like two images printed on transparent film overlapping. |
| BlendScreen | 2 | "screen" | The final color is the result of inverting the colors, multiplying them, and inverting that value. A black layer leads to no change, and a white layer leads to a white final layer. The effect is like two images shone onto a projection screen. |
| BlendOverlay | 3 | "overlay" | The final color is the result of multiply if the bottom color is darker, or screen if the bottom color is lighter. This blend mode is equivalent to hard-light but with the layers swapped. |
| BlendDarken | 4 | "darken" | The final color is composed of the darkest values of each color channel. |
| BlendLighten | 5 | "lighten" | The final color is composed of the lightest values of each color channel. |
| BlendColorDodge | 6 | "color-dodge"| The final color is the result of dividing the bottom color by the inverse of the top color. A black foreground leads to no change. A foreground with the inverse color of the backdrop leads to a fully lit color. This blend mode is similar to screen, but the foreground need only be as light as the inverse of the backdrop to create a fully lit color. |
| BlendColorBurn | 7 | "color-burn" | The final color is the result of inverting the bottom color, dividing the value by the top color, and inverting that value. A white foreground leads to no change. A foreground with the inverse color of the backdrop leads to a black final image. This blend mode is similar to multiply, but the foreground need only be as dark as the inverse of the backdrop to make the final image black. |
| BlendHardLight | 8 | "hard-light" | The final color is the result of multiply if the top color is darker, or screen if the top color is lighter. This blend mode is equivalent to overlay but with the layers swapped. The effect is similar to shining a harsh spotlight on the backdrop. |
| BlendSoftLight | 9 | "soft-light" | The final color is similar to hard-light, but softer. This blend mode behaves similar to hard-light. The effect is similar to shining a diffused spotlight on the backdrop*.* |
| BlendDifference | 10 | "difference" | The final color is the result of subtracting the darker of the two colors from the lighter one. A black layer has no effect, while a white layer inverts the other layer's color. |
| BlendExclusion | 11 | "exclusion" | The final color is similar to difference, but with less contrast. As with difference, a black layer has no effect, while a white layer inverts the other layer's color. |
| BlendHue | 12 | "hue" | The final color has the hue of the top color, while using the saturation and luminosity of the bottom color. |
| BlendSaturation | 13 | "saturation" | The final color has the saturation of the top color, while using the hue and luminosity of the bottom color. A pure gray backdrop, having no saturation, will have no effect. |
| BlendColor | 14 | "color" | The final color has the hue and saturation of the top color, while using the luminosity of the bottom color. The effect preserves gray levels and can be used to colorize the foreground. |
| BlendLuminosity | 15 | "luminosity" | The final color has the luminosity of the top color, while using the hue and saturation of the bottom color. This blend mode is equivalent to BlendColor, but with the layers swapped. |
You can get the value of this property using the function
func GetBackgroundBlendMode(view View, subviewID ...string) int
### "mix-blend-mode" property
The "mix-blend-mode" int property (MixBlendMode constant) sets how a view's content should blend
with the content of the view's parent and the view's background.
Possible values of this property are similar to the values of the "background-blend-mode" property (see above)
You can get the value of this property using the function
func GetMixBlendMode(view View, subviewID ...string) int
### "clip" property ### "clip" property
The "clip" property (Clip constant) of the ClipShape type specifies the crop area. The "clip" property (Clip constant) of the ClipShape type specifies the crop area.
@ -1257,13 +1340,29 @@ The textual description of the polygonal cropping area is in the following forma
### "opacity" property ### "opacity" property
The "opacity" property (constant Opacity) of the float64 type sets the transparency of the View. Valid values are from 0 to 1. The "opacity" property (Opacity constant) of the float64 type sets the transparency of the View. Valid values are from 0 to 1.
Where 1 - View is fully opaque, 0 - fully transparent. Where 1 - View is fully opaque, 0 - fully transparent.
You can get the value of this property using the function You can get the value of this property using the function
func GetOpacity(view View, subviewID ...string) float64 func GetOpacity(view View, subviewID ...string) float64
### "tabindex" property
The "tabindex" int property (TabIndex constant) determines whether this View should participate in sequential navigation
throughout the page using the keyboard and in what order. It can take one of the following types of values:
* negative value - View can be selected with the mouse or touch, but does not participate in sequential navigation;
* 0 - View can be selected and reached using sequential navigation, the order of navigation is determined by the browser (usually in order of addition);
* positive value - the element will be reached (and selected) using sequential navigation, and navigation is performed by ascending "tabindex" value.
If multiple elements contain the same "tabindex" value, navigation is done in the order in which they were added.
You can get the value of this property using the function
func GetTabIndex(viewView, subviewID ...string) int
### "z-index" property ### "z-index" property
The "z-index" property (constant ZIndex) of type int defines the position of the element and its children along the z-axis. The "z-index" property (constant ZIndex) of type int defines the position of the element and its children along the z-axis.
@ -1354,6 +1453,12 @@ It also helps to voice the interface to systems for people with disabilities:
| 18 | "blockquote" | Quote. Changes the style of the text | | 18 | "blockquote" | Quote. Changes the style of the text |
| 19 | "code" | Program code. Changes the style of the text | | 19 | "code" | Program code. Changes the style of the text |
### "tooltip" property
The "tooltip" string property (Tooltip constant) specifies the tooltip text.
Tooltip pops up when hovering the mouse cursor.
You can use html tags when formatting the tooltip text.
### Text properties ### Text properties
All properties listed in this section are inherited, i.e. the property will apply All properties listed in this section are inherited, i.e. the property will apply
@ -1518,7 +1623,7 @@ You can get the value of this property using the function
#### "text-weight" property #### "text-weight" property
Свойство "text-weight" (константа TextWeight) - свойство типа int устанавливает начертание шрифта. Допустимые значения: The "text-weight" int property (TextWeight constant) sets the font style. Valid values:
| Value | Constant | Common name of the face | | Value | Constant | Common name of the face |
|:-----:|----------------|---------------------------| |:-----:|----------------|---------------------------|
@ -1553,7 +1658,7 @@ To create a ViewShadow for the text shadow, the following functions are used:
The NewShadowWithParams function is used when constants must be used as parameters. For example: The NewShadowWithParams function is used when constants must be used as parameters. For example:
shadow := NewShadowWithParams(rui.Params{ shadow := NewShadowWithParams(rui.Params{
rui.ColorProperty : "@shadowColor", rui.ColorTag : "@shadowColor",
rui.BlurRadius: 8.0, rui.BlurRadius: 8.0,
}) })
@ -2171,6 +2276,15 @@ If index is less than 0, then to the beginning of the list.
This function removes the View from the given position and returns it. This function removes the View from the given position and returns it.
If index points outside the bounds of the list, then nothing is removed, and the function returns nil. If index points outside the bounds of the list, then nothing is removed, and the function returns nil.
ViewIndex(view View) int
This function returns the index of the child View, or -1 if there is no such View in the container.
It is often used in conjunction with RemoveView if the index of the child View is unknown:
if index := container.ViewIndex(view); index >= 0 {
container.RemoveView(index)
}
## ListLayout ## ListLayout
ListLayout is a container that implements the ViewsContainer interface. To create it, use the function ListLayout is a container that implements the ViewsContainer interface. To create it, use the function
@ -2238,6 +2352,17 @@ alignment of items in the list. Valid values:
The "list-row-gap" and "list-column-gap" SizeUnit properties (ListRowGap and ListColumnGap constants) The "list-row-gap" and "list-column-gap" SizeUnit properties (ListRowGap and ListColumnGap constants)
allow you to set the distance between the rows and columns of the container, respectively. The default is 0px. allow you to set the distance between the rows and columns of the container, respectively. The default is 0px.
### "order"
The "order" property (Order constant) of type int is used by Views placed in a ListLayout or GridLayout container (see below),
to change its position in the container.
The "order" property defines the order used to place the View in the container. The elements are arranged in ascending order by their order value.
Elements with the same order value are placed in the order in which they were added to the container.
The default value is 0. Therefore, negative values of the "order" property must be used to place the View at the beginning.
Note: The "order" property only affects the visual order of the elements, not the logical order or tabs.
## GridLayout ## GridLayout
GridLayout is a container that implements the ViewsContainer interface. To create it, use the function GridLayout is a container that implements the ViewsContainer interface. To create it, use the function
@ -2420,15 +2545,15 @@ The value of the "column-separator" property is stored as the ColumnSeparatorPro
which implements the Properties interface (see above). ColumnSeparatorProperty can contain the following properties: which implements the Properties interface (see above). ColumnSeparatorProperty can contain the following properties:
| Property | Constant | Type | Description | | Property | Constant | Type | Description |
|----------|---------------|----------|----------------| |----------|-----------|----------|----------------|
| "style" | Style | int | Line style | | "style" | Style | int | Line style |
| "width" | Width | SizeUnit | Line thickness | | "width" | Width | SizeUnit | Line thickness |
| "color" | ColorProperty | Color | Line color | | "color" | ColorTag | Color | Line color |
Line style can take the following values: Line style can take the following values:
| Value | Constant | Name | Description | | Value | Constant | Name | Description |
|:-----:|------------|----------| ------------------| |:-----:|------------|----------|-------------------|
| 0 | NoneLine | "none" | No frame | | 0 | NoneLine | "none" | No frame |
| 1 | SolidLine | "solid" | Solid line | | 1 | SolidLine | "solid" | Solid line |
| 2 | DashedLine | "dashed" | Dashed line | | 2 | DashedLine | "dashed" | Dashed line |
@ -2479,9 +2604,23 @@ equivalent to
view.Set(rui.ColumnSeparator, ColumnSeparatorProperty(rui.Params{ view.Set(rui.ColumnSeparator, ColumnSeparatorProperty(rui.Params{
rui.Style : rui.SolidBorder, rui.Style : rui.SolidBorder,
rui.Width : rui.Px(1), rui.Width : rui.Px(1),
rui.ColorProperty: rui.Black, rui.ColorTag: rui.Black,
})) }))
### "column-fill" property
The "column-fill" int property (ColumnFill constant) controls how an ColumnLayout's contents are balanced when broken into columns.
Valid values:
| Value | Constant | Name | Description |
|:-----:|-------------------|-----------|------------------------------------------------------------|
| 0 | ColumnFillBalance | "balance" | Content is equally divided between columns (default value) |
| 1 | ColumnFillAuto | "auto" | Columns are filled sequentially. Content takes up only the room it needs, possibly resulting in some columns remaining empty |
You can get the value of this property using the function
func GetColumnFill(view View, subviewID ...string) int
### "avoid-break" property ### "avoid-break" property
When forming columns, ColumnLayout can break some types of View, so that the beginning When forming columns, ColumnLayout can break some types of View, so that the beginning
@ -2497,6 +2636,20 @@ You can get the value of this property using the function
func GetAvoidBreak(view View, subviewID ...string) bool func GetAvoidBreak(view View, subviewID ...string) bool
### "column-span-all" property
The "column-span-all" bool property (ColumnSpanAll constant) is set for Views placed in the ColumnLayout.
If this property is set to true, then the View expands to the full width of the ColumnLayout, covering all columns.
Such a View will, as it were, break the container.
Typically, this property is used for headers.
The default value is "false".
You can get the value of this property using the function
func IsColumnSpanAll(view View, subviewID ...string) bool
## StackLayout ## StackLayout
StackLayout is a container that implements the ViewsContainer interface. StackLayout is a container that implements the ViewsContainer interface.
@ -2739,7 +2892,7 @@ It determines how the text is cut if it goes out of bounds.
This property of type int can take the following values This property of type int can take the following values
| Value | Constant | Name | Cropping Text | | Value | Constant | Name | Cropping Text |
|:-----:|----------------------| -----------|-------------------------------------------------------------| |:-----:|----------------------|------------|-------------------------------------------------------------|
| 0 | TextOverflowClip | "clip" | Text is clipped at the border (default) | | 0 | TextOverflowClip | "clip" | Text is clipped at the border (default) |
| 1 | TextOverflowEllipsis | "ellipsis" | At the end of the visible part of the text '…' is displayed | | 1 | TextOverflowEllipsis | "ellipsis" | At the end of the visible part of the text '…' is displayed |
@ -2752,7 +2905,25 @@ To create an ImageView function is used:
func NewImageView(session Session, params Params) ImageView func NewImageView(session Session, params Params) ImageView
The displayed image is specified by the string property "src" (Source constant). The displayed image is specified by the string property "src" (Source constant).
As a value, this property is assigned either the name of the image in the "images" folder of the resources, or the url of the image. As a value, this property is assigned either the name of the image in the "images" folder of the resources, or the url of the image, or inline-image.
An inline-image is the content of an image file encoded in base64 format.
To get an inline-image from the application resources, use the function
func InlineImageFromResource(filename string) (string, bool)
Inline-images must be used in WebAssembly applications
if you want to host images in resources rather than on an external server.
Inline-images can cause app freezes in Safari and should be avoided.
Example
if runtime.GOOS == "js" {
if image, ok := rui.InlineImageFromResource("image.png"); ok {
view.Set(rui.Source, image)
}
} else {
view.Set(rui.Source, "image.png")
}
ImageView allows you to display different images depending on screen density ImageView allows you to display different images depending on screen density
(See section "Images for screens with different pixel densities"). (See section "Images for screens with different pixel densities").
@ -2818,6 +2989,39 @@ The following functions can be used to retrieve ImageView property values:
func GetImageViewVerticalAlign(view View, subviewID ...string) int func GetImageViewVerticalAlign(view View, subviewID ...string) int
func GetImageViewHorizontalAlign(view View, subviewID ...string) int func GetImageViewHorizontalAlign(view View, subviewID ...string) int
## SvgImageView
The SvgImageView element extending the View interface is designed to display svg images.
To create an SvgImageView function is used:
func NewSvgImageView(session Session, params Params) ImageView
The image to be displayed is specified by the string property "content" (constant Content).
The value of this property can be assigned
* the image file name in the images folder of the resources;
* image url;
* content of the svg image.
Examples
rui.Set(rootView, "iconView", rui.Content, "icon.svg")
rui.Set(rootView, "iconView", rui.Content, `<svg width="32" height="32" version="1.1" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-499.08 -247.12)">
<path d="m508.08 249.12 14 14-14 14" fill="none" stroke="#0f0" stroke-linecap="round" stroke-width="1px"/>
</g>
</svg>`)
Regardless of how you determined the property of "Content" to the client is always transmitted the contents of the SVG image. For example, if you set the image as follows
rui.Set(rootView, "iconView", rui.Content, "icon.svg")
then the program will first upload the contents of the "icon.svg" file to the memory,
and then transmit this contents to the client as the value of the "content" property.
This allows you to include SVG images in the resources of a WebAssembly application.
## EditView ## EditView
The EditView element is a test editor and extends the View interface. The EditView element is a test editor and extends the View interface.
@ -2882,13 +3086,21 @@ The following functions can be used to get the values of the properties of an Ed
The "edit-text-changed" event (EditTextChangedEvent constant) is used to track changes to the text. The "edit-text-changed" event (EditTextChangedEvent constant) is used to track changes to the text.
The main event listener has the following format: The main event listener has the following format:
func(EditView, string) func(EditView, string, string)
where the second argument is the new text value where the second argument is the new text value, the third argument is the previous text value.
Additional event listeners can have the following format
func(EditView, newText string)
func(newText, oldText string)
func(newText string)
func(EditView)
func()
You can get the current list of text change listeners using the function You can get the current list of text change listeners using the function
func GetTextChangedListeners(view View, subviewID ...string) []func(EditView, string) func GetTextChangedListeners(view View, subviewID ...string) []func(EditView, string, string)
## NumberPicker ## NumberPicker
@ -2944,13 +3156,21 @@ You can read the values of these properties using the functions:
The "number-changed" event (NumberChangedEvent constant) is used to track the change in the entered value. The "number-changed" event (NumberChangedEvent constant) is used to track the change in the entered value.
The main event listener has the following format: The main event listener has the following format:
func(picker NumberPicker, newValue float64) func(picker NumberPicker, newValue, oldValue float64)
where the second argument is the new value where the second argument is the new value, the third argument is the previous value.
Additional event listeners can have the following format
func(picker NumberPicker, newValue float64)
func(newValue, oldValue float64)
func(newValue float64)
func(picker NumberPicker)
func()
You can get the current list of value change listeners using the function You can get the current list of value change listeners using the function
func GetNumberChangedListeners(view View, subviewID ...string) []func(NumberPicker, float64) func GetNumberChangedListeners(view View, subviewID ...string) []func(NumberPicker, float64, float64)
## DatePicker ## DatePicker
@ -2991,13 +3211,21 @@ You can read the values of these properties using the functions:
The "date-changed" event (DateChangedEvent constant) is used to track the change in the entered value. The "date-changed" event (DateChangedEvent constant) is used to track the change in the entered value.
The main event listener has the following format: The main event listener has the following format:
func(picker DatePicker, newDate time.Time) func(picker DatePicker, newDate, oldDate time.Time)
where the second argument is the new date value where the second argument is the new date value, the third argument is the previous date value.
Additional event listeners can have the following format
func(picker DatePicker, newDate time.Time)
func(newDate, oldDate time.Time)
func(newDate time.Time)
func(picker DatePicker)
func()
You can get the current list of date change listeners using the function You can get the current list of date change listeners using the function
func GetDateChangedListeners(view View, subviewID ...string) []func(DatePicker, time.Time) func GetDateChangedListeners(view View, subviewID ...string) []func(DatePicker, time.Time, time.Time)
## TimePicker ## TimePicker
@ -3038,13 +3266,21 @@ You can read the values of these properties using the functions:
The "time-changed" event (TimeChangedEvent constant) is used to track the change in the entered value. The "time-changed" event (TimeChangedEvent constant) is used to track the change in the entered value.
The main event listener has the following format: The main event listener has the following format:
func(picker TimePicker, newTime time.Time) func(picker TimePicker, newTime, oldTime time.Time)
where the second argument is the new time value where the second argument is the new time value, the third argument is the previous time value.
Additional event listeners can have the following format
func(picker TimePicker, newTime time.Time)
func(newTime, oldTime time.Time)
func(newTime time.Time)
func(picker TimePicker)
func()
You can get the current list of date change listeners using the function You can get the current list of date change listeners using the function
func GetTimeChangedListeners(view View, subviewID ...string) []func(TimePicker, time.Time) func GetTimeChangedListeners(view View, subviewID ...string) []func(TimePicker, time.Time, time.Time)
## ColorPicker ## ColorPicker
@ -3068,13 +3304,21 @@ The value of the property "color-picker-value" can also be read using the functi
The "color-changed" event (ColorChangedEvent constant) is used to track the change in the selected color. The "color-changed" event (ColorChangedEvent constant) is used to track the change in the selected color.
The main event listener has the following format: The main event listener has the following format:
func(picker ColorPicker, newColor Color) func(picker ColorPicker, newColor, oldColor Color)
where the second argument is the new color value where the second argument is the new color value, the third argument is the previous color value.
Additional event listeners can have the following format
func(picker ColorPicker, newColor string)
func(newColor, oldColor string)
func(newColor string)
func(picker ColorPicker)
func()
You can get the current list of date change listeners using the function You can get the current list of date change listeners using the function
func GetColorChangedListeners(view View, subviewID ...string) []func(ColorPicker, Color) func GetColorChangedListeners(view View, subviewID ...string) []func(ColorPicker, Color, Color)
## FilePicker ## FilePicker
@ -3204,11 +3448,19 @@ The main event listener has the following format:
func(list DropDownList, newCurrent int) func(list DropDownList, newCurrent int)
where the second argument is the index of the selected item where the second argument is the index of the selected item, the third argument is the previous index value.
Additional event listeners can have the following format
func(list DropDownList, newCurrent int)
func(newCurrent, oldCurrent int)
func(newCurrent int)
func(list DropDownList)
func()
You can get the current list of date change listeners using the function You can get the current list of date change listeners using the function
func GetDropDownListeners(view View, subviewID ...string) []func(DropDownList, int) func GetDropDownListeners(view View, subviewID ...string) []func(DropDownList, int, int)
## ProgressBar ## ProgressBar
@ -3511,6 +3763,18 @@ The "content" property can also be assigned the following data types
[][]any and [][]string are converted to a TableAdapter when assigned. [][]any and [][]string are converted to a TableAdapter when assigned.
If the elements of the table change during operation, then to update the contents of the table,
you must call one of the two methods of the TableView interface
* ReloadTableData()
* ReloadCell(row, column int)
The ReloadTableData method updates the entire table, while ReloadCell updates the contents of only a specific table cell.
Global functions can be used to call the ReloadTableData and ReloadCell methods
func ReloadTableViewData(view View, subviewID ...string) bool
func ReloadTableViewCell(row, column int, view View, subviewID ...string) bool
### "cell-style" property ### "cell-style" property
The "cell-style" property (CellStyle constant) is used to customize the appearance of a table cell. The "cell-style" property (CellStyle constant) is used to customize the appearance of a table cell.
@ -4136,7 +4400,7 @@ AudioPlayer and VideoPlayer are elements for audio and video playback.
Both elements implement the MediaPlayer interface. Most of the properties and all events Both elements implement the MediaPlayer interface. Most of the properties and all events
of AudioPlayer and VideoPlayer are implemented through the MediaPlayer. of AudioPlayer and VideoPlayer are implemented through the MediaPlayer.
### Свойство "src" ### "src" property
The "src" property (Source constant) specifies one or more media sources. The "src" property can take on the following types: The "src" property (Source constant) specifies one or more media sources. The "src" property can take on the following types:
@ -4907,6 +5171,9 @@ It is used when the client needs to transfer a file from the server.
* DownloadFileData(filename string, data [] byte) downloads (saves) on the client side a file * DownloadFileData(filename string, data [] byte) downloads (saves) on the client side a file
with a specified name and specified content. Typically used to transfer a file generated in server memory. with a specified name and specified content. Typically used to transfer a file generated in server memory.
* SetHotKey(keyCode KeyCode, controlKeys ControlKeyMask, fn func(Session)) - sets the function that will be called
when the given hotkey is pressed.
## Resource description format ## Resource description format
Application resources (themes, views, translations) can be described as text (utf-8). Application resources (themes, views, translations) can be described as text (utf-8).
@ -5194,9 +5461,17 @@ In addition to general styles, you can add styles for specific work modes. To do
* ":portrait" or ":landscape" are respectively styles for portrait or landscape mode of the program. * ":portrait" or ":landscape" are respectively styles for portrait or landscape mode of the program.
Attention means the aspect ratio of the program window, not the screen. Attention means the aspect ratio of the program window, not the screen.
* ":width< size >" are styles for a screen whose width does not exceed the specified size in logical pixels. * ":width<min-width>-<max-width>" - styles for a screen whose width is in the range specified in logical pixels.
* ":height< size >" are styles for a screen whose height does not exceed the specified size in logical pixels. * ":width<max-width>" - styles for a screen whose width does not exceed the specified value in logical pixels.
* ":width<min-width>-" - styles for a screen whose width is greater than the specified value in logical pixels.
* ":height<min-height>-<max-height>" - styles for a screen whose height is in the range specified in logical pixels.
* ":height<max-height>" - styles for a screen whose height does not exceed the specified value in logical pixels.
* ":height<minimum-height>-" - styles for a screen whose height is greater than the specified value in logical pixels.
For example For example
@ -5228,7 +5503,19 @@ For example
width = 100%, width = 100%,
height = 50%, height = 50%,
}, },
] ],
styles:portrait:width320-640 = [
samplePage {
width = 90%,
height = 60%,
},
],
styles:portrait:width640- = [
samplePage {
width = 80%,
height = 70%,
},
],
} }
## Standard constants and styles ## Standard constants and styles
@ -5296,6 +5583,9 @@ System color constants that you can override:
| ruiPopupTextColor | Popup text color | | ruiPopupTextColor | Popup text color |
| ruiPopupTitleColor | Popup title background color | | ruiPopupTitleColor | Popup title background color |
| ruiPopupTitleTextColor | Popup Title Text Color | | ruiPopupTitleTextColor | Popup Title Text Color |
| ruiTooltipBackground | Tooltip background color |
| ruiTooltipTextColor | Tooltip text color |
| ruiTooltipShadowColor | Tooltip shadow color |
Constants that you can override: Constants that you can override:

View File

@ -85,7 +85,7 @@ const (
LinearTiming = "linear" LinearTiming = "linear"
) )
// StepsTiming return a timing function along stepCount stops along the transition, diplaying each stop for equal lengths of time // StepsTiming return a timing function along stepCount stops along the transition, displaying each stop for equal lengths of time
func StepsTiming(stepCount int) string { func StepsTiming(stepCount int) string {
return "steps(" + strconv.Itoa(stepCount) + ")" return "steps(" + strconv.Itoa(stepCount) + ")"
} }
@ -130,7 +130,7 @@ type Animation interface {
writeTransitionString(tag string, buffer *strings.Builder) writeTransitionString(tag string, buffer *strings.Builder)
animationCSS(session Session) string animationCSS(session Session) string
transitionCSS(buffer *strings.Builder, session Session) transitionCSS(buffer *strings.Builder, session Session)
hasAnimatedPropery() bool hasAnimatedProperty() bool
animationName() string animationName() string
} }
@ -160,7 +160,7 @@ func NewAnimation(params Params) Animation {
return animation return animation
} }
func (animation *animationData) hasAnimatedPropery() bool { func (animation *animationData) hasAnimatedProperty() bool {
props := animation.getRaw(PropertyTag) props := animation.getRaw(PropertyTag)
if props == nil { if props == nil {
ErrorLog("There are no animated properties.") ErrorLog("There are no animated properties.")

View File

@ -30,7 +30,7 @@ const (
AnimationStartEvent = "animation-start-event" AnimationStartEvent = "animation-start-event"
// AnimationEndEvent is the constant for "animation-end-event" property tag. // AnimationEndEvent is the constant for "animation-end-event" property tag.
// The "animation-end-event" is fired when aт фnimation has completed. // The "animation-end-event" is fired when an animation has completed.
// If the animation aborts before reaching completion, such as if the element is removed // If the animation aborts before reaching completion, such as if the element is removed
// or the animation is removed from the element, the "animation-end-event" is not fired. // or the animation is removed from the element, the "animation-end-event" is not fired.
AnimationEndEvent = "animation-end-event" AnimationEndEvent = "animation-end-event"
@ -91,7 +91,10 @@ func transitionEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range transitionEvents { for tag, js := range transitionEvents {
if value := view.getRaw(tag); value != nil { if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View, string)); ok && len(listeners) > 0 { if listeners, ok := value.([]func(View, string)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `) buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
} }
} }
} }
@ -157,7 +160,10 @@ func animationEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range animationEvents { for tag, js := range animationEvents {
if value := view.getRaw(tag); value != nil { if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View)); ok && len(listeners) > 0 { if listeners, ok := value.([]func(View)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `) buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
} }
} }
} }

View File

@ -156,14 +156,7 @@ func (app *application) socketReader(bridge webBridge) {
ErrorLog(`"session" key not found`) ErrorLog(`"session" key not found`)
} }
answer := "" bridge.writeMessage("restartSession();")
if session, answer = app.startSession(obj, events, bridge); session != nil {
if !bridge.writeMessage(answer) {
return
}
session.onStart()
go sessionEventHandler(session, events, bridge)
}
case "answer": case "answer":
session.handleAnswer(obj) session.handleAnswer(obj)
@ -295,7 +288,7 @@ func OpenBrowser(url string) bool {
case "linux": case "linux":
for _, provider := range []string{"xdg-open", "x-www-browser", "www-browser"} { for _, provider := range []string{"xdg-open", "x-www-browser", "www-browser"} {
if _, err = exec.LookPath(provider); err == nil { if _, err = exec.LookPath(provider); err == nil {
if exec.Command(provider, url).Start(); err == nil { if err = exec.Command(provider, url).Start(); err == nil {
return true return true
} }
} }

View File

@ -149,7 +149,6 @@ func (app *wasmApp) init(params AppParams) {
div = document.Call("createElement", "div") div = document.Call("createElement", "div")
div.Set("className", "ruiPopupLayer") div.Set("className", "ruiPopupLayer")
div.Set("id", "ruiPopupLayer") div.Set("id", "ruiPopupLayer")
div.Set("onclick", "clickOutsidePopup(event)")
div.Set("style", "visibility: hidden;") div.Set("style", "visibility: hidden;")
body.Call("appendChild", div) body.Call("appendChild", div)

View File

@ -52,9 +52,31 @@ function sessionInfo() {
message += ",pixel-ratio=" + pixelRatio; message += ",pixel-ratio=" + pixelRatio;
} }
if (localStorage.length > 0) {
message += ",storage="
lead = "_{"
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i)
var value = localStorage.getItem(key)
key = key.replaceAll(/\\/g, "\\\\")
key = key.replaceAll(/\"/g, "\\\"")
key = key.replaceAll(/\'/g, "\\\'")
value = value.replaceAll(/\\/g, "\\\\")
value = value.replaceAll(/\"/g, "\\\"")
value = value.replaceAll(/\'/g, "\\\'")
message += lead + "\"" + key + "\"=\"" + value + "\""
lead = ","
}
message += "}"
}
return message + "}"; return message + "}";
} }
function restartSession() {
sendMessage( sessionInfo() );
}
function getIntAttribute(element, tag) { function getIntAttribute(element, tag) {
let value = element.getAttribute(tag); let value = element.getAttribute(tag);
if (value) { if (value) {
@ -224,22 +246,26 @@ function enterOrSpaceKeyClickEvent(event) {
function activateTab(layoutId, tabNumber) { function activateTab(layoutId, tabNumber) {
var element = document.getElementById(layoutId); var element = document.getElementById(layoutId);
if (element) { if (element) {
var currentTabId = element.getAttribute("data-current"); var currentNumber = element.getAttribute("data-current");
var newTabId = layoutId + '-' + tabNumber; if (currentNumber != tabNumber) {
if (currentTabId != newTabId) { function setTab(number, styleProperty, display) {
function setTab(tabId, styleProperty, display) { var tab = document.getElementById(layoutId + '-' + number);
var tab = document.getElementById(tabId);
if (tab) { if (tab) {
tab.className = element.getAttribute(styleProperty); tab.className = element.getAttribute(styleProperty);
var page = document.getElementById(tab.getAttribute("data-view")); var page = document.getElementById(tab.getAttribute("data-view"));
if (page) { if (page) {
page.style.display = display; page.style.display = display;
} }
return
}
var page = document.getElementById(layoutId + "-page" + number);
if (page) {
page.style.display = display;
} }
} }
setTab(currentTabId, "data-inactiveTabStyle", "none") setTab(currentNumber, "data-inactiveTabStyle", "none")
setTab(newTabId, "data-activeTabStyle", ""); setTab(tabNumber, "data-activeTabStyle", "");
element.setAttribute("data-current", newTabId); element.setAttribute("data-current", tabNumber);
scanElementsSize() scanElementsSize()
} }
} }
@ -285,8 +311,19 @@ function keyEvent(element, event, tag) {
message += ",timeStamp=" + event.timeStamp; message += ",timeStamp=" + event.timeStamp;
} }
if (event.key) { if (event.key) {
switch (event.key) {
case '"':
message += ",key=`\"`";
break
case '\\':
message += ",key=`\\`";
break
default:
message += ",key=\"" + event.key + "\""; message += ",key=\"" + event.key + "\"";
} }
}
if (event.code) { if (event.code) {
message += ",code=\"" + event.code + "\""; message += ",code=\"" + event.code + "\"";
} }
@ -408,6 +445,7 @@ function mouseOutEvent(element, event) {
function clickEvent(element, event) { function clickEvent(element, event) {
mouseEvent(element, event, "click-event") mouseEvent(element, event, "click-event")
event.preventDefault(); event.preventDefault();
event.stopPropagation();
} }
function doubleClickEvent(element, event) { function doubleClickEvent(element, event) {
@ -571,6 +609,10 @@ function selectDropDownListItem(elementId, number) {
function listItemClickEvent(element, event) { function listItemClickEvent(element, event) {
event.stopPropagation(); event.stopPropagation();
if (element.getAttribute("data-disabled") == "1") {
return
}
var selected = false; var selected = false;
if (element.classList) { if (element.classList) {
const focusStyle = getListFocusedItemStyle(element); const focusStyle = getListFocusedItemStyle(element);
@ -688,6 +730,9 @@ function findRightListItem(list, x, y) {
var count = list.childNodes.length; var count = list.childNodes.length;
for (var i = 0; i < count; i++) { for (var i = 0; i < count; i++) {
var item = list.childNodes[i]; var item = list.childNodes[i];
if (item.getAttribute("data-disabled") == "1") {
continue;
}
if (item.offsetLeft >= x) { if (item.offsetLeft >= x) {
if (result) { if (result) {
var result_dy = Math.abs(result.offsetTop - y); var result_dy = Math.abs(result.offsetTop - y);
@ -709,6 +754,9 @@ function findLeftListItem(list, x, y) {
var count = list.childNodes.length; var count = list.childNodes.length;
for (var i = 0; i < count; i++) { for (var i = 0; i < count; i++) {
var item = list.childNodes[i]; var item = list.childNodes[i];
if (item.getAttribute("data-disabled") == "1") {
continue;
}
if (item.offsetLeft < x) { if (item.offsetLeft < x) {
if (result) { if (result) {
var result_dy = Math.abs(result.offsetTop - y); var result_dy = Math.abs(result.offsetTop - y);
@ -730,6 +778,9 @@ function findTopListItem(list, x, y) {
var count = list.childNodes.length; var count = list.childNodes.length;
for (var i = 0; i < count; i++) { for (var i = 0; i < count; i++) {
var item = list.childNodes[i]; var item = list.childNodes[i];
if (item.getAttribute("data-disabled") == "1") {
continue;
}
if (item.offsetTop < y) { if (item.offsetTop < y) {
if (result) { if (result) {
var result_dx = Math.abs(result.offsetLeft - x); var result_dx = Math.abs(result.offsetLeft - x);
@ -751,6 +802,9 @@ function findBottomListItem(list, x, y) {
var count = list.childNodes.length; var count = list.childNodes.length;
for (var i = 0; i < count; i++) { for (var i = 0; i < count; i++) {
var item = list.childNodes[i]; var item = list.childNodes[i];
if (item.getAttribute("data-disabled") == "1") {
continue;
}
if (item.offsetTop >= y) { if (item.offsetTop >= y) {
if (result) { if (result) {
var result_dx = Math.abs(result.offsetLeft - x); var result_dx = Math.abs(result.offsetLeft - x);
@ -843,6 +897,33 @@ function listViewKeyDownEvent(element, event) {
if (item && item !== current) { if (item && item !== current) {
selectListItem(element, item, true); selectListItem(element, item, true);
} }
} else {
switch (key) {
case " ":
case "Enter":
case "ArrowLeft":
case "ArrowUp":
case "ArrowRight":
case "ArrowDown":
case "Home":
case "End":
case "PageUp":
case "PageDown":
var list = element.childNodes[0];
var count = list.childNodes.length;
for (var i = 0; i < count; i++) {
var item = list.childNodes[i];
if (item.getAttribute("data-disabled") == "1") {
continue;
}
selectListItem(element, item, true);
return;
}
break;
default:
return;
}
} }
} }
@ -940,8 +1021,8 @@ function radioButtonKeyClickEvent(element, event) {
function editViewInputEvent(element) { function editViewInputEvent(element) {
var text = element.value var text = element.value
text = text.replace(/\\/g, "\\\\") text = text.replaceAll(/\\/g, "\\\\")
text = text.replace(/\"/g, "\\\"") text = text.replaceAll(/\"/g, "\\\"")
var message = "textChanged{session=" + sessionID + ",id=" + element.id + ",text=\"" + text + "\"}" var message = "textChanged{session=" + sessionID + ",id=" + element.id + ",text=\"" + text + "\"}"
sendMessage(message); sendMessage(message);
} }
@ -1147,7 +1228,7 @@ function loadImage(url) {
img.addEventListener("error", function(event) { img.addEventListener("error", function(event) {
var message = "imageError{session=" + sessionID + ",url=\"" + url + "\""; var message = "imageError{session=" + sessionID + ",url=\"" + url + "\"";
if (event && event.message) { if (event && event.message) {
var text = event.message.replace(new RegExp("\"", 'g'), "\\\"") var text = event.message.replaceAll(new RegExp("\"", 'g'), "\\\"")
message += ",message=\"" + text + "\""; message += ",message=\"" + text + "\"";
} }
sendMessage(message + "}") sendMessage(message + "}")
@ -1178,7 +1259,7 @@ function loadInlineImage(url, content) {
img.addEventListener("error", function(event) { img.addEventListener("error", function(event) {
var message = "imageError{session=" + sessionID + ",url=\"" + url + "\""; var message = "imageError{session=" + sessionID + ",url=\"" + url + "\"";
if (event && event.message) { if (event && event.message) {
var text = event.message.replace(new RegExp("\"", 'g'), "\\\"") var text = event.message.replaceAll(new RegExp("\"", 'g'), "\\\"")
message += ",message=\"" + text + "\""; message += ",message=\"" + text + "\"";
} }
sendMessage(message + "}") sendMessage(message + "}")
@ -1187,11 +1268,6 @@ function loadInlineImage(url, content) {
img.src = content; img.src = content;
} }
function clickOutsidePopup(e) {
sendMessage("clickOutsidePopup{session=" + sessionID + "}")
e.stopPropagation();
}
function clickClosePopup(element, e) { function clickClosePopup(element, e) {
var popupId = element.getAttribute("data-popupId"); var popupId = element.getAttribute("data-popupId");
sendMessage("clickClosePopup{session=" + sessionID + ",id=" + popupId + "}") sendMessage("clickClosePopup{session=" + sessionID + ",id=" + popupId + "}")
@ -1339,7 +1415,7 @@ function mediaSetVolume(elementId, volume) {
} }
} }
function startDowndload(url, filename) { function startDownload(url, filename) {
var element = document.getElementById("ruiDownloader"); var element = document.getElementById("ruiDownloader");
if (element) { if (element) {
element.href = url; element.href = url;
@ -1408,6 +1484,24 @@ function tableViewBlurEvent(element, event) {
} }
} }
function setTableCellCursorByID(tableID, row, column) {
var table = document.getElementById(tableID);
if (table) {
if (!setTableCellCursor(table, row, column)) {
const focusStyle = getTableFocusedItemStyle(table);
const oldCellID = table.getAttribute("data-current");
if (oldCellID) {
const oldCell = document.getElementById(oldCellID);
if (oldCell && oldCell.classList) {
oldCell.classList.remove(focusStyle);
oldCell.classList.remove(getTableSelectedItemStyle(table));
}
table.removeAttribute("data-current");
}
}
}
}
function setTableCellCursor(element, row, column) { function setTableCellCursor(element, row, column) {
const cellID = element.id + "-" + row + "-" + column; const cellID = element.id + "-" + row + "-" + column;
var cell = document.getElementById(cellID); var cell = document.getElementById(cellID);
@ -1495,6 +1589,8 @@ function tableViewCellKeyDownEvent(element, event) {
case "End": case "End":
case "PageUp": case "PageUp":
case "PageDown": case "PageDown":
event.stopPropagation();
event.preventDefault();
const rows = element.getAttribute("data-rows"); const rows = element.getAttribute("data-rows");
const columns = element.getAttribute("data-columns"); const columns = element.getAttribute("data-columns");
if (rows && columns) { if (rows && columns) {
@ -1505,8 +1601,6 @@ function tableViewCellKeyDownEvent(element, event) {
column = 0; column = 0;
while (columns < columnCount) { while (columns < columnCount) {
if (setTableCellCursor(element, row, column)) { if (setTableCellCursor(element, row, column)) {
event.stopPropagation();
event.preventDefault();
return; return;
} }
column++; column++;
@ -1514,8 +1608,6 @@ function tableViewCellKeyDownEvent(element, event) {
row++; row++;
} }
} }
event.stopPropagation();
event.preventDefault();
break; break;
} }
return; return;
@ -1583,6 +1675,24 @@ function tableViewCellKeyDownEvent(element, event) {
} }
} }
function setTableRowCursorByID(tableID, row) {
var table = document.getElementById(tableID);
if (table) {
if (!setTableRowCursor(table, row)) {
const focusStyle = getTableFocusedItemStyle(table);
const oldRowID = table.getAttribute("data-current");
if (oldRowID) {
const oldRow = document.getElementById(oldRowID);
if (oldRow && oldRow.classList) {
oldRow.classList.remove(focusStyle);
oldRow.classList.remove(getTableSelectedItemStyle(table));
}
table.removeAttribute("data-current");
}
}
}
}
function setTableRowCursor(element, row) { function setTableRowCursor(element, row) {
const tableRowID = element.id + "-" + row; const tableRowID = element.id + "-" + row;
var tableRow = document.getElementById(tableRowID); var tableRow = document.getElementById(tableRowID);
@ -1597,7 +1707,6 @@ function setTableRowCursor(element, row) {
if (oldRow && oldRow.classList) { if (oldRow && oldRow.classList) {
oldRow.classList.remove(focusStyle); oldRow.classList.remove(focusStyle);
oldRow.classList.remove(getTableSelectedItemStyle(element)); oldRow.classList.remove(getTableSelectedItemStyle(element));
} }
} }
@ -1842,3 +1951,152 @@ function getCanvasContext(elementId) {
} }
return null; return null;
} }
function localStorageSet(key, value) {
try {
localStorage.setItem(key, value)
} catch (err) {
sendMessage("storageError{session=" + sessionID + ", error=`" + err + "`}")
}
}
function localStorageClear() {
try {
localStorage.setItem(key, value)
} catch (err) {
sendMessage("storageError{session=" + sessionID + ", error=`" + err + "`}")
}
}
function showTooltip(element, tooltip) {
const layer = document.getElementById("ruiTooltipLayer");
if (!layer) {
return;
}
layer.style.left = "0px";
layer.style.right = "0px";
var tooltipBox = document.getElementById("ruiTooltipText");
if (tooltipBox) {
tooltipBox.innerHTML = tooltip;
}
var left = element.offsetLeft;
var top = element.offsetTop;
var width = element.offsetWidth;
var height = element.offsetHeight;
var parent = element.offsetParent;
while (parent) {
left += parent.offsetLeft;
top += parent.offsetTop;
width = parent.offsetWidth;
height = parent.offsetHeight;
parent = parent.offsetParent;
}
if (element.offsetWidth >= tooltipBox.offsetWidth) {
layer.style.left = left + "px";
layer.style.justifyItems = "start";
} else {
const rightOff = width - (left + element.offsetWidth);
if (left > rightOff) {
if (width - left < tooltipBox.offsetWidth) {
layer.style.right = rightOff + "px";
layer.style.justifyItems = "end";
} else {
layer.style.left = (left - rightOff) + "px";
layer.style.right = "0px";
layer.style.justifyItems = "center";
}
} else {
if (width - rightOff < tooltipBox.offsetWidth) {
layer.style.left = left + "px";
layer.style.justifyItems = "start";
} else {
layer.style.right = (rightOff - left) + "px";
layer.style.justifyItems = "center";
}
}
}
const bottomOff = height - (top + element.offsetHeight);
var arrow = document.getElementById("ruiTooltipTopArrow");
if (bottomOff < arrow.offsetHeight + tooltipBox.offsetHeight) {
if (arrow) {
arrow.style.visibility = "hidden";
}
arrow = document.getElementById("ruiTooltipBottomArrow");
if (arrow) {
arrow.style.visibility = "visible";
}
layer.style.top = "0px";
layer.style.bottom = height - top - arrow.offsetHeight / 2 + "px";
layer.style.gridTemplateRows = "1fr auto auto"
} else {
if (arrow) {
arrow.style.visibility = "visible";
}
layer.style.top = top + element.offsetHeight - arrow.offsetHeight / 2 + "px";
layer.style.bottom = "0px";
layer.style.gridTemplateRows = "auto auto 1fr"
arrow = document.getElementById("ruiTooltipBottomArrow");
if (arrow) {
arrow.style.visibility = "hidden";
}
}
layer.style.visibility = "visible";
layer.style.opacity = 1;
}
function mouseEnterEvent(element, event) {
event.stopPropagation();
let tooltip = element.getAttribute("data-tooltip");
if (tooltip) {
showTooltip(element, tooltip);
}
sendMessage("mouse-enter{session=" + sessionID + ",id=" + element.id + mouseEventData(element, event) + "}");
}
function mouseLeaveEvent(element, event) {
event.stopPropagation();
if (element.getAttribute("data-tooltip")) {
const layer = document.getElementById("ruiTooltipLayer");
if (layer) {
layer.style.opacity = 0;
layer.style.visibility = "hidden";
}
}
sendMessage("mouse-leave{session=" + sessionID + ",id=" + element.id + mouseEventData(element, event) + "}");
}
function hideTooltip() {
const layer = document.getElementById("ruiTooltipLayer");
if (layer) {
layer.style.opacity = 0;
layer.style.visibility = "hidden";
}
}
function stopEventPropagation(element, event) {
event.stopPropagation();
}
function setCssVar(tag, value) {
const root = document.querySelector(':root');
if (root) {
root.style.setProperty(tag, value);
}
}

View File

@ -8,6 +8,13 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
:root {
--tooltip-arrow-size: 6px;
--tooltip-background: white;
--tooltip-text-color: black;
--tooltip-shadow-color: gray;
}
body { body {
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;
@ -76,7 +83,54 @@ ul:focus {
left: 0px; left: 0px;
} }
.ruiTooltipLayer {
display: grid;
grid-template-rows: 1fr auto 1fr;
justify-items: center;
align-items: center;
position: absolute;
top: 0px;
bottom: 0px;
right: 0px;
left: 0px;
transition: opacity 0.5s ease-out;
filter: drop-shadow(0px 0px 2px var(--tooltip-shadow-color));
}
.ruiTooltipTopArrow {
grid-row-start: 1;
grid-row-end: 2;
border-width: var(--tooltip-arrow-size);
border-style: solid;
border-color: transparent transparent var(--tooltip-background) transparent;
margin-left: 12px;
margin-right: 12px;
}
.ruiTooltipBottomArrow {
grid-row-start: 3;
grid-row-end: 4;
border-width: var(--tooltip-arrow-size);
border-style: solid;
border-color: var(--tooltip-background) transparent transparent transparent;
margin-left: 12px;
margin-right: 12px;
}
.ruiTooltipText {
grid-row-start: 2;
grid-row-end: 3;
padding: 4px 8px 4px 8px;
margin-left: 8px;
margin-right: 8px;
background-color: var(--tooltip-background);
color: var(--tooltip-text-color);
/*box-shadow: 0px 0px 4px 2px #8888;*/
border-radius: 4px;
}
.ruiView { .ruiView {
box-sizing: border-box;
} }
.ruiAbsoluteLayout { .ruiAbsoluteLayout {
@ -112,16 +166,17 @@ ul:focus {
} }
.ruiImageView { .ruiImageView {
display: block;
}
.ruiSvgImageView {
display: grid; display: grid;
} }
.ruiListView { .ruiListView {
overflow: auto; overflow: auto;
/*
display: flex;
align-content: stretch;
*/
} }
/* /*
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
body { body {

View File

@ -5,8 +5,6 @@ import (
"strings" "strings"
) )
var wasmMediaResources = false
//go:embed app_scripts.js //go:embed app_scripts.js
var defaultScripts string var defaultScripts string
@ -75,9 +73,15 @@ func getStartPage(buffer *strings.Builder, params AppParams, addScripts string)
buffer.WriteString(addScripts) buffer.WriteString(addScripts)
buffer.WriteString(`</script> buffer.WriteString(`</script>
</head> </head>
<body> <body id="body" onkeydown="keyDownEvent(this, event)">
<div class="ruiRoot" id="ruiRootView"></div> <div class="ruiRoot" id="ruiRootView"></div>
<div class="ruiPopupLayer" id="ruiPopupLayer" style="visibility: hidden;" onclick="clickOutsidePopup(event)"></div> <div class="ruiPopupLayer" id="ruiPopupLayer" style="visibility: hidden; isolation: isolate;"></div>
<div class="ruiTooltipLayer" id="ruiTooltipLayer" style="visibility: hidden; opacity: 0;">
<div id="ruiTooltipText" class="ruiTooltipText"></div>
<div id="ruiTooltipTopArrow" class="ruiTooltipTopArrow"></div>
<div id="ruiTooltipBottomArrow" class="ruiTooltipBottomArrow"></div>
</div>
<a id="ruiDownloader" download style="display: none;"></a> <a id="ruiDownloader" download style="display: none;"></a>
</body>`) </body>`)
} }

View File

@ -209,7 +209,7 @@ func (gradient *backgroundConicGradient) parseGradientText(value string) []Backg
for i, element := range elements { for i, element := range elements {
var ok bool var ok bool
if vector[i], ok = gradient.stringToGradientPoint(strings.Trim(element, " ")); !ok { if vector[i], ok = gradient.stringToGradientPoint(strings.Trim(element, " ")); !ok {
ErrorLogF(`Ivalid %d element of the conic gradient: "%s"`, i, element) ErrorLogF(`Invalid %d element of the conic gradient: "%s"`, i, element)
return nil return nil
} }
} }
@ -240,7 +240,7 @@ func (gradient *backgroundConicGradient) setGradient(value any) bool {
return true return true
} }
ErrorLogF(`Ivalid conic gradient: "%s"`, value) ErrorLogF(`Invalid conic gradient: "%s"`, value)
return false return false
case []BackgroundGradientAngle: case []BackgroundGradientAngle:
@ -252,7 +252,7 @@ func (gradient *backgroundConicGradient) setGradient(value any) bool {
for i, point := range value { for i, point := range value {
if point.Color == nil { if point.Color == nil {
ErrorLogF("Ivalid %d element of the conic gradient: Color is nil", i) ErrorLogF("Invalid %d element of the conic gradient: Color is nil", i)
return false return false
} }
} }

View File

@ -99,7 +99,7 @@ func (gradient *backgroundGradient) parseGradientText(value string) []Background
points := make([]BackgroundGradientPoint, count) points := make([]BackgroundGradientPoint, count)
for i, element := range elements { for i, element := range elements {
if !points[i].setValue(element) { if !points[i].setValue(element) {
ErrorLogF(`Ivalid %d element of the conic gradient: "%s"`, i, element) ErrorLogF(`Invalid %d element of the conic gradient: "%s"`, i, element)
return nil return nil
} }
} }

View File

@ -286,7 +286,7 @@ func (border *borderProperty) setBorderObject(obj DataObject) bool {
result := true result := true
for _, side := range []string{Top, Right, Bottom, Left} { for _, side := range []string{Top, Right, Bottom, Left} {
if node := obj.PropertyWithTag(side); node != nil { if node := obj.PropertyByTag(side); node != nil {
if node.Type() == ObjectNode { if node.Type() == ObjectNode {
if !border.setSingleBorderObject(side, node.Object()) { if !border.setSingleBorderObject(side, node.Object()) {
result = false result = false

View File

@ -28,6 +28,7 @@ func (button *buttonData) CreateSuperView(session Session) View {
HorizontalAlign: CenterAlign, HorizontalAlign: CenterAlign,
VerticalAlign: CenterAlign, VerticalAlign: CenterAlign,
Orientation: StartToEndOrientation, Orientation: StartToEndOrientation,
TabIndex: 0,
}) })
} }

View File

@ -182,7 +182,7 @@ type Canvas interface {
SetRadialGradientStrokeStyle(x0, y0, r0 float64, color0 Color, x1, y1, r1 float64, color1 Color, stopPoints []GradientPoint) SetRadialGradientStrokeStyle(x0, y0, r0 float64, color0 Color, x1, y1, r1 float64, color1 Color, stopPoints []GradientPoint)
// SetImageFillStyle set the image as the filling pattern. // SetImageFillStyle set the image as the filling pattern.
// repeate - indicating how to repeat the pattern's image. Possible values are: // repeat - indicating how to repeat the pattern's image. Possible values are:
// NoRepeat (0) - neither direction, // NoRepeat (0) - neither direction,
// RepeatXY (1) - both directions, // RepeatXY (1) - both directions,
// RepeatX (2) - horizontal only, // RepeatX (2) - horizontal only,
@ -286,7 +286,7 @@ type Canvas interface {
DrawImage(x, y float64, image Image) DrawImage(x, y float64, image Image)
// DrawImageInRect draws the image in the rectangle (x, y, width, height), scaling in height and width if necessary // DrawImageInRect draws the image in the rectangle (x, y, width, height), scaling in height and width if necessary
DrawImageInRect(x, y, width, height float64, image Image) DrawImageInRect(x, y, width, height float64, image Image)
// DrawImageFragment draws the frament (described by srcX, srcY, srcWidth, srcHeight) of image // DrawImageFragment draws the fragment (described by srcX, srcY, srcWidth, srcHeight) of image
// in the rectangle (dstX, dstY, dstWidth, dstHeight), scaling in height and width if necessary // in the rectangle (dstX, dstY, dstWidth, dstHeight), scaling in height and width if necessary
DrawImageFragment(srcX, srcY, srcWidth, srcHeight, dstX, dstY, dstWidth, dstHeight float64, image Image) DrawImageFragment(srcX, srcY, srcWidth, srcHeight, dstX, dstY, dstWidth, dstHeight float64, image Image)
@ -302,12 +302,12 @@ func newCanvas(view CanvasView) Canvas {
canvas := new(canvasData) canvas := new(canvasData)
canvas.view = view canvas.view = view
canvas.session = view.Session() canvas.session = view.Session()
canvas.session.cavnasStart(view.htmlID()) canvas.session.canvasStart(view.htmlID())
return canvas return canvas
} }
func (canvas *canvasData) finishDraw() { func (canvas *canvasData) finishDraw() {
canvas.session.cavnasFinish() canvas.session.canvasFinish()
} }
func (canvas *canvasData) View() CanvasView { func (canvas *canvasData) View() CanvasView {

View File

@ -55,8 +55,8 @@ const (
BlueViolet Color = 0xff8a2be2 BlueViolet Color = 0xff8a2be2
// Brown color constant // Brown color constant
Brown Color = 0xffa52a2a Brown Color = 0xffa52a2a
// Burlywood color constant // BurlyWood color constant
Burlywood Color = 0xffdeb887 BurlyWood Color = 0xffdeb887
// CadetBlue color constant // CadetBlue color constant
CadetBlue Color = 0xff5f9ea0 CadetBlue Color = 0xff5f9ea0
// Chartreuse color constant // Chartreuse color constant
@ -67,8 +67,8 @@ const (
Coral Color = 0xffff7f50 Coral Color = 0xffff7f50
// CornflowerBlue color constant // CornflowerBlue color constant
CornflowerBlue Color = 0xff6495ed CornflowerBlue Color = 0xff6495ed
// Cornsilk color constant // CornSilk color constant
Cornsilk Color = 0xfffff8dc CornSilk Color = 0xfffff8dc
// Crimson color constant // Crimson color constant
Crimson Color = 0xffdc143c Crimson Color = 0xffdc143c
// Cyan color constant // Cyan color constant
@ -105,8 +105,8 @@ const (
DarkSlateBlue Color = 0xff483d8b DarkSlateBlue Color = 0xff483d8b
// DarkSlateGray color constant // DarkSlateGray color constant
DarkSlateGray Color = 0xff2f4f4f DarkSlateGray Color = 0xff2f4f4f
// Darkslategrey color constant // DarkSlateGrey color constant
Darkslategrey Color = 0xff2f4f4f DarkSlateGrey Color = 0xff2f4f4f
// DarkTurquoise color constant // DarkTurquoise color constant
DarkTurquoise Color = 0xff00ced1 DarkTurquoise Color = 0xff00ced1
// DarkViolet color constant // DarkViolet color constant
@ -135,8 +135,8 @@ const (
Gold Color = 0xffffd700 Gold Color = 0xffffd700
// GoldenRod color constant // GoldenRod color constant
GoldenRod Color = 0xffdaa520 GoldenRod Color = 0xffdaa520
// GreenyEllow color constant // GreenYellow color constant
GreenyEllow Color = 0xffadff2f GreenYellow Color = 0xffadff2f
// Grey color constant // Grey color constant
Grey Color = 0xff808080 Grey Color = 0xff808080
// Honeydew color constant // Honeydew color constant
@ -293,8 +293,8 @@ const (
Violet Color = 0xffee82ee Violet Color = 0xffee82ee
// Wheat color constant // Wheat color constant
Wheat Color = 0xfff5deb3 Wheat Color = 0xfff5deb3
// Whitesmoke color constant // WhiteSmoke color constant
Whitesmoke Color = 0xfff5f5f5 WhiteSmoke Color = 0xfff5f5f5
// YellowGreen color constant // YellowGreen color constant
YellowGreen Color = 0xff9acd32 YellowGreen Color = 0xff9acd32
) )

View File

@ -16,7 +16,7 @@ type ColorPicker interface {
type colorPickerData struct { type colorPickerData struct {
viewData viewData
colorChangedListeners []func(ColorPicker, Color) colorChangedListeners []func(ColorPicker, Color, Color)
} }
// NewColorPicker create new ColorPicker object and return it // NewColorPicker create new ColorPicker object and return it
@ -34,7 +34,7 @@ func newColorPicker(session Session) View {
func (picker *colorPickerData) init(session Session) { func (picker *colorPickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "ColorPicker" picker.tag = "ColorPicker"
picker.colorChangedListeners = []func(ColorPicker, Color){} picker.colorChangedListeners = []func(ColorPicker, Color, Color){}
picker.properties[Padding] = Px(0) picker.properties[Padding] = Px(0)
} }
@ -60,7 +60,7 @@ func (picker *colorPickerData) remove(tag string) {
switch tag { switch tag {
case ColorChangedEvent: case ColorChangedEvent:
if len(picker.colorChangedListeners) > 0 { if len(picker.colorChangedListeners) > 0 {
picker.colorChangedListeners = []func(ColorPicker, Color){} picker.colorChangedListeners = []func(ColorPicker, Color, Color){}
picker.propertyChangedEvent(tag) picker.propertyChangedEvent(tag)
} }
@ -86,12 +86,12 @@ func (picker *colorPickerData) set(tag string, value any) bool {
switch tag { switch tag {
case ColorChangedEvent: case ColorChangedEvent:
listeners, ok := valueToEventListeners[ColorPicker, Color](value) listeners, ok := valueToEventWithOldListeners[ColorPicker, Color](value)
if !ok { if !ok {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return false
} else if listeners == nil { } else if listeners == nil {
listeners = []func(ColorPicker, Color){} listeners = []func(ColorPicker, Color, Color){}
} }
picker.colorChangedListeners = listeners picker.colorChangedListeners = listeners
picker.propertyChangedEvent(tag) picker.propertyChangedEvent(tag)
@ -116,7 +116,7 @@ func (picker *colorPickerData) colorChanged(oldColor Color) {
picker.session.callFunc("setInputValue", picker.htmlID(), newColor.rgbString()) picker.session.callFunc("setInputValue", picker.htmlID(), newColor.rgbString())
} }
for _, listener := range picker.colorChangedListeners { for _, listener := range picker.colorChangedListeners {
listener(picker, newColor) listener(picker, newColor, oldColor)
} }
picker.propertyChangedEvent(ColorTag) picker.propertyChangedEvent(ColorTag)
} }
@ -169,7 +169,7 @@ func (picker *colorPickerData) handleCommand(self View, command string, data Dat
picker.properties[ColorPickerValue] = color picker.properties[ColorPickerValue] = color
if color != oldColor { if color != oldColor {
for _, listener := range picker.colorChangedListeners { for _, listener := range picker.colorChangedListeners {
listener(picker, color) listener(picker, color, oldColor)
} }
} }
} }
@ -204,6 +204,6 @@ func GetColorPickerValue(view View, subviewID ...string) Color {
// GetColorChangedListeners returns the ColorChangedListener list of an ColorPicker subview. // GetColorChangedListeners returns the ColorChangedListener list of an ColorPicker subview.
// If there are no listeners then the empty list is returned // If there are no listeners then the empty list is returned
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetColorChangedListeners(view View, subviewID ...string) []func(ColorPicker, Color) { func GetColorChangedListeners(view View, subviewID ...string) []func(ColorPicker, Color, Color) {
return getEventListeners[ColorPicker, Color](view, subviewID, ColorChangedEvent) return getEventWithOldListeners[ColorPicker, Color](view, subviewID, ColorChangedEvent)
} }

View File

@ -11,28 +11,45 @@ const (
// Values less than zero are not valid. if the "column-count" property value is 0 then // Values less than zero are not valid. if the "column-count" property value is 0 then
// the number of columns is calculated based on the "column-width" property // the number of columns is calculated based on the "column-width" property
ColumnCount = "column-count" ColumnCount = "column-count"
// ColumnWidth is the constant for the "column-width" property tag. // ColumnWidth is the constant for the "column-width" property tag.
// The "column-width" SizeUnit property specifies the width of each column. // The "column-width" SizeUnit property specifies the width of each column.
ColumnWidth = "column-width" ColumnWidth = "column-width"
// ColumnGap is the constant for the "column-gap" property tag. // ColumnGap is the constant for the "column-gap" property tag.
// The "column-width" SizeUnit property sets the size of the gap (gutter) between columns. // The "column-width" SizeUnit property sets the size of the gap (gutter) between columns.
ColumnGap = "column-gap" ColumnGap = "column-gap"
// ColumnSeparator is the constant for the "column-separator" property tag. // ColumnSeparator is the constant for the "column-separator" property tag.
// The "column-separator" property specifies the line drawn between columns in a multi-column layout. // The "column-separator" property specifies the line drawn between columns in a multi-column layout.
ColumnSeparator = "column-separator" ColumnSeparator = "column-separator"
// ColumnSeparatorStyle is the constant for the "column-separator-style" property tag. // ColumnSeparatorStyle is the constant for the "column-separator-style" property tag.
// The "column-separator-style" int property sets the style of the line drawn between // The "column-separator-style" int property sets the style of the line drawn between
// columns in a multi-column layout. // columns in a multi-column layout.
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4). // Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
ColumnSeparatorStyle = "column-separator-style" ColumnSeparatorStyle = "column-separator-style"
// ColumnSeparatorWidth is the constant for the "column-separator-width" property tag. // ColumnSeparatorWidth is the constant for the "column-separator-width" property tag.
// The "column-separator-width" SizeUnit property sets the width of the line drawn between // The "column-separator-width" SizeUnit property sets the width of the line drawn between
// columns in a multi-column layout. // columns in a multi-column layout.
ColumnSeparatorWidth = "column-separator-width" ColumnSeparatorWidth = "column-separator-width"
// ColumnSeparatorColor is the constant for the "column-separator-color" property tag. // ColumnSeparatorColor is the constant for the "column-separator-color" property tag.
// The "column-separator-color" Color property sets the color of the line drawn between // The "column-separator-color" Color property sets the color of the line drawn between
// columns in a multi-column layout. // columns in a multi-column layout.
ColumnSeparatorColor = "column-separator-color" ColumnSeparatorColor = "column-separator-color"
// ColumnFill is the constant for the "column-fill" property tag.
// The "column-fill" int property controls how an ColumnLayout's contents are balanced when broken into columns.
// Valid values are
// * ColumnFillBalance (0) - Content is equally divided between columns (default value);
// * ColumnFillAuto (1) - Columns are filled sequentially. Content takes up only the room it needs, possibly resulting in some columns remaining empty.
ColumnFill = "column-fill"
// ColumnSpanAll is the constant for the "column-span-all" property tag.
// The "column-span-all" bool property makes it possible for a view to span across all columns when its value is set to true.
ColumnSpanAll = "column-span-all"
) )
// ColumnLayout - grid-container of View // ColumnLayout - grid-container of View
@ -206,3 +223,16 @@ func GetColumnSeparatorColor(view View, subviewID ...string) Color {
border := getColumnSeparator(view, subviewID) border := getColumnSeparator(view, subviewID)
return border.Color return border.Color
} }
// GetColumnFill returns a "column-fill" property value of the subview.
// Returns one of next values: ColumnFillBalance (0) or ColumnFillAuto (1)
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetColumnFill(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, ColumnFill, ColumnFillBalance, true)
}
// IsColumnSpanAll returns a "column-span-all" property value of the subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func IsColumnSpanAll(view View, subviewID ...string) bool {
return boolStyledProperty(view, subviewID, ColumnSpanAll, false)
}

View File

@ -243,10 +243,19 @@ func (customView *CustomViewData) RemoveView(index int) View {
return container.RemoveView(index) return container.RemoveView(index)
} }
} }
return nil return nil
} }
// Remove removes a view from the list of a view children and return it
func (customView *CustomViewData) ViewIndex(view View) int {
if customView.superView != nil {
if container, ok := customView.superView.(ViewsContainer); ok {
return container.ViewIndex(view)
}
}
return -1
}
func (customView *CustomViewData) String() string { func (customView *CustomViewData) String() string {
if customView.superView != nil { if customView.superView != nil {
return getViewString(customView) return getViewString(customView)

66
data.go
View File

@ -18,11 +18,12 @@ type DataObject interface {
Tag() string Tag() string
PropertyCount() int PropertyCount() int
Property(index int) DataNode Property(index int) DataNode
PropertyWithTag(tag string) DataNode PropertyByTag(tag string) DataNode
PropertyValue(tag string) (string, bool) PropertyValue(tag string) (string, bool)
PropertyObject(tag string) DataObject PropertyObject(tag string) DataObject
SetPropertyValue(tag, value string) SetPropertyValue(tag, value string)
SetPropertyObject(tag string, object DataObject) SetPropertyObject(tag string, object DataObject)
ToParams() Params
} }
const ( const (
@ -43,6 +44,7 @@ type DataNode interface {
ArraySize() int ArraySize() int
ArrayElement(index int) DataValue ArrayElement(index int) DataValue
ArrayElements() []DataValue ArrayElements() []DataValue
ArrayAsParams() []Params
} }
/******************************************************************************/ /******************************************************************************/
@ -106,7 +108,7 @@ func (object *dataObject) Property(index int) DataNode {
return object.property[index] return object.property[index]
} }
func (object *dataObject) PropertyWithTag(tag string) DataNode { func (object *dataObject) PropertyByTag(tag string) DataNode {
if object.property != nil { if object.property != nil {
for _, node := range object.property { for _, node := range object.property {
if node.Tag() == tag { if node.Tag() == tag {
@ -118,14 +120,14 @@ func (object *dataObject) PropertyWithTag(tag string) DataNode {
} }
func (object *dataObject) PropertyValue(tag string) (string, bool) { func (object *dataObject) PropertyValue(tag string) (string, bool) {
if node := object.PropertyWithTag(tag); node != nil && node.Type() == TextNode { if node := object.PropertyByTag(tag); node != nil && node.Type() == TextNode {
return node.Text(), true return node.Text(), true
} }
return "", false return "", false
} }
func (object *dataObject) PropertyObject(tag string) DataObject { func (object *dataObject) PropertyObject(tag string) DataObject {
if node := object.PropertyWithTag(tag); node != nil && node.Type() == ObjectNode { if node := object.PropertyByTag(tag); node != nil && node.Type() == ObjectNode {
return node.Object() return node.Object()
} }
return nil return nil
@ -165,6 +167,42 @@ func (object *dataObject) SetPropertyObject(tag string, obj DataObject) {
object.setNode(node) object.setNode(node)
} }
func (object *dataObject) ToParams() Params {
params := Params{}
for _, node := range object.property {
switch node.Type() {
case TextNode:
if text := node.Text(); text != "" {
params[node.Tag()] = text
}
case ObjectNode:
if obj := node.Object(); obj != nil {
params[node.Tag()] = node.Object()
}
case ArrayNode:
array := []any{}
for i := 0; i < node.ArraySize(); i++ {
if data := node.ArrayElement(i); data != nil {
if data.IsObject() {
if obj := data.Object(); obj != nil {
array = append(array, obj)
}
} else if text := data.Value(); text != "" {
array = append(array, text)
}
}
}
if len(array) > 0 {
params[node.Tag()] = array
}
}
}
return params
}
/******************************************************************************/ /******************************************************************************/
type dataNode struct { type dataNode struct {
tag string tag string
@ -221,6 +259,22 @@ func (node *dataNode) ArrayElements() []DataValue {
return []DataValue{} return []DataValue{}
} }
func (node *dataNode) ArrayAsParams() []Params {
result := []Params{}
if node.array != nil {
for _, data := range node.array {
if data.IsObject() {
if obj := data.Object(); obj != nil {
if params := obj.ToParams(); len(params) > 0 {
result = append(result, params)
}
}
}
}
}
return result
}
// ParseDataText - parse text and return DataNode // ParseDataText - parse text and return DataNode
func ParseDataText(text string) DataObject { func ParseDataText(text string) DataObject {
@ -440,8 +494,8 @@ func ParseDataText(text string) DataObject {
endPos := pos endPos := pos
skipSpaces(false) skipSpaces(false)
if startPos == endPos { if startPos == endPos {
ErrorLog("empty tag") //ErrorLog("empty tag")
return "", false return "", true
} }
return string(data[startPos:endPos]), true return string(data[startPos:endPos]), true
} }

View File

@ -22,7 +22,7 @@ type DatePicker interface {
type datePickerData struct { type datePickerData struct {
viewData viewData
dateChangedListeners []func(DatePicker, time.Time) dateChangedListeners []func(DatePicker, time.Time, time.Time)
} }
// NewDatePicker create new DatePicker object and return it // NewDatePicker create new DatePicker object and return it
@ -40,7 +40,7 @@ func newDatePicker(session Session) View {
func (picker *datePickerData) init(session Session) { func (picker *datePickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "DatePicker" picker.tag = "DatePicker"
picker.dateChangedListeners = []func(DatePicker, time.Time){} picker.dateChangedListeners = []func(DatePicker, time.Time, time.Time){}
} }
func (picker *datePickerData) String() string { func (picker *datePickerData) String() string {
@ -69,7 +69,7 @@ func (picker *datePickerData) remove(tag string) {
switch tag { switch tag {
case DateChangedEvent: case DateChangedEvent:
if len(picker.dateChangedListeners) > 0 { if len(picker.dateChangedListeners) > 0 {
picker.dateChangedListeners = []func(DatePicker, time.Time){} picker.dateChangedListeners = []func(DatePicker, time.Time, time.Time){}
picker.propertyChangedEvent(tag) picker.propertyChangedEvent(tag)
} }
return return
@ -94,13 +94,14 @@ func (picker *datePickerData) remove(tag string) {
case DatePickerValue: case DatePickerValue:
if _, ok := picker.properties[DatePickerValue]; ok { if _, ok := picker.properties[DatePickerValue]; ok {
oldDate := GetDatePickerValue(picker)
delete(picker.properties, DatePickerValue) delete(picker.properties, DatePickerValue)
date := GetDatePickerValue(picker) date := GetDatePickerValue(picker)
if picker.created { if picker.created {
picker.session.callFunc("setInputValue", picker.htmlID(), date.Format(dateFormat)) picker.session.callFunc("setInputValue", picker.htmlID(), date.Format(dateFormat))
} }
for _, listener := range picker.dateChangedListeners { for _, listener := range picker.dateChangedListeners {
listener(picker, date) listener(picker, date, oldDate)
} }
} else { } else {
return return
@ -226,7 +227,7 @@ func (picker *datePickerData) set(tag string, value any) bool {
picker.session.callFunc("setInputValue", picker.htmlID(), date.Format(dateFormat)) picker.session.callFunc("setInputValue", picker.htmlID(), date.Format(dateFormat))
} }
for _, listener := range picker.dateChangedListeners { for _, listener := range picker.dateChangedListeners {
listener(picker, date) listener(picker, date, oldDate)
} }
picker.propertyChangedEvent(tag) picker.propertyChangedEvent(tag)
} }
@ -234,12 +235,12 @@ func (picker *datePickerData) set(tag string, value any) bool {
} }
case DateChangedEvent: case DateChangedEvent:
listeners, ok := valueToEventListeners[DatePicker, time.Time](value) listeners, ok := valueToEventWithOldListeners[DatePicker, time.Time](value)
if !ok { if !ok {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return false
} else if listeners == nil { } else if listeners == nil {
listeners = []func(DatePicker, time.Time){} listeners = []func(DatePicker, time.Time, time.Time){}
} }
picker.dateChangedListeners = listeners picker.dateChangedListeners = listeners
picker.propertyChangedEvent(tag) picker.propertyChangedEvent(tag)
@ -318,7 +319,7 @@ func (picker *datePickerData) handleCommand(self View, command string, data Data
picker.properties[DatePickerValue] = value picker.properties[DatePickerValue] = value
if value != oldValue { if value != oldValue {
for _, listener := range picker.dateChangedListeners { for _, listener := range picker.dateChangedListeners {
listener(picker, value) listener(picker, value, oldValue)
} }
} }
} }
@ -410,6 +411,6 @@ func GetDatePickerValue(view View, subviewID ...string) time.Time {
// GetDateChangedListeners returns the DateChangedListener list of an DatePicker subview. // GetDateChangedListeners returns the DateChangedListener list of an DatePicker subview.
// If there are no listeners then the empty list is returned // If there are no listeners then the empty list is returned
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetDateChangedListeners(view View, subviewID ...string) []func(DatePicker, time.Time) { func GetDateChangedListeners(view View, subviewID ...string) []func(DatePicker, time.Time, time.Time) {
return getEventListeners[DatePicker, time.Time](view, subviewID, DateChangedEvent) return getEventWithOldListeners[DatePicker, time.Time](view, subviewID, DateChangedEvent)
} }

View File

@ -23,6 +23,9 @@ theme {
ruiTabTextColor = #FF404040, ruiTabTextColor = #FF404040,
ruiCurrentTabColor = #FFFFFFFF, ruiCurrentTabColor = #FFFFFFFF,
ruiCurrentTabTextColor = #FF000000, ruiCurrentTabTextColor = #FF000000,
ruiTooltipBackground = #FFFFFFFF,
ruiTooltipTextColor = #FF000000,
ruiTooltipShadowColor = #FF808080,
}, },
colors:dark = _{ colors:dark = _{
ruiTextColor = #FFE0E0E0, ruiTextColor = #FFE0E0E0,
@ -43,6 +46,9 @@ theme {
ruiTabTextColor = #FFE0E0E0, ruiTabTextColor = #FFE0E0E0,
ruiCurrentTabColor = #FF000000, ruiCurrentTabColor = #FF000000,
ruiCurrentTabTextColor = #FFFFFFFF, ruiCurrentTabTextColor = #FFFFFFFF,
ruiTooltipBackground = #FF303030,
ruiTooltipTextColor = #FFDDDDDD,
ruiTooltipShadowColor = #FFDDDDDD,
}, },
constants = _{ constants = _{
ruiButtonHorizontalPadding = 16px, ruiButtonHorizontalPadding = 16px,
@ -104,6 +110,26 @@ theme {
ruiButton:active { ruiButton:active {
background-color = @ruiButtonActiveColor background-color = @ruiButtonActiveColor
}, },
ruiDefaultButton {
align = center,
padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding",
margin = @ruiButtonMargin,
radius = @ruiButtonRadius,
background-color = @ruiButtonColor,
text-color = @ruiButtonTextColor,
text-weight = bold,
border = _{width = 1px, style = solid, color = @ruiButtonTextColor}
},
ruiDefaultButton:hover {
text-color = @ruiTextColor,
background-color = @ruiBackgroundColor,
},
ruiDefaultButton:focus {
shadow = _{spread-radius = @ruiButtonHighlightDilation, blur = @ruiButtonHighlightBlur, color = @ruiHighlightColor },
},
ruiDefaultButton:active {
background-color = @ruiButtonActiveColor
},
ruiCheckbox { ruiCheckbox {
radius = 2px, radius = 2px,
padding = 1px, padding = 1px,

View File

@ -23,7 +23,7 @@ func (session *sessionData) startDownload(file downloadFile) {
currentDownloadId++ currentDownloadId++
id := strconv.Itoa(currentDownloadId) id := strconv.Itoa(currentDownloadId)
downloadFiles[id] = file downloadFiles[id] = file
session.callFunc("startDowndload", id, file.filename) session.callFunc("startDownload", id, file.filename)
} }
func serveDownloadFile(id string, w http.ResponseWriter, r *http.Request) bool { func serveDownloadFile(id string, w http.ResponseWriter, r *http.Request) bool {

View File

@ -21,7 +21,7 @@ type dropDownListData struct {
viewData viewData
items []string items []string
disabledItems []any disabledItems []any
dropDownListener []func(DropDownList, int) dropDownListener []func(DropDownList, int, int)
} }
// NewDropDownList create new DropDownList object and return it // NewDropDownList create new DropDownList object and return it
@ -41,7 +41,7 @@ func (list *dropDownListData) init(session Session) {
list.tag = "DropDownList" list.tag = "DropDownList"
list.items = []string{} list.items = []string{}
list.disabledItems = []any{} list.disabledItems = []any{}
list.dropDownListener = []func(DropDownList, int){} list.dropDownListener = []func(DropDownList, int, int){}
} }
func (list *dropDownListData) String() string { func (list *dropDownListData) String() string {
@ -78,7 +78,7 @@ func (list *dropDownListData) remove(tag string) {
case DropDownEvent: case DropDownEvent:
if len(list.dropDownListener) > 0 { if len(list.dropDownListener) > 0 {
list.dropDownListener = []func(DropDownList, int){} list.dropDownListener = []func(DropDownList, int, int){}
list.propertyChangedEvent(tag) list.propertyChangedEvent(tag)
} }
@ -89,7 +89,7 @@ func (list *dropDownListData) remove(tag string) {
if list.created { if list.created {
list.session.callFunc("selectDropDownListItem", list.htmlID(), 0) list.session.callFunc("selectDropDownListItem", list.htmlID(), 0)
} }
list.onSelectedItemChanged(0) list.onSelectedItemChanged(0, oldCurrent)
} }
default: default:
@ -116,12 +116,12 @@ func (list *dropDownListData) set(tag string, value any) bool {
return list.setDisabledItems(value) return list.setDisabledItems(value)
case DropDownEvent: case DropDownEvent:
listeners, ok := valueToEventListeners[DropDownList, int](value) listeners, ok := valueToEventWithOldListeners[DropDownList, int](value)
if !ok { if !ok {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return false
} else if listeners == nil { } else if listeners == nil {
listeners = []func(DropDownList, int){} listeners = []func(DropDownList, int, int){}
} }
list.dropDownListener = listeners list.dropDownListener = listeners
list.propertyChangedEvent(tag) list.propertyChangedEvent(tag)
@ -137,7 +137,7 @@ func (list *dropDownListData) set(tag string, value any) bool {
if list.created { if list.created {
list.session.callFunc("selectDropDownListItem", list.htmlID(), current) list.session.callFunc("selectDropDownListItem", list.htmlID(), current)
} }
list.onSelectedItemChanged(current) list.onSelectedItemChanged(current, oldCurrent)
} }
return true return true
} }
@ -377,9 +377,9 @@ func (list *dropDownListData) htmlDisabledProperties(self View, buffer *strings.
} }
} }
func (list *dropDownListData) onSelectedItemChanged(number int) { func (list *dropDownListData) onSelectedItemChanged(number, old int) {
for _, listener := range list.dropDownListener { for _, listener := range list.dropDownListener {
listener(list, number) listener(list, number, old)
} }
list.propertyChangedEvent(Current) list.propertyChangedEvent(Current)
} }
@ -390,8 +390,9 @@ func (list *dropDownListData) handleCommand(self View, command string, data Data
if text, ok := data.PropertyValue("number"); ok { if text, ok := data.PropertyValue("number"); ok {
if number, err := strconv.Atoi(text); err == nil { if number, err := strconv.Atoi(text); err == nil {
if GetCurrent(list) != number && number >= 0 && number < len(list.items) { if GetCurrent(list) != number && number >= 0 && number < len(list.items) {
old := GetCurrent(list)
list.properties[Current] = number list.properties[Current] = number
list.onSelectedItemChanged(number) list.onSelectedItemChanged(number, old)
} }
} else { } else {
ErrorLog(err.Error()) ErrorLog(err.Error())
@ -406,8 +407,8 @@ func (list *dropDownListData) handleCommand(self View, command string, data Data
// GetDropDownListeners returns the "drop-down-event" listener list. If there are no listeners then the empty list is returned. // GetDropDownListeners returns the "drop-down-event" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetDropDownListeners(view View, subviewID ...string) []func(DropDownList, int) { func GetDropDownListeners(view View, subviewID ...string) []func(DropDownList, int, int) {
return getEventListeners[DropDownList, int](view, subviewID, DropDownEvent) return getEventWithOldListeners[DropDownList, int](view, subviewID, DropDownEvent)
} }
// GetDropDownItems return the DropDownList items list. // GetDropDownItems return the DropDownList items list.

View File

@ -23,7 +23,7 @@ const (
PasswordText = 1 PasswordText = 1
// EmailText - e-mail type of EditView. Allows to enter one email // EmailText - e-mail type of EditView. Allows to enter one email
EmailText = 2 EmailText = 2
// EmailsText - e-mail type of EditView. Allows to enter multiple emails separeted by comma // EmailsText - e-mail type of EditView. Allows to enter multiple emails separated by comma
EmailsText = 3 EmailsText = 3
// URLText - url type of EditView. Allows to enter one url // URLText - url type of EditView. Allows to enter one url
URLText = 4 URLText = 4
@ -41,7 +41,7 @@ type EditView interface {
type editViewData struct { type editViewData struct {
viewData viewData
textChangeListeners []func(EditView, string) textChangeListeners []func(EditView, string, string)
} }
// NewEditView create new EditView object and return it // NewEditView create new EditView object and return it
@ -58,7 +58,7 @@ func newEditView(session Session) View {
func (edit *editViewData) init(session Session) { func (edit *editViewData) init(session Session) {
edit.viewData.init(session) edit.viewData.init(session)
edit.textChangeListeners = []func(EditView, string){} edit.textChangeListeners = []func(EditView, string, string){}
edit.tag = "EditView" edit.tag = "EditView"
} }
@ -125,7 +125,7 @@ func (edit *editViewData) remove(tag string) {
case EditTextChangedEvent: case EditTextChangedEvent:
if len(edit.textChangeListeners) > 0 { if len(edit.textChangeListeners) > 0 {
edit.textChangeListeners = []func(EditView, string){} edit.textChangeListeners = []func(EditView, string, string){}
edit.propertyChangedEvent(tag) edit.propertyChangedEvent(tag)
} }
@ -134,7 +134,7 @@ func (edit *editViewData) remove(tag string) {
oldText := GetText(edit) oldText := GetText(edit)
delete(edit.properties, tag) delete(edit.properties, tag)
if oldText != "" { if oldText != "" {
edit.textChanged("") edit.textChanged("", oldText)
if edit.created { if edit.created {
edit.session.callFunc("setInputValue", edit.htmlID(), "") edit.session.callFunc("setInputValue", edit.htmlID(), "")
} }
@ -205,7 +205,7 @@ func (edit *editViewData) set(tag string, value any) bool {
if text, ok := value.(string); ok { if text, ok := value.(string); ok {
edit.properties[Text] = text edit.properties[Text] = text
if text = GetText(edit); oldText != text { if text = GetText(edit); oldText != text {
edit.textChanged(text) edit.textChanged(text, oldText)
if edit.created { if edit.created {
if GetEditViewType(edit) == MultiLineText { if GetEditViewType(edit) == MultiLineText {
updateInnerHTML(edit.htmlID(), edit.Session()) updateInnerHTML(edit.htmlID(), edit.Session())
@ -328,12 +328,12 @@ func (edit *editViewData) set(tag string, value any) bool {
return false return false
case EditTextChangedEvent: case EditTextChangedEvent:
listeners, ok := valueToEventListeners[EditView, string](value) listeners, ok := valueToEventWithOldListeners[EditView, string](value)
if !ok { if !ok {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return false
} else if listeners == nil { } else if listeners == nil {
listeners = []func(EditView, string){} listeners = []func(EditView, string, string){}
} }
edit.textChangeListeners = listeners edit.textChangeListeners = listeners
edit.propertyChangedEvent(tag) edit.propertyChangedEvent(tag)
@ -358,10 +358,11 @@ func (edit *editViewData) AppendText(text string) {
if GetEditViewType(edit) == MultiLineText { if GetEditViewType(edit) == MultiLineText {
if value := edit.getRaw(Text); value != nil { if value := edit.getRaw(Text); value != nil {
if textValue, ok := value.(string); ok { if textValue, ok := value.(string); ok {
oldText := textValue
textValue += text textValue += text
edit.properties[Text] = textValue edit.properties[Text] = textValue
edit.session.callFunc("appendToInnerHTML", edit.htmlID(), text) edit.session.callFunc("appendToInnerHTML", edit.htmlID(), text)
edit.textChanged(textValue) edit.textChanged(textValue, oldText)
return return
} }
} }
@ -371,9 +372,9 @@ func (edit *editViewData) AppendText(text string) {
} }
} }
func (edit *editViewData) textChanged(newText string) { func (edit *editViewData) textChanged(newText, oldText string) {
for _, listener := range edit.textChangeListeners { for _, listener := range edit.textChangeListeners {
listener(edit, newText) listener(edit, newText, oldText)
} }
edit.propertyChangedEvent(Text) edit.propertyChangedEvent(Text)
} }
@ -485,7 +486,7 @@ func (edit *editViewData) handleCommand(self View, command string, data DataObje
if text, ok := data.PropertyValue("text"); ok { if text, ok := data.PropertyValue("text"); ok {
edit.properties[Text] = text edit.properties[Text] = text
if text := GetText(edit); text != oldText { if text := GetText(edit); text != oldText {
edit.textChanged(text) edit.textChanged(text, oldText)
} }
} }
return true return true
@ -531,7 +532,7 @@ func GetHint(view View, subviewID ...string) string {
return "" return ""
} }
// GetMaxLength returns a maximal lenght of EditView. If a maximal lenght is not limited then 0 is returned // GetMaxLength returns a maximal length of EditView. If a maximal length is not limited then 0 is returned
// If the second argument (subviewID) is not specified or it is "" then a value of the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then a value of the first argument (view) is returned.
func GetMaxLength(view View, subviewID ...string) int { func GetMaxLength(view View, subviewID ...string) int {
return intStyledProperty(view, subviewID, MaxLength, 0) return intStyledProperty(view, subviewID, MaxLength, 0)
@ -552,8 +553,8 @@ func IsSpellcheck(view View, subviewID ...string) bool {
// GetTextChangedListeners returns the TextChangedListener list of an EditView or MultiLineEditView subview. // GetTextChangedListeners returns the TextChangedListener list of an EditView or MultiLineEditView subview.
// If there are no listeners then the empty list is returned // If there are no listeners then the empty list is returned
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetTextChangedListeners(view View, subviewID ...string) []func(EditView, string) { func GetTextChangedListeners(view View, subviewID ...string) []func(EditView, string, string) {
return getEventListeners[EditView, string](view, subviewID, EditTextChangedEvent) return getEventWithOldListeners[EditView, string](view, subviewID, EditTextChangedEvent)
} }
// GetEditViewType returns a value of the Type property of EditView. // GetEditViewType returns a value of the Type property of EditView.
@ -604,7 +605,7 @@ func AppendEditText(view View, subviewID string, text string) {
} }
} }
// GetCaretColor returns the color of the text input carret. // GetCaretColor returns the color of the text input caret.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetCaretColor(view View, subviewID ...string) Color { func GetCaretColor(view View, subviewID ...string) Color {
return colorStyledProperty(view, subviewID, CaretColor, false) return colorStyledProperty(view, subviewID, CaretColor, false)

View File

@ -270,7 +270,7 @@ func (picker *filePickerData) htmlDisabledProperties(self View, buffer *strings.
func (picker *filePickerData) handleCommand(self View, command string, data DataObject) bool { func (picker *filePickerData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "fileSelected": case "fileSelected":
if node := data.PropertyWithTag("files"); node != nil && node.Type() == ArrayNode { if node := data.PropertyByTag("files"); node != nil && node.Type() == ArrayNode {
count := node.ArraySize() count := node.ArraySize()
files := make([]FileInfo, count) files := make([]FileInfo, count)
for i := 0; i < count; i++ { for i := 0; i < count; i++ {

View File

@ -143,7 +143,10 @@ func getFocusListeners(view View, subviewID []string, tag string) []func(View) {
func focusEventsHtml(view View, buffer *strings.Builder) { func focusEventsHtml(view View, buffer *strings.Builder) {
if view.Focusable() { if view.Focusable() {
for _, js := range focusEvents { for _, js := range focusEvents {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `) buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
} }
} }
} }

8
go.mod
View File

@ -1,5 +1,7 @@
module github.com/anoshenko/rui module git.mbk-lab.ru/mbk-lab/rui
go 1.18 go 1.20
require github.com/gorilla/websocket v1.5.0 require github.com/gorilla/websocket v1.5.1
require golang.org/x/net v0.17.0 // indirect

6
go.sum
View File

@ -1,2 +1,4 @@
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=

55
http_handler.go Normal file
View File

@ -0,0 +1,55 @@
package rui
import (
"net/http"
"strings"
)
type httpHandler struct {
app *application
prefix string
}
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
path := `/` + strings.TrimPrefix(req.URL.Path, `/`)
req.URL.Path = `/` + strings.TrimPrefix(strings.TrimPrefix(path, h.prefix), `/`)
h.app.ServeHTTP(w, req)
}
}
/*
NewHandler используется для встраивания приложения rui в сторонние WEB-фреймворки (net/http, gin, echo...).
Пример для echo:
e := echo.New()
e.Any(`/ui/*`, func()echo.HandlerFunc{
rui.AddEmbedResources(&resources)
h := rui.NewHandler("/ui", CreateSessionContent, rui.AppParams{
Title: `Awesome app`,
Icon: `favicon.png`,
})
return func(c echo.Context) error {
h.ServeHTTP(c.Response(), c.Request())
return nil
}
})
*/
func NewHandler(urlPrefix string, createContentFunc func(Session) SessionContent, params AppParams) *httpHandler {
app := new(application)
app.params = params
app.sessions = map[int]Session{}
app.createContentFunc = createContentFunc
apps = append(apps, app)
h := &httpHandler{
app: app,
prefix: `/` + strings.Trim(urlPrefix, `/`),
}
return h
}

View File

@ -1,11 +1,7 @@
package rui package rui
import ( import (
"encoding/base64"
"path/filepath"
"runtime"
"strconv" "strconv"
"strings"
) )
const ( const (
@ -83,27 +79,6 @@ func (manager *imageManager) loadImage(url string, onLoaded func(Image), session
image.loadingStatus = ImageLoading image.loadingStatus = ImageLoading
manager.images[url] = image manager.images[url] = image
if runtime.GOOS == "js" && wasmMediaResources {
if file, ok := resources.images[url]; ok && file.fs != nil {
dataType := map[string]string{
".svg": "data:image/svg+xml",
".png": "data:image/png",
".jpg": "data:image/jpg",
".jpeg": "data:image/jpg",
".gif": "data:image/gif",
}
ext := strings.ToLower(filepath.Ext(url))
if prefix, ok := dataType[ext]; ok {
if data, err := file.fs.ReadFile(file.path); err == nil {
session.callFunc("loadInlineImage", url, prefix+";base64,"+base64.StdEncoding.EncodeToString(data))
return image
} else {
DebugLog(err.Error())
}
}
}
}
session.callFunc("loadImage", url) session.callFunc("loadImage", url)
return image return image
} }

View File

@ -1,10 +1,7 @@
package rui package rui
import ( import (
"encoding/base64"
"fmt" "fmt"
"path/filepath"
"runtime"
"strings" "strings"
) )
@ -70,8 +67,7 @@ func newImageView(session Session) View {
func (imageView *imageViewData) init(session Session) { func (imageView *imageViewData) init(session Session) {
imageView.viewData.init(session) imageView.viewData.init(session)
imageView.tag = "ImageView" imageView.tag = "ImageView"
//imageView.systemClass = "ruiImageView" imageView.systemClass = "ruiImageView"
} }
func (imageView *imageViewData) String() string { func (imageView *imageViewData) String() string {
@ -272,27 +268,7 @@ func (imageView *imageViewData) src(src string) (string, string) {
} }
if src != "" { if src != "" {
srcset := imageView.srcSet(src) return src, imageView.srcSet(src)
if runtime.GOOS == "js" && wasmMediaResources {
if image, ok := resources.images[src]; ok && image.fs != nil {
dataType := map[string]string{
".svg": "data:image/svg+xml",
".png": "data:image/png",
".jpg": "data:image/jpg",
".jpeg": "data:image/jpg",
".gif": "data:image/gif",
}
ext := strings.ToLower(filepath.Ext(src))
if prefix, ok := dataType[ext]; ok {
if data, err := image.fs.ReadFile(image.path); err == nil {
return prefix + ";base64," + base64.StdEncoding.EncodeToString(data), ""
} else {
DebugLog(err.Error())
}
}
}
}
return src, srcset
} }
return "", "" return "", ""
} }

View File

@ -20,6 +20,127 @@ const (
KeyUpEvent = "key-up-event" KeyUpEvent = "key-up-event"
) )
type ControlKeyMask int
type KeyCode string
const (
// AltKey is the mask of the "alt" key
AltKey ControlKeyMask = 1
// CtrlKey is the mask of the "ctrl" key
CtrlKey ControlKeyMask = 2
// ShiftKey is the mask of the "shift" key
ShiftKey ControlKeyMask = 4
// MetaKey is the mask of the "meta" key
MetaKey ControlKeyMask = 8
KeyA KeyCode = "KeyA"
KeyB KeyCode = "KeyB"
KeyC KeyCode = "KeyC"
KeyD KeyCode = "KeyD"
KeyE KeyCode = "KeyE"
KeyF KeyCode = "KeyF"
KeyG KeyCode = "KeyG"
KeyH KeyCode = "KeyH"
KeyI KeyCode = "KeyI"
KeyJ KeyCode = "KeyJ"
KeyK KeyCode = "KeyK"
KeyL KeyCode = "KeyL"
KeyM KeyCode = "KeyM"
KeyN KeyCode = "KeyN"
KeyO KeyCode = "KeyO"
KeyP KeyCode = "KeyP"
KeyQ KeyCode = "KeyQ"
KeyR KeyCode = "KeyR"
KeyS KeyCode = "KeyS"
KeyT KeyCode = "KeyT"
KeyU KeyCode = "KeyU"
KeyV KeyCode = "KeyV"
KeyW KeyCode = "KeyW"
KeyX KeyCode = "KeyX"
KeyY KeyCode = "KeyY"
KeyZ KeyCode = "KeyZ"
Digit0Key KeyCode = "Digit0"
Digit1Key KeyCode = "Digit1"
Digit2Key KeyCode = "Digit2"
Digit3Key KeyCode = "Digit3"
Digit4Key KeyCode = "Digit4"
Digit5Key KeyCode = "Digit5"
Digit6Key KeyCode = "Digit6"
Digit7Key KeyCode = "Digit7"
Digit8Key KeyCode = "Digit8"
Digit9Key KeyCode = "Digit9"
SpaceKey KeyCode = "Space"
MinusKey KeyCode = "Minus"
EqualKey KeyCode = "Equal"
IntlBackslashKey KeyCode = "IntlBackslash"
BracketLeftKey KeyCode = "BracketLeft"
BracketRightKey KeyCode = "BracketRight"
SemicolonKey KeyCode = "Semicolon"
CommaKey KeyCode = "Comma"
PeriodKey KeyCode = "Period"
QuoteKey KeyCode = "Quote"
BackquoteKey KeyCode = "Backquote"
SlashKey KeyCode = "Slash"
EscapeKey KeyCode = "Escape"
EnterKey KeyCode = "Enter"
TabKey KeyCode = "Tab"
CapsLockKey KeyCode = "CapsLock"
DeleteKey KeyCode = "Delete"
InsertKey KeyCode = "Insert"
HelpKey KeyCode = "Help"
BackspaceKey KeyCode = "Backspace"
PrintScreenKey KeyCode = "PrintScreen"
ScrollLockKey KeyCode = "ScrollLock"
PauseKey KeyCode = "Pause"
ContextMenuKey KeyCode = "ContextMenu"
ArrowLeftKey KeyCode = "ArrowLeft"
ArrowRightKey KeyCode = "ArrowRight"
ArrowUpKey KeyCode = "ArrowUp"
ArrowDownKey KeyCode = "ArrowDown"
HomeKey KeyCode = "Home"
EndKey KeyCode = "End"
PageUpKey KeyCode = "PageUp"
PageDownKey KeyCode = "PageDown"
F1Key KeyCode = "F1"
F2Key KeyCode = "F2"
F3Key KeyCode = "F3"
F4Key KeyCode = "F4"
F5Key KeyCode = "F5"
F6Key KeyCode = "F6"
F7Key KeyCode = "F7"
F8Key KeyCode = "F8"
F9Key KeyCode = "F9"
F10Key KeyCode = "F10"
F11Key KeyCode = "F11"
F12Key KeyCode = "F12"
F13Key KeyCode = "F13"
NumLockKey KeyCode = "NumLock"
NumpadKey0 KeyCode = "Numpad0"
NumpadKey1 KeyCode = "Numpad1"
NumpadKey2 KeyCode = "Numpad2"
NumpadKey3 KeyCode = "Numpad3"
NumpadKey4 KeyCode = "Numpad4"
NumpadKey5 KeyCode = "Numpad5"
NumpadKey6 KeyCode = "Numpad6"
NumpadKey7 KeyCode = "Numpad7"
NumpadKey8 KeyCode = "Numpad8"
NumpadKey9 KeyCode = "Numpad9"
NumpadDecimalKey KeyCode = "NumpadDecimal"
NumpadEnterKey KeyCode = "NumpadEnter"
NumpadAddKey KeyCode = "NumpadAdd"
NumpadSubtractKey KeyCode = "NumpadSubtract"
NumpadMultiplyKey KeyCode = "NumpadMultiply"
NumpadDivideKey KeyCode = "NumpadDivide"
ShiftLeftKey KeyCode = "ShiftLeft"
ShiftRightKey KeyCode = "ShiftRight"
ControlLeftKey KeyCode = "ControlLeft"
ControlRightKey KeyCode = "ControlRight"
AltLeftKey KeyCode = "AltLeft"
AltRightKey KeyCode = "AltRight"
MetaLeftKey KeyCode = "MetaLeft"
MetaRightKey KeyCode = "MetaRight"
)
type KeyEvent struct { type KeyEvent struct {
// TimeStamp is the time at which the event was created (in milliseconds). // TimeStamp is the time at which the event was created (in milliseconds).
// This value is time since epoch—but in reality, browsers' definitions vary. // This value is time since epoch—but in reality, browsers' definitions vary.
@ -32,7 +153,7 @@ type KeyEvent struct {
// Code holds a string that identifies the physical key being pressed. The value is not affected // Code holds a string that identifies the physical key being pressed. The value is not affected
// by the current keyboard layout or modifier state, so a particular key will always return the same value. // by the current keyboard layout or modifier state, so a particular key will always return the same value.
Code string Code KeyCode
// Repeat == true if a key has been depressed long enough to trigger key repetition, otherwise false. // Repeat == true if a key has been depressed long enough to trigger key repetition, otherwise false.
Repeat bool Repeat bool
@ -59,7 +180,8 @@ func (event *KeyEvent) init(data DataObject) {
} }
event.Key, _ = data.PropertyValue("key") event.Key, _ = data.PropertyValue("key")
event.Code, _ = data.PropertyValue("code") code, _ := data.PropertyValue("code")
event.Code = KeyCode(code)
event.TimeStamp = getTimeStamp(data) event.TimeStamp = getTimeStamp(data)
event.Repeat = getBool("repeat") event.Repeat = getBool("repeat")
event.CtrlKey = getBool("ctrlKey") event.CtrlKey = getBool("ctrlKey")
@ -193,9 +315,183 @@ func valueToEventListeners[V View, E any](value any) ([]func(V, E), bool) {
return nil, false return nil, false
} }
var keyEvents = map[string]struct{ jsEvent, jsFunc string }{ func valueToEventWithOldListeners[V View, E any](value any) ([]func(V, E, E), bool) {
KeyDownEvent: {jsEvent: "onkeydown", jsFunc: "keyDownEvent"}, if value == nil {
KeyUpEvent: {jsEvent: "onkeyup", jsFunc: "keyUpEvent"}, return nil, true
}
switch value := value.(type) {
case func(V, E, E):
return []func(V, E, E){value}, true
case func(V, E):
fn := func(v V, val, _ E) {
value(v, val)
}
return []func(V, E, E){fn}, true
case func(E, E):
fn := func(_ V, val, old E) {
value(val, old)
}
return []func(V, E, E){fn}, true
case func(E):
fn := func(_ V, val, _ E) {
value(val)
}
return []func(V, E, E){fn}, true
case func(V):
fn := func(v V, _, _ E) {
value(v)
}
return []func(V, E, E){fn}, true
case func():
fn := func(V, E, E) {
value()
}
return []func(V, E, E){fn}, true
case []func(V, E, E):
if len(value) == 0 {
return nil, true
}
for _, fn := range value {
if fn == nil {
return nil, false
}
}
return value, true
case []func(V, E):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E, E), count)
for i, fn := range value {
if fn == nil {
return nil, false
}
listeners[i] = func(view V, val, _ E) {
fn(view, val)
}
}
return listeners, true
case []func(E):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E, E), count)
for i, fn := range value {
if fn == nil {
return nil, false
}
listeners[i] = func(_ V, val, _ E) {
fn(val)
}
}
return listeners, true
case []func(E, E):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E, E), count)
for i, fn := range value {
if fn == nil {
return nil, false
}
listeners[i] = func(_ V, val, old E) {
fn(val, old)
}
}
return listeners, true
case []func(V):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E, E), count)
for i, fn := range value {
if fn == nil {
return nil, false
}
listeners[i] = func(view V, _, _ E) {
fn(view)
}
}
return listeners, true
case []func():
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E, E), count)
for i, fn := range value {
if fn == nil {
return nil, false
}
listeners[i] = func(V, E, E) {
fn()
}
}
return listeners, true
case []any:
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
switch fn := v.(type) {
case func(V, E, E):
listeners[i] = fn
case func(V, E):
listeners[i] = func(view V, val, _ E) {
fn(view, val)
}
case func(E, E):
listeners[i] = func(_ V, val, old E) {
fn(val, old)
}
case func(E):
listeners[i] = func(_ V, val, _ E) {
fn(val)
}
case func(V):
listeners[i] = func(view V, _, _ E) {
fn(view)
}
case func():
listeners[i] = func(V, E, E) {
fn()
}
default:
return nil, false
}
}
return listeners, true
}
return nil, false
} }
func (view *viewData) setKeyListener(tag string, value any) bool { func (view *viewData) setKeyListener(tag string, value any) bool {
@ -207,26 +503,57 @@ func (view *viewData) setKeyListener(tag string, value any) bool {
if listeners == nil { if listeners == nil {
view.removeKeyListener(tag) view.removeKeyListener(tag)
} else if js, ok := keyEvents[tag]; ok { } else {
switch tag {
case KeyDownEvent:
view.properties[tag] = listeners view.properties[tag] = listeners
if view.created { if view.created {
view.session.updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)") view.session.updateProperty(view.htmlID(), "onkeydown", "keyDownEvent(this, event)")
} }
} else {
case KeyUpEvent:
view.properties[tag] = listeners
if view.created {
view.session.updateProperty(view.htmlID(), "onkeyup", "keyUpEvent(this, event)")
}
default:
return false return false
} }
}
return true return true
} }
func (view *viewData) removeKeyListener(tag string) { func (view *viewData) removeKeyListener(tag string) {
delete(view.properties, tag) delete(view.properties, tag)
if view.created { if view.created {
if js, ok := keyEvents[tag]; ok { switch tag {
view.session.removeProperty(view.htmlID(), js.jsEvent) case KeyDownEvent:
if !view.Focusable() {
view.session.removeProperty(view.htmlID(), "onkeydown")
}
case KeyUpEvent:
view.session.removeProperty(view.htmlID(), "onkeyup")
} }
} }
} }
func getEventWithOldListeners[V View, E any](view View, subviewID []string, tag string) []func(V, E, E) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.Get(tag); value != nil {
if result, ok := value.([]func(V, E, E)); ok {
return result
}
}
}
return []func(V, E, E){}
}
func getEventListeners[V View, E any](view View, subviewID []string, tag string) []func(V, E) { func getEventListeners[V View, E any](view View, subviewID []string, tag string) []func(V, E) {
if len(subviewID) > 0 && subviewID[0] != "" { if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0]) view = ViewByID(view, subviewID[0])
@ -242,22 +569,52 @@ func getEventListeners[V View, E any](view View, subviewID []string, tag string)
} }
func keyEventsHtml(view View, buffer *strings.Builder) { func keyEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range keyEvents { if len(getEventListeners[View, KeyEvent](view, nil, KeyDownEvent)) > 0 {
if listeners := getEventListeners[View, KeyEvent](view, nil, tag); len(listeners) > 0 { buffer.WriteString(`onkeydown="keyDownEvent(this, event)" `)
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `) } else if view.Focusable() {
if len(getEventListeners[View, MouseEvent](view, nil, ClickEvent)) > 0 {
buffer.WriteString(`onkeydown="keyDownEvent(this, event)" `)
} }
} }
if listeners := getEventListeners[View, KeyEvent](view, nil, KeyUpEvent); len(listeners) > 0 {
buffer.WriteString(`onkeyup="keyUpEvent(this, event)" `)
}
} }
func handleKeyEvents(view View, tag string, data DataObject) { func handleKeyEvents(view View, tag string, data DataObject) {
listeners := getEventListeners[View, KeyEvent](view, nil, tag)
if len(listeners) > 0 {
var event KeyEvent var event KeyEvent
event.init(data) event.init(data)
listeners := getEventListeners[View, KeyEvent](view, nil, tag)
if len(listeners) > 0 {
for _, listener := range listeners { for _, listener := range listeners {
listener(view, event) listener(view, event)
} }
return
}
if tag == KeyDownEvent && view.Focusable() && (event.Key == " " || event.Key == "Enter") && !IsDisabled(view) {
if listeners := getEventListeners[View, MouseEvent](view, nil, ClickEvent); len(listeners) > 0 {
clickEvent := MouseEvent{
TimeStamp: event.TimeStamp,
Button: PrimaryMouseButton,
Buttons: PrimaryMouseMask,
CtrlKey: event.CtrlKey,
AltKey: event.AltKey,
ShiftKey: event.ShiftKey,
MetaKey: event.MetaKey,
ClientX: view.Frame().Width / 2,
ClientY: view.Frame().Height / 2,
X: view.Frame().Width / 2,
Y: view.Frame().Height / 2,
ScreenX: view.Frame().Left + view.Frame().Width/2,
ScreenY: view.Frame().Top + view.Frame().Height/2,
}
for _, listener := range listeners {
listener(view, clickEvent)
}
}
} }
} }

View File

@ -47,7 +47,7 @@ const (
// ListView - the list view interface // ListView - the list view interface
type ListView interface { type ListView interface {
View View
ParanetView ParentView
// ReloadListViewData updates ListView content // ReloadListViewData updates ListView content
ReloadListViewData() ReloadListViewData()
@ -129,78 +129,92 @@ func (listView *listViewData) remove(tag string) {
case Gap: case Gap:
listView.remove(ListRowGap) listView.remove(ListRowGap)
listView.remove(ListColumnGap) listView.remove(ListColumnGap)
return
case Checked: case Checked:
if len(listView.checkedItem) > 0 { if len(listView.checkedItem) == 0 {
return
}
listView.checkedItem = []int{} listView.checkedItem = []int{}
if listView.created { if listView.created {
updateInnerHTML(listView.htmlID(), listView.session) updateInnerHTML(listView.htmlID(), listView.session)
} }
listView.propertyChangedEvent(tag)
}
case Items: case Items:
if listView.adapter != nil { if listView.adapter == nil {
return
}
listView.adapter = nil listView.adapter = nil
if listView.created { if listView.created {
updateInnerHTML(listView.htmlID(), listView.session) updateInnerHTML(listView.htmlID(), listView.session)
} }
listView.propertyChangedEvent(tag)
}
case Orientation, ListWrap: case Orientation, ListWrap:
if _, ok := listView.properties[tag]; ok { if _, ok := listView.properties[tag]; !ok {
return
}
delete(listView.properties, tag) delete(listView.properties, tag)
if listView.created { if listView.created {
updateCSSStyle(listView.htmlID(), listView.session) updateCSSStyle(listView.htmlID(), listView.session)
} }
listView.propertyChangedEvent(tag)
}
case Current: case Current:
current := GetCurrent(listView) current := GetCurrent(listView)
if current == -1 {
return
}
delete(listView.properties, tag) delete(listView.properties, tag)
if listView.created { if listView.created {
updateInnerHTML(listView.htmlID(), listView.session) htmlID := listView.htmlID()
session := listView.session
session.removeProperty(htmlID, "data-current")
updateInnerHTML(htmlID, session)
} }
if current != -1 { if current != -1 {
for _, listener := range listView.selectedListeners { for _, listener := range listView.selectedListeners {
listener(listView, -1) listener(listView, -1)
} }
listView.propertyChangedEvent(tag)
} }
case ItemWidth, ItemHeight, ItemHorizontalAlign, ItemVerticalAlign, ItemCheckbox, case ItemWidth, ItemHeight, ItemHorizontalAlign, ItemVerticalAlign, ItemCheckbox,
CheckboxHorizontalAlign, CheckboxVerticalAlign, ListItemStyle, CurrentStyle, CurrentInactiveStyle: CheckboxHorizontalAlign, CheckboxVerticalAlign:
if _, ok := listView.properties[tag]; ok { if _, ok := listView.properties[tag]; !ok {
return
}
delete(listView.properties, tag) delete(listView.properties, tag)
if listView.created { if listView.created {
updateInnerHTML(listView.htmlID(), listView.session) updateInnerHTML(listView.htmlID(), listView.session)
} }
listView.propertyChangedEvent(tag)
case ListItemStyle, CurrentStyle, CurrentInactiveStyle:
if !listView.setItemStyle(tag, "") {
return
} }
case ListItemClickedEvent: case ListItemClickedEvent:
if len(listView.clickedListeners) > 0 { if len(listView.clickedListeners) == 0 {
listView.clickedListeners = []func(ListView, int){} return
listView.propertyChangedEvent(tag)
} }
listView.clickedListeners = []func(ListView, int){}
case ListItemSelectedEvent: case ListItemSelectedEvent:
if len(listView.selectedListeners) > 0 { if len(listView.selectedListeners) == 0 {
listView.selectedListeners = []func(ListView, int){} return
listView.propertyChangedEvent(tag)
} }
listView.selectedListeners = []func(ListView, int){}
case ListItemCheckedEvent: case ListItemCheckedEvent:
if len(listView.checkedListeners) > 0 { if len(listView.checkedListeners) == 0 {
listView.checkedListeners = []func(ListView, []int){} return
listView.propertyChangedEvent(tag)
} }
listView.checkedListeners = []func(ListView, []int){}
default: default:
listView.viewData.remove(tag) listView.viewData.remove(tag)
return
} }
listView.propertyChangedEvent(tag)
} }
func (listView *listViewData) Set(tag string, value any) bool { func (listView *listViewData) Set(tag string, value any) bool {
@ -268,11 +282,20 @@ func (listView *listViewData) set(tag string, value any) bool {
if !listView.setIntProperty(Current, value) { if !listView.setIntProperty(Current, value) {
return false return false
} }
current := GetCurrent(listView) current := GetCurrent(listView)
if oldCurrent == current { if oldCurrent == current {
return true return true
} }
if listView.created {
htmlID := listView.htmlID()
if current >= 0 {
listView.session.updateProperty(htmlID, "data-current", fmt.Sprintf("%s-%d", htmlID, current))
} else {
listView.session.removeProperty(htmlID, "data-current")
}
}
for _, listener := range listView.selectedListeners { for _, listener := range listView.selectedListeners {
listener(listView, current) listener(listView, current)
} }
@ -290,12 +313,7 @@ func (listView *listViewData) set(tag string, value any) bool {
} }
case ListItemStyle, CurrentStyle, CurrentInactiveStyle: case ListItemStyle, CurrentStyle, CurrentInactiveStyle:
switch value := value.(type) { if !listView.setItemStyle(tag, value) {
case string:
listView.properties[tag] = value
default:
notCompatibleType(tag, value)
return false return false
} }
@ -310,6 +328,33 @@ func (listView *listViewData) set(tag string, value any) bool {
return true return true
} }
func (listView *listViewData) setItemStyle(tag string, value any) bool {
switch value := value.(type) {
case string:
if value == "" {
delete(listView.properties, tag)
} else {
listView.properties[tag] = value
}
default:
notCompatibleType(tag, value)
return false
}
if listView.created {
switch tag {
case CurrentStyle:
listView.session.updateProperty(listView.htmlID(), "data-focusitemstyle", listView.currentStyle())
case CurrentInactiveStyle:
listView.session.updateProperty(listView.htmlID(), "data-bluritemstyle", listView.currentInactiveStyle())
}
}
return true
}
func (listView *listViewData) Get(tag string) any { func (listView *listViewData) Get(tag string) any {
return listView.get(listView.normalizeTag(tag)) return listView.get(listView.normalizeTag(tag))
} }
@ -740,6 +785,9 @@ func (listView *listViewData) checkboxSubviews(self View, buffer *strings.Builde
} }
buffer.WriteString(`" onclick="listItemClickEvent(this, event)" data-left="0" data-top="0" data-width="0" data-height="0" style="display: grid; justify-items: stretch; align-items: stretch;`) buffer.WriteString(`" onclick="listItemClickEvent(this, event)" data-left="0" data-top="0" data-width="0" data-height="0" style="display: grid; justify-items: stretch; align-items: stretch;`)
listView.itemSize(self, buffer) listView.itemSize(self, buffer)
if !listView.adapter.IsListItemEnabled(i) {
buffer.WriteString(`" data-disabled="1`)
}
buffer.WriteString(`">`) buffer.WriteString(`">`)
buffer.WriteString(itemDiv) buffer.WriteString(itemDiv)
@ -796,6 +844,9 @@ func (listView *listViewData) noneCheckboxSubviews(self View, buffer *strings.Bu
} }
buffer.WriteString(`" `) buffer.WriteString(`" `)
buffer.WriteString(itemStyle) buffer.WriteString(itemStyle)
if !listView.adapter.IsListItemEnabled(i) {
buffer.WriteString(` data-disabled="1"`)
}
buffer.WriteString(`>`) buffer.WriteString(`>`)
if view := listView.getItemView(i); view != nil { if view := listView.getItemView(i); view != nil {

View File

@ -184,14 +184,22 @@ func (view *viewData) removeMouseListener(tag string) {
} }
} }
func mouseEventsHtml(view View, buffer *strings.Builder) { func mouseEventsHtml(view View, buffer *strings.Builder, hasTooltip bool) {
for tag, js := range mouseEvents { for tag, js := range mouseEvents {
if value := view.getRaw(tag); value != nil { if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View, MouseEvent)); ok && len(listeners) > 0 { if listeners, ok := value.([]func(View, MouseEvent)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `) buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
} }
} }
} }
if hasTooltip {
buffer.WriteString(`onmouseenter="mouseEnterEvent(this, event)" `)
buffer.WriteString(`onmouseleave="mouseLeaveEvent(this, event)" `)
}
} }
func getTimeStamp(data DataObject) uint64 { func getTimeStamp(data DataObject) uint64 {

View File

@ -29,7 +29,7 @@ type NumberPicker interface {
type numberPickerData struct { type numberPickerData struct {
viewData viewData
numberChangedListeners []func(NumberPicker, float64) numberChangedListeners []func(NumberPicker, float64, float64)
} }
// NewNumberPicker create new NumberPicker object and return it // NewNumberPicker create new NumberPicker object and return it
@ -47,7 +47,7 @@ func newNumberPicker(session Session) View {
func (picker *numberPickerData) init(session Session) { func (picker *numberPickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "NumberPicker" picker.tag = "NumberPicker"
picker.numberChangedListeners = []func(NumberPicker, float64){} picker.numberChangedListeners = []func(NumberPicker, float64, float64){}
} }
func (picker *numberPickerData) String() string { func (picker *numberPickerData) String() string {
@ -76,7 +76,20 @@ func (picker *numberPickerData) remove(tag string) {
switch tag { switch tag {
case NumberChangedEvent: case NumberChangedEvent:
if len(picker.numberChangedListeners) > 0 { if len(picker.numberChangedListeners) > 0 {
picker.numberChangedListeners = []func(NumberPicker, float64){} picker.numberChangedListeners = []func(NumberPicker, float64, float64){}
picker.propertyChangedEvent(tag)
}
case NumberPickerValue:
oldValue := GetNumberPickerValue(picker)
picker.viewData.remove(tag)
if oldValue != 0 {
if picker.created {
picker.session.callFunc("setInputValue", picker.htmlID(), 0)
}
for _, listener := range picker.numberChangedListeners {
listener(picker, 0, oldValue)
}
picker.propertyChangedEvent(tag) picker.propertyChangedEvent(tag)
} }
@ -98,12 +111,12 @@ func (picker *numberPickerData) set(tag string, value any) bool {
switch tag { switch tag {
case NumberChangedEvent: case NumberChangedEvent:
listeners, ok := valueToEventListeners[NumberPicker, float64](value) listeners, ok := valueToEventWithOldListeners[NumberPicker, float64](value)
if !ok { if !ok {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return false
} else if listeners == nil { } else if listeners == nil {
listeners = []func(NumberPicker, float64){} listeners = []func(NumberPicker, float64, float64){}
} }
picker.numberChangedListeners = listeners picker.numberChangedListeners = listeners
picker.propertyChangedEvent(tag) picker.propertyChangedEvent(tag)
@ -119,7 +132,7 @@ func (picker *numberPickerData) set(tag string, value any) bool {
picker.session.callFunc("setInputValue", picker.htmlID(), newValue) picker.session.callFunc("setInputValue", picker.htmlID(), newValue)
} }
for _, listener := range picker.numberChangedListeners { for _, listener := range picker.numberChangedListeners {
listener(picker, f) listener(picker, f, oldValue)
} }
picker.propertyChangedEvent(tag) picker.propertyChangedEvent(tag)
} }
@ -159,13 +172,6 @@ func (picker *numberPickerData) propertyChanged(tag string) {
} else { } else {
picker.session.updateProperty(picker.htmlID(), Step, "any") picker.session.updateProperty(picker.htmlID(), Step, "any")
} }
case NumberPickerValue:
value := GetNumberPickerValue(picker)
picker.session.callFunc("setInputValue", picker.htmlID(), value)
for _, listener := range picker.numberChangedListeners {
listener(picker, value)
}
} }
} }
} }
@ -242,7 +248,7 @@ func (picker *numberPickerData) handleCommand(self View, command string, data Da
picker.properties[NumberPickerValue] = text picker.properties[NumberPickerValue] = text
if value != oldValue { if value != oldValue {
for _, listener := range picker.numberChangedListeners { for _, listener := range picker.numberChangedListeners {
listener(picker, value) listener(picker, value, oldValue)
} }
} }
} }
@ -323,6 +329,6 @@ func GetNumberPickerValue(view View, subviewID ...string) float64 {
// GetNumberChangedListeners returns the NumberChangedListener list of an NumberPicker subview. // GetNumberChangedListeners returns the NumberChangedListener list of an NumberPicker subview.
// If there are no listeners then the empty list is returned // If there are no listeners then the empty list is returned
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetNumberChangedListeners(view View, subviewID ...string) []func(NumberPicker, float64) { func GetNumberChangedListeners(view View, subviewID ...string) []func(NumberPicker, float64, float64) {
return getEventListeners[NumberPicker, float64](view, subviewID, NumberChangedEvent) return getEventWithOldListeners[NumberPicker, float64](view, subviewID, NumberChangedEvent)
} }

View File

@ -129,7 +129,10 @@ func pointerEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range pointerEvents { for tag, js := range pointerEvents {
if value := view.getRaw(tag); value != nil { if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View, PointerEvent)); ok && len(listeners) > 0 { if listeners, ok := value.([]func(View, PointerEvent)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `) buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
} }
} }
} }

100
popup.go
View File

@ -78,11 +78,21 @@ const (
// LeftArrow is value of the popup "arrow" property: // LeftArrow is value of the popup "arrow" property:
// Arrow on the left side of the pop-up window // Arrow on the left side of the pop-up window
LeftArrow = 4 LeftArrow = 4
// NormalButton is the constant of the popup button type: the normal button
NormalButton PopupButtonType = 0
// DefaultButton is the constant of the popup button type: button that fires when the "Enter" key is pressed
DefaultButton PopupButtonType = 1
// CancelButton is the constant of the popup button type: button that fires when the "Escape" key is pressed
CancelButton PopupButtonType = 2
) )
type PopupButtonType int
// PopupButton describes a button that will be placed at the bottom of the window. // PopupButton describes a button that will be placed at the bottom of the window.
type PopupButton struct { type PopupButton struct {
Title string Title string
Type PopupButtonType
OnClick func(Popup) OnClick func(Popup)
} }
@ -95,11 +105,14 @@ type Popup interface {
onDismiss() onDismiss()
html(buffer *strings.Builder) html(buffer *strings.Builder)
viewByHTMLID(id string) View viewByHTMLID(id string) View
keyEvent(event KeyEvent) bool
} }
type popupData struct { type popupData struct {
layerView View layerView View
view View view View
buttons []PopupButton
cancelable bool
dismissListener []func(Popup) dismissListener []func(Popup)
} }
@ -273,6 +286,7 @@ func (arrow *popupArrow) createView(popupView View) View {
func (popup *popupData) init(view View, popupParams Params) { func (popup *popupData) init(view View, popupParams Params) {
popup.view = view popup.view = view
popup.cancelable = false
session := view.Session() session := view.Session()
columnCount := 3 columnCount := 3
@ -392,13 +406,15 @@ func (popup *popupData) init(view View, popupParams Params) {
TextSize: Px(20), TextSize: Px(20),
Content: "✕", Content: "✕",
NotTranslate: true, NotTranslate: true,
ClickEvent: func(View) { ClickEvent: popup.cancel,
popup.Dismiss()
},
}) })
popup.cancelable = true
case OutsideClose: case OutsideClose:
outsideClose, _ = boolProperty(popupParams, OutsideClose, session) outsideClose, _ = boolProperty(popupParams, OutsideClose, session)
if outsideClose {
popup.cancelable = true
}
case Buttons: case Buttons:
switch value := value.(type) { switch value := value.(type) {
@ -494,6 +510,7 @@ func (popup *popupData) init(view View, popupParams Params) {
view.Set(Row, viewRow) view.Set(Row, viewRow)
popupView.Append(view) popupView.Append(view)
popup.buttons = buttons
if buttonCount := len(buttons); buttonCount > 0 { if buttonCount := len(buttons); buttonCount > 0 {
buttonsAlign, _ := enumProperty(params, ButtonsAlign, session, RightAlign) buttonsAlign, _ := enumProperty(params, ButtonsAlign, session, RightAlign)
popupCellHeight = append(popupCellHeight, AutoSize()) popupCellHeight = append(popupCellHeight, AutoSize())
@ -511,21 +528,31 @@ func (popup *popupData) init(view View, popupParams Params) {
buttonsPanel.Set(Margin, gap) buttonsPanel.Set(Margin, gap)
} }
createButton := func(n int, button PopupButton) Button {
return NewButton(session, Params{
Column: n,
Content: button.Title,
ClickEvent: func() {
if button.OnClick != nil {
button.OnClick(popup)
} else {
popup.Dismiss()
}
},
})
}
for i, button := range buttons { for i, button := range buttons {
buttonsPanel.Append(createButton(i, button)) title := button.Title
if title == "" && button.Type == CancelButton {
title = "Cancel"
}
buttonView := NewButton(session, Params{
Column: i,
Content: title,
})
if button.OnClick != nil {
fn := button.OnClick
buttonView.Set(ClickEvent, func() {
fn(popup)
})
} else if button.Type == CancelButton {
buttonView.Set(ClickEvent, popup.cancel)
}
if button.Type == DefaultButton {
buttonView.Set(Style, "ruiDefaultButton")
}
buttonsPanel.Append(buttonView)
} }
popupView.Append(NewGridLayout(session, Params{ popupView.Append(NewGridLayout(session, Params{
@ -544,11 +571,8 @@ func (popup *popupData) init(view View, popupParams Params) {
} }
popup.layerView = NewGridLayout(session, layerParams) popup.layerView = NewGridLayout(session, layerParams)
if outsideClose { if outsideClose {
popup.layerView.Set(ClickEvent, func(View) { popup.layerView.Set(ClickEvent, popup.cancel)
popup.Dismiss()
})
} }
} }
@ -560,12 +584,21 @@ func (popup *popupData) Session() Session {
return popup.view.Session() return popup.view.Session()
} }
func (popup *popupData) cancel() {
for _, button := range popup.buttons {
if button.Type == CancelButton && button.OnClick != nil {
button.OnClick(popup)
return
}
}
popup.Dismiss()
}
func (popup *popupData) Dismiss() { func (popup *popupData) Dismiss() {
popup.Session().popupManager().dismissPopup(popup) popup.Session().popupManager().dismissPopup(popup)
for _, listener := range popup.dismissListener { for _, listener := range popup.dismissListener {
listener(popup) listener(popup)
} }
// TODO
} }
func (popup *popupData) Show() { func (popup *popupData) Show() {
@ -587,6 +620,27 @@ func (popup *popupData) onDismiss() {
} }
} }
func (popup *popupData) keyEvent(event KeyEvent) bool {
if !event.AltKey && !event.CtrlKey && !event.ShiftKey && !event.MetaKey {
switch event.Code {
case EnterKey:
for _, button := range popup.buttons {
if button.Type == DefaultButton && button.OnClick != nil {
button.OnClick(popup)
return true
}
}
case EscapeKey:
if popup.cancelable {
popup.Dismiss()
return true
}
}
}
return false
}
// NewPopup creates a new Popup // NewPopup creates a new Popup
func NewPopup(view View, param Params) Popup { func NewPopup(view View, param Params) Popup {
if view == nil { if view == nil {
@ -637,6 +691,8 @@ func (manager *popupManager) showPopup(popup Popup) {
session.callFunc("blurCurrent") session.callFunc("blurCurrent")
manager.updatePopupLayerInnerHTML(session) manager.updatePopupLayerInnerHTML(session)
session.updateCSSProperty("ruiTooltipLayer", "visibility", "hidden")
session.updateCSSProperty("ruiTooltipLayer", "opacity", "0")
session.updateCSSProperty("ruiPopupLayer", "visibility", "visible") session.updateCSSProperty("ruiPopupLayer", "visibility", "visible")
session.updateCSSProperty("ruiRoot", "pointer-events", "none") session.updateCSSProperty("ruiRoot", "pointer-events", "none")
} }

View File

@ -28,17 +28,9 @@ func ShowQuestion(title, text string, session Session, onYes func(), onNo func()
CloseButton: false, CloseButton: false,
OutsideClose: false, OutsideClose: false,
Buttons: []PopupButton{ Buttons: []PopupButton{
{
Title: "No",
OnClick: func(popup Popup) {
popup.Dismiss()
if onNo != nil {
onNo()
}
},
},
{ {
Title: "Yes", Title: "Yes",
Type: DefaultButton,
OnClick: func(popup Popup) { OnClick: func(popup Popup) {
popup.Dismiss() popup.Dismiss()
if onYes != nil { if onYes != nil {
@ -46,6 +38,16 @@ func ShowQuestion(title, text string, session Session, onYes func(), onNo func()
} }
}, },
}, },
{
Title: "No",
Type: CancelButton,
OnClick: func(popup Popup) {
popup.Dismiss()
if onNo != nil {
onNo()
}
},
},
}, },
} }
if title != "" { if title != "" {
@ -68,11 +70,12 @@ func ShowCancellableQuestion(title, text string, session Session, onYes func(),
OutsideClose: false, OutsideClose: false,
Buttons: []PopupButton{ Buttons: []PopupButton{
{ {
Title: "Cancel", Title: "Yes",
Type: DefaultButton,
OnClick: func(popup Popup) { OnClick: func(popup Popup) {
popup.Dismiss() popup.Dismiss()
if onCancel != nil { if onYes != nil {
onCancel() onYes()
} }
}, },
}, },
@ -86,11 +89,12 @@ func ShowCancellableQuestion(title, text string, session Session, onYes func(),
}, },
}, },
{ {
Title: "Yes", Title: "Cancel",
Type: CancelButton,
OnClick: func(popup Popup) { OnClick: func(popup Popup) {
popup.Dismiss() popup.Dismiss()
if onYes != nil { if onCancel != nil {
onYes() onCancel()
} }
}, },
}, },
@ -127,10 +131,14 @@ func (popup *popupMenuData) ListSize() int {
} }
func (popup *popupMenuData) ListItem(index int, session Session) View { func (popup *popupMenuData) ListItem(index int, session Session) View {
return NewTextView(popup.session, Params{ view := NewTextView(popup.session, Params{
Text: popup.items[index], Text: popup.items[index],
Style: "ruiPopupMenuItem", Style: "ruiPopupMenuItem",
}) })
if !popup.IsListItemEnabled(index) {
view.Set(TextColor, "@ruiDisabledTextColor")
}
return view
} }
func (popup *popupMenuData) IsListItemEnabled(index int) bool { func (popup *popupMenuData) IsListItemEnabled(index int) bool {

View File

@ -277,6 +277,10 @@ const (
// The "outline-width" SizeUnit property sets the width of an view's outline. // The "outline-width" SizeUnit property sets the width of an view's outline.
OutlineWidth = "outline-width" OutlineWidth = "outline-width"
// OutlineWidth is the constant for the "outline-offset" property tag.
// The "outline-offset" SizeUnit property sets the amount of space between an outline and the edge or border of an element..
OutlineOffset = "outline-offset"
// Shadow is the constant for the "shadow" property tag. // Shadow is the constant for the "shadow" property tag.
// The "shadow" property adds shadow effects around a view's frame. A shadow is described // The "shadow" property adds shadow effects around a view's frame. A shadow is described
// by X and Y offsets relative to the element, blur and spread radius, and color. // by X and Y offsets relative to the element, blur and spread radius, and color.
@ -421,7 +425,7 @@ const (
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
VerticalTextOrientation = "vertical-text-orientation" VerticalTextOrientation = "vertical-text-orientation"
// TextTverflow is the constant for the "text-overflow" property tag. // TextOverflow is the constant for the "text-overflow" property tag.
// The "text-overflow" int property sets how hidden overflow content is signaled to users. // The "text-overflow" int property sets how hidden overflow content is signaled to users.
// It can be clipped or display an ellipsis ('…'). Valid values are // It can be clipped or display an ellipsis ('…'). Valid values are
TextOverflow = "text-overflow" TextOverflow = "text-overflow"
@ -538,7 +542,7 @@ const (
// AvoidBreak is the constant for the "avoid-break" property tag. // AvoidBreak is the constant for the "avoid-break" property tag.
// The "avoid-break" bool property sets how region breaks should behave inside a generated box. // The "avoid-break" bool property sets how region breaks should behave inside a generated box.
// If the property value is "true" then fvoids any break from being inserted within the principal box. // If the property value is "true" then avoids any break from being inserted within the principal box.
// If the property value is "false" then allows, but does not force, any break to be inserted within // If the property value is "false" then allows, but does not force, any break to be inserted within
// the principal box. // the principal box.
AvoidBreak = "avoid-break" AvoidBreak = "avoid-break"
@ -656,7 +660,7 @@ const (
// allowing text and inline Views to wrap around it. // allowing text and inline Views to wrap around it.
Float = "float" Float = "float"
// UsetData is the constant for the "user-data" property tag. // UserData is the constant for the "user-data" property tag.
// The "user-data" property can contain any user data // The "user-data" property can contain any user data
UserData = "user-data" UserData = "user-data"
@ -669,4 +673,35 @@ const (
// The "user-select" bool property controls whether the user can select text. // The "user-select" bool property controls whether the user can select text.
// This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used.
UserSelect = "user-select" UserSelect = "user-select"
// Order is the constant for the "Order" property tag.
// The "Order" int property sets the order to layout an item in a ListLayout or GridLayout container.
// Items in a container are sorted by ascending order value and then by their source code order.
Order = "Order"
// BackgroundBlendMode is the constant for the "background-blend-mode" property tag.
// The "background-blend-mode" int property sets how an view's background images should blend
// with each other and with the view's background color.
// Valid values are "normal" (0), "multiply" (1), "screen" (2), "overlay" (3), "darken" (4), "lighten" (5),
// "color-dodge" (6), "color-burn" (7), "hard-light" (8), "soft-light" (9), "difference" (10),
// "exclusion" (11), "hue" (12), "saturation" (13), "color" (14), "luminosity" (15).
BackgroundBlendMode = "background-blend-mode"
// MixBlendMode is the constant for the "mix-blend-mode" property tag.
// The "mix-blend-mode" int property sets how a view's content should blend
// with the content of the view's parent and the view's background.
// Valid values are "normal" (0), "multiply" (1), "screen" (2), "overlay" (3), "darken" (4), "lighten" (5),
// "color-dodge" (6), "color-burn" (7), "hard-light" (8), "soft-light" (9), "difference" (10),
// "exclusion" (11), "hue" (12), "saturation" (13), "color" (14), "luminosity" (15).
MixBlendMode = "mix-blend-mode"
// TabIndex is the constant for the "tabindex" property tag.
// The "tabindex" int property indicates that View can be focused, and where it participates in sequential keyboard navigation
// (usually with the Tab key, hence the name).
// * A negative value means that View is not reachable via sequential keyboard navigation, but could be focused by clicking with the mouse or touching.
// * tabindex="0" means that View should be focusable in sequential keyboard navigation, after any positive tabindex values and its order is defined in order of its addition.
// * A positive value means View should be focusable in sequential keyboard navigation, with its order defined by the value of the number.
TabIndex = "tabindex"
Tooltip = "tooltip"
) )

View File

@ -63,6 +63,7 @@ var boolProperties = []string{
TabCloseButton, TabCloseButton,
Repeating, Repeating,
UserSelect, UserSelect,
ColumnSpanAll,
} }
var intProperties = []string{ var intProperties = []string{
@ -73,6 +74,8 @@ var intProperties = []string{
RowSpan, RowSpan,
ColumnSpan, ColumnSpan,
ColumnCount, ColumnCount,
Order,
TabIndex,
} }
var floatProperties = map[string]struct{ min, max float64 }{ var floatProperties = map[string]struct{ min, max float64 }{
@ -133,6 +136,7 @@ var sizeProperties = map[string]string{
BorderTopWidth: BorderTopWidth, BorderTopWidth: BorderTopWidth,
BorderBottomWidth: BorderBottomWidth, BorderBottomWidth: BorderBottomWidth,
OutlineWidth: OutlineWidth, OutlineWidth: OutlineWidth,
OutlineOffset: OutlineOffset,
XOffset: XOffset, XOffset: XOffset,
YOffset: YOffset, YOffset: YOffset,
BlurRadius: BlurRadius, BlurRadius: BlurRadius,
@ -447,6 +451,21 @@ var enumProperties = map[string]struct {
"", "",
[]string{"none", "top", "right", "bottom", "left"}, []string{"none", "top", "right", "bottom", "left"},
}, },
MixBlendMode: {
[]string{"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"},
MixBlendMode,
[]string{"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"},
},
BackgroundBlendMode: {
[]string{"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"},
BackgroundBlendMode,
[]string{"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"},
},
ColumnFill: {
[]string{"balance", "auto"},
ColumnFill,
[]string{"balance", "auto"},
},
} }
func notCompatibleType(tag string, value any) { func notCompatibleType(tag string, value any) {

View File

@ -307,4 +307,92 @@ const (
// "dense" packing algorithm attempts to fill in holes earlier in the grid, if smaller items come up later. // "dense" packing algorithm attempts to fill in holes earlier in the grid, if smaller items come up later.
// This may cause views to appear out-of-order, when doing so would fill in holes left by larger views. // This may cause views to appear out-of-order, when doing so would fill in holes left by larger views.
ColumnDenseAutoFlow = 3 ColumnDenseAutoFlow = 3
// BlendNormal - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the top color, regardless of what the bottom color is.
// The effect is like two opaque pieces of paper overlapping.
BlendNormal = 0
// BlendMultiply - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of multiplying the top and bottom colors.
// A black layer leads to a black final layer, and a white layer leads to no change.
// The effect is like two images printed on transparent film overlapping.
BlendMultiply = 1
// BlendScreen - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of inverting the colors, multiplying them, and inverting that value.
// A black layer leads to no change, and a white layer leads to a white final layer.
// The effect is like two images shone onto a projection screen.
BlendScreen = 2
// BlendOverlay - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of multiply if the bottom color is darker, or screen if the bottom color is lighter.
// This blend mode is equivalent to hard-light but with the layers swapped.
BlendOverlay = 3
// BlendDarken - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is composed of the darkest values of each color channel.
BlendDarken = 4
// BlendLighten - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is composed of the lightest values of each color channel.
BlendLighten = 5
// BlendColorDodge - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of dividing the bottom color by the inverse of the top color.
// A black foreground leads to no change. A foreground with the inverse color of the backdrop leads to a fully lit color.
// This blend mode is similar to screen, but the foreground need only be as light as the inverse of the backdrop to create a fully lit color.
BlendColorDodge = 6
// BlendColorBurn - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of inverting the bottom color, dividing the value by the top color, and inverting that value.
// A white foreground leads to no change. A foreground with the inverse color of the backdrop leads to a black final image.
// This blend mode is similar to multiply, but the foreground need only be as dark as the inverse of the backdrop to make the final image black.
BlendColorBurn = 7
// BlendHardLight - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of multiply if the top color is darker, or screen if the top color is lighter.
// This blend mode is equivalent to overlay but with the layers swapped. The effect is similar to shining a harsh spotlight on the backdrop.
BlendHardLight = 8
// BlendSoftLight - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is similar to hard-light, but softer. This blend mode behaves similar to hard-light.
// The effect is similar to shining a diffused spotlight on the backdrop*.*
BlendSoftLight = 9
// BlendDifference - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of subtracting the darker of the two colors from the lighter one.
// A black layer has no effect, while a white layer inverts the other layer's color.
BlendDifference = 10
// BlendExclusion - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is similar to difference, but with less contrast.
// As with difference, a black layer has no effect, while a white layer inverts the other layer's color.
BlendExclusion = 11
// BlendHue - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color has the hue of the top color, while using the saturation and luminosity of the bottom color.
BlendHue = 12
// BlendSaturation - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color has the saturation of the top color, while using the hue and luminosity of the bottom color.
// A pure gray backdrop, having no saturation, will have no effect.
BlendSaturation = 13
// BlendColor - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color has the hue and saturation of the top color, while using the luminosity of the bottom color.
// The effect preserves gray levels and can be used to colorize the foreground.
BlendColor = 14
// BlendLuminosity - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color has the luminosity of the top color, while using the hue and saturation of the bottom color.
// This blend mode is equivalent to BlendColor, but with the layers swapped.
BlendLuminosity = 15
// ColumnFillBalance - value of the "column-fill" property: content is equally divided between columns.
ColumnFillBalance = 0
// ColumnFillAuto - value of the "column-fill" property:
// Columns are filled sequentially. Content takes up only the room it needs, possibly resulting in some columns remaining empty.
ColumnFillAuto = 1
) )

View File

@ -34,7 +34,7 @@ const (
// Resizable - grid-container of View // Resizable - grid-container of View
type Resizable interface { type Resizable interface {
View View
ParanetView ParentView
} }
type resizableData struct { type resizableData struct {

View File

@ -123,7 +123,7 @@ func scanEmbedImagesDir(fs *embed.FS, dir, prefix string) {
} else { } else {
ext := strings.ToLower(filepath.Ext(name)) ext := strings.ToLower(filepath.Ext(name))
switch ext { switch ext {
case ".png", ".jpg", ".jpeg", ".svg", ".gif", ".bmp": case ".png", ".jpg", ".jpeg", ".svg", ".gif", ".bmp", ".webp":
registerImage(fs, path, prefix+name) registerImage(fs, path, prefix+name)
} }
} }
@ -272,8 +272,8 @@ func serveResourceFile(filename string, w http.ResponseWriter, r *http.Request)
if serveEmbed(fs, dir+"/"+filename) { if serveEmbed(fs, dir+"/"+filename) {
return true return true
} }
if subdirs, err := fs.ReadDir(dir); err == nil { if subDirs, err := fs.ReadDir(dir); err == nil {
for _, subdir := range subdirs { for _, subdir := range subDirs {
if subdir.IsDir() { if subdir.IsDir() {
if serveEmbed(fs, dir+"/"+subdir.Name()+"/"+filename) { if serveEmbed(fs, dir+"/"+subdir.Name()+"/"+filename) {
return true return true

View File

@ -20,13 +20,13 @@ type webBridge interface {
writeMessage(text string) bool writeMessage(text string) bool
addAnimationCSS(css string) addAnimationCSS(css string)
clearAnimation() clearAnimation()
cavnasStart(htmlID string) canvasStart(htmlID string)
callCanvasFunc(funcName string, args ...any) callCanvasFunc(funcName string, args ...any)
callCanvasVarFunc(v any, funcName string, args ...any) callCanvasVarFunc(v any, funcName string, args ...any)
callCanvasImageFunc(url string, property string, funcName string, args ...any) callCanvasImageFunc(url string, property string, funcName string, args ...any)
createCanvasVar(funcName string, args ...any) any createCanvasVar(funcName string, args ...any) any
updateCanvasProperty(property string, value any) updateCanvasProperty(property string, value any)
cavnasFinish() canvasFinish()
canvasTextMetrics(htmlID, font, text string) TextMetrics canvasTextMetrics(htmlID, font, text string) TextMetrics
htmlPropertyValue(htmlID, name string) string htmlPropertyValue(htmlID, name string) string
answerReceived(answer DataObject) answerReceived(answer DataObject)
@ -100,6 +100,17 @@ type Session interface {
// OpenURL opens the url in the new browser tab // OpenURL opens the url in the new browser tab
OpenURL(url string) OpenURL(url string)
// ClientItem reads value by key from the client-side storage
ClientItem(key string) (string, bool)
// SetClientItem stores a key-value pair in the client-side storage
SetClientItem(key, value string)
// RemoveAllClientItems removes all key-value pair from the client-side storage
RemoveAllClientItems()
// SetHotKey sets the function that will be called when the given hotkey is pressed.
// Invoke SetHotKey(..., ..., nil) for remove hotkey function.
SetHotKey(keyCode KeyCode, controlKeys ControlKeyMask, fn func(Session))
getCurrentTheme() Theme getCurrentTheme() Theme
registerAnimation(props []AnimatedProperty) string registerAnimation(props []AnimatedProperty) string
@ -125,13 +136,13 @@ type Session interface {
finishUpdateScript(htmlID string) finishUpdateScript(htmlID string)
addAnimationCSS(css string) addAnimationCSS(css string)
clearAnimation() clearAnimation()
cavnasStart(htmlID string) canvasStart(htmlID string)
callCanvasFunc(funcName string, args ...any) callCanvasFunc(funcName string, args ...any)
createCanvasVar(funcName string, args ...any) any createCanvasVar(funcName string, args ...any) any
callCanvasVarFunc(v any, funcName string, args ...any) callCanvasVarFunc(v any, funcName string, args ...any)
callCanvasImageFunc(url string, property string, funcName string, args ...any) callCanvasImageFunc(url string, property string, funcName string, args ...any)
updateCanvasProperty(property string, value any) updateCanvasProperty(property string, value any)
cavnasFinish() canvasFinish()
canvasTextMetrics(htmlID, font, text string) TextMetrics canvasTextMetrics(htmlID, font, text string) TextMetrics
htmlPropertyValue(htmlID, name string) string htmlPropertyValue(htmlID, name string) string
handleAnswer(data DataObject) handleAnswer(data DataObject)
@ -183,6 +194,8 @@ type sessionData struct {
animationCounter int animationCounter int
animationCSS string animationCSS string
updateScripts map[string]*strings.Builder updateScripts map[string]*strings.Builder
clientStorage map[string]string
hotkeys map[string]func(Session)
} }
func newSession(app Application, id int, customTheme string, params DataObject) Session { func newSession(app Application, id int, customTheme string, params DataObject) Session {
@ -199,6 +212,8 @@ func newSession(app Application, id int, customTheme string, params DataObject)
session.animationCounter = 0 session.animationCounter = 0
session.animationCSS = "" session.animationCSS = ""
session.updateScripts = map[string]*strings.Builder{} session.updateScripts = map[string]*strings.Builder{}
session.clientStorage = map[string]string{}
session.hotkeys = map[string]func(Session){}
if customTheme != "" { if customTheme != "" {
if theme, ok := CreateThemeFromText(customTheme); ok { if theme, ok := CreateThemeFromText(customTheme); ok {
@ -290,9 +305,28 @@ func (session *sessionData) writeInitScript(writer *strings.Builder) {
if session.rootView != nil { if session.rootView != nil {
writer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`) writer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`)
viewHTML(session.rootView, writer) buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
viewHTML(session.rootView, buffer)
text := strings.ReplaceAll(buffer.String(), "'", `\'`)
writer.WriteString(text)
writer.WriteString("';\nscanElementsSize();") writer.WriteString("';\nscanElementsSize();")
} }
session.updateTooltipConstants()
}
func (session *sessionData) updateTooltipConstants() {
if color, ok := session.Color("ruiTooltipBackground"); ok {
session.bridge.callFunc("setCssVar", "--tooltip-background", color.cssString())
}
if color, ok := session.Color("ruiTooltipTextColor"); ok {
session.bridge.callFunc("setCssVar", "--tooltip-text-color", color.cssString())
}
if color, ok := session.Color("ruiTooltipShadowColor"); ok {
session.bridge.callFunc("setCssVar", "--tooltip-shadow-color", color.cssString())
}
} }
func (session *sessionData) reload() { func (session *sessionData) reload() {
@ -313,6 +347,7 @@ func (session *sessionData) reload() {
} }
session.bridge.writeMessage(buffer.String()) session.bridge.writeMessage(buffer.String())
session.updateTooltipConstants()
} }
func (session *sessionData) ignoreViewUpdates() bool { func (session *sessionData) ignoreViewUpdates() bool {
@ -424,9 +459,9 @@ func (session *sessionData) clearAnimation() {
} }
} }
func (session *sessionData) cavnasStart(htmlID string) { func (session *sessionData) canvasStart(htmlID string) {
if session.bridge != nil { if session.bridge != nil {
session.bridge.cavnasStart(htmlID) session.bridge.canvasStart(htmlID)
} }
} }
@ -461,9 +496,9 @@ func (session *sessionData) callCanvasImageFunc(url string, property string, fun
} }
} }
func (session *sessionData) cavnasFinish() { func (session *sessionData) canvasFinish() {
if session.bridge != nil { if session.bridge != nil {
session.bridge.cavnasFinish() session.bridge.canvasFinish()
} }
} }
@ -512,7 +547,7 @@ func (session *sessionData) handleRootSize(data DataObject) {
} }
func (session *sessionData) handleResize(data DataObject) { func (session *sessionData) handleResize(data DataObject) {
if node := data.PropertyWithTag("views"); node != nil && node.Type() == ArrayNode { if node := data.PropertyByTag("views"); node != nil && node.Type() == ArrayNode {
for _, el := range node.ArrayElements() { for _, el := range node.ArrayElements() {
if el.IsObject() { if el.IsObject() {
obj := el.Object() obj := el.Object()
@ -587,6 +622,16 @@ func (session *sessionData) handleSessionInfo(params DataObject) {
session.pixelRatio = f session.pixelRatio = f
} }
} }
if node := params.PropertyByTag("storage"); node != nil && node.Type() == ObjectNode {
if obj := node.Object(); obj != nil {
for i := 0; i < obj.PropertyCount(); i++ {
if element := obj.Property(i); element.Type() == TextNode {
session.clientStorage[element.Tag()] = element.Text()
}
}
}
}
} }
func (session *sessionData) handleEvent(command string, data DataObject) { func (session *sessionData) handleEvent(command string, data DataObject) {
@ -606,17 +651,89 @@ func (session *sessionData) handleEvent(command string, data DataObject) {
case "sessionInfo": case "sessionInfo":
session.handleSessionInfo(data) session.handleSessionInfo(data)
case "storageError":
if text, ok := data.PropertyValue("error"); ok {
ErrorLog(text)
}
default: default:
if viewID, ok := data.PropertyValue("id"); ok { if viewID, ok := data.PropertyValue("id"); ok {
if viewID != "body" {
if view := session.viewByHTMLID(viewID); view != nil { if view := session.viewByHTMLID(viewID); view != nil {
view.handleCommand(view, command, data) view.handleCommand(view, command, data)
} }
} else if command != "clickOutsidePopup" { }
if command == KeyDownEvent {
var event KeyEvent
event.init(data)
session.hotKey(event)
}
} else {
ErrorLog(`"id" property not found. Event: ` + command) ErrorLog(`"id" property not found. Event: ` + command)
} }
} }
} }
func (session *sessionData) hotKey(event KeyEvent) {
popups := session.popupManager().popups
if count := len(popups); count > 0 {
if popups[count-1].keyEvent(event) {
return
}
}
var controlKeys ControlKeyMask = 0
if event.AltKey {
controlKeys |= AltKey
}
if event.CtrlKey {
controlKeys |= CtrlKey
}
if event.MetaKey {
controlKeys |= MetaKey
}
if event.ShiftKey {
controlKeys |= ShiftKey
}
key := hotkeyCode(KeyCode(event.Code), controlKeys)
if fn, ok := session.hotkeys[key]; ok && fn != nil {
fn(session)
}
}
func hotkeyCode(keyCode KeyCode, controlKeys ControlKeyMask) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(strings.ToLower(string(keyCode)))
if controlKeys != 0 {
buffer.WriteRune('-')
if controlKeys&AltKey != 0 {
buffer.WriteRune('a')
}
if controlKeys&CtrlKey != 0 {
buffer.WriteRune('c')
}
if controlKeys&MetaKey != 0 {
buffer.WriteRune('m')
}
if controlKeys&ShiftKey != 0 {
buffer.WriteRune('s')
}
}
return buffer.String()
}
func (session *sessionData) SetHotKey(keyCode KeyCode, controlKeys ControlKeyMask, fn func(Session)) {
hotkey := hotkeyCode(keyCode, controlKeys)
if fn == nil {
delete(session.hotkeys, hotkey)
} else {
session.hotkeys[hotkey] = fn
}
}
func (session *sessionData) SetTitle(title string) { func (session *sessionData) SetTitle(title string) {
title, _ = session.GetString(title) title, _ = session.GetString(title)
session.callFunc("setTitle", title) session.callFunc("setTitle", title)
@ -637,3 +754,18 @@ func (session *sessionData) OpenURL(urlStr string) {
} }
session.callFunc("openURL", urlStr) session.callFunc("openURL", urlStr)
} }
func (session *sessionData) ClientItem(key string) (string, bool) {
value, ok := session.clientStorage[key]
return value, ok
}
func (session *sessionData) SetClientItem(key, value string) {
session.clientStorage[key] = value
session.bridge.callFunc("localStorageSet", key, value)
}
func (session *sessionData) RemoveAllClientItems() {
session.clientStorage = map[string]string{}
session.bridge.callFunc("localStorageClear")
}

View File

@ -27,6 +27,8 @@ func updateInnerHTML(htmlID string, session Session) {
view = session.viewByHTMLID(htmlID) view = session.viewByHTMLID(htmlID)
} }
if view != nil { if view != nil {
session.callFunc("hideTooltip")
script := allocStringBuilder() script := allocStringBuilder()
defer freeStringBuilder(script) defer freeStringBuilder(script)
@ -42,7 +44,7 @@ func viewByHTMLID(id string, startView View) View {
if startView.htmlID() == id { if startView.htmlID() == id {
return startView return startView
} }
if container, ok := startView.(ParanetView); ok { if container, ok := startView.(ParentView); ok {
for _, view := range container.Views() { for _, view := range container.Views() {
if view != nil { if view != nil {
if v := viewByHTMLID(id, view); v != nil { if v := viewByHTMLID(id, view); v != nil {

View File

@ -104,7 +104,7 @@ func (data *sizeFuncData) parseArgs(args []any, allowNumber bool) bool {
} }
} }
ErrorLogF(`The %s function argument cann't be a number`, data.tag) ErrorLogF(`The %s function argument can't be a number`, data.tag)
return false return false
} }

161
svgImageView.go Normal file
View File

@ -0,0 +1,161 @@
package rui
import (
"io"
"net/http"
"os"
"strings"
)
// SvgImageView - image View
type SvgImageView interface {
View
}
type svgImageViewData struct {
viewData
}
// NewSvgImageView create new SvgImageView object and return it
func NewSvgImageView(session Session, params Params) SvgImageView {
view := new(svgImageViewData)
view.init(session)
setInitParams(view, params)
return view
}
func newSvgImageView(session Session) View {
return NewSvgImageView(session, nil)
}
// Init initialize fields of imageView by default values
func (imageView *svgImageViewData) init(session Session) {
imageView.viewData.init(session)
imageView.tag = "SvgImageView"
imageView.systemClass = "ruiSvgImageView"
}
func (imageView *svgImageViewData) String() string {
return getViewString(imageView)
}
func (imageView *svgImageViewData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag {
case Source, "source":
tag = Content
case VerticalAlign:
tag = CellVerticalAlign
case HorizontalAlign:
tag = CellHorizontalAlign
}
return tag
}
func (imageView *svgImageViewData) Remove(tag string) {
imageView.remove(imageView.normalizeTag(tag))
}
func (imageView *svgImageViewData) remove(tag string) {
imageView.viewData.remove(tag)
if imageView.created {
switch tag {
case Content:
updateInnerHTML(imageView.htmlID(), imageView.session)
}
}
}
func (imageView *svgImageViewData) Set(tag string, value any) bool {
return imageView.set(imageView.normalizeTag(tag), value)
}
func (imageView *svgImageViewData) set(tag string, value any) bool {
if value == nil {
imageView.remove(tag)
return true
}
switch tag {
case Content:
if text, ok := value.(string); ok {
imageView.properties[Content] = text
if imageView.created {
updateInnerHTML(imageView.htmlID(), imageView.session)
}
imageView.propertyChangedEvent(Content)
return true
}
notCompatibleType(Source, value)
return false
default:
return imageView.viewData.set(tag, value)
}
}
func (imageView *svgImageViewData) Get(tag string) any {
return imageView.viewData.get(imageView.normalizeTag(tag))
}
func (imageView *svgImageViewData) htmlTag() string {
return "div"
}
func (imageView *svgImageViewData) htmlSubviews(self View, buffer *strings.Builder) {
if value := imageView.getRaw(Content); value != nil {
if content, ok := value.(string); ok && content != "" {
if strings.HasPrefix(content, "@") {
if name, ok := imageView.session.ImageConstant(content[1:]); ok {
content = name
}
}
if image, ok := resources.images[content]; ok {
if image.fs != nil {
if data, err := image.fs.ReadFile(image.path); err == nil {
buffer.WriteString(string(data))
return
} else {
DebugLog(err.Error())
}
} else if data, err := os.ReadFile(image.path); err == nil {
buffer.WriteString(string(data))
return
} else {
DebugLog(err.Error())
}
}
if strings.HasPrefix(content, "http://") || strings.HasPrefix(content, "https://") {
resp, err := http.Get(content)
if err == nil {
defer resp.Body.Close()
if body, err := io.ReadAll(resp.Body); err == nil {
buffer.WriteString(string(body))
return
}
}
DebugLog(err.Error())
}
buffer.WriteString(content)
}
}
}
// GetSvgImageViewVerticalAlign return the vertical align of an SvgImageView subview: TopAlign (0), BottomAlign (1), CenterAlign (2)
// If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned
func GetSvgImageViewVerticalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, CellVerticalAlign, LeftAlign, false)
}
// GetSvgImageViewHorizontalAlign return the vertical align of an SvgImageView subview: LeftAlign (0), RightAlign (1), CenterAlign (2)
// If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned
func GetSvgImageViewHorizontalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, CellHorizontalAlign, LeftAlign, false)
}

View File

@ -228,11 +228,21 @@ func (adapter *textTableAdapter) Cell(row, column int) any {
return nil return nil
} }
type simpleTableRowStyle struct { type simpleTableLineStyle struct {
params []Params params []Params
} }
func (style *simpleTableRowStyle) RowStyle(row int) Params { func (style *simpleTableLineStyle) ColumnStyle(column int) Params {
if column < len(style.params) {
params := style.params[column]
if len(params) > 0 {
return params
}
}
return nil
}
func (style *simpleTableLineStyle) RowStyle(row int) Params {
if row < len(style.params) { if row < len(style.params) {
params := style.params[row] params := style.params[row]
if len(params) > 0 { if len(params) > 0 {
@ -242,50 +252,24 @@ func (style *simpleTableRowStyle) RowStyle(row int) Params {
return nil return nil
} }
func (table *tableViewData) setRowStyle(value any) bool { func (table *tableViewData) setLineStyle(tag string, value any) bool {
newSimpleTableRowStyle := func(params []Params) TableRowStyle {
if len(params) == 0 {
return nil
}
result := new(simpleTableRowStyle)
result.params = params
return result
}
switch value := value.(type) { switch value := value.(type) {
case TableRowStyle:
table.properties[RowStyle] = value
case []Params: case []Params:
if style := newSimpleTableRowStyle(value); style != nil { if len(value) > 0 {
table.properties[RowStyle] = style style := new(simpleTableLineStyle)
style.params = value
table.properties[tag] = style
} else { } else {
delete(table.properties, RowStyle) delete(table.properties, tag)
} }
case DataNode: case DataNode:
if value.Type() == ArrayNode { if params := value.ArrayAsParams(); len(params) > 0 {
params := make([]Params, value.ArraySize()) style := new(simpleTableLineStyle)
for i, element := range value.ArrayElements() { style.params = params
params[i] = Params{} table.properties[tag] = style
if element.IsObject() {
obj := element.Object()
for k := 0; k < obj.PropertyCount(); k++ {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
params[i][prop.Tag()] = prop.Text()
}
}
} else { } else {
params[i][Style] = element.Value() delete(table.properties, tag)
}
}
if style := newSimpleTableRowStyle(params); style != nil {
table.properties[RowStyle] = style
} else {
delete(table.properties, RowStyle)
}
} else {
return false
} }
default: default:
@ -294,6 +278,14 @@ func (table *tableViewData) setRowStyle(value any) bool {
return true return true
} }
func (table *tableViewData) setRowStyle(value any) bool {
switch value := value.(type) {
case TableRowStyle:
table.properties[RowStyle] = value
}
return table.setLineStyle(RowStyle, value)
}
func (table *tableViewData) getRowStyle() TableRowStyle { func (table *tableViewData) getRowStyle() TableRowStyle {
for _, tag := range []string{RowStyle, Content} { for _, tag := range []string{RowStyle, Content} {
if value := table.getRaw(tag); value != nil { if value := table.getRaw(tag); value != nil {
@ -305,70 +297,12 @@ func (table *tableViewData) getRowStyle() TableRowStyle {
return nil return nil
} }
type simpleTableColumnStyle struct {
params []Params
}
func (style *simpleTableColumnStyle) ColumnStyle(row int) Params {
if row < len(style.params) {
params := style.params[row]
if len(params) > 0 {
return params
}
}
return nil
}
func (table *tableViewData) setColumnStyle(value any) bool { func (table *tableViewData) setColumnStyle(value any) bool {
newSimpleTableColumnStyle := func(params []Params) TableColumnStyle {
if len(params) == 0 {
return nil
}
result := new(simpleTableColumnStyle)
result.params = params
return result
}
switch value := value.(type) { switch value := value.(type) {
case TableColumnStyle: case TableColumnStyle:
table.properties[ColumnStyle] = value table.properties[ColumnStyle] = value
case []Params:
if style := newSimpleTableColumnStyle(value); style != nil {
table.properties[ColumnStyle] = style
} else {
delete(table.properties, ColumnStyle)
} }
return table.setLineStyle(ColumnStyle, value)
case DataNode:
if value.Type() == ArrayNode {
params := make([]Params, value.ArraySize())
for i, element := range value.ArrayElements() {
params[i] = Params{}
if element.IsObject() {
obj := element.Object()
for k := 0; k < obj.PropertyCount(); k++ {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
params[i][prop.Tag()] = prop.Text()
}
}
} else {
params[i][Style] = element.Value()
}
}
if style := newSimpleTableColumnStyle(params); style != nil {
table.properties[ColumnStyle] = style
} else {
delete(table.properties, ColumnStyle)
}
} else {
return false
}
default:
return false
}
return true
} }
func (table *tableViewData) getColumnStyle() TableColumnStyle { func (table *tableViewData) getColumnStyle() TableColumnStyle {

View File

@ -218,8 +218,9 @@ type CellIndex struct {
// TableView - text View // TableView - text View
type TableView interface { type TableView interface {
View View
ParanetView ParentView
ReloadTableData() ReloadTableData()
ReloadCell(row, column int)
CellFrame(row, column int) Frame CellFrame(row, column int) Frame
content() TableAdapter content() TableAdapter
@ -524,10 +525,6 @@ func (table *tableViewData) set(tag string, value any) bool {
case Current: case Current:
switch value := value.(type) { switch value := value.(type) {
case int:
table.current.Row = value
table.current.Column = -1
case CellIndex: case CellIndex:
table.current = value table.current = value
@ -554,6 +551,7 @@ func (table *tableViewData) set(tag string, value any) bool {
table.current.Column = n[1] table.current.Column = n[1]
} else { } else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false
} }
} else { } else {
n, err := strconv.Atoi(value) n, err := strconv.Atoi(value)
@ -566,9 +564,14 @@ func (table *tableViewData) set(tag string, value any) bool {
} }
default: default:
if n, ok := isInt(value); ok {
table.current.Row = n
table.current.Column = -1
} else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return false
} }
}
default: default:
return table.viewData.set(tag, value) return table.viewData.set(tag, value)
@ -585,9 +588,18 @@ func (table *tableViewData) propertyChanged(tag string) {
CellBorder, HeadHeight, HeadStyle, FootHeight, FootStyle, CellBorder, HeadHeight, HeadStyle, FootHeight, FootStyle,
CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft, CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft,
TableCellClickedEvent, TableCellSelectedEvent, TableRowClickedEvent, TableCellClickedEvent, TableCellSelectedEvent, TableRowClickedEvent,
TableRowSelectedEvent, AllowSelection, Current: TableRowSelectedEvent, AllowSelection:
table.ReloadTableData() table.ReloadTableData()
case Current:
switch GetTableSelectionMode(table) {
case CellSelection:
table.session.callFunc("setTableCellCursorByID", table.htmlID(), table.current.Row, table.current.Column)
case RowSelection:
table.session.callFunc("setTableRowCursorByID", table.htmlID(), table.current.Row)
}
case Gap: case Gap:
htmlID := table.htmlID() htmlID := table.htmlID()
session := table.Session() session := table.Session()
@ -606,7 +618,8 @@ func (table *tableViewData) propertyChanged(tag string) {
switch GetTableSelectionMode(table) { switch GetTableSelectionMode(table) {
case CellSelection: case CellSelection:
session.updateProperty(htmlID, "tabindex", "0") tabIndex, _ := intProperty(table, TabIndex, session, 0)
session.updateProperty(htmlID, "tabindex", tabIndex)
session.updateProperty(htmlID, "onfocus", "tableViewFocusEvent(this, event)") session.updateProperty(htmlID, "onfocus", "tableViewFocusEvent(this, event)")
session.updateProperty(htmlID, "onblur", "tableViewBlurEvent(this, event)") session.updateProperty(htmlID, "onblur", "tableViewBlurEvent(this, event)")
session.updateProperty(htmlID, "data-selection", "cell") session.updateProperty(htmlID, "data-selection", "cell")
@ -621,7 +634,8 @@ func (table *tableViewData) propertyChanged(tag string) {
session.updateProperty(htmlID, "onkeydown", "tableViewCellKeyDownEvent(this, event)") session.updateProperty(htmlID, "onkeydown", "tableViewCellKeyDownEvent(this, event)")
case RowSelection: case RowSelection:
session.updateProperty(htmlID, "tabindex", "0") tabIndex, _ := intProperty(table, TabIndex, session, 0)
session.updateProperty(htmlID, "tabindex", tabIndex)
session.updateProperty(htmlID, "onfocus", "tableViewFocusEvent(this, event)") session.updateProperty(htmlID, "onfocus", "tableViewFocusEvent(this, event)")
session.updateProperty(htmlID, "onblur", "tableViewBlurEvent(this, event)") session.updateProperty(htmlID, "onblur", "tableViewBlurEvent(this, event)")
session.updateProperty(htmlID, "data-selection", "row") session.updateProperty(htmlID, "data-selection", "row")
@ -636,7 +650,11 @@ func (table *tableViewData) propertyChanged(tag string) {
session.updateProperty(htmlID, "onkeydown", "tableViewRowKeyDownEvent(this, event)") session.updateProperty(htmlID, "onkeydown", "tableViewRowKeyDownEvent(this, event)")
default: // NoneSelection default: // NoneSelection
for _, prop := range []string{"tabindex", "data-current", "onfocus", "onblur", "onkeydown", "data-selection"} { if tabIndex, ok := intProperty(table, TabIndex, session, -1); !ok || tabIndex < 0 {
session.removeProperty(htmlID, "tabindex")
}
for _, prop := range []string{"data-current", "onfocus", "onblur", "onkeydown", "data-selection"} {
session.removeProperty(htmlID, prop) session.removeProperty(htmlID, prop)
} }
} }
@ -831,7 +849,7 @@ func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) {
var view tableCellView var view tableCellView
view.init(session) view.init(session)
ignorCells := []struct{ row, column int }{} ignoreCells := []struct{ row, column int }{}
selectionMode := GetTableSelectionMode(table) selectionMode := GetTableSelectionMode(table)
var allowCellSelection TableAllowCellSelection = nil var allowCellSelection TableAllowCellSelection = nil
@ -863,7 +881,7 @@ func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) {
vAlign := vAlignCss[vAlignValue] vAlign := vAlignCss[vAlignValue]
tableCSS := func(startRow, endRow int, cellTag string, cellBorder BorderProperty, cellPadding BoundsProperty) { tableCSS := func(startRow, endRow int, cellTag string, cellBorder BorderProperty, cellPadding BoundsProperty) {
var namedColors []NamedColor = nil //var namedColors []NamedColor = nil
for row := startRow; row < endRow; row++ { for row := startRow; row < endRow; row++ {
cssBuilder.buffer.Reset() cssBuilder.buffer.Reset()
@ -908,7 +926,7 @@ func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) {
for column := 0; column < columnCount; column++ { for column := 0; column < columnCount; column++ {
ignore := false ignore := false
for _, cell := range ignorCells { for _, cell := range ignoreCells {
if cell.row == row && cell.column == column { if cell.row == row && cell.column == column {
ignore = true ignore = true
break break
@ -994,7 +1012,7 @@ func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) {
buffer.WriteString(strconv.Itoa(columnSpan)) buffer.WriteString(strconv.Itoa(columnSpan))
buffer.WriteRune('"') buffer.WriteRune('"')
for c := column + 1; c < column+columnSpan; c++ { for c := column + 1; c < column+columnSpan; c++ {
ignorCells = append(ignorCells, struct { ignoreCells = append(ignoreCells, struct {
row int row int
column int column int
}{row: row, column: c}) }{row: row, column: c})
@ -1010,7 +1028,7 @@ func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) {
} }
for r := row + 1; r < row+rowSpan; r++ { for r := row + 1; r < row+rowSpan; r++ {
for c := column; c < column+columnSpan; c++ { for c := column; c < column+columnSpan; c++ {
ignorCells = append(ignorCells, struct { ignoreCells = append(ignoreCells, struct {
row int row int
column int column int
}{row: r, column: c}) }{row: r, column: c})
@ -1025,6 +1043,8 @@ func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) {
} }
buffer.WriteRune('>') buffer.WriteRune('>')
table.writeCellHtml(adapter, row, column, buffer)
/*
switch value := adapter.Cell(row, column).(type) { switch value := adapter.Cell(row, column).(type) {
case string: case string:
buffer.WriteString(value) buffer.WriteString(value)
@ -1076,6 +1096,7 @@ func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) {
buffer.WriteString("<Unsupported value>") buffer.WriteString("<Unsupported value>")
} }
} }
*/
buffer.WriteString(`</`) buffer.WriteString(`</`)
buffer.WriteString(cellTag) buffer.WriteString(cellTag)
@ -1114,7 +1135,8 @@ func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) {
footHeight := GetTableFootHeight(table) footHeight := GetTableFootHeight(table)
cellBorder := table.getCellBorder() cellBorder := table.getCellBorder()
cellPadding := table.boundsProperty(CellPadding) cellPadding := table.boundsProperty(CellPadding)
if cellPadding == nil { if cellPadding == nil || len(cellPadding.AllTags()) == 0 {
cellPadding = nil
if style, ok := stringProperty(table, Style, table.Session()); ok { if style, ok := stringProperty(table, Style, table.Session()); ok {
if style, ok := table.Session().resolveConstants(style); ok { if style, ok := table.Session().resolveConstants(style); ok {
cellPadding = table.cellPaddingFromStyle(style) cellPadding = table.cellPaddingFromStyle(style)
@ -1290,6 +1312,59 @@ func (table *tableViewData) cellPaddingFromStyle(style string) BoundsProperty {
return nil return nil
} }
func (table *tableViewData) writeCellHtml(adapter TableAdapter, row, column int, buffer *strings.Builder) {
switch value := adapter.Cell(row, column).(type) {
case string:
buffer.WriteString(value)
case View:
viewHTML(value, buffer)
table.cellViews = append(table.cellViews, value)
case Color:
buffer.WriteString(`<div style="display: inline; height: 1em; background-color: `)
buffer.WriteString(value.cssString())
buffer.WriteString(`">&nbsp;&nbsp;&nbsp;&nbsp;</div> `)
buffer.WriteString(value.String())
namedColors := NamedColors()
for _, namedColor := range namedColors {
if namedColor.Color == value {
buffer.WriteString(" (")
buffer.WriteString(namedColor.Name)
buffer.WriteRune(')')
break
}
}
case fmt.Stringer:
buffer.WriteString(value.String())
case rune:
buffer.WriteString(string(value))
case float32:
buffer.WriteString(fmt.Sprintf("%g", float64(value)))
case float64:
buffer.WriteString(fmt.Sprintf("%g", value))
case bool:
if value {
buffer.WriteString(table.Session().checkboxOnImage())
} else {
buffer.WriteString(table.Session().checkboxOffImage())
}
default:
if n, ok := isInt(value); ok {
buffer.WriteString(fmt.Sprintf("%d", n))
} else {
buffer.WriteString("<Unsupported value>")
}
}
}
func (table *tableViewData) cellBorderFromStyle(style string) BorderProperty { func (table *tableViewData) cellBorderFromStyle(style string) BorderProperty {
if value := table.Session().styleProperty(style, CellBorder); value != nil { if value := table.Session().styleProperty(style, CellBorder); value != nil {
if border, ok := value.(BorderProperty); ok { if border, ok := value.(BorderProperty); ok {
@ -1377,6 +1452,19 @@ func (table *tableViewData) CellFrame(row, column int) Frame {
return Frame{} return Frame{}
} }
func (table *tableViewData) ReloadCell(row, column int) {
adapter := table.content()
if adapter == nil {
return
}
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
table.writeCellHtml(adapter, row, column, buffer)
table.session.updateInnerHTML(table.cellID(row, column), buffer.String())
}
func (table *tableViewData) Views() []View { func (table *tableViewData) Views() []View {
return table.cellViews return table.cellViews
} }

View File

@ -94,7 +94,7 @@ func GetTableSelectionMode(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, SelectionMode, NoneSelection, false) return enumStyledProperty(view, subviewID, SelectionMode, NoneSelection, false)
} }
// GetTableVerticalAlign returns a vertical align in a TavleView cell. Returns one of next values: // GetTableVerticalAlign returns a vertical align in a TableView cell. Returns one of next values:
// TopAlign (0), BottomAlign (1), CenterAlign (2), and BaselineAlign (3) // TopAlign (0), BottomAlign (1), CenterAlign (2), and BaselineAlign (3)
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetTableVerticalAlign(view View, subviewID ...string) int { func GetTableVerticalAlign(view View, subviewID ...string) int {
@ -182,6 +182,7 @@ func GetTableRowSelectedListeners(view View, subviewID ...string) []func(TableVi
} }
// ReloadTableViewData updates TableView // ReloadTableViewData updates TableView
// If the second argument (subviewID) is not specified or it is "" then updates the first argument (TableView).
func ReloadTableViewData(view View, subviewID ...string) bool { func ReloadTableViewData(view View, subviewID ...string) bool {
var tableView TableView var tableView TableView
if len(subviewID) > 0 && subviewID[0] != "" { if len(subviewID) > 0 && subviewID[0] != "" {
@ -198,3 +199,22 @@ func ReloadTableViewData(view View, subviewID ...string) bool {
tableView.ReloadTableData() tableView.ReloadTableData()
return true return true
} }
// ReloadTableViewCell updates the given table cell.
// If the last argument (subviewID) is not specified or it is "" then updates the cell of the first argument (TableView).
func ReloadTableViewCell(row, column int, view View, subviewID ...string) bool {
var tableView TableView
if len(subviewID) > 0 && subviewID[0] != "" {
if tableView = TableViewByID(view, subviewID[0]); tableView == nil {
return false
}
} else {
var ok bool
if tableView, ok = view.(TableView); !ok {
return false
}
}
tableView.ReloadCell(row, column)
return true
}

View File

@ -1,7 +1,6 @@
package rui package rui
import ( import (
"fmt"
"strconv" "strconv"
"strings" "strings"
) )
@ -567,7 +566,22 @@ func (tabsLayout *tabsLayoutData) IsListItemEnabled(index int) bool {
return true return true
} }
func (tabsLayout *tabsLayoutData) updateContent(view View, tag string) { func (tabsLayout *tabsLayoutData) updateTitle(view View, tag string) {
session := tabsLayout.session
title, _ := stringProperty(view, Title, session)
if !GetNotTranslate(tabsLayout) {
title, _ = session.GetString(title)
}
session.updateInnerHTML(view.htmlID()+"-title", title)
}
func (tabsLayout *tabsLayoutData) updateIcon(view View, tag string) {
session := tabsLayout.session
icon, _ := stringProperty(view, Icon, session)
session.updateProperty(view.htmlID()+"-icon", "src", icon)
}
func (tabsLayout *tabsLayoutData) updateTabCloseButton(view View, tag string) {
updateInnerHTML(tabsLayout.htmlID(), tabsLayout.session) updateInnerHTML(tabsLayout.htmlID(), tabsLayout.session)
} }
@ -578,9 +592,9 @@ func (tabsLayout *tabsLayoutData) Append(view View) {
} }
if view != nil { if view != nil {
tabsLayout.viewsContainerData.Append(view) tabsLayout.viewsContainerData.Append(view)
view.SetChangeListener(Title, tabsLayout.updateContent) view.SetChangeListener(Title, tabsLayout.updateTitle)
view.SetChangeListener(Icon, tabsLayout.updateContent) view.SetChangeListener(Icon, tabsLayout.updateIcon)
view.SetChangeListener(TabCloseButton, tabsLayout.updateContent) view.SetChangeListener(TabCloseButton, tabsLayout.updateTabCloseButton)
if len(tabsLayout.views) == 1 { if len(tabsLayout.views) == 1 {
tabsLayout.properties[Current] = 0 tabsLayout.properties[Current] = 0
for _, listener := range tabsLayout.tabListener { for _, listener := range tabsLayout.tabListener {
@ -602,9 +616,9 @@ func (tabsLayout *tabsLayoutData) Insert(view View, index int) {
defer tabsLayout.propertyChangedEvent(Current) defer tabsLayout.propertyChangedEvent(Current)
} }
tabsLayout.viewsContainerData.Insert(view, index) tabsLayout.viewsContainerData.Insert(view, index)
view.SetChangeListener(Title, tabsLayout.updateContent) view.SetChangeListener(Title, tabsLayout.updateTitle)
view.SetChangeListener(Icon, tabsLayout.updateContent) view.SetChangeListener(Icon, tabsLayout.updateIcon)
view.SetChangeListener(TabCloseButton, tabsLayout.updateContent) view.SetChangeListener(TabCloseButton, tabsLayout.updateTabCloseButton)
} }
} }
@ -650,10 +664,6 @@ func (tabsLayout *tabsLayoutData) RemoveView(index int) View {
return view return view
} }
func (tabsLayout *tabsLayoutData) currentID() string {
return fmt.Sprintf("%s-%d", tabsLayout.htmlID(), tabsLayout.currentItem(0))
}
func (tabsLayout *tabsLayoutData) htmlProperties(self View, buffer *strings.Builder) { func (tabsLayout *tabsLayoutData) htmlProperties(self View, buffer *strings.Builder) {
tabsLayout.viewsContainerData.htmlProperties(self, buffer) tabsLayout.viewsContainerData.htmlProperties(self, buffer)
buffer.WriteString(` data-inactiveTabStyle="`) buffer.WriteString(` data-inactiveTabStyle="`)
@ -661,7 +671,7 @@ func (tabsLayout *tabsLayoutData) htmlProperties(self View, buffer *strings.Buil
buffer.WriteString(`" data-activeTabStyle="`) buffer.WriteString(`" data-activeTabStyle="`)
buffer.WriteString(tabsLayout.activeTabStyle()) buffer.WriteString(tabsLayout.activeTabStyle())
buffer.WriteString(`" data-current="`) buffer.WriteString(`" data-current="`)
buffer.WriteString(tabsLayout.currentID()) buffer.WriteString(strconv.Itoa(tabsLayout.currentItem(0)))
buffer.WriteRune('"') buffer.WriteRune('"')
} }
@ -726,7 +736,7 @@ func (tabsLayout *tabsLayoutData) htmlSubviews(self View, buffer *strings.Builde
notTranslate := GetNotTranslate(tabsLayout) notTranslate := GetNotTranslate(tabsLayout)
closeButton, _ := boolProperty(tabsLayout, TabCloseButton, tabsLayout.session) closeButton, _ := boolProperty(tabsLayout, TabCloseButton, tabsLayout.session)
var tabStyle, titleDiv string var tabStyle, titleStyle string
switch location { switch location {
case LeftTabs, RightTabs: case LeftTabs, RightTabs:
tabStyle = `display: grid; grid-template-rows: auto 1fr auto; align-items: center; justify-items: center; grid-row-gap: 8px;` tabStyle = `display: grid; grid-template-rows: auto 1fr auto; align-items: center; justify-items: center; grid-row-gap: 8px;`
@ -740,13 +750,13 @@ func (tabsLayout *tabsLayoutData) htmlSubviews(self View, buffer *strings.Builde
switch location { switch location {
case LeftTabs: case LeftTabs:
titleDiv = `<div style="writing-mode: vertical-lr; transform: rotate(180deg); grid-row-start: 2; grid-row-end: 3; grid-column-start: 1; grid-column-end: 2;">` titleStyle = ` style="writing-mode: vertical-lr; transform: rotate(180deg); grid-row-start: 2; grid-row-end: 3; grid-column-start: 1; grid-column-end: 2;">`
case RightTabs: case RightTabs:
titleDiv = `<div style="writing-mode: vertical-lr; grid-row-start: 2; grid-row-end: 3; grid-column-start: 1; grid-column-end: 2;">` titleStyle = ` style="writing-mode: vertical-lr; grid-row-start: 2; grid-row-end: 3; grid-column-start: 1; grid-column-end: 2;">`
default: default:
titleDiv = `<div style="grid-row-start: 1; grid-row-end: 2; grid-column-start: 2; grid-column-end: 3;">` titleStyle = ` style="grid-row-start: 1; grid-row-end: 2; grid-column-start: 2; grid-column-end: 3;">`
} }
for n, view := range tabsLayout.views { for n, view := range tabsLayout.views {
@ -785,21 +795,26 @@ func (tabsLayout *tabsLayoutData) htmlSubviews(self View, buffer *strings.Builde
buffer.WriteString(`">`) buffer.WriteString(`">`)
if icon != "" { if icon != "" {
buffer.WriteString(`<img id="`)
buffer.WriteString(view.htmlID())
switch location { switch location {
case LeftTabs: case LeftTabs:
buffer.WriteString(`<img style="grid-row-start: 3; grid-row-end: 4; grid-column-start: 1; grid-column-end: 2;" src="`) buffer.WriteString(`-icon" style="grid-row-start: 3; grid-row-end: 4; grid-column-start: 1; grid-column-end: 2;" src="`)
case RightTabs: case RightTabs:
buffer.WriteString(`<img style="grid-row-start: 1; grid-row-end: 2; grid-column-start: 1; grid-column-end: 2;" src="`) buffer.WriteString(`-icon" style="grid-row-start: 1; grid-row-end: 2; grid-column-start: 1; grid-column-end: 2;" src="`)
default: default:
buffer.WriteString(`<img style="grid-row-start: 1; grid-row-end: 2; grid-column-start: 1; grid-column-end: 2;" src="`) buffer.WriteString(`-icon" style="grid-row-start: 1; grid-row-end: 2; grid-column-start: 1; grid-column-end: 2;" src="`)
} }
buffer.WriteString(icon) buffer.WriteString(icon)
buffer.WriteString(`">`) buffer.WriteString(`">`)
} }
buffer.WriteString(titleDiv) buffer.WriteString(`<div id="`)
buffer.WriteString(view.htmlID())
buffer.WriteString(`-title"`)
buffer.WriteString(titleStyle)
buffer.WriteString(title) buffer.WriteString(title)
buffer.WriteString(`</div>`) buffer.WriteString(`</div>`)

301
theme.go
View File

@ -13,10 +13,16 @@ const (
LandscapeMedia = 2 LandscapeMedia = 2
) )
type MediaStyleParams struct {
Orientation int
MinWidth int
MaxWidth int
MinHeight int
MaxHeight int
}
type mediaStyle struct { type mediaStyle struct {
orientation int MediaStyleParams
maxWidth int
maxHeight int
styles map[string]ViewStyle styles map[string]ViewStyle
} }
@ -49,12 +55,13 @@ type Theme interface {
ImageConstantTags() []string ImageConstantTags() []string
Style(tag string) ViewStyle Style(tag string) ViewStyle
SetStyle(tag string, style ViewStyle) SetStyle(tag string, style ViewStyle)
MediaStyle(tag string, orientation, maxWidth, maxHeight int) ViewStyle RemoveStyle(tag string)
SetMediaStyle(tag string, orientation, maxWidth, maxHeight int, style ViewStyle) MediaStyle(tag string, params MediaStyleParams) ViewStyle
SetMediaStyle(tag string, params MediaStyleParams, style ViewStyle)
StyleTags() []string StyleTags() []string
MediaStyles(tag string) []struct { MediaStyles(tag string) []struct {
Selectors string Selectors string
Orientation, MaxWidth, MaxHeight int Params MediaStyleParams
} }
Append(anotherTheme Theme) Append(anotherTheme Theme)
@ -70,7 +77,7 @@ func (rule mediaStyle) cssText() string {
builder := allocStringBuilder() builder := allocStringBuilder()
defer freeStringBuilder(builder) defer freeStringBuilder(builder)
switch rule.orientation { switch rule.Orientation {
case PortraitMedia: case PortraitMedia:
builder.WriteString(" and (orientation: portrait)") builder.WriteString(" and (orientation: portrait)")
@ -78,26 +85,27 @@ func (rule mediaStyle) cssText() string {
builder.WriteString(" and (orientation: landscape)") builder.WriteString(" and (orientation: landscape)")
} }
if rule.maxWidth > 0 { writeSize := func(tag string, minSize, maxSize int) {
builder.WriteString(" and (max-width: ") if minSize != maxSize {
builder.WriteString(strconv.Itoa(rule.maxWidth)) if minSize > 0 {
builder.WriteString("px)") builder.WriteString(fmt.Sprintf(" and (min-%s: %d.001px)", tag, minSize))
}
if maxSize > 0 {
builder.WriteString(fmt.Sprintf(" and (max-%s: %dpx)", tag, maxSize))
}
} else if minSize > 0 {
builder.WriteString(fmt.Sprintf(" and (%s: %dpx)", tag, minSize))
}
} }
if rule.maxHeight > 0 { writeSize("width", rule.MinWidth, rule.MaxWidth)
builder.WriteString(" and (max-height: ") writeSize("height", rule.MinHeight, rule.MaxHeight)
builder.WriteString(strconv.Itoa(rule.maxHeight))
builder.WriteString("px)")
}
return builder.String() return builder.String()
} }
func parseMediaRule(text string) (mediaStyle, bool) { func parseMediaRule(text string) (mediaStyle, bool) {
rule := mediaStyle{ rule := mediaStyle{
orientation: DefaultMedia,
maxWidth: 0,
maxHeight: 0,
styles: map[string]ViewStyle{}, styles: map[string]ViewStyle{},
} }
@ -105,52 +113,80 @@ func parseMediaRule(text string) (mediaStyle, bool) {
for i := 1; i < len(elements); i++ { for i := 1; i < len(elements); i++ {
switch element := elements[i]; element { switch element := elements[i]; element {
case "portrait": case "portrait":
if rule.orientation != DefaultMedia { if rule.Orientation != DefaultMedia {
ErrorLog(`Duplicate orientation tag in the style section "` + text + `"`) ErrorLog(`Duplicate orientation tag in the style section "` + text + `"`)
return rule, false return rule, false
} }
rule.orientation = PortraitMedia rule.Orientation = PortraitMedia
case "landscape": case "landscape":
if rule.orientation != DefaultMedia { if rule.Orientation != DefaultMedia {
ErrorLog(`Duplicate orientation tag in the style section "` + text + `"`) ErrorLog(`Duplicate orientation tag in the style section "` + text + `"`)
return rule, false return rule, false
} }
rule.orientation = LandscapeMedia rule.Orientation = LandscapeMedia
default: default:
elementSize := func(name string) (int, bool) { elementSize := func(name string) (int, int, bool, error) {
if strings.HasPrefix(element, name) { if strings.HasPrefix(element, name) {
size, err := strconv.Atoi(element[len(name):]) var err error = nil
if err == nil && size > 0 { min := 0
return size, true max := 0
data := element[len(name):]
if pos := strings.Index(data, "-"); pos >= 0 {
if pos > 0 {
min, err = strconv.Atoi(data[:pos])
} }
ErrorLogF(`Invalid style section name "%s": %s`, text, err.Error()) if err == nil && pos+1 < len(data) {
return 0, false max, err = strconv.Atoi(data[pos+1:])
} }
return 0, true } else {
max, err = strconv.Atoi(data)
}
return min, max, true, err
}
return 0, 0, false, nil
} }
if size, ok := elementSize("width"); !ok || size > 0 { if min, max, ok, err := elementSize("width"); ok {
if !ok {
if err != nil {
ErrorLogF(`Invalid style section name "%s": %s`, text, err.Error())
return rule, false return rule, false
} }
if rule.maxWidth != 0 { if rule.MinWidth != 0 || rule.MaxWidth != 0 {
ErrorLog(`Duplicate "width" tag in the style section "` + text + `"`) ErrorLog(`Duplicate "width" tag in the style section "` + text + `"`)
return rule, false return rule, false
} }
rule.maxWidth = size if min == 0 && max == 0 {
} else if size, ok := elementSize("height"); !ok || size > 0 { ErrorLog(`Invalid arguments of "width" tag in the style section "` + text + `"`)
if !ok {
return rule, false return rule, false
} }
if rule.maxHeight != 0 {
rule.MinWidth = min
rule.MaxWidth = max
} else if min, max, ok, err := elementSize("height"); ok {
if err != nil {
ErrorLogF(`Invalid style section name "%s": %s`, text, err.Error())
return rule, false
}
if rule.MinHeight != 0 || rule.MaxHeight != 0 {
ErrorLog(`Duplicate "height" tag in the style section "` + text + `"`) ErrorLog(`Duplicate "height" tag in the style section "` + text + `"`)
return rule, false return rule, false
} }
rule.maxHeight = size if min == 0 && max == 0 {
ErrorLog(`Invalid arguments of "height" tag in the style section "` + text + `"`)
return rule, false
}
rule.MinHeight = min
rule.MaxHeight = max
} else { } else {
ErrorLogF(`Unknown elemnet "%s" in the style section name "%s"`, element, text)
ErrorLogF(`Unknown element "%s" in the style section name "%s"`, element, text)
return rule, false return rule, false
} }
} }
@ -264,36 +300,72 @@ func (theme *theme) SetStyle(tag string, style ViewStyle) {
} }
} }
func (theme *theme) MediaStyle(tag string, orientation, maxWidth, maxHeight int) ViewStyle { func (theme *theme) RemoveStyle(tag string) {
tag2 := tag + ":"
remove := func(styles map[string]ViewStyle) {
tags := []string{tag}
for t := range styles {
if strings.HasPrefix(t, tag2) {
tags = append(tags, t)
}
}
for _, t := range tags {
delete(styles, t)
}
}
remove(theme.styles)
for _, mediaStyle := range theme.mediaStyles {
remove(mediaStyle.styles)
}
}
func (theme *theme) MediaStyle(tag string, params MediaStyleParams) ViewStyle {
for _, styles := range theme.mediaStyles { for _, styles := range theme.mediaStyles {
if styles.orientation == orientation && styles.maxWidth == maxWidth && styles.maxHeight == maxHeight { if styles.Orientation == params.Orientation &&
styles.MaxWidth == params.MaxWidth &&
styles.MinWidth == params.MinWidth &&
styles.MaxHeight == params.MaxHeight &&
styles.MinHeight == params.MinHeight {
if style, ok := styles.styles[tag]; ok { if style, ok := styles.styles[tag]; ok {
return style return style
} }
} }
} }
if orientation == 0 && maxWidth <= 0 && maxHeight <= 0 {
if params.Orientation == 0 && params.MaxWidth == 0 && params.MinWidth == 0 &&
params.MaxHeight == 0 && params.MinHeight == 0 {
return theme.style(tag) return theme.style(tag)
} }
return nil return nil
} }
func (theme *theme) SetMediaStyle(tag string, orientation, maxWidth, maxHeight int, style ViewStyle) { func (theme *theme) SetMediaStyle(tag string, params MediaStyleParams, style ViewStyle) {
if maxWidth < 0 { if params.MaxWidth < 0 {
maxWidth = 0 params.MaxWidth = 0
} }
if maxHeight < 0 { if params.MinWidth < 0 {
maxHeight = 0 params.MinWidth = 0
}
if params.MaxHeight < 0 {
params.MaxHeight = 0
}
if params.MinHeight < 0 {
params.MinHeight = 0
} }
if orientation == DefaultMedia && maxWidth == 0 && maxHeight == 0 { if params.Orientation == 0 && params.MaxWidth == 0 && params.MinWidth == 0 &&
params.MaxHeight == 0 && params.MinHeight == 0 {
theme.SetStyle(tag, style) theme.SetStyle(tag, style)
return return
} }
for i, styles := range theme.mediaStyles { for i, styles := range theme.mediaStyles {
if styles.orientation == orientation && styles.maxWidth == maxWidth && styles.maxHeight == maxHeight { if styles.Orientation == params.Orientation &&
styles.MaxWidth == params.MaxWidth &&
styles.MinWidth == params.MinWidth &&
styles.MaxHeight == params.MaxHeight &&
styles.MinHeight == params.MinHeight {
if style != nil { if style != nil {
theme.mediaStyles[i].styles[tag] = style theme.mediaStyles[i].styles[tag] = style
} else { } else {
@ -305,9 +377,7 @@ func (theme *theme) SetMediaStyle(tag string, orientation, maxWidth, maxHeight i
if style != nil { if style != nil {
theme.mediaStyles = append(theme.mediaStyles, mediaStyle{ theme.mediaStyles = append(theme.mediaStyles, mediaStyle{
orientation: orientation, MediaStyleParams: params,
maxWidth: maxWidth,
maxHeight: maxHeight,
styles: map[string]ViewStyle{tag: style}, styles: map[string]ViewStyle{tag: style},
}) })
theme.sortMediaStyles() theme.sortMediaStyles()
@ -407,11 +477,11 @@ func (theme *theme) StyleTags() []string {
func (theme *theme) MediaStyles(tag string) []struct { func (theme *theme) MediaStyles(tag string) []struct {
Selectors string Selectors string
Orientation, MaxWidth, MaxHeight int Params MediaStyleParams
} { } {
result := []struct { result := []struct {
Selectors string Selectors string
Orientation, MaxWidth, MaxHeight int Params MediaStyleParams
}{} }{}
prefix := tag + ":" prefix := tag + ":"
@ -420,12 +490,10 @@ func (theme *theme) MediaStyles(tag string) []struct {
if strings.HasPrefix(themeTag, prefix) { if strings.HasPrefix(themeTag, prefix) {
result = append(result, struct { result = append(result, struct {
Selectors string Selectors string
Orientation, MaxWidth, MaxHeight int Params MediaStyleParams
}{ }{
Selectors: themeTag[prefixLen:], Selectors: themeTag[prefixLen:],
Orientation: DefaultMedia, Params: MediaStyleParams{},
MaxWidth: 0,
MaxHeight: 0,
}) })
} }
} }
@ -434,24 +502,20 @@ func (theme *theme) MediaStyles(tag string) []struct {
if _, ok := media.styles[tag]; ok { if _, ok := media.styles[tag]; ok {
result = append(result, struct { result = append(result, struct {
Selectors string Selectors string
Orientation, MaxWidth, MaxHeight int Params MediaStyleParams
}{ }{
Selectors: "", Selectors: "",
Orientation: media.orientation, Params: media.MediaStyleParams,
MaxWidth: media.maxWidth,
MaxHeight: media.maxHeight,
}) })
} }
for themeTag := range media.styles { for themeTag := range media.styles {
if strings.HasPrefix(themeTag, prefix) { if strings.HasPrefix(themeTag, prefix) {
result = append(result, struct { result = append(result, struct {
Selectors string Selectors string
Orientation, MaxWidth, MaxHeight int Params MediaStyleParams
}{ }{
Selectors: themeTag[prefixLen:], Selectors: themeTag[prefixLen:],
Orientation: media.orientation, Params: media.MediaStyleParams,
MaxWidth: media.maxWidth,
MaxHeight: media.maxHeight,
}) })
} }
} }
@ -500,9 +564,11 @@ func (theme *theme) Append(anotherTheme Theme) {
for _, anotherMedia := range another.mediaStyles { for _, anotherMedia := range another.mediaStyles {
exists := false exists := false
for _, media := range theme.mediaStyles { for _, media := range theme.mediaStyles {
if anotherMedia.maxHeight == media.maxHeight && if anotherMedia.MinHeight == media.MinHeight &&
anotherMedia.maxWidth == media.maxWidth && anotherMedia.MaxHeight == media.MaxHeight &&
anotherMedia.orientation == media.orientation { anotherMedia.MinWidth == media.MinWidth &&
anotherMedia.MaxWidth == media.MaxWidth &&
anotherMedia.Orientation == media.Orientation {
for tag, style := range anotherMedia.styles { for tag, style := range anotherMedia.styles {
media.styles[tag] = style media.styles[tag] = style
} }
@ -525,19 +591,38 @@ func (theme *theme) cssText(session Session) string {
var builder cssStyleBuilder var builder cssStyleBuilder
builder.init() builder.init()
for tag, style := range theme.styles { styleList := func(styles map[string]ViewStyle) []string {
ruiStyles := []string{}
customStyles := []string{}
for tag := range styles {
if strings.HasPrefix(tag, "rui") {
ruiStyles = append(ruiStyles, tag)
} else {
customStyles = append(customStyles, tag)
}
}
sort.Strings(ruiStyles)
sort.Strings(customStyles)
return append(ruiStyles, customStyles...)
}
for _, tag := range styleList(theme.styles) {
if style := theme.styles[tag]; style != nil {
builder.startStyle(tag) builder.startStyle(tag)
style.cssViewStyle(&builder, session) style.cssViewStyle(&builder, session)
builder.endStyle() builder.endStyle()
} }
}
for _, media := range theme.mediaStyles { for _, media := range theme.mediaStyles {
builder.startMedia(media.cssText()) builder.startMedia(media.cssText())
for tag, style := range media.styles { for _, tag := range styleList(media.styles) {
if style := media.styles[tag]; style != nil {
builder.startStyle(tag) builder.startStyle(tag)
style.cssViewStyle(&builder, session) style.cssViewStyle(&builder, session)
builder.endStyle() builder.endStyle()
} }
}
builder.endMedia() builder.endMedia()
} }
@ -687,13 +772,19 @@ func (theme *theme) addText(themeText string) bool {
func (theme *theme) sortMediaStyles() { func (theme *theme) sortMediaStyles() {
if len(theme.mediaStyles) > 1 { if len(theme.mediaStyles) > 1 {
sort.SliceStable(theme.mediaStyles, func(i, j int) bool { sort.SliceStable(theme.mediaStyles, func(i, j int) bool {
if theme.mediaStyles[i].orientation != theme.mediaStyles[j].orientation { if theme.mediaStyles[i].Orientation != theme.mediaStyles[j].Orientation {
return theme.mediaStyles[i].orientation < theme.mediaStyles[j].orientation return theme.mediaStyles[i].Orientation < theme.mediaStyles[j].Orientation
} }
if theme.mediaStyles[i].maxWidth != theme.mediaStyles[j].maxWidth { if theme.mediaStyles[i].MinWidth != theme.mediaStyles[j].MinWidth {
return theme.mediaStyles[i].maxWidth < theme.mediaStyles[j].maxWidth return theme.mediaStyles[i].MinWidth < theme.mediaStyles[j].MinWidth
} }
return theme.mediaStyles[i].maxHeight < theme.mediaStyles[j].maxHeight if theme.mediaStyles[i].MinHeight != theme.mediaStyles[j].MinHeight {
return theme.mediaStyles[i].MinHeight < theme.mediaStyles[j].MinHeight
}
if theme.mediaStyles[i].MaxWidth != theme.mediaStyles[j].MaxWidth {
return theme.mediaStyles[i].MaxWidth < theme.mediaStyles[j].MaxWidth
}
return theme.mediaStyles[i].MaxHeight < theme.mediaStyles[j].MaxHeight
}) })
} }
} }
@ -811,7 +902,7 @@ func (theme *theme) String() string {
writeConstants("constants", theme.constants) writeConstants("constants", theme.constants)
writeConstants("constants:touch", theme.touchConstants) writeConstants("constants:touch", theme.touchConstants)
writeStyles := func(orientation, maxWidth, maxHeihgt int, styles map[string]ViewStyle) bool { writeStyles := func(orientation, maxWidth, maxHeight int, styles map[string]ViewStyle) bool {
count := len(styles) count := len(styles)
if count == 0 { if count == 0 {
return false return false
@ -834,16 +925,16 @@ func (theme *theme) String() string {
if maxWidth > 0 { if maxWidth > 0 {
buffer.WriteString(fmt.Sprintf(":width%d", maxWidth)) buffer.WriteString(fmt.Sprintf(":width%d", maxWidth))
} }
if maxHeihgt > 0 { if maxHeight > 0 {
buffer.WriteString(fmt.Sprintf(":heihgt%d", maxHeihgt)) buffer.WriteString(fmt.Sprintf(":height%d", maxHeight))
} }
buffer.WriteString(" = [\n") buffer.WriteString(" = [\n")
for _, tag := range tags { for _, tag := range tags {
if style, ok := styles[tag]; ok { if style, ok := styles[tag]; ok && len(style.AllTags()) > 0 {
buffer.WriteString("\t\t") buffer.WriteString("\t\t")
writeViewStyle(tag, style, buffer, "\t\t") writeViewStyle(tag, style, buffer, "\t\t")
buffer.WriteString(",") buffer.WriteString(",\n")
} }
} }
buffer.WriteString("\t],\n") buffer.WriteString("\t],\n")
@ -852,7 +943,53 @@ func (theme *theme) String() string {
writeStyles(0, 0, 0, theme.styles) writeStyles(0, 0, 0, theme.styles)
for _, media := range theme.mediaStyles { for _, media := range theme.mediaStyles {
writeStyles(media.orientation, media.maxWidth, media.maxHeight, media.styles) //writeStyles(media.orientation, media.maxWidth, media.maxHeight, media.styles)
if count := len(media.styles); count > 0 {
tags := make([]string, 0, count)
for name := range media.styles {
tags = append(tags, name)
}
sort.Strings(tags)
buffer.WriteString("\tstyles")
switch media.Orientation {
case PortraitMedia:
buffer.WriteString(":portrait")
case LandscapeMedia:
buffer.WriteString(":landscape")
}
if media.MinWidth > 0 {
buffer.WriteString(fmt.Sprintf(":width%d-", media.MinWidth))
if media.MaxWidth > 0 {
buffer.WriteString(strconv.Itoa(media.MaxWidth))
}
} else if media.MaxWidth > 0 {
buffer.WriteString(fmt.Sprintf(":width%d", media.MaxWidth))
}
if media.MinHeight > 0 {
buffer.WriteString(fmt.Sprintf(":height%d-", media.MinHeight))
if media.MaxHeight > 0 {
buffer.WriteString(strconv.Itoa(media.MaxHeight))
}
} else if media.MaxHeight > 0 {
buffer.WriteString(fmt.Sprintf(":height%d", media.MaxHeight))
}
buffer.WriteString(" = [\n")
for _, tag := range tags {
if style, ok := media.styles[tag]; ok && len(style.AllTags()) > 0 {
buffer.WriteString("\t\t")
writeViewStyle(tag, style, buffer, "\t\t")
buffer.WriteString(",\n")
}
}
buffer.WriteString("\t],\n")
}
} }
buffer.WriteString("}\n") buffer.WriteString("}\n")

View File

@ -22,7 +22,7 @@ type TimePicker interface {
type timePickerData struct { type timePickerData struct {
viewData viewData
timeChangedListeners []func(TimePicker, time.Time) timeChangedListeners []func(TimePicker, time.Time, time.Time)
} }
// NewTimePicker create new TimePicker object and return it // NewTimePicker create new TimePicker object and return it
@ -40,7 +40,7 @@ func newTimePicker(session Session) View {
func (picker *timePickerData) init(session Session) { func (picker *timePickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "TimePicker" picker.tag = "TimePicker"
picker.timeChangedListeners = []func(TimePicker, time.Time){} picker.timeChangedListeners = []func(TimePicker, time.Time, time.Time){}
} }
func (picker *timePickerData) String() string { func (picker *timePickerData) String() string {
@ -69,7 +69,7 @@ func (picker *timePickerData) remove(tag string) {
switch tag { switch tag {
case TimeChangedEvent: case TimeChangedEvent:
if len(picker.timeChangedListeners) > 0 { if len(picker.timeChangedListeners) > 0 {
picker.timeChangedListeners = []func(TimePicker, time.Time){} picker.timeChangedListeners = []func(TimePicker, time.Time, time.Time){}
picker.propertyChangedEvent(tag) picker.propertyChangedEvent(tag)
} }
return return
@ -94,13 +94,14 @@ func (picker *timePickerData) remove(tag string) {
case TimePickerValue: case TimePickerValue:
if _, ok := picker.properties[TimePickerValue]; ok { if _, ok := picker.properties[TimePickerValue]; ok {
oldTime := GetTimePickerValue(picker)
delete(picker.properties, TimePickerValue) delete(picker.properties, TimePickerValue)
time := GetTimePickerValue(picker) time := GetTimePickerValue(picker)
if picker.created { if picker.created {
picker.session.callFunc("setInputValue", picker.htmlID(), time.Format(timeFormat)) picker.session.callFunc("setInputValue", picker.htmlID(), time.Format(timeFormat))
} }
for _, listener := range picker.timeChangedListeners { for _, listener := range picker.timeChangedListeners {
listener(picker, time) listener(picker, time, oldTime)
} }
} else { } else {
return return
@ -214,7 +215,7 @@ func (picker *timePickerData) set(tag string, value any) bool {
picker.session.callFunc("setInputValue", picker.htmlID(), time.Format(timeFormat)) picker.session.callFunc("setInputValue", picker.htmlID(), time.Format(timeFormat))
} }
for _, listener := range picker.timeChangedListeners { for _, listener := range picker.timeChangedListeners {
listener(picker, time) listener(picker, time, oldTime)
} }
picker.propertyChangedEvent(tag) picker.propertyChangedEvent(tag)
} }
@ -222,12 +223,12 @@ func (picker *timePickerData) set(tag string, value any) bool {
} }
case TimeChangedEvent: case TimeChangedEvent:
listeners, ok := valueToEventListeners[TimePicker, time.Time](value) listeners, ok := valueToEventWithOldListeners[TimePicker, time.Time](value)
if !ok { if !ok {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return false
} else if listeners == nil { } else if listeners == nil {
listeners = []func(TimePicker, time.Time){} listeners = []func(TimePicker, time.Time, time.Time){}
} }
picker.timeChangedListeners = listeners picker.timeChangedListeners = listeners
picker.propertyChangedEvent(tag) picker.propertyChangedEvent(tag)
@ -306,7 +307,7 @@ func (picker *timePickerData) handleCommand(self View, command string, data Data
picker.properties[TimePickerValue] = value picker.properties[TimePickerValue] = value
if value != oldValue { if value != oldValue {
for _, listener := range picker.timeChangedListeners { for _, listener := range picker.timeChangedListeners {
listener(picker, value) listener(picker, value, oldValue)
} }
} }
} }
@ -398,6 +399,6 @@ func GetTimePickerValue(view View, subviewID ...string) time.Time {
// GetTimeChangedListeners returns the TimeChangedListener list of an TimePicker subview. // GetTimeChangedListeners returns the TimeChangedListener list of an TimePicker subview.
// If there are no listeners then the empty list is returned // If there are no listeners then the empty list is returned
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetTimeChangedListeners(view View, subviewID ...string) []func(TimePicker, time.Time) { func GetTimeChangedListeners(view View, subviewID ...string) []func(TimePicker, time.Time, time.Time) {
return getEventListeners[TimePicker, time.Time](view, subviewID, TimeChangedEvent) return getEventWithOldListeners[TimePicker, time.Time](view, subviewID, TimeChangedEvent)
} }

View File

@ -130,7 +130,10 @@ func touchEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range touchEvents { for tag, js := range touchEvents {
if value := view.getRaw(tag); value != nil { if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View, TouchEvent)); ok && len(listeners) > 0 { if listeners, ok := value.([]func(View, TouchEvent)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `) buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
} }
} }
} }
@ -140,7 +143,7 @@ func (event *TouchEvent) init(data DataObject) {
event.Touches = []Touch{} event.Touches = []Touch{}
event.TimeStamp = getTimeStamp(data) event.TimeStamp = getTimeStamp(data)
if node := data.PropertyWithTag("touches"); node != nil && node.Type() == ArrayNode { if node := data.PropertyByTag("touches"); node != nil && node.Type() == ArrayNode {
for i := 0; i < node.ArraySize(); i++ { for i := 0; i < node.ArraySize(); i++ {
if element := node.ArrayElement(i); element != nil && element.IsObject() { if element := node.ArrayElement(i); element != nil && element.IsObject() {
if obj := element.Object(); obj != nil { if obj := element.Object(); obj != nil {

View File

@ -1,7 +1,9 @@
package rui package rui
import ( import (
"encoding/base64"
"net" "net"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
) )
@ -76,3 +78,31 @@ func dataFloatProperty(data DataObject, tag string) float64 {
} }
return 0 return 0
} }
// InlineImageFromResource reads image from resources and converts it to an inline image.
// Supported png, jpeg, gif, and svg files
func InlineImageFromResource(filename string) (string, bool) {
if image, ok := resources.images[filename]; ok && image.fs != nil {
dataType := map[string]string{
".svg": "data:image/svg+xml",
".png": "data:image/png",
".jpg": "data:image/jpg",
".jpeg": "data:image/jpg",
".gif": "data:image/gif",
}
ext := strings.ToLower(filepath.Ext(filename))
if prefix, ok := dataType[ext]; ok {
if data, err := image.fs.ReadFile(image.path); err == nil {
return prefix + ";base64," + base64.StdEncoding.EncodeToString(data), true
} else {
DebugLog(err.Error())
}
} else {
DebugLogF(`InlineImageFromResource("%s") error: Unsupported file`, filename)
}
} else {
DebugLogF(`The resource image "%s" not found`, filename)
}
return "", false
}

73
view.go
View File

@ -45,7 +45,7 @@ type View interface {
Focusable() bool Focusable() bool
// Frame returns the location and size of the view in pixels // Frame returns the location and size of the view in pixels
Frame() Frame Frame() Frame
// Scroll returns the location size of the scrolable view in pixels // Scroll returns the location size of the scrollable view in pixels
Scroll() Frame Scroll() Frame
// SetAnimated sets the value (second argument) of the property with name defined by the first argument. // SetAnimated sets the value (second argument) of the property with name defined by the first argument.
// Return "true" if the value has been set, in the opposite case "false" are returned and // Return "true" if the value has been set, in the opposite case "false" are returned and
@ -175,6 +175,17 @@ func (view *viewData) Focusable() bool {
if focus, ok := boolProperty(view, Focusable, view.session); ok { if focus, ok := boolProperty(view, Focusable, view.session); ok {
return focus return focus
} }
if style, ok := stringProperty(view, Style, view.session); ok {
if style, ok := view.session.resolveConstants(style); ok {
if value := view.session.styleProperty(style, Focusable); ok {
if focus, ok := valueToBool(value, view.Session()); ok {
return focus
}
}
}
}
return false return false
} }
@ -187,6 +198,14 @@ func (view *viewData) remove(tag string) {
case ID: case ID:
view.viewID = "" view.viewID = ""
case TabIndex, "tab-index":
delete(view.properties, tag)
if view.Focusable() {
view.session.updateProperty(view.htmlID(), "tabindex", "0")
} else {
view.session.updateProperty(view.htmlID(), "tabindex", "-1")
}
case UserData: case UserData:
delete(view.properties, tag) delete(view.properties, tag)
@ -312,6 +331,18 @@ func (view *viewData) set(tag string, value any) bool {
} }
view.viewID = text view.viewID = text
case TabIndex, "tab-index":
if !view.setIntProperty(tag, value) {
return false
}
if value, ok := intProperty(view, TabIndex, view.Session(), 0); ok {
view.session.updateProperty(view.htmlID(), "tabindex", strconv.Itoa(value))
} else if view.Focusable() {
view.session.updateProperty(view.htmlID(), "tabindex", "0")
} else {
view.session.updateProperty(view.htmlID(), "tabindex", "-1")
}
case UserData: case UserData:
view.properties[tag] = value view.properties[tag] = value
@ -381,10 +412,12 @@ func viewPropertyChanged(view *viewData, tag string) {
case Invisible: case Invisible:
session.updateCSSProperty(htmlID, Visibility, "hidden") session.updateCSSProperty(htmlID, Visibility, "hidden")
session.updateCSSProperty(htmlID, "display", "") session.updateCSSProperty(htmlID, "display", "")
session.callFunc("hideTooltip")
case Gone: case Gone:
session.updateCSSProperty(htmlID, Visibility, "hidden") session.updateCSSProperty(htmlID, Visibility, "hidden")
session.updateCSSProperty(htmlID, "display", "none") session.updateCSSProperty(htmlID, "display", "none")
session.callFunc("hideTooltip")
default: default:
session.updateCSSProperty(htmlID, Visibility, "visible") session.updateCSSProperty(htmlID, Visibility, "visible")
@ -577,7 +610,7 @@ func viewPropertyChanged(view *viewData, tag string) {
} }
return return
case ZIndex, TabSize: case ZIndex, Order, TabSize:
if i, ok := intProperty(view, tag, session, 0); ok { if i, ok := intProperty(view, tag, session, 0); ok {
session.updateCSSProperty(htmlID, tag, strconv.Itoa(i)) session.updateCSSProperty(htmlID, tag, strconv.Itoa(i))
} }
@ -606,6 +639,24 @@ func viewPropertyChanged(view *viewData, tag string) {
session.updateCSSProperty(htmlID, "user-select", "") session.updateCSSProperty(htmlID, "user-select", "")
} }
return return
case ColumnSpanAll:
if spanAll, ok := boolProperty(view, ColumnSpanAll, session); ok && spanAll {
session.updateCSSProperty(htmlID, `column-span`, `all`)
} else {
session.updateCSSProperty(htmlID, `column-span`, `none`)
}
return
case Tooltip:
if tooltip := GetTooltip(view); tooltip == "" {
session.removeProperty(htmlID, "data-tooltip")
} else {
session.updateProperty(htmlID, "data-tooltip", tooltip)
session.updateProperty(htmlID, "onmouseenter", "mouseEnterEvent(this, event)")
session.updateProperty(htmlID, "onmouseleave", "mouseLeaveEvent(this, event)")
}
return
} }
if cssTag, ok := sizeProperties[tag]; ok { if cssTag, ok := sizeProperties[tag]; ok {
@ -758,14 +809,28 @@ func viewHTML(view View, buffer *strings.Builder) {
buffer.WriteRune(' ') buffer.WriteRune(' ')
} }
if view.Focusable() && !disabled { if !disabled {
if value, ok := intProperty(view, TabIndex, view.Session(), -1); ok {
buffer.WriteString(`tabindex="`)
buffer.WriteString(strconv.Itoa(value))
buffer.WriteString(`" `)
} else if view.Focusable() {
buffer.WriteString(`tabindex="0" `) buffer.WriteString(`tabindex="0" `)
} }
}
hasTooltip := false
if tooltip := GetTooltip(view); tooltip != "" {
buffer.WriteString(`data-tooltip=" `)
buffer.WriteString(tooltip)
buffer.WriteString(`" `)
hasTooltip = true
}
buffer.WriteString(`onscroll="scrollEvent(this, event)" `) buffer.WriteString(`onscroll="scrollEvent(this, event)" `)
keyEventsHtml(view, buffer) keyEventsHtml(view, buffer)
mouseEventsHtml(view, buffer) mouseEventsHtml(view, buffer, hasTooltip)
pointerEventsHtml(view, buffer) pointerEventsHtml(view, buffer)
touchEventsHtml(view, buffer) touchEventsHtml(view, buffer)
focusEventsHtml(view, buffer) focusEventsHtml(view, buffer)

View File

@ -12,7 +12,7 @@ func ViewByID(rootView View, id string) View {
return rootView return rootView
} }
if container, ok := rootView.(ParanetView); ok { if container, ok := rootView.(ParentView); ok {
if view := viewByID(container, id); view != nil { if view := viewByID(container, id); view != nil {
return view return view
} }
@ -32,13 +32,13 @@ func ViewByID(rootView View, id string) View {
return nil return nil
} }
func viewByID(rootView ParanetView, id string) View { func viewByID(rootView ParentView, id string) View {
for _, view := range rootView.Views() { for _, view := range rootView.Views() {
if view != nil { if view != nil {
if view.ID() == id { if view.ID() == id {
return view return view
} }
if container, ok := view.(ParanetView); ok { if container, ok := view.(ParentView); ok {
if v := viewByID(container, id); v != nil { if v := viewByID(container, id); v != nil {
return v return v
} }

View File

@ -30,6 +30,7 @@ var viewCreators = map[string]func(Session) View{
"ListView": newListView, "ListView": newListView,
"CanvasView": newCanvasView, "CanvasView": newCanvasView,
"ImageView": newImageView, "ImageView": newImageView,
"SvgImageView": newSvgImageView,
"TableView": newTableView, "TableView": newTableView,
"AudioPlayer": newAudioPlayer, "AudioPlayer": newAudioPlayer,
"VideoPlayer": newVideoPlayer, "VideoPlayer": newVideoPlayer,

View File

@ -193,22 +193,26 @@ func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session) {
outline.ViewOutline(session).cssValue(builder, session) outline.ViewOutline(session).cssValue(builder, session)
} }
if z, ok := intProperty(style, ZIndex, session, 0); ok { for _, tag := range []string{ZIndex, Order} {
builder.add(ZIndex, strconv.Itoa(z)) if value, ok := intProperty(style, tag, session, 0); ok {
builder.add(tag, strconv.Itoa(value))
}
} }
if opacity, ok := floatProperty(style, Opacity, session, 1.0); ok && opacity >= 0 && opacity <= 1 { if opacity, ok := floatProperty(style, Opacity, session, 1.0); ok && opacity >= 0 && opacity <= 1 {
builder.add(Opacity, strconv.FormatFloat(opacity, 'f', 3, 32)) builder.add(Opacity, strconv.FormatFloat(opacity, 'f', 3, 32))
} }
if n, ok := intProperty(style, ColumnCount, session, 0); ok && n > 0 { for _, tag := range []string{ColumnCount, TabSize} {
builder.add(ColumnCount, strconv.Itoa(n)) if value, ok := intProperty(style, tag, session, 0); ok && value > 0 {
builder.add(tag, strconv.Itoa(value))
}
} }
for _, tag := range []string{ for _, tag := range []string{
Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight, Left, Right, Top, Bottom, Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight, Left, Right, Top, Bottom,
TextSize, TextIndent, LetterSpacing, WordSpacing, LineHeight, TextLineThickness, TextSize, TextIndent, LetterSpacing, WordSpacing, LineHeight, TextLineThickness,
ListRowGap, ListColumnGap, GridRowGap, GridColumnGap, ColumnGap, ColumnWidth} { ListRowGap, ListColumnGap, GridRowGap, GridColumnGap, ColumnGap, ColumnWidth, OutlineOffset} {
if size, ok := sizeProperty(style, tag, session); ok && size.Type != Auto { if size, ok := sizeProperty(style, tag, session); ok && size.Type != Auto {
cssTag, ok := sizeProperties[tag] cssTag, ok := sizeProperties[tag]
@ -248,7 +252,7 @@ func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session) {
for _, tag := range []string{ for _, tag := range []string{
Overflow, TextAlign, TextTransform, TextWeight, TextLineStyle, WritingMode, TextDirection, Overflow, TextAlign, TextTransform, TextWeight, TextLineStyle, WritingMode, TextDirection,
VerticalTextOrientation, CellVerticalAlign, CellHorizontalAlign, GridAutoFlow, Cursor, VerticalTextOrientation, CellVerticalAlign, CellHorizontalAlign, GridAutoFlow, Cursor,
WhiteSpace, WordBreak, TextOverflow, Float, TableVerticalAlign, Resize} { WhiteSpace, WordBreak, TextOverflow, Float, TableVerticalAlign, Resize, MixBlendMode, BackgroundBlendMode} {
if data, ok := enumProperties[tag]; ok { if data, ok := enumProperties[tag]; ok {
if tag != VerticalTextOrientation || (writingMode != VerticalLeftToRight && writingMode != VerticalRightToLeft) { if tag != VerticalTextOrientation || (writingMode != VerticalLeftToRight && writingMode != VerticalRightToLeft) {
@ -279,10 +283,6 @@ func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session) {
} }
} }
if tabSize, ok := intProperty(style, TabSize, session, 8); ok && tabSize > 0 {
builder.add(TabSize, strconv.Itoa(tabSize))
}
if text := style.cssTextDecoration(session); text != "" { if text := style.cssTextDecoration(session); text != "" {
builder.add("text-decoration", text) builder.add("text-decoration", text)
} }
@ -453,6 +453,14 @@ func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session) {
builder.add(`animation-play-state`, `running`) builder.add(`animation-play-state`, `running`)
} }
} }
if spanAll, ok := boolProperty(style, ColumnSpanAll, session); ok {
if spanAll {
builder.add(`column-span`, `all`)
} else {
builder.add(`column-span`, `none`)
}
}
} }
func valueToOrientation(value any, session Session) (int, bool) { func valueToOrientation(value any, session Session) (int, bool) {

View File

@ -202,6 +202,60 @@ func (style *viewStyle) set(tag string, value any) bool {
case CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft: case CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft:
return style.setBoundsSide(CellPadding, tag, value) return style.setBoundsSide(CellPadding, tag, value)
case HeadStyle, FootStyle:
switch value := value.(type) {
case string:
style.properties[tag] = value
return true
case Params:
style.properties[tag] = value
return true
case DataObject:
if params := value.ToParams(); len(params) > 0 {
style.properties[tag] = params
}
return true
}
case CellStyle, ColumnStyle, RowStyle:
switch value := value.(type) {
case string:
style.properties[tag] = value
return true
case Params:
style.properties[tag] = value
return true
case DataObject:
if params := value.ToParams(); len(params) > 0 {
style.properties[tag] = params
}
return true
case DataNode:
switch value.Type() {
case TextNode:
if text := value.Text(); text != "" {
style.properties[tag] = text
}
return true
case ObjectNode:
if obj := value.Object(); obj != nil {
if params := obj.ToParams(); len(params) > 0 {
style.properties[tag] = params
}
return true
}
case ArrayNode:
// TODO
}
}
case Outline: case Outline:
return style.setOutline(value) return style.setOutline(value)
@ -338,7 +392,7 @@ func (style *viewStyle) set(tag string, value any) bool {
return true return true
case DataObject: case DataObject:
if animation := parseAnimation(value); animation.hasAnimatedPropery() { if animation := parseAnimation(value); animation.hasAnimatedProperty() {
style.properties[tag] = []Animation{animation} style.properties[tag] = []Animation{animation}
return true return true
} }
@ -348,7 +402,7 @@ func (style *viewStyle) set(tag string, value any) bool {
result := true result := true
for i := 0; i < value.ArraySize(); i++ { for i := 0; i < value.ArraySize(); i++ {
if obj := value.ArrayElement(i).Object(); obj != nil { if obj := value.ArrayElement(i).Object(); obj != nil {
if anim := parseAnimation(obj); anim.hasAnimatedPropery() { if anim := parseAnimation(obj); anim.hasAnimatedProperty() {
animations = append(animations, anim) animations = append(animations, anim)
} else { } else {
result = false result = false

View File

@ -153,12 +153,37 @@ func GetOverflow(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, Overflow, defaultOverflow, false) return enumStyledProperty(view, subviewID, Overflow, defaultOverflow, false)
} }
// GetTabIndex returns the subview tab-index.
// If the second argument (subviewID) is not specified or it is "" then a tab-index of the first argument (view) is returned
func GetTabIndex(view View, subviewID ...string) int {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
defaultValue := -1
if view != nil {
if view.Focusable() {
defaultValue = 0
}
if value, ok := intProperty(view, TabIndex, view.Session(), defaultValue); ok {
return value
}
}
return defaultValue
}
// GetZIndex returns the subview z-order. // GetZIndex returns the subview z-order.
// If the second argument (subviewID) is not specified or it is "" then a z-order of the first argument (view) is returned // If the second argument (subviewID) is not specified or it is "" then a z-order of the first argument (view) is returned
func GetZIndex(view View, subviewID ...string) int { func GetZIndex(view View, subviewID ...string) int {
return intStyledProperty(view, subviewID, ZIndex, 0) return intStyledProperty(view, subviewID, ZIndex, 0)
} }
// GetOrder returns the subview order to layout an item in a ListLayout or GridLayout container.
// If the second argument (subviewID) is not specified or it is "" then an order of the first argument (view) is returned
func GetOrder(view View, subviewID ...string) int {
return intStyledProperty(view, subviewID, Order, 0)
}
// GetWidth returns the subview width. // GetWidth returns the subview width.
// If the second argument (subviewID) is not specified or it is "" then a width of the first argument (view) is returned // If the second argument (subviewID) is not specified or it is "" then a width of the first argument (view) is returned
func GetWidth(view View, subviewID ...string) SizeUnit { func GetWidth(view View, subviewID ...string) SizeUnit {
@ -296,6 +321,12 @@ func GetOutline(view View, subviewID ...string) ViewOutline {
return ViewOutline{Style: NoneLine, Width: AutoSize(), Color: 0} return ViewOutline{Style: NoneLine, Width: AutoSize(), Color: 0}
} }
// GetOutlineOffset returns the subview outline offset.
// If the second argument (subviewID) is not specified or it is "" then a offset of the first argument (view) is returned
func GetOutlineOffset(view View, subviewID ...string) SizeUnit {
return sizeStyledProperty(view, subviewID, OutlineOffset, false)
}
// GetViewShadows returns shadows of the subview. // GetViewShadows returns shadows of the subview.
// If the second argument (subviewID) is not specified or it is "" then shadows of the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then shadows of the first argument (view) is returned.
func GetViewShadows(view View, subviewID ...string) []ViewShadow { func GetViewShadows(view View, subviewID ...string) []ViewShadow {
@ -799,9 +830,13 @@ func colorStyledProperty(view View, subviewID []string, tag string, inherit bool
return Color(0) return Color(0)
} }
// FocusView sets focus on the specified View, if it can be focused. // FocusView sets focus on the specified subview, if it can be focused.
// The focused View is the View which will receive keyboard events by default. // The focused View is the View which will receive keyboard events by default.
func FocusView(view View) { // If the second argument (subviewID) is not specified or it is "" then focus is set on the first argument (view)
func FocusView(view View, subviewID ...string) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil { if view != nil {
view.Session().callFunc("focus", view.htmlID()) view.Session().callFunc("focus", view.htmlID())
} }
@ -890,3 +925,45 @@ func isUserSelect(view View) (bool, bool) {
return false, false return false, false
} }
// GetMixBlendMode returns a "mix-blend-mode" of the subview. Returns one of next values:
//
// BlendNormal (0), BlendMultiply (1), BlendScreen (2), BlendOverlay (3), BlendDarken (4),
// BlendLighten (5), BlendColorDodge (6), BlendColorBurn (7), BlendHardLight (8),
// BlendSoftLight (9), BlendDifference (10), BlendExclusion (11), BlendHue (12),
// BlendSaturation (13), BlendColor (14), BlendLuminosity (15)
//
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetMixBlendMode(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, MixBlendMode, BlendNormal, true)
}
// GetBackgroundBlendMode returns a "background-blend-mode" of the subview. Returns one of next values:
//
// BlendNormal (0), BlendMultiply (1), BlendScreen (2), BlendOverlay (3), BlendDarken (4),
// BlendLighten (5), BlendColorDodge (6), BlendColorBurn (7), BlendHardLight (8),
// BlendSoftLight (9), BlendDifference (10), BlendExclusion (11), BlendHue (12),
// BlendSaturation (13), BlendColor (14), BlendLuminosity (15)
//
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetBackgroundBlendMode(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, BackgroundBlendMode, BlendNormal, true)
}
// GetTooltip returns a tooltip text of the subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetTooltip(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.Get(Tooltip); value != nil {
if text, ok := value.(string); ok {
return text
}
}
}
return ""
}

View File

@ -2,7 +2,7 @@ package rui
import "strings" import "strings"
type ParanetView interface { type ParentView interface {
// Views return a list of child views // Views return a list of child views
Views() []View Views() []View
} }
@ -10,13 +10,15 @@ type ParanetView interface {
// ViewsContainer - mutable list-container of Views // ViewsContainer - mutable list-container of Views
type ViewsContainer interface { type ViewsContainer interface {
View View
ParanetView ParentView
// Append appends a view to the end of the list of a view children // Append appends a view to the end of the list of a view children
Append(view View) Append(view View)
// Insert inserts a view to the "index" position in the list of a view children // Insert inserts a view to the "index" position in the list of a view children
Insert(view View, index int) Insert(view View, index int)
// Remove removes a view from the list of a view children and return it // Remove removes a view from the list of a view children and return it
RemoveView(index int) View RemoveView(index int) View
// ViewIndex returns the index of view, -1 overwise
ViewIndex(view View) int
} }
type viewsContainerData struct { type viewsContainerData struct {
@ -117,6 +119,15 @@ func (container *viewsContainerData) RemoveView(index int) View {
return view return view
} }
func (container *viewsContainerData) ViewIndex(view View) int {
for index, v := range container.views {
if v == view {
return index
}
}
return -1
}
func (container *viewsContainerData) cssStyle(self View, builder cssBuilder) { func (container *viewsContainerData) cssStyle(self View, builder cssBuilder) {
container.viewData.cssStyle(self, builder) container.viewData.cssStyle(self, builder)
builder.add(`overflow`, `auto`) builder.add(`overflow`, `auto`)

View File

@ -135,7 +135,7 @@ func (bridge *wasmBridge) clearAnimation() {
styles.Set("textContent", "") styles.Set("textContent", "")
} }
func (bridge *wasmBridge) cavnasStart(htmlID string) { func (bridge *wasmBridge) canvasStart(htmlID string) {
if ProtocolInDebugLog { if ProtocolInDebugLog {
DebugLog("const ctx = document.getElementById('" + htmlID + "'elementId').getContext('2d');\nctx.save();") DebugLog("const ctx = document.getElementById('" + htmlID + "'elementId').getContext('2d');\nctx.save();")
} }
@ -204,7 +204,7 @@ func (bridge *wasmBridge) updateCanvasProperty(property string, value any) {
} }
} }
func (bridge *wasmBridge) cavnasFinish() { func (bridge *wasmBridge) canvasFinish() {
if !bridge.canvas.IsNull() { if !bridge.canvas.IsNull() {
DebugLog("ctx.restore()") DebugLog("ctx.restore()")
bridge.canvas.Call("restore") bridge.canvas.Call("restore")

View File

@ -146,7 +146,7 @@ func (bridge *wsBridge) argToString(arg any) (string, bool) {
} }
} }
ErrorLog("Unsupported agument type") ErrorLog("Unsupported argument type")
return "", false return "", false
} }
@ -238,7 +238,7 @@ if (styles) {
}`) }`)
} }
func (bridge *wsBridge) cavnasStart(htmlID string) { func (bridge *wsBridge) canvasStart(htmlID string) {
bridge.canvasBuffer.Reset() bridge.canvasBuffer.Reset()
bridge.canvasBuffer.WriteString(`const ctx = getCanvasContext('`) bridge.canvasBuffer.WriteString(`const ctx = getCanvasContext('`)
bridge.canvasBuffer.WriteString(htmlID) bridge.canvasBuffer.WriteString(htmlID)
@ -328,7 +328,7 @@ func (bridge *wsBridge) callCanvasImageFunc(url string, property string, funcNam
bridge.canvasBuffer.WriteString(");\n}") bridge.canvasBuffer.WriteString(");\n}")
} }
func (bridge *wsBridge) cavnasFinish() { func (bridge *wsBridge) canvasFinish() {
bridge.canvasBuffer.WriteString("\n") bridge.canvasBuffer.WriteString("\n")
script := bridge.canvasBuffer.String() script := bridge.canvasBuffer.String()
if ProtocolInDebugLog { if ProtocolInDebugLog {