Compare commits

...

79 Commits

Author SHA1 Message Date
Alexei Anoshenko d3b07f35ca Bug fixing 2025-03-13 11:50:57 +03:00
Alexei Anoshenko 31489dbc03 Bug fixing 2025-02-07 22:14:21 +03:00
Alexei Anoshenko f49b73e979 Bug fixing 2025-02-07 21:45:02 +03:00
Alexei Anoshenko b6832dac40 updated go.mod 2025-01-21 10:47:03 -05:00
Alexei Anoshenko 5d50d5b772 Bug fining 2024-12-18 19:11:51 +03:00
Alexei Anoshenko 6c0980c46e Merge branch 'v0.18' 2024-12-17 19:14:33 +03:00
Alexei Anoshenko fa984dcf78 Changed ViewByID functions 2024-12-10 18:23:04 +03:00
Alexei Anoshenko 0e0b73fdb9 Bug fixing 2024-12-08 21:11:46 +03:00
Alexei Anoshenko 86e58ef33a Bug fixing 2024-12-08 20:47:43 +03:00
Alexei Anoshenko 848606a3be Added "hide-summary-marker" DetailsView property 2024-12-07 19:24:54 +03:00
Alexei Anoshenko 0bdfe48f09 Update animation.go 2024-12-06 20:00:53 +03:00
Alexei Anoshenko 5971cd9105 Update animation.go 2024-12-06 19:56:51 +03:00
Alexei Anoshenko 28881bac9a Added NewTransitionAnimation, NewAnimation 2024-12-06 19:52:57 +03:00
Alexei Anoshenko 0c2bea9a75 Renamed Animation interface -> AnimationProperty 2024-12-06 19:15:23 +03:00
Alexei Anoshenko 1a60488537 Renamed ViewFilter interface -> FilterProperty 2024-12-06 18:52:34 +03:00
Alexei Anoshenko ec796b3697 Update README.md 2024-12-06 18:44:28 +03:00
Alexei Anoshenko 5039998cf9 Renamed ClipShape -> ClipShapeProperty 2024-12-06 18:38:43 +03:00
Alexei Anoshenko a8242c99fe Updated doc comments 2024-12-05 20:15:39 +03:00
Alexei Anoshenko 0919376f09 Updated CHANGELOG.md 2024-12-04 19:35:28 +03:00
Alexei Anoshenko cccf1e6ee1 Added conic gradient to canvas 2024-12-04 19:27:33 +03:00
Alexei Anoshenko 7bb90e5b2a Added New...Gradient functions 2024-12-04 18:45:08 +03:00
Alexei Anoshenko 5f55d30443 Added NewRadius functions 2024-12-03 11:20:32 +03:00
Alexei Anoshenko 8e20e80899 Added NewBounds function 2024-12-03 10:45:55 +03:00
Alexei Anoshenko 32141b996a Rename ViewShadow interface 2024-12-03 10:25:55 +03:00
Alexei Anoshenko 5efa2f5ae8 Bug fixing 2024-12-02 15:47:06 +03:00
Alexei Anoshenko 8a353f765e Improved SizeUnit and AngleUnit functions 2024-12-02 15:05:49 +03:00
Alexei Anoshenko f81ffe6bed Refactoring 2024-12-01 12:42:38 +03:00
Alexei Anoshenko bed6c1bf41 Optimisation 2024-12-01 12:30:33 +03:00
Alexei Anoshenko f632104d49 Update backgroundConicGradient.go 2024-11-27 16:28:11 +02:00
Alexei Anoshenko 7ce631b6ce Bug fixing 2024-11-27 16:06:12 +02:00
Alexei Anoshenko 27beb1e679 Added "mask" property 2024-11-27 10:32:13 +02:00
Alexei Anoshenko a5577273e6 Update CHANGELOG.md 2024-11-26 12:15:06 +02:00
Alexei Anoshenko 87735b3a4d Update CHANGELOG.md 2024-11-26 12:11:39 +02:00
Alexei Anoshenko e9937a8f3a Update popup.go 2024-11-26 11:56:52 +02:00
Alexei Anoshenko bc6e0c4db9 Added Popup show/hide animation 2024-11-25 22:13:29 +02:00
Alexei Anoshenko 5cc4475370 * Added "push-perspective", "push-rotate-x", "push-rotate-y", "push-rotate-z", "push-rotate", "push-skew-x", "push-skew-y", "push-scale-x", "push-scale-y", "push-scale-z", "push-translate-x", "push-translate-y", "push-translate-z" properties. 2024-11-25 12:03:08 +02:00
Alexei Anoshenko bdc8472600 Cleanup 2024-11-24 21:15:05 +02:00
Alexei Anoshenko 91093637c7 Update stackLayout.go 2024-11-24 21:14:35 +02:00
Alexei Anoshenko 488368de8c Optimisation 2024-11-24 17:23:24 +02:00
Alexei Anoshenko 6b2a5b4aee Optimisation 2024-11-24 15:43:31 +02:00
Alexei Anoshenko ed639c94c6 Improved StackLayout push/pop animation 2024-11-24 08:52:43 +02:00
Alexei Anoshenko 31c07ced98 Bug fixing 2024-11-22 15:36:08 +02:00
Alexei Anoshenko e8da32fca8 Bug fixing 2024-11-21 17:50:58 +02:00
Alexei Anoshenko 32f0f83ebf Bug fixing 2024-11-21 15:48:21 +03:00
Alexei Anoshenko 84a00af595 Improved DetailsView 2024-11-21 08:25:46 +02:00
Alexei Anoshenko 7d4b90769f Bug fixing 2024-11-20 15:06:59 +02:00
Alexei Anoshenko 857ad69260 Transform interface renamed to TransformProperty. TransformTag constant renamed to Transform. 2024-11-18 16:35:21 +02:00
Alexei Anoshenko 0f2e7e55ea OriginX, OriginY, and OriginZ properties renamed to TransformOriginX, TransformOriginY, and TransformOriginZ. GetOrigin function renamed to GetTransformOrigin 2024-11-18 16:20:25 +02:00
Alexei Anoshenko ff6b7c7e67
Merge pull request #7 from anoshenko/feature/fix-view-radius-setting 2024-11-14 14:20:13 +03:00
Mikalai Turankou bdd722ba09 Fixed issue while setting the view radius corner values from resource file 2024-11-14 12:26:35 +03:00
Alexei Anoshenko e2775d52f2 Added PropertyName type 2024-11-13 12:56:39 +03:00
Alexei Anoshenko 8fcc52de63 Added LineJoin and LineCap type 2024-10-28 13:11:43 +03:00
Alexei Anoshenko b65b7f6df8 Added comments 2024-10-21 18:37:35 +03:00
Alexei Anoshenko daf41dd7e0 Update CHANGELOG.md 2024-10-17 18:08:35 +03:00
Alexei Anoshenko d392d5214b Bug fixing 2024-10-17 18:07:17 +03:00
Alexei Anoshenko 7ac196c549 Bug fixing 2024-09-25 13:45:47 +03:00
Mikalai Turankou f239af2324 Added more comments for the constants which represent UI element's properties and events 2024-09-18 13:50:06 +03:00
Mikalai Turankou f9822a22f2 Fixed typo 2024-09-16 14:11:54 +03:00
Mikalai Turankou 1a21487540 Added missing comments for exported types like constants, variables, functions, structs and interfaces 2024-09-12 14:05:11 +03:00
Alexei Anoshenko 5707ca3088 Added "item-separators" property to DropDownList and GetDropDownItemSeparators function 2024-09-03 19:55:14 +03:00
Alexei Anoshenko 10cf3a69fc Update defaultTheme.rui 2024-09-03 18:53:14 +03:00
Alexei Anoshenko 1110375cb6 Added support of AccentColor to Checkbox, ListView, and TableView 2024-09-03 15:31:11 +03:00
Alexei Anoshenko 6afb518645 Added GetCheckboxChangedListeners function 2024-09-03 14:51:19 +03:00
Alexei Anoshenko e65c04188c Bug fixing 2024-08-24 19:39:18 +03:00
Alexei Anoshenko 87148836c0 Updated Path
* Added NewPath and NewPathFromSvg methods to Canvas interface
* Removed NewPath function
* Removed Reset methods from Path interface
2024-08-20 20:01:26 +03:00
Alexei Anoshenko 2708c7ceb6 Added RemoveClientItem to Session interface 2024-08-14 18:48:10 +03:00
Alexei Anoshenko a6707495cf Bug fixing 2024-08-13 16:27:54 +03:00
Alexei Anoshenko 7f06a9d201 Added "transform" property and Transform interface 2024-08-13 13:52:47 +03:00
Alexei Anoshenko 1d94d14b1e Update animationRun.go 2024-08-01 22:32:31 +03:00
Alexei Anoshenko ea388467b4 Update README.md 2024-08-01 16:58:31 +03:00
Alexei Anoshenko c9954afa7f Added OpenRawResource function 2024-07-17 16:32:05 +03:00
Alexei Anoshenko 95043b0b9a Fixed animation 2024-07-17 16:30:43 +03:00
Alexei Anoshenko 9b27cb4a55 Updated Animation.String method 2024-07-06 13:04:12 +03:00
anoshenko 5e3d37a6a0 Added Start, Stop, Pause, and Resume methods to Animation interface 2024-07-05 16:41:07 +03:00
anoshenko 2f3de8fce3 Optimised animation 2024-07-01 19:17:03 +03:00
anoshenko 09031b9fa0 Bug fixing 2024-06-29 12:05:27 +03:00
Alexei Anoshenko b2b9befa14 Optimisation 2024-06-26 19:01:00 +03:00
Alexei Anoshenko 00d6e2379d Added "mod", "rem", "round", "round-up", "round-down", and "round-to-zero" SizeFunc functions 2024-06-26 18:45:47 +03:00
Alexei Anoshenko d1d8c2af37 Bug fixing 2024-06-19 16:36:50 +03:00
106 changed files with 18050 additions and 11747 deletions

View File

@ -1,3 +1,81 @@
# v0.18.2
* fixed typo: GetShadowPropertys -> GetShadowProperty
# v0.18.0
* Property name type changed from string to PropertyName.
* Renamed:
Transform interface -> TransformProperty
NewTransform function -> NewTransformProperty
TransformTag constant -> Transform.
"origin-x" property -> "transform-origin-x"
"origin-y" property -> "transform-origin-y"
"origin-z" property -> "transform-origin-z"
GetOrigin function -> GetTransformOrigin.
BorderBoxClip constant -> BorderBox
PaddingBoxClip constant -> PaddingBox
ContentBoxClip constant -> ContentBox.
ViewShadow interface -> ShadowProperty
NewViewShadow function -> NewShadow
NewInsetViewShadow function -> NewInsetShadow
NewShadowWithParams function -> NewShadowProperty
NewColumnSeparator function -> NewColumnSeparatorProperty
ClipShape interface -> ClipShapeProperty
InsetClip function -> NewInsetClip
CircleClip function -> NewCircleClip
EllipseClip function -> NewEllipseClip
PolygonClip function -> NewPolygonClip
PolygonPointsClip function -> NewPolygonPointsClip
ViewFilter interface -> FilterProperty
NewViewFilter function -> NewFilterProperty
Animation interface -> AnimationProperty
AnimationTag constant -> Animation
NewAnimation function -> NewAnimationProperty
* Added functions: NewBounds, NewEllipticRadius, NewRadii, NewLinearGradient, NewCircleRadialGradient,
NewEllipseRadialGradient, GetPushTransform, GetPushDuration, GetPushTiming, IsMoveToFrontAnimation,
GetBackground, GetMask, GetBackgroundClip,GetBackgroundOrigin, GetMaskClip, GetMaskOrigin, NewColumnSeparator,
NewClipShapeProperty, NewTransitionAnimation, NewAnimation, IsSummaryMarkerHidden.
* Changed ViewByID functions
* Added SetConicGradientFillStyle and SetConicGradientStrokeStyle methods to Canvas interface.
* Changed Push, Pop, MoveToFront, and MoveToFrontByID methods of StackLayout interface.
* Removed DefaultAnimation, StartToEndAnimation, EndToStartAnimation, TopDownAnimation, and BottomUpAnimation constants.
* Added StackLayout properties: "push-transform", "push-duration", "push-timing", "move-to-front-animation", "push-perspective",
"push-rotate-x", "push-rotate-y", "push-rotate-z", "push-rotate", "push-skew-x", "push-skew-y",
"push-scale-x", "push-scale-y", "push-scale-z", "push-translate-x", "push-translate-y", "push-translate-z".
* Added "show-opacity", "show-transform", "show-duration", and "show-timing" Popup properties.
* Added "mask", "mask-clip", "mask-origin", and "background-origin" properties.
* Added "hide-summary-marker" DetailsView property.
* Added LineJoin type. Type of constants MiterJoin, RoundJoin, and BevelJoin changed to LineJoin. Type of Canvas.SetLineJoin function argument changed to LineJoin.
* Added LineCap type. Type of constants ButtCap, RoundCap, and SquareCap changed to LineCap. Type of Canvas.SetLineCap function argument changed to LineCap.
# v0.17.3
Added SetParams method to View interface
# v0.17.0
* Added "mod", "rem", "round", "round-up", "round-down", and "round-to-zero" SizeFunc functions
* Added ModSize, RemSize, RoundSize, RoundUpSize, RoundDownSize, and RoundToZeroSize functions
* Added Start, Stop, Pause, and Resume methods to Animation interface
* Added "transform" property and Transform interface
* Added OpenRawResource, GetCheckboxChangedListeners functions
* Added RemoveClientItem method to Session interface
* Added "item-separators" property to DropDownList and GetDropDownItemSeparators function
* Added NewPath and NewPathFromSvg methods to Canvas interface
* Removed NewPath function
* Removed Reset methods from Path interface
# v0.16.0
* Can use ListAdapter as "content" property value of ListLayout
* The IsListItemEnabled method of the ListAdapter interface has been made optional

View File

@ -161,6 +161,12 @@ SizeUnit объявлена как
| "sub(<arg1>, <arg2>)" | SubSize(arg0, arg1 any) | находит разность значений аргументов |
| "mul(<arg1>, <arg2>)" | MulSize(arg0, arg1 any) | находит результат умножения значений аргументов |
| "div(<arg1>, <arg2>)" | DivSize(arg0, arg1 any) | находит результат деления значений аргументов |
| "rem(<arg1>, <arg2>)" | ModSize(arg0, arg1 any) | находит остаток деления значений аргументов, результат имеет тотже знак что и делимое |
| "mod(<arg1>, <arg2>)" | ModSize(arg0, arg1 any) | находит остаток деления значений аргументов, результат имеет тотже знак что и делитель |
| "round(<arg1>, <arg2>)" | RoundSize(arg0, arg1 any) | округляет первый аргумент до ближайшего целого числа кратного второму аргументу |
| "round-up(<arg1>, <arg2>)" | RoundUpSize(arg0, arg1 any) | округляет первый аргумент до ближайшего большего целого числа, кратного второму аргументу |
| "round-down(<arg1>, <arg2>)" | RoundDownSize(arg0, arg1 any) | округляет первый аргумент до ближайшего меньшего целого числа кратного второму аргументу |
| "round-to-zero(<arg1>, <arg2>)" | RoundToZeroSize(arg0, arg1 any) | округляет первый аргумент до ближайшего целого числа кратного второму аргументу, которое ближе к нулю по сравнению с первым аргументом |
| "clamp(<min>, <val>, <max>)" | ClampSize(min, val, max any) | ограничивает значение заданным диапазоном |
Дополнительные пояснения к функции "clamp(<min>, <val>, <max>)": результат вычисляется следующим образом:
@ -169,6 +175,17 @@ SizeUnit объявлена как
* if val < min then min;
* if max < val then max;
Аргументы всех функций могут иметь следующий тип:
* SizeUnit;
* SizeFunc;
* string являющееся SizeUnit константой или текстовым представлением SizeUnit или SizeFunc.
Кроме этого второй аргумент функций mul, div, mod, rem и всех round может быть числом
(float32, float32, int, int8...int64, uint, uint8...unit64).
Также второй аргумент функций div, mod, rem и всех round не может быть нулевым значением.
### Color
Тип Color описывает 32-битный цвет в формате ARGB:
@ -408,7 +425,7 @@ View имеет ряд свойств, таких как высота, шири
(View реализует данный интерфейс):
type Properties interface {
Get(tag string) any
Get(tag PropertyName) any
Set(tag string, value any) bool
Remove(tag string)
Clear()
@ -1030,7 +1047,7 @@ RadiusProperty, а не структура BoxRadius. Получить стру
### Свойство "shadow"
Свойство "shadow" позволяет задать тени для View. Теней может быть несколько. Тень описывается
с помощью интерфейса ViewShadow расширяющего интерфейс Properties (см. выше). У тени имеются следующие свойства:
с помощью интерфейса ShadowProperty расширяющего интерфейс Properties (см. выше). У тени имеются следующие свойства:
| Свойство | Константа | Тип | Описание |
|-----------------|---------------|----------|---------------------------------------------------------------|
@ -1041,13 +1058,13 @@ RadiusProperty, а не структура BoxRadius. Получить стру
| "blur" | BlurRadius | float | Радиус размытия тени. Значение должно быть >= 0 |
| "spread-radius" | SpreadRadius | float | Увеличение тени. Значение > 0 увеличивает тень, < 0 уменьшает |
Для создания ViewShadow используются три функции:
Для создания ShadowProperty используются три функции:
func NewViewShadow(offsetX, offsetY, blurRadius, spread-radius SizeUnit, color Color) ViewShadow
func NewInsetViewShadow(offsetX, offsetY, blurRadius, spread-radius SizeUnit, color Color) ViewShadow
func NewShadowWithParams(params Params) ViewShadow
func NewShadowProperty(offsetX, offsetY, blurRadius, spread-radius SizeUnit, color Color) ShadowProperty
func NewInsetShadowProperty(offsetX, offsetY, blurRadius, spread-radius SizeUnit, color Color) ShadowProperty
func NewShadowWithParams(params Params) ShadowProperty
Функция NewViewShadow создает внешнюю тень (Inset == false), NewInsetViewShadow - внутреннюю
Функция NewShadowProperty создает внешнюю тень (Inset == false), NewInsetShadowProperty - внутреннюю
(Inset == true).
Функция NewShadowWithParams используется когда в качестве параметров необходимо использовать
константы. Например:
@ -1058,10 +1075,10 @@ RadiusProperty, а не структура BoxRadius. Получить стру
rui.Dilation : 16.0,
})
В качестве значения свойству "shadow" может быть присвоено ViewShadow, массив ViewShadow,
текстовое представление ViewShadow.
В качестве значения свойству "shadow" может быть присвоено ShadowProperty, массив ShadowProperty,
текстовое представление ShadowProperty.
Текстовое представление ViewShadow имеет следующий формат:
Текстовое представление ShadowProperty имеет следующий формат:
_{ color = <цвет> [, x-offset = <смещение>] [, y-offset = <смещение>] [, blur = <радиус>]
[, spread-radius = <увеличение>] [, inset = <тип>] }
@ -1069,7 +1086,7 @@ RadiusProperty, а не структура BoxRadius. Получить стру
Получить значение данного свойства можно с помощью функции
func GetViewShadows(view View, subviewID ...string) []ViewShadow
func GetShadowPropertys(view View, subviewID ...string) []ShadowProperty
Если тень не задана, то данная функция вернет пустой массив
@ -1305,14 +1322,14 @@ AngleUnit или string (угловая константа или текстов
### Свойство "clip"
Свойство "clip" (константа Clip) типа ClipShape задает задает область образки.
Свойство "clip" (константа Clip) типа ClipShapeProperty задает задает область образки.
Есть 4 типа областей обрезки
#### inset
Прямоугольная область обрезки. Создается с помощью функции:
func InsetClip(top, right, bottom, left SizeUnit, radius RadiusProperty) ClipShape
func NewInsetClip(top, right, bottom, left SizeUnit, radius RadiusProperty) ClipShapeProperty
где top, right, bottom, left это расстояние от соответственно верхней, правой, нижней и левой границы
View до одноименной границы обрезки; radius - задает радиусы скругления углов области обрезки
@ -1329,7 +1346,7 @@ radius необходимо передать nil
Круглая область обрезки. Создается с помощью функции:
func CircleClip(x, y, radius SizeUnit) ClipShape
func NewCircleClip(x, y, radius SizeUnit) ClipShapeProperty
где x, y - координаты центра окружности; radius - радиус
@ -1341,7 +1358,7 @@ radius необходимо передать nil
Эллиптическая область обрезки. Создается с помощью функции:
func EllipseClip(x, y, rx, ry SizeUnit) ClipShape
func NewEllipseClip(x, y, rx, ry SizeUnit) ClipShapeProperty
где x, y - координаты центра эллипса; rх - радиус эллипса по оси X; ry - радиус эллипса по оси Y.
@ -1353,8 +1370,8 @@ radius необходимо передать nil
Многоугольная область обрезки. Создается с помощью функций:
func PolygonClip(points []any) ClipShape
func PolygonPointsClip(points []SizeUnit) ClipShape
func NewPolygonClip(points []any) ClipShapeProperty
func NewPolygonPointsClip(points []SizeUnit) ClipShapeProperty
в качестве аргумента передается массив угловых точек многоугольника в следующем порядке: x1, y1, x2, y2, …
В качестве элементов аргумента функции PolygonClip могут быть или текстовые константы, или
@ -1418,10 +1435,10 @@ radius необходимо передать nil
Свойство "filter" (константа Filter) применяет ко View такие графические эффекты, как размытие, смещение цвета, изменение яркости/контрастности и т.п.
Свойства "backdrop-filter" (константа BackdropFilter) применяет такие же эффекты но к содержимому располагающемся ниже View.
В качестве значения свойств "filter" и "backdrop-filter" используется только интерфейс ViewFilter. ViewFilter создается с помощью
В качестве значения свойств "filter" и "backdrop-filter" используется только интерфейс FilterProperty. FilterProperty создается с помощью
функции
func NewViewFilter(params Params) ViewFilter
func NewFilterProperty(params Params) FilterProperty
В аргументе перечисляются применяемые эффекты. Возможны следующие эффекты:
@ -1430,7 +1447,7 @@ radius необходимо передать nil
| "blur" | Blur | float64 0…10000px | Размытие по Гауссу |
| "brightness" | Brightness | float64 0…10000% | Изменение яркости |
| "contrast" | Contrast | float64 0…10000% | Изменение контрастности |
| "drop-shadow" | DropShadow | []ViewShadow | Добавление тени |
| "drop-shadow" | DropShadow | []ShadowProperty | Добавление тени |
| "grayscale" | Grayscale | float64 0…100% | Преобразование к оттенкам серого |
| "hue-rotate" | HueRotate | AngleUnit | Вращение оттенка |
| "invert" | Invert | float64 0…100% | Инвертирование цветов |
@ -1440,8 +1457,8 @@ radius необходимо передать nil
Получить значение текущего фильтра можно с помощью функций
func GetFilter(view View, subviewID ...string) ViewFilter
func GetBackdropFilter(view View, subviewID ...string) ViewFilter
func GetFilter(view View, subviewID ...string) FilterProperty
func GetBackdropFilter(view View, subviewID ...string) FilterProperty
### Свойство "semantics"
@ -1687,14 +1704,14 @@ radius необходимо передать nil
#### Свойство "text-shadow"
Свойство "text-shadow" позволяет задать тени для текста. Теней может быть несколько. Тень описывается
с помощью интерфейса ViewShadow (см. выше, раздел "Свойство 'shadow'"). Для тени текста используются только
с помощью интерфейса ShadowProperty (см. выше, раздел "Свойство 'shadow'"). Для тени текста используются только
Свойства "color", "x-offset", "y-offset" и "blur". Свойства "inset" и "spread-radius" игнорируются (т.е. их
задание не является ошибкой, просто никакого влияния на тень текста они не имеют).
Для создания ViewShadow для тени текста используются функции:
Для создания ShadowProperty для тени текста используются функции:
func NewTextShadow(offsetX, offsetY, blurRadius SizeUnit, color Color) ViewShadow
func NewShadowWithParams(params Params) ViewShadow
func NewTextShadow(offsetX, offsetY, blurRadius SizeUnit, color Color) ShadowProperty
func NewShadowWithParams(params Params) ShadowProperty
Функция NewShadowWithParams используется когда в качестве параметров необходимо использовать
константы. Например:
@ -1704,12 +1721,12 @@ radius необходимо передать nil
rui.BlurRadius : 8.0,
})
В качестве значения свойству "text-shadow" может быть присвоено ViewShadow, массив ViewShadow,
текстовое представление ViewShadow (см. выше, раздел "Свойство 'shadow'").
В качестве значения свойству "text-shadow" может быть присвоено ShadowProperty, массив ShadowProperty,
текстовое представление ShadowProperty (см. выше, раздел "Свойство 'shadow'").
Получить значение данного свойства можно с помощью функции
func GetTextShadows(view View, subviewID ...string) []ViewShadow
func GetTextShadows(view View, subviewID ...string) []ShadowProperty
Если тень не задана, то данная функция вернет пустой массив
@ -2963,13 +2980,14 @@ DetailsView переключается между состояниями по к
"expanded" (константа Expanded). Соответственно значение "true" показывает дочерние
View, "false" - скрывает.
Получить значение свойства "expanded" можно с помощью функции
По умолчанию в начале элемента "summary" отображается маркер ▶︎/▼. Его можно спрятать.
Для этого используется свойство "hide-summary-marker" (константа DetailsView) типа bool.
func IsDetailsExpanded(view View, subviewID ...string) bool
а значение свойства "summary" можно получить с помощью функции
Получить значение свойств "summary", "expanded" и "hide-summary-marker" можно с помощью функций
func GetDetailsSummary(view View, subviewID ...string) View
func IsDetailsExpanded(view View, subviewID ...string) bool
func IsSummaryMarkerHidden(view View, subviewID ...string) bool
## Resizable
@ -3626,6 +3644,13 @@ float32, float64, int, int8…int64, uint, uint8…uint64.
func GetDropDownDisabledItems(view View, subviewID ...string) []int
Между пунктами списка можно добавлять разделителями. Для этого используется свойство "item-separators" (константа ItemSeparators).
Данному свойству присваивается массив индексов пунктов после которых необходимо добавить разделители.
Свойству "item-separators" могут присваиваться теже типы данных что и свойству "disabled-items".
Прочитать значение свойства "item-separators" можно с помощью функции
func GetDropDownItemSeparators(view View, subviewID ...string) []int
Выбранное значение определяется int свойством "current" (константа Current). Значение по умолчанию 0.
Прочитать значение данного свойства можно с помощью функции
@ -4426,7 +4451,17 @@ rotation - угол поворота эллипса относительно ц
#### Path
Интерфейс Path позволяет описать сложную фигуру. Создается Path с помощью функции NewPath().
Интерфейс Path позволяет описать сложную фигуру. Для создания объекта Path используются два метода Canvas:
NewPath() Path
NewPathFromSvg(data string) Path
Метод NewPath() создает пустую фигуру. Далее вы должны описать фигуру используя методы интерфейса Path
Метод NewPathFromSvg(data string) Path создает фигуру описанную в параметре data.
Параметр data является описанием фигуры в формате елемента <path> svg изображения. Например
path := canvas.NewPathFromSvg("M 30,0 C 30,0 27,8.6486 17,21.622 7,34.595 0,40 0,40 0,40 6,44.3243 17,58.378 28,72.432 30,80 30,80 30,80 37.8387,65.074 43,58.378 53,45.405 60,40 60,40 60,40 53,34.5946 43,21.622 33,8.649 30,0 30,0 Z")
После создания вы должны описать фигуру. Для этого могут использоваться следующие функции интерфейса:
@ -5004,14 +5039,14 @@ onNo или onCancel (если она не nil).
* Анимированное изменения значения свойства (далее "анимация перехода")
* Сценарий анимированного изменения одного или нескольких свойств (далее просто "сценарий анимации")
### Интерфейс Animation
### Интерфейс AnimationProperty
Для задания параметров анимации используется интерфейс Animation. Он расширяет интерфейс Properties.
Для задания параметров анимации используется интерфейс AnimationProperty. Он расширяет интерфейс Properties.
Интерфейс создается с помощью функции:
func NewAnimation(params Params) Animation
func NewAnimationProperty(params Params) AnimationProperty
Часть свойств интерфейса Animation используется в обоих типах анимации, остальные используются
Часть свойств интерфейса AnimationProperty используется в обоих типах анимации, остальные используются
только в сценариях анимации.
Общими свойствами являются
@ -5048,13 +5083,13 @@ onNo или onCancel (если она не nil).
Например
animation := rui.NewAnimation(rui.Params{
animation := rui.NewAnimationProperty(rui.Params{
rui.TimingFunction: rui.StepsTiming(10),
})
эквивалентно
animation := rui.NewAnimation(rui.Params{
animation := rui.NewAnimationProperty(rui.Params{
rui.TimingFunction: "steps(10)",
})
@ -5078,31 +5113,31 @@ x1 и x2 должны быть в диапазоне [0, 1]. Вы можете
Однократная анимация запускается с помощью функции SetAnimated интерфейса View. Данная функция имеет следующее
описание:
SetAnimated(tag string, value any, animation Animation) bool
SetAnimated(tag string, value any, animation AnimationProperty) bool
Она присваивает свойству новое значение, при этом изменение происходит с использованием заданной анимации.
Например,
view.SetAnimated(rui.Width, rui.Px(400), rui.NewAnimation(rui.Params{
view.SetAnimated(rui.Width, rui.Px(400), rui.NewAnimationProperty(rui.Params{
rui.Duration: 0.75,
rui.TimingFunction: rui.EaseOutTiming,
}))
Есть также глобальная функция для анимированного однократного изменения значения свойства дочернего View
func SetAnimated(rootView View, viewID, tag string, value any, animation Animation) bool
func SetAnimated(rootView View, viewID, tag string, value any, animation AnimationProperty) bool
Постоянная анимация запускается каждый раз когда изменяется значение свойства. Для задания постоянной
анимации перехода используется свойство "transition" (константа Transition). В качества значения данному
Свойству присваивается rui.Params, где в качестве ключа должно быть имя свойства, а значение - интерфейс Animation.
Свойству присваивается rui.Params, где в качестве ключа должно быть имя свойства, а значение - интерфейс AnimationProperty.
Например,
view.Set(rui.Transition, rui.Params{
rui.Height: rui.NewAnimation(rui.Params{
rui.Height: rui.NewAnimationProperty(rui.Params{
rui.Duration: 0.75,
rui.TimingFunction: rui.EaseOutTiming,
},
rui.BackgroundColor: rui.NewAnimation(rui.Params{
rui.BackgroundColor: rui.NewAnimationProperty(rui.Params{
rui.Duration: 1.5,
rui.Delay: 0.5,
rui.TimingFunction: rui.Linear,
@ -5117,7 +5152,7 @@ x1 и x2 должны быть в диапазоне [0, 1]. Вы можете
Добавлять новые анимации перехода рекомендуется с помощью функции
func AddTransition(view View, subviewID, tag string, animation Animation) bool
func AddTransition(view View, subviewID, tag string, animation AnimationProperty) bool
Вызов данной функции эквивалентен следующему коду
@ -5158,7 +5193,7 @@ x1 и x2 должны быть в диапазоне [0, 1]. Вы можете
### Сценарий анимации
Сценарий анимации описывает более сложную анимацию, по сравнению с анимацией перехода. Для этого
в Animation добавляются дополнительные свойства:
в AnimationProperty добавляются дополнительные свойства:
#### Свойство "property"
@ -5222,11 +5257,11 @@ KeyFrames - промежуточные значения свойства (клю
#### Запуск анимации
Для запуска сценария анимации необходимо созданный Animation интерфейс присвоить свойству "animation"
(константа AnimationTag). Если View уже отображается на экране, то анимация запускается сразу (с учетом
Для запуска сценария анимации необходимо созданный AnimationProperty интерфейс присвоить свойству "animation"
(константа Animation). Если View уже отображается на экране, то анимация запускается сразу (с учетом
заданной задержки), в противоположном случае анимация запускается как только View отобразится на экране.
Свойству "animation" можно присваивать Animation и []Animation, т.е. можно запускать несколько анимаций
Свойству "animation" можно присваивать AnimationProperty и []AnimationProperty, т.е. можно запускать несколько анимаций
одновременно для одного View
Пример,
@ -5239,12 +5274,12 @@ KeyFrames - промежуточные значения свойства (клю
90: rui.Px(220),
}
}
animation := rui.NewAnimation(rui.Params{
animation := rui.NewAnimationProperty(rui.Params{
rui.PropertyTag: []rui.AnimatedProperty{prop},
rui.Duration: 2,
rui.TimingFunction: LinearTiming,
})
rui.Set(view, "subview", rui.AnimationTag, animation)
rui.Set(view, "subview", rui.Animation, animation)
#### Свойство "animation-paused"

145
README.md
View File

@ -164,6 +164,12 @@ In addition to "min", there are the following functions
| "sub(<arg1>, <arg2>)" | SubSize(arg0, arg1 any) | calculates the subtraction of argument values |
| "mul(<arg1>, <arg2>)" | MulSize(arg0, arg1 any) | calculates the result of multiplying the argument values |
| "div(<arg1>, <arg2>)" | DivSize(arg0, arg1 any) | calculates the result of dividing the argument values |
| "rem(<arg1>, <arg2>)" | ModSize(arg0, arg1 any) | calculates the remainder of a division operation with the same sign as the dividend |
| "mod(<arg1>, <arg2>)" | ModSize(arg0, arg1 any) | calculates the remainder of a division operation with the same sign as the divisor |
| "round(<arg1>, <arg2>)" | RoundSize(arg0, arg1 any) | rounds the first argument to the nearest integer multiple of the second argument, which may be either above or below the value. |
| "round-up(<arg1>, <arg2>)" | RoundUpSize(arg0, arg1 any) | rounds the first argument up to the nearest integer multiple of the second argument (if the value is negative, it will become "more positive") |
| "round-down(<arg1>, <arg2>)" | RoundDownSize(arg0, arg1 any) | rounds the first argument down to the nearest integer multiple of the second argument (if the value is negative, it will become "more negative") |
| "round-to-zero(<arg1>, <arg2>)" | RoundToZeroSize(arg0, arg1 any) | rounds the first argument to the nearest integer multiple of the second argument closer to/towards zero (a positive number will decrease, while a negative value will become "less negative") |
| "clamp(<min>, <val>, <max>)" | ClampSize(min, val, max any) | limits value to specified range |
Additional explanations for the function "clamp(<min>, <val>, <max>)": the result is calculated as follows:
@ -172,6 +178,17 @@ Additional explanations for the function "clamp(<min>, <val>, <max>)": the resul
* if val < min then min;
* if max < val then max;
The arguments of all functions can be of the following type:
* SizeUnit;
* SizeFunc;
* string being a SizeUnit constant or a text representation of SizeUnit or SizeFunc.
In addition, the second argument of the functions mul, div, mod, rem, and all round can be a number
(float32, float32, int, int8...int64, uint, uint8...unit64).
Also, the second argument of the div, mod, rem, and all round functions cannot be a zero value.
### Color
The Color type describes a 32-bit ARGB color:
@ -412,7 +429,7 @@ View has a number of properties like height, width, color, text parameters, etc.
The Properties interface is used to read and write the property value (View implements this interface):
type Properties interface {
Get(tag string) any
Get(tag PropertyName) any
Set(tag string, value any) bool
Remove(tag string)
Clear()
@ -1007,7 +1024,7 @@ equivalent to
### "shadow" property
The "shadow" property allows you to set shadows for the View. There may be several shadows.
The shadow is described using the ViewShadow interface extending the Properties interface (see above).
The shadow is described using the ShadowProperty interface extending the Properties interface (see above).
The shadow has the following properties:
| Property | Constant | Type | Description |
@ -1019,14 +1036,14 @@ The shadow has the following properties:
| "blur" | BlurRadius | float | Shadow blur radius. The value must be >= 0 |
| "spread-radius" | SpreadRadius | float | Increase the shadow. Value > 0 increases shadow, < 0 decreases shadow |
Three functions are used to create a ViewShadow:
Three functions are used to create a ShadowProperty:
func NewViewShadow(offsetX, offsetY, blurRadius, spread-radius SizeUnit, color Color) ViewShadow
func NewInsetViewShadow(offsetX, offsetY, blurRadius, spread-radius SizeUnit, color Color) ViewShadow
func NewShadowWithParams(params Params) ViewShadow
func NewShadowProperty(offsetX, offsetY, blurRadius, spread-radius SizeUnit, color Color) ShadowProperty
func NewInsetShadowProperty(offsetX, offsetY, blurRadius, spread-radius SizeUnit, color Color) ShadowProperty
func NewShadowWithParams(params Params) ShadowProperty
The NewViewShadow function creates an outer shadow (Inset == false),
NewInsetViewShadow - an inner one (Inset == true).
The NewShadowProperty function creates an outer shadow (Inset == false),
NewInsetShadowProperty - an inner one (Inset == true).
The NewShadowWithParams function is used when constants must be used as parameters.
For example:
@ -1036,16 +1053,16 @@ For example:
rui.Dilation : 16.0,
})
ViewShadow, ViewShadow array, and ViewShadow textual representation can be assigned as a value to the "shadow" property.
ShadowProperty, ShadowProperty array, and ShadowProperty textual representation can be assigned as a value to the "shadow" property.
The ViewShadow text representation has the following format:
The ShadowProperty text representation has the following format:
_{ color = <color> [, x-offset = <offset>] [, y-offset = <offset>] [, blur = <radius>]
[, spread-radius = <increase>] [, inset = <type>] }
You can get the value of "shadow" property using the function
func GetViewShadows(view View, subviewID ...string) []ViewShadow
func GetShadowPropertys(view View, subviewID ...string) []ShadowProperty
If no shadow is specified, then this function will return an empty array
@ -1202,6 +1219,10 @@ The textual representation of a conic gradient looks like this:
#### Image
The background image is created using the function
func NewBackgroundImage(params Params) BackgroundElement
The image has the following parameters:
* Source ("src") - Specifies the URL of the image
@ -1280,14 +1301,14 @@ You can get the value of this property using the function
### "clip" property
The "clip" property (Clip constant) of the ClipShape type specifies the crop area.
The "clip" property (Clip constant) of the ClipShapeProperty type specifies the crop area.
There are 4 types of crop areas
#### inset
Rectangular cropping area. Created with the function:
func InsetClip(top, right, bottom, left SizeUnit, radius RadiusProperty) ClipShape
func NewInsetClip(top, right, bottom, left SizeUnit, radius RadiusProperty) ClipShapeProperty
where top, right, bottom, left are the distance from respectively the top, right, bottom and left borders of the View
to the cropping border of the same name; radius - sets the radii of the corners of the cropping area
@ -1303,7 +1324,7 @@ The textual description of the rectangular cropping area is in the following for
Round cropping area. Created with the function:
func CircleClip(x, y, radius SizeUnit) ClipShape
func NewCircleClip(x, y, radius SizeUnit) ClipShapeProperty
where x, y - coordinates of the center of the circle; radius - radius
@ -1315,7 +1336,7 @@ The textual description of the circular cropping area is in the following format
Elliptical cropping area. Created with the function:
func EllipseClip(x, y, rx, ry SizeUnit) ClipShape
func NewEllipseClip(x, y, rx, ry SizeUnit) ClipShapeProperty
where x, y - coordinates of the center of the ellipse; rх - radius of the ellipse along the X axis; ry is the radius of the ellipse along the Y axis.
@ -1327,8 +1348,8 @@ The textual description of the elliptical clipping region is in the following fo
Polygonal cropping area. Created using functions:
func PolygonClip(points []any) ClipShape
func PolygonPointsClip(points []SizeUnit) ClipShape
func NewPolygonClip(points []any) ClipShapeProperty
func NewPolygonPointsClip(points []SizeUnit) ClipShapeProperty
an array of corner points of the polygon is passed as an argument in the following order: x1, y1, x2, y2, …
The elements of the argument to the PolygonClip function can be either text constants,
@ -1392,10 +1413,10 @@ You can get the value of this property using the function
The "filter" property (Filter constant) applies graphical effects to the View, such as blurring, color shifting, changing brightness/contrast, etc.
The "backdrop-filter" property (BackdropFilter constant) applies the same effects but to the area behind a View.
Only the ViewFilter interface is used as the value of the "filter" properties.
ViewFilter is created using the function
Only the FilterProperty interface is used as the value of the "filter" properties.
FilterProperty is created using the function
func NewViewFilter(params Params) ViewFilter
func NewFilterProperty(params Params) FilterProperty
The argument lists the effects to apply. The following effects are possible:
@ -1404,7 +1425,7 @@ The argument lists the effects to apply. The following effects are possible:
| "blur" | Blur | float64 0…10000px | Gaussian blur |
| "brightness" | Brightness | float64 0…10000% | Brightness change |
| "contrast" | Contrast | float64 0…10000% | Contrast change |
| "drop-shadow" | DropShadow | []ViewShadow | Adding shadow |
| "drop-shadow" | DropShadow | []ShadowProperty | Adding shadow |
| "grayscale" | Grayscale | float64 0…100% | Converting to grayscale |
| "hue-rotate" | HueRotate | AngleUnit | Hue rotation |
| "invert" | Invert | float64 0…100% | Invert colors |
@ -1421,8 +1442,8 @@ Example
You can get the value of the current filter using functions
func GetFilter(view View, subviewID ...string) ViewFilter
func GetBackdropFilter(view View, subviewID ...string) ViewFilter
func GetFilter(view View, subviewID ...string) FilterProperty
func GetBackdropFilter(view View, subviewID ...string) FilterProperty
### "semantics" property
@ -1665,14 +1686,14 @@ You can get the value of this property using the function
#### "text-shadow" property
The "text-shadow" property allows you to set shadows for the text. There may be several shadows.
The shadow is described using the ViewShadow interface (see above, section "The 'shadow' property").
The shadow is described using the ShadowProperty interface (see above, section "The 'shadow' property").
For text shadow, only the "color", "x-offset", "y-offset" and "blur" properties are used.
The "inset" and "spread-radius" properties are ignored (i.e. setting them is not an error, they just have no effect on the text shadow).
To create a ViewShadow for the text shadow, the following functions are used:
To create a ShadowProperty for the text shadow, the following functions are used:
func NewTextShadow(offsetX, offsetY, blurRadius SizeUnit, color Color) ViewShadow
func NewShadowWithParams(params Params) ViewShadow
func NewTextShadow(offsetX, offsetY, blurRadius SizeUnit, color Color) ShadowProperty
func NewShadowWithParams(params Params) ShadowProperty
The NewShadowWithParams function is used when constants must be used as parameters. For example:
@ -1681,11 +1702,11 @@ The NewShadowWithParams function is used when constants must be used as paramete
rui.BlurRadius: 8.0,
})
ViewShadow, ViewShadow array, ViewShadow textual representation can be assigned as a value to the "text-shadow" property (see above, section "The 'shadow' property").
ShadowProperty, ShadowProperty array, ShadowProperty textual representation can be assigned as a value to the "text-shadow" property (see above, section "The 'shadow' property").
You can get the value of this property using the function
func GetTextShadows(view View, subviewID ...string) []ViewShadow
func GetTextShadows(view View, subviewID ...string) []ShadowProperty
If no shadow is specified, then this function will return an empty array
@ -2942,13 +2963,14 @@ DetailsView switches between states by clicking on "summary" view.
For forced switching of the DetailsView states, the bool property "expanded" (Expanded constant) is used.
Accordingly, the value "true" shows child Views, "false" - hides.
You can get the value of the "expanded" property using the function
By default, a ▶︎/▼ marker is displayed at the beginning of the "summary" element. It can be hidden.
For this, the "hide-summary-marker" bool property (DetailsView constant) is used.
func IsDetailsExpanded(view View, subviewID ...string) bool
and the value of the "summary" property can be obtained using the function
The value of the "summary", "expanded" and "hide-summary-marker" properties can be obtained using the functions
func GetDetailsSummary(view View, subviewID ...string) View
func IsDetailsExpanded(view View, subviewID ...string) bool
func IsSummaryMarkerHidden(view View, subviewID ...string) bool
## Resizable
@ -3599,6 +3621,13 @@ You can read the value of the "disabled-items" property using the function
func GetDropDownDisabledItems(view View, subviewID ...string) []int
You can add separators between list items. To do this, use the "item-separators" property (the ItemSeparators constant).
This property is assigned an array of item indices after which separators must be added.
The "item-separators" property can be assigned the same data types as the "disabled-items" property.
You can read the value of the "item-separators" property using the function
func GetDropDownItemSeparators(view View, subviewID ...string) []int
The selected value is determined by the int property "current" (Current constant). The default is 0.
You can read the value of this property using the function
@ -4390,7 +4419,17 @@ rotation - the angle of rotation of the ellipse relative to the center in radian
#### Path
The Path interface allows you to describe a complex shape. Path is created using the NewPath () function.
The Path interface allows you to describe a complex shape. Two Canvas methods are used to create a Path object:
NewPath() Path
NewPathFromSvg(data string) Path
The NewPath() method creates an empty shape. Next, you must describe the shape using the methods of the Path interface
The NewPathFromSvg(data string) Path method creates the shape described in the data parameter.
The data parameter is a description of the shape in the format of a <path> svg image element. For example
path := canvas.NewPathFromSvg("M 30,0 C 30,0 27,8.6486 17,21.622 7,34.595 0,40 0,40 0,40 6,44.3243 17,58.378 28,72.432 30,80 30,80 30,80 37.8387,65.074 43,58.378 53,45.405 60,40 60,40 60,40 53,34.5946 43,21.622 33,8.649 30,0 30,0 Z")
Once created, you must describe the shape. For this, the following interface functions can be used:
@ -4971,14 +5010,14 @@ The library supports two types of animation:
* Animated property value changes (hereinafter "transition animation")
* Script animated change of one or more properties (hereinafter simply "animation script")
### Animation interface
### AnimationProperty interface
The Animation interface is used to set animation parameters. It extends the Properties interface.
The AnimationProperty interface is used to set animation parameters. It extends the Properties interface.
The interface is created using the function:
func NewAnimation(params Params) Animation
func NewAnimationProperty(params Params) AnimationProperty
Some of the properties of the Animation interface are used in both types of animation, the rest are used only
Some of the properties of the AnimationProperty interface are used in both types of animation, the rest are used only
in animation scripts.
Common properties are
@ -5015,13 +5054,13 @@ You can specify this function either as text or using the function:
For example
animation := rui.NewAnimation(rui.Params{
animation := rui.NewAnimationProperty(rui.Params{
rui.TimingFunction: rui.StepsTiming(10),
})
equivalent to
animation := rui.NewAnimation(rui.Params{
animation := rui.NewAnimationProperty(rui.Params{
rui.TimingFunction: "steps(10)",
})
@ -5044,32 +5083,32 @@ There are two types of transition animations:
A one-time animation is triggered using the SetAnimated function of the View interface.
This function has the following description:
SetAnimated(tag string, value any, animation Animation) bool
SetAnimated(tag string, value any, animation AnimationProperty) bool
It assigns a new value to the property, and the change occurs using the specified animation.
For example,
view.SetAnimated(rui.Width, rui.Px(400), rui.NewAnimation(rui.Params{
view.SetAnimated(rui.Width, rui.Px(400), rui.NewAnimationProperty(rui.Params{
rui.Duration: 0.75,
rui.TimingFunction: rui.EaseOutTiming,
}))
There is also a global function for animated one-time change of the property value of the child View
func SetAnimated(rootView View, viewID, tag string, value any, animation Animation) bool
func SetAnimated(rootView View, viewID, tag string, value any, animation AnimationProperty) bool
A persistent animation runs every time the property value changes.
To set the constant animation of the transition, use the "transition" property (the Transition constant).
As a value, this property is assigned rui.Params, where the property name should be the key,
and the value should be the Animation interface.
and the value should be the AnimationProperty interface.
For example,
view.Set(rui.Transition, rui.Params{
rui.Height: rui.NewAnimation(rui.Params{
rui.Height: rui.NewAnimationProperty(rui.Params{
rui.Duration: 0.75,
rui.TimingFunction: rui.EaseOutTiming,
},
rui.BackgroundColor: rui.NewAnimation(rui.Params{
rui.BackgroundColor: rui.NewAnimationProperty(rui.Params{
rui.Duration: 1.5,
rui.Delay: 0.5,
rui.TimingFunction: rui.Linear,
@ -5084,7 +5123,7 @@ To get the current list of permanent transition animations, use the function
It is recommended to add new transition animations using the function
func AddTransition(view View, subviewID, tag string, animation Animation) bool
func AddTransition(view View, subviewID, tag string, animation AnimationProperty) bool
Calling this function is equivalent to the following code
@ -5124,7 +5163,7 @@ Get lists of listeners for transition animation events using functions:
### Animation script
An animation script describes a more complex animation than a transition animation. To do this, additional properties are added to Animation:
An animation script describes a more complex animation than a transition animation. To do this, additional properties are added to AnimationProperty:
#### "property" property
@ -5190,12 +5229,12 @@ backward playback of the sequence. It can take the following values:
#### Animation start
To start the animation script, you must assign the interface created by Animation to the "animation" property
(the AnimationTag constant). If the View is already displayed on the screen, then the animation starts immediately
To start the animation script, you must assign the interface created by AnimationProperty to the "animation" property
(the Animation constant). If the View is already displayed on the screen, then the animation starts immediately
(taking into account the specified delay), otherwise the animation starts as soon as the View is displayed
on the screen.
The "animation" property can be assigned Animation and [] Animation, ie. you can run several animations
The "animation" property can be assigned AnimationProperty and [] AnimationProperty, ie. you can run several animations
at the same time for one View
Example,
@ -5208,12 +5247,12 @@ Example,
90: rui.Px(220),
}
}
animation := rui.NewAnimation(rui.Params{
animation := rui.NewAnimationProperty(rui.Params{
rui.PropertyTag: []rui.AnimatedProperty{prop},
rui.Duration: 2,
rui.TimingFunction: LinearTiming,
})
rui.Set(view, "subview", rui.AnimationTag, animation)
rui.Set(view, "subview", rui.Animation, animation)
#### "animation-paused" property

View File

@ -2,7 +2,7 @@ package rui
import "strings"
// AbsoluteLayout - list-container of View
// AbsoluteLayout represent an AbsoluteLayout view where child views can be arbitrary positioned
type AbsoluteLayout interface {
ViewsContainer
}
@ -20,7 +20,8 @@ func NewAbsoluteLayout(session Session, params Params) AbsoluteLayout {
}
func newAbsoluteLayout(session Session) View {
return NewAbsoluteLayout(session, nil)
//return NewAbsoluteLayout(session, nil)
return new(absoluteLayoutData)
}
// Init initialize fields of ViewsContainer by default values
@ -34,7 +35,7 @@ func (layout *absoluteLayoutData) htmlSubviews(self View, buffer *strings.Builde
if layout.views != nil {
for _, view := range layout.views {
view.addToCSSStyle(map[string]string{`position`: `absolute`})
viewHTML(view, buffer)
viewHTML(view, buffer, "")
}
}
}

View File

@ -11,43 +11,51 @@ import (
// Can take the following values: Radian, Degree, Gradian, and Turn
type AngleUnitType uint8
// Constants which represent values or the [AngleUnitType]
const (
// Radian - angle in radians
Radian AngleUnitType = 0
// Radian - angle in radians * π
PiRadian AngleUnitType = 1
// Degree - angle in degrees
Degree AngleUnitType = 2
// Gradian - angle in gradian (1400 of a full circle)
Gradian AngleUnitType = 3
// Turn - angle in turns (1 turn = 360 degree)
Turn AngleUnitType = 4
)
// AngleUnit describe a size (Value field) and size unit (Type field).
// AngleUnit used to represent an angular values
type AngleUnit struct {
Type AngleUnitType
// Type of the angle value
Type AngleUnitType
// Value of the angle in Type units
Value float64
}
// Deg creates AngleUnit with Degree type
func Deg(value float64) AngleUnit {
return AngleUnit{Type: Degree, Value: value}
func Deg[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) AngleUnit {
return AngleUnit{Type: Degree, Value: float64(value)}
}
// Rad create AngleUnit with Radian type
func Rad(value float64) AngleUnit {
return AngleUnit{Type: Radian, Value: value}
func Rad[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) AngleUnit {
return AngleUnit{Type: Radian, Value: float64(value)}
}
// PiRad create AngleUnit with PiRadian type
func PiRad(value float64) AngleUnit {
return AngleUnit{Type: PiRadian, Value: value}
func PiRad[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) AngleUnit {
return AngleUnit{Type: PiRadian, Value: float64(value)}
}
// Grad create AngleUnit with Gradian type
func Grad(value float64) AngleUnit {
return AngleUnit{Type: Gradian, Value: value}
func Grad[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) AngleUnit {
return AngleUnit{Type: Gradian, Value: float64(value)}
}
// Equal compare two AngleUnit. Return true if AngleUnit are equal

File diff suppressed because it is too large Load Diff

View File

@ -1,176 +1,259 @@
package rui
import "strings"
// Constants which describe values for view's animation events properties
const (
// TransitionRunEvent is the constant for "transition-run-event" property tag.
// The "transition-run-event" is fired when a transition is first created,
// i.e. before any transition delay has begun.
TransitionRunEvent = "transition-run-event"
//
// Used by View:
// Is fired when a transition is first created, i.e. before any transition delay has begun.
//
// General listener format:
// func(view rui.View, propertyName rui.PropertyName).
//
// where:
// - view - Interface of a view which generated this event,
// - propertyName - Name of the property.
//
// Allowed listener formats:
// func(view rui.View),
// func(propertyName rui.PropertyName)
// func().
TransitionRunEvent PropertyName = "transition-run-event"
// TransitionStartEvent is the constant for "transition-end-event" property tag.
// The "transition-start-event" is fired when a transition has actually started,
// i.e., after "delay" has ended.
TransitionStartEvent = "transition-start-event"
// TransitionStartEvent is the constant for "transition-start-event" property tag.
//
// Used by View:
// Is fired when a transition has actually started, i.e., after "delay" has ended.
//
// General listener format:
// func(view rui.View, propertyName rui.PropertyName).
//
// where:
// - view - Interface of a view which generated this event,
// - propertyName - Name of the property.
//
// Allowed listener formats:
// func(view rui.View)
// func(propertyName rui.PropertyName)
// func()
TransitionStartEvent PropertyName = "transition-start-event"
// TransitionEndEvent is the constant for "transition-end-event" property tag.
// The "transition-end-event" is fired when a transition has completed.
TransitionEndEvent = "transition-end-event"
//
// Used by View:
// Is fired when a transition has completed.
//
// General listener format:
// func(view rui.View, propertyName rui.PropertyName).
//
// where:
// - view - Interface of a view which generated this event,
// - propertyName - Name of the property.
//
// Allowed listener formats:
// func(view rui.View)
// func(propertyName rui.PropertyName)
// func()
TransitionEndEvent PropertyName = "transition-end-event"
// TransitionCancelEvent is the constant for "transition-cancel-event" property tag.
// The "transition-cancel-event" is fired when a transition is cancelled. The transition is cancelled when:
// * A new property transition has begun.
// * The "visibility" property is set to "gone".
// * The transition is stopped before it has run to completion, e.g. by moving the mouse off a hover-transitioning view.
TransitionCancelEvent = "transition-cancel-event"
//
// Used by View:
// Is fired when a transition is cancelled. The transition is cancelled when:
// - A new property transition has begun.
// - The "visibility" property is set to "gone".
// - The transition is stopped before it has run to completion, e.g. by moving the mouse off a hover-transitioning view.
//
// General listener format:
// func(view rui.View, propertyName rui.PropertyName).
//
// where:
// - view - Interface of a view which generated this event,
// - propertyName - Name of the property.
//
// Allowed listener formats:
// func(view rui.View)
// func(propertyName rui.PropertyName)
// func()
TransitionCancelEvent PropertyName = "transition-cancel-event"
// AnimationStartEvent is the constant for "animation-start-event" property tag.
// The "animation-start-event" is fired when an animation has started.
// If there is an animation-delay, this event will fire once the delay period has expired.
AnimationStartEvent = "animation-start-event"
//
// Used by View:
// Fired when an animation has started. If there is an "animation-delay", this event will fire once the delay period has
// expired.
//
// General listener format:
// func(view rui.View, animationId string).
//
// where:
// - view - Interface of a view which generated this event,
// - animationId - Id of the animation.
//
// Allowed listener formats:
// func(view rui.View)
// func(animationId string)
// func()
AnimationStartEvent PropertyName = "animation-start-event"
// AnimationEndEvent is the constant for "animation-end-event" property tag.
// 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
// or the animation is removed from the element, the "animation-end-event" is not fired.
AnimationEndEvent = "animation-end-event"
//
// Used by View:
// Fired when an animation has completed. 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.
//
// General listener format:
// func(view rui.View, animationId string).
//
// where:
// - view - Interface of a view which generated this event,
// - animationId - Id of the animation.
//
// Allowed listener formats:
// func(view rui.View)
// func(animationId string)
// func()
AnimationEndEvent PropertyName = "animation-end-event"
// AnimationCancelEvent is the constant for "animation-cancel-event" property tag.
// The "animation-cancel-event" is fired when an animation unexpectedly aborts.
// In other words, any time it stops running without sending the "animation-end-event".
// This might happen when the animation-name is changed such that the animation is removed,
// or when the animating view is hidden. Therefore, either directly or because any of its
// containing views are hidden.
// The event is not supported by all browsers.
AnimationCancelEvent = "animation-cancel-event"
//
// Used by View:
// Fired when an animation unexpectedly aborts. In other words, any time it stops running without sending the
// "animation-end-event". This might happen when the animation-name is changed such that the animation is removed, or when
// the animating view is hidden. Therefore, either directly or because any of its containing views are hidden. The event
// is not supported by all browsers.
//
// General listener format:
// func(view rui.View, animationId string).
//
// where:
// - view - Interface of a view which generated this event,
// - animationId - Id of the animation.
//
// Allowed listener formats:
// func(view rui.View)
// func(animationId string)
// func()
AnimationCancelEvent PropertyName = "animation-cancel-event"
// AnimationIterationEvent is the constant for "animation-iteration-event" property tag.
// The "animation-iteration-event" is fired when an iteration of an animation ends,
// and another one begins. This event does not occur at the same time as the animation end event,
// and therefore does not occur for animations with an "iteration-count" of one.
AnimationIterationEvent = "animation-iteration-event"
//
// Used by View:
// Fired when an iteration of an animation ends, and another one begins. This event does not occur at the same time as the
// animation end event, and therefore does not occur for animations with an "iteration-count" of one.
//
// General listener format:
// func(view rui.View, animationId string).
//
// where:
// - view - Interface of a view which generated this event,
// - animationId - Id of the animation.
//
// Allowed listener formats:
// func(view rui.View)
// func(animationId string)
// func()
AnimationIterationEvent PropertyName = "animation-iteration-event"
)
var transitionEvents = map[string]struct{ jsEvent, jsFunc string }{
TransitionRunEvent: {jsEvent: "ontransitionrun", jsFunc: "transitionRunEvent"},
TransitionStartEvent: {jsEvent: "ontransitionstart", jsFunc: "transitionStartEvent"},
TransitionEndEvent: {jsEvent: "ontransitionend", jsFunc: "transitionEndEvent"},
TransitionCancelEvent: {jsEvent: "ontransitioncancel", jsFunc: "transitionCancelEvent"},
}
func (view *viewData) setTransitionListener(tag string, value any) bool {
listeners, ok := valueToEventListeners[View, string](value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeTransitionListener(tag)
} else if js, ok := transitionEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
view.session.updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)")
/*
func setTransitionListener(properties Properties, tag PropertyName, value any) bool {
if listeners, ok := valueToOneArgEventListeners[View, string](value); ok {
if len(listeners) == 0 {
properties.setRaw(tag, nil)
} else {
properties.setRaw(tag, listeners)
}
} else {
return false
return true
}
return true
notCompatibleType(tag, value)
return false
}
func (view *viewData) removeTransitionListener(tag string) {
func (view *viewData) removeTransitionListener(tag PropertyName) {
delete(view.properties, tag)
if view.created {
if js, ok := transitionEvents[tag]; ok {
if js, ok := eventJsFunc[tag]; ok {
view.session.removeProperty(view.htmlID(), js.jsEvent)
}
}
}
func transitionEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range transitionEvents {
for _, tag := range []PropertyName{TransitionRunEvent, TransitionStartEvent, TransitionEndEvent, TransitionCancelEvent} {
if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View, string)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
if js, ok := eventJsFunc[tag]; ok {
if listeners, ok := value.([]func(View, string)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
}
}
}
}
}
*/
func (view *viewData) handleTransitionEvents(tag string, data DataObject) {
if property, ok := data.PropertyValue("property"); ok {
func (view *viewData) handleTransitionEvents(tag PropertyName, data DataObject) {
if propertyName, ok := data.PropertyValue("property"); ok {
property := PropertyName(propertyName)
if tag == TransitionEndEvent || tag == TransitionCancelEvent {
if animation, ok := view.singleTransition[property]; ok {
delete(view.singleTransition, property)
if animation != nil {
view.transitions[property] = animation
} else {
delete(view.transitions, property)
}
view.updateTransitionCSS()
setTransition(view, property, animation)
session := view.session
session.updateCSSProperty(view.htmlID(), "transition", transitionCSS(view, session))
}
}
for _, listener := range getEventListeners[View, string](view, nil, tag) {
for _, listener := range getOneArgEventListeners[View, PropertyName](view, nil, tag) {
listener(view, property)
}
}
}
var animationEvents = map[string]struct{ jsEvent, jsFunc string }{
AnimationStartEvent: {jsEvent: "onanimationstart", jsFunc: "animationStartEvent"},
AnimationEndEvent: {jsEvent: "onanimationend", jsFunc: "animationEndEvent"},
AnimationIterationEvent: {jsEvent: "onanimationiteration", jsFunc: "animationIterationEvent"},
AnimationCancelEvent: {jsEvent: "onanimationcancel", jsFunc: "animationCancelEvent"},
}
func (view *viewData) setAnimationListener(tag string, value any) bool {
listeners, ok := valueToEventListeners[View, string](value)
if !ok {
/*
func setAnimationListener(properties Properties, tag PropertyName, value any) bool {
if listeners, ok := valueToOneArgEventListeners[View, string](value); ok {
if len(listeners) == 0 {
properties.setRaw(tag, nil)
} else {
properties.setRaw(tag, listeners)
}
return true
}
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeAnimationListener(tag)
} else if js, ok := animationEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
view.session.updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)")
}
} else {
return false
}
return true
}
func (view *viewData) removeAnimationListener(tag string) {
func (view *viewData) removeAnimationListener(tag PropertyName) {
delete(view.properties, tag)
if view.created {
if js, ok := animationEvents[tag]; ok {
if js, ok := eventJsFunc[tag]; ok {
view.session.removeProperty(view.htmlID(), js.jsEvent)
}
}
}
func animationEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range animationEvents {
for _, tag := range []PropertyName{AnimationStartEvent, AnimationEndEvent, AnimationIterationEvent, AnimationCancelEvent} {
if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
if js, ok := eventJsFunc[tag]; ok {
if listeners, ok := value.([]func(View, string)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
}
}
}
}
}
*/
func (view *viewData) handleAnimationEvents(tag string, data DataObject) {
if listeners := getEventListeners[View, string](view, nil, tag); len(listeners) > 0 {
func (view *viewData) handleAnimationEvents(tag PropertyName, data DataObject) {
if listeners := getOneArgEventListeners[View, string](view, nil, tag); len(listeners) > 0 {
id := ""
if name, ok := data.PropertyValue("name"); ok {
for _, animation := range GetAnimation(view) {
@ -189,54 +272,54 @@ func (view *viewData) handleAnimationEvents(tag string, data DataObject) {
// 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.
func GetTransitionRunListeners(view View, subviewID ...string) []func(View, string) {
return getEventListeners[View, string](view, subviewID, TransitionRunEvent)
return getOneArgEventListeners[View, string](view, subviewID, TransitionRunEvent)
}
// GetTransitionStartListeners returns the "transition-start-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.
func GetTransitionStartListeners(view View, subviewID ...string) []func(View, string) {
return getEventListeners[View, string](view, subviewID, TransitionStartEvent)
return getOneArgEventListeners[View, string](view, subviewID, TransitionStartEvent)
}
// GetTransitionEndListeners returns the "transition-end-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.
func GetTransitionEndListeners(view View, subviewID ...string) []func(View, string) {
return getEventListeners[View, string](view, subviewID, TransitionEndEvent)
return getOneArgEventListeners[View, string](view, subviewID, TransitionEndEvent)
}
// GetTransitionCancelListeners returns the "transition-cancel-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.
func GetTransitionCancelListeners(view View, subviewID ...string) []func(View, string) {
return getEventListeners[View, string](view, subviewID, TransitionCancelEvent)
return getOneArgEventListeners[View, string](view, subviewID, TransitionCancelEvent)
}
// GetAnimationStartListeners returns the "animation-start-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.
func GetAnimationStartListeners(view View, subviewID ...string) []func(View, string) {
return getEventListeners[View, string](view, subviewID, AnimationStartEvent)
return getOneArgEventListeners[View, string](view, subviewID, AnimationStartEvent)
}
// GetAnimationEndListeners returns the "animation-end-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.
func GetAnimationEndListeners(view View, subviewID ...string) []func(View, string) {
return getEventListeners[View, string](view, subviewID, AnimationEndEvent)
return getOneArgEventListeners[View, string](view, subviewID, AnimationEndEvent)
}
// GetAnimationCancelListeners returns the "animation-cancel-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.
func GetAnimationCancelListeners(view View, subviewID ...string) []func(View, string) {
return getEventListeners[View, string](view, subviewID, AnimationCancelEvent)
return getOneArgEventListeners[View, string](view, subviewID, AnimationCancelEvent)
}
// GetAnimationIterationListeners returns the "animation-iteration-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.
func GetAnimationIterationListeners(view View, subviewID ...string) []func(View, string) {
return getEventListeners[View, string](view, subviewID, AnimationIterationEvent)
return getOneArgEventListeners[View, string](view, subviewID, AnimationIterationEvent)
}

139
animationRun.go Normal file
View File

@ -0,0 +1,139 @@
package rui
func (animation *animationData) Start(view View, listener func(view View, animation AnimationProperty, event PropertyName)) bool {
if view == nil {
ErrorLog("nil View in animation.Start() function")
return false
}
if !animation.hasAnimatedProperty() {
return false
}
animation.view = view
animation.listener = listener
animation.oldAnimation = nil
if value := view.Get(Animation); value != nil {
if oldAnimation, ok := value.([]AnimationProperty); ok && len(oldAnimation) > 0 {
animation.oldAnimation = oldAnimation
}
}
animation.oldListeners = map[PropertyName][]func(View, PropertyName){}
setListeners := func(event PropertyName, listener func(View, PropertyName)) {
var listeners []func(View, PropertyName) = nil
if value := view.Get(event); value != nil {
if oldListeners, ok := value.([]func(View, PropertyName)); ok && len(oldListeners) > 0 {
listeners = oldListeners
}
}
if listeners == nil {
view.Set(event, listener)
} else {
animation.oldListeners[event] = listeners
view.Set(event, append(listeners, listener))
}
}
setListeners(AnimationStartEvent, animation.onAnimationStart)
setListeners(AnimationEndEvent, animation.onAnimationEnd)
setListeners(AnimationCancelEvent, animation.onAnimationCancel)
setListeners(AnimationIterationEvent, animation.onAnimationIteration)
view.Set(Animation, animation)
return true
}
func (animation *animationData) finish() {
if animation.view != nil {
for _, event := range []PropertyName{AnimationStartEvent, AnimationEndEvent, AnimationCancelEvent, AnimationIterationEvent} {
if listeners, ok := animation.oldListeners[event]; ok {
animation.view.Set(event, listeners)
} else {
animation.view.Remove(event)
}
}
if animation.oldAnimation != nil {
animation.view.Set(Animation, animation.oldAnimation)
animation.oldAnimation = nil
} else {
animation.view.Set(Animation, "")
}
animation.oldListeners = map[PropertyName][]func(View, PropertyName){}
animation.view = nil
animation.listener = nil
}
}
func (animation *animationData) Stop() {
animation.onAnimationCancel(animation.view, "")
}
func (animation *animationData) Pause() {
if animation.view != nil {
animation.view.Set(AnimationPaused, true)
}
}
func (animation *animationData) Resume() {
if animation.view != nil {
animation.view.Remove(AnimationPaused)
}
}
func (animation *animationData) onAnimationStart(view View, _ PropertyName) {
if animation.view != nil && animation.listener != nil {
animation.listener(animation.view, animation, AnimationStartEvent)
}
}
func (animation *animationData) onAnimationEnd(view View, _ PropertyName) {
if animation.view != nil {
animationView := animation.view
listener := animation.listener
if value, ok := animation.properties[PropertyTag]; ok {
if props, ok := value.([]AnimatedProperty); ok {
for _, prop := range props {
animationView.setRaw(prop.Tag, prop.To)
}
}
}
animation.finish()
if listener != nil {
listener(animationView, animation, AnimationEndEvent)
}
}
}
func (animation *animationData) onAnimationIteration(view View, _ PropertyName) {
if animation.view != nil && animation.listener != nil {
animation.listener(animation.view, animation, AnimationIterationEvent)
}
}
func (animation *animationData) onAnimationCancel(view View, _ PropertyName) {
if animation.view != nil {
animationView := animation.view
listener := animation.listener
if value, ok := animation.properties[PropertyTag]; ok {
if props, ok := value.([]AnimatedProperty); ok {
for _, prop := range props {
animationView.Set(prop.Tag, prop.To)
}
}
}
animation.finish()
if listener != nil {
listener(animationView, animation, AnimationCancelEvent)
}
}
}

View File

@ -415,6 +415,7 @@ func StartApp(addr string, createContentFunc func(Session) SessionContent, param
}
}
// FinishApp finishes application
func FinishApp() {
for _, app := range apps {
app.Finish()
@ -422,6 +423,8 @@ func FinishApp() {
apps = []*application{}
}
// OpenBrowser open browser with specific URL locally. Useful for applications which run on local machine
// or for debug purposes.
func OpenBrowser(url string) bool {
var err error

View File

@ -141,7 +141,7 @@ func (app *wasmApp) init(params AppParams) {
div := document.Call("createElement", "div")
div.Set("className", "ruiRoot")
div.Set("id", "ruiRootView")
viewHTML(app.session.RootView(), buffer)
viewHTML(app.session.RootView(), buffer, "")
div.Set("innerHTML", buffer.String())
body.Call("appendChild", div)

View File

@ -215,6 +215,21 @@ function appendToInnerHTML(elementId, content) {
}
}
function appendToInputValue(elementId, content) {
const element = document.getElementById(elementId);
if (element) {
element.value += content;
scanElementsSize();
}
}
function removeView(elementId) {
const element = document.getElementById(elementId);
if (element) {
element.remove()
}
}
function setDisabled(elementId, disabled) {
const element = document.getElementById(elementId);
if (element) {
@ -251,22 +266,22 @@ function activateTab(layoutId, tabNumber) {
if (element) {
const currentNumber = element.getAttribute("data-current");
if (currentNumber != tabNumber) {
function setTab(number, styleProperty, display) {
function setTab(number, styleProperty, visibility) {
const tab = document.getElementById(layoutId + '-' + number);
if (tab) {
tab.className = element.getAttribute(styleProperty);
const page = document.getElementById(tab.getAttribute("data-view"));
if (page) {
page.style.display = display;
page.style.visibility = visibility
}
return
}
const page = document.getElementById(layoutId + "-page" + number);
if (page) {
page.style.display = display;
page.style.visibility = visibility
}
}
setTab(currentNumber, "data-inactiveTabStyle", "none")
setTab(currentNumber, "data-inactiveTabStyle", "hidden")
setTab(tabNumber, "data-activeTabStyle", "");
element.setAttribute("data-current", tabNumber);
scanElementsSize()
@ -615,21 +630,11 @@ function listItemClickEvent(element, event) {
return
}
let selected = false;
if (element.classList) {
const focusStyle = getListFocusedItemStyle(element);
const blurStyle = getListSelectedItemStyle(element);
selected = (element.classList.contains(focusStyle) || element.classList.contains(blurStyle));
}
const list = element.parentNode.parentNode
if (list) {
if (!selected) {
selectListItem(list, element, true)
}
const message = "itemClick{session=" + sessionID + ",id=" + list.id + "}"
sendMessage(message);
const number = getListItemNumber(element.id)
selectListItem(list, element)
sendMessage("itemClick{session=" + sessionID + ",id=" + list.id + ",number=" + number + "}");
}
}
@ -656,7 +661,7 @@ function getListSelectedItemStyle(element) {
return getStyleAttribute(element, "data-bluritemstyle", "ruiListItemSelected");
}
function selectListItem(element, item, needSendMessage) {
function selectListItem(element, item) {
const currentId = element.getAttribute("data-current");
let message;
const focusStyle = getListFocusedItemStyle(element);
@ -668,9 +673,7 @@ function selectListItem(element, item, needSendMessage) {
if (current.classList) {
current.classList.remove(focusStyle, blurStyle);
}
if (sendMessage) {
message = "itemUnselected{session=" + sessionID + ",id=" + element.id + "}";
}
message = "itemUnselected{session=" + sessionID + ",id=" + element.id + "}";
}
}
@ -685,12 +688,10 @@ function selectListItem(element, item, needSendMessage) {
}
}
element.setAttribute("data-current", item.id);
if (sendMessage) {
const number = getListItemNumber(item.id)
if (number != undefined) {
message = "itemSelected{session=" + sessionID + ",id=" + element.id + ",number=" + number + "}";
}
element.setAttribute("data-current", item.id);
const number = getListItemNumber(item.id)
if (number != undefined) {
message = "itemSelected{session=" + sessionID + ",id=" + element.id + ",number=" + number + "}";
}
if (item.scrollIntoViewIfNeeded) {
@ -698,29 +699,9 @@ function selectListItem(element, item, needSendMessage) {
} else {
item.scrollIntoView({block: "nearest", inline: "nearest"});
}
/*
let left = item.offsetLeft - element.offsetLeft;
if (left < element.scrollLeft) {
element.scrollLeft = left;
}
let top = item.offsetTop - element.offsetTop;
if (top < element.scrollTop) {
element.scrollTop = top;
}
let right = left + item.offsetWidth;
if (right > element.scrollLeft + element.clientWidth) {
element.scrollLeft = right - element.clientWidth;
}
let bottom = top + item.offsetHeight
if (bottom > element.scrollTop + element.clientHeight) {
element.scrollTop = bottom - element.clientHeight;
}*/
}
if (needSendMessage && message != undefined) {
if (message != undefined) {
sendMessage(message);
}
scanElementsSize();
@ -849,7 +830,7 @@ function listViewKeyDownEvent(element, event) {
switch (key) {
case " ":
case "Enter":
const message = "itemClick{session=" + sessionID + ",id=" + element.id + "}";
const message = "itemClick{session=" + sessionID + ",id=" + element.id + ",number=" + getListItemNumber(currentId) + "}";
sendMessage(message);
break;
@ -889,7 +870,7 @@ function listViewKeyDownEvent(element, event) {
return;
}
if (item && item !== current) {
selectListItem(element, item, true);
selectListItem(element, item);
}
} else {
switch (key) {
@ -908,7 +889,7 @@ function listViewKeyDownEvent(element, event) {
if (item.getAttribute("data-disabled") == "1") {
continue;
}
selectListItem(element, item, true);
selectListItem(element, item);
return;
}
break;
@ -1982,7 +1963,15 @@ function getCanvasContext(elementId) {
function localStorageSet(key, value) {
try {
localStorage.setItem(key, value)
localStorage.setItem(key, value);
} catch (err) {
sendMessage("storageError{session=" + sessionID + ", error=`" + err + "`}")
}
}
function localStorageRemove(key) {
try {
localStorage.removeItem(key);
} catch (err) {
sendMessage("storageError{session=" + sessionID + ", error=`" + err + "`}")
}
@ -1990,7 +1979,7 @@ function localStorageSet(key, value) {
function localStorageClear() {
try {
localStorage.setItem(key, value)
localStorage.clear();
} catch (err) {
sendMessage("storageError{session=" + sessionID + ", error=`" + err + "`}")
}
@ -2128,3 +2117,11 @@ function setCssVar(tag, value) {
root.style.setProperty(tag, value);
}
}
function createPath2D(svg) {
if (svg) {
return new Path2D(svg);
} else {
return new Path2D();
}
}

View File

@ -79,7 +79,7 @@ ul:focus {
}
.ruiPopupLayer {
background-color: rgba(128,128,128,0.1);
/*background-color: rgba(128,128,128,0.1);*/
position: absolute;
top: 0px;
bottom: 0px;
@ -147,6 +147,11 @@ ul:focus {
.ruiListLayout {
display: flex;
overflow: auto;
}
.ruiColumnLayout {
overflow: auto;
}
.ruiStackLayout {
@ -181,6 +186,14 @@ ul:focus {
overflow: auto;
}
.hiddenMarker {
list-style: none;
}
.hiddenMarker::-webkit-details-marker {
display: none;
}
/*
@media (prefers-color-scheme: light) {
body {

View File

@ -14,10 +14,14 @@ var appStyles string
//go:embed defaultTheme.rui
var defaultThemeText string
// Application - app interface
// Application represent generic application interface, see also [Session]
type Application interface {
// Finish finishes the application
Finish()
// Params returns application parameters set by StartApp function
Params() AppParams
removeSession(id int)
}

View File

@ -1,5 +1,6 @@
package rui
// AudioPlayer is a type of a [View] which can play audio files
type AudioPlayer interface {
MediaPlayer
}
@ -12,13 +13,12 @@ type audioPlayerData struct {
func NewAudioPlayer(session Session, params Params) AudioPlayer {
view := new(audioPlayerData)
view.init(session)
view.tag = "AudioPlayer"
setInitParams(view, params)
return view
}
func newAudioPlayer(session Session) View {
return NewAudioPlayer(session, nil)
return new(audioPlayerData) // NewAudioPlayer(session, nil)
}
func (player *audioPlayerData) init(session Session) {
@ -26,10 +26,6 @@ func (player *audioPlayerData) init(session Session) {
player.tag = "AudioPlayer"
}
func (player *audioPlayerData) String() string {
return getViewString(player, nil)
}
func (player *audioPlayerData) htmlTag() string {
return "audio"
}

View File

@ -2,107 +2,65 @@ package rui
import (
"fmt"
"strings"
)
const (
// NoRepeat is value of the Repeat property of an background image:
// The image is not repeated (and hence the background image painting area
// will not necessarily be entirely covered). The position of the non-repeated
// background image is defined by the background-position CSS property.
NoRepeat = 0
// RepeatXY is value of the Repeat property of an background image:
// The image is repeated as much as needed to cover the whole background
// image painting area. The last image will be clipped if it doesn't fit.
RepeatXY = 1
// RepeatX is value of the Repeat property of an background image:
// The image is repeated horizontally as much as needed to cover
// the whole width background image painting area. The image is not repeated vertically.
// The last image will be clipped if it doesn't fit.
RepeatX = 2
// RepeatY is value of the Repeat property of an background image:
// The image is repeated vertically as much as needed to cover
// the whole height background image painting area. The image is not repeated horizontally.
// The last image will be clipped if it doesn't fit.
RepeatY = 3
// RepeatRound is value of the Repeat property of an background image:
// As the allowed space increases in size, the repeated images will stretch (leaving no gaps)
// until there is room (space left >= half of the image width) for another one to be added.
// When the next image is added, all of the current ones compress to allow room.
RepeatRound = 4
// RepeatSpace is value of the Repeat property of an background image:
// The image is repeated as much as possible without clipping. The first and last images
// are pinned to either side of the element, and whitespace is distributed evenly between the images.
RepeatSpace = 5
// BorderBox is the value of the following properties:
// - BackgroundClip - The background extends to the outside edge of the border (but underneath the border in z-ordering).
// - BackgroundOrigin - The background is positioned relative to the border box.
// - MaskClip - The painted content is clipped to the border box.
// - MaskOrigin - The mask is positioned relative to the border box.
BorderBox = 0
// ScrollAttachment is value of the Attachment property of an background image:
// The background is fixed relative to the element itself and does not scroll with its contents.
// (It is effectively attached to the element's border.)
ScrollAttachment = 0
// FixedAttachment is value of the Attachment property of an background image:
// The background is fixed relative to the viewport. Even if an element has
// a scrolling mechanism, the background doesn't move with the element.
FixedAttachment = 1
// LocalAttachment is value of the Attachment property of an background image:
// The background is fixed relative to the element's contents. If the element has a scrolling mechanism,
// the background scrolls with the element's contents, and the background painting area
// and background positioning area are relative to the scrollable area of the element
// rather than to the border framing them.
LocalAttachment = 2
// PaddingBox is value of the BackgroundClip and MaskClip property:
// - BackgroundClip - The background extends to the outside edge of the padding. No background is drawn beneath the border.
// - BackgroundOrigin - The background is positioned relative to the padding box.
// - MaskClip - The painted content is clipped to the padding box.
// - MaskOrigin - The mask is positioned relative to the padding box.
PaddingBox = 1
// BorderBoxClip is value of the BackgroundClip property:
// The background extends to the outside edge of the border (but underneath the border in z-ordering).
BorderBoxClip = 0
// PaddingBoxClip is value of the BackgroundClip property:
// The background extends to the outside edge of the padding. No background is drawn beneath the border.
PaddingBoxClip = 1
// ContentBoxClip is value of the BackgroundClip property:
// The background is painted within (clipped to) the content box.
ContentBoxClip = 2
// ContentBox is value of the BackgroundClip and MaskClip property:
// - BackgroundClip - The background is painted within (clipped to) the content box.
// - BackgroundOrigin - The background is positioned relative to the content box.
// - MaskClip - The painted content is clipped to the content box.
// - MaskOrigin - The mask is positioned relative to the content box.
ContentBox = 2
)
// BackgroundElement describes the background element.
// BackgroundElement describes the background element
type BackgroundElement interface {
Properties
fmt.Stringer
stringWriter
cssStyle(session Session) string
// Tag returns type of the background element.
// Possible values are: "image", "conic-gradient", "linear-gradient" and "radial-gradient"
Tag() string
// Clone creates a new copy of BackgroundElement
Clone() BackgroundElement
}
type backgroundElement struct {
propertyList
dataProperty
}
type backgroundImage struct {
backgroundElement
}
// NewBackgroundImage creates the new background image
func createBackground(obj DataObject) BackgroundElement {
var result BackgroundElement = nil
switch obj.Tag() {
case "image":
image := new(backgroundImage)
image.properties = map[string]any{}
result = image
result = NewBackgroundImage(nil)
case "linear-gradient":
gradient := new(backgroundLinearGradient)
gradient.properties = map[string]any{}
result = gradient
result = NewBackgroundLinearGradient(nil)
case "radial-gradient":
gradient := new(backgroundRadialGradient)
gradient.properties = map[string]any{}
result = gradient
result = NewBackgroundRadialGradient(nil)
case "conic-gradient":
gradient := new(backgroundConicGradient)
gradient.properties = map[string]any{}
result = gradient
result = NewBackgroundConicGradient(nil)
default:
return nil
@ -112,7 +70,7 @@ func createBackground(obj DataObject) BackgroundElement {
for i := 0; i < count; i++ {
if node := obj.Property(i); node.Type() == TextNode {
if value := node.Text(); value != "" {
result.Set(node.Tag(), value)
result.Set(PropertyName(node.Tag()), value)
}
}
}
@ -120,144 +78,240 @@ func createBackground(obj DataObject) BackgroundElement {
return result
}
// NewBackgroundImage creates the new background image
func NewBackgroundImage(params Params) BackgroundElement {
result := new(backgroundImage)
result.properties = map[string]any{}
for tag, value := range params {
result.Set(tag, value)
}
return result
}
func parseBackgroundValue(value any) []BackgroundElement {
func (image *backgroundImage) Tag() string {
return "image"
}
switch value := value.(type) {
case BackgroundElement:
return []BackgroundElement{value}
func (image *backgroundImage) Clone() BackgroundElement {
result := NewBackgroundImage(nil)
for tag, value := range image.properties {
result.setRaw(tag, value)
}
return result
}
case []BackgroundElement:
return value
func (image *backgroundImage) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag {
case "source":
tag = Source
case []DataValue:
background := []BackgroundElement{}
for _, el := range value {
if el.IsObject() {
if element := createBackground(el.Object()); element != nil {
background = append(background, element)
} else {
return nil
}
} else if obj := ParseDataText(el.Value()); obj != nil {
if element := createBackground(obj); element != nil {
background = append(background, element)
} else {
return nil
}
} else {
return nil
}
}
return background
case Fit:
tag = backgroundFit
case HorizontalAlign:
tag = ImageHorizontalAlign
case VerticalAlign:
tag = ImageVerticalAlign
}
return tag
}
func (image *backgroundImage) Set(tag string, value any) bool {
tag = image.normalizeTag(tag)
switch tag {
case Attachment, Width, Height, Repeat, ImageHorizontalAlign, ImageVerticalAlign,
backgroundFit, Source:
return image.backgroundElement.Set(tag, value)
}
return false
}
func (image *backgroundImage) Get(tag string) any {
return image.backgroundElement.Get(image.normalizeTag(tag))
}
func (image *backgroundImage) cssStyle(session Session) string {
if src, ok := imageProperty(image, Source, session); ok && src != "" {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(`url(`)
buffer.WriteString(src)
buffer.WriteRune(')')
attachment, _ := enumProperty(image, Attachment, session, NoRepeat)
values := enumProperties[Attachment].values
if attachment > 0 && attachment < len(values) {
buffer.WriteRune(' ')
buffer.WriteString(values[attachment])
case DataObject:
if element := createBackground(value); element != nil {
return []BackgroundElement{element}
}
align, _ := enumProperty(image, ImageHorizontalAlign, session, LeftAlign)
values = enumProperties[ImageHorizontalAlign].values
if align >= 0 && align < len(values) {
buffer.WriteRune(' ')
buffer.WriteString(values[align])
} else {
buffer.WriteString(` left`)
case []DataObject:
background := []BackgroundElement{}
for _, obj := range value {
if element := createBackground(obj); element != nil {
background = append(background, element)
} else {
return nil
}
}
return background
align, _ = enumProperty(image, ImageVerticalAlign, session, TopAlign)
values = enumProperties[ImageVerticalAlign].values
if align >= 0 && align < len(values) {
buffer.WriteRune(' ')
buffer.WriteString(values[align])
} else {
buffer.WriteString(` top`)
}
fit, _ := enumProperty(image, backgroundFit, session, NoneFit)
values = enumProperties[backgroundFit].values
if fit > 0 && fit < len(values) {
buffer.WriteString(` / `)
buffer.WriteString(values[fit])
} else {
width, _ := sizeProperty(image, Width, session)
height, _ := sizeProperty(image, Height, session)
if width.Type != Auto || height.Type != Auto {
buffer.WriteString(` / `)
buffer.WriteString(width.cssString("auto", session))
buffer.WriteRune(' ')
buffer.WriteString(height.cssString("auto", session))
case string:
if obj := ParseDataText(value); obj != nil {
if element := createBackground(obj); element != nil {
return []BackgroundElement{element}
}
}
repeat, _ := enumProperty(image, Repeat, session, NoRepeat)
values = enumProperties[Repeat].values
if repeat >= 0 && repeat < len(values) {
buffer.WriteRune(' ')
buffer.WriteString(values[repeat])
} else {
buffer.WriteString(` no-repeat`)
case []string:
elements := make([]BackgroundElement, 0, len(value))
for _, element := range value {
if obj := ParseDataText(element); obj != nil {
if element := createBackground(obj); element != nil {
elements = append(elements, element)
} else {
return nil
}
} else {
return nil
}
}
return elements
case []any:
elements := make([]BackgroundElement, 0, len(value))
for _, element := range value {
switch element := element.(type) {
case BackgroundElement:
elements = append(elements, element)
case string:
if obj := ParseDataText(element); obj != nil {
if element := createBackground(obj); element != nil {
elements = append(elements, element)
} else {
return nil
}
} else {
return nil
}
default:
return nil
}
}
return elements
return buffer.String()
}
return nil
}
func setBackgroundProperty(properties Properties, tag PropertyName, value any) []PropertyName {
background := parseBackgroundValue(value)
if background == nil {
notCompatibleType(tag, value)
return nil
}
if len(background) > 0 {
properties.setRaw(tag, background)
} else if properties.getRaw(tag) != nil {
properties.setRaw(tag, nil)
} else {
return []PropertyName{}
}
return []PropertyName{tag}
}
func backgroundCSS(properties Properties, session Session) string {
if value := properties.getRaw(Background); value != nil {
if backgrounds, ok := value.([]BackgroundElement); ok && len(backgrounds) > 0 {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for _, background := range backgrounds {
if value := background.cssStyle(session); value != "" {
if buffer.Len() > 0 {
buffer.WriteString(", ")
}
buffer.WriteString(value)
}
}
if buffer.Len() > 0 {
backgroundColor, _ := colorProperty(properties, BackgroundColor, session)
if backgroundColor != 0 {
buffer.WriteRune(' ')
buffer.WriteString(backgroundColor.cssString())
}
return buffer.String()
}
}
}
return ""
}
func (image *backgroundImage) writeString(buffer *strings.Builder, indent string) {
image.writeToBuffer(buffer, indent, image.Tag(), []string{
Source,
Width,
Height,
ImageHorizontalAlign,
ImageVerticalAlign,
backgroundFit,
Repeat,
Attachment,
})
func maskCSS(properties Properties, session Session) string {
if value := properties.getRaw(Mask); value != nil {
if backgrounds, ok := value.([]BackgroundElement); ok && len(backgrounds) > 0 {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for _, background := range backgrounds {
if value := background.cssStyle(session); value != "" {
if buffer.Len() > 0 {
buffer.WriteString(", ")
}
buffer.WriteString(value)
}
}
return buffer.String()
}
}
return ""
}
func (image *backgroundImage) String() string {
return runStringWriter(image)
func backgroundStyledPropery(view View, subviewID []string, tag PropertyName) []BackgroundElement {
var background []BackgroundElement = nil
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(tag); value != nil {
if backgrounds, ok := value.([]BackgroundElement); ok {
background = backgrounds
}
} else if value := valueFromStyle(view, tag); value != nil {
background = parseBackgroundValue(value)
}
}
if count := len(background); count > 0 {
result := make([]BackgroundElement, count)
copy(result, background)
return result
}
return []BackgroundElement{}
}
// GetBackground returns the view background.
//
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetBackground(view View, subviewID ...string) []BackgroundElement {
return backgroundStyledPropery(view, subviewID, Background)
}
// GetMask returns the view mask.
//
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetMask(view View, subviewID ...string) []BackgroundElement {
return backgroundStyledPropery(view, subviewID, Mask)
}
// GetBackgroundClip returns a "background-clip" of the subview. Returns one of next values:
//
// BorderBox (0), PaddingBox (1), ContentBox (2)
//
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetBackgroundClip(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, BackgroundClip, 0, false)
}
// GetBackgroundOrigin returns a "background-origin" of the subview. Returns one of next values:
//
// BorderBox (0), PaddingBox (1), ContentBox (2)
//
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetBackgroundOrigin(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, BackgroundOrigin, 0, false)
}
// GetMaskClip returns a "mask-clip" of the subview. Returns one of next values:
//
// BorderBox (0), PaddingBox (1), ContentBox (2)
//
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetMaskClip(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, MaskClip, 0, false)
}
// GetMaskOrigin returns a "mask-origin" of the subview. Returns one of next values:
//
// BorderBox (0), PaddingBox (1), ContentBox (2)
//
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetMaskOrigin(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, MaskOrigin, 0, false)
}

View File

@ -19,15 +19,23 @@ type BackgroundGradientAngle struct {
}
// NewBackgroundConicGradient creates the new background conic gradient
//
// The following properties can be used:
// - "gradient" [Gradient] - Describes gradient stop points. This is a mandatory property while describing background gradients.
// - "center-x" [CenterX] - center X point of the gradient.
// - "center-y" [CenterY] - center Y point of the gradient.
// - "from" [From] - start angle position of the gradient.
// - "repeating" [Repeating] - Defines whether stop points needs to be repeated after the last one.
func NewBackgroundConicGradient(params Params) BackgroundElement {
result := new(backgroundConicGradient)
result.properties = map[string]any{}
result.init()
for tag, value := range params {
result.Set(tag, value)
}
return result
}
// String convert internal representation of [BackgroundGradientAngle] into a string.
func (point *BackgroundGradientAngle) String() string {
result := "black"
if point.Color != nil {
@ -47,7 +55,6 @@ func (point *BackgroundGradientAngle) String() string {
case AngleUnit:
result += " " + value.String()
}
}
@ -72,6 +79,11 @@ func (point *BackgroundGradientAngle) color(session Session) (Color, bool) {
case Color:
return color, true
default:
if n, ok := isInt(color); ok {
return Color(n), true
}
}
}
return 0, false
@ -114,6 +126,15 @@ func (point *BackgroundGradientAngle) cssString(session Session, buffer *strings
}
}
func (gradient *backgroundConicGradient) init() {
gradient.backgroundElement.init()
gradient.normalize = normalizeConicGradientTag
gradient.set = backgroundConicGradientSet
gradient.supportedProperties = []PropertyName{
CenterX, CenterY, Repeating, From, Gradient,
}
}
func (gradient *backgroundConicGradient) Tag() string {
return "conic-gradient"
}
@ -126,8 +147,8 @@ func (image *backgroundConicGradient) Clone() BackgroundElement {
return result
}
func (gradient *backgroundConicGradient) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeConicGradientTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case "x-center":
tag = CenterX
@ -139,18 +160,50 @@ func (gradient *backgroundConicGradient) normalizeTag(tag string) string {
return tag
}
func (gradient *backgroundConicGradient) Set(tag string, value any) bool {
tag = gradient.normalizeTag(tag)
func backgroundConicGradientSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case CenterX, CenterY, Repeating, From:
return gradient.propertyList.Set(tag, value)
case Gradient:
return gradient.setGradient(value)
switch value := value.(type) {
case string:
if value == "" {
return propertiesRemove(properties, tag)
}
if strings.Contains(value, ",") || strings.Contains(value, " ") {
if vector := parseGradientText(value); vector != nil {
properties.setRaw(Gradient, vector)
return []PropertyName{tag}
}
} else if isConstantName(value) {
properties.setRaw(Gradient, value)
return []PropertyName{tag}
}
ErrorLogF(`Invalid conic gradient: "%s"`, value)
case []BackgroundGradientAngle:
count := len(value)
if count < 2 {
ErrorLog("The gradient must contain at least 2 points")
return nil
}
for i, point := range value {
if point.Color == nil {
ErrorLogF("Invalid %d element of the conic gradient: Color is nil", i)
return nil
}
}
properties.setRaw(Gradient, value)
return []PropertyName{tag}
default:
notCompatibleType(tag, value)
}
return nil
}
ErrorLogF(`"%s" property is not supported by BackgroundConicGradient`, tag)
return false
return propertiesSet(properties, tag, value)
}
func (gradient *backgroundConicGradient) stringToAngle(text string) (any, bool) {
@ -215,57 +268,6 @@ func (gradient *backgroundConicGradient) parseGradientText(value string) []Backg
}
return vector
}
func (gradient *backgroundConicGradient) setGradient(value any) bool {
if value == nil {
delete(gradient.properties, Gradient)
return true
}
switch value := value.(type) {
case string:
if value == "" {
delete(gradient.properties, Gradient)
return true
}
if strings.Contains(value, ",") || strings.Contains(value, " ") {
if vector := gradient.parseGradientText(value); vector != nil {
gradient.properties[Gradient] = vector
return true
}
return false
} else if value[0] == '@' {
gradient.properties[Gradient] = value
return true
}
ErrorLogF(`Invalid conic gradient: "%s"`, value)
return false
case []BackgroundGradientAngle:
count := len(value)
if count < 2 {
ErrorLog("The gradient must contain at least 2 points")
return false
}
for i, point := range value {
if point.Color == nil {
ErrorLogF("Invalid %d element of the conic gradient: Color is nil", i)
return false
}
}
gradient.properties[Gradient] = value
return true
}
return false
}
func (gradient *backgroundConicGradient) Get(tag string) any {
return gradient.backgroundElement.Get(gradient.normalizeTag(tag))
}
func (gradient *backgroundConicGradient) cssStyle(session Session) string {
points := []BackgroundGradientAngle{}
@ -338,7 +340,7 @@ func (gradient *backgroundConicGradient) cssStyle(session Session) string {
}
func (gradient *backgroundConicGradient) writeString(buffer *strings.Builder, indent string) {
gradient.writeToBuffer(buffer, indent, gradient.Tag(), []string{
gradient.writeToBuffer(buffer, indent, gradient.Tag(), []PropertyName{
Gradient,
CenterX,
CenterY,

View File

@ -1,665 +0,0 @@
package rui
import "strings"
const (
// ToTopGradient is value of the Direction property of a linear gradient. The value is equivalent to the 0deg angle
ToTopGradient = 0
// ToRightTopGradient is value of the Direction property of a linear gradient.
ToRightTopGradient = 1
// ToRightGradient is value of the Direction property of a linear gradient. The value is equivalent to the 90deg angle
ToRightGradient = 2
// ToRightBottomGradient is value of the Direction property of a linear gradient.
ToRightBottomGradient = 3
// ToBottomGradient is value of the Direction property of a linear gradient. The value is equivalent to the 180deg angle
ToBottomGradient = 4
// ToLeftBottomGradient is value of the Direction property of a linear gradient.
ToLeftBottomGradient = 5
// ToLeftGradient is value of the Direction property of a linear gradient. The value is equivalent to the 270deg angle
ToLeftGradient = 6
// ToLeftTopGradient is value of the Direction property of a linear gradient.
ToLeftTopGradient = 7
// EllipseGradient is value of the Shape property of a radial gradient background:
// the shape is an axis-aligned ellipse
EllipseGradient = 0
// CircleGradient is value of the Shape property of a radial gradient background:
// the gradient's shape is a circle with constant radius
CircleGradient = 1
// ClosestSideGradient is value of the Radius property of a radial gradient background:
// The gradient's ending shape meets the side of the box closest to its center (for circles)
// or meets both the vertical and horizontal sides closest to the center (for ellipses).
ClosestSideGradient = 0
// ClosestCornerGradient is value of the Radius property of a radial gradient background:
// The gradient's ending shape is sized so that it exactly meets the closest corner
// of the box from its center.
ClosestCornerGradient = 1
// FarthestSideGradient is value of the Radius property of a radial gradient background:
// Similar to closest-side, except the ending shape is sized to meet the side of the box
// farthest from its center (or vertical and horizontal sides).
FarthestSideGradient = 2
// FarthestCornerGradient is value of the Radius property of a radial gradient background:
// The default value, the gradient's ending shape is sized so that it exactly meets
// the farthest corner of the box from its center.
FarthestCornerGradient = 3
)
// BackgroundGradientPoint define point on gradient straight line
type BackgroundGradientPoint struct {
// Color - the color of the point. Must not be nil.
// Can take a value of Color type or string (color constant or textual description of the color)
Color any
// Pos - the distance from the start of the gradient straight line. Optional (may be nil).
// Can take a value of SizeUnit type or string (angle constant or textual description of the SizeUnit)
Pos any
}
type backgroundGradient struct {
backgroundElement
}
type backgroundLinearGradient struct {
backgroundGradient
}
type backgroundRadialGradient struct {
backgroundGradient
}
// NewBackgroundLinearGradient creates the new background linear gradient
func NewBackgroundLinearGradient(params Params) BackgroundElement {
result := new(backgroundLinearGradient)
result.properties = map[string]any{}
for tag, value := range params {
result.Set(tag, value)
}
return result
}
// NewBackgroundRadialGradient creates the new background radial gradient
func NewBackgroundRadialGradient(params Params) BackgroundElement {
result := new(backgroundRadialGradient)
result.properties = map[string]any{}
for tag, value := range params {
result.Set(tag, value)
}
return result
}
func (gradient *backgroundGradient) parseGradientText(value string) []BackgroundGradientPoint {
elements := strings.Split(value, ",")
count := len(elements)
if count < 2 {
ErrorLog("The gradient must contain at least 2 points")
return nil
}
points := make([]BackgroundGradientPoint, count)
for i, element := range elements {
if !points[i].setValue(element) {
ErrorLogF(`Invalid %d element of the conic gradient: "%s"`, i, element)
return nil
}
}
return points
}
func (gradient *backgroundGradient) Set(tag string, value any) bool {
switch tag = strings.ToLower(tag); tag {
case Repeating:
return gradient.setBoolProperty(tag, value)
case Gradient:
switch value := value.(type) {
case string:
if value != "" {
if strings.Contains(value, " ") || strings.Contains(value, ",") {
if points := gradient.parseGradientText(value); len(points) >= 2 {
gradient.properties[Gradient] = points
return true
}
} else if value[0] == '@' {
gradient.properties[Gradient] = value
return true
}
}
case []BackgroundGradientPoint:
if len(value) >= 2 {
gradient.properties[Gradient] = value
return true
}
case []Color:
count := len(value)
if count >= 2 {
points := make([]BackgroundGradientPoint, count)
for i, color := range value {
points[i].Color = color
}
gradient.properties[Gradient] = points
return true
}
case []GradientPoint:
count := len(value)
if count >= 2 {
points := make([]BackgroundGradientPoint, count)
for i, point := range value {
points[i].Color = point.Color
points[i].Pos = Percent(point.Offset * 100)
}
gradient.properties[Gradient] = points
return true
}
}
ErrorLogF("Invalid gradient %v", value)
return false
}
ErrorLogF("Property %s is not supported by a background gradient", tag)
return false
}
func (point *BackgroundGradientPoint) setValue(text string) bool {
text = strings.Trim(text, " ")
colorText := text
pointText := ""
if index := strings.Index(text, " "); index > 0 {
colorText = text[:index]
pointText = strings.Trim(text[index+1:], " ")
}
if colorText == "" {
return false
}
if colorText[0] == '@' {
point.Color = colorText
} else if color, ok := StringToColor(colorText); ok {
point.Color = color
} else {
return false
}
if pointText == "" {
point.Pos = nil
} else if pointText[0] == '@' {
point.Pos = pointText
} else if pos, ok := StringToSizeUnit(pointText); ok {
point.Pos = pos
} else {
return false
}
return true
}
func (point *BackgroundGradientPoint) color(session Session) (Color, bool) {
if point.Color != nil {
switch color := point.Color.(type) {
case string:
if color != "" {
if color[0] == '@' {
if clr, ok := session.Color(color[1:]); ok {
return clr, true
}
} else {
if clr, ok := StringToColor(color); ok {
return clr, true
}
}
}
case Color:
return color, true
}
}
return 0, false
}
func (point *BackgroundGradientPoint) String() string {
result := "black"
if point.Color != nil {
switch color := point.Color.(type) {
case string:
result = color
case Color:
result = color.String()
}
}
if point.Pos != nil {
switch value := point.Pos.(type) {
case string:
result += " " + value
case SizeUnit:
if value.Type != Auto {
result += " " + value.String()
}
}
}
return result
}
func (gradient *backgroundGradient) writeGradient(session Session, buffer *strings.Builder) bool {
value, ok := gradient.properties[Gradient]
if !ok {
return false
}
var points []BackgroundGradientPoint = nil
switch value := value.(type) {
case string:
if value != "" && value[0] == '@' {
if text, ok := session.Constant(value[1:]); ok {
points = gradient.parseGradientText(text)
}
}
case []BackgroundGradientPoint:
points = value
}
if len(points) > 0 {
for i, point := range points {
if i > 0 {
buffer.WriteString(`, `)
}
if color, ok := point.color(session); ok {
buffer.WriteString(color.cssString())
} else {
return false
}
if point.Pos != nil {
switch value := point.Pos.(type) {
case string:
if value != "" {
if value, ok := session.resolveConstants(value); ok {
if pos, ok := StringToSizeUnit(value); ok && pos.Type != Auto {
buffer.WriteRune(' ')
buffer.WriteString(pos.cssString("", session))
}
}
}
case SizeUnit:
if value.Type != Auto {
buffer.WriteRune(' ')
buffer.WriteString(value.cssString("", session))
}
}
}
}
return true
}
return false
}
func (gradient *backgroundLinearGradient) Tag() string {
return "linear-gradient"
}
func (image *backgroundLinearGradient) Clone() BackgroundElement {
result := NewBackgroundLinearGradient(nil)
for tag, value := range image.properties {
result.setRaw(tag, value)
}
return result
}
func (gradient *backgroundLinearGradient) Set(tag string, value any) bool {
if strings.ToLower(tag) == Direction {
switch value := value.(type) {
case AngleUnit:
gradient.properties[Direction] = value
return true
case string:
if gradient.setSimpleProperty(tag, value) {
return true
}
if angle, ok := StringToAngleUnit(value); ok {
gradient.properties[Direction] = angle
return true
}
}
return gradient.setEnumProperty(tag, value, enumProperties[Direction].values)
}
return gradient.backgroundGradient.Set(tag, value)
}
func (gradient *backgroundLinearGradient) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if repeating, _ := boolProperty(gradient, Repeating, session); repeating {
buffer.WriteString(`repeating-linear-gradient(`)
} else {
buffer.WriteString(`linear-gradient(`)
}
if value, ok := gradient.properties[Direction]; ok {
switch value := value.(type) {
case string:
if text, ok := session.resolveConstants(value); ok {
direction := enumProperties[Direction]
if n, ok := enumStringToInt(text, direction.values, false); ok {
buffer.WriteString(direction.cssValues[n])
buffer.WriteString(", ")
} else {
if angle, ok := StringToAngleUnit(text); ok {
buffer.WriteString(angle.cssString())
buffer.WriteString(", ")
} else {
ErrorLog(`Invalid linear gradient direction: ` + text)
}
}
} else {
ErrorLog(`Invalid linear gradient direction: ` + value)
}
case int:
values := enumProperties[Direction].cssValues
if value >= 0 && value < len(values) {
buffer.WriteString(values[value])
buffer.WriteString(", ")
} else {
ErrorLogF(`Invalid linear gradient direction: %d`, value)
}
case AngleUnit:
buffer.WriteString(value.cssString())
buffer.WriteString(", ")
}
}
if !gradient.writeGradient(session, buffer) {
return ""
}
buffer.WriteString(") ")
return buffer.String()
}
func (gradient *backgroundLinearGradient) writeString(buffer *strings.Builder, indent string) {
gradient.writeToBuffer(buffer, indent, gradient.Tag(), []string{
Gradient,
Repeating,
Direction,
})
}
func (gradient *backgroundLinearGradient) String() string {
return runStringWriter(gradient)
}
func (gradient *backgroundRadialGradient) Tag() string {
return "radial-gradient"
}
func (image *backgroundRadialGradient) Clone() BackgroundElement {
result := NewBackgroundRadialGradient(nil)
for tag, value := range image.properties {
result.setRaw(tag, value)
}
return result
}
func (gradient *backgroundRadialGradient) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag {
case Radius:
tag = RadialGradientRadius
case Shape:
tag = RadialGradientShape
case "x-center":
tag = CenterX
case "y-center":
tag = CenterY
}
return tag
}
func (gradient *backgroundRadialGradient) Set(tag string, value any) bool {
tag = gradient.normalizeTag(tag)
switch tag {
case RadialGradientRadius:
switch value := value.(type) {
case []SizeUnit:
switch len(value) {
case 0:
delete(gradient.properties, RadialGradientRadius)
return true
case 1:
if value[0].Type == Auto {
delete(gradient.properties, RadialGradientRadius)
} else {
gradient.properties[RadialGradientRadius] = value[0]
}
return true
default:
gradient.properties[RadialGradientRadius] = value
return true
}
case []any:
switch len(value) {
case 0:
delete(gradient.properties, RadialGradientRadius)
return true
case 1:
return gradient.Set(RadialGradientRadius, value[0])
default:
gradient.properties[RadialGradientRadius] = value
return true
}
case string:
if gradient.setSimpleProperty(RadialGradientRadius, value) {
return true
}
if size, err := stringToSizeUnit(value); err == nil {
if size.Type == Auto {
delete(gradient.properties, RadialGradientRadius)
} else {
gradient.properties[RadialGradientRadius] = size
}
return true
}
return gradient.setEnumProperty(RadialGradientRadius, value, enumProperties[RadialGradientRadius].values)
case SizeUnit:
if value.Type == Auto {
delete(gradient.properties, RadialGradientRadius)
} else {
gradient.properties[RadialGradientRadius] = value
}
return true
case int:
n := value
if n >= 0 && n < len(enumProperties[RadialGradientRadius].values) {
return gradient.propertyList.Set(RadialGradientRadius, value)
}
}
ErrorLogF(`Invalid value of "%s" property: %v`, tag, value)
case RadialGradientShape, CenterX, CenterY:
return gradient.propertyList.Set(tag, value)
}
return gradient.backgroundGradient.Set(tag, value)
}
func (gradient *backgroundRadialGradient) Get(tag string) any {
return gradient.backgroundGradient.Get(gradient.normalizeTag(tag))
}
func (gradient *backgroundRadialGradient) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if repeating, _ := boolProperty(gradient, Repeating, session); repeating {
buffer.WriteString(`repeating-radial-gradient(`)
} else {
buffer.WriteString(`radial-gradient(`)
}
var shapeText string
if shape, ok := enumProperty(gradient, RadialGradientShape, session, EllipseGradient); ok && shape == CircleGradient {
shapeText = `circle `
} else {
shapeText = `ellipse `
}
if value, ok := gradient.properties[RadialGradientRadius]; ok {
switch value := value.(type) {
case string:
if text, ok := session.resolveConstants(value); ok {
values := enumProperties[RadialGradientRadius]
if n, ok := enumStringToInt(text, values.values, false); ok {
buffer.WriteString(shapeText)
shapeText = ""
buffer.WriteString(values.cssValues[n])
buffer.WriteString(" ")
} else {
if r, ok := StringToSizeUnit(text); ok && r.Type != Auto {
buffer.WriteString("ellipse ")
shapeText = ""
buffer.WriteString(r.cssString("", session))
buffer.WriteString(" ")
buffer.WriteString(r.cssString("", session))
buffer.WriteString(" ")
} else {
ErrorLog(`Invalid radial gradient radius: ` + text)
}
}
} else {
ErrorLog(`Invalid radial gradient radius: ` + value)
}
case int:
values := enumProperties[RadialGradientRadius].cssValues
if value >= 0 && value < len(values) {
buffer.WriteString(shapeText)
shapeText = ""
buffer.WriteString(values[value])
buffer.WriteString(" ")
} else {
ErrorLogF(`Invalid radial gradient radius: %d`, value)
}
case SizeUnit:
if value.Type != Auto {
buffer.WriteString("ellipse ")
shapeText = ""
buffer.WriteString(value.cssString("", session))
buffer.WriteString(" ")
buffer.WriteString(value.cssString("", session))
buffer.WriteString(" ")
}
case []SizeUnit:
count := len(value)
if count > 2 {
count = 2
}
buffer.WriteString("ellipse ")
shapeText = ""
for i := 0; i < count; i++ {
buffer.WriteString(value[i].cssString("50%", session))
buffer.WriteString(" ")
}
case []any:
count := len(value)
if count > 2 {
count = 2
}
buffer.WriteString("ellipse ")
shapeText = ""
for i := 0; i < count; i++ {
if value[i] != nil {
switch value := value[i].(type) {
case SizeUnit:
buffer.WriteString(value.cssString("50%", session))
buffer.WriteString(" ")
case string:
if text, ok := session.resolveConstants(value); ok {
if size, err := stringToSizeUnit(text); err == nil {
buffer.WriteString(size.cssString("50%", session))
buffer.WriteString(" ")
} else {
buffer.WriteString("50% ")
}
} else {
buffer.WriteString("50% ")
}
}
} else {
buffer.WriteString("50% ")
}
}
}
}
x, _ := sizeProperty(gradient, CenterX, session)
y, _ := sizeProperty(gradient, CenterX, session)
if x.Type != Auto || y.Type != Auto {
if shapeText != "" {
buffer.WriteString(shapeText)
}
buffer.WriteString("at ")
buffer.WriteString(x.cssString("50%", session))
buffer.WriteString(" ")
buffer.WriteString(y.cssString("50%", session))
}
buffer.WriteString(", ")
if !gradient.writeGradient(session, buffer) {
return ""
}
buffer.WriteString(") ")
return buffer.String()
}
func (gradient *backgroundRadialGradient) writeString(buffer *strings.Builder, indent string) {
gradient.writeToBuffer(buffer, indent, gradient.Tag(), []string{
Gradient,
CenterX,
CenterY,
Repeating,
RadialGradientShape,
RadialGradientRadius,
})
}
func (gradient *backgroundRadialGradient) String() string {
return runStringWriter(gradient)
}

217
backgroundImage.go Normal file
View File

@ -0,0 +1,217 @@
package rui
import (
"strings"
)
// Constants related to view's background description
const (
// NoRepeat is value of the Repeat property of an background image:
//
// The image is not repeated (and hence the background image painting area
// will not necessarily be entirely covered). The position of the non-repeated
// background image is defined by the background-position CSS property.
NoRepeat = 0
// RepeatXY is value of the Repeat property of an background image:
//
// The image is repeated as much as needed to cover the whole background
// image painting area. The last image will be clipped if it doesn't fit.
RepeatXY = 1
// RepeatX is value of the Repeat property of an background image:
//
// The image is repeated horizontally as much as needed to cover
// the whole width background image painting area. The image is not repeated vertically.
// The last image will be clipped if it doesn't fit.
RepeatX = 2
// RepeatY is value of the Repeat property of an background image:
//
// The image is repeated vertically as much as needed to cover
// the whole height background image painting area. The image is not repeated horizontally.
// The last image will be clipped if it doesn't fit.
RepeatY = 3
// RepeatRound is value of the Repeat property of an background image:
//
// As the allowed space increases in size, the repeated images will stretch (leaving no gaps)
// until there is room (space left >= half of the image width) for another one to be added.
// When the next image is added, all of the current ones compress to allow room.
RepeatRound = 4
// RepeatSpace is value of the Repeat property of an background image:
//
// The image is repeated as much as possible without clipping. The first and last images
// are pinned to either side of the element, and whitespace is distributed evenly between the images.
RepeatSpace = 5
// ScrollAttachment is value of the Attachment property of an background image:
//
// The background is fixed relative to the element itself and does not scroll with its contents.
// (It is effectively attached to the element's border.)
ScrollAttachment = 0
// FixedAttachment is value of the Attachment property of an background image:
//
// The background is fixed relative to the viewport. Even if an element has
// a scrolling mechanism, the background doesn't move with the element.
FixedAttachment = 1
// LocalAttachment is value of the Attachment property of an background image:
//
// The background is fixed relative to the element's contents. If the element has a scrolling mechanism,
// the background scrolls with the element's contents, and the background painting area
// and background positioning area are relative to the scrollable area of the element
// rather than to the border framing them.
LocalAttachment = 2
)
type backgroundImage struct {
backgroundElement
}
// NewBackgroundImage creates the new background image
//
// The following properties can be used:
// - "src" [Source] - the name of the image in the "images" folder of the resources, or the URL of the image or inline-image.
// - "width" [Width] - the width of the image.
// - "height" [Height] - the height of the image.
// - "image-horizontal-align" [ImageHorizontalAlign] - the horizontal alignment of the image relative to view's bounds.
// - "image-vertical-align" [ImageVerticalAlign] - the vertical alignment of the image relative to view's bounds.
// - "repeat" [Repeat] - the repetition of the image.
// - "fit" [Fit] - the image scaling parameters.
// - "attachment" [Attachment] - defines whether a background image's position is fixed within the viewport or scrolls with its containing block.
func NewBackgroundImage(params Params) BackgroundElement {
result := new(backgroundImage)
result.init()
for tag, value := range params {
result.Set(tag, value)
}
return result
}
func (image *backgroundImage) init() {
image.backgroundElement.init()
image.normalize = normalizeBackgroundImageTag
image.supportedProperties = []PropertyName{
Attachment, Width, Height, Repeat, ImageHorizontalAlign, ImageVerticalAlign, backgroundFit, Source,
}
}
func (image *backgroundImage) Tag() string {
return "image"
}
func (image *backgroundImage) Clone() BackgroundElement {
result := NewBackgroundImage(nil)
for tag, value := range image.properties {
result.setRaw(tag, value)
}
return result
}
func normalizeBackgroundImageTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case "source":
tag = Source
case Fit:
tag = backgroundFit
case HorizontalAlign:
tag = ImageHorizontalAlign
case VerticalAlign:
tag = ImageVerticalAlign
}
return tag
}
func (image *backgroundImage) cssStyle(session Session) string {
if src, ok := imageProperty(image, Source, session); ok && src != "" {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(`url(`)
buffer.WriteString(src)
buffer.WriteRune(')')
attachment, _ := enumProperty(image, Attachment, session, NoRepeat)
values := enumProperties[Attachment].values
if attachment > 0 && attachment < len(values) {
buffer.WriteRune(' ')
buffer.WriteString(values[attachment])
}
align, _ := enumProperty(image, ImageHorizontalAlign, session, LeftAlign)
values = enumProperties[ImageHorizontalAlign].values
if align >= 0 && align < len(values) {
buffer.WriteRune(' ')
buffer.WriteString(values[align])
} else {
buffer.WriteString(` left`)
}
align, _ = enumProperty(image, ImageVerticalAlign, session, TopAlign)
values = enumProperties[ImageVerticalAlign].values
if align >= 0 && align < len(values) {
buffer.WriteRune(' ')
buffer.WriteString(values[align])
} else {
buffer.WriteString(` top`)
}
fit, _ := enumProperty(image, backgroundFit, session, NoneFit)
values = enumProperties[backgroundFit].values
if fit > 0 && fit < len(values) {
buffer.WriteString(` / `)
buffer.WriteString(values[fit])
} else {
width, _ := sizeProperty(image, Width, session)
height, _ := sizeProperty(image, Height, session)
if width.Type != Auto || height.Type != Auto {
buffer.WriteString(` / `)
buffer.WriteString(width.cssString("auto", session))
buffer.WriteRune(' ')
buffer.WriteString(height.cssString("auto", session))
}
}
repeat, _ := enumProperty(image, Repeat, session, NoRepeat)
values = enumProperties[Repeat].values
if repeat >= 0 && repeat < len(values) {
buffer.WriteRune(' ')
buffer.WriteString(values[repeat])
} else {
buffer.WriteString(` no-repeat`)
}
return buffer.String()
}
return ""
}
func (image *backgroundImage) writeString(buffer *strings.Builder, indent string) {
image.writeToBuffer(buffer, indent, image.Tag(), []PropertyName{
Source,
Width,
Height,
ImageHorizontalAlign,
ImageVerticalAlign,
backgroundFit,
Repeat,
Attachment,
})
}
func (image *backgroundImage) String() string {
return runStringWriter(image)
}

413
backgroundLinearGradient.go Normal file
View File

@ -0,0 +1,413 @@
package rui
import "strings"
type LinearGradientDirectionType int
// Constants related to view's background gradient description
const (
// ToTopGradient is value of the Direction property of a linear gradient. The value is equivalent to the 0deg angle
ToTopGradient LinearGradientDirectionType = 0
// ToRightTopGradient is value of the Direction property of a linear gradient.
ToRightTopGradient LinearGradientDirectionType = 1
// ToRightGradient is value of the Direction property of a linear gradient. The value is equivalent to the 90deg angle
ToRightGradient LinearGradientDirectionType = 2
// ToRightBottomGradient is value of the Direction property of a linear gradient.
ToRightBottomGradient LinearGradientDirectionType = 3
// ToBottomGradient is value of the Direction property of a linear gradient. The value is equivalent to the 180deg angle
ToBottomGradient LinearGradientDirectionType = 4
// ToLeftBottomGradient is value of the Direction property of a linear gradient.
ToLeftBottomGradient LinearGradientDirectionType = 5
// ToLeftGradient is value of the Direction property of a linear gradient. The value is equivalent to the 270deg angle
ToLeftGradient LinearGradientDirectionType = 6
// ToLeftTopGradient is value of the Direction property of a linear gradient.
ToLeftTopGradient LinearGradientDirectionType = 7
)
// BackgroundGradientPoint define point on gradient straight line
type BackgroundGradientPoint struct {
// Color - the color of the point. Must not be nil.
// Can take a value of Color type or string (color constant or textual description of the color)
Color any
// Pos - the distance from the start of the gradient straight line. Optional (may be nil).
// Can take a value of SizeUnit type or string (size constant or textual description of the SizeUnit)
Pos any
}
type backgroundGradient struct {
backgroundElement
}
type backgroundLinearGradient struct {
backgroundGradient
}
// NewBackgroundLinearGradient creates the new background linear gradient.
//
// The following properties can be used:
// - "gradient" [Gradient] - Describes gradient stop points. This is a mandatory property while describing background gradients.
// - "direction" [Direction] - Defines the direction of the gradient line.
// - "repeating" [Repeating] - Defines whether stop points needs to be repeated after the last one.
func NewBackgroundLinearGradient(params Params) BackgroundElement {
result := new(backgroundLinearGradient)
result.init()
for tag, value := range params {
result.Set(tag, value)
}
return result
}
// NewLinearGradient creates the new background linear gradient.
func NewLinearGradient[DirectionType LinearGradientDirectionType | AngleUnit](direction DirectionType, repeating bool, point1 GradientPoint, point2 GradientPoint, points ...GradientPoint) BackgroundElement {
params := Params{
Direction: direction,
Gradient: append([]GradientPoint{point1, point2}, points...),
}
if repeating {
params[Repeating] = true
}
return NewBackgroundLinearGradient(params)
}
func parseGradientText(value string) []BackgroundGradientPoint {
elements := strings.Split(value, ",")
count := len(elements)
if count < 2 {
ErrorLog("The gradient must contain at least 2 points")
return nil
}
points := make([]BackgroundGradientPoint, count)
for i, element := range elements {
if !points[i].setValue(element) {
ErrorLogF(`Invalid %d element of the conic gradient: "%s"`, i, element)
return nil
}
}
return points
}
func backgroundGradientSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case Repeating:
return setBoolProperty(properties, tag, value)
case Gradient:
switch value := value.(type) {
case string:
if value != "" {
if strings.Contains(value, " ") || strings.Contains(value, ",") {
if points := parseGradientText(value); len(points) >= 2 {
properties.setRaw(Gradient, points)
return []PropertyName{tag}
}
} else if value[0] == '@' {
properties.setRaw(Gradient, value)
return []PropertyName{tag}
}
}
case []BackgroundGradientPoint:
if len(value) >= 2 {
properties.setRaw(Gradient, value)
return []PropertyName{tag}
}
case []Color:
count := len(value)
if count >= 2 {
points := make([]BackgroundGradientPoint, count)
for i, color := range value {
points[i].Color = color
}
properties.setRaw(Gradient, points)
return []PropertyName{tag}
}
case []GradientPoint:
count := len(value)
if count >= 2 {
points := make([]BackgroundGradientPoint, count)
for i, point := range value {
points[i].Color = point.Color
points[i].Pos = Percent(point.Offset * 100)
}
properties.setRaw(Gradient, points)
return []PropertyName{tag}
}
}
ErrorLogF("Invalid gradient %v", value)
return nil
}
ErrorLogF("Property %s is not supported by a background gradient", tag)
return nil
}
func (point *BackgroundGradientPoint) setValue(text string) bool {
text = strings.Trim(text, " ")
colorText := text
pointText := ""
if index := strings.Index(text, " "); index > 0 {
colorText = text[:index]
pointText = strings.Trim(text[index+1:], " ")
}
if colorText == "" {
return false
}
if colorText[0] == '@' {
point.Color = colorText
} else if color, ok := StringToColor(colorText); ok {
point.Color = color
} else {
return false
}
if pointText == "" {
point.Pos = nil
} else if pointText[0] == '@' {
point.Pos = pointText
} else if pos, ok := StringToSizeUnit(pointText); ok {
point.Pos = pos
} else {
return false
}
return true
}
func (point *BackgroundGradientPoint) color(session Session) (Color, bool) {
if point.Color != nil {
switch color := point.Color.(type) {
case string:
if color != "" {
if color[0] == '@' {
if clr, ok := session.Color(color[1:]); ok {
return clr, true
}
} else {
if clr, ok := StringToColor(color); ok {
return clr, true
}
}
}
case Color:
return color, true
default:
if n, ok := isInt(point.Color); ok {
return Color(n), true
}
}
}
return 0, false
}
// String convert internal representation of [BackgroundGradientPoint] into a string.
func (point *BackgroundGradientPoint) String() string {
result := "black"
if point.Color != nil {
switch color := point.Color.(type) {
case string:
result = color
case Color:
result = color.String()
}
}
if point.Pos != nil {
switch value := point.Pos.(type) {
case string:
result += " " + value
case SizeUnit:
if value.Type != Auto {
result += " " + value.String()
}
}
}
return result
}
func (gradient *backgroundGradient) writeGradient(session Session, buffer *strings.Builder) bool {
value, ok := gradient.properties[Gradient]
if !ok {
return false
}
var points []BackgroundGradientPoint = nil
switch value := value.(type) {
case string:
if value != "" && value[0] == '@' {
if text, ok := session.Constant(value[1:]); ok {
points = parseGradientText(text)
}
}
case []BackgroundGradientPoint:
points = value
}
if len(points) > 0 {
for i, point := range points {
if i > 0 {
buffer.WriteString(`, `)
}
if color, ok := point.color(session); ok {
buffer.WriteString(color.cssString())
} else {
return false
}
if point.Pos != nil {
switch value := point.Pos.(type) {
case string:
if value != "" {
if value, ok := session.resolveConstants(value); ok {
if pos, ok := StringToSizeUnit(value); ok && pos.Type != Auto {
buffer.WriteRune(' ')
buffer.WriteString(pos.cssString("", session))
}
}
}
case SizeUnit:
if value.Type != Auto {
buffer.WriteRune(' ')
buffer.WriteString(value.cssString("", session))
}
}
}
}
return true
}
return false
}
func (gradient *backgroundLinearGradient) init() {
gradient.backgroundElement.init()
gradient.set = backgroundLinearGradientSet
gradient.supportedProperties = []PropertyName{Direction, Repeating, Gradient}
}
func (gradient *backgroundLinearGradient) Tag() string {
return "linear-gradient"
}
func (image *backgroundLinearGradient) Clone() BackgroundElement {
result := NewBackgroundLinearGradient(nil)
for tag, value := range image.properties {
result.setRaw(tag, value)
}
return result
}
func backgroundLinearGradientSet(properties Properties, tag PropertyName, value any) []PropertyName {
if tag == Direction {
switch value := value.(type) {
case AngleUnit:
properties.setRaw(Direction, value)
return []PropertyName{tag}
case string:
if setSimpleProperty(properties, tag, value) {
return []PropertyName{tag}
}
if angle, ok := StringToAngleUnit(value); ok {
properties.setRaw(Direction, angle)
return []PropertyName{tag}
}
case LinearGradientDirectionType:
return setEnumProperty(properties, tag, int(value), enumProperties[Direction].values)
}
return setEnumProperty(properties, tag, value, enumProperties[Direction].values)
}
return backgroundGradientSet(properties, tag, value)
}
func (gradient *backgroundLinearGradient) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if repeating, _ := boolProperty(gradient, Repeating, session); repeating {
buffer.WriteString(`repeating-linear-gradient(`)
} else {
buffer.WriteString(`linear-gradient(`)
}
if value, ok := gradient.properties[Direction]; ok {
switch value := value.(type) {
case string:
if text, ok := session.resolveConstants(value); ok {
direction := enumProperties[Direction]
if n, ok := enumStringToInt(text, direction.values, false); ok {
buffer.WriteString(direction.cssValues[n])
buffer.WriteString(", ")
} else {
if angle, ok := StringToAngleUnit(text); ok {
buffer.WriteString(angle.cssString())
buffer.WriteString(", ")
} else {
ErrorLog(`Invalid linear gradient direction: ` + text)
}
}
} else {
ErrorLog(`Invalid linear gradient direction: ` + value)
}
case int:
values := enumProperties[Direction].cssValues
if value >= 0 && value < len(values) {
buffer.WriteString(values[value])
buffer.WriteString(", ")
} else {
ErrorLogF(`Invalid linear gradient direction: %d`, value)
}
case AngleUnit:
buffer.WriteString(value.cssString())
buffer.WriteString(", ")
}
}
if !gradient.writeGradient(session, buffer) {
return ""
}
buffer.WriteString(") ")
return buffer.String()
}
func (gradient *backgroundLinearGradient) writeString(buffer *strings.Builder, indent string) {
gradient.writeToBuffer(buffer, indent, gradient.Tag(), []PropertyName{
Gradient,
Repeating,
Direction,
})
}
func (gradient *backgroundLinearGradient) String() string {
return runStringWriter(gradient)
}

357
backgroundRadialGradient.go Normal file
View File

@ -0,0 +1,357 @@
package rui
import "strings"
type RadialGradientRadiusType int
// Constants related to view's background gradient description
const (
// EllipseGradient is value of the Shape property of a radial gradient background:
// the shape is an axis-aligned ellipse
EllipseGradient = 0
// CircleGradient is value of the Shape property of a radial gradient background:
// the gradient's shape is a circle with constant radius
CircleGradient = 1
// ClosestSideGradient is value of the Radius property of a radial gradient background:
// The gradient's ending shape meets the side of the box closest to its center (for circles)
// or meets both the vertical and horizontal sides closest to the center (for ellipses).
ClosestSideGradient RadialGradientRadiusType = 0
// ClosestCornerGradient is value of the Radius property of a radial gradient background:
// The gradient's ending shape is sized so that it exactly meets the closest corner
// of the box from its center.
ClosestCornerGradient RadialGradientRadiusType = 1
// FarthestSideGradient is value of the Radius property of a radial gradient background:
// Similar to closest-side, except the ending shape is sized to meet the side of the box
// farthest from its center (or vertical and horizontal sides).
FarthestSideGradient RadialGradientRadiusType = 2
// FarthestCornerGradient is value of the Radius property of a radial gradient background:
// The default value, the gradient's ending shape is sized so that it exactly meets
// the farthest corner of the box from its center.
FarthestCornerGradient RadialGradientRadiusType = 3
)
type backgroundRadialGradient struct {
backgroundGradient
}
// NewBackgroundRadialGradient creates the new background radial gradient.
//
// The following properties can be used:
// - "gradient" (Gradient) - Describes gradient stop points. This is a mandatory property while describing background gradients.
// - "center-x" (CenterX), "center-y" (CenterY) - Defines the gradient center point cooordinates.
// - "radial-gradient-radius" (RadialGradientRadius) - Defines radius of the radial gradient.
// - "radial-gradient-shape" (RadialGradientShape) - Defines shape of the radial gradient.
// - "repeating" (Repeating) - Defines whether stop points needs to be repeated after the last one.
func NewBackgroundRadialGradient(params Params) BackgroundElement {
result := new(backgroundRadialGradient)
result.init()
for tag, value := range params {
result.Set(tag, value)
}
return result
}
// NewCircleRadialGradient creates the new background circle radial gradient.
func NewCircleRadialGradient[radiusType SizeUnit | RadialGradientRadiusType](xCenter, yCenter SizeUnit, radius radiusType, repeating bool, point1 GradientPoint, point2 GradientPoint, points ...GradientPoint) BackgroundElement {
params := Params{
RadialGradientShape: CircleGradient,
Gradient: append([]GradientPoint{point1, point2}, points...),
RadialGradientRadius: radius,
}
if xCenter.Type != Auto {
params[CenterX] = xCenter
}
if yCenter.Type != Auto {
params[CenterY] = yCenter
}
if repeating {
params[Repeating] = true
}
return NewBackgroundRadialGradient(params)
}
// NewEllipseRadialGradient creates the new background ellipse radial gradient.
func NewEllipseRadialGradient[radiusType []SizeUnit | RadialGradientRadiusType](xCenter, yCenter SizeUnit, radius radiusType, repeating bool, point1 GradientPoint, point2 GradientPoint, points ...GradientPoint) BackgroundElement {
params := Params{
RadialGradientShape: EllipseGradient,
Gradient: append([]GradientPoint{point1, point2}, points...),
RadialGradientRadius: radius,
}
if xCenter.Type != Auto {
params[CenterX] = xCenter
}
if yCenter.Type != Auto {
params[CenterY] = yCenter
}
if repeating {
params[Repeating] = true
}
return NewBackgroundRadialGradient(params)
}
func (gradient *backgroundRadialGradient) init() {
gradient.backgroundElement.init()
gradient.normalize = normalizeRadialGradientTag
gradient.set = backgroundRadialGradientSet
gradient.supportedProperties = []PropertyName{
RadialGradientRadius, RadialGradientShape, CenterX, CenterY, Gradient, Repeating,
}
}
func (gradient *backgroundRadialGradient) Tag() string {
return "radial-gradient"
}
func (image *backgroundRadialGradient) Clone() BackgroundElement {
result := NewBackgroundRadialGradient(nil)
for tag, value := range image.properties {
result.setRaw(tag, value)
}
return result
}
func normalizeRadialGradientTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Radius:
tag = RadialGradientRadius
case Shape:
tag = RadialGradientShape
case "x-center":
tag = CenterX
case "y-center":
tag = CenterY
}
return tag
}
func backgroundRadialGradientSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case RadialGradientRadius:
switch value := value.(type) {
case []SizeUnit:
switch len(value) {
case 0:
properties.setRaw(RadialGradientRadius, nil)
case 1:
if value[0].Type == Auto {
properties.setRaw(RadialGradientRadius, nil)
} else {
properties.setRaw(RadialGradientRadius, value[0])
}
default:
properties.setRaw(RadialGradientRadius, value)
}
return []PropertyName{tag}
case []any:
switch len(value) {
case 0:
properties.setRaw(RadialGradientRadius, nil)
return []PropertyName{tag}
case 1:
return backgroundRadialGradientSet(properties, RadialGradientRadius, value[0])
default:
properties.setRaw(RadialGradientRadius, value)
return []PropertyName{tag}
}
case string:
if setSimpleProperty(properties, RadialGradientRadius, value) {
return []PropertyName{tag}
}
if size, err := stringToSizeUnit(value); err == nil {
if size.Type == Auto {
properties.setRaw(RadialGradientRadius, nil)
} else {
properties.setRaw(RadialGradientRadius, size)
}
return []PropertyName{tag}
}
return setEnumProperty(properties, RadialGradientRadius, value, enumProperties[RadialGradientRadius].values)
case SizeUnit:
if value.Type == Auto {
properties.setRaw(RadialGradientRadius, nil)
} else {
properties.setRaw(RadialGradientRadius, value)
}
return []PropertyName{tag}
case RadialGradientRadiusType:
return setEnumProperty(properties, RadialGradientRadius, int(value), enumProperties[RadialGradientRadius].values)
case int:
return setEnumProperty(properties, RadialGradientRadius, value, enumProperties[RadialGradientRadius].values)
}
ErrorLogF(`Invalid value of "%s" property: %v`, tag, value)
return nil
case RadialGradientShape, CenterX, CenterY:
return propertiesSet(properties, tag, value)
}
return backgroundGradientSet(properties, tag, value)
}
func (gradient *backgroundRadialGradient) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if repeating, _ := boolProperty(gradient, Repeating, session); repeating {
buffer.WriteString(`repeating-radial-gradient(`)
} else {
buffer.WriteString(`radial-gradient(`)
}
var shapeText string
if shape, ok := enumProperty(gradient, RadialGradientShape, session, EllipseGradient); ok && shape == CircleGradient {
shapeText = `circle `
} else {
shapeText = `ellipse `
}
if value, ok := gradient.properties[RadialGradientRadius]; ok {
switch value := value.(type) {
case string:
if text, ok := session.resolveConstants(value); ok {
values := enumProperties[RadialGradientRadius]
if n, ok := enumStringToInt(text, values.values, false); ok {
buffer.WriteString(shapeText)
shapeText = ""
buffer.WriteString(values.cssValues[n])
buffer.WriteString(" ")
} else {
if r, ok := StringToSizeUnit(text); ok && r.Type != Auto {
buffer.WriteString("ellipse ")
shapeText = ""
buffer.WriteString(r.cssString("", session))
buffer.WriteString(" ")
buffer.WriteString(r.cssString("", session))
buffer.WriteString(" ")
} else {
ErrorLog(`Invalid radial gradient radius: ` + text)
}
}
} else {
ErrorLog(`Invalid radial gradient radius: ` + value)
}
case int:
values := enumProperties[RadialGradientRadius].cssValues
if value >= 0 && value < len(values) {
buffer.WriteString(shapeText)
shapeText = ""
buffer.WriteString(values[value])
buffer.WriteString(" ")
} else {
ErrorLogF(`Invalid radial gradient radius: %d`, value)
}
case SizeUnit:
if value.Type != Auto {
buffer.WriteString("ellipse ")
shapeText = ""
buffer.WriteString(value.cssString("", session))
buffer.WriteString(" ")
buffer.WriteString(value.cssString("", session))
buffer.WriteString(" ")
}
case []SizeUnit:
count := len(value)
if count > 2 {
count = 2
}
buffer.WriteString("ellipse ")
shapeText = ""
for i := 0; i < count; i++ {
buffer.WriteString(value[i].cssString("50%", session))
buffer.WriteString(" ")
}
case []any:
count := len(value)
if count > 2 {
count = 2
}
buffer.WriteString("ellipse ")
shapeText = ""
for i := 0; i < count; i++ {
if value[i] != nil {
switch value := value[i].(type) {
case SizeUnit:
buffer.WriteString(value.cssString("50%", session))
buffer.WriteString(" ")
case string:
if text, ok := session.resolveConstants(value); ok {
if size, err := stringToSizeUnit(text); err == nil {
buffer.WriteString(size.cssString("50%", session))
buffer.WriteString(" ")
} else {
buffer.WriteString("50% ")
}
} else {
buffer.WriteString("50% ")
}
}
} else {
buffer.WriteString("50% ")
}
}
}
}
x, _ := sizeProperty(gradient, CenterX, session)
y, _ := sizeProperty(gradient, CenterX, session)
if x.Type != Auto || y.Type != Auto {
if shapeText != "" {
buffer.WriteString(shapeText)
}
buffer.WriteString("at ")
buffer.WriteString(x.cssString("50%", session))
buffer.WriteString(" ")
buffer.WriteString(y.cssString("50%", session))
} else if shapeText != "" {
buffer.WriteString(shapeText)
}
buffer.WriteString(", ")
if !gradient.writeGradient(session, buffer) {
return ""
}
buffer.WriteString(") ")
return buffer.String()
}
func (gradient *backgroundRadialGradient) writeString(buffer *strings.Builder, indent string) {
gradient.writeToBuffer(buffer, indent, gradient.Tag(), []PropertyName{
Gradient,
CenterX,
CenterY,
Repeating,
RadialGradientShape,
RadialGradientRadius,
})
}
func (gradient *backgroundRadialGradient) String() string {
return runStringWriter(gradient)
}

610
border.go
View File

@ -5,44 +5,173 @@ import (
"strings"
)
// Constants related to view's border description
const (
// NoneLine constant specifies that there is no border
NoneLine = 0
// SolidLine constant specifies the border/line as a solid line
SolidLine = 1
// DashedLine constant specifies the border/line as a dashed line
DashedLine = 2
// DottedLine constant specifies the border/line as a dotted line
DottedLine = 3
// DoubleLine constant specifies the border/line as a double solid line
DoubleLine = 4
// DoubleLine constant specifies the border/line as a double solid line
WavyLine = 5
// LeftStyle is the constant for "left-style" property tag.
LeftStyle = "left-style"
// RightStyle is the constant for "-right-style" property tag.
RightStyle = "right-style"
//
// Used by BorderProperty.
// Left border line style.
//
// Supported types: int, string.
//
// Values:
// - 0 (NoneLine) or "none" - The border will not be drawn.
// - 1 (SolidLine) or "solid" - Solid line as a border.
// - 2 (DashedLine) or "dashed" - Dashed line as a border.
// - 3 (DottedLine) or "dotted" - Dotted line as a border.
// - 4 (DoubleLine) or "double" - Double line as a border.
LeftStyle PropertyName = "left-style"
// RightStyle is the constant for "right-style" property tag.
//
// Used by BorderProperty.
// Right border line style.
//
// Supported types: int, string.
//
// Values:
// - 0 (NoneLine) or "none" - The border will not be drawn.
// - 1 (SolidLine) or "solid" - Solid line as a border.
// - 2 (DashedLine) or "dashed" - Dashed line as a border.
// - 3 (DottedLine) or "dotted" - Dotted line as a border.
// - 4 (DoubleLine) or "double" - Double line as a border.
RightStyle PropertyName = "right-style"
// TopStyle is the constant for "top-style" property tag.
TopStyle = "top-style"
//
// Used by BorderProperty.
// Top border line style.
//
// Supported types: int, string.
//
// Values:
// - 0 (NoneLine) or "none" - The border will not be drawn.
// - 1 (SolidLine) or "solid" - Solid line as a border.
// - 2 (DashedLine) or "dashed" - Dashed line as a border.
// - 3 (DottedLine) or "dotted" - Dotted line as a border.
// - 4 (DoubleLine) or "double" - Double line as a border.
TopStyle PropertyName = "top-style"
// BottomStyle is the constant for "bottom-style" property tag.
BottomStyle = "bottom-style"
//
// Used by BorderProperty.
// Bottom border line style.
//
// Supported types: int, string.
//
// Values:
// - 0 (NoneLine) or "none" - The border will not be drawn.
// - 1 (SolidLine) or "solid" - Solid line as a border.
// - 2 (DashedLine) or "dashed" - Dashed line as a border.
// - 3 (DottedLine) or "dotted" - Dotted line as a border.
// - 4 (DoubleLine) or "double" - Double line as a border.
BottomStyle PropertyName = "bottom-style"
// LeftWidth is the constant for "left-width" property tag.
LeftWidth = "left-width"
// RightWidth is the constant for "-right-width" property tag.
RightWidth = "right-width"
//
// Used by BorderProperty.
// Left border line width.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See [SizeUnit] description for more details.
LeftWidth PropertyName = "left-width"
// RightWidth is the constant for "right-width" property tag.
//
// Used by BorderProperty.
// Right border line width.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See [SizeUnit] description for more details.
RightWidth PropertyName = "right-width"
// TopWidth is the constant for "top-width" property tag.
TopWidth = "top-width"
//
// Used by BorderProperty.
// Top border line width.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See [SizeUnit] description for more details.
TopWidth PropertyName = "top-width"
// BottomWidth is the constant for "bottom-width" property tag.
BottomWidth = "bottom-width"
//
// Used by BorderProperty.
// Bottom border line width.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See [SizeUnit] description for more details.
BottomWidth PropertyName = "bottom-width"
// LeftColor is the constant for "left-color" property tag.
LeftColor = "left-color"
// RightColor is the constant for "-right-color" property tag.
RightColor = "right-color"
//
// Used by BorderProperty.
// Left border line color.
//
// Supported types: Color, string.
//
// Internal type is Color, other types converted to it during assignment.
// See [Color] description for more details.
LeftColor PropertyName = "left-color"
// RightColor is the constant for "right-color" property tag.
//
// Used by BorderProperty.
// Right border line color.
//
// Supported types: Color, string.
//
// Internal type is Color, other types converted to it during assignment.
// See [Color] description for more details.
RightColor PropertyName = "right-color"
// TopColor is the constant for "top-color" property tag.
TopColor = "top-color"
//
// Used by BorderProperty.
// Top border line color.
//
// Supported types: Color, string.
//
// Internal type is Color, other types converted to it during assignment.
// See [Color] description for more details.
TopColor PropertyName = "top-color"
// BottomColor is the constant for "bottom-color" property tag.
BottomColor = "bottom-color"
//
// Used by BorderProperty.
// Bottom border line color.
//
// Supported types: Color, string.
//
// Internal type is Color, other types converted to it during assignment.
// See [Color] description for more details.
BottomColor PropertyName = "bottom-color"
)
// BorderProperty is the interface of a view border data
@ -50,8 +179,11 @@ type BorderProperty interface {
Properties
fmt.Stringer
stringWriter
// ViewBorders returns top, right, bottom and left borders information all together
ViewBorders(session Session) ViewBorders
delete(tag string)
deleteTag(tag PropertyName) bool
cssStyle(builder cssBuilder, session Session)
cssWidth(builder cssBuilder, session Session)
cssColor(builder cssBuilder, session Session)
@ -61,12 +193,12 @@ type BorderProperty interface {
}
type borderProperty struct {
propertyList
dataProperty
}
func newBorderProperty(value any) BorderProperty {
border := new(borderProperty)
border.properties = map[string]any{}
border.init()
if value != nil {
switch value := value.(type) {
@ -128,12 +260,20 @@ func newBorderProperty(value any) BorderProperty {
return border
}
// NewBorder creates the new BorderProperty
// NewBorder creates the new BorderProperty.
// The following properties can be used:
//
// "style" (Style). Determines the line style (int). Valid values: 0 (NoneLine), 1 (SolidLine), 2 (DashedLine), 3 (DottedLine), or 4 (DoubleLine);
//
// "color" (ColorTag). Determines the line color (Color);
//
// "width" (Width). Determines the line thickness (SizeUnit).
func NewBorder(params Params) BorderProperty {
border := new(borderProperty)
border.properties = map[string]any{}
border.init()
if params != nil {
for _, tag := range []string{Style, Width, ColorTag, Left, Right, Top, Bottom,
for _, tag := range []PropertyName{Style, Width, ColorTag, Left, Right, Top, Bottom,
LeftStyle, RightStyle, TopStyle, BottomStyle,
LeftWidth, RightWidth, TopWidth, BottomWidth,
LeftColor, RightColor, TopColor, BottomColor} {
@ -145,8 +285,37 @@ func NewBorder(params Params) BorderProperty {
return border
}
func (border *borderProperty) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func (border *borderProperty) init() {
border.dataProperty.init()
border.normalize = normalizeBorderTag
border.get = borderGet
border.set = borderSet
border.remove = borderRemove
border.supportedProperties = []PropertyName{
Left,
Right,
Top,
Bottom,
Style,
LeftStyle,
RightStyle,
TopStyle,
BottomStyle,
Width,
LeftWidth,
RightWidth,
TopWidth,
BottomWidth,
ColorTag,
LeftColor,
RightColor,
TopColor,
BottomColor,
}
}
func normalizeBorderTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case BorderLeft, CellBorderLeft:
return Left
@ -213,23 +382,23 @@ func (border *borderProperty) writeString(buffer *strings.Builder, indent string
buffer.WriteString("_{ ")
comma := false
write := func(tag string, value any) {
write := func(tag PropertyName, value any) {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
writePropertyValue(buffer, BorderStyle, value, indent)
comma = true
}
for _, tag := range []string{Style, Width, ColorTag} {
for _, tag := range []PropertyName{Style, Width, ColorTag} {
if value, ok := border.properties[tag]; ok {
write(tag, value)
}
}
for _, side := range []string{Top, Right, Bottom, Left} {
for _, side := range []PropertyName{Top, Right, Bottom, Left} {
style, okStyle := border.properties[side+"-"+Style]
width, okWidth := border.properties[side+"-"+Width]
color, okColor := border.properties[side+"-"+ColorTag]
@ -239,7 +408,7 @@ func (border *borderProperty) writeString(buffer *strings.Builder, indent string
comma = false
}
buffer.WriteString(side)
buffer.WriteString(string(side))
buffer.WriteString(" = _{ ")
if okStyle {
write(Style, style)
@ -262,164 +431,96 @@ func (border *borderProperty) String() string {
return runStringWriter(border)
}
func (border *borderProperty) setSingleBorderObject(prefix string, obj DataObject) bool {
result := true
if text, ok := obj.PropertyValue(Style); ok {
if !border.setEnumProperty(prefix+"-style", text, enumProperties[BorderStyle].values) {
result = false
}
}
if text, ok := obj.PropertyValue(ColorTag); ok {
if !border.setColorProperty(prefix+"-color", text) {
result = false
}
}
if text, ok := obj.PropertyValue("width"); ok {
if !border.setSizeProperty(prefix+"-width", text) {
result = false
}
}
return result
}
func (border *borderProperty) setBorderObject(obj DataObject) bool {
result := true
for _, side := range []string{Top, Right, Bottom, Left} {
if node := obj.PropertyByTag(side); node != nil {
if node.Type() == ObjectNode {
if !border.setSingleBorderObject(side, node.Object()) {
for i := 0; i < obj.PropertyCount(); i++ {
if node := obj.Property(i); node != nil {
tag := PropertyName(node.Tag())
switch node.Type() {
case TextNode:
if borderSet(border, tag, node.Text()) == nil {
result = false
}
} else {
notCompatibleType(side, node)
result = false
}
}
}
if text, ok := obj.PropertyValue(Style); ok {
values := split4Values(text)
styles := enumProperties[BorderStyle].values
switch len(values) {
case 1:
if !border.setEnumProperty(Style, values[0], styles) {
result = false
}
case 4:
for n, tag := range [4]string{TopStyle, RightStyle, BottomStyle, LeftStyle} {
if !border.setEnumProperty(tag, values[n], styles) {
case ObjectNode:
if borderSet(border, tag, node.Object()) == nil {
result = false
}
}
default:
notCompatibleType(Style, text)
default:
result = false
}
} else {
result = false
}
}
if text, ok := obj.PropertyValue(ColorTag); ok {
values := split4Values(text)
switch len(values) {
case 1:
if !border.setColorProperty(ColorTag, values[0]) {
return false
}
case 4:
for n, tag := range [4]string{TopColor, RightColor, BottomColor, LeftColor} {
if !border.setColorProperty(tag, values[n]) {
return false
}
}
default:
notCompatibleType(ColorTag, text)
result = false
}
}
if text, ok := obj.PropertyValue(Width); ok {
values := split4Values(text)
switch len(values) {
case 1:
if !border.setSizeProperty(Width, values[0]) {
result = false
}
case 4:
for n, tag := range [4]string{TopWidth, RightWidth, BottomWidth, LeftWidth} {
if !border.setSizeProperty(tag, values[n]) {
result = false
}
}
default:
notCompatibleType(Width, text)
result = false
}
}
return result
}
func (border *borderProperty) Remove(tag string) {
tag = border.normalizeTag(tag)
func borderRemove(properties Properties, tag PropertyName) []PropertyName {
result := []PropertyName{}
removeTag := func(t PropertyName) {
if properties.getRaw(t) != nil {
properties.setRaw(t, nil)
result = append(result, t)
}
}
switch tag {
case Style:
for _, t := range []string{tag, TopStyle, RightStyle, BottomStyle, LeftStyle} {
delete(border.properties, t)
for _, t := range []PropertyName{tag, TopStyle, RightStyle, BottomStyle, LeftStyle} {
removeTag(t)
}
case Width:
for _, t := range []string{tag, TopWidth, RightWidth, BottomWidth, LeftWidth} {
delete(border.properties, t)
for _, t := range []PropertyName{tag, TopWidth, RightWidth, BottomWidth, LeftWidth} {
removeTag(t)
}
case ColorTag:
for _, t := range []string{tag, TopColor, RightColor, BottomColor, LeftColor} {
delete(border.properties, t)
for _, t := range []PropertyName{tag, TopColor, RightColor, BottomColor, LeftColor} {
removeTag(t)
}
case Left, Right, Top, Bottom:
border.Remove(tag + "-style")
border.Remove(tag + "-width")
border.Remove(tag + "-color")
removeTag(tag + "-style")
removeTag(tag + "-width")
removeTag(tag + "-color")
case LeftStyle, RightStyle, TopStyle, BottomStyle:
delete(border.properties, tag)
if style, ok := border.properties[Style]; ok && style != nil {
for _, t := range []string{TopStyle, RightStyle, BottomStyle, LeftStyle} {
removeTag(tag)
if style := properties.getRaw(Style); style != nil {
for _, t := range []PropertyName{TopStyle, RightStyle, BottomStyle, LeftStyle} {
if t != tag {
if _, ok := border.properties[t]; !ok {
border.properties[t] = style
if properties.getRaw(t) == nil {
properties.setRaw(t, style)
result = append(result, t)
}
}
}
}
case LeftWidth, RightWidth, TopWidth, BottomWidth:
delete(border.properties, tag)
if width, ok := border.properties[Width]; ok && width != nil {
for _, t := range []string{TopWidth, RightWidth, BottomWidth, LeftWidth} {
removeTag(tag)
if width := properties.getRaw(Width); width != nil {
for _, t := range []PropertyName{TopWidth, RightWidth, BottomWidth, LeftWidth} {
if t != tag {
if _, ok := border.properties[t]; !ok {
border.properties[t] = width
if properties.getRaw(t) == nil {
properties.setRaw(t, width)
result = append(result, t)
}
}
}
}
case LeftColor, RightColor, TopColor, BottomColor:
delete(border.properties, tag)
if color, ok := border.properties[ColorTag]; ok && color != nil {
for _, t := range []string{TopColor, RightColor, BottomColor, LeftColor} {
removeTag(tag)
if color := properties.getRaw(ColorTag); color != nil {
for _, t := range []PropertyName{TopColor, RightColor, BottomColor, LeftColor} {
if t != tag {
if _, ok := border.properties[t]; !ok {
border.properties[t] = color
if properties.getRaw(t) == nil {
properties.setRaw(t, color)
result = append(result, t)
}
}
}
@ -428,80 +529,118 @@ func (border *borderProperty) Remove(tag string) {
default:
ErrorLogF(`"%s" property is not compatible with the BorderProperty`, tag)
}
return result
}
func (border *borderProperty) Set(tag string, value any) bool {
if value == nil {
border.Remove(tag)
return true
}
func borderSet(properties Properties, tag PropertyName, value any) []PropertyName {
tag = border.normalizeTag(tag)
setSingleBorderObject := func(prefix PropertyName, obj DataObject) []PropertyName {
result := []PropertyName{}
if text, ok := obj.PropertyValue(string(Style)); ok {
props := setEnumProperty(properties, prefix+"-style", text, enumProperties[BorderStyle].values)
if props == nil {
return nil
}
result = append(result, props...)
}
if text, ok := obj.PropertyValue(string(ColorTag)); ok {
props := setColorProperty(properties, prefix+"-color", text)
if props == nil && len(result) == 0 {
return nil
}
result = append(result, props...)
}
if text, ok := obj.PropertyValue("width"); ok {
props := setSizeProperty(properties, prefix+"-width", text)
if props == nil && len(result) == 0 {
return nil
}
result = append(result, props...)
}
if len(result) > 0 {
result = append(result, prefix)
}
return result
}
switch tag {
case Style:
if border.setEnumProperty(Style, value, enumProperties[BorderStyle].values) {
for _, side := range []string{TopStyle, RightStyle, BottomStyle, LeftStyle} {
delete(border.properties, side)
if result := setEnumProperty(properties, Style, value, enumProperties[BorderStyle].values); result != nil {
for _, side := range []PropertyName{TopStyle, RightStyle, BottomStyle, LeftStyle} {
if value := properties.getRaw(side); value != nil {
properties.setRaw(side, nil)
result = append(result, side)
}
}
return true
return result
}
case Width:
if border.setSizeProperty(Width, value) {
for _, side := range []string{TopWidth, RightWidth, BottomWidth, LeftWidth} {
delete(border.properties, side)
if result := setSizeProperty(properties, Width, value); result != nil {
for _, side := range []PropertyName{TopWidth, RightWidth, BottomWidth, LeftWidth} {
if value := properties.getRaw(side); value != nil {
properties.setRaw(side, nil)
result = append(result, side)
}
}
return true
return result
}
case ColorTag:
if border.setColorProperty(ColorTag, value) {
for _, side := range []string{TopColor, RightColor, BottomColor, LeftColor} {
delete(border.properties, side)
if result := setColorProperty(properties, ColorTag, value); result != nil {
for _, side := range []PropertyName{TopColor, RightColor, BottomColor, LeftColor} {
if value := properties.getRaw(side); value != nil {
properties.setRaw(side, nil)
result = append(result, side)
}
}
return true
return result
}
case LeftStyle, RightStyle, TopStyle, BottomStyle:
return border.setEnumProperty(tag, value, enumProperties[BorderStyle].values)
return setEnumProperty(properties, tag, value, enumProperties[BorderStyle].values)
case LeftWidth, RightWidth, TopWidth, BottomWidth:
return border.setSizeProperty(tag, value)
return setSizeProperty(properties, tag, value)
case LeftColor, RightColor, TopColor, BottomColor:
return border.setColorProperty(tag, value)
return setColorProperty(properties, tag, value)
case Left, Right, Top, Bottom:
switch value := value.(type) {
case string:
if obj := ParseDataText(value); obj != nil {
return border.setSingleBorderObject(tag, obj)
return setSingleBorderObject(tag, obj)
}
case DataObject:
return border.setSingleBorderObject(tag, value)
return setSingleBorderObject(tag, value)
case BorderProperty:
result := []PropertyName{}
styleTag := tag + "-" + Style
if style := value.Get(styleTag); value != nil {
border.properties[styleTag] = style
properties.setRaw(styleTag, style)
result = append(result, styleTag)
}
colorTag := tag + "-" + ColorTag
if color := value.Get(colorTag); value != nil {
border.properties[colorTag] = color
properties.setRaw(colorTag, color)
result = append(result, colorTag)
}
widthTag := tag + "-" + Width
if width := value.Get(widthTag); value != nil {
border.properties[widthTag] = width
properties.setRaw(widthTag, width)
result = append(result, widthTag)
}
return true
return result
case ViewBorder:
border.properties[tag+"-"+Style] = value.Style
border.properties[tag+"-"+Width] = value.Width
border.properties[tag+"-"+ColorTag] = value.Color
return true
properties.setRaw(tag+"-"+Style, value.Style)
properties.setRaw(tag+"-"+Width, value.Width)
properties.setRaw(tag+"-"+ColorTag, value.Color)
return []PropertyName{tag + "-" + Style, tag + "-" + Width, tag + "-" + ColorTag}
}
fallthrough
@ -509,105 +648,119 @@ func (border *borderProperty) Set(tag string, value any) bool {
ErrorLogF(`"%s" property is not compatible with the BorderProperty`, tag)
}
return false
return nil
}
func (border *borderProperty) Get(tag string) any {
tag = border.normalizeTag(tag)
if result, ok := border.properties[tag]; ok {
func borderGet(properties Properties, tag PropertyName) any {
if result := properties.getRaw(tag); result != nil {
return result
}
switch tag {
case Left, Right, Top, Bottom:
result := newBorderProperty(nil)
if style, ok := border.properties[tag+"-"+Style]; ok {
if style := properties.getRaw(tag + "-" + Style); style != nil {
result.Set(Style, style)
} else if style, ok := border.properties[Style]; ok {
} else if style := properties.getRaw(Style); style != nil {
result.Set(Style, style)
}
if width, ok := border.properties[tag+"-"+Width]; ok {
if width := properties.getRaw(tag + "-" + Width); width != nil {
result.Set(Width, width)
} else if width, ok := border.properties[Width]; ok {
} else if width := properties.getRaw(Width); width != nil {
result.Set(Width, width)
}
if color, ok := border.properties[tag+"-"+ColorTag]; ok {
if color := properties.getRaw(tag + "-" + ColorTag); color != nil {
result.Set(ColorTag, color)
} else if color, ok := border.properties[ColorTag]; ok {
} else if color := properties.getRaw(ColorTag); color != nil {
result.Set(ColorTag, color)
}
return result
case LeftStyle, RightStyle, TopStyle, BottomStyle:
if style, ok := border.properties[tag]; ok {
if style := properties.getRaw(tag); style != nil {
return style
}
return border.properties[Style]
return properties.getRaw(Style)
case LeftWidth, RightWidth, TopWidth, BottomWidth:
if width, ok := border.properties[tag]; ok {
if width := properties.getRaw(tag); width != nil {
return width
}
return border.properties[Width]
return properties.getRaw(Width)
case LeftColor, RightColor, TopColor, BottomColor:
if color, ok := border.properties[tag]; ok {
if color := properties.getRaw(tag); color != nil {
return color
}
return border.properties[ColorTag]
return properties.getRaw(ColorTag)
}
return nil
}
func (border *borderProperty) delete(tag string) {
tag = border.normalizeTag(tag)
remove := []string{}
func (border *borderProperty) deleteTag(tag PropertyName) bool {
result := false
removeTags := func(tags []PropertyName) {
for _, tag := range tags {
if border.getRaw(tag) != nil {
border.setRaw(tag, nil)
result = true
}
}
}
switch tag {
case Style:
remove = []string{Style, LeftStyle, RightStyle, TopStyle, BottomStyle}
removeTags([]PropertyName{Style, LeftStyle, RightStyle, TopStyle, BottomStyle})
case Width:
remove = []string{Width, LeftWidth, RightWidth, TopWidth, BottomWidth}
removeTags([]PropertyName{Width, LeftWidth, RightWidth, TopWidth, BottomWidth})
case ColorTag:
remove = []string{ColorTag, LeftColor, RightColor, TopColor, BottomColor}
removeTags([]PropertyName{ColorTag, LeftColor, RightColor, TopColor, BottomColor})
case Left, Right, Top, Bottom:
if border.Get(Style) != nil {
border.properties[tag+"-"+Style] = 0
remove = []string{tag + "-" + ColorTag, tag + "-" + Width}
result = true
removeTags([]PropertyName{tag + "-" + ColorTag, tag + "-" + Width})
} else {
remove = []string{tag + "-" + Style, tag + "-" + ColorTag, tag + "-" + Width}
removeTags([]PropertyName{tag + "-" + Style, tag + "-" + ColorTag, tag + "-" + Width})
}
case LeftStyle, RightStyle, TopStyle, BottomStyle:
if border.Get(Style) != nil {
border.properties[tag] = 0
} else {
remove = []string{tag}
if border.getRaw(tag) != nil {
if border.Get(Style) != nil {
border.properties[tag] = 0
result = true
} else {
removeTags([]PropertyName{tag})
}
}
case LeftWidth, RightWidth, TopWidth, BottomWidth:
if border.Get(Width) != nil {
border.properties[tag] = AutoSize()
} else {
remove = []string{tag}
if border.getRaw(tag) != nil {
if border.Get(Width) != nil {
border.properties[tag] = AutoSize()
result = true
} else {
removeTags([]PropertyName{tag})
}
}
case LeftColor, RightColor, TopColor, BottomColor:
if border.Get(ColorTag) != nil {
border.properties[tag] = 0
} else {
remove = []string{tag}
if border.getRaw(tag) != nil {
if border.Get(ColorTag) != nil {
border.properties[tag] = 0
result = true
} else {
removeTags([]PropertyName{tag})
}
}
}
for _, tag := range remove {
delete(border.properties, tag)
}
return result
}
func (border *borderProperty) ViewBorders(session Session) ViewBorders {
@ -616,7 +769,7 @@ func (border *borderProperty) ViewBorders(session Session) ViewBorders {
defWidth, _ := sizeProperty(border, Width, session)
defColor, _ := colorProperty(border, ColorTag, session)
getBorder := func(prefix string) ViewBorder {
getBorder := func(prefix PropertyName) ViewBorder {
var result ViewBorder
var ok bool
if result.Style, ok = valueToEnum(border.getRaw(prefix+Style), BorderStyle, session, NoneLine); !ok {
@ -645,9 +798,9 @@ func (border *borderProperty) cssStyle(builder cssBuilder, session Session) {
if borders.Top.Style == borders.Right.Style &&
borders.Top.Style == borders.Left.Style &&
borders.Top.Style == borders.Bottom.Style {
builder.add(BorderStyle, values[borders.Top.Style])
builder.add(string(BorderStyle), values[borders.Top.Style])
} else {
builder.addValues(BorderStyle, " ", values[borders.Top.Style],
builder.addValues(string(BorderStyle), " ", values[borders.Top.Style],
values[borders.Right.Style], values[borders.Bottom.Style], values[borders.Left.Style])
}
}
@ -703,8 +856,13 @@ func (border *borderProperty) cssColorValue(session Session) string {
// ViewBorder describes parameters of a view border
type ViewBorder struct {
// Style of the border line
Style int
// Color of the border line
Color Color
// Width of the border line
Width SizeUnit
}
@ -726,11 +884,25 @@ func (border *ViewBorders) AllTheSame() bool {
border.Top.Width.Equal(border.Bottom.Width)
}
func getBorder(style Properties, tag string) BorderProperty {
if value := style.Get(tag); value != nil {
func getBorderProperty(properties Properties, tag PropertyName) BorderProperty {
if value := properties.getRaw(tag); value != nil {
if border, ok := value.(BorderProperty); ok {
return border
}
}
return nil
}
func setBorderPropertyElement(properties Properties, mainTag, tag PropertyName, value any) []PropertyName {
border := getBorderProperty(properties, mainTag)
if border == nil {
border = NewBorder(nil)
if border.Set(tag, value) {
properties.setRaw(mainTag, border)
return []PropertyName{mainTag, tag}
}
} else if border.Set(tag, value) {
return []PropertyName{mainTag, tag}
}
return nil
}

230
bounds.go
View File

@ -5,34 +5,60 @@ import (
"strings"
)
// BorderProperty is the interface of a bounds property data
// BorderProperty is an interface of a bounds property data
type BoundsProperty interface {
Properties
fmt.Stringer
stringWriter
// Bounds returns top, right, bottom and left size of the bounds
Bounds(session Session) Bounds
}
type boundsPropertyData struct {
propertyList
dataProperty
}
// NewBoundsProperty creates the new BoundsProperty object
// NewBoundsProperty creates the new BoundsProperty object.
//
// The following SizeUnit properties can be used: "left" (Left), "right" (Right), "top" (Top), and "bottom" (Bottom).
func NewBoundsProperty(params Params) BoundsProperty {
bounds := new(boundsPropertyData)
bounds.properties = map[string]any{}
bounds.init()
if params != nil {
for _, tag := range []string{Top, Right, Bottom, Left} {
if value, ok := params[tag]; ok {
bounds.Set(tag, value)
for _, tag := range bounds.supportedProperties {
if value, ok := params[tag]; ok && value != nil {
bounds.set(bounds, tag, value)
}
}
}
return bounds
}
func (bounds *boundsPropertyData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
// NewBounds creates the new BoundsProperty object.
//
// The arguments specify the boundaries in a clockwise direction: "top", "right", "bottom", and "left".
//
// If the argument is specified as int or float64, the value is considered to be in pixels.
func NewBounds[topType SizeUnit | int | float64, rightType SizeUnit | int | float64, bottomType SizeUnit | int | float64, leftType SizeUnit | int | float64](
top topType, right rightType, bottom bottomType, left leftType) BoundsProperty {
return NewBoundsProperty(Params{
Top: top,
Right: right,
Bottom: bottom,
Left: left,
})
}
func (bounds *boundsPropertyData) init() {
bounds.dataProperty.init()
bounds.normalize = normalizeBoundsTag
bounds.supportedProperties = []PropertyName{Top, Right, Bottom, Left}
}
func normalizeBoundsTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case MarginTop, PaddingTop, CellPaddingTop,
"top-margin", "top-padding", "top-cell-padding":
@ -61,12 +87,12 @@ func (bounds *boundsPropertyData) String() string {
func (bounds *boundsPropertyData) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("_{ ")
comma := false
for _, tag := range []string{Top, Right, Bottom, Left} {
for _, tag := range []PropertyName{Top, Right, Bottom, Left} {
if value, ok := bounds.properties[tag]; ok {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
writePropertyValue(buffer, tag, value, indent)
comma = true
@ -75,38 +101,6 @@ func (bounds *boundsPropertyData) writeString(buffer *strings.Builder, indent st
buffer.WriteString(" }")
}
func (bounds *boundsPropertyData) Remove(tag string) {
bounds.propertyList.Remove(bounds.normalizeTag(tag))
}
func (bounds *boundsPropertyData) Set(tag string, value any) bool {
if value == nil {
bounds.Remove(tag)
return true
}
tag = bounds.normalizeTag(tag)
switch tag {
case Top, Right, Bottom, Left:
return bounds.setSizeProperty(tag, value)
default:
ErrorLogF(`"%s" property is not compatible with the BoundsProperty`, tag)
}
return false
}
func (bounds *boundsPropertyData) Get(tag string) any {
tag = bounds.normalizeTag(tag)
if value, ok := bounds.properties[tag]; ok {
return value
}
return nil
}
func (bounds *boundsPropertyData) Bounds(session Session) Bounds {
top, _ := sizeProperty(bounds, Top, session)
right, _ := sizeProperty(bounds, Right, session)
@ -138,7 +132,7 @@ func (bounds *Bounds) SetAll(value SizeUnit) {
bounds.Left = value
}
func (bounds *Bounds) setFromProperties(tag, topTag, rightTag, bottomTag, leftTag string, properties Properties, session Session) {
func (bounds *Bounds) setFromProperties(tag, topTag, rightTag, bottomTag, leftTag PropertyName, properties Properties, session Session) {
bounds.Top = AutoSize()
if size, ok := sizeProperty(properties, tag, session); ok {
bounds.Top = size
@ -161,22 +155,6 @@ func (bounds *Bounds) setFromProperties(tag, topTag, rightTag, bottomTag, leftTa
}
}
/*
func (bounds *Bounds) allFieldsAuto() bool {
return bounds.Left.Type == Auto &&
bounds.Top.Type == Auto &&
bounds.Right.Type == Auto &&
bounds.Bottom.Type == Auto
}
func (bounds *Bounds) allFieldsZero() bool {
return (bounds.Left.Type == Auto || bounds.Left.Value == 0) &&
(bounds.Top.Type == Auto || bounds.Top.Value == 0) &&
(bounds.Right.Type == Auto || bounds.Right.Value == 0) &&
(bounds.Bottom.Type == Auto || bounds.Bottom.Value == 0)
}
*/
func (bounds *Bounds) allFieldsEqual() bool {
if bounds.Left.Type == bounds.Top.Type &&
bounds.Left.Type == bounds.Right.Type &&
@ -190,20 +168,6 @@ func (bounds *Bounds) allFieldsEqual() bool {
return false
}
/*
func (bounds Bounds) writeCSSString(buffer *strings.Builder, textForAuto string) {
buffer.WriteString(bounds.Top.cssString(textForAuto))
if !bounds.allFieldsEqual() {
buffer.WriteRune(' ')
buffer.WriteString(bounds.Right.cssString(textForAuto))
buffer.WriteRune(' ')
buffer.WriteString(bounds.Bottom.cssString(textForAuto))
buffer.WriteRune(' ')
buffer.WriteString(bounds.Left.cssString(textForAuto))
}
}
*/
// String convert Bounds to string
func (bounds *Bounds) String() string {
if bounds.allFieldsEqual() {
@ -213,11 +177,11 @@ func (bounds *Bounds) String() string {
bounds.Bottom.String() + "," + bounds.Left.String()
}
func (bounds *Bounds) cssValue(tag string, builder cssBuilder, session Session) {
func (bounds *Bounds) cssValue(tag PropertyName, builder cssBuilder, session Session) {
if bounds.allFieldsEqual() {
builder.add(tag, bounds.Top.cssString("0", session))
builder.add(string(tag), bounds.Top.cssString("0", session))
} else {
builder.addValues(tag, " ",
builder.addValues(string(tag), " ",
bounds.Top.cssString("0", session),
bounds.Right.cssString("0", session),
bounds.Bottom.cssString("0", session),
@ -231,8 +195,8 @@ func (bounds *Bounds) cssString(session Session) string {
return builder.finish()
}
func (properties *propertyList) setBounds(tag string, value any) bool {
if !properties.setSimpleProperty(tag, value) {
func setBoundsProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
switch value := value.(type) {
case string:
if strings.Contains(value, ",") {
@ -244,88 +208,119 @@ func (properties *propertyList) setBounds(tag string, value any) bool {
case 4:
bounds := NewBoundsProperty(nil)
for i, tag := range []string{Top, Right, Bottom, Left} {
for i, tag := range []PropertyName{Top, Right, Bottom, Left} {
if !bounds.Set(tag, values[i]) {
notCompatibleType(tag, value)
return false
return nil
}
}
properties.properties[tag] = bounds
return true
properties.setRaw(tag, bounds)
return []PropertyName{tag}
default:
notCompatibleType(tag, value)
return false
return nil
}
}
return properties.setSizeProperty(tag, value)
return setSizeProperty(properties, tag, value)
case SizeUnit:
properties.properties[tag] = value
properties.setRaw(tag, value)
case float32:
properties.properties[tag] = Px(float64(value))
properties.setRaw(tag, Px(float64(value)))
case float64:
properties.properties[tag] = Px(value)
properties.setRaw(tag, Px(value))
case Bounds:
bounds := NewBoundsProperty(nil)
if value.Top.Type != Auto {
bounds.Set(Top, value.Top)
bounds.setRaw(Top, value.Top)
}
if value.Right.Type != Auto {
bounds.Set(Right, value.Right)
bounds.setRaw(Right, value.Right)
}
if value.Bottom.Type != Auto {
bounds.Set(Bottom, value.Bottom)
bounds.setRaw(Bottom, value.Bottom)
}
if value.Left.Type != Auto {
bounds.Set(Left, value.Left)
bounds.setRaw(Left, value.Left)
}
properties.properties[tag] = bounds
properties.setRaw(tag, bounds)
case BoundsProperty:
properties.properties[tag] = value
properties.setRaw(tag, value)
case DataObject:
bounds := NewBoundsProperty(nil)
for _, tag := range []string{Top, Right, Bottom, Left} {
if text, ok := value.PropertyValue(tag); ok {
for _, tag := range []PropertyName{Top, Right, Bottom, Left} {
if text, ok := value.PropertyValue(string(tag)); ok {
if !bounds.Set(tag, text) {
notCompatibleType(tag, value)
return false
return nil
}
}
}
properties.properties[tag] = bounds
properties.setRaw(tag, bounds)
default:
if n, ok := isInt(value); ok {
properties.properties[tag] = Px(float64(n))
properties.setRaw(tag, Px(float64(n)))
} else {
notCompatibleType(tag, value)
return false
return nil
}
}
}
return true
return []PropertyName{tag}
}
func (properties *propertyList) boundsProperty(tag string) BoundsProperty {
if value, ok := properties.properties[tag]; ok {
func removeBoundsPropertySide(properties Properties, mainTag, sideTag PropertyName) []PropertyName {
if bounds := getBoundsProperty(properties, mainTag); bounds != nil {
if bounds.getRaw(sideTag) != nil {
bounds.Remove(sideTag)
if bounds.empty() {
bounds = nil
}
properties.setRaw(mainTag, bounds)
return []PropertyName{mainTag, sideTag}
}
}
return []PropertyName{}
}
func setBoundsPropertySide(properties Properties, mainTag, sideTag PropertyName, value any) []PropertyName {
if value == nil {
return removeBoundsPropertySide(properties, mainTag, sideTag)
}
bounds := getBoundsProperty(properties, mainTag)
if bounds == nil {
bounds = NewBoundsProperty(nil)
}
if bounds.Set(sideTag, value) {
properties.setRaw(mainTag, bounds)
return []PropertyName{mainTag, sideTag}
}
notCompatibleType(sideTag, value)
return nil
}
func getBoundsProperty(properties Properties, tag PropertyName) BoundsProperty {
if value := properties.getRaw(tag); value != nil {
switch value := value.(type) {
case string:
bounds := NewBoundsProperty(nil)
for _, t := range []string{Top, Right, Bottom, Left} {
for _, t := range []PropertyName{Top, Right, Bottom, Left} {
bounds.Set(t, value)
}
return bounds
case SizeUnit:
bounds := NewBoundsProperty(nil)
for _, t := range []string{Top, Right, Bottom, Left} {
for _, t := range []PropertyName{Top, Right, Bottom, Left} {
bounds.Set(t, value)
}
return bounds
@ -342,29 +337,10 @@ func (properties *propertyList) boundsProperty(tag string) BoundsProperty {
}
}
return NewBoundsProperty(nil)
return nil
}
func (properties *propertyList) removeBoundsSide(mainTag, sideTag string) {
bounds := properties.boundsProperty(mainTag)
if bounds.Get(sideTag) != nil {
bounds.Remove(sideTag)
properties.properties[mainTag] = bounds
}
}
func (properties *propertyList) setBoundsSide(mainTag, sideTag string, value any) bool {
bounds := properties.boundsProperty(mainTag)
if bounds.Set(sideTag, value) {
properties.properties[mainTag] = bounds
return true
}
notCompatibleType(sideTag, value)
return false
}
func boundsProperty(properties Properties, tag string, session Session) (Bounds, bool) {
func getBounds(properties Properties, tag PropertyName, session Session) (Bounds, bool) {
if value := properties.Get(tag); value != nil {
switch value := value.(type) {
case string:

View File

@ -1,6 +1,6 @@
package rui
// Button - button view
// Button represent a Button view
type Button interface {
CustomView
}
@ -18,6 +18,7 @@ func NewButton(session Session, params Params) Button {
func newButton(session Session) View {
return NewButton(session, nil)
//return new(buttonData)
}
func (button *buttonData) CreateSuperView(session Session) View {

207
canvas.go
View File

@ -6,38 +6,54 @@ import (
"strings"
)
// LineJoin is the type for setting the shape used to join two line segments where they meet.
type LineJoin int
// LineCap is the type for setting the shape used to draw the end points of lines.
type LineCap int
// Constants related to canvas view operations
const (
// MiterJoin - Connected segments are joined by extending their outside edges
// to connect at a single point, with the effect of filling an additional
// lozenge-shaped area. This setting is affected by the miterLimit property
MiterJoin = 0
MiterJoin LineJoin = 0
// RoundJoin - rounds off the corners of a shape by filling an additional sector
// of disc centered at the common endpoint of connected segments.
// The radius for these rounded corners is equal to the line width.
RoundJoin = 1
RoundJoin LineJoin = 1
// BevelJoin - Fills an additional triangular area between the common endpoint
// of connected segments, and the separate outside rectangular corners of each segment.
BevelJoin = 2
BevelJoin LineJoin = 2
// ButtCap - the ends of lines are squared off at the endpoints. Default value.
ButtCap = 0
ButtCap LineCap = 0
// RoundCap - the ends of lines are rounded.
RoundCap = 1
RoundCap LineCap = 1
// SquareCap - the ends of lines are squared off by adding a box with an equal width
// and half the height of the line's thickness.
SquareCap = 2
SquareCap LineCap = 2
// AlphabeticBaseline - the text baseline is the normal alphabetic baseline. Default value.
AlphabeticBaseline = 0
// TopBaseline - the text baseline is the top of the em square.
TopBaseline = 1
// MiddleBaseline - the text baseline is the middle of the em square.
MiddleBaseline = 2
// BottomBaseline - the text baseline is the bottom of the bounding box.
// This differs from the ideographic baseline in that the ideographic baseline doesn't consider descenders.
BottomBaseline = 3
// HangingBaseline - the text baseline is the hanging baseline. (Used by Tibetan and other Indic scripts.)
HangingBaseline = 4
// IdeographicBaseline - the text baseline is the ideographic baseline; this is
// the bottom of the body of the characters, if the main body of characters protrudes
// beneath the alphabetic baseline. (Used by Chinese, Japanese, and Korean scripts.)
@ -46,6 +62,7 @@ const (
// StartAlign - the text is aligned at the normal start of the line (left-aligned
// for left-to-right locales, right-aligned for right-to-left locales).
StartAlign = 3
// EndAlign - the text is aligned at the normal end of the line (right-aligned
// for left-to-right locales, left-aligned for right-to-left locales).
EndAlign = 4
@ -91,7 +108,7 @@ type TextMetrics struct {
Right float64
}
// Canvas is a drawing interface
// Canvas is a drawing interface used by the [CanvasView]
type Canvas interface {
// View return the view for the drawing
View() CanvasView
@ -112,15 +129,15 @@ type Canvas interface {
ClipPath(path Path)
// SetScale adds a scaling transformation to the canvas units horizontally and/or vertically.
// x - scaling factor in the horizontal direction. A negative value flips pixels across
// * x - scaling factor in the horizontal direction. A negative value flips pixels across
// the vertical axis. A value of 1 results in no horizontal scaling;
// y - scaling factor in the vertical direction. A negative value flips pixels across
// * y - scaling factor in the vertical direction. A negative value flips pixels across
// the horizontal axis. A value of 1 results in no vertical scaling.
SetScale(x, y float64)
// SetTranslation adds a translation transformation to the current matrix.
// x - distance to move in the horizontal direction. Positive values are to the right, and negative to the left;
// y - distance to move in the vertical direction. Positive values are down, and negative are up.
// * x - distance to move in the horizontal direction. Positive values are to the right, and negative to the left;
// * y - distance to move in the vertical direction. Positive values are down, and negative are up.
SetTranslation(x, y float64)
// SetRotation adds a rotation to the transformation matrix.
@ -130,12 +147,13 @@ type Canvas interface {
// SetTransformation multiplies the current transformation with the matrix described by the arguments
// of this method. This lets you scale, rotate, translate (move), and skew the context.
// The transformation matrix is described by:
// ⎡ xScale xSkew dx ⎤
// ⎢ ySkew yScale dy ⎥
// ⎣ 0 0 1 ⎦
// xScale, yScale - horizontal and vertical scaling. A value of 1 results in no scaling;
// xSkew, ySkew - horizontal and vertical skewing;
// dx, dy - horizontal and vertical translation (moving).
// ⎡ xScale xSkew dx ⎤
// ⎢ ySkew yScale dy ⎥
// ⎣ 0 0 1 ⎦
// where
// * xScale, yScale - horizontal and vertical scaling. A value of 1 results in no scaling;
// * xSkew, ySkew - horizontal and vertical skewing;
// * dx, dy - horizontal and vertical translation (moving).
SetTransformation(xScale, yScale, xSkew, ySkew, dx, dy float64)
// ResetTransformation resets the current transform to the identity matrix
@ -148,45 +166,64 @@ type Canvas interface {
SetSolidColorStrokeStyle(color Color)
// SetLinearGradientFillStyle sets a gradient along the line connecting two given coordinates to use inside shapes
// x0, y0 - coordinates of the start point;
// x1, y1 - coordinates of the end point;
// startColor, endColor - the start and end color
// stopPoints - the array of stop points
// * x0, y0 - coordinates of the start point;
// * x1, y1 - coordinates of the end point;
// * startColor, endColor - the start and end color
// * stopPoints - the array of stop points
SetLinearGradientFillStyle(x0, y0 float64, color0 Color, x1, y1 float64, color1 Color, stopPoints []GradientPoint)
// SetLinearGradientStrokeStyle sets a gradient along the line connecting two given coordinates to use for the strokes (outlines) around shapes
// x0, y0 - coordinates of the start point;
// x1, y1 - coordinates of the end point;
// color0, color1 - the start and end color
// stopPoints - the array of stop points
// * x0, y0 - coordinates of the start point;
// * x1, y1 - coordinates of the end point;
// * color0, color1 - the start and end color
// * stopPoints - the array of stop points
SetLinearGradientStrokeStyle(x0, y0 float64, color0 Color, x1, y1 float64, color1 Color, stopPoints []GradientPoint)
// SetRadialGradientFillStyle sets a radial gradient using the size and coordinates of two circles
// to use inside shapes
// x0, y0 - coordinates of the center of the start circle;
// r0 - the radius of the start circle;
// x1, y1 - coordinates the center of the end circle;
// r1 - the radius of the end circle;
// color0, color1 - the start and end color
// stopPoints - the array of stop points
// * x0, y0 - coordinates of the center of the start circle;
// * r0 - the radius of the start circle;
// * x1, y1 - coordinates the center of the end circle;
// * r1 - the radius of the end circle;
// * color0, color1 - the start and end color
// * stopPoints - the array of stop points
SetRadialGradientFillStyle(x0, y0, r0 float64, color0 Color, x1, y1, r1 float64, color1 Color, stopPoints []GradientPoint)
// SetRadialGradientStrokeStyle sets a radial gradient using the size and coordinates of two circles
// to use for the strokes (outlines) around shapes
// x0, y0 - coordinates of the center of the start circle;
// r0 - the radius of the start circle;
// x1, y1 - coordinates the center of the end circle;
// r1 - the radius of the end circle;
// color0, color1 - the start and end color
// stopPoints - the array of stop points
// * x0, y0 - coordinates of the center of the start circle;
// * r0 - the radius of the start circle;
// * x1, y1 - coordinates the center of the end circle;
// * r1 - the radius of the end circle;
// * color0, color1 - the start and end color
// * stopPoints - the array of stop points
SetRadialGradientStrokeStyle(x0, y0, r0 float64, color0 Color, x1, y1, r1 float64, color1 Color, stopPoints []GradientPoint)
// SetConicGradientFillStyle sets a conic gradient around a point
// to use inside shapes
// * x, y - coordinates of the center of the conic gradient in pilels;
// * startAngle - the angle at which to begin the gradient, in radians. The angle starts from a line going horizontally right from the center, and proceeds clockwise.
// * startColor - the start color;
// * endColor - the end color;
// * stopPoints - the array of stop points. The Pos field of GradientPoint, in the range from 0 to 1, specifies the angle in turns.
SetConicGradientFillStyle(x, y, startAngle float64, startColor, endColor Color, stopPoints []GradientPoint)
// SetConicGradientFillStyle sets a conic gradient around a point
// to use inside shapes
// * x, y - coordinates of the center of the conic gradient in pilels;
// * startAngle - the angle at which to begin the gradient, in radians. The angle starts from a line going horizontally right from the center, and proceeds clockwise.
// * startColor - the start color;
// * endColor - the end color;
// * stopPoints - the array of stop points. The Pos field of GradientPoint, in the range from 0 to 1, specifies the angle in turns.
SetConicGradientStrokeStyle(x, y, startAngle float64, startColor, endColor Color, stopPoints []GradientPoint)
// SetImageFillStyle set the image as the filling pattern.
// repeat - indicating how to repeat the pattern's image. Possible values are:
// NoRepeat (0) - neither direction,
// RepeatXY (1) - both directions,
// RepeatX (2) - horizontal only,
// RepeatY (3) - vertical only.
//
// repeat - indicating how to repeat the pattern's image. Possible values are:
// * NoRepeat (0) - neither direction,
// * RepeatXY (1) - both directions,
// * RepeatX (2) - horizontal only,
// * RepeatY (3) - vertical only.
SetImageFillStyle(image Image, repeat int)
// SetLineWidth the line width, in coordinate space units. Zero, negative, Infinity, and NaN values are ignored.
@ -194,11 +231,11 @@ type Canvas interface {
// SetLineJoin sets the shape used to join two line segments where they meet.
// Valid values: MiterJoin (0), RoundJoin (1), BevelJoin (2). All other values are ignored.
SetLineJoin(join int)
SetLineJoin(join LineJoin)
// SetLineJoin sets the shape used to draw the end points of lines.
// Valid values: ButtCap (0), RoundCap (1), SquareCap (2). All other values are ignored.
SetLineCap(cap int)
SetLineCap(cap LineCap)
// SetLineDash sets the line dash pattern used when stroking lines.
// dash - an array of values that specify alternating lengths of lines and gaps which describe the pattern.
@ -223,51 +260,67 @@ type Canvas interface {
SetTextAlign(align int)
// SetShadow sets shadow parameters:
// offsetX, offsetY - the distance that shadows will be offset horizontally and vertically;
// blur - the amount of blur applied to shadows. Must be non-negative;
// color - the color of shadows.
// * offsetX, offsetY - the distance that shadows will be offset horizontally and vertically;
// * blur - the amount of blur applied to shadows. Must be non-negative;
// * color - the color of shadows.
SetShadow(offsetX, offsetY, blur float64, color Color)
// ResetShadow sets shadow parameters to default values (invisible shadow)
ResetShadow()
// ClearRect erases the pixels in a rectangular area by setting them to transparent black
ClearRect(x, y, width, height float64)
// FillRect draws a rectangle that is filled according to the current FillStyle.
FillRect(x, y, width, height float64)
// StrokeRect draws a rectangle that is stroked (outlined) according to the current strokeStyle
// and other context settings
StrokeRect(x, y, width, height float64)
// FillAndStrokeRect draws a rectangle that is filled according to the current FillStyle and
// is stroked (outlined) according to the current strokeStyle and other context settings
FillAndStrokeRect(x, y, width, height float64)
// FillRoundedRect draws a rounded rectangle that is filled according to the current FillStyle.
FillRoundedRect(x, y, width, height, r float64)
// StrokeRoundedRect draws a rounded rectangle that is stroked (outlined) according
// to the current strokeStyle and other context settings
StrokeRoundedRect(x, y, width, height, r float64)
// FillAndStrokeRoundedRect draws a rounded rectangle that is filled according to the current FillStyle
// and is stroked (outlined) according to the current strokeStyle and other context settings
FillAndStrokeRoundedRect(x, y, width, height, r float64)
// FillEllipse draws a ellipse that is filled according to the current FillStyle.
// x, y - coordinates of the ellipse's center;
// radiusX - the ellipse's major-axis radius. Must be non-negative;
// radiusY - the ellipse's minor-axis radius. Must be non-negative;
// rotation - the rotation of the ellipse, expressed in radians.
// * x, y - coordinates of the ellipse's center;
// * radiusX - the ellipse's major-axis radius. Must be non-negative;
// * radiusY - the ellipse's minor-axis radius. Must be non-negative;
// * rotation - the rotation of the ellipse, expressed in radians.
FillEllipse(x, y, radiusX, radiusY, rotation float64)
// StrokeRoundedRect draws a ellipse that is stroked (outlined) according
// to the current strokeStyle and other context settings
StrokeEllipse(x, y, radiusX, radiusY, rotation float64)
// FillAndStrokeEllipse draws a ellipse that is filled according to the current FillStyle
// and is stroked (outlined) according to the current strokeStyle and other context settings
FillAndStrokeEllipse(x, y, radiusX, radiusY, rotation float64)
// NewPath creates a new Path object
NewPath() Path
// NewPathFromSvg creates a new Path and initialize it by a string consisting of SVG path data
NewPathFromSvg(data string) Path
// FillPath draws a path that is filled according to the current FillStyle.
FillPath(path Path)
// StrokePath draws a path that is stroked (outlined) according to the current strokeStyle
// and other context settings
StrokePath(path Path)
// FillAndStrokeRect draws a path that is filled according to the current FillStyle and
// is stroked (outlined) according to the current strokeStyle and other context settings
FillAndStrokePath(path Path)
@ -278,14 +331,17 @@ type Canvas interface {
// FillText draws a text string at the specified coordinates, filling the string's characters
// with the current FillStyle
FillText(x, y float64, text string)
// StrokeText strokes — that is, draws the outlines of — the characters of a text string
// at the specified coordinates
StrokeText(x, y float64, text string)
// DrawImage draws the image at the (x, y) position
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(x, y, width, height float64, image 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
DrawImageFragment(srcX, srcY, srcWidth, srcHeight, dstX, dstY, dstWidth, dstHeight float64, image Image)
@ -299,10 +355,13 @@ type canvasData struct {
}
func newCanvas(view CanvasView) Canvas {
session := view.Session()
if !session.canvasStart(view.htmlID()) {
return nil
}
canvas := new(canvasData)
canvas.view = view
canvas.session = view.Session()
canvas.session.canvasStart(view.htmlID())
canvas.session = session
return canvas
}
@ -343,8 +402,7 @@ func (canvas *canvasData) ClipRect(x, y, width, height float64) {
}
func (canvas *canvasData) ClipPath(path Path) {
path.create(canvas.session)
canvas.session.callCanvasFunc("clip")
canvas.session.callCanvasFunc("clip", path.obj())
}
func (canvas *canvasData) SetScale(x, y float64) {
@ -427,6 +485,30 @@ func (canvas *canvasData) SetRadialGradientStrokeStyle(x0, y0, r0 float64, color
canvas.session.updateCanvasProperty("strokeStyle", gradient)
}
func (canvas *canvasData) createConicGradient(x, y, startAngle float64, startColor, endColor Color, stopPoints []GradientPoint) any {
gradient := canvas.session.createCanvasVar("createConicGradient", startAngle, x, y)
canvas.session.callCanvasVarFunc(gradient, "addColorStop", 0, startColor.cssString())
for _, point := range stopPoints {
if point.Offset >= 0 && point.Offset <= 1 {
canvas.session.callCanvasVarFunc(gradient, "addColorStop", point.Offset, point.Color.cssString())
}
}
canvas.session.callCanvasVarFunc(gradient, "addColorStop", 1, endColor.cssString())
return gradient
}
func (canvas *canvasData) SetConicGradientFillStyle(x, y, startAngle float64, startColor, endColor Color, stopPoints []GradientPoint) {
gradient := canvas.createConicGradient(x, y, startAngle, startColor, endColor, stopPoints)
canvas.session.updateCanvasProperty("fillStyle", gradient)
}
func (canvas *canvasData) SetConicGradientStrokeStyle(x, y, startAngle float64, startColor, endColor Color, stopPoints []GradientPoint) {
gradient := canvas.createConicGradient(x, y, startAngle, startColor, endColor, stopPoints)
canvas.session.updateCanvasProperty("strokeStyle", gradient)
}
func (canvas *canvasData) SetImageFillStyle(image Image, repeat int) {
if image == nil || image.LoadingStatus() != ImageReady {
return
@ -459,7 +541,7 @@ func (canvas *canvasData) SetLineWidth(width float64) {
}
}
func (canvas *canvasData) SetLineJoin(join int) {
func (canvas *canvasData) SetLineJoin(join LineJoin) {
switch join {
case MiterJoin:
canvas.session.updateCanvasProperty("lineJoin", "miter")
@ -472,7 +554,7 @@ func (canvas *canvasData) SetLineJoin(join int) {
}
}
func (canvas *canvasData) SetLineCap(cap int) {
func (canvas *canvasData) SetLineCap(cap LineCap) {
switch cap {
case ButtCap:
canvas.session.updateCanvasProperty("lineCap", "butt")
@ -776,19 +858,16 @@ func (canvas *canvasData) StrokeText(x, y float64, text string) {
}
func (canvas *canvasData) FillPath(path Path) {
path.create(canvas.session)
canvas.session.callCanvasFunc("fill")
canvas.session.callCanvasFunc("fill", path.obj())
}
func (canvas *canvasData) StrokePath(path Path) {
path.create(canvas.session)
canvas.session.callCanvasFunc("stroke")
canvas.session.callCanvasFunc("stroke", path.obj())
}
func (canvas *canvasData) FillAndStrokePath(path Path) {
path.create(canvas.session)
canvas.session.callCanvasFunc("fill")
canvas.session.callCanvasFunc("stroke")
canvas.session.callCanvasFunc("fill", path.obj())
canvas.session.callCanvasFunc("stroke", path.obj())
}
func (canvas *canvasData) DrawLine(x0, y0, x1, y1 float64) {

View File

@ -1,21 +1,23 @@
package rui
import "strings"
// DrawFunction is the constant for the "draw-function" property tag.
// The "draw-function" property sets the draw function of CanvasView.
// The function should have the following format: func(Canvas)
const DrawFunction = "draw-function"
// DrawFunction is the constant for "draw-function" property tag.
//
// Used by `CanvasView`.
// Property sets the draw function of `CanvasView`.
//
// Supported types: `func(Canvas)`.
const DrawFunction PropertyName = "draw-function"
// CanvasView interface of a custom draw view
type CanvasView interface {
View
// Redraw forces CanvasView to redraw its content
Redraw()
}
type canvasViewData struct {
viewData
drawer func(Canvas)
}
// NewCanvasView creates the new custom draw view
@ -27,21 +29,21 @@ func NewCanvasView(session Session, params Params) CanvasView {
}
func newCanvasView(session Session) View {
return NewCanvasView(session, nil)
return new(canvasViewData)
}
// Init initialize fields of ViewsContainer by default values
func (canvasView *canvasViewData) init(session Session) {
canvasView.viewData.init(session)
canvasView.tag = "CanvasView"
canvasView.normalize = normalizeCanvasViewTag
canvasView.set = canvasView.setFunc
canvasView.remove = canvasView.removeFunc
canvasView.changed = canvasView.propertyChanged
}
func (canvasView *canvasViewData) String() string {
return getViewString(canvasView, nil)
}
func (canvasView *canvasViewData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeCanvasViewTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case "draw-func":
tag = DrawFunction
@ -49,51 +51,39 @@ func (canvasView *canvasViewData) normalizeTag(tag string) string {
return tag
}
func (canvasView *canvasViewData) Remove(tag string) {
canvasView.remove(canvasView.normalizeTag(tag))
}
func (canvasView *canvasViewData) remove(tag string) {
func (canvasView *canvasViewData) removeFunc(tag PropertyName) []PropertyName {
if tag == DrawFunction {
canvasView.drawer = nil
canvasView.Redraw()
canvasView.propertyChangedEvent(tag)
} else {
canvasView.viewData.remove(tag)
if canvasView.getRaw(DrawFunction) != nil {
canvasView.setRaw(DrawFunction, nil)
canvasView.Redraw()
return []PropertyName{DrawFunction}
}
return []PropertyName{}
}
return canvasView.viewData.removeFunc(tag)
}
func (canvasView *canvasViewData) Set(tag string, value any) bool {
return canvasView.set(canvasView.normalizeTag(tag), value)
}
func (canvasView *canvasViewData) set(tag string, value any) bool {
func (canvasView *canvasViewData) setFunc(tag PropertyName, value any) []PropertyName {
if tag == DrawFunction {
if value == nil {
canvasView.drawer = nil
} else if fn, ok := value.(func(Canvas)); ok {
canvasView.drawer = fn
if fn, ok := value.(func(Canvas)); ok {
canvasView.setRaw(DrawFunction, fn)
} else {
notCompatibleType(tag, value)
return false
return nil
}
canvasView.Redraw()
canvasView.propertyChangedEvent(tag)
return true
return []PropertyName{DrawFunction}
}
return canvasView.viewData.set(tag, value)
return canvasView.viewData.setFunc(tag, value)
}
func (canvasView *canvasViewData) Get(tag string) any {
return canvasView.get(canvasView.normalizeTag(tag))
}
func (canvasView *canvasViewData) get(tag string) any {
func (canvasView *canvasViewData) propertyChanged(tag PropertyName) {
if tag == DrawFunction {
return canvasView.drawer
canvasView.Redraw()
} else {
canvasView.viewData.propertyChanged(tag)
}
return canvasView.viewData.get(tag)
}
func (canvasView *canvasViewData) htmlTag() string {
@ -101,14 +91,14 @@ func (canvasView *canvasViewData) htmlTag() string {
}
func (canvasView *canvasViewData) Redraw() {
if canvasView.drawer != nil {
canvas := newCanvas(canvasView)
canvas.ClearRect(0, 0, canvasView.frame.Width, canvasView.frame.Height)
if canvasView.drawer != nil {
canvasView.drawer(canvas)
canvas := newCanvas(canvasView)
canvas.ClearRect(0, 0, canvasView.frame.Width, canvasView.frame.Height)
if value := canvasView.getRaw(DrawFunction); value != nil {
if drawer, ok := value.(func(Canvas)); ok {
drawer(canvas)
}
canvas.finishDraw()
}
canvas.finishDraw()
}
func (canvasView *canvasViewData) onResize(self View, x, y, width, height float64) {

View File

@ -5,183 +5,149 @@ import (
)
// CheckboxChangedEvent is the constant for "checkbox-event" property tag.
// The "checkbox-event" event occurs when the checkbox becomes checked/unchecked.
// The main listener format: func(Checkbox, bool), where the second argument is the checkbox state.
const CheckboxChangedEvent = "checkbox-event"
//
// Used by `Checkbox`.
// Event occurs when the checkbox becomes checked/unchecked.
//
// General listener format:
//
// func(checkbox rui.Checkbox, checked bool)
//
// where:
// - checkbox - Interface of a checkbox which generated this event,
// - checked - Checkbox state.
//
// Allowed listener formats:
//
// func(checkbox rui.Checkbox)
// func(checked bool)
// func()
const CheckboxChangedEvent PropertyName = "checkbox-event"
// Checkbox - checkbox view
// Checkbox represent a Checkbox view
type Checkbox interface {
ViewsContainer
}
type checkboxData struct {
viewsContainerData
checkedListeners []func(Checkbox, bool)
}
// NewCheckbox create new Checkbox object and return it
func NewCheckbox(session Session, params Params) Checkbox {
view := new(checkboxData)
view.init(session)
setInitParams(view, Params{
ClickEvent: checkboxClickListener,
KeyDownEvent: checkboxKeyListener,
})
setInitParams(view, params)
return view
}
func newCheckbox(session Session) View {
return NewCheckbox(session, nil)
return new(checkboxData)
}
func (button *checkboxData) init(session Session) {
button.viewsContainerData.init(session)
button.tag = "Checkbox"
button.systemClass = "ruiGridLayout ruiCheckbox"
button.checkedListeners = []func(Checkbox, bool){}
}
button.set = button.setFunc
button.remove = button.removeFunc
button.changed = button.propertyChanged
func (button *checkboxData) String() string {
return getViewString(button, nil)
button.setRaw(ClickEvent, []func(View, MouseEvent){checkboxClickListener})
button.setRaw(KeyDownEvent, []func(View, KeyEvent){checkboxKeyListener})
}
func (button *checkboxData) Focusable() bool {
return true
}
func (button *checkboxData) Get(tag string) any {
switch strings.ToLower(tag) {
case CheckboxChangedEvent:
return button.checkedListeners
}
return button.viewsContainerData.Get(tag)
}
func (button *checkboxData) Set(tag string, value any) bool {
return button.set(tag, value)
}
func (button *checkboxData) set(tag string, value any) bool {
func (button *checkboxData) propertyChanged(tag PropertyName) {
switch tag {
case CheckboxChangedEvent:
if !button.setChangedListener(value) {
notCompatibleType(tag, value)
return false
}
case Checked:
oldChecked := button.checked()
if !button.setBoolProperty(Checked, value) {
return false
}
if button.created {
checked := button.checked()
if checked != oldChecked {
button.changedCheckboxState(checked)
session := button.Session()
checked := IsCheckboxChecked(button)
if listeners := GetCheckboxChangedListeners(button); len(listeners) > 0 {
for _, listener := range listeners {
listener(button, checked)
}
}
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
checkboxHtml(button, buffer, checked)
session.updateInnerHTML(button.htmlID()+"checkbox", buffer.String())
case CheckboxHorizontalAlign, CheckboxVerticalAlign:
if !button.setEnumProperty(tag, value, enumProperties[tag].values) {
return false
}
if button.created {
htmlID := button.htmlID()
updateCSSStyle(htmlID, button.session)
updateInnerHTML(htmlID, button.session)
}
htmlID := button.htmlID()
session := button.Session()
updateCSSStyle(htmlID, session)
updateInnerHTML(htmlID, session)
case VerticalAlign:
if !button.setEnumProperty(tag, value, enumProperties[tag].values) {
return false
}
if button.created {
button.session.updateCSSProperty(button.htmlID()+"content", "align-items", button.cssVerticalAlign())
}
button.Session().updateCSSProperty(button.htmlID()+"content", "align-items", checkboxVerticalAlignCSS(button))
case HorizontalAlign:
if !button.setEnumProperty(tag, value, enumProperties[tag].values) {
return false
}
if button.created {
button.session.updateCSSProperty(button.htmlID()+"content", "justify-items", button.cssHorizontalAlign())
}
button.Session().updateCSSProperty(button.htmlID()+"content", "justify-items", checkboxHorizontalAlignCSS(button))
case CellVerticalAlign, CellHorizontalAlign, CellWidth, CellHeight:
return false
case AccentColor:
updateInnerHTML(button.htmlID(), button.Session())
default:
return button.viewsContainerData.set(tag, value)
button.viewsContainerData.propertyChanged(tag)
}
button.propertyChangedEvent(tag)
return true
}
func (button *checkboxData) Remove(tag string) {
button.remove(strings.ToLower(tag))
}
func (button *checkboxData) remove(tag string) {
func (button *checkboxData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case ClickEvent:
if !button.viewsContainerData.set(ClickEvent, checkboxClickListener) {
delete(button.properties, tag)
if listeners, ok := valueToOneArgEventListeners[View, MouseEvent](value); ok && listeners != nil {
listeners = append(listeners, checkboxClickListener)
button.setRaw(tag, listeners)
return []PropertyName{tag}
}
return nil
case KeyDownEvent:
if !button.viewsContainerData.set(KeyDownEvent, checkboxKeyListener) {
delete(button.properties, tag)
if listeners, ok := valueToOneArgEventListeners[View, KeyEvent](value); ok && listeners != nil {
listeners = append(listeners, checkboxKeyListener)
button.setRaw(tag, listeners)
return []PropertyName{tag}
}
return nil
case CheckboxChangedEvent:
if len(button.checkedListeners) > 0 {
button.checkedListeners = []func(Checkbox, bool){}
}
return setOneArgEventListener[Checkbox, bool](button, tag, value)
case Checked:
oldChecked := button.checked()
delete(button.properties, tag)
if button.created && oldChecked {
button.changedCheckboxState(false)
}
return setBoolProperty(button, Checked, value)
case CheckboxHorizontalAlign, CheckboxVerticalAlign:
delete(button.properties, tag)
if button.created {
htmlID := button.htmlID()
updateCSSStyle(htmlID, button.session)
updateInnerHTML(htmlID, button.session)
}
case VerticalAlign:
delete(button.properties, tag)
if button.created {
button.session.updateCSSProperty(button.htmlID()+"content", "align-items", button.cssVerticalAlign())
}
case HorizontalAlign:
delete(button.properties, tag)
if button.created {
button.session.updateCSSProperty(button.htmlID()+"content", "justify-items", button.cssHorizontalAlign())
}
default:
button.viewsContainerData.remove(tag)
return
case CellVerticalAlign, CellHorizontalAlign, CellWidth, CellHeight:
ErrorLogF(`"%s" property is not compatible with the BoundsProperty`, string(tag))
return nil
}
button.propertyChangedEvent(tag)
return button.viewsContainerData.setFunc(tag, value)
}
func (button *checkboxData) checked() bool {
checked, _ := boolProperty(button, Checked, button.Session())
return checked
func (button *checkboxData) removeFunc(tag PropertyName) []PropertyName {
switch tag {
case ClickEvent:
button.setRaw(ClickEvent, []func(View, MouseEvent){checkboxClickListener})
return []PropertyName{ClickEvent}
case KeyDownEvent:
button.setRaw(KeyDownEvent, []func(View, KeyEvent){checkboxKeyListener})
return []PropertyName{ClickEvent}
}
return button.viewsContainerData.removeFunc(tag)
}
/*
func (button *checkboxData) changedCheckboxState(state bool) {
for _, listener := range button.checkedListeners {
for _, listener := range GetCheckboxChangedListeners(button) {
listener(button, state)
}
@ -191,8 +157,9 @@ func (button *checkboxData) changedCheckboxState(state bool) {
button.htmlCheckbox(buffer, state)
button.Session().updateInnerHTML(button.htmlID()+"checkbox", buffer.String())
}
*/
func checkboxClickListener(view View) {
func checkboxClickListener(view View, _ MouseEvent) {
view.Set(Checked, !IsCheckboxChecked(view))
BlurView(view)
}
@ -204,17 +171,6 @@ func checkboxKeyListener(view View, event KeyEvent) {
}
}
func (button *checkboxData) setChangedListener(value any) bool {
listeners, ok := valueToEventListeners[Checkbox, bool](value)
if !ok {
return false
} else if listeners == nil {
listeners = []func(Checkbox, bool){}
}
button.checkedListeners = listeners
return true
}
func (button *checkboxData) cssStyle(self View, builder cssBuilder) {
session := button.Session()
vAlign := GetCheckboxVerticalAlign(button)
@ -241,10 +197,11 @@ func (button *checkboxData) cssStyle(self View, builder cssBuilder) {
builder.add("align-items", "stretch")
builder.add("justify-items", "stretch")
button.viewsContainerData.cssStyle(self, builder)
button.viewData.cssStyle(self, builder)
}
func (button *checkboxData) htmlCheckbox(buffer *strings.Builder, checked bool) (int, int) {
func checkboxHtml(button View, buffer *strings.Builder, checked bool) (int, int) {
//func (button *checkboxData) htmlCheckbox(buffer *strings.Builder, checked bool) (int, int) {
vAlign := GetCheckboxVerticalAlign(button)
hAlign := GetCheckboxHorizontalAlign(button)
@ -278,10 +235,16 @@ func (button *checkboxData) htmlCheckbox(buffer *strings.Builder, checked bool)
}
buffer.WriteString(`">`)
accentColor := Color(0)
if color := GetAccentColor(button, ""); color != 0 {
accentColor = color
}
if checked {
buffer.WriteString(button.Session().checkboxOnImage())
buffer.WriteString(button.Session().checkboxOnImage(accentColor))
} else {
buffer.WriteString(button.Session().checkboxOffImage())
buffer.WriteString(button.Session().checkboxOffImage(accentColor))
}
buffer.WriteString(`</div>`)
@ -290,7 +253,7 @@ func (button *checkboxData) htmlCheckbox(buffer *strings.Builder, checked bool)
func (button *checkboxData) htmlSubviews(self View, buffer *strings.Builder) {
vCheckboxAlign, hCheckboxAlign := button.htmlCheckbox(buffer, IsCheckboxChecked(button))
vCheckboxAlign, hCheckboxAlign := checkboxHtml(button, buffer, IsCheckboxChecked(button))
buffer.WriteString(`<div id="`)
buffer.WriteString(button.htmlID())
@ -308,11 +271,11 @@ func (button *checkboxData) htmlSubviews(self View, buffer *strings.Builder) {
}
buffer.WriteString(" align-items: ")
buffer.WriteString(button.cssVerticalAlign())
buffer.WriteString(checkboxVerticalAlignCSS(button))
buffer.WriteRune(';')
buffer.WriteString(" justify-items: ")
buffer.WriteString(button.cssHorizontalAlign())
buffer.WriteString(checkboxHorizontalAlignCSS(button))
buffer.WriteRune(';')
buffer.WriteString(`">`)
@ -320,8 +283,8 @@ func (button *checkboxData) htmlSubviews(self View, buffer *strings.Builder) {
buffer.WriteString(`</div>`)
}
func (button *checkboxData) cssHorizontalAlign() string {
align := GetHorizontalAlign(button)
func checkboxHorizontalAlignCSS(view View) string {
align := GetHorizontalAlign(view)
values := enumProperties[CellHorizontalAlign].cssValues
if align >= 0 && align < len(values) {
return values[align]
@ -329,8 +292,8 @@ func (button *checkboxData) cssHorizontalAlign() string {
return values[0]
}
func (button *checkboxData) cssVerticalAlign() string {
align := GetVerticalAlign(button)
func checkboxVerticalAlignCSS(view View) string {
align := GetVerticalAlign(view)
values := enumProperties[CellVerticalAlign].cssValues
if align >= 0 && align < len(values) {
return values[align]
@ -355,3 +318,10 @@ func GetCheckboxVerticalAlign(view View, subviewID ...string) int {
func GetCheckboxHorizontalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, CheckboxHorizontalAlign, TopAlign, false)
}
// GetCheckboxChangedListeners returns the CheckboxChangedListener list of an Checkbox subview.
// 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.
func GetCheckboxChangedListeners(view View, subviewID ...string) []func(Checkbox, bool) {
return getOneArgEventListeners[Checkbox, bool](view, subviewID, CheckboxChangedEvent)
}

690
clipShape.go Normal file
View File

@ -0,0 +1,690 @@
package rui
import (
"fmt"
"strings"
)
type ClipShape string
const (
InsetClip ClipShape = "inset"
CircleClip ClipShape = "circle"
EllipseClip ClipShape = "ellipse"
PolygonClip ClipShape = "polygon"
)
// ClipShapeProperty defines a View clipping area
type ClipShapeProperty interface {
Properties
fmt.Stringer
stringWriter
// Shape returns the clip shape type
Shape() ClipShape
cssStyle(session Session) string
valid(session Session) bool
}
type insetClipData struct {
dataProperty
}
type ellipseClipData struct {
dataProperty
}
type circleClipData struct {
dataProperty
}
type polygonClipData struct {
dataProperty
}
// NewClipShapeProperty creates ClipShapeProperty.
//
// The following properties can be used for shapes:
//
// InsetClip:
// - "top" (Top) - offset (SizeUnit) from the top border of a View;
// - "right" (Right) - offset (SizeUnit) from the right border of a View;
// - "bottom" (Bottom) - offset (SizeUnit) from the bottom border of a View;
// - "left" (Left) - offset (SizeUnit) from the left border of a View;
// - "radius" (Radius) - corner radius (RadiusProperty).
//
// CircleClip:
// - "x" (X) - x-axis position (SizeUnit) of the circle clip center;
// - "y" (Y) - y-axis position (SizeUnit) of the circle clip center;
// - "radius" (Radius) - radius (SizeUnit) of the circle clip center.
//
// EllipseClip:
// - "x" (X) - x-axis position (SizeUnit) of the ellipse clip center;
// - "y" (Y) - y-axis position (SizeUnit) of the ellipse clip center;
// - "radius-x" (RadiusX) - x-axis radius (SizeUnit) of the ellipse clip center;
// - "radius-y" (RadiusY) - y-axis radius (SizeUnit) of the ellipse clip center.
//
// PolygonClip:
// - "points" (Points) - an array ([]SizeUnit) of corner points of the polygon in the following order: x1, y1, x2, y2, ….
//
// The function will return nil if no properties are specified, unsupported properties are specified, or at least one property has an invalid value.
func NewClipShapeProperty(shape ClipShape, params Params) ClipShapeProperty {
if len(params) == 0 {
ErrorLog("No ClipShapeProperty params")
return nil
}
var result ClipShapeProperty
switch shape {
case InsetClip:
clip := new(insetClipData)
clip.init()
result = clip
case CircleClip:
clip := new(circleClipData)
clip.init()
result = clip
case EllipseClip:
clip := new(ellipseClipData)
clip.init()
result = clip
case PolygonClip:
clip := new(polygonClipData)
clip.init()
result = clip
default:
ErrorLog("Unknown ClipShape: " + string(shape))
return nil
}
for tag, value := range params {
if !result.Set(tag, value) {
return nil
}
}
return result
}
// NewInsetClip creates a rectangle View clipping area.
// - top - offset from the top border of a View;
// - right - offset from the right border of a View;
// - bottom - offset from the bottom border of a View;
// - left - offset from the left border of a View;
// - radius - corner radius, pass nil if you don't need to round corners
func NewInsetClip(top, right, bottom, left SizeUnit, radius RadiusProperty) ClipShapeProperty {
clip := new(insetClipData)
clip.init()
clip.setRaw(Top, top)
clip.setRaw(Right, right)
clip.setRaw(Bottom, bottom)
clip.setRaw(Left, left)
if radius != nil {
clip.setRaw(Radius, radius)
}
return clip
}
// NewCircleClip creates a circle View clipping area.
// - x - x-axis position of the circle clip center;
// - y - y-axis position of the circle clip center;
// - radius - radius of the circle clip center.
func NewCircleClip(x, y, radius SizeUnit) ClipShapeProperty {
clip := new(circleClipData)
clip.init()
clip.setRaw(X, x)
clip.setRaw(Y, y)
clip.setRaw(Radius, radius)
return clip
}
// NewEllipseClip creates a ellipse View clipping area.
// - x - x-axis position of the ellipse clip center;
// - y - y-axis position of the ellipse clip center;
// - rx - x-axis radius of the ellipse clip center;
// - ry - y-axis radius of the ellipse clip center.
func NewEllipseClip(x, y, rx, ry SizeUnit) ClipShapeProperty {
clip := new(ellipseClipData)
clip.init()
clip.setRaw(X, x)
clip.setRaw(Y, y)
clip.setRaw(RadiusX, rx)
clip.setRaw(RadiusY, ry)
return clip
}
// NewPolygonClip creates a polygon View clipping area.
// - points - an array of corner points of the polygon in the following order: x1, y1, x2, y2, …
//
// The elements of the function argument can be or text constants,
// or the text representation of SizeUnit, or elements of SizeUnit type.
func NewPolygonClip(points []any) ClipShapeProperty {
clip := new(polygonClipData)
clip.init()
if polygonClipDataSet(clip, Points, points) != nil {
return clip
}
return nil
}
// NewPolygonPointsClip creates a polygon View clipping area.
// - points - an array of corner points of the polygon in the following order: x1, y1, x2, y2, …
func NewPolygonPointsClip(points []SizeUnit) ClipShapeProperty {
clip := new(polygonClipData)
clip.init()
if polygonClipDataSet(clip, Points, points) != nil {
return clip
}
return nil
}
func (clip *insetClipData) init() {
clip.dataProperty.init()
clip.set = insetClipDataSet
clip.supportedProperties = []PropertyName{
Top, Right, Bottom, Left, Radius,
RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY,
RadiusTopRight, RadiusTopRightX, RadiusTopRightY,
RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY,
RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY,
}
}
func (clip *insetClipData) Shape() ClipShape {
return InsetClip
}
func insetClipDataSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case Top, Right, Bottom, Left:
return setSizeProperty(properties, tag, value)
case Radius:
return setRadiusProperty(properties, value)
case RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY,
RadiusTopRight, RadiusTopRightX, RadiusTopRightY,
RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY,
RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY:
if setRadiusPropertyElement(properties, tag, value) {
return []PropertyName{tag, Radius}
}
return nil
}
ErrorLogF(`"%s" property is not supported by the inset clip shape`, tag)
return nil
}
func (clip *insetClipData) String() string {
return runStringWriter(clip)
}
func (clip *insetClipData) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("inset { ")
comma := false
for _, tag := range []PropertyName{Top, Right, Bottom, Left, Radius} {
if value, ok := clip.properties[tag]; ok {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
writePropertyValue(buffer, tag, value, indent)
comma = true
}
}
buffer.WriteString(" }")
}
func (clip *insetClipData) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
leadText := "inset("
for _, tag := range []PropertyName{Top, Right, Bottom, Left} {
value, _ := sizeProperty(clip, tag, session)
buffer.WriteString(leadText)
buffer.WriteString(value.cssString("0px", session))
leadText = " "
}
if radius := getRadiusProperty(clip); radius != nil {
buffer.WriteString(" round ")
buffer.WriteString(radius.BoxRadius(session).cssString(session))
}
buffer.WriteRune(')')
return buffer.String()
}
func (clip *insetClipData) valid(session Session) bool {
for _, tag := range []PropertyName{Top, Right, Bottom, Left, Radius, RadiusX, RadiusY} {
if value, ok := sizeProperty(clip, tag, session); ok && value.Type != Auto && value.Value != 0 {
return true
}
}
return false
}
func (clip *circleClipData) init() {
clip.dataProperty.init()
clip.set = circleClipDataSet
clip.supportedProperties = []PropertyName{X, Y, Radius}
}
func (clip *circleClipData) Shape() ClipShape {
return CircleClip
}
func circleClipDataSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case X, Y, Radius:
return setSizeProperty(properties, tag, value)
}
ErrorLogF(`"%s" property is not supported by the circle clip shape`, tag)
return nil
}
func (clip *circleClipData) String() string {
return runStringWriter(clip)
}
func (clip *circleClipData) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("circle { ")
comma := false
for _, tag := range []PropertyName{Radius, X, Y} {
if value, ok := clip.properties[tag]; ok {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
writePropertyValue(buffer, tag, value, indent)
comma = true
}
}
buffer.WriteString(" }")
}
func (clip *circleClipData) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString("circle(")
r, _ := sizeProperty(clip, Radius, session)
buffer.WriteString(r.cssString("50%", session))
buffer.WriteString(" at ")
x, _ := sizeProperty(clip, X, session)
buffer.WriteString(x.cssString("50%", session))
buffer.WriteRune(' ')
y, _ := sizeProperty(clip, Y, session)
buffer.WriteString(y.cssString("50%", session))
buffer.WriteRune(')')
return buffer.String()
}
func (clip *circleClipData) valid(session Session) bool {
if value, ok := sizeProperty(clip, Radius, session); ok && value.Value == 0 {
return false
}
return true
}
func (clip *ellipseClipData) init() {
clip.dataProperty.init()
clip.set = ellipseClipDataSet
clip.supportedProperties = []PropertyName{X, Y, Radius, RadiusX, RadiusY}
}
func (clip *ellipseClipData) Shape() ClipShape {
return EllipseClip
}
func ellipseClipDataSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case X, Y, RadiusX, RadiusY:
return setSizeProperty(properties, tag, value)
case Radius:
if result := setSizeProperty(properties, RadiusX, value); result != nil {
properties.setRaw(RadiusY, properties.getRaw(RadiusX))
return append(result, RadiusY)
}
return nil
}
ErrorLogF(`"%s" property is not supported by the ellipse clip shape`, tag)
return nil
}
func (clip *ellipseClipData) String() string {
return runStringWriter(clip)
}
func (clip *ellipseClipData) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("ellipse { ")
comma := false
for _, tag := range []PropertyName{RadiusX, RadiusY, X, Y} {
if value, ok := clip.properties[tag]; ok {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
writePropertyValue(buffer, tag, value, indent)
comma = true
}
}
buffer.WriteString(" }")
}
func (clip *ellipseClipData) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
rx, _ := sizeProperty(clip, RadiusX, session)
ry, _ := sizeProperty(clip, RadiusX, session)
buffer.WriteString("ellipse(")
buffer.WriteString(rx.cssString("50%", session))
buffer.WriteRune(' ')
buffer.WriteString(ry.cssString("50%", session))
buffer.WriteString(" at ")
x, _ := sizeProperty(clip, X, session)
buffer.WriteString(x.cssString("50%", session))
buffer.WriteRune(' ')
y, _ := sizeProperty(clip, Y, session)
buffer.WriteString(y.cssString("50%", session))
buffer.WriteRune(')')
return buffer.String()
}
func (clip *ellipseClipData) valid(session Session) bool {
rx, _ := sizeProperty(clip, RadiusX, session)
ry, _ := sizeProperty(clip, RadiusY, session)
return rx.Value != 0 && ry.Value != 0
}
func (clip *polygonClipData) init() {
clip.dataProperty.init()
clip.set = polygonClipDataSet
clip.supportedProperties = []PropertyName{Points}
}
func (clip *polygonClipData) Shape() ClipShape {
return PolygonClip
}
func polygonClipDataSet(properties Properties, tag PropertyName, value any) []PropertyName {
if Points == tag {
switch value := value.(type) {
case []any:
points := make([]any, len(value))
for i, val := range value {
switch val := val.(type) {
case string:
if isConstantName(val) {
points[i] = val
} else if size, ok := StringToSizeUnit(val); ok {
points[i] = size
} else {
notCompatibleType(tag, val)
return nil
}
case SizeUnit:
points[i] = val
default:
notCompatibleType(tag, val)
points[i] = AutoSize()
return nil
}
}
properties.setRaw(Points, points)
return []PropertyName{tag}
case []SizeUnit:
points := make([]any, len(value))
for i, point := range value {
points[i] = point
}
properties.setRaw(Points, points)
return []PropertyName{tag}
case string:
values := strings.Split(value, ",")
points := make([]any, len(values))
for i, val := range values {
val = strings.Trim(val, " \t\n\r")
if isConstantName(val) {
points[i] = val
} else if size, ok := StringToSizeUnit(val); ok {
points[i] = size
} else {
notCompatibleType(tag, val)
return nil
}
}
properties.setRaw(Points, points)
return []PropertyName{tag}
}
}
return nil
}
func (clip *polygonClipData) String() string {
return runStringWriter(clip)
}
func (clip *polygonClipData) points() []any {
if value := clip.getRaw(Points); value != nil {
if points, ok := value.([]any); ok {
return points
}
}
return nil
}
func (clip *polygonClipData) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("inset { ")
if points := clip.points(); points != nil {
buffer.WriteString(string(Points))
buffer.WriteString(` = "`)
for i, value := range points {
if i > 0 {
buffer.WriteString(", ")
}
writePropertyValue(buffer, "", value, indent)
}
buffer.WriteString(`" `)
}
buffer.WriteRune('}')
}
func (clip *polygonClipData) cssStyle(session Session) string {
points := clip.points()
count := len(points)
if count < 2 {
return ""
}
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
writePoint := func(value any) {
switch value := value.(type) {
case string:
if val, ok := session.resolveConstants(value); ok {
if size, ok := StringToSizeUnit(val); ok {
buffer.WriteString(size.cssString("0px", session))
return
}
}
case SizeUnit:
buffer.WriteString(value.cssString("0px", session))
return
}
buffer.WriteString("0px")
}
leadText := "polygon("
for i := 1; i < count; i += 2 {
buffer.WriteString(leadText)
writePoint(points[i-1])
buffer.WriteRune(' ')
writePoint(points[i])
leadText = ", "
}
buffer.WriteRune(')')
return buffer.String()
}
func (clip *polygonClipData) valid(session Session) bool {
return len(clip.points()) > 0
}
func parseClipShapeProperty(obj DataObject) ClipShapeProperty {
switch obj.Tag() {
case "inset":
clip := new(insetClipData)
clip.init()
for _, tag := range []PropertyName{Top, Right, Bottom, Left, Radius, RadiusX, RadiusY} {
if value, ok := obj.PropertyValue(string(tag)); ok {
insetClipDataSet(clip, tag, value)
}
}
return clip
case "circle":
clip := new(circleClipData)
clip.init()
for _, tag := range []PropertyName{X, Y, Radius} {
if value, ok := obj.PropertyValue(string(tag)); ok {
circleClipDataSet(clip, tag, value)
}
}
return clip
case "ellipse":
clip := new(ellipseClipData)
clip.init()
for _, tag := range []PropertyName{X, Y, RadiusX, RadiusY} {
if value, ok := obj.PropertyValue(string(tag)); ok {
ellipseClipDataSet(clip, tag, value)
}
}
return clip
case "polygon":
clip := new(polygonClipData)
clip.init()
if value, ok := obj.PropertyValue(string(Points)); ok {
polygonClipDataSet(clip, Points, value)
}
return clip
}
return nil
}
func setClipShapePropertyProperty(properties Properties, tag PropertyName, value any) []PropertyName {
switch value := value.(type) {
case ClipShapeProperty:
properties.setRaw(tag, value)
return []PropertyName{tag}
case string:
if isConstantName(value) {
properties.setRaw(tag, value)
return []PropertyName{tag}
}
if obj := NewDataObject(value); obj == nil {
if clip := parseClipShapeProperty(obj); clip != nil {
properties.setRaw(tag, clip)
return []PropertyName{tag}
}
}
case DataObject:
if clip := parseClipShapeProperty(value); clip != nil {
properties.setRaw(tag, clip)
return []PropertyName{tag}
}
case DataValue:
if value.IsObject() {
if clip := parseClipShapeProperty(value.Object()); clip != nil {
properties.setRaw(tag, clip)
return []PropertyName{tag}
}
}
}
notCompatibleType(tag, value)
return nil
}
func getClipShapeProperty(prop Properties, tag PropertyName, session Session) ClipShapeProperty {
if value := prop.getRaw(tag); value != nil {
switch value := value.(type) {
case ClipShapeProperty:
return value
case string:
if text, ok := session.resolveConstants(value); ok {
if obj := NewDataObject(text); obj == nil {
return parseClipShapeProperty(obj)
}
}
}
}
return nil
}
// GetClip returns a View clipping area.
// If the second argument (subviewID) is not specified or it is "" then a top position of the first argument (view) is returned
func GetClip(view View, subviewID ...string) ClipShapeProperty {
if view = getSubview(view, subviewID); view != nil {
return getClipShapeProperty(view, Clip, view.Session())
}
return nil
}
// GetShapeOutside returns a shape around which adjacent inline content.
// If the second argument (subviewID) is not specified or it is "" then a top position of the first argument (view) is returned
func GetShapeOutside(view View, subviewID ...string) ClipShapeProperty {
if view = getSubview(view, subviewID); view != nil {
return getClipShapeProperty(view, ShapeOutside, view.Session())
}
return nil
}

View File

@ -11,6 +11,22 @@ import (
// Color - represent color in argb format
type Color uint32
// ARGB creates Color using alpha, red, green and blue components
func ARGB[T int | uint | int8 | uint8](alpha, red, green, blue T) Color {
return ((Color(alpha) & 0xFF) << 24) +
((Color(red) & 0xFF) << 16) +
((Color(green) & 0xFF) << 8) +
(Color(blue) & 0xFF)
}
// RGB creates Color using red, green and blue components
func RGB[T int | uint | int8 | uint8](red, green, blue T) Color {
return (Color(0xFF) << 24) +
((Color(red) & 0xFF) << 16) +
((Color(green) & 0xFF) << 8) +
(Color(blue) & 0xFF)
}
// ARGB - return alpha, red, green and blue components of the color
func (color Color) ARGB() (uint8, uint8, uint8, uint8) {
return uint8(color >> 24),

View File

@ -2,6 +2,7 @@ package rui
import "sort"
// A set of predefined colors used in the library
const (
// Black color constant
Black Color = 0xff000000
@ -449,8 +450,12 @@ var colorConstants = map[string]Color{
"yellowgreen": 0xff9acd32,
}
// NamedColor make a relation between color and its name
type NamedColor struct {
Name string
// Name of a color
Name string
// Color value
Color Color
}

View File

@ -4,20 +4,48 @@ import (
"strings"
)
// Constants for [ColorPicker] specific properties and events.
const (
ColorChangedEvent = "color-changed"
ColorPickerValue = "color-picker-value"
// ColorChangedEvent is the constant for "color-changed" property tag.
//
// Used by `ColorPicker`.
// Event generated when color picker value has been changed.
//
// General listener format:
// func(picker rui.ColorPicker, newColor, oldColor rui.Color)
//
// where:
// - picker - Interface of a color picker which generated this event,
// - newColor - New color value,
// - oldColor - Old color value.
//
// Allowed listener formats:
// func(picker rui.ColorPicker, newColor rui.Color)
// func(newColor, oldColor rui.Color)
// func(newColor rui.Color)
// func(picker rui.ColorPicker)
// func()
ColorChangedEvent PropertyName = "color-changed"
// ColorPickerValue is the constant for "color-picker-value" property tag.
//
// Used by `ColorPicker`.
// Define current color picker value.
//
// Supported types: `Color`, `string`.
//
// Internal type is `Color`, other types converted to it during assignment.
// See `Color` description for more details.
ColorPickerValue PropertyName = "color-picker-value"
)
// ColorPicker - ColorPicker view
// ColorPicker represent a ColorPicker view
type ColorPicker interface {
View
}
type colorPickerData struct {
viewData
dataList
colorChangedListeners []func(ColorPicker, Color, Color)
}
// NewColorPicker create new ColorPicker object and return it
@ -29,125 +57,69 @@ func NewColorPicker(session Session, params Params) ColorPicker {
}
func newColorPicker(session Session) View {
return NewColorPicker(session, nil)
return new(colorPickerData)
}
func (picker *colorPickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "ColorPicker"
picker.hasHtmlDisabled = true
picker.colorChangedListeners = []func(ColorPicker, Color, Color){}
picker.properties[Padding] = Px(0)
picker.dataListInit()
picker.normalize = normalizeColorPickerTag
picker.set = picker.setFunc
picker.changed = picker.propertyChanged
}
func (picker *colorPickerData) String() string {
return getViewString(picker, nil)
}
func (picker *colorPickerData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeColorPickerTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Value, ColorTag:
return ColorPickerValue
}
return picker.normalizeDataListTag(tag)
return normalizeDataListTag(tag)
}
func (picker *colorPickerData) Remove(tag string) {
picker.remove(picker.normalizeTag(tag))
}
func (picker *colorPickerData) remove(tag string) {
func (picker *colorPickerData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case ColorChangedEvent:
if len(picker.colorChangedListeners) > 0 {
picker.colorChangedListeners = []func(ColorPicker, Color, Color){}
picker.propertyChangedEvent(tag)
}
return setTwoArgEventListener[ColorPicker, Color](picker, tag, value)
case ColorPickerValue:
oldColor := GetColorPickerValue(picker)
delete(picker.properties, ColorPickerValue)
picker.colorChanged(oldColor)
result := setColorProperty(picker, ColorPickerValue, value)
if result != nil {
picker.setRaw("old-color", oldColor)
}
return result
case DataList:
if len(picker.dataList.dataList) > 0 {
picker.setDataList(picker, []string{}, true)
}
default:
picker.viewData.remove(tag)
}
}
func (picker *colorPickerData) Set(tag string, value any) bool {
return picker.set(picker.normalizeTag(tag), value)
}
func (picker *colorPickerData) set(tag string, value any) bool {
if value == nil {
picker.remove(tag)
return true
return setDataList(picker, value, "")
}
return picker.viewData.setFunc(tag, value)
}
func (picker *colorPickerData) propertyChanged(tag PropertyName) {
switch tag {
case ColorChangedEvent:
listeners, ok := valueToEventWithOldListeners[ColorPicker, Color](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(ColorPicker, Color, Color){}
}
picker.colorChangedListeners = listeners
picker.propertyChangedEvent(tag)
return true
case ColorPickerValue:
oldColor := GetColorPickerValue(picker)
if picker.setColorProperty(ColorPickerValue, value) {
picker.colorChanged(oldColor)
return true
}
color := GetColorPickerValue(picker)
picker.Session().callFunc("setInputValue", picker.htmlID(), color.rgbString())
case DataList:
return picker.setDataList(picker, value, picker.created)
if listeners := GetColorChangedListeners(picker); len(listeners) > 0 {
oldColor := Color(0)
if value := picker.getRaw("old-color"); value != nil {
oldColor = value.(Color)
}
for _, listener := range listeners {
listener(picker, color, oldColor)
}
}
default:
return picker.viewData.set(tag, value)
picker.viewData.propertyChanged(tag)
}
return false
}
func (picker *colorPickerData) colorChanged(oldColor Color) {
if newColor := GetColorPickerValue(picker); oldColor != newColor {
if picker.created {
picker.session.callFunc("setInputValue", picker.htmlID(), newColor.rgbString())
}
for _, listener := range picker.colorChangedListeners {
listener(picker, newColor, oldColor)
}
picker.propertyChangedEvent(ColorTag)
}
}
func (picker *colorPickerData) Get(tag string) any {
return picker.get(picker.normalizeTag(tag))
}
func (picker *colorPickerData) get(tag string) any {
switch tag {
case ColorChangedEvent:
return picker.colorChangedListeners
case DataList:
return picker.dataList.dataList
default:
return picker.viewData.get(tag)
}
}
func (picker *colorPickerData) htmlTag() string {
@ -155,7 +127,10 @@ func (picker *colorPickerData) htmlTag() string {
}
func (picker *colorPickerData) htmlSubviews(self View, buffer *strings.Builder) {
picker.dataListHtmlSubviews(self, buffer)
dataListHtmlSubviews(self, buffer, func(text string, session Session) string {
text, _ = session.resolveConstants(text)
return text
})
}
func (picker *colorPickerData) htmlProperties(self View, buffer *strings.Builder) {
@ -170,20 +145,23 @@ func (picker *colorPickerData) htmlProperties(self View, buffer *strings.Builder
buffer.WriteString(` onclick="stopEventPropagation(this, event)"`)
}
picker.dataListHtmlProperties(picker, buffer)
dataListHtmlProperties(picker, buffer)
}
func (picker *colorPickerData) handleCommand(self View, command string, data DataObject) bool {
func (picker *colorPickerData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command {
case "textChanged":
if text, ok := data.PropertyValue("text"); ok {
oldColor := GetColorPickerValue(picker)
if color, ok := StringToColor(text); ok {
oldColor := GetColorPickerValue(picker)
picker.properties[ColorPickerValue] = color
if color != oldColor {
for _, listener := range picker.colorChangedListeners {
for _, listener := range GetColorChangedListeners(picker) {
listener(picker, color, oldColor)
}
if listener, ok := picker.changeListener[ColorPickerValue]; ok {
listener(picker, ColorPickerValue)
}
}
}
}
@ -196,14 +174,11 @@ func (picker *colorPickerData) handleCommand(self View, command string, data Dat
// GetColorPickerValue returns the value of ColorPicker subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetColorPickerValue(view View, subviewID ...string) Color {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
if value, ok := colorProperty(view, ColorPickerValue, view.Session()); ok {
return value
}
for _, tag := range []string{ColorPickerValue, Value, ColorTag} {
for _, tag := range []PropertyName{ColorPickerValue, Value, ColorTag} {
if value := valueFromStyle(view, tag); value != nil {
if result, ok := valueToColor(value, view.Session()); ok {
return result
@ -218,5 +193,5 @@ func GetColorPickerValue(view View, subviewID ...string) Color {
// 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.
func GetColorChangedListeners(view View, subviewID ...string) []func(ColorPicker, Color, Color) {
return getEventWithOldListeners[ColorPicker, Color](view, subviewID, ColorChangedEvent)
return getTwoArgEventListeners[ColorPicker, Color](view, subviewID, ColorChangedEvent)
}

View File

@ -2,57 +2,120 @@ package rui
import (
"strconv"
"strings"
)
// Constants for [ColumnLayout] specific properties and events
const (
// ColumnCount is the constant for the "column-count" property tag.
// The "column-count" int property specifies number of columns into which the content is break
// 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
ColumnCount = "column-count"
// ColumnCount is the constant for "column-count" property tag.
//
// Used by ColumnLayout.
// Specifies number of columns into which the content is break. Values less than zero are not valid. If this property
// value is 0 then the number of columns is calculated based on the "column-width" property.
//
// Supported types: int, string.
//
// Values:
// - 0 or "0" - Use "column-width" to control how many columns will be created.
// - positive value - Тhe number of columns into which the content is divided.
ColumnCount PropertyName = "column-count"
// ColumnWidth is the constant for the "column-width" property tag.
// The "column-width" SizeUnit property specifies the width of each column.
ColumnWidth = "column-width"
// ColumnWidth is the constant for "column-width" property tag.
//
// Used by ColumnLayout.
// Specifies the width of each column.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See [SizeUnit] description for more details.
ColumnWidth PropertyName = "column-width"
// ColumnGap is the constant for the "column-gap" property tag.
// The "column-width" SizeUnit property sets the size of the gap (gutter) between columns.
ColumnGap = "column-gap"
// ColumnGap is the constant for "column-gap" property tag.
//
// Used by ColumnLayout.
// Set the size of the gap (gutter) between columns.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See [SizeUnit] description for more details.
ColumnGap PropertyName = "column-gap"
// 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.
ColumnSeparator = "column-separator"
// ColumnSeparator is the constant for "column-separator" property tag.
//
// Used by ColumnLayout.
// Specifies the line drawn between columns in a multi-column layout.
//
// Supported types: ColumnSeparatorProperty, ViewBorder.
//
// Internal type is ColumnSeparatorProperty, other types converted to it during assignment.
// See [ColumnSeparatorProperty] and [ViewBorder] description for more details.
ColumnSeparator PropertyName = "column-separator"
// 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
// columns in a multi-column layout.
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
ColumnSeparatorStyle = "column-separator-style"
// ColumnSeparatorStyle is the constant for "column-separator-style" property tag.
//
// Used by ColumnLayout.
// Controls the style of the line drawn between columns in a multi-column layout.
//
// Supported types: int, string.
//
// Values:
// - 0 (NoneLine) or "none" - The separator will not be drawn.
// - 1 (SolidLine) or "solid" - Solid line as a separator.
// - 2 (DashedLine) or "dashed" - Dashed line as a separator.
// - 3 (DottedLine) or "dotted" - Dotted line as a separator.
// - 4 (DoubleLine) or "double" - Double line as a separator.
ColumnSeparatorStyle PropertyName = "column-separator-style"
// 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
// columns in a multi-column layout.
ColumnSeparatorWidth = "column-separator-width"
// ColumnSeparatorWidth is the constant for "column-separator-width" property tag.
//
// Used by ColumnLayout.
// Set the width of the line drawn between columns in a multi-column layout.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
ColumnSeparatorWidth PropertyName = "column-separator-width"
// 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
// columns in a multi-column layout.
ColumnSeparatorColor = "column-separator-color"
// ColumnSeparatorColor is the constant for "column-separator-color" property tag.
//
// Used by ColumnLayout.
// Set the color of the line drawn between columns in a multi-column layout.
//
// Supported types: Color, string.
//
// Internal type is Color, other types converted to it during assignment.
// See Color description for more details.
ColumnSeparatorColor PropertyName = "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"
// ColumnFill is the constant for "column-fill" property tag.
//
// Used by ColumnLayout.
// Controls how a ColumnLayout's content is balanced when broken into columns. Default value is "balance".
//
// Supported types: int, string.
//
// Values:
// - 0 (ColumnFillBalance) or "balance" - Content is equally divided between columns.
// - 1 (ColumnFillAuto) or "auto" - Columns are filled sequentially. Content takes up only the room it needs, possibly resulting in some columns remaining empty.
ColumnFill PropertyName = "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"
// ColumnSpanAll is the constant for "column-span-all" property tag.
//
// Used by ColumnLayout.
// Property used in views placed inside the column layout container. Makes it possible for a view to span across all
// columns. Default value is false.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", or "1" - View will span across all columns.
// - false, 0, "false", "no", "off", or "0" - View will be a part of a column.
ColumnSpanAll PropertyName = "column-span-all"
)
// ColumnLayout - grid-container of View
// ColumnLayout represent a ColumnLayout view
type ColumnLayout interface {
ViewsContainer
}
@ -70,22 +133,20 @@ func NewColumnLayout(session Session, params Params) ColumnLayout {
}
func newColumnLayout(session Session) View {
return NewColumnLayout(session, nil)
return new(columnLayoutData)
}
// Init initialize fields of ColumnLayout by default values
func (ColumnLayout *columnLayoutData) init(session Session) {
ColumnLayout.viewsContainerData.init(session)
ColumnLayout.tag = "ColumnLayout"
//ColumnLayout.systemClass = "ruiColumnLayout"
func (columnLayout *columnLayoutData) init(session Session) {
columnLayout.viewsContainerData.init(session)
columnLayout.tag = "ColumnLayout"
columnLayout.systemClass = "ruiColumnLayout"
columnLayout.normalize = normalizeColumnLayoutTag
columnLayout.changed = columnLayout.propertyChanged
}
func (columnLayout *columnLayoutData) String() string {
return getViewString(columnLayout, nil)
}
func (columnLayout *columnLayoutData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeColumnLayoutTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Gap:
return ColumnGap
@ -93,62 +154,28 @@ func (columnLayout *columnLayoutData) normalizeTag(tag string) string {
return tag
}
func (columnLayout *columnLayoutData) Get(tag string) any {
return columnLayout.get(columnLayout.normalizeTag(tag))
}
func (columnLayout *columnLayoutData) Remove(tag string) {
columnLayout.remove(columnLayout.normalizeTag(tag))
}
func (columnLayout *columnLayoutData) remove(tag string) {
columnLayout.viewsContainerData.remove(tag)
if columnLayout.created {
switch tag {
case ColumnCount, ColumnWidth, ColumnGap:
columnLayout.session.updateCSSProperty(columnLayout.htmlID(), tag, "")
case ColumnSeparator:
columnLayout.session.updateCSSProperty(columnLayout.htmlID(), "column-rule", "")
func (columnLayout *columnLayoutData) propertyChanged(tag PropertyName) {
switch tag {
case ColumnSeparator:
css := ""
session := columnLayout.Session()
if value := columnLayout.getRaw(ColumnSeparator); value != nil {
separator := value.(ColumnSeparatorProperty)
css = separator.cssValue(session)
}
}
}
session.updateCSSProperty(columnLayout.htmlID(), "column-rule", css)
func (columnLayout *columnLayoutData) Set(tag string, value any) bool {
return columnLayout.set(columnLayout.normalizeTag(tag), value)
}
func (columnLayout *columnLayoutData) set(tag string, value any) bool {
if value == nil {
columnLayout.remove(tag)
return true
}
if !columnLayout.viewsContainerData.set(tag, value) {
return false
}
if columnLayout.created {
switch tag {
case ColumnSeparator:
css := ""
session := columnLayout.Session()
if val, ok := columnLayout.properties[ColumnSeparator]; ok {
separator := val.(ColumnSeparatorProperty)
css = separator.cssValue(columnLayout.Session())
}
session.updateCSSProperty(columnLayout.htmlID(), "column-rule", css)
case ColumnCount:
session := columnLayout.Session()
if count, ok := intProperty(columnLayout, tag, session, 0); ok && count > 0 {
session.updateCSSProperty(columnLayout.htmlID(), tag, strconv.Itoa(count))
} else {
session.updateCSSProperty(columnLayout.htmlID(), tag, "auto")
}
case ColumnCount:
session := columnLayout.Session()
if count := GetColumnCount(columnLayout); count > 0 {
session.updateCSSProperty(columnLayout.htmlID(), string(ColumnCount), strconv.Itoa(count))
} else {
session.updateCSSProperty(columnLayout.htmlID(), string(ColumnCount), "auto")
}
default:
columnLayout.viewsContainerData.propertyChanged(tag)
}
return true
}
// GetColumnCount returns int value which specifies number of columns into which the content of
@ -172,11 +199,7 @@ func GetColumnGap(view View, subviewID ...string) SizeUnit {
}
func getColumnSeparator(view View, subviewID []string) ViewBorder {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
value := view.Get(ColumnSeparator)
if value == nil {
value = valueFromStyle(view, ColumnSeparator)

View File

@ -10,19 +10,22 @@ type ColumnSeparatorProperty interface {
Properties
fmt.Stringer
stringWriter
// ViewBorder returns column separator description in a form of ViewBorder
ViewBorder(session Session) ViewBorder
cssValue(session Session) string
}
type columnSeparatorProperty struct {
propertyList
dataProperty
}
func newColumnSeparatorProperty(value any) ColumnSeparatorProperty {
if value == nil {
separator := new(columnSeparatorProperty)
separator.properties = map[string]any{}
separator.init()
return separator
}
@ -32,17 +35,18 @@ func newColumnSeparatorProperty(value any) ColumnSeparatorProperty {
case DataObject:
separator := new(columnSeparatorProperty)
separator.properties = map[string]any{}
for _, tag := range []string{Style, Width, ColorTag} {
if val, ok := value.PropertyValue(tag); ok && val != "" {
separator.set(tag, value)
separator.init()
for _, tag := range []PropertyName{Style, Width, ColorTag} {
if val, ok := value.PropertyValue(string(tag)); ok && val != "" {
propertiesSet(separator, tag, value)
}
}
return separator
case ViewBorder:
separator := new(columnSeparatorProperty)
separator.properties = map[string]any{
separator.init()
separator.properties = map[PropertyName]any{
Style: value.Style,
Width: value.Width,
ColorTag: value.Color,
@ -54,12 +58,17 @@ func newColumnSeparatorProperty(value any) ColumnSeparatorProperty {
return nil
}
// NewColumnSeparator creates the new ColumnSeparatorProperty
func NewColumnSeparator(params Params) ColumnSeparatorProperty {
// NewColumnSeparatorProperty creates the new ColumnSeparatorProperty.
//
// The following properties can be used:
// - "style" (Style) - Determines the line style (type is int). Valid values: 0 (NoneLine), 1 (SolidLine), 2 (DashedLine), 3 (DottedLine), or 4 (DoubleLine);
// - "color" (ColorTag) - Determines the line color (type is [Color]);
// - "width" (Width) - Determines the line thickness (type is [SizeUnit]).
func NewColumnSeparatorProperty(params Params) ColumnSeparatorProperty {
separator := new(columnSeparatorProperty)
separator.properties = map[string]any{}
separator.init()
if params != nil {
for _, tag := range []string{Style, Width, ColorTag} {
for _, tag := range []PropertyName{Style, Width, ColorTag} {
if value, ok := params[tag]; ok && value != nil {
separator.Set(tag, value)
}
@ -68,8 +77,29 @@ func NewColumnSeparator(params Params) ColumnSeparatorProperty {
return separator
}
func (separator *columnSeparatorProperty) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
// NewColumnSeparator creates the new ColumnSeparatorProperty.
//
// Arguments:
// - style - determines the line style. Valid values: 0 [NoneLine], 1 [SolidLine], 2 [DashedLine], 3 [DottedLine], or 4 [DoubleLine];
// - color - determines the line color;
// - width - determines the line thickness.
func NewColumnSeparator(style int, color Color, width SizeUnit) ColumnSeparatorProperty {
return NewColumnSeparatorProperty(Params{
Width: width,
Style: style,
ColorTag: color,
})
}
func (separator *columnSeparatorProperty) init() {
separator.dataProperty.init()
separator.normalize = normalizeVolumnSeparatorTag
separator.set = columnSeparatorSet
separator.supportedProperties = []PropertyName{Style, Width, ColorTag}
}
func normalizeVolumnSeparatorTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case ColumnSeparatorStyle, "separator-style":
return Style
@ -87,12 +117,12 @@ func (separator *columnSeparatorProperty) normalizeTag(tag string) string {
func (separator *columnSeparatorProperty) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("_{ ")
comma := false
for _, tag := range []string{Style, Width, ColorTag} {
for _, tag := range []PropertyName{Style, Width, ColorTag} {
if value, ok := separator.properties[tag]; ok {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
writePropertyValue(buffer, BorderStyle, value, indent)
comma = true
@ -106,47 +136,12 @@ func (separator *columnSeparatorProperty) String() string {
return runStringWriter(separator)
}
func (separator *columnSeparatorProperty) Remove(tag string) {
switch tag = separator.normalizeTag(tag); tag {
case Style, Width, ColorTag:
delete(separator.properties, tag)
default:
ErrorLogF(`"%s" property is not compatible with the ColumnSeparatorProperty`, tag)
func getColumnSeparatorProperty(properties Properties) ColumnSeparatorProperty {
if val := properties.getRaw(ColumnSeparator); val != nil {
if separator, ok := val.(ColumnSeparatorProperty); ok {
return separator
}
}
}
func (separator *columnSeparatorProperty) Set(tag string, value any) bool {
tag = separator.normalizeTag(tag)
if value == nil {
separator.remove(tag)
return true
}
switch tag {
case Style:
return separator.setEnumProperty(Style, value, enumProperties[BorderStyle].values)
case Width:
return separator.setSizeProperty(Width, value)
case ColorTag:
return separator.setColorProperty(ColorTag, value)
}
ErrorLogF(`"%s" property is not compatible with the ColumnSeparatorProperty`, tag)
return false
}
func (separator *columnSeparatorProperty) Get(tag string) any {
tag = separator.normalizeTag(tag)
if result, ok := separator.properties[tag]; ok {
return result
}
return nil
}
@ -189,3 +184,10 @@ func (separator *columnSeparatorProperty) cssValue(session Session) string {
return buffer.String()
}
func columnSeparatorSet(properties Properties, tag PropertyName, value any) []PropertyName {
if tag == Style {
return setEnumProperty(properties, Style, value, enumProperties[BorderStyle].values)
}
return propertiesSet(properties, tag, value)
}

View File

@ -150,9 +150,11 @@ func (builder *cssValueBuilder) addValues(key, separator string, values ...strin
}
}
func (builder *cssStyleBuilder) init() {
func (builder *cssStyleBuilder) init(kbSize int) {
builder.buffer = allocStringBuilder()
builder.buffer.Grow(16 * 1024)
if kbSize > 0 {
builder.buffer.Grow(kbSize * 1024)
}
}
func (builder *cssStyleBuilder) finish() string {
@ -168,7 +170,7 @@ func (builder *cssStyleBuilder) finish() string {
func (builder *cssStyleBuilder) startMedia(rule string) {
if builder.buffer == nil {
builder.init()
builder.init(0)
}
builder.buffer.WriteString(`@media screen`)
builder.buffer.WriteString(rule)
@ -178,7 +180,7 @@ func (builder *cssStyleBuilder) startMedia(rule string) {
func (builder *cssStyleBuilder) endMedia() {
if builder.buffer == nil {
builder.init()
builder.init(0)
}
builder.buffer.WriteString(`}\n`)
builder.media = false
@ -192,7 +194,7 @@ func (builder *cssStyleBuilder) startStyle(name string) {
}
if builder.buffer == nil {
builder.init()
builder.init(0)
}
if builder.media {
builder.buffer.WriteString(`\t`)
@ -210,7 +212,7 @@ func (builder *cssStyleBuilder) startStyle(name string) {
func (builder *cssStyleBuilder) endStyle() {
if builder.buffer == nil {
builder.init()
builder.init(0)
}
if builder.media {
builder.buffer.WriteString(`\t`)
@ -220,7 +222,7 @@ func (builder *cssStyleBuilder) endStyle() {
func (builder *cssStyleBuilder) startAnimation(name string) {
if builder.buffer == nil {
builder.init()
builder.init(0)
}
builder.media = true
@ -231,7 +233,7 @@ func (builder *cssStyleBuilder) startAnimation(name string) {
func (builder *cssStyleBuilder) endAnimation() {
if builder.buffer == nil {
builder.init()
builder.init(0)
}
builder.buffer.WriteString(`}\n`)
builder.media = false
@ -239,7 +241,7 @@ func (builder *cssStyleBuilder) endAnimation() {
func (builder *cssStyleBuilder) startAnimationFrame(name string) {
if builder.buffer == nil {
builder.init()
builder.init(0)
}
builder.buffer.WriteString(`\t`)
@ -249,7 +251,7 @@ func (builder *cssStyleBuilder) startAnimationFrame(name string) {
func (builder *cssStyleBuilder) endAnimationFrame() {
if builder.buffer == nil {
builder.init()
builder.init(0)
}
builder.buffer.WriteString(`\t}\n`)
}
@ -257,7 +259,7 @@ func (builder *cssStyleBuilder) endAnimationFrame() {
func (builder *cssStyleBuilder) add(key, value string) {
if value != "" {
if builder.buffer == nil {
builder.init()
builder.init(0)
}
if builder.media {
builder.buffer.WriteString(`\t`)
@ -276,7 +278,7 @@ func (builder *cssStyleBuilder) addValues(key, separator string, values ...strin
}
if builder.buffer == nil {
builder.init()
builder.init(0)
}
if builder.media {
builder.buffer.WriteString(`\t`)

View File

@ -5,8 +5,13 @@ import "strings"
// CustomView defines a custom view interface
type CustomView interface {
ViewsContainer
// CreateSuperView must be implemented to create a base view from which custom control has been built
CreateSuperView(session Session) View
// SuperView must be implemented to return a base view from which custom control has been built
SuperView() View
setSuperView(view View)
setTag(tag string)
}
@ -31,6 +36,9 @@ func InitCustomView(customView CustomView, tag string, session Session, params P
return true
}
func (customView *CustomViewData) init(session Session) {
}
// SuperView returns a super view
func (customView *CustomViewData) SuperView() View {
return customView.superView
@ -52,43 +60,62 @@ func (customView *CustomViewData) setTag(tag string) {
// Get returns a value of the property with name defined by the argument.
// The type of return value depends on the property. If the property is not set then nil is returned.
func (customView *CustomViewData) Get(tag string) any {
func (customView *CustomViewData) Get(tag PropertyName) any {
return customView.superView.Get(tag)
}
func (customView *CustomViewData) getRaw(tag string) any {
func (customView *CustomViewData) getRaw(tag PropertyName) any {
return customView.superView.getRaw(tag)
}
func (customView *CustomViewData) setRaw(tag string, value any) {
func (customView *CustomViewData) setRaw(tag PropertyName, value any) {
customView.superView.setRaw(tag, value)
}
func (customView *CustomViewData) setContent(value any) bool {
if container, ok := customView.superView.(ViewsContainer); ok {
return container.setContent(value)
}
return false
}
// Set 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
// a description of the error is written to the log
func (customView *CustomViewData) Set(tag string, value any) bool {
func (customView *CustomViewData) Set(tag PropertyName, value any) bool {
return customView.superView.Set(tag, value)
}
func (customView *CustomViewData) SetAnimated(tag string, value any, animation Animation) bool {
// 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
// a description of the error is written to the log
func (customView *CustomViewData) SetAnimated(tag PropertyName, value any, animation AnimationProperty) bool {
return customView.superView.SetAnimated(tag, value, animation)
}
func (customView *CustomViewData) SetChangeListener(tag string, listener func(View, string)) {
func (customView *CustomViewData) SetParams(params Params) bool {
return customView.superView.SetParams(params)
}
// SetChangeListener set the function to track the change of the View property
func (customView *CustomViewData) SetChangeListener(tag PropertyName, listener func(View, PropertyName)) {
customView.superView.SetChangeListener(tag, listener)
}
// Remove removes the property with name defined by the argument
func (customView *CustomViewData) Remove(tag string) {
func (customView *CustomViewData) Remove(tag PropertyName) {
customView.superView.Remove(tag)
}
// AllTags returns an array of the set properties
func (customView *CustomViewData) AllTags() []string {
func (customView *CustomViewData) AllTags() []PropertyName {
return customView.superView.AllTags()
}
func (customView *CustomViewData) empty() bool {
return customView.superView.empty()
}
// Clear removes all properties
func (customView *CustomViewData) Clear() {
customView.superView.Clear()
@ -151,10 +178,12 @@ func (customView *CustomViewData) Frame() Frame {
return customView.superView.Frame()
}
// Scroll returns a location and size of a scrollable view in pixels
func (customView *CustomViewData) Scroll() Frame {
return customView.superView.Scroll()
}
// HasFocus returns "true" if the view has focus
func (customView *CustomViewData) HasFocus() bool {
return customView.superView.HasFocus()
}
@ -167,7 +196,7 @@ func (customView *CustomViewData) onItemResize(self View, index string, x, y, wi
customView.superView.onItemResize(customView.superView, index, x, y, width, height)
}
func (customView *CustomViewData) handleCommand(self View, command string, data DataObject) bool {
func (customView *CustomViewData) handleCommand(self View, command PropertyName, data DataObject) bool {
return customView.superView.handleCommand(customView.superView, command, data)
}
@ -195,6 +224,10 @@ func (customView *CustomViewData) htmlProperties(self View, buffer *strings.Buil
customView.superView.htmlProperties(customView.superView, buffer)
}
func (customView *CustomViewData) htmlDisabledProperty() bool {
return customView.superView.htmlDisabledProperty()
}
func (customView *CustomViewData) cssStyle(self View, builder cssBuilder) {
customView.superView.cssStyle(customView.superView, builder)
}
@ -259,9 +292,9 @@ func (customView *CustomViewData) ViewIndex(view View) int {
return -1
}
func (customView *CustomViewData) exscludeTags() []string {
func (customView *CustomViewData) exscludeTags() []PropertyName {
if customView.superView != nil {
exsclude := []string{}
exsclude := []PropertyName{}
for tag, value := range customView.defaultParams {
if value == customView.superView.getRaw(tag) {
exsclude = append(exsclude, tag)
@ -272,9 +305,13 @@ func (customView *CustomViewData) exscludeTags() []string {
return nil
}
// String convert internal representation of a [CustomViewData] into a string.
func (customView *CustomViewData) String() string {
if customView.superView != nil {
return getViewString(customView, customView.exscludeTags())
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
writeViewStyle(customView.tag, customView, buffer, "", customView.exscludeTags())
return buffer.String()
}
return customView.tag + " { }"
}
@ -285,21 +322,26 @@ func (customView *CustomViewData) setScroll(x, y, width, height float64) {
}
}
func (customView *CustomViewData) Transition(tag string) Animation {
// Transition returns the transition animation of the property(tag). Returns nil is there is no transition animation.
func (customView *CustomViewData) Transition(tag PropertyName) AnimationProperty {
if customView.superView != nil {
return customView.superView.Transition(tag)
}
return nil
}
func (customView *CustomViewData) Transitions() map[string]Animation {
// Transitions returns a map of transition animations. The result is always non-nil.
func (customView *CustomViewData) Transitions() map[PropertyName]AnimationProperty {
if customView.superView != nil {
return customView.superView.Transitions()
}
return map[string]Animation{}
return map[PropertyName]AnimationProperty{}
}
func (customView *CustomViewData) SetTransition(tag string, animation Animation) {
// SetTransition sets the transition animation for the property if "animation" argument is not nil, and
// removes the transition animation of the property if "animation" argument is nil.
// The "tag" argument is the property name.
func (customView *CustomViewData) SetTransition(tag PropertyName, animation AnimationProperty) {
if customView.superView != nil {
customView.superView.SetTransition(tag, animation)
}

47
data.go
View File

@ -7,25 +7,49 @@ import (
// DataValue interface of a data node value
type DataValue interface {
// IsObject returns "true" if data value is an object
IsObject() bool
// Object returns data value as a data object
Object() DataObject
// Value returns value as a string
Value() string
}
// DataObject interface of a data object
type DataObject interface {
DataValue
// Tag returns data object tag
Tag() string
// PropertyCount returns properties count
PropertyCount() int
// Property returns a data node corresponding to a property with specific index
Property(index int) DataNode
// PropertyByTag returns a data node corresponding to a property tag
PropertyByTag(tag string) DataNode
// PropertyValue returns a string value of a property with a specific tag
PropertyValue(tag string) (string, bool)
// PropertyObject returns an object value of a property with a specific tag
PropertyObject(tag string) DataObject
// SetPropertyValue sets a string value of a property with a specific tag
SetPropertyValue(tag, value string)
// SetPropertyObject sets an object value of a property with a specific tag
SetPropertyObject(tag string, object DataObject)
// ToParams create a params(map) representation of a data object
ToParams() Params
}
// Constants which are used to describe a node type, see [DataNode]
const (
// TextNode - node is the pair "tag - text value". Syntax: <tag> = <text>
TextNode = 0
@ -37,13 +61,28 @@ const (
// DataNode interface of a data node
type DataNode interface {
// Tag returns a tag name
Tag() string
// Type returns a node type. Possible values are TextNode, ObjectNode and ArrayNode
Type() int
// Text returns node text
Text() string
// Object returns node as object if that node type is an object
Object() DataObject
// ArraySize returns array size if that node type is an array
ArraySize() int
// ArrayElement returns a value of an array if that node type is an array
ArrayElement(index int) DataValue
// ArrayElements returns an array of objects if that node is an array
ArrayElements() []DataValue
// ArrayAsParams returns an array of a params(map) if that node is an array
ArrayAsParams() []Params
}
@ -134,7 +173,7 @@ func (object *dataObject) PropertyObject(tag string) DataObject {
}
func (object *dataObject) setNode(node DataNode) {
if object.property == nil || len(object.property) == 0 {
if len(object.property) == 0 {
object.property = []DataNode{node}
} else {
tag := node.Tag()
@ -173,12 +212,12 @@ func (object *dataObject) ToParams() Params {
switch node.Type() {
case TextNode:
if text := node.Text(); text != "" {
params[node.Tag()] = text
params[PropertyName(node.Tag())] = text
}
case ObjectNode:
if obj := node.Object(); obj != nil {
params[node.Tag()] = node.Object()
params[PropertyName(node.Tag())] = node.Object()
}
case ArrayNode:
@ -195,7 +234,7 @@ func (object *dataObject) ToParams() Params {
}
}
if len(array) > 0 {
params[node.Tag()] = array
params[PropertyName(node.Tag())] = array
}
}
}

View File

@ -1,26 +1,105 @@
package rui
import "strings"
const (
// DataList is the constant for the "data-list" property tag.
DataList = "data-list"
import (
"fmt"
"strconv"
"strings"
"time"
)
type dataList struct {
dataList []string
dataListHtml bool
}
const (
// DataList is the constant for "data-list" property tag.
//
// Used by ColorPicker, DatePicker, EditView, NumberPicker, TimePicker.
//
// # Usage in ColorPicker
//
// List of pre-defined colors.
//
// Supported types: []string, string, []fmt.Stringer, []Color, []any containing
// elements of string, fmt.Stringer, int, int8…int64, uint, uint8…uint64.
//
// Internal type is []string, other types converted to it during assignment.
//
// Conversion rules:
// - string - contain single item.
// - []string - an array of items.
// - []fmt.Stringer - an array of objects convertible to a string.
// - []Color - An array of color values which will be converted to a string array.
// - []any - this array must contain only types which were listed in Types section.
//
// # Usage in DatePicker
//
// List of predefined dates. If we set this property, date picker may have a drop-down menu with a list of these values.
// Some browsers may ignore this property, such as Safari for macOS. The value of this property must be an array of
// strings in the format "YYYY-MM-DD".
//
// Supported types: []string, string, []fmt.Stringer, []time.Time, []any containing elements of string, fmt.Stringer, time.Time.
//
// Internal type is []string, other types converted to it during assignment.
//
// Conversion rules:
// - string - contain single item.
// - []string - an array of items.
// - []fmt.Stringer - an array of objects convertible to a string.
// - []time.Time - an array of Time values which will be converted to a string array.
// - []any - this array must contain only types which were listed in Types section.
//
// # Usage in EditView
//
// Array of recommended values.
//
// Supported types: []string, string, []fmt.Stringer, and []any containing
// elements of string, fmt.Stringer, bool, rune, float32, float64, int, int8…int64, uint, uint8…uint64.
//
// Internal type is []string, other types converted to it during assignment.
//
// Conversion rules:
// - string - contain single item.
// - []string - an array of items.
// - []fmt.Stringer - an array of objects convertible to a string.
// - []any - this array must contain only types which were listed in Types section.
//
// # Usage in NumberPicker
//
// Specify an array of recommended values.
//
// Supported types: []string, string, []fmt.Stringer, []float, []int, []bool, []any containing elements
// of string, fmt.Stringer, rune, float32, float64, int, int8…int64, uint, uint8…uint64.
//
// Internal type is []string, other types converted to it during assignment.
//
// Conversion rules:
// - string - must contain integer or floating point number, converted to []string.
// - []string - an array of strings which must contain integer or floating point numbers, stored as is.
// - []fmt.Stringer - object which implement this interface must contain integer or floating point numbers, converted to a []string.
// - []float - converted to []string.
// - []int - converted to []string.
// - []any - an array which may contain types listed in Types section above, each value will be converted to a string and wrapped to array.
//
// # Usage in TimePicker
//
// An array of recommended values. The value of this property must be an array of strings in the format "HH:MM:SS" or
// "HH:MM".
//
// Supported types: []string, string, []fmt.Stringer, []time.Time, []any containing elements of string, fmt.Stringer, time.Time.
//
// Internal type is []string, other types converted to it during assignment.
//
// Conversion rules:
// - string - contain single item.
// - []string - an array of items.
// - []fmt.Stringer - an array of objects convertible to a string.
// - []time.Time - An array of Time values which will be converted to a string array.
// - []any - this array must contain only types which were listed in Types section.
DataList PropertyName = "data-list"
)
func (list *dataList) dataListInit() {
list.dataList = []string{}
}
func (list *dataList) dataListID(view View) string {
func dataListID(view View) string {
return view.htmlID() + "-datalist"
}
func (list *dataList) normalizeDataListTag(tag string) string {
func normalizeDataListTag(tag PropertyName) PropertyName {
switch tag {
case "datalist":
return DataList
@ -29,69 +108,205 @@ func (list *dataList) normalizeDataListTag(tag string) string {
return tag
}
func (list *dataList) setDataList(view View, value any, created bool) bool {
items, ok := anyToStringArray(value)
if !ok {
notCompatibleType(DataList, value)
return false
func setDataList(properties Properties, value any, dateTimeFormat string) []PropertyName {
if items, ok := anyToStringArray(value, dateTimeFormat); ok {
properties.setRaw(DataList, items)
return []PropertyName{DataList}
}
list.dataList = items
if created {
notCompatibleType(DataList, value)
return nil
}
func anyToStringArray(value any, dateTimeFormat string) ([]string, bool) {
switch value := value.(type) {
case string:
return []string{value}, true
case []string:
return value, true
case []DataValue:
items := make([]string, 0, len(value))
for _, val := range value {
if !val.IsObject() {
items = append(items, val.Value())
}
}
return items, true
case []fmt.Stringer:
items := make([]string, len(value))
for i, str := range value {
items[i] = str.String()
}
return items, true
case []Color:
items := make([]string, len(value))
for i, str := range value {
items[i] = str.String()
}
return items, true
case []SizeUnit:
items := make([]string, len(value))
for i, str := range value {
items[i] = str.String()
}
return items, true
case []AngleUnit:
items := make([]string, len(value))
for i, str := range value {
items[i] = str.String()
}
return items, true
case []float32:
items := make([]string, len(value))
for i, val := range value {
items[i] = fmt.Sprintf("%g", float64(val))
}
return items, true
case []float64:
items := make([]string, len(value))
for i, val := range value {
items[i] = fmt.Sprintf("%g", val)
}
return items, true
case []int:
return intArrayToStringArray(value), true
case []uint:
return intArrayToStringArray(value), true
case []int8:
return intArrayToStringArray(value), true
case []uint8:
return intArrayToStringArray(value), true
case []int16:
return intArrayToStringArray(value), true
case []uint16:
return intArrayToStringArray(value), true
case []int32:
return intArrayToStringArray(value), true
case []uint32:
return intArrayToStringArray(value), true
case []int64:
return intArrayToStringArray(value), true
case []uint64:
return intArrayToStringArray(value), true
case []bool:
items := make([]string, len(value))
for i, val := range value {
if val {
items[i] = "true"
} else {
items[i] = "false"
}
}
return items, true
case []time.Time:
if dateTimeFormat == "" {
dateTimeFormat = dateFormat + " " + timeFormat
}
items := make([]string, len(value))
for i, val := range value {
items[i] = val.Format(dateTimeFormat)
}
return items, true
case []any:
items := make([]string, 0, len(value))
for _, v := range value {
switch val := v.(type) {
case string:
items = append(items, val)
case fmt.Stringer:
items = append(items, val.String())
case bool:
if val {
items = append(items, "true")
} else {
items = append(items, "false")
}
case float32:
items = append(items, fmt.Sprintf("%g", float64(val)))
case float64:
items = append(items, fmt.Sprintf("%g", val))
case rune:
items = append(items, string(val))
default:
if n, ok := isInt(v); ok {
items = append(items, strconv.Itoa(n))
} else {
return []string{}, false
}
}
}
return items, true
}
return []string{}, false
}
func getDataListProperty(properties Properties) []string {
if value := properties.getRaw(DataList); value != nil {
if items, ok := value.([]string); ok {
return items
}
}
return nil
}
func dataListHtmlSubviews(view View, buffer *strings.Builder, normalizeItem func(text string, session Session) string) {
if items := getDataListProperty(view); len(items) > 0 {
session := view.Session()
dataListID := list.dataListID(view)
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(`<datalist id="`)
buffer.WriteString(dataListID(view))
buffer.WriteString(`">`)
for _, text := range items {
text = normalizeItem(text, session)
if list.dataListHtml {
list.dataListItemsHtml(buffer)
session.updateInnerHTML(dataListID, buffer.String())
} else {
list.dataListHtmlCode(view, buffer)
session.appendToInnerHTML(view.parentHTMLID(), buffer.String())
list.dataListHtml = true
session.updateProperty(view.htmlID(), "list", dataListID)
if strings.ContainsRune(text, '"') {
text = strings.ReplaceAll(text, `"`, `&#34;`)
}
if strings.ContainsRune(text, '\n') {
text = strings.ReplaceAll(text, "\n", `\n`)
}
buffer.WriteString(`<option value="`)
buffer.WriteString(text)
buffer.WriteString(`"></option>`)
}
}
return true
}
func (list *dataList) dataListHtmlSubviews(view View, buffer *strings.Builder) {
if len(list.dataList) > 0 {
list.dataListHtmlCode(view, buffer)
list.dataListHtml = true
} else {
list.dataListHtml = false
buffer.WriteString(`</datalist>`)
}
}
func (list *dataList) dataListHtmlCode(view View, buffer *strings.Builder) {
buffer.WriteString(`<datalist id="`)
buffer.WriteString(list.dataListID(view))
buffer.WriteString(`">`)
list.dataListItemsHtml(buffer)
buffer.WriteString(`</datalist>`)
}
func (list *dataList) dataListItemsHtml(buffer *strings.Builder) {
for _, text := range list.dataList {
if strings.ContainsRune(text, '"') {
text = strings.ReplaceAll(text, `"`, `&#34;`)
}
if strings.ContainsRune(text, '\n') {
text = strings.ReplaceAll(text, "\n", `\n`)
}
buffer.WriteString(`<option value="`)
buffer.WriteString(text)
buffer.WriteString(`"></option>`)
}
}
func (list *dataList) dataListHtmlProperties(view View, buffer *strings.Builder) {
if len(list.dataList) > 0 {
func dataListHtmlProperties(view View, buffer *strings.Builder) {
if len(getDataListProperty(view)) > 0 {
buffer.WriteString(` list="`)
buffer.WriteString(list.dataListID(view))
buffer.WriteString(dataListID(view))
buffer.WriteString(`"`)
}
}
@ -99,16 +314,8 @@ func (list *dataList) dataListHtmlProperties(view View, buffer *strings.Builder)
// GetDataList returns the data list of an editor.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetDataList(view View, subviewID ...string) []string {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.Get(DataList); value != nil {
if list, ok := value.([]string); ok {
return list
}
}
if view = getSubview(view, subviewID); view != nil {
return getDataListProperty(view)
}
return []string{}

View File

@ -6,24 +6,119 @@ import (
"time"
)
// Constants for [DatePicker] specific properties and events.
const (
DateChangedEvent = "date-changed"
DatePickerMin = "date-picker-min"
DatePickerMax = "date-picker-max"
DatePickerStep = "date-picker-step"
DatePickerValue = "date-picker-value"
dateFormat = "2006-01-02"
// DateChangedEvent is the constant for "date-changed" property tag.
//
// Used by DatePicker.
// Occur when date picker value has been changed.
//
// General listener format:
// func(picker rui.DatePicker, newDate time.Time, oldDate time.Time)
//
// where:
// - picker - Interface of a date picker which generated this event,
// - newDate - New date value,
// - oldDate - Old date value.
//
// Allowed listener formats:
// func(picker rui.DatePicker, newDate time.Time)
// func(newDate time.Time, oldDate time.Time)
// func(newDate time.Time)
// func(picker rui.DatePicker)
// func()
DateChangedEvent PropertyName = "date-changed"
// DatePickerMin is the constant for "date-picker-min" property tag.
//
// Used by DatePicker.
// Minimum date value.
//
// Supported types: time.Time, string.
//
// Internal type is time.Time, other types converted to it during assignment.
//
// Conversion rules:
// string - values of this type parsed and converted to time.Time. The following formats are supported:
// - "YYYYMMDD" - "20240102".
// - "Mon-DD-YYYY" - "Jan-02-24".
// - "Mon-DD-YY" - "Jan-02-2024".
// - "DD-Mon-YYYY" - "02-Jan-2024".
// - "YYYY-MM-DD" - "2024-01-02".
// - "Month DD, YYYY" - "January 02, 2024".
// - "DD Month YYYY" - "02 January 2024".
// - "MM/DD/YYYY" - "01/02/2024".
// - "MM/DD/YY" - "01/02/24".
// - "MMDDYY" - "010224".
DatePickerMin PropertyName = "date-picker-min"
// DatePickerMax is the constant for "date-picker-max" property tag.
//
// Used by DatePicker.
// Maximum date value.
//
// Supported types: time.Time, string.
//
// Internal type is time.Time, other types converted to it during assignment.
//
// Conversion rules:
// string - values of this type parsed and converted to time.Time. The following formats are supported:
// - "YYYYMMDD" - "20240102".
// - "Mon-DD-YYYY" - "Jan-02-24".
// - "Mon-DD-YY" - "Jan-02-2024".
// - "DD-Mon-YYYY" - "02-Jan-2024".
// - "YYYY-MM-DD" - "2024-01-02".
// - "Month DD, YYYY" - "January 02, 2024".
// - "DD Month YYYY" - "02 January 2024".
// - "MM/DD/YYYY" - "01/02/2024".
// - "MM/DD/YY" - "01/02/24".
// - "MMDDYY" - "010224".
DatePickerMax PropertyName = "date-picker-max"
// DatePickerStep is the constant for "date-picker-step" property tag.
//
// Used by DatePicker.
// Date change step in days.
//
// Supported types: int, string.
//
// Values:
// positive value - Step value in days used to increment or decrement date.
DatePickerStep PropertyName = "date-picker-step"
// DatePickerValue is the constant for "date-picker-value" property tag.
//
// Used by DatePicker.
// Current value.
//
// Supported types: time.Time, string.
//
// Internal type is time.Time, other types converted to it during assignment.
//
// Conversion rules:
// string - values of this type parsed and converted to time.Time. The following formats are supported:
// - "YYYYMMDD" - "20240102".
// - "Mon-DD-YYYY" - "Jan-02-24".
// - "Mon-DD-YY" - "Jan-02-2024".
// - "DD-Mon-YYYY" - "02-Jan-2024".
// - "YYYY-MM-DD" - "2024-01-02".
// - "Month DD, YYYY" - "January 02, 2024".
// - "DD Month YYYY" - "02 January 2024".
// - "MM/DD/YYYY" - "01/02/2024".
// - "MM/DD/YY" - "01/02/24".
// - "MMDDYY" - "010224".
DatePickerValue PropertyName = "date-picker-value"
dateFormat = "2006-01-02"
)
// DatePicker - DatePicker view
// DatePicker represent a DatePicker view
type DatePicker interface {
View
}
type datePickerData struct {
viewData
dataList
dateChangedListeners []func(DatePicker, time.Time, time.Time)
}
// NewDatePicker create new DatePicker object and return it
@ -35,248 +130,164 @@ func NewDatePicker(session Session, params Params) DatePicker {
}
func newDatePicker(session Session) View {
return NewDatePicker(session, nil)
return new(datePickerData) // NewDatePicker(session, nil)
}
func (picker *datePickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "DatePicker"
picker.hasHtmlDisabled = true
picker.dateChangedListeners = []func(DatePicker, time.Time, time.Time){}
picker.dataListInit()
}
func (picker *datePickerData) String() string {
return getViewString(picker, nil)
picker.normalize = normalizeDatePickerTag
picker.set = picker.setFunc
picker.changed = picker.propertyChanged
}
func (picker *datePickerData) Focusable() bool {
return true
}
func (picker *datePickerData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeDatePickerTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Type, Min, Max, Step, Value:
return "date-picker-" + tag
}
return tag
return normalizeDataListTag(tag)
}
func (picker *datePickerData) Remove(tag string) {
picker.remove(picker.normalizeTag(tag))
}
func (picker *datePickerData) remove(tag string) {
switch tag {
case DateChangedEvent:
if len(picker.dateChangedListeners) > 0 {
picker.dateChangedListeners = []func(DatePicker, time.Time, time.Time){}
picker.propertyChangedEvent(tag)
}
return
case DatePickerMin:
delete(picker.properties, DatePickerMin)
if picker.created {
picker.session.removeProperty(picker.htmlID(), Min)
}
case DatePickerMax:
delete(picker.properties, DatePickerMax)
if picker.created {
picker.session.removeProperty(picker.htmlID(), Max)
}
case DatePickerStep:
delete(picker.properties, DatePickerStep)
if picker.created {
picker.session.removeProperty(picker.htmlID(), Step)
}
case DatePickerValue:
if _, ok := picker.properties[DatePickerValue]; ok {
oldDate := GetDatePickerValue(picker)
delete(picker.properties, DatePickerValue)
date := GetDatePickerValue(picker)
if picker.created {
picker.session.callFunc("setInputValue", picker.htmlID(), date.Format(dateFormat))
func stringToDate(value string) (time.Time, bool) {
format := "20060102"
if strings.ContainsRune(value, '-') {
if part := strings.Split(value, "-"); len(part) == 3 {
if part[0] != "" && part[0][0] > '9' {
if len(part[2]) == 2 {
format = "Jan-02-06"
} else {
format = "Jan-02-2006"
}
} else if part[1] != "" && part[1][0] > '9' {
format = "02-Jan-2006"
} else {
format = "2006-01-02"
}
for _, listener := range picker.dateChangedListeners {
listener(picker, date, oldDate)
}
} else if strings.ContainsRune(value, ' ') {
if part := strings.Split(value, " "); len(part) == 3 {
if part[0] != "" && part[0][0] > '9' {
format = "January 02, 2006"
} else {
format = "02 January 2006"
}
} else {
return
}
case DataList:
if len(picker.dataList.dataList) > 0 {
picker.setDataList(picker, []string{}, true)
} else if strings.ContainsRune(value, '/') {
if part := strings.Split(value, "/"); len(part) == 3 {
if len(part[2]) == 2 {
format = "01/02/06"
} else {
format = "01/02/2006"
}
}
default:
picker.viewData.remove(tag)
return
}
picker.propertyChangedEvent(tag)
}
func (picker *datePickerData) Set(tag string, value any) bool {
return picker.set(picker.normalizeTag(tag), value)
}
func (picker *datePickerData) set(tag string, value any) bool {
if value == nil {
picker.remove(tag)
return true
} else if len(value) == 6 {
format = "010206"
}
setTimeValue := func(tag string) (time.Time, bool) {
if date, err := time.Parse(format, value); err == nil {
return date, true
}
return time.Now(), false
}
func (picker *datePickerData) setFunc(tag PropertyName, value any) []PropertyName {
setDateValue := func(tag PropertyName) []PropertyName {
switch value := value.(type) {
case time.Time:
picker.properties[tag] = value
return value, true
picker.setRaw(tag, value)
return []PropertyName{tag}
case string:
if text, ok := picker.Session().resolveConstants(value); ok {
format := "20060102"
if strings.ContainsRune(text, '-') {
if part := strings.Split(text, "-"); len(part) == 3 {
if part[0] != "" && part[0][0] > '9' {
if len(part[2]) == 2 {
format = "Jan-02-06"
} else {
format = "Jan-02-2006"
}
} else if part[1] != "" && part[1][0] > '9' {
format = "02-Jan-2006"
} else {
format = "2006-01-02"
}
}
} else if strings.ContainsRune(text, ' ') {
if part := strings.Split(text, " "); len(part) == 3 {
if part[0] != "" && part[0][0] > '9' {
format = "January 02, 2006"
} else {
format = "02 January 2006"
}
}
} else if strings.ContainsRune(text, '/') {
if part := strings.Split(text, "/"); len(part) == 3 {
if len(part[2]) == 2 {
format = "01/02/06"
} else {
format = "01/02/2006"
}
}
} else if len(text) == 6 {
format = "010206"
}
if isConstantName(value) {
picker.setRaw(tag, value)
return []PropertyName{tag}
}
if date, err := time.Parse(format, text); err == nil {
picker.properties[tag] = value
return date, true
}
if date, ok := stringToDate(value); ok {
picker.setRaw(tag, date)
return []PropertyName{tag}
}
}
notCompatibleType(tag, value)
return time.Now(), false
return nil
}
switch tag {
case DatePickerMin, DatePickerMax:
return setDateValue(tag)
case DatePickerStep:
return setIntProperty(picker, DatePickerStep, value)
case DatePickerValue:
picker.setRaw("old-date", GetDatePickerValue(picker))
return setDateValue(tag)
case DateChangedEvent:
return setTwoArgEventListener[DatePicker, time.Time](picker, tag, value)
case DataList:
return setDataList(picker, value, dateFormat)
}
return picker.viewData.setFunc(tag, value)
}
func (picker *datePickerData) propertyChanged(tag PropertyName) {
session := picker.Session()
switch tag {
case DatePickerMin:
old, oldOK := getDateProperty(picker, DatePickerMin, Min)
if date, ok := setTimeValue(DatePickerMin); ok {
if !oldOK || date != old {
if picker.created {
picker.session.updateProperty(picker.htmlID(), Min, date.Format(dateFormat))
}
picker.propertyChangedEvent(tag)
}
return true
if date, ok := GetDatePickerMin(picker); ok {
session.updateProperty(picker.htmlID(), "min", date.Format(dateFormat))
} else {
session.removeProperty(picker.htmlID(), "min")
}
case DatePickerMax:
old, oldOK := getDateProperty(picker, DatePickerMax, Max)
if date, ok := setTimeValue(DatePickerMax); ok {
if !oldOK || date != old {
if picker.created {
picker.session.updateProperty(picker.htmlID(), Max, date.Format(dateFormat))
}
picker.propertyChangedEvent(tag)
}
return true
if date, ok := GetDatePickerMax(picker); ok {
session.updateProperty(picker.htmlID(), "max", date.Format(dateFormat))
} else {
session.removeProperty(picker.htmlID(), "max")
}
case DatePickerStep:
oldStep := GetDatePickerStep(picker)
if picker.setIntProperty(DatePickerStep, value) {
if step := GetDatePickerStep(picker); oldStep != step {
if picker.created {
if step > 0 {
picker.session.updateProperty(picker.htmlID(), Step, strconv.Itoa(step))
} else {
picker.session.removeProperty(picker.htmlID(), Step)
}
}
picker.propertyChangedEvent(tag)
}
return true
if step := GetDatePickerStep(picker); step > 0 {
session.updateProperty(picker.htmlID(), "step", strconv.Itoa(step))
} else {
session.removeProperty(picker.htmlID(), "step")
}
case DatePickerValue:
oldDate := GetDatePickerValue(picker)
if date, ok := setTimeValue(DatePickerValue); ok {
if date != oldDate {
if picker.created {
picker.session.callFunc("setInputValue", picker.htmlID(), date.Format(dateFormat))
date := GetDatePickerValue(picker)
session.callFunc("setInputValue", picker.htmlID(), date.Format(dateFormat))
if listeners := GetDateChangedListeners(picker); len(listeners) > 0 {
oldDate := time.Now()
if value := picker.getRaw("old-date"); value != nil {
if date, ok := value.(time.Time); ok {
oldDate = date
}
for _, listener := range picker.dateChangedListeners {
listener(picker, date, oldDate)
}
picker.propertyChangedEvent(tag)
}
return true
for _, listener := range listeners {
listener(picker, date, oldDate)
}
}
case DateChangedEvent:
listeners, ok := valueToEventWithOldListeners[DatePicker, time.Time](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(DatePicker, time.Time, time.Time){}
}
picker.dateChangedListeners = listeners
picker.propertyChangedEvent(tag)
return true
case DataList:
return picker.setDataList(picker, value, picker.created)
default:
return picker.viewData.set(tag, value)
}
return false
}
func (picker *datePickerData) Get(tag string) any {
return picker.get(picker.normalizeTag(tag))
}
func (picker *datePickerData) get(tag string) any {
switch tag {
case DateChangedEvent:
return picker.dateChangedListeners
case DataList:
return picker.dataList.dataList
default:
return picker.viewData.get(tag)
picker.viewData.propertyChanged(tag)
}
}
@ -285,7 +296,13 @@ func (picker *datePickerData) htmlTag() string {
}
func (picker *datePickerData) htmlSubviews(self View, buffer *strings.Builder) {
picker.dataListHtmlSubviews(self, buffer)
dataListHtmlSubviews(self, buffer, func(text string, session Session) string {
text, _ = session.resolveConstants(text)
if date, ok := stringToDate(text); ok {
return date.Format(dateFormat)
}
return text
})
}
func (picker *datePickerData) htmlProperties(self View, buffer *strings.Builder) {
@ -320,10 +337,10 @@ func (picker *datePickerData) htmlProperties(self View, buffer *strings.Builder)
buffer.WriteString(` onclick="stopEventPropagation(this, event)"`)
}
picker.dataListHtmlProperties(picker, buffer)
dataListHtmlProperties(picker, buffer)
}
func (picker *datePickerData) handleCommand(self View, command string, data DataObject) bool {
func (picker *datePickerData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command {
case "textChanged":
if text, ok := data.PropertyValue("text"); ok {
@ -331,9 +348,12 @@ func (picker *datePickerData) handleCommand(self View, command string, data Data
oldValue := GetDatePickerValue(picker)
picker.properties[DatePickerValue] = value
if value != oldValue {
for _, listener := range picker.dateChangedListeners {
for _, listener := range GetDateChangedListeners(picker) {
listener(picker, value, oldValue)
}
if listener, ok := picker.changeListener[DatePickerValue]; ok {
listener(picker, DatePickerValue)
}
}
}
}
@ -343,7 +363,7 @@ func (picker *datePickerData) handleCommand(self View, command string, data Data
return picker.viewData.handleCommand(self, command, data)
}
func getDateProperty(view View, mainTag, shortTag string) (time.Time, bool) {
func getDateProperty(view View, mainTag, shortTag PropertyName) (time.Time, bool) {
valueToTime := func(value any) (time.Time, bool) {
if value != nil {
switch value := value.(type) {
@ -352,7 +372,7 @@ func getDateProperty(view View, mainTag, shortTag string) (time.Time, bool) {
case string:
if text, ok := view.Session().resolveConstants(value); ok {
if result, err := time.Parse(dateFormat, text); err == nil {
if result, ok := stringToDate(text); ok {
return result, true
}
}
@ -366,9 +386,11 @@ func getDateProperty(view View, mainTag, shortTag string) (time.Time, bool) {
return result, true
}
if value := valueFromStyle(view, shortTag); value != nil {
if result, ok := valueToTime(value); ok {
return result, true
for _, tag := range []PropertyName{mainTag, shortTag} {
if value := valueFromStyle(view, tag); value != nil {
if result, ok := valueToTime(value); ok {
return result, true
}
}
}
}
@ -380,10 +402,7 @@ func getDateProperty(view View, mainTag, shortTag string) (time.Time, bool) {
// "false" as the second value otherwise.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetDatePickerMin(view View, subviewID ...string) (time.Time, bool) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
return getDateProperty(view, DatePickerMin, Min)
}
return time.Now(), false
@ -393,10 +412,7 @@ func GetDatePickerMin(view View, subviewID ...string) (time.Time, bool) {
// "false" as the second value otherwise.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetDatePickerMax(view View, subviewID ...string) (time.Time, bool) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
return getDateProperty(view, DatePickerMax, Max)
}
return time.Now(), false
@ -411,10 +427,7 @@ func GetDatePickerStep(view View, subviewID ...string) int {
// GetDatePickerValue returns the date of DatePicker subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetDatePickerValue(view View, subviewID ...string) time.Time {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view == nil {
if view = getSubview(view, subviewID); view == nil {
return time.Now()
}
date, _ := getDateProperty(view, DatePickerValue, Value)
@ -425,5 +438,5 @@ func GetDatePickerValue(view View, subviewID ...string) time.Time {
// 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.
func GetDateChangedListeners(view View, subviewID ...string) []func(DatePicker, time.Time, time.Time) {
return getEventWithOldListeners[DatePicker, time.Time](view, subviewID, DateChangedEvent)
return getTwoArgEventListeners[DatePicker, time.Time](view, subviewID, DateChangedEvent)
}

View File

@ -136,8 +136,8 @@ theme {
margin = 2px,
},
ruiCheckbox:focus {
margin = 0,
border = _{style = solid, color = @ruiHighlightColor, width = 2px },
outline = _{style = solid, color = @ruiHighlightColor, width = 2px },
outline-offset = -1px,
},
ruiListItem {
radius = 4px,

View File

@ -2,17 +2,44 @@ package rui
import "strings"
// Constants for [DetailsView] specific properties and events
const (
// Summary is the constant for the "summary" property tag.
// The contents of the "summary" property are used as the label for the disclosure widget.
Summary = "summary"
// Expanded is the constant for the "expanded" property tag.
// If the "expanded" boolean property is "true", then the content of view is visible.
// If the value is "false" then the content is collapsed.
Expanded = "expanded"
// Summary is the constant for "summary" property tag.
//
// Used by DetailsView.
// The content of this property is used as the label for the disclosure widget.
//
// Supported types:
// - string - Summary as a text.
// - View - Summary as a view, in this case it can be quite complex if needed.
Summary PropertyName = "summary"
// Expanded is the constant for "expanded" property tag.
//
// Used by DetailsView.
// Controls the content expanded state of the details view. Default value is false.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", or "1" - Content is visible.
// - false, 0, "false", "no", "off", or "0" - Content is collapsed (hidden).
Expanded PropertyName = "expanded"
// HideSummaryMarker is the constant for "hide-summary-marker" property tag.
//
// Used by DetailsView.
// Allows you to hide the summary marker (▶︎). Default value is false.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", or "1" - The summary marker is hidden.
// - false, 0, "false", "no", "off", or "0" - The summary marker is displayed (default value).
HideSummaryMarker PropertyName = "hide-summary-marker"
)
// DetailsView - collapsible container of View
// DetailsView represent a DetailsView view, which is a collapsible container of views
type DetailsView interface {
ViewsContainer
}
@ -30,19 +57,21 @@ func NewDetailsView(session Session, params Params) DetailsView {
}
func newDetailsView(session Session) View {
return NewDetailsView(session, nil)
return new(detailsViewData)
}
// Init initialize fields of DetailsView by default values
func (detailsView *detailsViewData) init(session Session) {
detailsView.viewsContainerData.init(session)
detailsView.tag = "DetailsView"
detailsView.set = detailsView.setFunc
detailsView.changed = detailsView.propertyChanged
//detailsView.systemClass = "ruiDetailsView"
}
func (detailsView *detailsViewData) Views() []View {
views := detailsView.viewsContainerData.Views()
if summary := detailsView.get(Summary); summary != nil {
if summary := detailsView.Get(Summary); summary != nil {
switch summary := summary.(type) {
case View:
return append([]View{summary}, views...)
@ -51,94 +80,53 @@ func (detailsView *detailsViewData) Views() []View {
return views
}
func (detailsView *detailsViewData) Remove(tag string) {
detailsView.remove(strings.ToLower(tag))
}
func (detailsView *detailsViewData) remove(tag string) {
detailsView.viewsContainerData.remove(tag)
if detailsView.created {
switch tag {
case Summary:
updateInnerHTML(detailsView.htmlID(), detailsView.Session())
case Expanded:
detailsView.session.removeProperty(detailsView.htmlID(), "open")
}
}
}
func (detailsView *detailsViewData) Set(tag string, value any) bool {
return detailsView.set(strings.ToLower(tag), value)
}
func (detailsView *detailsViewData) set(tag string, value any) bool {
if value == nil {
detailsView.remove(tag)
return true
}
func (detailsView *detailsViewData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case Summary:
switch value := value.(type) {
case string:
detailsView.properties[Summary] = value
detailsView.setRaw(Summary, value)
case View:
detailsView.properties[Summary] = value
detailsView.setRaw(Summary, value)
value.setParentID(detailsView.htmlID())
case DataObject:
if view := CreateViewFromObject(detailsView.Session(), value); view != nil {
detailsView.properties[Summary] = view
detailsView.setRaw(Summary, view)
view.setParentID(detailsView.htmlID())
} else {
return false
return nil
}
default:
notCompatibleType(tag, value)
return false
}
if detailsView.created {
updateInnerHTML(detailsView.htmlID(), detailsView.Session())
return nil
}
return []PropertyName{tag}
}
return detailsView.viewsContainerData.setFunc(tag, value)
}
func (detailsView *detailsViewData) propertyChanged(tag PropertyName) {
switch tag {
case Summary, HideSummaryMarker:
updateInnerHTML(detailsView.htmlID(), detailsView.Session())
case Expanded:
if !detailsView.setBoolProperty(tag, value) {
notCompatibleType(tag, value)
return false
}
if detailsView.created {
if IsDetailsExpanded(detailsView) {
detailsView.session.updateProperty(detailsView.htmlID(), "open", "")
} else {
detailsView.session.removeProperty(detailsView.htmlID(), "open")
}
if IsDetailsExpanded(detailsView) {
detailsView.Session().updateProperty(detailsView.htmlID(), "open", "")
} else {
detailsView.Session().removeProperty(detailsView.htmlID(), "open")
}
case NotTranslate:
if !detailsView.viewData.set(tag, value) {
return false
}
if detailsView.created {
updateInnerHTML(detailsView.htmlID(), detailsView.Session())
}
updateInnerHTML(detailsView.htmlID(), detailsView.Session())
default:
return detailsView.viewsContainerData.Set(tag, value)
detailsView.viewsContainerData.propertyChanged(tag)
}
detailsView.propertyChangedEvent(tag)
return true
}
func (detailsView *detailsViewData) Get(tag string) any {
return detailsView.get(strings.ToLower(tag))
}
func (detailsView *detailsViewData) get(tag string) any {
return detailsView.viewsContainerData.get(tag)
}
func (detailsView *detailsViewData) htmlTag() string {
@ -154,31 +142,59 @@ func (detailsView *detailsViewData) htmlProperties(self View, buffer *strings.Bu
}
func (detailsView *detailsViewData) htmlSubviews(self View, buffer *strings.Builder) {
summary := false
hidden := IsSummaryMarkerHidden(detailsView)
if value, ok := detailsView.properties[Summary]; ok {
switch value := value.(type) {
case string:
if !GetNotTranslate(detailsView) {
value, _ = detailsView.session.GetString(value)
}
buffer.WriteString("<summary>")
if hidden {
buffer.WriteString(`<summary class="hiddenMarker">`)
} else {
buffer.WriteString("<summary>")
}
buffer.WriteString(value)
buffer.WriteString("</summary>")
summary = true
case View:
buffer.WriteString("<summary>")
viewHTML(value, buffer)
buffer.WriteString("</summary>")
if hidden {
buffer.WriteString(`<summary class="hiddenMarker">`)
viewHTML(value, buffer, "")
buffer.WriteString("</summary>")
} else if value.htmlTag() == "div" {
viewHTML(value, buffer, "summary")
} else {
buffer.WriteString(`<summary><div style="display: inline-block;">`)
viewHTML(value, buffer, "")
buffer.WriteString("</div></summary>")
}
summary = true
}
}
if !summary {
if hidden {
buffer.WriteString(`<summary class="hiddenMarker"></summary>`)
} else {
buffer.WriteString("<summary></summary>")
}
}
detailsView.viewsContainerData.htmlSubviews(self, buffer)
}
func (detailsView *detailsViewData) handleCommand(self View, command string, data DataObject) bool {
func (detailsView *detailsViewData) handleCommand(self View, command PropertyName, data DataObject) bool {
if command == "details-open" {
if n, ok := dataIntProperty(data, "open"); ok {
detailsView.properties[Expanded] = (n != 0)
detailsView.propertyChangedEvent(Expanded)
if listener, ok := detailsView.changeListener[Expanded]; ok {
listener(detailsView, Expanded)
}
}
return true
}
@ -188,10 +204,7 @@ func (detailsView *detailsViewData) handleCommand(self View, command string, dat
// GetDetailsSummary returns a value of the Summary property of DetailsView.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetDetailsSummary(view View, subviewID ...string) View {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
if value := view.Get(Summary); value != nil {
switch value := value.(type) {
case string:
@ -210,3 +223,9 @@ func GetDetailsSummary(view View, subviewID ...string) View {
func IsDetailsExpanded(view View, subviewID ...string) bool {
return boolStyledProperty(view, subviewID, Expanded, false)
}
// IsDetailsExpanded returns a value of the HideSummaryMarker property of DetailsView.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func IsSummaryMarkerHidden(view View, subviewID ...string) bool {
return boolStyledProperty(view, subviewID, HideSummaryMarker, false)
}

View File

@ -1,27 +1,37 @@
package rui
import (
"fmt"
"strconv"
"strings"
)
// DropDownEvent is the constant for "drop-down-event" property tag.
// The "drop-down-event" event occurs when a list item becomes selected.
// The main listener format: func(DropDownList, int), where the second argument is the item index.
const DropDownEvent = "drop-down-event"
//
// Used by DropDownList.
// Occur when a list item becomes selected.
//
// General listener format:
//
// func(list rui.DropDownList, index int)
//
// where:
// - list - Interface of a drop down list which generated this event,
// - index - Index of a newly selected item.
//
// Allowed listener formats:
//
// func(index int)
// func(list rui.DropDownList)
// func()
const DropDownEvent PropertyName = "drop-down-event"
// DropDownList - the interface of a drop-down list view
// DropDownList represent a DropDownList view
type DropDownList interface {
View
getItems() []string
}
type dropDownListData struct {
viewData
items []string
disabledItems []any
dropDownListener []func(DropDownList, int, int)
}
// NewDropDownList create new DropDownList object and return it
@ -33,135 +43,76 @@ func NewDropDownList(session Session, params Params) DropDownList {
}
func newDropDownList(session Session) View {
return NewDropDownList(session, nil)
return new(dropDownListData)
}
func (list *dropDownListData) init(session Session) {
list.viewData.init(session)
list.tag = "DropDownList"
list.hasHtmlDisabled = true
list.items = []string{}
list.disabledItems = []any{}
list.dropDownListener = []func(DropDownList, int, int){}
}
func (list *dropDownListData) String() string {
return getViewString(list, nil)
list.normalize = normalizeDropDownListTag
list.set = list.setFunc
list.changed = list.propertyChanged
}
func (list *dropDownListData) Focusable() bool {
return true
}
func (list *dropDownListData) Remove(tag string) {
list.remove(strings.ToLower(tag))
func normalizeDropDownListTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
if tag == "separators" {
return ItemSeparators
}
return tag
}
func (list *dropDownListData) remove(tag string) {
func (list *dropDownListData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case Items:
if len(list.items) > 0 {
list.items = []string{}
if list.created {
updateInnerHTML(list.htmlID(), list.session)
}
list.propertyChangedEvent(tag)
if items, ok := anyToStringArray(value, ""); ok {
return setArrayPropertyValue(list, tag, items)
}
notCompatibleType(Items, value)
return nil
case DisabledItems:
if len(list.disabledItems) > 0 {
list.disabledItems = []any{}
if list.created {
updateInnerHTML(list.htmlID(), list.session)
}
list.propertyChangedEvent(tag)
case DisabledItems, ItemSeparators:
if items, ok := parseIndicesArray(value); ok {
return setArrayPropertyValue(list, tag, items)
}
notCompatibleType(tag, value)
return nil
case DropDownEvent:
if len(list.dropDownListener) > 0 {
list.dropDownListener = []func(DropDownList, int, int){}
list.propertyChangedEvent(tag)
}
return setTwoArgEventListener[DropDownList, int](list, tag, value)
case Current:
oldCurrent := GetCurrent(list)
delete(list.properties, Current)
if oldCurrent != 0 {
if list.created {
list.session.callFunc("selectDropDownListItem", list.htmlID(), 0)
}
list.onSelectedItemChanged(0, oldCurrent)
list.setRaw("old-current", GetCurrent(list))
return setIntProperty(list, Current, value)
}
return list.viewData.setFunc(tag, value)
}
func (list *dropDownListData) propertyChanged(tag PropertyName) {
switch tag {
case Items, DisabledItems, ItemSeparators:
updateInnerHTML(list.htmlID(), list.Session())
case Current:
current := GetCurrent(list)
list.Session().callFunc("selectDropDownListItem", list.htmlID(), current)
oldCurrent, _ := intProperty(list, "old-current", list.Session(), -1)
for _, listener := range GetDropDownListeners(list) {
listener(list, current, oldCurrent)
}
default:
list.viewData.remove(tag)
return
list.viewData.propertyChanged(tag)
}
}
func (list *dropDownListData) Set(tag string, value any) bool {
return list.set(strings.ToLower(tag), value)
}
func (list *dropDownListData) set(tag string, value any) bool {
if value == nil {
list.remove(tag)
return true
}
switch tag {
case Items:
return list.setItems(value)
case DisabledItems:
return list.setDisabledItems(value)
case DropDownEvent:
listeners, ok := valueToEventWithOldListeners[DropDownList, int](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(DropDownList, int, int){}
}
list.dropDownListener = listeners
list.propertyChangedEvent(tag)
return true
case Current:
oldCurrent := GetCurrent(list)
if !list.setIntProperty(Current, value) {
return false
}
if current := GetCurrent(list); oldCurrent != current {
if list.created {
list.session.callFunc("selectDropDownListItem", list.htmlID(), current)
}
list.onSelectedItemChanged(current, oldCurrent)
}
return true
}
return list.viewData.set(tag, value)
}
func (list *dropDownListData) setItems(value any) bool {
items, ok := anyToStringArray(value)
if !ok {
notCompatibleType(Items, value)
return false
}
list.items = items
if list.created {
updateInnerHTML(list.htmlID(), list.session)
}
list.propertyChangedEvent(Items)
return true
}
func intArrayToStringArray[T int | uint | int8 | uint8 | int16 | uint16 | int32 | uint32 | int64 | uint64](array []T) []string {
items := make([]string, len(array))
for i, val := range array {
@ -170,14 +121,60 @@ func intArrayToStringArray[T int | uint | int8 | uint8 | int16 | uint16 | int32
return items
}
func anyToStringArray(value any) ([]string, bool) {
func parseIndicesArray(value any) ([]any, bool) {
switch value := value.(type) {
case string:
return []string{value}, true
case int:
return []any{value}, true
case []int:
items := make([]any, len(value))
for i, n := range value {
items[i] = n
}
return items, true
case []any:
items := make([]any, 0, len(value))
for _, val := range value {
if val != nil {
switch val := val.(type) {
case string:
if isConstantName(val) {
items = append(items, val)
} else if n, err := strconv.Atoi(val); err == nil {
items = append(items, n)
} else {
return nil, false
}
default:
if n, ok := isInt(val); ok {
items = append(items, n)
} else {
return nil, false
}
}
}
}
return items, true
case []string:
return value, true
items := make([]any, 0, len(value))
for _, str := range value {
if str = strings.Trim(str, " \t"); str != "" {
if isConstantName(str) {
items = append(items, str)
} else if n, err := strconv.Atoi(str); err == nil {
items = append(items, n)
} else {
return nil, false
}
}
}
return items, true
case string:
return parseIndicesArray(strings.Split(value, ","))
case []DataValue:
items := make([]string, 0, len(value))
@ -186,242 +183,10 @@ func anyToStringArray(value any) ([]string, bool) {
items = append(items, val.Value())
}
}
return items, true
case []fmt.Stringer:
items := make([]string, len(value))
for i, str := range value {
items[i] = str.String()
}
return items, true
case []Color:
items := make([]string, len(value))
for i, str := range value {
items[i] = str.String()
}
return items, true
case []SizeUnit:
items := make([]string, len(value))
for i, str := range value {
items[i] = str.String()
}
return items, true
case []AngleUnit:
items := make([]string, len(value))
for i, str := range value {
items[i] = str.String()
}
return items, true
case []float32:
items := make([]string, len(value))
for i, val := range value {
items[i] = fmt.Sprintf("%g", float64(val))
}
return items, true
case []float64:
items := make([]string, len(value))
for i, val := range value {
items[i] = fmt.Sprintf("%g", val)
}
return items, true
case []int:
return intArrayToStringArray(value), true
case []uint:
return intArrayToStringArray(value), true
case []int8:
return intArrayToStringArray(value), true
case []uint8:
return intArrayToStringArray(value), true
case []int16:
return intArrayToStringArray(value), true
case []uint16:
return intArrayToStringArray(value), true
case []int32:
return intArrayToStringArray(value), true
case []uint32:
return intArrayToStringArray(value), true
case []int64:
return intArrayToStringArray(value), true
case []uint64:
return intArrayToStringArray(value), true
case []bool:
items := make([]string, len(value))
for i, val := range value {
if val {
items[i] = "true"
} else {
items[i] = "false"
}
}
return items, true
case []any:
items := make([]string, 0, len(value))
for _, v := range value {
switch val := v.(type) {
case string:
items = append(items, val)
case fmt.Stringer:
items = append(items, val.String())
case bool:
if val {
items = append(items, "true")
} else {
items = append(items, "false")
}
case float32:
items = append(items, fmt.Sprintf("%g", float64(val)))
case float64:
items = append(items, fmt.Sprintf("%g", val))
case rune:
items = append(items, string(val))
default:
if n, ok := isInt(v); ok {
items = append(items, strconv.Itoa(n))
} else {
return []string{}, false
}
}
}
return items, true
return parseIndicesArray(items)
}
return []string{}, false
}
func (list *dropDownListData) setDisabledItems(value any) bool {
switch value := value.(type) {
case []int:
list.disabledItems = make([]any, len(value))
for i, n := range value {
list.disabledItems[i] = n
}
case []any:
disabledItems := make([]any, len(value))
for i, val := range value {
if val == nil {
notCompatibleType(DisabledItems, value)
return false
}
switch val := val.(type) {
case string:
if isConstantName(val) {
disabledItems[i] = val
} else {
n, err := strconv.Atoi(val)
if err != nil {
notCompatibleType(DisabledItems, value)
return false
}
disabledItems[i] = n
}
default:
if n, ok := isInt(val); ok {
disabledItems[i] = n
} else {
notCompatibleType(DisabledItems, value)
return false
}
}
}
list.disabledItems = disabledItems
case string:
values := strings.Split(value, ",")
disabledItems := make([]any, len(values))
for i, str := range values {
str = strings.Trim(str, " ")
if str == "" {
notCompatibleType(DisabledItems, value)
return false
}
if isConstantName(str) {
disabledItems[i] = str
} else {
n, err := strconv.Atoi(str)
if err != nil {
notCompatibleType(DisabledItems, value)
return false
}
disabledItems[i] = n
}
}
list.disabledItems = disabledItems
case []DataValue:
disabledItems := make([]string, 0, len(value))
for _, val := range value {
if !val.IsObject() {
disabledItems = append(disabledItems, val.Value())
}
}
return list.setDisabledItems(disabledItems)
default:
notCompatibleType(DisabledItems, value)
return false
}
if list.created {
updateInnerHTML(list.htmlID(), list.session)
}
list.propertyChangedEvent(Items)
return true
}
func (list *dropDownListData) Get(tag string) any {
return list.get(strings.ToLower(tag))
}
func (list *dropDownListData) get(tag string) any {
switch tag {
case Items:
return list.items
case DisabledItems:
return list.disabledItems
case Current:
result, _ := intProperty(list, Current, list.session, 0)
return result
case DropDownEvent:
return list.dropDownListener
}
return list.viewData.get(tag)
}
func (list *dropDownListData) getItems() []string {
return list.items
return nil, false
}
func (list *dropDownListData) htmlTag() string {
@ -429,11 +194,12 @@ func (list *dropDownListData) htmlTag() string {
}
func (list *dropDownListData) htmlSubviews(self View, buffer *strings.Builder) {
if list.items != nil {
if items := GetDropDownItems(list); len(items) > 0 {
current := GetCurrent(list)
notTranslate := GetNotTranslate(list)
disabledItems := GetDropDownDisabledItems(list)
for i, item := range list.items {
separators := GetDropDownItemSeparators(list)
for i, item := range items {
disabled := false
for _, index := range disabledItems {
if i == index {
@ -455,6 +221,12 @@ func (list *dropDownListData) htmlSubviews(self View, buffer *strings.Builder) {
buffer.WriteString(item)
buffer.WriteString("</option>")
for _, index := range separators {
if i == index {
buffer.WriteString("<hr>")
break
}
}
}
}
}
@ -464,22 +236,21 @@ func (list *dropDownListData) htmlProperties(self View, buffer *strings.Builder)
buffer.WriteString(` size="1" onchange="dropDownListEvent(this, event)"`)
}
func (list *dropDownListData) onSelectedItemChanged(number, old int) {
for _, listener := range list.dropDownListener {
listener(list, number, old)
}
list.propertyChangedEvent(Current)
}
func (list *dropDownListData) handleCommand(self View, command string, data DataObject) bool {
func (list *dropDownListData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command {
case "itemSelected":
if text, ok := data.PropertyValue("number"); ok {
if number, err := strconv.Atoi(text); err == nil {
if GetCurrent(list) != number && number >= 0 && number < len(list.items) {
items := GetDropDownItems(list)
if GetCurrent(list) != number && number >= 0 && number < len(items) {
old := GetCurrent(list)
list.properties[Current] = number
list.onSelectedItemChanged(number, old)
for _, listener := range GetDropDownListeners(list) {
listener(list, number, old)
}
if listener, ok := list.changeListener[Current]; ok {
listener(list, Current)
}
}
} else {
ErrorLog(err.Error())
@ -495,32 +266,25 @@ 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.
// 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, int) {
return getEventWithOldListeners[DropDownList, int](view, subviewID, DropDownEvent)
return getTwoArgEventListeners[DropDownList, int](view, subviewID, DropDownEvent)
}
// GetDropDownItems return the DropDownList items list.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetDropDownItems(view View, subviewID ...string) []string {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if list, ok := view.(DropDownList); ok {
return list.getItems()
if view = getSubview(view, subviewID); view != nil {
if value := view.Get(Items); value != nil {
if items, ok := value.([]string); ok {
return items
}
}
}
return []string{}
}
// GetDropDownDisabledItems return the list of DropDownList disabled item indexes.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetDropDownDisabledItems(view View, subviewID ...string) []int {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
func getIndicesArray(view View, tag PropertyName) []int {
if view != nil {
if value := view.Get(DisabledItems); value != nil {
if value := view.Get(tag); value != nil {
if values, ok := value.([]any); ok {
count := len(values)
if count > 0 {
@ -547,3 +311,17 @@ func GetDropDownDisabledItems(view View, subviewID ...string) []int {
}
return []int{}
}
// GetDropDownDisabledItems return an array of disabled(non selectable) items indices of DropDownList.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetDropDownDisabledItems(view View, subviewID ...string) []int {
view = getSubview(view, subviewID)
return getIndicesArray(view, DisabledItems)
}
// GetDropDownItemSeparators return an array of indices of DropDownList items after which a separator should be added.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetDropDownItemSeparators(view View, subviewID ...string) []int {
view = getSubview(view, subviewID)
return getIndicesArray(view, ItemSeparators)
}

View File

@ -5,47 +5,103 @@ import (
"strings"
)
// Constants for [EditView] specific properties and events
const (
// EditTextChangedEvent is the constant for the "edit-text-changed" property tag.
EditTextChangedEvent = "edit-text-changed"
// EditTextChangedEvent is the constant for "edit-text-changed" property tag.
//
// Used by EditView.
// Occur when edit view text has been changed.
//
// General listener format:
// func(editView rui.EditView, newText string, oldText string).
//
// where:
// - editView - Interface of an edit view which generated this event,
// - newText - New edit view text,
// - oldText - Previous edit view text.
//
// Allowed listener formats:
// - func(editView rui.EditView, newText string)
// - func(newText string, oldText string)
// - func(newText string)
// - func(editView rui.EditView)
// - func()
EditTextChangedEvent PropertyName = "edit-text-changed"
// EditViewType is the constant for the "edit-view-type" property tag.
EditViewType = "edit-view-type"
// EditViewType is the constant for "edit-view-type" property tag.
//
// Used by EditView.
// Type of the text input. Default value is "text".
//
// Supported types: int, string.
//
// Values:
// - 0 (SingleLineText) or "text" - One-line text editor.
// - 1 (PasswordText) or "password" - Password editor. The text is hidden by asterisks.
// - 2 (EmailText) or "email" - Single e-mail editor.
// - 3 (EmailsText) or "emails" - Multiple e-mail editor.
// - 4 (URLText) or "url" - Internet address input editor.
// - 5 (PhoneText) or "phone" - Phone number editor.
// - 6 (MultiLineText) or "multiline" - Multi-line text editor.
EditViewType PropertyName = "edit-view-type"
// EditViewPattern is the constant for the "edit-view-pattern" property tag.
EditViewPattern = "edit-view-pattern"
// EditViewPattern is the constant for "edit-view-pattern" property tag.
//
// Used by EditView.
// Regular expression to limit editing of a text.
//
// Supported types: string.
EditViewPattern PropertyName = "edit-view-pattern"
// Spellcheck is the constant for the "spellcheck" property tag.
Spellcheck = "spellcheck"
// Spellcheck is the constant for "spellcheck" property tag.
//
// Used by EditView.
// Enable or disable spell checker. Available in SingleLineText and MultiLineText types of edit view. Default value is
// false.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", "1" - Enable spell checker for text.
// - false, 0, "false", "no", "off", "0" - Disable spell checker for text.
Spellcheck PropertyName = "spellcheck"
)
// Constants for the values of an [EditView] "edit-view-type" property
const (
// SingleLineText - single-line text type of EditView
SingleLineText = 0
// PasswordText - password type of EditView
PasswordText = 1
// EmailText - e-mail type of EditView. Allows to enter one email
EmailText = 2
// EmailsText - e-mail type of EditView. Allows to enter multiple emails separated by comma
EmailsText = 3
// URLText - url type of EditView. Allows to enter one url
URLText = 4
// PhoneText - telephone type of EditView. Allows to enter one phone number
PhoneText = 5
// MultiLineText - multi-line text type of EditView
MultiLineText = 6
)
// EditView - grid-container of View
// EditView represent an EditView view
type EditView interface {
View
// AppendText appends text to the current text of an EditView view
AppendText(text string)
textChanged(newText, oldText string)
}
type editViewData struct {
viewData
dataList
textChangeListeners []func(EditView, string, string)
}
// NewEditView create new EditView object and return it
@ -57,27 +113,24 @@ func NewEditView(session Session, params Params) EditView {
}
func newEditView(session Session) View {
return NewEditView(session, nil)
return new(editViewData) // NewEditView(session, nil)
}
func (edit *editViewData) init(session Session) {
edit.viewData.init(session)
edit.hasHtmlDisabled = true
edit.textChangeListeners = []func(EditView, string, string){}
edit.tag = "EditView"
edit.dataListInit()
}
func (edit *editViewData) String() string {
return getViewString(edit, nil)
edit.normalize = normalizeEditViewTag
edit.set = edit.setFunc
edit.changed = edit.propertyChanged
}
func (edit *editViewData) Focusable() bool {
return true
}
func (edit *editViewData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeEditViewTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Type, "edit-type":
return EditViewType
@ -92,279 +145,107 @@ func (edit *editViewData) normalizeTag(tag string) string {
return EditWrap
}
return edit.normalizeDataListTag(tag)
return normalizeDataListTag(tag)
}
func (edit *editViewData) Remove(tag string) {
edit.remove(edit.normalizeTag(tag))
}
func (edit *editViewData) remove(tag string) {
_, exists := edit.properties[tag]
func (edit *editViewData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case Hint:
if exists {
delete(edit.properties, Hint)
if edit.created {
edit.session.removeProperty(edit.htmlID(), "placeholder")
}
edit.propertyChangedEvent(tag)
}
case MaxLength:
if exists {
delete(edit.properties, MaxLength)
if edit.created {
edit.session.removeProperty(edit.htmlID(), "maxlength")
}
edit.propertyChangedEvent(tag)
}
case ReadOnly, Spellcheck:
if exists {
delete(edit.properties, tag)
if edit.created {
edit.session.updateProperty(edit.htmlID(), tag, false)
}
edit.propertyChangedEvent(tag)
}
case EditTextChangedEvent:
if len(edit.textChangeListeners) > 0 {
edit.textChangeListeners = []func(EditView, string, string){}
edit.propertyChangedEvent(tag)
}
case Text:
if exists {
oldText := GetText(edit)
delete(edit.properties, tag)
if oldText != "" {
edit.textChanged("", oldText)
if edit.created {
edit.session.callFunc("setInputValue", edit.htmlID(), "")
if text, ok := value.(string); ok {
old := ""
if val := edit.getRaw(Text); val != nil {
if txt, ok := val.(string); ok {
old = txt
}
}
edit.setRaw("old-text", old)
edit.setRaw(tag, text)
return []PropertyName{tag}
}
case EditViewPattern:
if exists {
oldText := GetEditViewPattern(edit)
delete(edit.properties, tag)
if oldText != "" {
if edit.created {
edit.session.removeProperty(edit.htmlID(), Pattern)
}
edit.propertyChangedEvent(tag)
}
}
notCompatibleType(tag, value)
return nil
case EditViewType:
if exists {
oldType := GetEditViewType(edit)
delete(edit.properties, tag)
if oldType != 0 {
if edit.created {
updateInnerHTML(edit.parentHTMLID(), edit.session)
}
edit.propertyChangedEvent(tag)
}
}
case EditWrap:
if exists {
oldWrap := IsEditViewWrap(edit)
delete(edit.properties, tag)
if GetEditViewType(edit) == MultiLineText {
if wrap := IsEditViewWrap(edit); wrap != oldWrap {
if edit.created {
if wrap {
edit.session.updateProperty(edit.htmlID(), "wrap", "soft")
} else {
edit.session.updateProperty(edit.htmlID(), "wrap", "off")
}
}
edit.propertyChangedEvent(tag)
}
}
case Hint:
if text, ok := value.(string); ok {
return setStringPropertyValue(edit, tag, strings.Trim(text, " \t\n"))
}
notCompatibleType(tag, value)
return nil
case DataList:
if len(edit.dataList.dataList) > 0 {
edit.setDataList(edit, []string{}, true)
}
setDataList(edit, value, "")
default:
edit.viewData.remove(tag)
case EditTextChangedEvent:
return setTwoArgEventListener[EditView, string](edit, tag, value)
}
return edit.viewData.setFunc(tag, value)
}
func (edit *editViewData) Set(tag string, value any) bool {
return edit.set(edit.normalizeTag(tag), value)
}
func (edit *editViewData) set(tag string, value any) bool {
if value == nil {
edit.remove(tag)
return true
}
func (edit *editViewData) propertyChanged(tag PropertyName) {
session := edit.Session()
switch tag {
case Text:
if text, ok := value.(string); ok {
oldText := GetText(edit)
edit.properties[Text] = text
if text = GetText(edit); oldText != text {
edit.textChanged(text, oldText)
if edit.created {
edit.session.callFunc("setInputValue", edit.htmlID(), text)
}
text := GetText(edit)
session.callFunc("setInputValue", edit.htmlID(), text)
old := ""
if val := edit.getRaw("old-text"); val != nil {
if txt, ok := val.(string); ok {
old = txt
}
return true
}
return false
edit.textChanged(text, old)
case Hint:
if text, ok := value.(string); ok {
oldText := GetHint(edit)
edit.properties[Hint] = text
if text = GetHint(edit); oldText != text {
if edit.created {
if text != "" {
edit.session.updateProperty(edit.htmlID(), "placeholder", text)
} else {
edit.session.removeProperty(edit.htmlID(), "placeholder")
}
}
edit.propertyChangedEvent(tag)
}
return true
if text := GetHint(edit); text != "" {
session.updateProperty(edit.htmlID(), "placeholder", text)
} else {
session.removeProperty(edit.htmlID(), "placeholder")
}
return false
case MaxLength:
oldMaxLength := GetMaxLength(edit)
if edit.setIntProperty(MaxLength, value) {
if maxLength := GetMaxLength(edit); maxLength != oldMaxLength {
if edit.created {
if maxLength > 0 {
edit.session.updateProperty(edit.htmlID(), "maxlength", strconv.Itoa(maxLength))
} else {
edit.session.removeProperty(edit.htmlID(), "maxlength")
}
}
edit.propertyChangedEvent(tag)
}
return true
if maxLength := GetMaxLength(edit); maxLength > 0 {
session.updateProperty(edit.htmlID(), "maxlength", strconv.Itoa(maxLength))
} else {
session.removeProperty(edit.htmlID(), "maxlength")
}
return false
case ReadOnly:
if edit.setBoolProperty(ReadOnly, value) {
if edit.created {
if IsReadOnly(edit) {
edit.session.updateProperty(edit.htmlID(), ReadOnly, "")
} else {
edit.session.removeProperty(edit.htmlID(), ReadOnly)
}
}
edit.propertyChangedEvent(tag)
return true
if IsReadOnly(edit) {
session.updateProperty(edit.htmlID(), "readonly", "")
} else {
session.removeProperty(edit.htmlID(), "readonly")
}
return false
case Spellcheck:
if edit.setBoolProperty(Spellcheck, value) {
if edit.created {
edit.session.updateProperty(edit.htmlID(), Spellcheck, IsSpellcheck(edit))
}
edit.propertyChangedEvent(tag)
return true
}
return false
session.updateProperty(edit.htmlID(), "spellcheck", IsSpellcheck(edit))
case EditViewPattern:
oldText := GetEditViewPattern(edit)
if text, ok := value.(string); ok {
edit.properties[EditViewPattern] = text
if text = GetEditViewPattern(edit); oldText != text {
if edit.created {
if text != "" {
edit.session.updateProperty(edit.htmlID(), Pattern, text)
} else {
edit.session.removeProperty(edit.htmlID(), Pattern)
}
}
edit.propertyChangedEvent(tag)
}
return true
if text := GetEditViewPattern(edit); text != "" {
session.updateProperty(edit.htmlID(), "pattern", text)
} else {
session.removeProperty(edit.htmlID(), "pattern")
}
return false
case EditViewType:
oldType := GetEditViewType(edit)
if edit.setEnumProperty(EditViewType, value, enumProperties[EditViewType].values) {
if GetEditViewType(edit) != oldType {
if edit.created {
updateInnerHTML(edit.parentHTMLID(), edit.session)
}
edit.propertyChangedEvent(tag)
}
return true
}
return false
updateInnerHTML(edit.parentHTMLID(), session)
case EditWrap:
oldWrap := IsEditViewWrap(edit)
if edit.setBoolProperty(EditWrap, value) {
if GetEditViewType(edit) == MultiLineText {
if wrap := IsEditViewWrap(edit); wrap != oldWrap {
if edit.created {
if wrap {
edit.session.updateProperty(edit.htmlID(), "wrap", "soft")
} else {
edit.session.updateProperty(edit.htmlID(), "wrap", "off")
}
}
edit.propertyChangedEvent(tag)
}
}
return true
if wrap := IsEditViewWrap(edit); wrap {
session.updateProperty(edit.htmlID(), "wrap", "soft")
} else {
session.updateProperty(edit.htmlID(), "wrap", "off")
}
return false
case DataList:
return edit.setDataList(edit, value, edit.created)
updateInnerHTML(edit.htmlID(), session)
case EditTextChangedEvent:
listeners, ok := valueToEventWithOldListeners[EditView, string](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(EditView, string, string){}
}
edit.textChangeListeners = listeners
edit.propertyChangedEvent(tag)
return true
default:
edit.viewData.propertyChanged(tag)
}
return edit.viewData.set(tag, value)
}
func (edit *editViewData) Get(tag string) any {
return edit.get(edit.normalizeTag(tag))
}
func (edit *editViewData) get(tag string) any {
switch tag {
case EditTextChangedEvent:
return edit.textChangeListeners
case DataList:
return edit.dataList.dataList
}
return edit.viewData.get(tag)
}
func (edit *editViewData) AppendText(text string) {
@ -375,21 +256,24 @@ func (edit *editViewData) AppendText(text string) {
textValue += text
edit.properties[Text] = textValue
edit.session.callFunc("appendToInnerHTML", edit.htmlID(), text)
edit.session.callFunc("appendToInputValue", edit.htmlID(), text)
edit.textChanged(textValue, oldText)
return
}
}
edit.set(Text, text)
edit.setRaw(Text, text)
} else {
edit.set(Text, GetText(edit)+text)
edit.setRaw(Text, GetText(edit)+text)
}
}
func (edit *editViewData) textChanged(newText, oldText string) {
for _, listener := range edit.textChangeListeners {
for _, listener := range GetTextChangedListeners(edit) {
listener(edit, newText, oldText)
}
edit.propertyChangedEvent(Text)
if listener, ok := edit.changeListener[Text]; ok {
listener(edit, Text)
}
}
func (edit *editViewData) htmlTag() string {
@ -405,7 +289,9 @@ func (edit *editViewData) htmlSubviews(self View, buffer *strings.Builder) {
buffer.WriteString(text)
}
}
edit.dataListHtmlSubviews(self, buffer)
dataListHtmlSubviews(self, buffer, func(text string, session Session) string {
return text
})
}
func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) {
@ -490,16 +376,16 @@ func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) {
}
}
edit.dataListHtmlProperties(edit, buffer)
dataListHtmlProperties(edit, buffer)
}
func (edit *editViewData) handleCommand(self View, command string, data DataObject) bool {
func (edit *editViewData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command {
case "textChanged":
oldText := GetText(edit)
if text, ok := data.PropertyValue("text"); ok {
edit.properties[Text] = text
if text := GetText(edit); text != oldText {
edit.setRaw(Text, text)
if text != oldText {
edit.textChanged(text, oldText)
}
}
@ -512,10 +398,7 @@ func (edit *editViewData) handleCommand(self View, command string, data DataObje
// GetText returns a text of the EditView subview.
// If the second argument (subviewID) is not specified or it is "" then a text of the first argument (view) is returned.
func GetText(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(Text); value != nil {
if text, ok := value.(string); ok {
return text
@ -528,9 +411,7 @@ func GetText(view View, subviewID ...string) string {
// GetHint returns a hint text of the subview.
// If the second argument (subviewID) is not specified or it is "" then a text of the first argument (view) is returned.
func GetHint(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
view = getSubview(view, subviewID)
session := view.Session()
text := ""
@ -579,7 +460,7 @@ func IsSpellcheck(view View, subviewID ...string) bool {
// 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.
func GetTextChangedListeners(view View, subviewID ...string) []func(EditView, string, string) {
return getEventWithOldListeners[EditView, string](view, subviewID, EditTextChangedEvent)
return getTwoArgEventListeners[EditView, string](view, subviewID, EditTextChangedEvent)
}
// GetEditViewType returns a value of the Type property of EditView.
@ -591,10 +472,7 @@ func GetEditViewType(view View, subviewID ...string) int {
// GetEditViewPattern returns a value of the Pattern property of EditView.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetEditViewPattern(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
if pattern, ok := stringProperty(view, EditViewPattern, view.Session()); ok {
return pattern
}

517
events.go Normal file
View File

@ -0,0 +1,517 @@
package rui
import "strings"
var eventJsFunc = map[PropertyName]struct{ jsEvent, jsFunc string }{
FocusEvent: {jsEvent: "onfocus", jsFunc: "focusEvent"},
LostFocusEvent: {jsEvent: "onblur", jsFunc: "blurEvent"},
KeyDownEvent: {jsEvent: "onkeydown", jsFunc: "keyDownEvent"},
KeyUpEvent: {jsEvent: "onkeyup", jsFunc: "keyUpEvent"},
ClickEvent: {jsEvent: "onclick", jsFunc: "clickEvent"},
DoubleClickEvent: {jsEvent: "ondblclick", jsFunc: "doubleClickEvent"},
MouseDown: {jsEvent: "onmousedown", jsFunc: "mouseDownEvent"},
MouseUp: {jsEvent: "onmouseup", jsFunc: "mouseUpEvent"},
MouseMove: {jsEvent: "onmousemove", jsFunc: "mouseMoveEvent"},
MouseOut: {jsEvent: "onmouseout", jsFunc: "mouseOutEvent"},
MouseOver: {jsEvent: "onmouseover", jsFunc: "mouseOverEvent"},
ContextMenuEvent: {jsEvent: "oncontextmenu", jsFunc: "contextMenuEvent"},
PointerDown: {jsEvent: "onpointerdown", jsFunc: "pointerDownEvent"},
PointerUp: {jsEvent: "onpointerup", jsFunc: "pointerUpEvent"},
PointerMove: {jsEvent: "onpointermove", jsFunc: "pointerMoveEvent"},
PointerCancel: {jsEvent: "onpointercancel", jsFunc: "pointerCancelEvent"},
PointerOut: {jsEvent: "onpointerout", jsFunc: "pointerOutEvent"},
PointerOver: {jsEvent: "onpointerover", jsFunc: "pointerOverEvent"},
TouchStart: {jsEvent: "ontouchstart", jsFunc: "touchStartEvent"},
TouchEnd: {jsEvent: "ontouchend", jsFunc: "touchEndEvent"},
TouchMove: {jsEvent: "ontouchmove", jsFunc: "touchMoveEvent"},
TouchCancel: {jsEvent: "ontouchcancel", jsFunc: "touchCancelEvent"},
TransitionRunEvent: {jsEvent: "ontransitionrun", jsFunc: "transitionRunEvent"},
TransitionStartEvent: {jsEvent: "ontransitionstart", jsFunc: "transitionStartEvent"},
TransitionEndEvent: {jsEvent: "ontransitionend", jsFunc: "transitionEndEvent"},
TransitionCancelEvent: {jsEvent: "ontransitioncancel", jsFunc: "transitionCancelEvent"},
AnimationStartEvent: {jsEvent: "onanimationstart", jsFunc: "animationStartEvent"},
AnimationEndEvent: {jsEvent: "onanimationend", jsFunc: "animationEndEvent"},
AnimationIterationEvent: {jsEvent: "onanimationiteration", jsFunc: "animationIterationEvent"},
AnimationCancelEvent: {jsEvent: "onanimationcancel", jsFunc: "animationCancelEvent"},
}
func valueToNoArgEventListeners[V any](value any) ([]func(V), bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case func(V):
return []func(V){value}, true
case func():
fn := func(V) {
value()
}
return []func(V){fn}, true
case []func(V):
if len(value) == 0 {
return nil, true
}
for _, fn := range value {
if fn == nil {
return nil, false
}
}
return value, true
case []func():
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(V) {
v()
}
}
return listeners, true
case []any:
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V), count)
for i, v := range value {
if v == nil {
return nil, false
}
switch v := v.(type) {
case func(V):
listeners[i] = v
case func():
listeners[i] = func(V) {
v()
}
default:
return nil, false
}
}
return listeners, true
}
return nil, false
}
func valueToOneArgEventListeners[V View, E any](value any) ([]func(V, E), bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case func(V, E):
return []func(V, E){value}, true
case func(E):
fn := func(_ V, event E) {
value(event)
}
return []func(V, E){fn}, true
case func(V):
fn := func(view V, _ E) {
value(view)
}
return []func(V, E){fn}, true
case func():
fn := func(V, E) {
value()
}
return []func(V, E){fn}, true
case []func(V, E):
if len(value) == 0 {
return nil, true
}
for _, fn := range value {
if fn == nil {
return nil, false
}
}
return value, true
case []func(E):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(_ V, event E) {
v(event)
}
}
return listeners, true
case []func(V):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view V, _ E) {
v(view)
}
}
return listeners, true
case []func():
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(V, E) {
v()
}
}
return listeners, true
case []any:
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
switch v := v.(type) {
case func(V, E):
listeners[i] = v
case func(E):
listeners[i] = func(_ V, event E) {
v(event)
}
case func(V):
listeners[i] = func(view V, _ E) {
v(view)
}
case func():
listeners[i] = func(V, E) {
v()
}
default:
return nil, false
}
}
return listeners, true
}
return nil, false
}
func valueToTwoArgEventListeners[V View, E any](value any) ([]func(V, E, E), bool) {
if value == nil {
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 getNoArgEventListeners[V View](view View, subviewID []string, tag PropertyName) []func(V) {
if view = getSubview(view, subviewID); view != nil {
if value := view.Get(tag); value != nil {
if result, ok := value.([]func(V)); ok {
return result
}
}
}
return []func(V){}
}
func getOneArgEventListeners[V View, E any](view View, subviewID []string, tag PropertyName) []func(V, E) {
if view = getSubview(view, subviewID); view != nil {
if value := view.Get(tag); value != nil {
if result, ok := value.([]func(V, E)); ok {
return result
}
}
}
return []func(V, E){}
}
func getTwoArgEventListeners[V View, E any](view View, subviewID []string, tag PropertyName) []func(V, E, E) {
if view = getSubview(view, subviewID); 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 setNoArgEventListener[V View](properties Properties, tag PropertyName, value any) []PropertyName {
if listeners, ok := valueToNoArgEventListeners[V](value); ok {
if len(listeners) > 0 {
properties.setRaw(tag, listeners)
} else if properties.getRaw(tag) != nil {
properties.setRaw(tag, nil)
} else {
return []PropertyName{}
}
return []PropertyName{tag}
}
notCompatibleType(tag, value)
return nil
}
func setOneArgEventListener[V View, T any](properties Properties, tag PropertyName, value any) []PropertyName {
if listeners, ok := valueToOneArgEventListeners[V, T](value); ok {
if len(listeners) > 0 {
properties.setRaw(tag, listeners)
} else if properties.getRaw(tag) != nil {
properties.setRaw(tag, nil)
} else {
return []PropertyName{}
}
return []PropertyName{tag}
}
notCompatibleType(tag, value)
return nil
}
func setTwoArgEventListener[V View, T any](properties Properties, tag PropertyName, value any) []PropertyName {
listeners, ok := valueToTwoArgEventListeners[V, T](value)
if !ok {
notCompatibleType(tag, value)
return nil
} else if len(listeners) > 0 {
properties.setRaw(tag, listeners)
} else if properties.getRaw(tag) != nil {
properties.setRaw(tag, nil)
} else {
return []PropertyName{}
}
return []PropertyName{tag}
}
func viewEventsHtml[T any](view View, events []PropertyName, buffer *strings.Builder) {
for _, tag := range events {
if value := view.getRaw(tag); value != nil {
if js, ok := eventJsFunc[tag]; ok {
if listeners, ok := value.([]func(View, T)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
}
}
}
}
}
func updateEventListenerHtml(view View, tag PropertyName) {
if js, ok := eventJsFunc[tag]; ok {
value := view.getRaw(tag)
session := view.Session()
htmlID := view.htmlID()
if value == nil {
session.removeProperty(view.htmlID(), js.jsEvent)
} else {
session.updateProperty(htmlID, js.jsEvent, js.jsFunc+"(this, event)")
}
}
}

View File

@ -7,31 +7,69 @@ import (
"time"
)
// Constants for [FilePicker] specific properties and events
const (
// FileSelectedEvent is the constant for "file-selected-event" property tag.
// The "file-selected-event" is fired when user selects file(s) in the FilePicker.
FileSelectedEvent = "file-selected-event"
//
// Used by FilePicker.
// Fired when user selects file(s).
//
// General listener format:
// func(picker rui.FilePicker, files []rui.FileInfo).
//
// where:
// picker - Interface of a file picker which generated this event,
// files - Array of description of selected files.
//
// Allowed listener formats:
// func(picker rui.FilePicker)
// func(files []rui.FileInfo)
// func()
FileSelectedEvent PropertyName = "file-selected-event"
// Accept is the constant for "accept" property tag.
// The "accept" property of the FilePicker sets the list of allowed file extensions or MIME types.
Accept = "accept"
//
// Used by FilePicker.
// Set the list of allowed file extensions or MIME types.
//
// Supported types: string, []string.
//
// Internal type is string, other types converted to it during assignment.
//
// Conversion rules:
// - string - may contain single value of multiple separated by comma(,).
// - []string - an array of acceptable file extensions or MIME types.
Accept PropertyName = "accept"
// Multiple is the constant for "multiple" property tag.
// The "multiple" bool property of the FilePicker sets whether multiple files can be selected
Multiple = "multiple"
//
// Used by FilePicker.
// Controls whether multiple files can be selected.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", "1" - Several files can be selected.
// - false, 0, "false", "no", "off", "0" - Only one file can be selected.
Multiple PropertyName = "multiple"
)
// FileInfo describes a file which selected in the FilePicker view
type FileInfo struct {
// Name - the file's name.
Name string
// LastModified specifying the date and time at which the file was last modified
LastModified time.Time
// Size - the size of the file in bytes.
Size int64
// MimeType - the file's MIME type.
MimeType string
}
// FilePicker - the control view for the files selecting
// FilePicker represents the FilePicker view
type FilePicker interface {
View
// Files returns the list of selected files.
@ -44,9 +82,8 @@ type FilePicker interface {
type filePickerData struct {
viewData
files []FileInfo
fileSelectedListeners []func(FilePicker, []FileInfo)
loader map[int]func(FileInfo, []byte)
files []FileInfo
loader map[int]func(FileInfo, []byte)
}
func (file *FileInfo) initBy(node DataValue) {
@ -77,7 +114,7 @@ func NewFilePicker(session Session, params Params) FilePicker {
}
func newFilePicker(session Session) View {
return NewFilePicker(session, nil)
return new(filePickerData) // NewFilePicker(session, nil)
}
func (picker *filePickerData) init(session Session) {
@ -86,11 +123,9 @@ func (picker *filePickerData) init(session Session) {
picker.hasHtmlDisabled = true
picker.files = []FileInfo{}
picker.loader = map[int]func(FileInfo, []byte){}
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){}
}
picker.set = picker.setFunc
picker.changed = picker.propertyChanged
func (picker *filePickerData) String() string {
return getViewString(picker, nil)
}
func (picker *filePickerData) Focusable() bool {
@ -115,62 +150,16 @@ func (picker *filePickerData) LoadFile(file FileInfo, result func(FileInfo, []by
}
}
func (picker *filePickerData) Remove(tag string) {
picker.remove(strings.ToLower(tag))
}
func (picker *filePickerData) remove(tag string) {
switch tag {
case FileSelectedEvent:
if len(picker.fileSelectedListeners) > 0 {
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){}
picker.propertyChangedEvent(tag)
}
case Accept:
delete(picker.properties, tag)
if picker.created {
picker.session.removeProperty(picker.htmlID(), "accept")
}
picker.propertyChangedEvent(tag)
default:
picker.viewData.remove(tag)
}
}
func (picker *filePickerData) Set(tag string, value any) bool {
return picker.set(strings.ToLower(tag), value)
}
func (picker *filePickerData) set(tag string, value any) bool {
if value == nil {
picker.remove(tag)
return true
}
func (picker *filePickerData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case FileSelectedEvent:
listeners, ok := valueToEventListeners[FilePicker, []FileInfo](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(FilePicker, []FileInfo){}
}
picker.fileSelectedListeners = listeners
picker.propertyChangedEvent(tag)
return true
return setOneArgEventListener[FilePicker, []FileInfo](picker, tag, value)
case Accept:
switch value := value.(type) {
case string:
value = strings.Trim(value, " \t\n")
if value == "" {
picker.remove(Accept)
} else {
picker.properties[Accept] = value
}
return setStringPropertyValue(picker, Accept, strings.Trim(value, " \t\n"))
case []string:
buffer := allocStringBuilder()
@ -184,29 +173,27 @@ func (picker *filePickerData) set(tag string, value any) bool {
buffer.WriteString(val)
}
}
if buffer.Len() == 0 {
picker.remove(Accept)
} else {
picker.properties[Accept] = buffer.String()
}
default:
notCompatibleType(tag, value)
return false
return setStringPropertyValue(picker, Accept, buffer.String())
}
notCompatibleType(tag, value)
return nil
}
if picker.created {
if css := picker.acceptCSS(); css != "" {
picker.session.updateProperty(picker.htmlID(), "accept", css)
} else {
picker.session.removeProperty(picker.htmlID(), "accept")
}
return picker.viewData.setFunc(tag, value)
}
func (picker *filePickerData) propertyChanged(tag PropertyName) {
switch tag {
case Accept:
session := picker.Session()
if css := acceptPropertyCSS(picker); css != "" {
session.updateProperty(picker.htmlID(), "accept", css)
} else {
session.removeProperty(picker.htmlID(), "accept")
}
picker.propertyChangedEvent(tag)
return true
default:
return picker.viewData.set(tag, value)
picker.viewData.propertyChanged(tag)
}
}
@ -214,10 +201,10 @@ func (picker *filePickerData) htmlTag() string {
return "input"
}
func (picker *filePickerData) acceptCSS() string {
accept, ok := stringProperty(picker, Accept, picker.Session())
func acceptPropertyCSS(view View) string {
accept, ok := stringProperty(view, Accept, view.Session())
if !ok {
if value := valueFromStyle(picker, Accept); value != nil {
if value := valueFromStyle(view, Accept); value != nil {
accept, ok = value.(string)
}
}
@ -244,7 +231,7 @@ func (picker *filePickerData) acceptCSS() string {
func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder) {
picker.viewData.htmlProperties(self, buffer)
if accept := picker.acceptCSS(); accept != "" {
if accept := acceptPropertyCSS(picker); accept != "" {
buffer.WriteString(` accept="`)
buffer.WriteString(accept)
buffer.WriteRune('"')
@ -261,7 +248,7 @@ func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder)
}
}
func (picker *filePickerData) handleCommand(self View, command string, data DataObject) bool {
func (picker *filePickerData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command {
case "fileSelected":
if node := data.PropertyByTag("files"); node != nil && node.Type() == ArrayNode {
@ -274,7 +261,7 @@ func (picker *filePickerData) handleCommand(self View, command string, data Data
}
picker.files = files
for _, listener := range picker.fileSelectedListeners {
for _, listener := range GetFileSelectedListeners(picker) {
listener(picker, files)
}
}
@ -358,10 +345,7 @@ func IsMultipleFilePicker(view View, subviewID ...string) bool {
// GetFilePickerAccept returns sets the list of allowed file extensions or MIME types.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetFilePickerAccept(view View, subviewID ...string) []string {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
accept, ok := stringProperty(view, Accept, view.Session())
if !ok {
if value := valueFromStyle(view, Accept); value != nil {
@ -383,5 +367,5 @@ func GetFilePickerAccept(view View, subviewID ...string) []string {
// 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.
func GetFileSelectedListeners(view View, subviewID ...string) []func(FilePicker, []FileInfo) {
return getEventListeners[FilePicker, []FileInfo](view, subviewID, FileSelectedEvent)
return getOneArgEventListeners[FilePicker, []FileInfo](view, subviewID, FileSelectedEvent)
}

357
filter.go Normal file
View File

@ -0,0 +1,357 @@
package rui
import (
"fmt"
"strings"
)
// Constants for [FilterProperty] specific properties and events
const (
// Blur is the constant for "blur" property tag.
//
// Used by FilterProperty.
// Applies a Gaussian blur. The value of radius defines the value of the standard deviation to the Gaussian function, or
// how many pixels on the screen blend into each other, so a larger value will create more blur. The lacuna value for
// interpolation is 0. The parameter is specified as a length in pixels.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
Blur PropertyName = "blur"
// Brightness is the constant for "brightness" property tag.
//
// Used by FilterProperty.
// Applies a linear multiplier to input image, making it appear more or less bright. A value of 0% will create an image
// that is completely black. A value of 100% leaves the input unchanged. Other values are linear multipliers on the
// effect. Values of an amount over 100% are allowed, providing brighter results.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
Brightness PropertyName = "brightness"
// Contrast is the constant for "contrast" property tag.
//
// Used by FilterProperty.
// Adjusts the contrast of the input. A value of 0% will create an image that is completely black. A value of 100% leaves
// the input unchanged. Values of amount over 100% are allowed, providing results with less contrast.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
Contrast PropertyName = "contrast"
// DropShadow is the constant for "drop-shadow" property tag.
//
// Used by FilterProperty.
// Applies a drop shadow effect to the input image. A drop shadow is effectively a blurred, offset version of the input
// image's alpha mask drawn in a particular color, composited below the image. Shadow parameters are set using the
// ShadowProperty interface.
//
// Supported types: []ShadowProperty, ShadowProperty, string.
//
// Internal type is []ShadowProperty, other types converted to it during assignment.
// See ShadowProperty description for more details.
//
// Conversion rules:
// - []ShadowProperty - stored as is, no conversion performed.
// - ShadowProperty - converted to []ShadowProperty.
// - string - string representation of ShadowProperty. Example: "_{blur = 1em, color = black, spread-radius = 0.5em}".
DropShadow PropertyName = "drop-shadow"
// Grayscale is the constant for "grayscale" property tag.
//
// Used by FilterProperty.
// Converts the input image to grayscale. The value of amount defines the proportion of the conversion. A value of 100%
// is completely grayscale. A value of 0% leaves the input unchanged. Values between 0% and 100% are linear multipliers on
// the effect.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
Grayscale PropertyName = "grayscale"
// HueRotate is the constant for "hue-rotate" property tag.
//
// Used by FilterProperty.
// Applies a hue rotation on the input image. The value of angle defines the number of degrees around the color circle
// the input samples will be adjusted. A value of 0deg leaves the input unchanged. If the angle parameter is missing, a
// value of 0deg is used. Though there is no maximum value, the effect of values above 360deg wraps around.
//
// Supported types: AngleUnit, string, float, int.
//
// Internal type is AngleUnit, other types will be converted to it during assignment.
// See AngleUnit description for more details.
//
// Conversion rules:
// - AngleUnit - stored as is, no conversion performed.
// - string - must contain string representation of AngleUnit. If numeric value will be provided without any suffix then AngleUnit with value and Radian value type will be created.
// - float - a new AngleUnit value will be created with Radian as a type.
// - int - a new AngleUnit value will be created with Radian as a type.
HueRotate PropertyName = "hue-rotate"
// Invert is the constant for "invert" property tag.
//
// Used by FilterProperty.
// Inverts the samples in the input image. The value of amount defines the proportion of the conversion. A value of 100%
// is completely inverted. A value of 0% leaves the input unchanged. Values between 0% and 100% are linear multipliers on
// the effect.
//
// Supported types: float64, int, string.
//
// Internal type is float, other types converted to it during assignment.
Invert PropertyName = "invert"
// Saturate is the constant for "saturate" property tag.
//
// Used by FilterProperty.
// Saturates the input image. The value of amount defines the proportion of the conversion. A value of 0% is completely
// un-saturated. A value of 100% leaves the input unchanged. Other values are linear multipliers on the effect. Values of
// amount over 100% are allowed, providing super-saturated results.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
Saturate PropertyName = "saturate"
// Sepia is the constant for "sepia" property tag.
//
// Used by FilterProperty.
// Converts the input image to sepia. The value of amount defines the proportion of the conversion. A value of 100% is
// completely sepia. A value of 0% leaves the input unchanged. Values between 0% and 100% are linear multipliers on the
// effect.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
Sepia PropertyName = "sepia"
)
// FilterProperty defines an applied to a View a graphical effects like blur or color shift.
// Allowable properties are Blur, Brightness, Contrast, DropShadow, Grayscale, HueRotate, Invert, Opacity, Saturate, and Sepia
type FilterProperty interface {
Properties
fmt.Stringer
stringWriter
cssStyle(session Session) string
}
type filterData struct {
dataProperty
}
// NewFilterProperty creates the new FilterProperty
func NewFilterProperty(params Params) FilterProperty {
if len(params) > 0 {
filter := new(filterData)
filter.init()
for tag, value := range params {
if !filter.Set(tag, value) {
return nil
}
}
return filter
}
return nil
}
func newFilterProperty(obj DataObject) FilterProperty {
filter := new(filterData)
filter.init()
for i := 0; i < obj.PropertyCount(); i++ {
if node := obj.Property(i); node != nil {
tag := node.Tag()
switch node.Type() {
case TextNode:
filter.Set(PropertyName(tag), node.Text())
case ObjectNode:
if tag == string(HueRotate) {
// TODO
} else {
ErrorLog(`Invalid value of "` + tag + `"`)
}
default:
ErrorLog(`Invalid value of "` + tag + `"`)
}
}
}
if len(filter.properties) > 0 {
return filter
}
ErrorLog("Empty view filter")
return nil
}
func (filter *filterData) init() {
filter.dataProperty.init()
filter.set = filterDataSet
filter.supportedProperties = []PropertyName{Blur, Brightness, Contrast, Saturate, Grayscale, Invert, Opacity, Sepia, HueRotate, DropShadow}
}
func filterDataSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case Blur, Brightness, Contrast, Saturate:
return setFloatProperty(properties, tag, value, 0, 10000)
case Grayscale, Invert, Opacity, Sepia:
return setFloatProperty(properties, tag, value, 0, 100)
case HueRotate:
return setAngleProperty(properties, tag, value)
case DropShadow:
if setShadowProperty(properties, tag, value) {
return []PropertyName{tag}
}
}
ErrorLogF(`"%s" property is not supported by the view filter`, tag)
return nil
}
func (filter *filterData) String() string {
return runStringWriter(filter)
}
func (filter *filterData) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("filter { ")
comma := false
tags := filter.AllTags()
for _, tag := range tags {
if value, ok := filter.properties[tag]; ok {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
writePropertyValue(buffer, tag, value, indent)
comma = true
}
}
buffer.WriteString(" }")
}
func (filter *filterData) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if value, ok := floatTextProperty(filter, Blur, session, 0); ok {
buffer.WriteString(string(Blur))
buffer.WriteRune('(')
buffer.WriteString(value)
buffer.WriteString("px)")
}
for _, tag := range []PropertyName{Brightness, Contrast, Saturate, Grayscale, Invert, Opacity, Sepia} {
if value, ok := floatTextProperty(filter, tag, session, 0); ok {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString(string(tag))
buffer.WriteRune('(')
buffer.WriteString(value)
buffer.WriteString("%)")
}
}
if value, ok := angleProperty(filter, HueRotate, session); ok {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString(string(HueRotate))
buffer.WriteRune('(')
buffer.WriteString(value.cssString())
buffer.WriteRune(')')
}
var lead string
if buffer.Len() > 0 {
lead = " drop-shadow("
} else {
lead = "drop-shadow("
}
for _, shadow := range getShadows(filter, DropShadow) {
if shadow.cssTextStyle(buffer, session, lead) {
buffer.WriteRune(')')
lead = " drop-shadow("
}
}
return buffer.String()
}
func setFilterProperty(properties Properties, tag PropertyName, value any) []PropertyName {
switch value := value.(type) {
case FilterProperty:
properties.setRaw(tag, value)
return []PropertyName{tag}
case string:
if obj := NewDataObject(value); obj == nil {
if filter := newFilterProperty(obj); filter != nil {
properties.setRaw(tag, filter)
return []PropertyName{tag}
}
}
case DataObject:
if filter := newFilterProperty(value); filter != nil {
properties.setRaw(tag, filter)
return []PropertyName{tag}
}
case DataValue:
if value.IsObject() {
if filter := newFilterProperty(value.Object()); filter != nil {
properties.setRaw(tag, filter)
return []PropertyName{tag}
}
}
}
notCompatibleType(tag, value)
return nil
}
// GetFilter returns a View graphical effects like blur or color shift.
// If the second argument (subviewID) is not specified or it is "" then a top position of the first argument (view) is returned
func GetFilter(view View, subviewID ...string) FilterProperty {
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(Filter); value != nil {
if filter, ok := value.(FilterProperty); ok {
return filter
}
}
if value := valueFromStyle(view, Filter); value != nil {
if filter, ok := value.(FilterProperty); ok {
return filter
}
}
}
return nil
}
// GetBackdropFilter returns the area behind a View graphical effects like blur or color shift.
// If the second argument (subviewID) is not specified or it is "" then a top position of the first argument (view) is returned
func GetBackdropFilter(view View, subviewID ...string) FilterProperty {
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(BackdropFilter); value != nil {
if filter, ok := value.(FilterProperty); ok {
return filter
}
}
if value := valueFromStyle(view, BackdropFilter); value != nil {
if filter, ok := value.(FilterProperty); ok {
return filter
}
}
}
return nil
}

View File

@ -2,151 +2,48 @@ package rui
import "strings"
// Constants which represent [View] specific focus events properties
const (
// FocusEvent is the constant for "focus-event" property tag.
// The "focus-event" event occurs when the View takes input focus.
// The main listener format:
// func(View).
// The additional listener format:
// func().
FocusEvent = "focus-event"
//
// Used by View.
// Occur when the view takes input focus.
//
// General listener format:
// func(rui.View).
//
// where:
// view - Interface of a view which generated this event.
//
// Allowed listener formats:
// func().
FocusEvent PropertyName = "focus-event"
// LostFocusEvent is the constant for "lost-focus-event" property tag.
// The "lost-focus-event" event occurs when the View lost input focus.
// The main listener format:
// func(View).
// The additional listener format:
// func().
LostFocusEvent = "lost-focus-event"
//
// Used by View.
// Occur when the View lost input focus.
//
// General listener format:
// func(view rui.View).
//
// where:
// view - Interface of a view which generated this event.
//
// Allowed listener formats:
// func()
LostFocusEvent PropertyName = "lost-focus-event"
)
func valueToNoParamListeners[V any](value any) ([]func(V), bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case func(V):
return []func(V){value}, true
case func():
fn := func(V) {
value()
}
return []func(V){fn}, true
case []func(V):
if len(value) == 0 {
return nil, true
}
for _, fn := range value {
if fn == nil {
return nil, false
}
}
return value, true
case []func():
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(V) {
v()
}
}
return listeners, true
case []any:
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V), count)
for i, v := range value {
if v == nil {
return nil, false
}
switch v := v.(type) {
case func(V):
listeners[i] = v
case func():
listeners[i] = func(V) {
v()
}
default:
return nil, false
}
}
return listeners, true
}
return nil, false
}
var focusEvents = map[string]struct{ jsEvent, jsFunc string }{
FocusEvent: {jsEvent: "onfocus", jsFunc: "focusEvent"},
LostFocusEvent: {jsEvent: "onblur", jsFunc: "blurEvent"},
}
func (view *viewData) setFocusListener(tag string, value any) bool {
listeners, ok := valueToNoParamListeners[View](value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeFocusListener(tag)
} else if js, ok := focusEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
view.session.updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)")
}
} else {
return false
}
return true
}
func (view *viewData) removeFocusListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := focusEvents[tag]; ok {
view.session.removeProperty(view.htmlID(), js.jsEvent)
}
}
}
func getFocusListeners(view View, subviewID []string, tag string) []func(View) {
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(View)); ok {
return result
}
}
}
return []func(View){}
}
func focusEventsHtml(view View, buffer *strings.Builder) {
if view.Focusable() {
for _, js := range focusEvents {
buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
for _, tag := range []PropertyName{FocusEvent, LostFocusEvent} {
if js, ok := eventJsFunc[tag]; ok {
buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
}
}
}
}
@ -154,11 +51,11 @@ func focusEventsHtml(view View, buffer *strings.Builder) {
// GetFocusListeners returns a FocusListener 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.
func GetFocusListeners(view View, subviewID ...string) []func(View) {
return getFocusListeners(view, subviewID, FocusEvent)
return getNoArgEventListeners[View](view, subviewID, FocusEvent)
}
// GetLostFocusListeners returns a LostFocusListener 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.
func GetLostFocusListeners(view View, subviewID ...string) []func(View) {
return getFocusListeners(view, subviewID, LostFocusEvent)
return getNoArgEventListeners[View](view, subviewID, LostFocusEvent)
}

10
go.mod
View File

@ -2,6 +2,12 @@ module github.com/anoshenko/rui
go 1.18
require github.com/gorilla/websocket v1.5.1
require (
github.com/gorilla/websocket v1.5.1
golang.org/x/crypto v0.14.0
)
require golang.org/x/net v0.17.0 // indirect
require (
golang.org/x/net v0.17.0 // indirect
golang.org/x/text v0.13.0 // indirect
)

4
go.sum
View File

@ -1,4 +1,8 @@
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=

View File

@ -5,44 +5,79 @@ import (
"strings"
)
// Constants related to [GridLayout] specific properties and events
const (
// CellVerticalAlign is the constant for the "cell-vertical-align" property tag.
// The "cell-vertical-align" int property sets the default vertical alignment
// of GridLayout children within the cell they are occupying. Valid values:
// * TopAlign (0) / "top"
// * BottomAlign (1) / "bottom"
// * CenterAlign (2) / "center", and
// * StretchAlign (2) / "stretch"
CellVerticalAlign = "cell-vertical-align"
// CellVerticalAlign is the constant for "cell-vertical-align" property tag.
//
// Used by GridLayout, SvgImageView.
//
// Usage in GridLayout:
// Sets the default vertical alignment of GridLayout children within the cell they are occupying.
//
// Supported types: int, string.
//
// Values:
// - 0 (TopAlign) or "top" - Top alignment.
// - 1 (BottomAlign) or "bottom" - Bottom alignment.
// - 2 (CenterAlign) or "center" - Center alignment.
// - 3 (StretchAlign) or "stretch" - Full height stretch.
//
// Usage in SvgImageView:
// Same as "vertical-align".
CellVerticalAlign PropertyName = "cell-vertical-align"
// CellHorizontalAlign is the constant for the "cell-horizontal-align" property tag.
// The "cell-horizontal-align" int property sets the default horizontal alignment
// of GridLayout children within the occupied cell. Valid values:
// * LeftAlign (0) / "left"
// * RightAlign (1) / "right"
// * CenterAlign (2) / "center"
// * StretchAlign (3) / "stretch"
CellHorizontalAlign = "cell-horizontal-align"
// CellHorizontalAlign is the constant for "cell-horizontal-align" property tag.
//
// Used by GridLayout, SvgImageView.
//
// Usage in GridLayout:
// Sets the default horizontal alignment of GridLayout children within the occupied cell.
//
// Supported types: int, string.
//
// Values:
// - 0 (LeftAlign) or "left" - Left alignment.
// - 1 (RightAlign) or "right" - Right alignment.
// - 2 (CenterAlign) or "center" - Center alignment.
// - 3 (StretchAlign) or "stretch" - Full width stretch.
//
// Usage in SvgImageView:
// Same as "horizontal-align".
CellHorizontalAlign PropertyName = "cell-horizontal-align"
// CellVerticalSelfAlign is the constant for the "cell-vertical-self-align" property tag.
// The "cell-vertical-align" int property sets the vertical alignment of GridLayout children
// within the cell they are occupying. The property is set for the child view of GridLayout. Valid values:
// * TopAlign (0) / "top"
// * BottomAlign (1) / "bottom"
// * CenterAlign (2) / "center", and
// * StretchAlign (2) / "stretch"
CellVerticalSelfAlign = "cell-vertical-self-align"
// CellVerticalSelfAlign is the constant for "cell-vertical-self-align" property tag.
//
// Used by GridLayout.
// Sets the vertical alignment of GridLayout children within the cell they are occupying. The property is set for the
// child view of GridLayout.
//
// Supported types: int, string.
//
// Values:
// - 0 (TopAlign) or "top" - Top alignment.
// - 1 (BottomAlign) or "bottom" - Bottom alignment.
// - 2 (CenterAlign) or "center" - Center alignment.
// - 3 (StretchAlign) or "stretch" - Full height stretch.
CellVerticalSelfAlign PropertyName = "cell-vertical-self-align"
// CellHorizontalSelfAlign is the constant for the "cell-horizontal-self-align" property tag.
// The "cell-horizontal-self align" int property sets the horizontal alignment of GridLayout children
// within the occupied cell. The property is set for the child view of GridLayout. Valid values:
// * LeftAlign (0) / "left"
// * RightAlign (1) / "right"
// * CenterAlign (2) / "center"
// * StretchAlign (3) / "stretch"
CellHorizontalSelfAlign = "cell-horizontal-self-align"
// CellHorizontalSelfAlign is the constant for "cell-horizontal-self-align" property tag.
//
// Used by GridLayout.
// Sets the horizontal alignment of GridLayout children within the occupied cell. The property is set for the child view
// of GridLayout.
//
// Supported types: int, string.
//
// Values:
// - 0 (LeftAlign) or "left" - Left alignment.
// - 1 (RightAlign) or "right" - Right alignment.
// - 2 (CenterAlign) or "center" - Center alignment.
// - 3 (StretchAlign) or "stretch" - Full width stretch.
CellHorizontalSelfAlign PropertyName = "cell-horizontal-self-align"
)
// GridAdapter is an interface to define [GridLayout] content. [GridLayout] will query interface functions to populate
// its content
type GridAdapter interface {
// GridColumnCount returns the number of columns in the grid
GridColumnCount() int
@ -54,21 +89,21 @@ type GridAdapter interface {
GridCellContent(row, column int, session Session) View
}
// GridCellColumnSpanAdapter implements the optional method of GridAdapter interface
// GridCellColumnSpanAdapter implements the optional method of the [GridAdapter] interface
type GridCellColumnSpanAdapter interface {
// GridCellColumnSpan returns the number of columns that a cell spans.
// Values less than 1 are ignored.
GridCellColumnSpan(row, column int) int
}
// GridCellColumnSpanAdapter implements the optional method of GridAdapter interface
// GridCellColumnSpanAdapter implements the optional method of the [GridAdapter] interface
type GridCellRowSpanAdapter interface {
// GridCellRowSpan returns the number of rows that a cell spans
// Values less than 1 are ignored.
GridCellRowSpan(row, column int) int
}
// GridLayout - grid-container of View
// GridLayout represents a GridLayout view
type GridLayout interface {
ViewsContainer
@ -91,7 +126,8 @@ func NewGridLayout(session Session, params Params) GridLayout {
}
func newGridLayout(session Session) View {
return NewGridLayout(session, nil)
//return NewGridLayout(session, nil)
return new(gridLayoutData)
}
// Init initialize fields of GridLayout by default values
@ -100,13 +136,14 @@ func (gridLayout *gridLayoutData) init(session Session) {
gridLayout.tag = "GridLayout"
gridLayout.systemClass = "ruiGridLayout"
gridLayout.adapter = nil
gridLayout.normalize = normalizeGridLayoutTag
gridLayout.get = gridLayout.getFunc
gridLayout.set = gridLayout.setFunc
gridLayout.remove = gridLayout.removeFunc
gridLayout.changed = gridLayout.propertyChanged
}
func (gridLayout *gridLayoutData) String() string {
return getViewString(gridLayout, nil)
}
func (style *viewStyle) setGridCellSize(tag string, value any) bool {
func setGridCellSize(properties Properties, tag PropertyName, value any) []PropertyName {
setValues := func(values []string) bool {
count := len(values)
if count > 1 {
@ -124,11 +161,11 @@ func (style *viewStyle) setGridCellSize(tag string, value any) bool {
return false
}
}
style.properties[tag] = sizes
properties.setRaw(tag, sizes)
} else if isConstantName(values[0]) {
style.properties[tag] = values[0]
properties.setRaw(tag, values[0])
} else if size, err := stringToSizeUnit(values[0]); err == nil {
style.properties[tag] = size
properties.setRaw(tag, size)
} else {
invalidPropertyValue(tag, value)
return false
@ -140,41 +177,41 @@ func (style *viewStyle) setGridCellSize(tag string, value any) bool {
case CellWidth, CellHeight:
switch value := value.(type) {
case SizeUnit, []SizeUnit:
style.properties[tag] = value
properties.setRaw(tag, value)
case string:
if !setValues(strings.Split(value, ",")) {
return false
return nil
}
case []string:
if !setValues(value) {
return false
return nil
}
case []DataValue:
count := len(value)
if count == 0 {
invalidPropertyValue(tag, value)
return false
return nil
}
values := make([]string, count)
for i, val := range value {
if val.IsObject() {
invalidPropertyValue(tag, value)
return false
return nil
}
values[i] = val.Value()
}
if !setValues(values) {
return false
return nil
}
case []any:
count := len(value)
if count == 0 {
invalidPropertyValue(tag, value)
return false
return nil
}
sizes := make([]any, count)
for i, val := range value {
@ -189,29 +226,29 @@ func (style *viewStyle) setGridCellSize(tag string, value any) bool {
sizes[i] = size
} else {
invalidPropertyValue(tag, value)
return false
return nil
}
default:
invalidPropertyValue(tag, value)
return false
return nil
}
}
style.properties[tag] = sizes
properties.setRaw(tag, sizes)
default:
notCompatibleType(tag, value)
return false
return nil
}
return true
return []PropertyName{tag}
}
return false
return nil
}
func (style *viewStyle) gridCellSizesCSS(tag string, session Session) string {
switch cellSize := gridCellSizes(style, tag, session); len(cellSize) {
func gridCellSizesCSS(properties Properties, tag PropertyName, session Session) string {
switch cellSize := gridCellSizes(properties, tag, session); len(cellSize) {
case 0:
case 1:
@ -248,8 +285,8 @@ func (style *viewStyle) gridCellSizesCSS(tag string, session Session) string {
return ""
}
func (gridLayout *gridLayoutData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeGridLayoutTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case VerticalAlign:
return CellVerticalAlign
@ -266,162 +303,169 @@ func (gridLayout *gridLayoutData) normalizeTag(tag string) string {
return tag
}
func (gridLayout *gridLayoutData) Get(tag string) any {
return gridLayout.get(gridLayout.normalizeTag(tag))
}
func (gridLayout *gridLayoutData) get(tag string) any {
if tag == Gap {
func (gridLayout *gridLayoutData) getFunc(tag PropertyName) any {
switch tag {
case Gap:
rowGap := GetGridRowGap(gridLayout)
columnGap := GetGridColumnGap(gridLayout)
if rowGap.Equal(columnGap) {
return rowGap
}
return AutoSize()
}
return gridLayout.viewsContainerData.get(tag)
}
func (gridLayout *gridLayoutData) Remove(tag string) {
gridLayout.remove(gridLayout.normalizeTag(tag))
}
func (gridLayout *gridLayoutData) remove(tag string) {
switch tag {
case Gap:
gridLayout.remove(GridRowGap)
gridLayout.remove(GridColumnGap)
gridLayout.propertyChangedEvent(Gap)
return
case Content:
gridLayout.adapter = nil
}
gridLayout.viewsContainerData.remove(tag)
if gridLayout.created {
switch tag {
case CellWidth:
gridLayout.session.updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`,
gridLayout.gridCellSizesCSS(CellWidth, gridLayout.session))
case CellHeight:
gridLayout.session.updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`,
gridLayout.gridCellSizesCSS(CellHeight, gridLayout.session))
if gridLayout.adapter != nil {
return gridLayout.adapter
}
}
return gridLayout.viewsContainerData.getFunc(tag)
}
func (gridLayout *gridLayoutData) Set(tag string, value any) bool {
return gridLayout.set(gridLayout.normalizeTag(tag), value)
}
func (gridLayout *gridLayoutData) set(tag string, value any) bool {
if value == nil {
gridLayout.remove(tag)
return true
}
func (gridLayout *gridLayoutData) removeFunc(tag PropertyName) []PropertyName {
switch tag {
case Gap:
return gridLayout.set(GridRowGap, value) && gridLayout.set(GridColumnGap, value)
result := []PropertyName{}
for _, tag := range []PropertyName{GridRowGap, GridColumnGap} {
if gridLayout.getRaw(tag) != nil {
gridLayout.setRaw(tag, nil)
result = append(result, tag)
}
}
return result
case Content:
if len(gridLayout.views) > 0 || gridLayout.adapter != nil {
gridLayout.views = []View{}
gridLayout.adapter = nil
return []PropertyName{Content}
}
return []PropertyName{}
}
return gridLayout.viewsContainerData.removeFunc(tag)
}
func (gridLayout *gridLayoutData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case Gap:
result := gridLayout.setFunc(GridRowGap, value)
if result != nil {
if gap := gridLayout.getRaw(GridRowGap); gap != nil {
gridLayout.setRaw(GridColumnGap, gap)
result = append(result, GridColumnGap)
}
}
return result
case Content:
if adapter, ok := value.(GridAdapter); ok {
gridLayout.adapter = adapter
gridLayout.UpdateGridContent()
return true
gridLayout.createGridContent()
} else if gridLayout.setContent(value) {
gridLayout.adapter = nil
} else {
return nil
}
gridLayout.adapter = nil
return []PropertyName{Content}
}
if gridLayout.viewsContainerData.set(tag, value) {
if gridLayout.created {
switch tag {
case CellWidth:
gridLayout.session.updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`,
gridLayout.gridCellSizesCSS(CellWidth, gridLayout.session))
return gridLayout.viewsContainerData.setFunc(tag, value)
}
case CellHeight:
gridLayout.session.updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`,
gridLayout.gridCellSizesCSS(CellHeight, gridLayout.session))
func (gridLayout *gridLayoutData) propertyChanged(tag PropertyName) {
switch tag {
case CellWidth:
session := gridLayout.Session()
session.updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`,
gridCellSizesCSS(gridLayout, CellWidth, session))
case CellHeight:
session := gridLayout.Session()
session.updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`,
gridCellSizesCSS(gridLayout, CellHeight, session))
default:
gridLayout.viewsContainerData.propertyChanged(tag)
}
}
func (gridLayout *gridLayoutData) createGridContent() bool {
if gridLayout.adapter == nil {
return false
}
adapter := gridLayout.adapter
gridLayout.views = []View{}
session := gridLayout.session
htmlID := gridLayout.htmlID()
isDisabled := IsDisabled(gridLayout)
var columnSpan GridCellColumnSpanAdapter = nil
if span, ok := adapter.(GridCellColumnSpanAdapter); ok {
columnSpan = span
}
var rowSpan GridCellRowSpanAdapter = nil
if span, ok := adapter.(GridCellRowSpanAdapter); ok {
rowSpan = span
}
width := adapter.GridColumnCount()
height := adapter.GridRowCount()
for column := 0; column < width; column++ {
for row := 0; row < height; row++ {
if view := adapter.GridCellContent(row, column, session); view != nil {
view.setParentID(htmlID)
columnCount := 1
if columnSpan != nil {
columnCount = columnSpan.GridCellColumnSpan(row, column)
}
if columnCount > 1 {
view.Set(Column, Range{First: column, Last: column + columnCount - 1})
} else {
view.Set(Column, column)
}
rowCount := 1
if rowSpan != nil {
rowCount = rowSpan.GridCellRowSpan(row, column)
}
if rowCount > 1 {
view.Set(Row, Range{First: row, Last: row + rowCount - 1})
} else {
view.Set(Row, row)
}
if isDisabled {
view.Set(Disabled, true)
}
gridLayout.views = append(gridLayout.views, view)
}
}
return true
}
return false
return true
}
func (gridLayout *gridLayoutData) UpdateGridContent() {
if adapter := gridLayout.adapter; adapter != nil {
gridLayout.views = []View{}
session := gridLayout.session
htmlID := gridLayout.htmlID()
isDisabled := IsDisabled(gridLayout)
var columnSpan GridCellColumnSpanAdapter = nil
if span, ok := adapter.(GridCellColumnSpanAdapter); ok {
columnSpan = span
}
var rowSpan GridCellRowSpanAdapter = nil
if span, ok := adapter.(GridCellRowSpanAdapter); ok {
rowSpan = span
}
width := adapter.GridColumnCount()
height := adapter.GridRowCount()
for column := 0; column < width; column++ {
for row := 0; row < height; row++ {
if view := adapter.GridCellContent(row, column, session); view != nil {
view.setParentID(htmlID)
columnCount := 1
if columnSpan != nil {
columnCount = columnSpan.GridCellColumnSpan(row, column)
}
if columnCount > 1 {
view.Set(Column, Range{First: column, Last: column + columnCount - 1})
} else {
view.Set(Column, column)
}
rowCount := 1
if rowSpan != nil {
rowCount = rowSpan.GridCellRowSpan(row, column)
}
if rowCount > 1 {
view.Set(Row, Range{First: row, Last: row + rowCount - 1})
} else {
view.Set(Row, row)
}
if isDisabled {
view.Set(Disabled, true)
}
gridLayout.views = append(gridLayout.views, view)
}
}
}
if gridLayout.createGridContent() {
if gridLayout.created {
updateInnerHTML(htmlID, session)
updateInnerHTML(gridLayout.htmlID(), gridLayout.session)
}
gridLayout.propertyChangedEvent(Content)
if listener, ok := gridLayout.changeListener[Content]; ok {
listener(gridLayout, Content)
}
}
}
func gridCellSizes(properties Properties, tag string, session Session) []SizeUnit {
func gridCellSizes(properties Properties, tag PropertyName, session Session) []SizeUnit {
if value := properties.Get(tag); value != nil {
switch value := value.(type) {
case []SizeUnit:
@ -461,12 +505,6 @@ func gridCellSizes(properties Properties, tag string, session Session) []SizeUni
return []SizeUnit{}
}
/*
func (gridLayout *gridLayoutData) cssStyle(self View, builder cssBuilder) {
gridLayout.viewsContainerData.cssStyle(self, builder)
}
*/
// GetCellVerticalAlign returns the vertical align of a GridLayout cell content: TopAlign (0), BottomAlign (1), CenterAlign (2), StretchAlign (3)
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetCellVerticalAlign(view View, subviewID ...string) int {
@ -489,10 +527,7 @@ func GetGridAutoFlow(view View, subviewID ...string) int {
// If the result is a single value array, then the width of all cell is equal.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetCellWidth(view View, subviewID ...string) []SizeUnit {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
return gridCellSizes(view, CellWidth, view.Session())
}
return []SizeUnit{}
@ -502,10 +537,7 @@ func GetCellWidth(view View, subviewID ...string) []SizeUnit {
// If the result is a single value array, then the height of all cell is equal.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetCellHeight(view View, subviewID ...string) []SizeUnit {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
return gridCellSizes(view, CellHeight, view.Session())
}
return []SizeUnit{}

View File

@ -1,3 +1,5 @@
//go:build !wasm
package rui
import (
@ -20,25 +22,24 @@ func (h *httpHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
}
/*
NewHandler is used to embed the rui application in third-party web frameworks (net/http, gin, echo...).
Example for 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
}
})
*/
// NewHandler is used to embed the rui application in third-party web frameworks (net/http, gin, echo...).
//
// Example for 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

View File

@ -4,6 +4,7 @@ import (
"strconv"
)
// Constants which represent return values of the LoadingStatus function of an [Image] view
const (
// ImageLoading is the image loading status: in the process of loading
ImageLoading = 0

View File

@ -5,35 +5,63 @@ import (
"strings"
)
// Constants which represent [ImageView] specific properties and events
const (
// LoadedEvent is the constant for the "loaded-event" property tag.
// The "loaded-event" event occurs event occurs when the image has been loaded.
LoadedEvent = "loaded-event"
// ErrorEvent is the constant for the "error-event" property tag.
// The "error-event" event occurs event occurs when the image loading failed.
ErrorEvent = "error-event"
// LoadedEvent is the constant for "loaded-event" property tag.
//
// Used by ImageView.
// Occur when the image has been loaded.
//
// General listener format:
// func(image rui.ImageView)
//
// where:
// image - Interface of an image view which generated this event.
//
// Allowed listener formats:
// func()
LoadedEvent PropertyName = "loaded-event"
// ErrorEvent is the constant for "error-event" property tag.
//
// Used by ImageView.
// Occur when the image loading has been failed.
//
// General listener format:
// func(image rui.ImageView)
//
// where:
// image - Interface of an image view which generated this event.
//
// Allowed listener formats:
// func()
ErrorEvent PropertyName = "error-event"
// NoneFit - value of the "object-fit" property of an ImageView. The replaced content is not resized
NoneFit = 0
// ContainFit - value of the "object-fit" property of an ImageView. The replaced content
// is scaled to maintain its aspect ratio while fitting within the elements content box.
// The entire object is made to fill the box, while preserving its aspect ratio, so the object
// will be "letterboxed" if its aspect ratio does not match the aspect ratio of the box.
ContainFit = 1
// CoverFit - value of the "object-fit" property of an ImageView. The replaced content
// is sized to maintain its aspect ratio while filling the elements entire content box.
// If the object's aspect ratio does not match the aspect ratio of its box, then the object will be clipped to fit.
CoverFit = 2
// FillFit - value of the "object-fit" property of an ImageView. The replaced content is sized
// to fill the elements content box. The entire object will completely fill the box.
// If the object's aspect ratio does not match the aspect ratio of its box, then the object will be stretched to fit.
FillFit = 3
// ScaleDownFit - value of the "object-fit" property of an ImageView. The content is sized as
// if NoneFit or ContainFit were specified, whichever would result in a smaller concrete object size.
ScaleDownFit = 4
)
// ImageView - image View
// ImageView represents an ImageView view
type ImageView interface {
View
// NaturalSize returns the intrinsic, density-corrected size (width, height) of the image in pixels.
@ -60,7 +88,7 @@ func NewImageView(session Session, params Params) ImageView {
}
func newImageView(session Session) View {
return NewImageView(session, nil)
return new(imageViewData)
}
// Init initialize fields of imageView by default values
@ -68,14 +96,13 @@ func (imageView *imageViewData) init(session Session) {
imageView.viewData.init(session)
imageView.tag = "ImageView"
imageView.systemClass = "ruiImageView"
imageView.normalize = normalizeImageViewTag
imageView.set = imageView.setFunc
imageView.changed = imageView.propertyChanged
}
func (imageView *imageViewData) String() string {
return getViewString(imageView, nil)
}
func (imageView *imageViewData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeImageViewTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case "source":
tag = Source
@ -95,127 +122,58 @@ func (imageView *imageViewData) normalizeTag(tag string) string {
return tag
}
func (imageView *imageViewData) Remove(tag string) {
imageView.remove(imageView.normalizeTag(tag))
}
func (imageView *imageViewData) setFunc(tag PropertyName, value any) []PropertyName {
func (imageView *imageViewData) remove(tag string) {
imageView.viewData.remove(tag)
if imageView.created {
switch tag {
case Source:
imageView.session.updateProperty(imageView.htmlID(), "src", "")
imageView.session.removeProperty(imageView.htmlID(), "srcset")
case AltText:
updateInnerHTML(imageView.htmlID(), imageView.session)
case ImageVerticalAlign, ImageHorizontalAlign:
updateCSSStyle(imageView.htmlID(), imageView.session)
switch tag {
case Source, SrcSet, AltText:
if text, ok := value.(string); ok {
return setStringPropertyValue(imageView, tag, text)
}
notCompatibleType(tag, value)
return nil
case LoadedEvent, ErrorEvent:
return setNoArgEventListener[ImageView](imageView, tag, value)
}
return imageView.viewData.setFunc(tag, value)
}
func (imageView *imageViewData) Set(tag string, value any) bool {
return imageView.set(imageView.normalizeTag(tag), value)
}
func (imageView *imageViewData) set(tag string, value any) bool {
if value == nil {
imageView.remove(tag)
return true
}
func (imageView *imageViewData) propertyChanged(tag PropertyName) {
session := imageView.Session()
htmlID := imageView.htmlID()
switch tag {
case Source:
if text, ok := value.(string); ok {
imageView.properties[tag] = text
if imageView.created {
src, srcset := imageView.src(text)
imageView.session.updateProperty(imageView.htmlID(), "src", src)
if srcset != "" {
imageView.session.updateProperty(imageView.htmlID(), "srcset", srcset)
} else {
imageView.session.removeProperty(imageView.htmlID(), "srcset")
}
}
imageView.propertyChangedEvent(Source)
return true
src, srcset := imageViewSrc(imageView, GetImageViewSource(imageView))
session.updateProperty(htmlID, "src", src)
if srcset != "" {
session.updateProperty(htmlID, "srcset", srcset)
} else {
session.removeProperty(htmlID, "srcset")
}
notCompatibleType(Source, value)
case SrcSet:
if text, ok := value.(string); ok {
if text == "" {
delete(imageView.properties, tag)
} else {
imageView.properties[tag] = text
}
if imageView.created {
_, srcset := imageView.src(text)
if srcset != "" {
imageView.session.updateProperty(imageView.htmlID(), "srcset", srcset)
} else {
imageView.session.removeProperty(imageView.htmlID(), "srcset")
}
}
imageView.propertyChangedEvent(Source)
return true
_, srcset := imageViewSrc(imageView, GetImageViewSource(imageView))
if srcset != "" {
session.updateProperty(htmlID, "srcset", srcset)
} else {
session.removeProperty(htmlID, "srcset")
}
notCompatibleType(Source, value)
case AltText:
if text, ok := value.(string); ok {
imageView.properties[AltText] = text
if imageView.created {
updateInnerHTML(imageView.htmlID(), imageView.session)
}
imageView.propertyChangedEvent(Source)
return true
}
notCompatibleType(tag, value)
updateInnerHTML(htmlID, session)
case LoadedEvent, ErrorEvent:
if listeners, ok := valueToNoParamListeners[ImageView](value); ok {
if listeners == nil {
delete(imageView.properties, tag)
} else {
imageView.properties[tag] = listeners
}
return true
}
case ImageVerticalAlign, ImageHorizontalAlign:
updateCSSStyle(htmlID, session)
default:
if imageView.viewData.set(tag, value) {
if imageView.created {
switch tag {
case ImageVerticalAlign, ImageHorizontalAlign:
updateCSSStyle(imageView.htmlID(), imageView.session)
}
}
return true
}
imageView.viewData.propertyChanged(tag)
}
return false
}
func (imageView *imageViewData) Get(tag string) any {
return imageView.viewData.get(imageView.normalizeTag(tag))
}
func (imageView *imageViewData) imageListeners(tag string) []func(ImageView) {
if value := imageView.getRaw(tag); value != nil {
if listeners, ok := value.([]func(ImageView)); ok {
return listeners
}
}
return []func(ImageView){}
}
func (imageView *imageViewData) srcSet(path string) string {
if value := imageView.getRaw(SrcSet); value != nil {
func imageViewSrcSet(view View, path string) string {
if value := view.getRaw(SrcSet); value != nil {
if text, ok := value.(string); ok {
srcset := strings.Split(text, ",")
buffer := allocStringBuilder()
@ -258,9 +216,9 @@ func (imageView *imageViewData) htmlTag() string {
return "img"
}
func (imageView *imageViewData) src(src string) (string, string) {
func imageViewSrc(view View, src string) (string, string) {
if src != "" && src[0] == '@' {
if image, ok := imageView.Session().ImageConstant(src[1:]); ok {
if image, ok := view.Session().ImageConstant(src[1:]); ok {
src = image
} else {
src = ""
@ -268,7 +226,7 @@ func (imageView *imageViewData) src(src string) (string, string) {
}
if src != "" {
return src, imageView.srcSet(src)
return src, imageViewSrcSet(view, src)
}
return "", ""
}
@ -278,7 +236,7 @@ func (imageView *imageViewData) htmlProperties(self View, buffer *strings.Builde
imageView.viewData.htmlProperties(self, buffer)
if imageResource, ok := imageProperty(imageView, Source, imageView.Session()); ok && imageResource != "" {
if src, srcset := imageView.src(imageResource); src != "" {
if src, srcset := imageViewSrc(imageView, imageResource); src != "" {
buffer.WriteString(` src="`)
buffer.WriteString(src)
buffer.WriteString(`"`)
@ -298,7 +256,7 @@ func (imageView *imageViewData) htmlProperties(self View, buffer *strings.Builde
buffer.WriteString(` onload="imageLoaded(this, event)"`)
if len(imageView.imageListeners(ErrorEvent)) > 0 {
if len(getNoArgEventListeners[ImageView](imageView, nil, ErrorEvent)) > 0 {
buffer.WriteString(` onerror="imageError(this, event)"`)
}
}
@ -338,10 +296,10 @@ func (imageView *imageViewData) cssStyle(self View, builder cssBuilder) {
}
}
func (imageView *imageViewData) handleCommand(self View, command string, data DataObject) bool {
func (imageView *imageViewData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command {
case "imageViewError":
for _, listener := range imageView.imageListeners(ErrorEvent) {
for _, listener := range getNoArgEventListeners[ImageView](imageView, nil, ErrorEvent) {
listener(imageView)
}
@ -350,7 +308,7 @@ func (imageView *imageViewData) handleCommand(self View, command string, data Da
imageView.naturalHeight = dataFloatProperty(data, "natural-height")
imageView.currentSrc, _ = data.PropertyValue("current-src")
for _, listener := range imageView.imageListeners(LoadedEvent) {
for _, listener := range getNoArgEventListeners[ImageView](imageView, nil, LoadedEvent) {
listener(imageView)
}
@ -371,11 +329,7 @@ func (imageView *imageViewData) CurrentSource() string {
// GetImageViewSource returns the image URL of an ImageView subview.
// If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned
func GetImageViewSource(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
if image, ok := imageProperty(view, Source, view.Session()); ok {
return image
}
@ -387,11 +341,7 @@ func GetImageViewSource(view View, subviewID ...string) string {
// GetImageViewAltText returns an alternative text description of an ImageView subview.
// If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned
func GetImageViewAltText(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(AltText); value != nil {
if text, ok := value.(string); ok {
text, _ = view.Session().GetString(text)

View File

@ -2,27 +2,58 @@ package rui
import "strings"
// Constants which represent [View] specific keyboard events properties
const (
// KeyDown is the constant for "key-down-event" property tag.
// The "key-down-event" event is fired when a key is pressed.
// The main listener format:
// func(View, KeyEvent).
// The additional listener formats:
// func(KeyEvent), func(View), and func().
KeyDownEvent = "key-down-event"
// KeyDownEvent is the constant for "key-down-event" property tag.
//
// Used by View.
// Is fired when a key is pressed.
//
// General listener format:
//
// func(view rui.View, event rui.KeyEvent).
//
// where:
// - view - Interface of a view which generated this event,
// - event - Key event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.KeyEvent)
// func()
KeyDownEvent PropertyName = "key-down-event"
// KeyPp is the constant for "key-up-event" property tag.
// The "key-up-event" event is fired when a key is released.
// The main listener format:
// func(View, KeyEvent).
// The additional listener formats:
// func(KeyEvent), func(View), and func().
KeyUpEvent = "key-up-event"
// KeyUpEvent is the constant for "key-up-event" property tag.
//
// Used by View.
// Is fired when a key is released.
//
// General listener format:
//
// func(view rui.View, event rui.KeyEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Key event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.KeyEvent)
// func()
KeyUpEvent PropertyName = "key-up-event"
)
// ControlKeyMask represent ORed state of keyboard's control keys like [AltKey], [CtrlKey], [ShiftKey] and [MetaKey]
type ControlKeyMask int
// KeyCode is a string representation the a physical key being pressed.
// The value is not affected by the current keyboard layout or modifier state,
// so a particular key will always have the same value.
type KeyCode string
// Constants for specific keyboard keys.
const (
// AltKey is the mask of the "alt" key
AltKey ControlKeyMask = 1
@ -33,114 +64,326 @@ const (
// 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"
// KeyA represent "A" key on the keyboard
KeyA KeyCode = "KeyA"
// KeyB represent "B" key on the keyboard
KeyB KeyCode = "KeyB"
// KeyC represent "C" key on the keyboard
KeyC KeyCode = "KeyC"
// KeyD represent "D" key on the keyboard
KeyD KeyCode = "KeyD"
// KeyE represent "E" key on the keyboard
KeyE KeyCode = "KeyE"
// KeyF represent "F" key on the keyboard
KeyF KeyCode = "KeyF"
// KeyG represent "G" key on the keyboard
KeyG KeyCode = "KeyG"
// KeyH represent "H" key on the keyboard
KeyH KeyCode = "KeyH"
// KeyI represent "I" key on the keyboard
KeyI KeyCode = "KeyI"
// KeyJ represent "J" key on the keyboard
KeyJ KeyCode = "KeyJ"
// KeyK represent "K" key on the keyboard
KeyK KeyCode = "KeyK"
// KeyL represent "L" key on the keyboard
KeyL KeyCode = "KeyL"
// KeyM represent "M" key on the keyboard
KeyM KeyCode = "KeyM"
// KeyN represent "N" key on the keyboard
KeyN KeyCode = "KeyN"
// KeyO represent "O" key on the keyboard
KeyO KeyCode = "KeyO"
// KeyP represent "P" key on the keyboard
KeyP KeyCode = "KeyP"
// KeyQ represent "Q" key on the keyboard
KeyQ KeyCode = "KeyQ"
// KeyR represent "R" key on the keyboard
KeyR KeyCode = "KeyR"
// KeyS represent "S" key on the keyboard
KeyS KeyCode = "KeyS"
// KeyT represent "T" key on the keyboard
KeyT KeyCode = "KeyT"
// KeyU represent "U" key on the keyboard
KeyU KeyCode = "KeyU"
// KeyV represent "V" key on the keyboard
KeyV KeyCode = "KeyV"
// KeyW represent "W" key on the keyboard
KeyW KeyCode = "KeyW"
// KeyX represent "X" key on the keyboard
KeyX KeyCode = "KeyX"
// KeyY represent "Y" key on the keyboard
KeyY KeyCode = "KeyY"
// KeyZ represent "Z" key on the keyboard
KeyZ KeyCode = "KeyZ"
// Digit0Key represent "Digit0" key on the keyboard
Digit0Key KeyCode = "Digit0"
// Digit1Key represent "Digit1" key on the keyboard
Digit1Key KeyCode = "Digit1"
// Digit2Key represent "Digit2" key on the keyboard
Digit2Key KeyCode = "Digit2"
// Digit3Key represent "Digit3" key on the keyboard
Digit3Key KeyCode = "Digit3"
// Digit4Key represent "Digit4" key on the keyboard
Digit4Key KeyCode = "Digit4"
// Digit5Key represent "Digit5" key on the keyboard
Digit5Key KeyCode = "Digit5"
// Digit6Key represent "Digit6" key on the keyboard
Digit6Key KeyCode = "Digit6"
// Digit7Key represent "Digit7" key on the keyboard
Digit7Key KeyCode = "Digit7"
// Digit8Key represent "Digit8" key on the keyboard
Digit8Key KeyCode = "Digit8"
// Digit9Key represent "Digit9" key on the keyboard
Digit9Key KeyCode = "Digit9"
// SpaceKey represent "Space" key on the keyboard
SpaceKey KeyCode = "Space"
// MinusKey represent "Minus" key on the keyboard
MinusKey KeyCode = "Minus"
// EqualKey represent "Equal" key on the keyboard
EqualKey KeyCode = "Equal"
// IntlBackslashKey represent "IntlBackslash" key on the keyboard
IntlBackslashKey KeyCode = "IntlBackslash"
// BracketLeftKey represent "BracketLeft" key on the keyboard
BracketLeftKey KeyCode = "BracketLeft"
// BracketRightKey represent "BracketRight" key on the keyboard
BracketRightKey KeyCode = "BracketRight"
// SemicolonKey represent "Semicolon" key on the keyboard
SemicolonKey KeyCode = "Semicolon"
// CommaKey represent "Comma" key on the keyboard
CommaKey KeyCode = "Comma"
// PeriodKey represent "Period" key on the keyboard
PeriodKey KeyCode = "Period"
// QuoteKey represent "Quote" key on the keyboard
QuoteKey KeyCode = "Quote"
// BackquoteKey represent "Backquote" key on the keyboard
BackquoteKey KeyCode = "Backquote"
// SlashKey represent "Slash" key on the keyboard
SlashKey KeyCode = "Slash"
// EscapeKey represent "Escape" key on the keyboard
EscapeKey KeyCode = "Escape"
// EnterKey represent "Enter" key on the keyboard
EnterKey KeyCode = "Enter"
// TabKey represent "Tab" key on the keyboard
TabKey KeyCode = "Tab"
// CapsLockKey represent "CapsLock" key on the keyboard
CapsLockKey KeyCode = "CapsLock"
// DeleteKey represent "Delete" key on the keyboard
DeleteKey KeyCode = "Delete"
// InsertKey represent "Insert" key on the keyboard
InsertKey KeyCode = "Insert"
// HelpKey represent "Help" key on the keyboard
HelpKey KeyCode = "Help"
// BackspaceKey represent "Backspace" key on the keyboard
BackspaceKey KeyCode = "Backspace"
// PrintScreenKey represent "PrintScreen" key on the keyboard
PrintScreenKey KeyCode = "PrintScreen"
// ScrollLockKey represent "ScrollLock" key on the keyboard
ScrollLockKey KeyCode = "ScrollLock"
// PauseKey represent "Pause" key on the keyboard
PauseKey KeyCode = "Pause"
// ContextMenuKey represent "ContextMenu" key on the keyboard
ContextMenuKey KeyCode = "ContextMenu"
// ArrowLeftKey represent "ArrowLeft" key on the keyboard
ArrowLeftKey KeyCode = "ArrowLeft"
// ArrowRightKey represent "ArrowRight" key on the keyboard
ArrowRightKey KeyCode = "ArrowRight"
// ArrowUpKey represent "ArrowUp" key on the keyboard
ArrowUpKey KeyCode = "ArrowUp"
// ArrowDownKey represent "ArrowDown" key on the keyboard
ArrowDownKey KeyCode = "ArrowDown"
// HomeKey represent "Home" key on the keyboard
HomeKey KeyCode = "Home"
// EndKey represent "End" key on the keyboard
EndKey KeyCode = "End"
// PageUpKey represent "PageUp" key on the keyboard
PageUpKey KeyCode = "PageUp"
// PageDownKey represent "PageDown" key on the keyboard
PageDownKey KeyCode = "PageDown"
// F1Key represent "F1" key on the keyboard
F1Key KeyCode = "F1"
// F2Key represent "F2" key on the keyboard
F2Key KeyCode = "F2"
// F3Key represent "F3" key on the keyboard
F3Key KeyCode = "F3"
// F4Key represent "F4" key on the keyboard
F4Key KeyCode = "F4"
// F5Key represent "F5" key on the keyboard
F5Key KeyCode = "F5"
// F6Key represent "F6" key on the keyboard
F6Key KeyCode = "F6"
// F7Key represent "F7" key on the keyboard
F7Key KeyCode = "F7"
// F8Key represent "F8" key on the keyboard
F8Key KeyCode = "F8"
// F9Key represent "F9" key on the keyboard
F9Key KeyCode = "F9"
// F10Key represent "F10" key on the keyboard
F10Key KeyCode = "F10"
// F11Key represent "F11" key on the keyboard
F11Key KeyCode = "F11"
// F12Key represent "F12" key on the keyboard
F12Key KeyCode = "F12"
// F13Key represent "F13" key on the keyboard
F13Key KeyCode = "F13"
// NumLockKey represent "NumLock" key on the keyboard
NumLockKey KeyCode = "NumLock"
// NumpadKey0 represent "Numpad0" key on the keyboard
NumpadKey0 KeyCode = "Numpad0"
// NumpadKey1 represent "Numpad1" key on the keyboard
NumpadKey1 KeyCode = "Numpad1"
// NumpadKey2 represent "Numpad2" key on the keyboard
NumpadKey2 KeyCode = "Numpad2"
// NumpadKey3 represent "Numpad3" key on the keyboard
NumpadKey3 KeyCode = "Numpad3"
// NumpadKey4 represent "Numpad4" key on the keyboard
NumpadKey4 KeyCode = "Numpad4"
// NumpadKey5 represent "Numpad5" key on the keyboard
NumpadKey5 KeyCode = "Numpad5"
// NumpadKey6 represent "Numpad6" key on the keyboard
NumpadKey6 KeyCode = "Numpad6"
// NumpadKey7 represent "Numpad7" key on the keyboard
NumpadKey7 KeyCode = "Numpad7"
// NumpadKey8 represent "Numpad8" key on the keyboard
NumpadKey8 KeyCode = "Numpad8"
// NumpadKey9 represent "Numpad9" key on the keyboard
NumpadKey9 KeyCode = "Numpad9"
// NumpadDecimalKey represent "NumpadDecimal" key on the keyboard
NumpadDecimalKey KeyCode = "NumpadDecimal"
// NumpadEnterKey represent "NumpadEnter" key on the keyboard
NumpadEnterKey KeyCode = "NumpadEnter"
// NumpadAddKey represent "NumpadAdd" key on the keyboard
NumpadAddKey KeyCode = "NumpadAdd"
// NumpadSubtractKey represent "NumpadSubtract" key on the keyboard
NumpadSubtractKey KeyCode = "NumpadSubtract"
// NumpadMultiplyKey represent "NumpadMultiply" key on the keyboard
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"
// NumpadDivideKey represent "NumpadDivide" key on the keyboard
NumpadDivideKey KeyCode = "NumpadDivide"
// ShiftLeftKey represent "ShiftLeft" key on the keyboard
ShiftLeftKey KeyCode = "ShiftLeft"
// ShiftRightKey represent "ShiftRight" key on the keyboard
ShiftRightKey KeyCode = "ShiftRight"
// ControlLeftKey represent "ControlLeft" key on the keyboard
ControlLeftKey KeyCode = "ControlLeft"
// ControlRightKey represent "ControlRight" key on the keyboard
ControlRightKey KeyCode = "ControlRight"
// AltLeftKey represent "AltLeft" key on the keyboard
AltLeftKey KeyCode = "AltLeft"
// AltRightKey represent "AltRight" key on the keyboard
AltRightKey KeyCode = "AltRight"
// MetaLeftKey represent "MetaLeft" key on the keyboard
MetaLeftKey KeyCode = "MetaLeft"
// MetaRightKey represent "MetaRight" key on the keyboard
MetaRightKey KeyCode = "MetaRight"
)
// KeyEvent represent a keyboard event
type KeyEvent struct {
// TimeStamp is the time at which the event was created (in milliseconds).
// This value is time since epoch—but in reality, browsers' definitions vary.
@ -190,402 +433,24 @@ func (event *KeyEvent) init(data DataObject) {
event.MetaKey = getBool("metaKey")
}
func valueToEventListeners[V View, E any](value any) ([]func(V, E), bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case func(V, E):
return []func(V, E){value}, true
case func(E):
fn := func(_ V, event E) {
value(event)
}
return []func(V, E){fn}, true
case func(V):
fn := func(view V, _ E) {
value(view)
}
return []func(V, E){fn}, true
case func():
fn := func(V, E) {
value()
}
return []func(V, E){fn}, true
case []func(V, E):
if len(value) == 0 {
return nil, true
}
for _, fn := range value {
if fn == nil {
return nil, false
}
}
return value, true
case []func(E):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(_ V, event E) {
v(event)
}
}
return listeners, true
case []func(V):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view V, _ E) {
v(view)
}
}
return listeners, true
case []func():
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(V, E) {
v()
}
}
return listeners, true
case []any:
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
switch v := v.(type) {
case func(V, E):
listeners[i] = v
case func(E):
listeners[i] = func(_ V, event E) {
v(event)
}
case func(V):
listeners[i] = func(view V, _ E) {
v(view)
}
case func():
listeners[i] = func(V, E) {
v()
}
default:
return nil, false
}
}
return listeners, true
}
return nil, false
}
func valueToEventWithOldListeners[V View, E any](value any) ([]func(V, E, E), bool) {
if value == nil {
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 {
listeners, ok := valueToEventListeners[View, KeyEvent](value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeKeyListener(tag)
} else {
switch tag {
case KeyDownEvent:
view.properties[tag] = listeners
if view.created {
view.session.updateProperty(view.htmlID(), "onkeydown", "keyDownEvent(this, event)")
}
case KeyUpEvent:
view.properties[tag] = listeners
if view.created {
view.session.updateProperty(view.htmlID(), "onkeyup", "keyUpEvent(this, event)")
}
default:
return false
}
}
return true
}
func (view *viewData) removeKeyListener(tag string) {
delete(view.properties, tag)
if view.created {
switch tag {
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) {
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)); ok {
return result
}
}
}
return []func(V, E){}
}
func keyEventsHtml(view View, buffer *strings.Builder) {
if len(getEventListeners[View, KeyEvent](view, nil, KeyDownEvent)) > 0 {
if len(getOneArgEventListeners[View, KeyEvent](view, nil, KeyDownEvent)) > 0 {
buffer.WriteString(`onkeydown="keyDownEvent(this, event)" `)
} else if view.Focusable() {
if len(getEventListeners[View, MouseEvent](view, nil, ClickEvent)) > 0 {
if len(getOneArgEventListeners[View, MouseEvent](view, nil, ClickEvent)) > 0 {
buffer.WriteString(`onkeydown="keyDownEvent(this, event)" `)
}
}
if listeners := getEventListeners[View, KeyEvent](view, nil, KeyUpEvent); len(listeners) > 0 {
if listeners := getOneArgEventListeners[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 PropertyName, data DataObject) {
var event KeyEvent
event.init(data)
listeners := getEventListeners[View, KeyEvent](view, nil, tag)
listeners := getOneArgEventListeners[View, KeyEvent](view, nil, tag)
if len(listeners) > 0 {
for _, listener := range listeners {
@ -595,7 +460,7 @@ func handleKeyEvents(view View, tag string, data DataObject) {
}
if tag == KeyDownEvent && view.Focusable() && (event.Key == " " || event.Key == "Enter") && !IsDisabled(view) {
if listeners := getEventListeners[View, MouseEvent](view, nil, ClickEvent); len(listeners) > 0 {
if listeners := getOneArgEventListeners[View, MouseEvent](view, nil, ClickEvent); len(listeners) > 0 {
clickEvent := MouseEvent{
TimeStamp: event.TimeStamp,
Button: PrimaryMouseButton,
@ -621,11 +486,11 @@ func handleKeyEvents(view View, tag string, data DataObject) {
// GetKeyDownListeners returns the "key-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.
func GetKeyDownListeners(view View, subviewID ...string) []func(View, KeyEvent) {
return getEventListeners[View, KeyEvent](view, subviewID, KeyDownEvent)
return getOneArgEventListeners[View, KeyEvent](view, subviewID, KeyDownEvent)
}
// GetKeyUpListeners returns the "key-up-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.
func GetKeyUpListeners(view View, subviewID ...string) []func(View, KeyEvent) {
return getEventListeners[View, KeyEvent](view, subviewID, KeyUpEvent)
return getOneArgEventListeners[View, KeyEvent](view, subviewID, KeyUpEvent)
}

View File

@ -4,6 +4,7 @@ import (
"strings"
)
// Constants which represent values of the "orientation" property of the [ListLayout]
const (
// TopDownOrientation - subviews are arranged from top to bottom. Synonym of VerticalOrientation
TopDownOrientation = 0
@ -16,7 +17,10 @@ const (
// EndToStartOrientation - subviews are arranged from right to left
EndToStartOrientation = 3
)
// Constants which represent values of the "list-wrap" property of the [ListLayout]
const (
// ListWrapOff - subviews are scrolled and "true" if a new row/column starts
ListWrapOff = 0
@ -27,7 +31,7 @@ const (
ListWrapReverse = 2
)
// ListLayout - list-container of View
// ListLayout represents a ListLayout view
type ListLayout interface {
ViewsContainer
// UpdateContent updates child Views if the "content" property value is set to ListAdapter,
@ -49,7 +53,8 @@ func NewListLayout(session Session, params Params) ListLayout {
}
func newListLayout(session Session) View {
return NewListLayout(session, nil)
//return NewListLayout(session, nil)
return new(listLayoutData)
}
// Init initialize fields of ViewsAlignContainer by default values
@ -57,14 +62,16 @@ func (listLayout *listLayoutData) init(session Session) {
listLayout.viewsContainerData.init(session)
listLayout.tag = "ListLayout"
listLayout.systemClass = "ruiListLayout"
listLayout.normalize = normalizeListLayoutTag
listLayout.get = listLayout.getFunc
listLayout.set = listLayout.setFunc
listLayout.remove = listLayout.removeFunc
listLayout.changed = listLayout.propertyChanged
}
func (listLayout *listLayoutData) String() string {
return getViewString(listLayout, nil)
}
func (listLayout *listLayoutData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeListLayoutTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case "wrap":
tag = ListWrap
@ -78,91 +85,90 @@ func (listLayout *listLayoutData) normalizeTag(tag string) string {
return tag
}
func (listLayout *listLayoutData) Get(tag string) any {
return listLayout.get(listLayout.normalizeTag(tag))
}
func (listLayout *listLayoutData) get(tag string) any {
if tag == Gap {
func (listLayout *listLayoutData) getFunc(tag PropertyName) any {
switch tag {
case Gap:
if rowGap := GetListRowGap(listLayout); rowGap.Equal(GetListColumnGap(listLayout)) {
return rowGap
}
return AutoSize()
}
return listLayout.viewsContainerData.get(tag)
}
func (listLayout *listLayoutData) Remove(tag string) {
listLayout.remove(listLayout.normalizeTag(tag))
}
func (listLayout *listLayoutData) remove(tag string) {
switch tag {
case Gap:
listLayout.remove(ListRowGap)
listLayout.remove(ListColumnGap)
return
case Content:
listLayout.adapter = nil
}
listLayout.viewsContainerData.remove(tag)
if listLayout.created {
switch tag {
case Orientation, ListWrap, HorizontalAlign, VerticalAlign:
updateCSSStyle(listLayout.htmlID(), listLayout.session)
if listLayout.adapter != nil {
return listLayout.adapter
}
}
return listLayout.viewsContainerData.getFunc(tag)
}
func (listLayout *listLayoutData) Set(tag string, value any) bool {
return listLayout.set(listLayout.normalizeTag(tag), value)
}
func (listLayout *listLayoutData) set(tag string, value any) bool {
if value == nil {
listLayout.remove(tag)
return true
}
func (listLayout *listLayoutData) removeFunc(tag PropertyName) []PropertyName {
switch tag {
case Gap:
return listLayout.set(ListRowGap, value) && listLayout.set(ListColumnGap, value)
result := []PropertyName{}
for _, tag := range []PropertyName{ListRowGap, ListColumnGap} {
if listLayout.getRaw(tag) != nil {
listLayout.setRaw(tag, nil)
result = append(result, tag)
}
}
return result
case Content:
listLayout.viewsContainerData.removeFunc(Content)
listLayout.adapter = nil
return []PropertyName{Content}
}
return listLayout.viewsContainerData.removeFunc(tag)
}
func (listLayout *listLayoutData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case Gap:
result := listLayout.setFunc(ListRowGap, value)
if result != nil {
if gap := listLayout.getRaw(ListRowGap); gap != nil {
listLayout.setRaw(ListColumnGap, gap)
result = append(result, ListColumnGap)
}
}
return result
case Content:
if adapter, ok := value.(ListAdapter); ok {
listLayout.adapter = adapter
listLayout.UpdateContent()
// TODO
return true
listLayout.createContent()
} else if listLayout.setContent(value) {
listLayout.adapter = nil
} else {
return nil
}
listLayout.adapter = nil
return []PropertyName{Content}
}
return listLayout.viewsContainerData.setFunc(tag, value)
}
if listLayout.viewsContainerData.set(tag, value) {
if listLayout.created {
switch tag {
case Orientation, ListWrap, HorizontalAlign, VerticalAlign:
updateCSSStyle(listLayout.htmlID(), listLayout.session)
}
}
return true
func (listLayout *listLayoutData) propertyChanged(tag PropertyName) {
switch tag {
case Orientation, ListWrap, HorizontalAlign, VerticalAlign:
updateCSSStyle(listLayout.htmlID(), listLayout.Session())
default:
listLayout.viewsContainerData.propertyChanged(tag)
}
return false
}
func (listLayout *listLayoutData) htmlSubviews(self View, buffer *strings.Builder) {
if listLayout.views != nil {
for _, view := range listLayout.views {
view.addToCSSStyle(map[string]string{`flex`: `0 0 auto`})
viewHTML(view, buffer)
viewHTML(view, buffer, "")
}
}
}
func (listLayout *listLayoutData) UpdateContent() {
func (listLayout *listLayoutData) createContent() bool {
if adapter := listLayout.adapter; adapter != nil {
listLayout.views = []View{}
@ -181,11 +187,20 @@ func (listLayout *listLayoutData) UpdateContent() {
}
}
return true
}
return false
}
func (listLayout *listLayoutData) UpdateContent() {
if listLayout.createContent() {
if listLayout.created {
updateInnerHTML(htmlID, session)
updateInnerHTML(listLayout.htmlID(), listLayout.session)
}
listLayout.propertyChangedEvent(Content)
if listener, ok := listLayout.changeListener[Content]; ok {
listener(listLayout, Content)
}
}
}
@ -207,11 +222,7 @@ func GetListHorizontalAlign(view View, subviewID ...string) int {
// TopDownOrientation (0), StartToEndOrientation (1), BottomUpOrientation (2), or EndToStartOrientation (3)
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetListOrientation(view View, subviewID ...string) int {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
if orientation, ok := valueToOrientation(view.Get(Orientation), view.Session()); ok {
return orientation
}
@ -249,11 +260,7 @@ func GetListColumnGap(view View, subviewID ...string) SizeUnit {
// otherwise does nothing.
// If the second argument (subviewID) is not specified or it is "" then the first argument (view) updates.
func UpdateContent(view View, subviewID ...string) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
switch view := view.(type) {
case GridLayout:
view.UpdateGridContent()
@ -263,6 +270,9 @@ func UpdateContent(view View, subviewID ...string) {
case ListView:
view.ReloadListViewData()
case TableView:
view.ReloadTableData()
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -5,80 +5,171 @@ import (
"strings"
)
// Constants related to [View] mouse events properties
const (
// ClickEvent is the constant for "click-event" property tag.
// The "click-event" event occurs when the user clicks on the View.
// The main listener format:
// func(View, MouseEvent).
// The additional listener formats:
// func(MouseEvent), func(View), and func().
ClickEvent = "click-event"
//
// Used by View.
// Occur when the user clicks on the view.
//
// General listener format:
//
// func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
ClickEvent PropertyName = "click-event"
// DoubleClickEvent is the constant for "double-click-event" property tag.
// The "double-click-event" event occurs when the user double clicks on the View.
// The main listener format:
// func(View, MouseEvent).
// The additional listener formats:
// func(MouseEvent), func(View), and func().
DoubleClickEvent = "double-click-event"
//
// Used by View.
// Occur when the user double clicks on the view.
//
// General listener format:
//
// func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
DoubleClickEvent PropertyName = "double-click-event"
// MouseDown is the constant for "mouse-down" property tag.
// The "mouse-down" event is fired at a View when a pointing device button is pressed
// while the pointer is inside the view.
// The main listener format:
// func(View, MouseEvent).
// The additional listener formats:
// func(MouseEvent), func(View), and func().
MouseDown = "mouse-down"
//
// Used by View.
// Is fired at a View when a pointing device button is pressed while the pointer is inside the view.
//
// General listener format:
//
// func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
MouseDown PropertyName = "mouse-down"
// MouseUp is the constant for "mouse-up" property tag.
// The "mouse-up" event is fired at a View when a button on a pointing device (such as a mouse
// or trackpad) is released while the pointer is located inside it.
// "mouse-up" events are the counterpoint to "mouse-down" events.
// The main listener format:
// func(View, MouseEvent).
// The additional listener formats:
// func(MouseEvent), func(View), and func().
MouseUp = "mouse-up"
//
// Used by View.
// Is fired at a View when a button on a pointing device (such as a mouse or trackpad) is released while the pointer is
// located inside it. "mouse-up" events are the counterpoint to "mouse-down" events.
//
// General listener format:
//
// func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
MouseUp PropertyName = "mouse-up"
// MouseMove is the constant for "mouse-move" property tag.
// The "mouse-move" event is fired at a view when a pointing device (usually a mouse) is moved
// while the cursor's hotspot is inside it.
// The main listener format:
// func(View, MouseEvent).
// The additional listener formats:
// func(MouseEvent), func(View), and func().
MouseMove = "mouse-move"
//
// Used by View.
// Is fired at a view when a pointing device(usually a mouse) is moved while the cursor's hotspot is inside it.
//
// General listener format:
//
// func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
MouseMove PropertyName = "mouse-move"
// MouseOut is the constant for "mouse-out" property tag.
// The "mouse-out" event is fired at a View when a pointing device (usually a mouse) is used to move
// the cursor so that it is no longer contained within the view or one of its children.
// "mouse-out" is also delivered to a view if the cursor enters a child view,
// because the child view obscures the visible area of the view.
// The main listener format:
// func(View, MouseEvent).
// The additional listener formats:
// func(MouseEvent), func(View), and func().
// The additional listener formats:
// func(MouseEvent), func(View), and func().
MouseOut = "mouse-out"
//
// Used by View.
// Is fired at a View when a pointing device (usually a mouse) is used to move the cursor so that it is no longer
// contained within the view or one of its children. "mouse-out" is also delivered to a view if the cursor enters a child
// view, because the child view obscures the visible area of the view.
//
// General listener format:
//
// func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
MouseOut PropertyName = "mouse-out"
// MouseOver is the constant for "mouse-over" property tag.
// The "mouse-over" event is fired at a View when a pointing device (such as a mouse or trackpad)
// is used to move the cursor onto the view or one of its child views.
// The main listener formats:
// func(View, MouseEvent).
// The additional listener formats:
// func(MouseEvent), func(View), and func().
MouseOver = "mouse-over"
//
// Used by View.
// Is fired at a View when a pointing device (such as a mouse or trackpad) is used to move the cursor onto the view or one
// of its child views.
//
// General listener format:
//
// func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
MouseOver PropertyName = "mouse-over"
// ContextMenuEvent is the constant for "context-menu-event" property tag.
// The "context-menu-event" event occurs when the user calls the context menu by the right mouse clicking.
// The main listener format:
// func(View, MouseEvent).
// The additional listener formats:
// func(MouseEvent), func(View), and func().
ContextMenuEvent = "context-menu-event"
//
// Used by View.
// Occur when the user calls the context menu by the right mouse clicking.
//
// General listener format:
//
// func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
ContextMenuEvent PropertyName = "context-menu-event"
// PrimaryMouseButton is a number of the main pressed button, usually the left button or the un-initialized state
PrimaryMouseButton = 0
@ -112,6 +203,7 @@ const (
MouseMask5 = 16
)
// MouseEvent represent a mouse event
type MouseEvent struct {
// TimeStamp is the time at which the event was created (in milliseconds).
// This value is time since epoch—but in reality, browsers' definitions vary.
@ -152,64 +244,6 @@ type MouseEvent struct {
MetaKey bool
}
var mouseEvents = map[string]struct{ jsEvent, jsFunc string }{
ClickEvent: {jsEvent: "onclick", jsFunc: "clickEvent"},
DoubleClickEvent: {jsEvent: "ondblclick", jsFunc: "doubleClickEvent"},
MouseDown: {jsEvent: "onmousedown", jsFunc: "mouseDownEvent"},
MouseUp: {jsEvent: "onmouseup", jsFunc: "mouseUpEvent"},
MouseMove: {jsEvent: "onmousemove", jsFunc: "mouseMoveEvent"},
MouseOut: {jsEvent: "onmouseout", jsFunc: "mouseOutEvent"},
MouseOver: {jsEvent: "onmouseover", jsFunc: "mouseOverEvent"},
ContextMenuEvent: {jsEvent: "oncontextmenu", jsFunc: "contextMenuEvent"},
}
func (view *viewData) setMouseListener(tag string, value any) bool {
listeners, ok := valueToEventListeners[View, MouseEvent](value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeMouseListener(tag)
} else if js, ok := mouseEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
view.session.updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)")
}
} else {
return false
}
return true
}
func (view *viewData) removeMouseListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := mouseEvents[tag]; ok {
view.session.removeProperty(view.htmlID(), js.jsEvent)
}
}
}
func mouseEventsHtml(view View, buffer *strings.Builder, hasTooltip bool) {
for tag, js := range mouseEvents {
if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View, MouseEvent)); ok && len(listeners) > 0 {
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 {
if value, ok := data.PropertyValue("timeStamp"); ok {
if index := strings.Index(value, "."); index > 0 {
@ -239,8 +273,8 @@ func (event *MouseEvent) init(data DataObject) {
event.MetaKey = dataBoolProperty(data, "metaKey")
}
func handleMouseEvents(view View, tag string, data DataObject) {
listeners := getEventListeners[View, MouseEvent](view, nil, tag)
func handleMouseEvents(view View, tag PropertyName, data DataObject) {
listeners := getOneArgEventListeners[View, MouseEvent](view, nil, tag)
if len(listeners) > 0 {
var event MouseEvent
event.init(data)
@ -254,48 +288,48 @@ func handleMouseEvents(view View, tag string, data DataObject) {
// GetClickListeners returns the "click-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.
func GetClickListeners(view View, subviewID ...string) []func(View, MouseEvent) {
return getEventListeners[View, MouseEvent](view, subviewID, ClickEvent)
return getOneArgEventListeners[View, MouseEvent](view, subviewID, ClickEvent)
}
// GetDoubleClickListeners returns the "double-click-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.
func GetDoubleClickListeners(view View, subviewID ...string) []func(View, MouseEvent) {
return getEventListeners[View, MouseEvent](view, subviewID, DoubleClickEvent)
return getOneArgEventListeners[View, MouseEvent](view, subviewID, DoubleClickEvent)
}
// GetContextMenuListeners returns the "context-menu" 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.
func GetContextMenuListeners(view View, subviewID ...string) []func(View, MouseEvent) {
return getEventListeners[View, MouseEvent](view, subviewID, ContextMenuEvent)
return getOneArgEventListeners[View, MouseEvent](view, subviewID, ContextMenuEvent)
}
// GetMouseDownListeners returns the "mouse-down" 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.
func GetMouseDownListeners(view View, subviewID ...string) []func(View, MouseEvent) {
return getEventListeners[View, MouseEvent](view, subviewID, MouseDown)
return getOneArgEventListeners[View, MouseEvent](view, subviewID, MouseDown)
}
// GetMouseUpListeners returns the "mouse-up" 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.
func GetMouseUpListeners(view View, subviewID ...string) []func(View, MouseEvent) {
return getEventListeners[View, MouseEvent](view, subviewID, MouseUp)
return getOneArgEventListeners[View, MouseEvent](view, subviewID, MouseUp)
}
// GetMouseMoveListeners returns the "mouse-move" 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.
func GetMouseMoveListeners(view View, subviewID ...string) []func(View, MouseEvent) {
return getEventListeners[View, MouseEvent](view, subviewID, MouseMove)
return getOneArgEventListeners[View, MouseEvent](view, subviewID, MouseMove)
}
// GetMouseOverListeners returns the "mouse-over" 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.
func GetMouseOverListeners(view View, subviewID ...string) []func(View, MouseEvent) {
return getEventListeners[View, MouseEvent](view, subviewID, MouseOver)
return getOneArgEventListeners[View, MouseEvent](view, subviewID, MouseOver)
}
// GetMouseOutListeners returns the "mouse-out" 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.
func GetMouseOutListeners(view View, subviewID ...string) []func(View, MouseEvent) {
return getEventListeners[View, MouseEvent](view, subviewID, MouseOut)
return getOneArgEventListeners[View, MouseEvent](view, subviewID, MouseOut)
}

View File

@ -1,39 +1,100 @@
package rui
import (
"fmt"
"math"
"strconv"
"strings"
)
// Constants related to [NumberPicker] specific properties and events
const (
// NumberChangedEvent is the constant for the "" property tag.
// The "number-changed" property sets listener(s) that track the change in the entered value.
NumberChangedEvent = "number-changed"
// NumberChangedEvent is the constant for "number-changed" property tag.
//
// Used by NumberPicker.
// Set listener(s) that track the change in the entered value.
//
// General listener format:
//
// func(picker rui.NumberPicker, newValue float64, oldValue float64)
//
// where:
// - picker - Interface of a number picker which generated this event,
// - newValue - New value,
// - oldValue - Old Value.
//
// Allowed listener formats:
//
// func(picker rui.NumberPicker, newValue float64)
// func(newValue float64, oldValue float64)
// func(newValue float64)
// func()
NumberChangedEvent PropertyName = "number-changed"
// NumberPickerType is the constant for the "number-picker-type" property tag.
// The "number-picker-type" int property sets the mode of NumberPicker. It can take the following values:
// * NumberEditor (0) - NumberPicker is presented by editor. Default value;
// * NumberSlider (1) - NumberPicker is presented by slider. |
NumberPickerType = "number-picker-type"
// NumberPickerType is the constant for "number-picker-type" property tag.
//
// Used by NumberPicker.
// Sets the visual representation.
//
// Supported types: int, string.
//
// Values:
// - 0 (NumberEditor) or "editor" - Displayed as an editor.
// - 1 (NumberSlider) or "slider" - Displayed as a slider.
NumberPickerType PropertyName = "number-picker-type"
// NumberPickerMin is the constant for the "number-picker-min" property tag.
// The "number-picker-min" int property sets the minimum value of NumberPicker. The default value is 0.
NumberPickerMin = "number-picker-min"
// NumberPickerMin is the constant for "number-picker-min" property tag.
//
// Used by NumberPicker.
// Set the minimum value. The default value is 0.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
NumberPickerMin PropertyName = "number-picker-min"
// NumberPickerMax is the constant for the "number-picker-max" property tag.
// The "number-picker-max" int property sets the maximum value of NumberPicker. The default value is 1.
NumberPickerMax = "number-picker-max"
// NumberPickerMax is the constant for "number-picker-max" property tag.
//
// Used by NumberPicker.
// Set the maximum value. The default value is 1.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
NumberPickerMax PropertyName = "number-picker-max"
// NumberPickerStep is the constant for the "number-picker-step" property tag.
// The "number-picker-step" int property sets the value change step of NumberPicker
NumberPickerStep = "number-picker-step"
// NumberPickerStep is the constant for "number-picker-step" property tag.
//
// Used by NumberPicker.
// Set the value change step.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
NumberPickerStep PropertyName = "number-picker-step"
// NumberPickerValue is the constant for the "number-picker-value" property tag.
// The "number-picker-value" int property sets the current value of NumberPicker. The default value is 0.
NumberPickerValue = "number-picker-value"
// NumberPickerValue is the constant for "number-picker-value" property tag.
//
// Used by NumberPicker.
// Current value. The default value is 0.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
NumberPickerValue PropertyName = "number-picker-value"
// NumberPickerValue is the constant for "number-picker-value" property tag.
//
// Used by NumberPicker.
// Precision of displaying fractional part in editor. The default value is 0 (not used).
//
// Supported types: int, int8...int64, uint, uint8...uint64, string.
//
// Internal type is float, other types converted to it during assignment.
NumberPickerPrecision PropertyName = "number-picker-precision"
)
// Constants which describe values of the "number-picker-type" property of a [NumberPicker]
const (
// NumberEditor - type of NumberPicker. NumberPicker is presented by editor
NumberEditor = 0
@ -42,15 +103,13 @@ const (
NumberSlider = 1
)
// NumberPicker - NumberPicker view
// NumberPicker represents a NumberPicker view
type NumberPicker interface {
View
}
type numberPickerData struct {
viewData
dataList
numberChangedListeners []func(NumberPicker, float64, float64)
}
// NewNumberPicker create new NumberPicker object and return it
@ -62,165 +121,102 @@ func NewNumberPicker(session Session, params Params) NumberPicker {
}
func newNumberPicker(session Session) View {
return NewNumberPicker(session, nil)
return new(numberPickerData)
}
func (picker *numberPickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "NumberPicker"
picker.hasHtmlDisabled = true
picker.numberChangedListeners = []func(NumberPicker, float64, float64){}
picker.dataListInit()
}
func (picker *numberPickerData) String() string {
return getViewString(picker, nil)
picker.normalize = normalizeNumberPickerTag
picker.set = picker.setFunc
picker.changed = picker.propertyChanged
}
func (picker *numberPickerData) Focusable() bool {
return true
}
func (picker *numberPickerData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeNumberPickerTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Type, Min, Max, Step, Value:
case Type, Min, Max, Step, Value, "precision":
return "number-picker-" + tag
}
return picker.normalizeDataListTag(tag)
return normalizeDataListTag(tag)
}
func (picker *numberPickerData) Remove(tag string) {
picker.remove(picker.normalizeTag(tag))
}
func (picker *numberPickerData) remove(tag string) {
func (picker *numberPickerData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case NumberChangedEvent:
if len(picker.numberChangedListeners) > 0 {
picker.numberChangedListeners = []func(NumberPicker, float64, float64){}
picker.propertyChangedEvent(tag)
}
return setTwoArgEventListener[NumberPicker, float64](picker, tag, value)
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)
}
case DataList:
if len(picker.dataList.dataList) > 0 {
picker.setDataList(picker, []string{}, true)
}
default:
picker.viewData.remove(tag)
picker.propertyChanged(tag)
}
}
func (picker *numberPickerData) Set(tag string, value any) bool {
return picker.set(picker.normalizeTag(tag), value)
}
func (picker *numberPickerData) set(tag string, value any) bool {
if value == nil {
picker.remove(tag)
return true
}
switch tag {
case NumberChangedEvent:
listeners, ok := valueToEventWithOldListeners[NumberPicker, float64](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(NumberPicker, float64, float64){}
}
picker.numberChangedListeners = listeners
picker.propertyChangedEvent(tag)
return true
case NumberPickerValue:
oldValue := GetNumberPickerValue(picker)
picker.setRaw("old-number", GetNumberPickerValue(picker))
min, max := GetNumberPickerMinMax(picker)
if picker.setFloatProperty(NumberPickerValue, value, min, max) {
if f, ok := floatProperty(picker, NumberPickerValue, picker.Session(), min); ok && f != oldValue {
newValue, _ := floatTextProperty(picker, NumberPickerValue, picker.Session(), min)
if picker.created {
picker.session.callFunc("setInputValue", picker.htmlID(), newValue)
}
for _, listener := range picker.numberChangedListeners {
listener(picker, f, oldValue)
}
picker.propertyChangedEvent(tag)
}
return true
}
return setFloatProperty(picker, NumberPickerValue, value, min, max)
case DataList:
return picker.setDataList(picker, value, picker.created)
default:
if picker.viewData.set(tag, value) {
picker.propertyChanged(tag)
return true
}
return setDataList(picker, value, "")
}
return false
return picker.viewData.setFunc(tag, value)
}
func (picker *numberPickerData) propertyChanged(tag string) {
if picker.created {
switch tag {
case NumberPickerType:
if GetNumberPickerType(picker) == NumberSlider {
picker.session.updateProperty(picker.htmlID(), "type", "range")
} else {
picker.session.updateProperty(picker.htmlID(), "type", "number")
}
case NumberPickerMin:
min, _ := GetNumberPickerMinMax(picker)
picker.session.updateProperty(picker.htmlID(), Min, strconv.FormatFloat(min, 'f', -1, 32))
case NumberPickerMax:
_, max := GetNumberPickerMinMax(picker)
picker.session.updateProperty(picker.htmlID(), Max, strconv.FormatFloat(max, 'f', -1, 32))
case NumberPickerStep:
if step := GetNumberPickerStep(picker); step > 0 {
picker.session.updateProperty(picker.htmlID(), Step, strconv.FormatFloat(step, 'f', -1, 32))
} else {
picker.session.updateProperty(picker.htmlID(), Step, "any")
}
}
func (picker *numberPickerData) numberFormat() string {
if precission := GetNumberPickerPrecision(picker); precission > 0 {
return fmt.Sprintf("%%.%df", precission)
}
return "%g"
}
func (picker *numberPickerData) Get(tag string) any {
return picker.get(picker.normalizeTag(tag))
}
func (picker *numberPickerData) get(tag string) any {
func (picker *numberPickerData) propertyChanged(tag PropertyName) {
switch tag {
case NumberChangedEvent:
return picker.numberChangedListeners
case NumberPickerType:
if GetNumberPickerType(picker) == NumberSlider {
picker.Session().updateProperty(picker.htmlID(), "type", "range")
} else {
picker.Session().updateProperty(picker.htmlID(), "type", "number")
}
case DataList:
return picker.dataList.dataList
case NumberPickerMin:
min, _ := GetNumberPickerMinMax(picker)
picker.Session().updateProperty(picker.htmlID(), "min", fmt.Sprintf(picker.numberFormat(), min))
case NumberPickerMax:
_, max := GetNumberPickerMinMax(picker)
picker.Session().updateProperty(picker.htmlID(), "max", fmt.Sprintf(picker.numberFormat(), max))
case NumberPickerStep:
if step := GetNumberPickerStep(picker); step > 0 {
picker.Session().updateProperty(picker.htmlID(), "step", fmt.Sprintf(picker.numberFormat(), step))
} else {
picker.Session().updateProperty(picker.htmlID(), "step", "any")
}
case NumberPickerValue:
value := GetNumberPickerValue(picker)
format := picker.numberFormat()
picker.Session().callFunc("setInputValue", picker.htmlID(), fmt.Sprintf(format, value))
if listeners := GetNumberChangedListeners(picker); len(listeners) > 0 {
old := 0.0
if val := picker.getRaw("old-number"); val != nil {
if n, ok := val.(float64); ok {
old = n
}
}
if old != value {
for _, listener := range listeners {
listener(picker, value, old)
}
}
}
default:
return picker.viewData.get(tag)
picker.viewData.propertyChanged(tag)
}
}
@ -229,7 +225,10 @@ func (picker *numberPickerData) htmlTag() string {
}
func (picker *numberPickerData) htmlSubviews(self View, buffer *strings.Builder) {
picker.dataListHtmlSubviews(self, buffer)
dataListHtmlSubviews(self, buffer, func(text string, session Session) string {
text, _ = session.resolveConstants(text)
return text
})
}
func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builder) {
@ -241,38 +240,39 @@ func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builde
buffer.WriteString(` type="number"`)
}
format := picker.numberFormat()
min, max := GetNumberPickerMinMax(picker)
if min != math.Inf(-1) {
buffer.WriteString(` min="`)
buffer.WriteString(strconv.FormatFloat(min, 'f', -1, 64))
buffer.WriteString(fmt.Sprintf(format, min))
buffer.WriteByte('"')
}
if max != math.Inf(1) {
buffer.WriteString(` max="`)
buffer.WriteString(strconv.FormatFloat(max, 'f', -1, 64))
buffer.WriteString(fmt.Sprintf(format, max))
buffer.WriteByte('"')
}
step := GetNumberPickerStep(picker)
if step != 0 {
buffer.WriteString(` step="`)
buffer.WriteString(strconv.FormatFloat(step, 'f', -1, 64))
buffer.WriteString(fmt.Sprintf(format, step))
buffer.WriteByte('"')
} else {
buffer.WriteString(` step="any"`)
}
buffer.WriteString(` value="`)
buffer.WriteString(strconv.FormatFloat(GetNumberPickerValue(picker), 'f', -1, 64))
buffer.WriteString(fmt.Sprintf(format, GetNumberPickerValue(picker)))
buffer.WriteByte('"')
buffer.WriteString(` oninput="editViewInputEvent(this)"`)
picker.dataListHtmlProperties(picker, buffer)
dataListHtmlProperties(picker, buffer)
}
func (picker *numberPickerData) handleCommand(self View, command string, data DataObject) bool {
func (picker *numberPickerData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command {
case "textChanged":
if text, ok := data.PropertyValue("text"); ok {
@ -280,9 +280,12 @@ func (picker *numberPickerData) handleCommand(self View, command string, data Da
oldValue := GetNumberPickerValue(picker)
picker.properties[NumberPickerValue] = text
if value != oldValue {
for _, listener := range picker.numberChangedListeners {
for _, listener := range GetNumberChangedListeners(picker) {
listener(picker, value, oldValue)
}
if listener, ok := picker.changeListener[NumberPickerValue]; ok {
listener(picker, NumberPickerValue)
}
}
}
}
@ -303,12 +306,8 @@ func GetNumberPickerType(view View, subviewID ...string) int {
// GetNumberPickerMinMax returns the min and max value of NumberPicker subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetNumberPickerMinMax(view View, subviewID ...string) (float64, float64) {
var pickerType int
if len(subviewID) > 0 && subviewID[0] != "" {
pickerType = GetNumberPickerType(view, subviewID[0])
} else {
pickerType = GetNumberPickerType(view)
}
view = getSubview(view, subviewID)
pickerType := GetNumberPickerType(view)
var defMin, defMax float64
if pickerType == NumberSlider {
@ -319,8 +318,8 @@ func GetNumberPickerMinMax(view View, subviewID ...string) (float64, float64) {
defMax = math.Inf(1)
}
min := floatStyledProperty(view, subviewID, NumberPickerMin, defMin)
max := floatStyledProperty(view, subviewID, NumberPickerMax, defMax)
min := floatStyledProperty(view, nil, NumberPickerMin, defMin)
max := floatStyledProperty(view, nil, NumberPickerMax, defMax)
if min > max {
return max, min
@ -331,14 +330,10 @@ func GetNumberPickerMinMax(view View, subviewID ...string) (float64, float64) {
// GetNumberPickerStep returns the value changing step of NumberPicker subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetNumberPickerStep(view View, subviewID ...string) float64 {
var max float64
if len(subviewID) > 0 && subviewID[0] != "" {
_, max = GetNumberPickerMinMax(view, subviewID[0])
} else {
_, max = GetNumberPickerMinMax(view)
}
view = getSubview(view, subviewID)
_, max := GetNumberPickerMinMax(view)
result := floatStyledProperty(view, subviewID, NumberPickerStep, 0)
result := floatStyledProperty(view, nil, NumberPickerStep, 0)
if result > max {
return max
}
@ -348,20 +343,20 @@ func GetNumberPickerStep(view View, subviewID ...string) float64 {
// GetNumberPickerValue returns the value of NumberPicker subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetNumberPickerValue(view View, subviewID ...string) float64 {
var min float64
if len(subviewID) > 0 && subviewID[0] != "" {
min, _ = GetNumberPickerMinMax(view, subviewID[0])
} else {
min, _ = GetNumberPickerMinMax(view)
}
result := floatStyledProperty(view, subviewID, NumberPickerValue, min)
return result
view = getSubview(view, subviewID)
min, _ := GetNumberPickerMinMax(view)
return floatStyledProperty(view, nil, NumberPickerValue, min)
}
// GetNumberChangedListeners returns the NumberChangedListener list of an NumberPicker subview.
// 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.
func GetNumberChangedListeners(view View, subviewID ...string) []func(NumberPicker, float64, float64) {
return getEventWithOldListeners[NumberPicker, float64](view, subviewID, NumberChangedEvent)
return getTwoArgEventListeners[NumberPicker, float64](view, subviewID, NumberChangedEvent)
}
// GetNumberPickerPrecision returns the precision of displaying fractional part in editor of NumberPicker subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetNumberPickerPrecision(view View, subviewID ...string) int {
return intStyledProperty(view, subviewID, NumberPickerPrecision, 0)
}

View File

@ -5,35 +5,50 @@ import (
"strings"
)
// OutlineProperty defines a view's outside border
type OutlineProperty interface {
Properties
stringWriter
fmt.Stringer
// ViewOutline returns style color and line width of an outline
ViewOutline(session Session) ViewOutline
}
type outlinePropertyData struct {
propertyList
dataProperty
}
// NewOutlineProperty creates the new OutlineProperty.
//
// The following properties can be used:
// - "color" (ColorTag) - Determines the line color (Color);
// - "width" (Width) - Determines the line thickness (SizeUnit).
func NewOutlineProperty(params Params) OutlineProperty {
outline := new(outlinePropertyData)
outline.properties = map[string]any{}
outline.init()
for tag, value := range params {
outline.Set(tag, value)
}
return outline
}
func (outline *outlinePropertyData) init() {
outline.propertyList.init()
outline.normalize = normalizeOutlineTag
outline.set = outlineSet
outline.supportedProperties = []PropertyName{Style, Width, ColorTag}
}
func (outline *outlinePropertyData) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("_{ ")
comma := false
for _, tag := range []string{Style, Width, ColorTag} {
for _, tag := range []PropertyName{Style, Width, ColorTag} {
if value, ok := outline.properties[tag]; ok {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
writePropertyValue(buffer, BorderStyle, value, indent)
comma = true
@ -47,46 +62,33 @@ func (outline *outlinePropertyData) String() string {
return runStringWriter(outline)
}
func (outline *outlinePropertyData) normalizeTag(tag string) string {
return strings.TrimPrefix(strings.ToLower(tag), "outline-")
func normalizeOutlineTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
return PropertyName(strings.TrimPrefix(string(tag), "outline-"))
}
func (outline *outlinePropertyData) Remove(tag string) {
delete(outline.properties, outline.normalizeTag(tag))
}
func (outline *outlinePropertyData) Set(tag string, value any) bool {
if value == nil {
outline.Remove(tag)
return true
}
tag = outline.normalizeTag(tag)
func outlineSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case Style:
return outline.setEnumProperty(Style, value, enumProperties[BorderStyle].values)
return setEnumProperty(properties, Style, value, enumProperties[BorderStyle].values)
case Width:
if width, ok := value.(SizeUnit); ok {
switch width.Type {
case SizeInFraction, SizeInPercent:
notCompatibleType(tag, value)
return false
return nil
}
}
return outline.setSizeProperty(Width, value)
return setSizeProperty(properties, Width, value)
case ColorTag:
return outline.setColorProperty(ColorTag, value)
return setColorProperty(properties, ColorTag, value)
default:
ErrorLogF(`"%s" property is not compatible with the OutlineProperty`, tag)
}
return false
}
func (outline *outlinePropertyData) Get(tag string) any {
return outline.propertyList.Get(outline.normalizeTag(tag))
return nil
}
func (outline *outlinePropertyData) ViewOutline(session Session) ViewOutline {
@ -98,8 +100,13 @@ func (outline *outlinePropertyData) ViewOutline(session Session) ViewOutline {
// ViewOutline describes parameters of a view border
type ViewOutline struct {
// Style of the outline line
Style int
// Color of the outline line
Color Color
// Width of the outline line
Width SizeUnit
}
@ -118,7 +125,7 @@ func (outline ViewOutline) cssString(session Session) string {
return builder.finish()
}
func getOutline(properties Properties) OutlineProperty {
func getOutlineProperty(properties Properties) OutlineProperty {
if value := properties.Get(Outline); value != nil {
if outline, ok := value.(OutlineProperty); ok {
return outline
@ -128,30 +135,30 @@ func getOutline(properties Properties) OutlineProperty {
return nil
}
func (style *viewStyle) setOutline(value any) bool {
func setOutlineProperty(properties Properties, value any) []PropertyName {
switch value := value.(type) {
case OutlineProperty:
style.properties[Outline] = value
properties.setRaw(Outline, value)
case ViewOutline:
style.properties[Outline] = NewOutlineProperty(Params{Style: value.Style, Width: value.Width, ColorTag: value.Color})
properties.setRaw(Outline, NewOutlineProperty(Params{Style: value.Style, Width: value.Width, ColorTag: value.Color}))
case ViewBorder:
style.properties[Outline] = NewOutlineProperty(Params{Style: value.Style, Width: value.Width, ColorTag: value.Color})
properties.setRaw(Outline, NewOutlineProperty(Params{Style: value.Style, Width: value.Width, ColorTag: value.Color}))
case DataObject:
outline := NewOutlineProperty(nil)
for _, tag := range []string{Style, Width, ColorTag} {
if text, ok := value.PropertyValue(tag); ok && text != "" {
for _, tag := range []PropertyName{Style, Width, ColorTag} {
if text, ok := value.PropertyValue(string(tag)); ok && text != "" {
outline.Set(tag, text)
}
}
style.properties[Outline] = outline
properties.setRaw(Outline, outline)
default:
notCompatibleType(Outline, value)
return false
return nil
}
return true
return []PropertyName{Outline}
}

View File

@ -3,25 +3,29 @@ package rui
import "sort"
// Params defines a type of a parameters list
type Params map[string]any
type Params map[PropertyName]any
func (params Params) Get(tag string) any {
// Get returns a value of the property with name defined by the argument. The type of return value depends
// on the property. If the property is not set then nil is returned.
func (params Params) Get(tag PropertyName) any {
return params.getRaw(tag)
}
func (params Params) getRaw(tag string) any {
func (params Params) getRaw(tag PropertyName) any {
if value, ok := params[tag]; ok {
return value
}
return nil
}
func (params Params) Set(tag string, value any) bool {
// Set 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" is returned and a description of an error is written to the log
func (params Params) Set(tag PropertyName, value any) bool {
params.setRaw(tag, value)
return true
}
func (params Params) setRaw(tag string, value any) {
func (params Params) setRaw(tag PropertyName, value any) {
if value != nil {
params[tag] = value
} else {
@ -29,21 +33,30 @@ func (params Params) setRaw(tag string, value any) {
}
}
func (params Params) Remove(tag string) {
// Remove removes the property with name defined by the argument from a map.
func (params Params) Remove(tag PropertyName) {
delete(params, tag)
}
// Clear removes all properties from a map.
func (params Params) Clear() {
for tag := range params {
delete(params, tag)
}
}
func (params Params) AllTags() []string {
tags := make([]string, 0, len(params))
// AllTags returns a sorted slice of all properties.
func (params Params) AllTags() []PropertyName {
tags := make([]PropertyName, 0, len(params))
for t := range params {
tags = append(tags, t)
}
sort.Strings(tags)
sort.Slice(tags, func(i, j int) bool {
return tags[i] < tags[j]
})
return tags
}
func (params Params) empty() bool {
return len(params) == 0
}

93
path.go
View File

@ -2,9 +2,6 @@ package rui
// Path is a path interface
type Path interface {
// Reset erases the Path
Reset()
// MoveTo begins a new sub-path at the point specified by the given (x, y) coordinates
MoveTo(x, y float64)
@ -14,123 +11,119 @@ type Path interface {
// ArcTo adds a circular arc to the current sub-path, using the given control points and radius.
// The arc is automatically connected to the path's latest point with a straight line, if necessary.
// x0, y0 - coordinates of the first control point;
// x1, y1 - coordinates of the second control point;
// radius - the arc's radius. Must be non-negative.
// - x0, y0 - coordinates of the first control point;
// - x1, y1 - coordinates of the second control point;
// - radius - the arc's radius. Must be non-negative.
ArcTo(x0, y0, x1, y1, radius float64)
// Arc adds a circular arc to the current sub-path.
// x, y - coordinates of the arc's center;
// radius - the arc's radius. Must be non-negative;
// startAngle - the angle at which the arc starts, measured clockwise from the positive
// - x, y - coordinates of the arc's center;
// - radius - the arc's radius. Must be non-negative;
// - startAngle - the angle at which the arc starts, measured clockwise from the positive
// x-axis and expressed in radians.
// endAngle - the angle at which the arc ends, measured clockwise from the positive
// - endAngle - the angle at which the arc ends, measured clockwise from the positive
// x-axis and expressed in radians.
// clockwise - if true, causes the arc to be drawn clockwise between the start and end angles,
// - clockwise - if true, causes the arc to be drawn clockwise between the start and end angles,
// otherwise - counter-clockwise
Arc(x, y, radius, startAngle, endAngle float64, clockwise bool)
// BezierCurveTo adds a cubic Bézier curve to the current sub-path. The starting point is
// the latest point in the current path.
// cp0x, cp0y - coordinates of the first control point;
// cp1x, cp1y - coordinates of the second control point;
// x, y - coordinates of the end point.
// - cp0x, cp0y - coordinates of the first control point;
// - cp1x, cp1y - coordinates of the second control point;
// - x, y - coordinates of the end point.
BezierCurveTo(cp0x, cp0y, cp1x, cp1y, x, y float64)
// QuadraticCurveTo adds a quadratic Bézier curve to the current sub-path.
// cpx, cpy - coordinates of the control point;
// x, y - coordinates of the end point.
// - cpx, cpy - coordinates of the control point;
// - x, y - coordinates of the end point.
QuadraticCurveTo(cpx, cpy, x, y float64)
// Ellipse adds an elliptical arc to the current sub-path
// x, y - coordinates of the ellipse's center;
// radiusX - the ellipse's major-axis radius. Must be non-negative;
// radiusY - the ellipse's minor-axis radius. Must be non-negative;
// rotation - the rotation of the ellipse, expressed in radians;
// startAngle - the angle at which the ellipse starts, measured clockwise
// - x, y - coordinates of the ellipse's center;
// - radiusX - the ellipse's major-axis radius. Must be non-negative;
// - radiusY - the ellipse's minor-axis radius. Must be non-negative;
// - rotation - the rotation of the ellipse, expressed in radians;
// - startAngle - the angle at which the ellipse starts, measured clockwise
// from the positive x-axis and expressed in radians;
// endAngle - the angle at which the ellipse ends, measured clockwise
// - endAngle - the angle at which the ellipse ends, measured clockwise
// from the positive x-axis and expressed in radians.
// clockwise - if true, draws the ellipse clockwise, otherwise draws counter-clockwise
// - clockwise - if true, draws the ellipse clockwise, otherwise draws counter-clockwise
Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, clockwise bool)
// Close adds a straight line from the current point to the start of the current sub-path.
// If the shape has already been closed or has only one point, this function does nothing.
Close()
create(session Session)
}
type pathElement struct {
funcName string
args []any
obj() any
}
type pathData struct {
elements []pathElement
session Session
varName any
}
// NewPath creates a new empty Path
func NewPath() Path {
func (canvas *canvasData) NewPath() Path {
path := new(pathData)
path.Reset()
path.session = canvas.session
path.varName = canvas.session.createPath("")
return path
}
func (path *pathData) Reset() {
path.elements = []pathElement{
{funcName: "beginPath", args: []any{}},
}
func (canvas *canvasData) NewPathFromSvg(data string) Path {
path := new(pathData)
path.session = canvas.session
path.varName = canvas.session.createPath(data)
return path
}
func (path *pathData) MoveTo(x, y float64) {
path.elements = append(path.elements, pathElement{funcName: "moveTo", args: []any{x, y}})
path.session.callCanvasVarFunc(path.varName, "moveTo", x, y)
}
func (path *pathData) LineTo(x, y float64) {
path.elements = append(path.elements, pathElement{funcName: "lineTo", args: []any{x, y}})
path.session.callCanvasVarFunc(path.varName, "lineTo", x, y)
}
func (path *pathData) ArcTo(x0, y0, x1, y1, radius float64) {
if radius > 0 {
path.elements = append(path.elements, pathElement{funcName: "arcTo", args: []any{x0, y0, x1, y1, radius}})
path.session.callCanvasVarFunc(path.varName, "arcTo", x0, y0, x1, y1, radius)
}
}
func (path *pathData) Arc(x, y, radius, startAngle, endAngle float64, clockwise bool) {
if radius > 0 {
if !clockwise {
path.elements = append(path.elements, pathElement{funcName: "arc", args: []any{x, y, radius, startAngle, endAngle, true}})
path.session.callCanvasVarFunc(path.varName, "arc", x, y, radius, startAngle, endAngle, true)
} else {
path.elements = append(path.elements, pathElement{funcName: "arc", args: []any{x, y, radius, startAngle, endAngle}})
path.session.callCanvasVarFunc(path.varName, "arc", x, y, radius, startAngle, endAngle)
}
}
}
func (path *pathData) BezierCurveTo(cp0x, cp0y, cp1x, cp1y, x, y float64) {
path.elements = append(path.elements, pathElement{funcName: "bezierCurveTo", args: []any{cp0x, cp0y, cp1x, cp1y, x, y}})
path.session.callCanvasVarFunc(path.varName, "bezierCurveTo", cp0x, cp0y, cp1x, cp1y, x, y)
}
func (path *pathData) QuadraticCurveTo(cpx, cpy, x, y float64) {
path.elements = append(path.elements, pathElement{funcName: "quadraticCurveTo", args: []any{cpx, cpy, x, y}})
path.session.callCanvasVarFunc(path.varName, "quadraticCurveTo", cpx, cpy, x, y)
}
func (path *pathData) Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, clockwise bool) {
if radiusX > 0 && radiusY > 0 {
if !clockwise {
path.elements = append(path.elements, pathElement{funcName: "ellipse", args: []any{x, y, radiusX, radiusY, rotation, startAngle, endAngle, true}})
path.session.callCanvasVarFunc(path.varName, "ellipse", x, y, radiusX, radiusY, rotation, startAngle, endAngle, true)
} else {
path.elements = append(path.elements, pathElement{funcName: "ellipse", args: []any{x, y, radiusX, radiusY, rotation, startAngle, endAngle}})
path.session.callCanvasVarFunc(path.varName, "ellipse", x, y, radiusX, radiusY, rotation, startAngle, endAngle)
}
}
}
func (path *pathData) Close() {
path.elements = append(path.elements, pathElement{funcName: "close", args: []any{}})
path.session.callCanvasVarFunc(path.varName, "closePath")
}
func (path *pathData) create(session Session) {
for _, element := range path.elements {
session.callCanvasFunc(element.funcName, element.args...)
}
func (path *pathData) obj() any {
return path.varName
}

View File

@ -1,54 +1,133 @@
package rui
import (
"strings"
)
// Constants for [View] specific pointer events properties
const (
// PointerDown is the constant for "pointer-down" property tag.
// The "pointer-down" event is fired when a pointer becomes active. For mouse, it is fired when
// the device transitions from no buttons depressed to at least one button depressed.
// For touch, it is fired when physical contact is made with the digitizer.
// For pen, it is fired when the stylus makes physical contact with the digitizer.
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerDown = "pointer-down"
//
// Used by View.
// Fired when a pointer becomes active. For mouse, it is fired when the device transitions from no buttons depressed to at
// least one button depressed. For touch, it is fired when physical contact is made with the digitizer. For pen, it is
// fired when the stylus makes physical contact with the digitizer.
//
// General listener format:
//
// func(view rui.View, event rui.PointerEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Pointer event.
//
// Allowed listener formats:
//
// func(event rui.PointerEvent)
// func(view rui.View)
// func()
PointerDown PropertyName = "pointer-down"
// PointerUp is the constant for "pointer-up" property tag.
// The "pointer-up" event is fired when a pointer is no longer active.
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerUp = "pointer-up"
//
// Used by View.
// Is fired when a pointer is no longer active.
//
// General listener format:
//
// func(view rui.View, event rui.PointerEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Pointer event.
//
// Allowed listener formats:
//
// func(event rui.PointerEvent)
// func(view rui.View)
// func()
PointerUp PropertyName = "pointer-up"
// PointerMove is the constant for "pointer-move" property tag.
// The "pointer-move" event is fired when a pointer changes coordinates.
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerMove = "pointer-move"
//
// Used by View.
// Is fired when a pointer changes coordinates.
//
// General listener format:
//
// func(view rui.View, event rui.PointerEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Pointer event.
//
// Allowed listener formats:
//
// func(event rui.PointerEvent)
// func(view rui.View)
// func()
PointerMove PropertyName = "pointer-move"
// PointerCancel is the constant for "pointer-cancel" property tag.
// The "pointer-cancel" event is fired if the pointer will no longer be able to generate events
// (for example the related device is deactivated).
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerCancel = "pointer-cancel"
//
// Used by View.
// Is fired if the pointer will no longer be able to generate events (for example the related device is deactivated).
//
// General listener format:
//
// func(view rui.View, event rui.PointerEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Pointer event.
//
// Allowed listener formats:
//
// func(event rui.PointerEvent)
// func(view rui.View)
// func()
PointerCancel PropertyName = "pointer-cancel"
// PointerOut is the constant for "pointer-out" property tag.
// The "pointer-out" event is fired for several reasons including: pointing device is moved out
// of the hit test boundaries of an element; firing the pointerup event for a device
// that does not support hover (see "pointer-up"); after firing the pointercancel event (see "pointer-cancel");
// when a pen stylus leaves the hover range detectable by the digitizer.
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerOut = "pointer-out"
//
// Used by View.
// Is fired for several reasons including: pointing device is moved out of the hit test boundaries of an element; firing
// the "pointer-up" event for a device that does not support hover (see "pointer-up"); after firing the "pointer-cancel"
// event (see "pointer-cancel"); when a pen stylus leaves the hover range detectable by the digitizer.
//
// General listener format:
//
// func(view rui.View, event rui.PointerEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Pointer event.
//
// Allowed listener formats:
//
// func(event rui.PointerEvent)
// func(view rui.View)
// func()
PointerOut PropertyName = "pointer-out"
// PointerOver is the constant for "pointer-over" property tag.
// The "pointer-over" event is fired when a pointing device is moved into an view's hit test boundaries.
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerOver = "pointer-over"
//
// Used by View.
// Is fired when a pointing device is moved into an view's hit test boundaries.
//
// General listener format:
//
// func(view rui.View, event rui.PointerEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Pointer event.
//
// Allowed listener formats:
//
// func(event rui.PointerEvent)
// func(view rui.View)
// func()
PointerOver PropertyName = "pointer-over"
)
// PointerEvent represent a stylus events. Also inherit [MouseEvent] attributes
type PointerEvent struct {
MouseEvent
@ -87,57 +166,6 @@ type PointerEvent struct {
IsPrimary bool
}
var pointerEvents = map[string]struct{ jsEvent, jsFunc string }{
PointerDown: {jsEvent: "onpointerdown", jsFunc: "pointerDownEvent"},
PointerUp: {jsEvent: "onpointerup", jsFunc: "pointerUpEvent"},
PointerMove: {jsEvent: "onpointermove", jsFunc: "pointerMoveEvent"},
PointerCancel: {jsEvent: "onpointercancel", jsFunc: "pointerCancelEvent"},
PointerOut: {jsEvent: "onpointerout", jsFunc: "pointerOutEvent"},
PointerOver: {jsEvent: "onpointerover", jsFunc: "pointerOverEvent"},
}
func (view *viewData) setPointerListener(tag string, value any) bool {
listeners, ok := valueToEventListeners[View, PointerEvent](value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removePointerListener(tag)
} else if js, ok := pointerEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
view.session.updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)")
}
} else {
return false
}
return true
}
func (view *viewData) removePointerListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := pointerEvents[tag]; ok {
view.session.removeProperty(view.htmlID(), js.jsEvent)
}
}
}
func pointerEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range pointerEvents {
if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View, PointerEvent)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
}
}
}
}
func (event *PointerEvent) init(data DataObject) {
event.MouseEvent.init(data)
@ -154,8 +182,8 @@ func (event *PointerEvent) init(data DataObject) {
event.IsPrimary = dataBoolProperty(data, "isPrimary")
}
func handlePointerEvents(view View, tag string, data DataObject) {
listeners := getEventListeners[View, PointerEvent](view, nil, tag)
func handlePointerEvents(view View, tag PropertyName, data DataObject) {
listeners := getOneArgEventListeners[View, PointerEvent](view, nil, tag)
if len(listeners) == 0 {
return
}
@ -171,35 +199,35 @@ func handlePointerEvents(view View, tag string, data DataObject) {
// GetPointerDownListeners returns the "pointer-down" 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.
func GetPointerDownListeners(view View, subviewID ...string) []func(View, PointerEvent) {
return getEventListeners[View, PointerEvent](view, subviewID, PointerDown)
return getOneArgEventListeners[View, PointerEvent](view, subviewID, PointerDown)
}
// GetPointerUpListeners returns the "pointer-up" 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.
func GetPointerUpListeners(view View, subviewID ...string) []func(View, PointerEvent) {
return getEventListeners[View, PointerEvent](view, subviewID, PointerUp)
return getOneArgEventListeners[View, PointerEvent](view, subviewID, PointerUp)
}
// GetPointerMoveListeners returns the "pointer-move" 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.
func GetPointerMoveListeners(view View, subviewID ...string) []func(View, PointerEvent) {
return getEventListeners[View, PointerEvent](view, subviewID, PointerMove)
return getOneArgEventListeners[View, PointerEvent](view, subviewID, PointerMove)
}
// GetPointerCancelListeners returns the "pointer-cancel" 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.
func GetPointerCancelListeners(view View, subviewID ...string) []func(View, PointerEvent) {
return getEventListeners[View, PointerEvent](view, subviewID, PointerCancel)
return getOneArgEventListeners[View, PointerEvent](view, subviewID, PointerCancel)
}
// GetPointerOverListeners returns the "pointer-over" 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.
func GetPointerOverListeners(view View, subviewID ...string) []func(View, PointerEvent) {
return getEventListeners[View, PointerEvent](view, subviewID, PointerOver)
return getOneArgEventListeners[View, PointerEvent](view, subviewID, PointerOver)
}
// GetPointerOutListeners returns the "pointer-out" 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.
func GetPointerOutListeners(view View, subviewID ...string) []func(View, PointerEvent) {
return getEventListeners[View, PointerEvent](view, subviewID, PointerOut)
return getOneArgEventListeners[View, PointerEvent](view, subviewID, PointerOut)
}

536
popup.go
View File

@ -1,64 +1,216 @@
package rui
import (
"fmt"
"strings"
)
// Constants for [Popup] specific properties and events
const (
// Title is the constant for the "title" property tag.
// The "title" property is defined the Popup/Tabs title
Title = "title"
// Title is the constant for "title" property tag.
//
// Used by Popup, TabsLayout.
//
// Usage in Popup:
// Define the title.
//
// Supported types: string.
//
// Usage in TabsLayout:
// Set the title of the tab. The property is set for the child view of TabsLayout.
//
// Supported types: string.
Title PropertyName = "title"
// TitleStyle is the constant for the "title-style" property tag.
// The "title-style" string property is used to set the title style of the Popup.
TitleStyle = "title-style"
// TitleStyle is the constant for "title-style" property tag.
//
// Used by Popup.
// Set popup title style. Default title style is "ruiPopupTitle".
//
// Supported types: string.
TitleStyle PropertyName = "title-style"
// CloseButton is the constant for the "close-button" property tag.
// The "close-button" bool property allow to add the close button to the Popup.
// Setting this property to "true" adds a window close button to the title bar (the default value is "false").
CloseButton = "close-button"
// CloseButton is the constant for "close-button" property tag.
//
// Used by Popup.
// Controls whether a close button can be added to the popup. Default value is false.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", "1" - Close button will be added to a title bar of a window.
// - false, 0, "false", "no", "off", "0" - Popup without a close button.
CloseButton PropertyName = "close-button"
// OutsideClose is the constant for the "outside-close" property tag.
// The "outside-close" is a bool property. If it is set to "true",
// then clicking outside the popup window automatically calls the Dismiss() method.
OutsideClose = "outside-close"
// OutsideClose is the constant for "outside-close" property tag.
//
// Used by Popup.
// Controls whether popup can be closed by clicking outside of the window. Default value is false.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", "1" - Clicking outside the popup window will automatically call the Dismiss() method.
// - false, 0, "false", "no", "off", "0" - Clicking outside the popup window has no effect.
OutsideClose PropertyName = "outside-close"
// Buttons is the constant for the "buttons" property tag.
// Using the "buttons" property you can add buttons that will be placed at the bottom of the Popup.
// The "buttons" property can be assigned the following data types: PopupButton and []PopupButton
Buttons = "buttons"
// Buttons is the constant for "buttons" property tag.
//
// Used by Popup.
// Buttons that will be placed at the bottom of the popup.
//
// Supported types: PopupButton, []PopupButton.
//
// Internal type is []PopupButton, other types converted to it during assignment.
// See PopupButton description for more details.
Buttons PropertyName = "buttons"
// ButtonsAlign is the constant for the "buttons-align" property tag.
// The "buttons-align" int property is used for set the horizontal alignment of Popup buttons.
// Valid values: LeftAlign (0), RightAlign (1), CenterAlign (2), and StretchAlign (3)
ButtonsAlign = "buttons-align"
// ButtonsAlign is the constant for "buttons-align" property tag.
//
// Used by Popup.
// Set the horizontal alignment of popup buttons.
//
// Supported types: int, string.
//
// Values:
// - 0 (LeftAlign) or "left" - Left alignment.
// - 1 (RightAlign) or "right" - Right alignment.
// - 2 (CenterAlign) or "center" - Center alignment.
// - 3 (StretchAlign) or "stretch" - Width alignment.
ButtonsAlign PropertyName = "buttons-align"
// DismissEvent is the constant for the "dismiss-event" property tag.
// The "dismiss-event" event is used to track the closing of the Popup.
// It occurs after the Popup disappears from the screen.
// The main listener for this event has the following format: func(Popup)
DismissEvent = "dismiss-event"
// DismissEvent is the constant for "dismiss-event" property tag.
//
// Used by Popup.
// Used to track the closing state of the Popup. It occurs after the Popup disappears from the screen.
//
// General listener format:
//
// func(popup rui.Popup)
//
// where:
// popup - Interface of a popup which generated this event.
//
// Allowed listener formats:
//
// func()
DismissEvent PropertyName = "dismiss-event"
// Arrow is the constant for the "arrow" property tag.
// Using the "popup-arrow" int property you can add ...
Arrow = "arrow"
// Arrow is the constant for "arrow" property tag.
//
// Used by Popup.
// Add an arrow to popup. Default value is "none".
//
// Supported types: int, string.
//
// Values:
// - 0 (NoneArrow) or "none" - No arrow.
// - 1 (TopArrow) or "top" - Arrow at the top side of the pop-up window.
// - 2 (RightArrow) or "right" - Arrow on the right side of the pop-up window.
// - 3 (BottomArrow) or "bottom" - Arrow at the bottom of the pop-up window.
// - 4 (LeftArrow) or "left" - Arrow on the left side of the pop-up window.
Arrow PropertyName = "arrow"
// ArrowAlign is the constant for the "arrow-align" property tag.
// The "arrow-align" int property is used for set the horizontal alignment of the Popup arrow.
// Valid values: LeftAlign (0), RightAlign (1), TopAlign (0), BottomAlign (1), CenterAlign (2)
ArrowAlign = "arrow-align"
// ArrowAlign is the constant for "arrow-align" property tag.
//
// Used by Popup.
// Set the horizontal alignment of the popup arrow. Default value is "center".
//
// Supported types: int, string.
//
// Values:
// - 0 (TopAlign/LeftAlign) or "top" - Top/left alignment.
// - 1 (BottomAlign/RightAlign) or "bottom" - Bottom/right alignment.
// - 2 (CenterAlign) or "center" - Center alignment.
ArrowAlign PropertyName = "arrow-align"
// ArrowSize is the constant for the "arrow-size" property tag.
// The "arrow-size" SizeUnit property is used for set the size (length) of the Popup arrow.
ArrowSize = "arrow-size"
// ArrowSize is the constant for "arrow-size" property tag.
//
// Used by Popup.
// Set the size(length) of the popup arrow. Default value is 16px defined by @ruiArrowSize constant.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
ArrowSize PropertyName = "arrow-size"
// ArrowWidth is the constant for the "arrow-width" property tag.
// The "arrow-width" SizeUnit property is used for set the width of the Popup arrow.
ArrowWidth = "arrow-width"
// ArrowWidth is the constant for "arrow-width" property tag.
//
// Used by Popup.
// Set the width of the popup arrow. Default value is 16px defined by @ruiArrowWidth constant.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
ArrowWidth PropertyName = "arrow-width"
// ArrowOffset is the constant for the "arrow-offset" property tag.
// The "arrow-offset" SizeUnit property is used for set the offset of the Popup arrow.
ArrowOffset = "arrow-offset"
// ShowTransform is the constant for "show-transform" property tag.
//
// Used by Popup.
// Specify start translation, scale and rotation over x, y and z axes as well as a distortion
// for an animated Popup showing/hidding.
//
// Supported types: TransformProperty, string.
//
// See TransformProperty description for more details.
//
// Conversion rules:
// - TransformProperty - stored as is, no conversion performed.
// - string - string representation of Transform interface. Example:
//
// "_{ translate-x = 10px, scale-y = 1.1}"
ShowTransform PropertyName = "show-transform"
// ShowDuration is the constant for "show-duration" property tag.
//
// Used by Popup.
// Sets the length of time in seconds that a Popup show/hide animation takes to complete.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
ShowDuration PropertyName = "show-duration"
// ShowTiming is the constant for "show-timing" property tag.
//
// Used by Popup.
// Set how a Popup show/hide animation progresses through the duration of each cycle.
//
// Supported types: string.
//
// Values:
// - "ease" (EaseTiming) - Speed increases towards the middle and slows down at the end.
// - "ease-in" (EaseInTiming) - Speed is slow at first, but increases in the end.
// - "ease-out" (EaseOutTiming) - Speed is fast at first, but decreases in the end.
// - "ease-in-out" (EaseInOutTiming) - Speed is slow at first, but quickly increases and at the end it decreases again.
// - "linear" (LinearTiming) - Constant speed.
// - "step(n)" (StepTiming(n int) function) - Timing function along stepCount stops along the transition, displaying each stop for equal lengths of time.
// - "cubic-bezier(x1, y1, x2, y2)" (CubicBezierTiming(x1, y1, x2, y2 float64) function) - Cubic-Bezier curve timing function. x1 and x2 must be in the range [0, 1].
ShowTiming PropertyName = "show-timing"
// ShowOpacity is the constant for "show-opacity" property tag.
//
// Used by Popup.
// In [1..0] range sets the start opacity of Popup show animation (the finish animation opacity is 1).
// Opacity is the degree to which content behind the view is hidden, and is the opposite of transparency.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
ShowOpacity PropertyName = "show-opacity"
// ArrowOffset is the constant for "arrow-offset" property tag.
//
// Used by Popup.
// Set the offset of the popup arrow.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
ArrowOffset PropertyName = "arrow-offset"
// NoneArrow is value of the popup "arrow" property: no arrow
NoneArrow = 0
@ -78,42 +230,68 @@ const (
// LeftArrow is value of the popup "arrow" property:
// Arrow on the left side of the pop-up window
LeftArrow = 4
)
// Constants which are used as a values of [PopupButtonType] variables
const (
// 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
)
// PopupButtonType represent popup button type
type PopupButtonType int
// PopupButton describes a button that will be placed at the bottom of the window.
type PopupButton struct {
Title string
Type PopupButtonType
// Title of the button
Title string
// Type of the button
Type PopupButtonType
// OnClick is the handler function that gets called when the button is pressed
OnClick func(Popup)
}
// Popup interface
// Popup represents a Popup view
type Popup interface {
// View returns a content view of the popup
View() View
// Session returns current client session
Session() Session
// Show displays a popup
Show()
// Dismiss closes a popup
Dismiss()
onDismiss()
html(buffer *strings.Builder)
viewByHTMLID(id string) View
keyEvent(event KeyEvent) bool
showAnimation()
dissmissAnimation(listener func(PropertyName)) bool
}
type popupData struct {
layerView View
view View
layerView GridLayout
popupView GridLayout
contentView View
buttons []PopupButton
cancelable bool
dismissListener []func(Popup)
showTransform TransformProperty
showOpacity float64
showDuration float64
showTiming string
}
type popupManager struct {
@ -191,7 +369,7 @@ func (arrow *popupArrow) createView(popupView View) View {
params := Params{BackgroundColor: GetBackgroundColor(popupView)}
if shadow := GetViewShadows(popupView); shadow != nil {
if shadow := GetShadowProperty(popupView); shadow != nil {
params[Shadow] = shadow
}
@ -203,28 +381,28 @@ func (arrow *popupArrow) createView(popupView View) View {
case TopArrow:
params[Row] = 0
params[Column] = 1
params[Clip] = PolygonClip([]any{"0%", "100%", "50%", "0%", "100%", "100%"})
params[Clip] = NewPolygonClip([]any{"0%", "100%", "50%", "0%", "100%", "100%"})
params[Width] = arrow.width
params[Height] = arrow.size
case RightArrow:
params[Row] = 1
params[Column] = 0
params[Clip] = PolygonClip([]any{"0%", "0%", "100%", "50%", "0%", "100%"})
params[Clip] = NewPolygonClip([]any{"0%", "0%", "100%", "50%", "0%", "100%"})
params[Width] = arrow.size
params[Height] = arrow.width
case BottomArrow:
params[Row] = 0
params[Column] = 1
params[Clip] = PolygonClip([]any{"0%", "0%", "50%", "100%", "100%", "0%"})
params[Clip] = NewPolygonClip([]any{"0%", "0%", "50%", "100%", "100%", "0%"})
params[Width] = arrow.width
params[Height] = arrow.size
case LeftArrow:
params[Row] = 1
params[Column] = 0
params[Clip] = PolygonClip([]any{"100%", "0%", "0%", "50%", "100%", "100%"})
params[Clip] = NewPolygonClip([]any{"100%", "0%", "0%", "50%", "100%", "100%"})
params[Width] = arrow.size
params[Height] = arrow.width
}
@ -284,39 +462,15 @@ func (arrow *popupArrow) createView(popupView View) View {
return NewGridLayout(session, params)
}
func (popup *popupData) init(view View, popupParams Params) {
popup.view = view
popup.cancelable = false
session := view.Session()
func (popup *popupData) layerCellWidth(arrowLocation int, popupParams Params, session Session) []SizeUnit {
columnCount := 3
rowCount := 3
popupRow := 1
popupColumn := 1
arrow := popupArrow{
row: 1,
column: 1,
align: CenterAlign,
}
switch arrow.location, _ = enumProperty(popupParams, Arrow, session, NoneArrow); arrow.location {
case TopArrow:
rowCount = 4
popupRow = 2
case BottomArrow:
rowCount = 4
arrow.row = 2
case LeftArrow:
var columnCount int
switch arrowLocation {
case LeftArrow, RightArrow:
columnCount = 4
popupColumn = 2
case RightArrow:
columnCount = 4
arrow.column = 2
default:
columnCount = 3
}
cellWidth := make([]SizeUnit, columnCount)
@ -331,6 +485,19 @@ func (popup *popupData) init(view View, popupParams Params) {
cellWidth[0] = Fr(1)
cellWidth[columnCount-1] = Fr(1)
}
return cellWidth
}
func (popup *popupData) layerCellHeight(arrowLocation int, popupParams Params, session Session) []SizeUnit {
var rowCount int
switch arrowLocation {
case TopArrow, BottomArrow:
rowCount = 4
default:
rowCount = 3
}
cellHeight := make([]SizeUnit, rowCount)
switch vAlign, _ := enumProperty(popupParams, VerticalAlign, session, CenterAlign); vAlign {
@ -345,16 +512,47 @@ func (popup *popupData) init(view View, popupParams Params) {
cellHeight[rowCount-1] = Fr(1)
}
return cellHeight
}
func (popup *popupData) init(view View, popupParams Params) {
popup.contentView = view
popup.cancelable = false
session := view.Session()
popupRow := 1
popupColumn := 1
arrow := popupArrow{
row: 1,
column: 1,
align: CenterAlign,
}
switch arrow.location, _ = enumProperty(popupParams, Arrow, session, NoneArrow); arrow.location {
case TopArrow:
popupRow = 2
case BottomArrow:
arrow.row = 2
case LeftArrow:
popupColumn = 2
case RightArrow:
arrow.column = 2
}
layerParams := Params{
Style: "ruiPopupLayer",
MaxWidth: Percent(100),
MaxHeight: Percent(100),
CellWidth: cellWidth,
CellHeight: cellHeight,
CellWidth: popup.layerCellWidth(arrow.location, popupParams, session),
CellHeight: popup.layerCellHeight(arrow.location, popupParams, session),
}
params := Params{
Style: "ruiPopup",
ID: "ruiPopup",
Row: popupRow,
Column: popupColumn,
MaxWidth: Percent(100),
@ -362,7 +560,7 @@ func (popup *popupData) init(view View, popupParams Params) {
CellVerticalAlign: StretchAlign,
CellHorizontalAlign: StretchAlign,
ClickEvent: func(View) {},
Shadow: NewShadowWithParams(Params{
Shadow: NewShadowProperty(Params{
SpreadRadius: Px(4),
Blur: Px(16),
ColorTag: "@ruiPopupShadow",
@ -370,10 +568,14 @@ func (popup *popupData) init(view View, popupParams Params) {
}
var closeButton View = nil
outsideClose := false
buttons := []PopupButton{}
titleStyle := "ruiPopupTitle"
var title View = nil
outsideClose := false
popup.buttons = []PopupButton{}
titleStyle := "ruiPopupTitle"
popup.showOpacity = 1.0
popup.showDuration = 1.0
popup.showTiming = "easy"
for tag, value := range popupParams {
if value != nil {
@ -419,10 +621,10 @@ func (popup *popupData) init(view View, popupParams Params) {
case Buttons:
switch value := value.(type) {
case PopupButton:
buttons = []PopupButton{value}
popup.buttons = []PopupButton{value}
case []PopupButton:
buttons = value
popup.buttons = value
}
case Title:
@ -447,7 +649,7 @@ func (popup *popupData) init(view View, popupParams Params) {
}
case DismissEvent:
if listeners, ok := valueToNoParamListeners[Popup](value); ok {
if listeners, ok := valueToNoArgEventListeners[Popup](value); ok {
if listeners != nil {
popup.dismissListener = listeners
}
@ -474,13 +676,36 @@ func (popup *popupData) init(view View, popupParams Params) {
case ArrowOffset:
arrow.off, _ = sizeProperty(popupParams, ArrowOffset, session)
case ShowOpacity:
if opacity, _ := floatProperty(popupParams, ShowOpacity, session, 1); opacity >= 0 && opacity < 1 {
popup.showOpacity = opacity
}
case ShowTransform:
if transform := valueToTransformProperty(value); transform != nil && !transform.empty() {
popup.showTransform = transform
}
case ShowDuration:
if duration, _ := floatProperty(popupParams, ShowDuration, session, 1); duration > 0 {
popup.showDuration = duration
}
case ShowTiming:
if text, ok := value.(string); ok {
text, _ = session.resolveConstants(text)
if isTimingFunctionValid(text) {
popup.showTiming = text
}
}
default:
params[tag] = value
}
}
}
popupView := NewGridLayout(view.Session(), params)
popup.popupView = NewGridLayout(view.Session(), params)
var popupCellHeight []SizeUnit
viewRow := 0
@ -492,7 +717,7 @@ func (popup *popupData) init(view View, popupParams Params) {
if closeButton != nil {
titleContent = append(titleContent, closeButton)
}
popupView.Append(NewGridLayout(session, Params{
popup.popupView.Append(NewGridLayout(session, Params{
Row: 0,
Style: titleStyle,
CellWidth: []any{Fr(1), AutoSize()},
@ -508,10 +733,9 @@ func (popup *popupData) init(view View, popupParams Params) {
}
view.Set(Row, viewRow)
popupView.Append(view)
popup.popupView.Append(view)
popup.buttons = buttons
if buttonCount := len(buttons); buttonCount > 0 {
if buttonCount := len(popup.buttons); buttonCount > 0 {
buttonsAlign, _ := enumProperty(params, ButtonsAlign, session, RightAlign)
popupCellHeight = append(popupCellHeight, AutoSize())
gap, _ := sizeConstant(session, "ruiPopupButtonGap")
@ -528,7 +752,7 @@ func (popup *popupData) init(view View, popupParams Params) {
buttonsPanel.Set(Margin, gap)
}
for i, button := range buttons {
for i, button := range popup.buttons {
title := button.Title
if title == "" && button.Type == CancelButton {
title = "Cancel"
@ -555,33 +779,82 @@ func (popup *popupData) init(view View, popupParams Params) {
buttonsPanel.Append(buttonView)
}
popupView.Append(NewGridLayout(session, Params{
popup.popupView.Append(NewGridLayout(session, Params{
Row: viewRow + 1,
CellHorizontalAlign: buttonsAlign,
Content: buttonsPanel,
}))
}
popupView.Set(CellHeight, popupCellHeight)
popup.popupView.Set(CellHeight, popupCellHeight)
if arrow.location != NoneArrow {
layerParams[Content] = []View{popupView, arrow.createView(popupView)}
layerParams[Content] = []View{popup.popupView, arrow.createView(popup.popupView)}
} else {
layerParams[Content] = []View{popupView}
layerParams[Content] = []View{popup.popupView}
}
popup.layerView = NewGridLayout(session, layerParams)
if popup.showOpacity != 1 || popup.showTransform != nil {
animation := NewAnimationProperty(Params{
Duration: popup.showDuration,
TimingFunction: popup.showTiming,
})
if popup.showOpacity != 1 {
popup.popupView.Set(Opacity, popup.showOpacity)
popup.popupView.SetTransition(Opacity, animation)
}
if popup.showTransform != nil {
popup.popupView.Set(Transform, popup.showTransform)
popup.popupView.SetTransition(Transform, animation)
}
} else {
session.updateCSSProperty("ruiPopupLayer", "transition", "")
}
if outsideClose {
popup.layerView.Set(ClickEvent, popup.cancel)
}
}
func (popup popupData) View() View {
return popup.view
func (popup *popupData) showAnimation() {
if popup.showOpacity != 1 || popup.showTransform != nil {
htmlID := popup.popupView.htmlID()
session := popup.Session()
if popup.showOpacity != 1 {
session.updateCSSProperty(htmlID, string(Opacity), "1")
}
if popup.showTransform != nil {
session.updateCSSProperty(htmlID, string(Transform), "")
}
}
}
func (popup *popupData) dissmissAnimation(listener func(PropertyName)) bool {
if popup.showOpacity != 1 || popup.showTransform != nil {
session := popup.Session()
popup.popupView.Set(TransitionEndEvent, listener)
popup.popupView.Set(TransitionCancelEvent, listener)
htmlID := popup.popupView.htmlID()
if popup.showOpacity != 1 {
session.updateCSSProperty(htmlID, string(Opacity), fmt.Sprintf("%.2f", popup.showOpacity))
}
if popup.showTransform != nil {
session.updateCSSProperty(htmlID, string(Transform), popup.showTransform.transformCSS(session))
}
return true
}
return false
}
func (popup *popupData) View() View {
return popup.contentView
}
func (popup *popupData) Session() Session {
return popup.view.Session()
return popup.contentView.Session()
}
func (popup *popupData) cancel() {
@ -596,9 +869,6 @@ func (popup *popupData) cancel() {
func (popup *popupData) Dismiss() {
popup.Session().popupManager().dismissPopup(popup)
for _, listener := range popup.dismissListener {
listener(popup)
}
}
func (popup *popupData) Show() {
@ -606,8 +876,7 @@ func (popup *popupData) Show() {
}
func (popup *popupData) html(buffer *strings.Builder) {
viewHTML(popup.layerView, buffer)
viewHTML(popup.layerView, buffer, "")
}
func (popup *popupData) viewByHTMLID(id string) View {
@ -615,6 +884,8 @@ func (popup *popupData) viewByHTMLID(id string) View {
}
func (popup *popupData) onDismiss() {
popup.Session().callFunc("removeView", popup.layerView.htmlID())
for _, listener := range popup.dismissListener {
listener(popup)
}
@ -683,7 +954,7 @@ func (manager *popupManager) showPopup(popup Popup) {
}
session := popup.Session()
if manager.popups == nil || len(manager.popups) == 0 {
if len(manager.popups) == 0 {
manager.popups = []Popup{popup}
} else {
manager.popups = append(manager.popups, popup)
@ -695,6 +966,7 @@ func (manager *popupManager) showPopup(popup Popup) {
session.updateCSSProperty("ruiTooltipLayer", "opacity", "0")
session.updateCSSProperty("ruiPopupLayer", "visibility", "visible")
session.updateCSSProperty("ruiRoot", "pointer-events", "none")
popup.showAnimation()
}
func (manager *popupManager) dismissPopup(popup Popup) {
@ -708,31 +980,37 @@ func (manager *popupManager) dismissPopup(popup Popup) {
return
}
session := popup.Session()
if manager.popups[count-1] == popup {
if count == 1 {
manager.popups = []Popup{}
session.updateCSSProperty("ruiRoot", "pointer-events", "auto")
session.updateCSSProperty("ruiPopupLayer", "visibility", "hidden")
session.updateInnerHTML("ruiPopupLayer", "")
} else {
manager.popups = manager.popups[:count-1]
manager.updatePopupLayerInnerHTML(session)
index := -1
for n, p := range manager.popups {
if p == popup {
index = n
break
}
popup.onDismiss()
}
if index < 0 {
return
}
for n, p := range manager.popups {
if p == popup {
if n == 0 {
manager.popups = manager.popups[1:]
session := popup.Session()
listener := func(PropertyName) {
if index == count-1 {
if count == 1 {
manager.popups = []Popup{}
session.updateCSSProperty("ruiRoot", "pointer-events", "auto")
session.updateCSSProperty("ruiPopupLayer", "visibility", "hidden")
} else {
manager.popups = append(manager.popups[:n], manager.popups[n+1:]...)
manager.popups = manager.popups[:count-1]
}
manager.updatePopupLayerInnerHTML(session)
popup.onDismiss()
return
} else if index == 0 {
manager.popups = manager.popups[1:]
} else {
manager.popups = append(manager.popups[:index], manager.popups[index+1:]...)
}
popup.onDismiss()
}
if !popup.dissmissAnimation(listener) {
listener("")
}
}

View File

@ -17,7 +17,9 @@ func ShowMessage(title, text string, session Session) {
}
// ShowQuestion displays a message with the given title and text and two buttons "Yes" and "No".
//
// When the "Yes" button is clicked, the message is closed and the onYes function is called (if it is not nil).
//
// When the "No" button is pressed, the message is closed and the onNo function is called (if it is not nil).
func ShowQuestion(title, text string, session Session, onYes func(), onNo func()) {
textView := NewTextView(session, Params{
@ -57,6 +59,7 @@ func ShowQuestion(title, text string, session Session, onYes func(), onNo func()
}
// ShowCancellableQuestion displays a message with the given title and text and three buttons "Yes", "No" and "Cancel".
//
// When the "Yes", "No" or "Cancel" button is pressed, the message is closed and the onYes, onNo or onCancel function
// (if it is not nil) is called, respectively.
func ShowCancellableQuestion(title, text string, session Session, onYes func(), onNo func(), onCancel func()) {
@ -152,9 +155,12 @@ func (popup *popupMenuData) IsListItemEnabled(index int) bool {
return true
}
// PopupMenuResult is the constant for the "popup-menu-result" property tag.
// The "popup-menu-result" property sets the function (format: func(int)) to be called when
// a menu item of popup menu is selected.
// PopupMenuResult is the constant for "popup-menu-result" property tag.
//
// Used by `Popup`.
// Set the function to be called when the menu item of popup menu is selected.
//
// Supported types: `func(index int)`.
const PopupMenuResult = "popup-menu-result"
// ShowMenu displays the menu. Menu items are set using the Items property.

View File

@ -5,12 +5,30 @@ import (
"strings"
)
// Constants for [ProgressBar] specific properties and events
const (
ProgressBarMax = "progress-max"
ProgressBarValue = "progress-value"
// ProgressBarMax is the constant for "progress-max" property tag.
//
// Used by ProgressBar.
// Maximum value, default is 1.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
ProgressBarMax PropertyName = "progress-max"
// ProgressBarValue is the constant for "progress-value" property tag.
//
// Used by ProgressBar.
// Current value, default is 0.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
ProgressBarValue PropertyName = "progress-value"
)
// ProgressBar - ProgressBar view
// ProgressBar represents a ProgressBar view
type ProgressBar interface {
View
}
@ -28,20 +46,18 @@ func NewProgressBar(session Session, params Params) ProgressBar {
}
func newProgressBar(session Session) View {
return NewProgressBar(session, nil)
return new(progressBarData)
}
func (progress *progressBarData) init(session Session) {
progress.viewData.init(session)
progress.tag = "ProgressBar"
progress.normalize = normalizeProgressBarTag
progress.changed = progress.propertyChanged
}
func (progress *progressBarData) String() string {
return getViewString(progress, nil)
}
func (progress *progressBarData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeProgressBarTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Max, "progress-bar-max", "progressbar-max":
return ProgressBarMax
@ -52,45 +68,22 @@ func (progress *progressBarData) normalizeTag(tag string) string {
return tag
}
func (progress *progressBarData) Remove(tag string) {
progress.remove(progress.normalizeTag(tag))
}
func (progress *progressBarData) propertyChanged(tag PropertyName) {
func (progress *progressBarData) remove(tag string) {
progress.viewData.remove(tag)
progress.propertyChanged(tag)
}
switch tag {
case ProgressBarMax:
progress.Session().updateProperty(progress.htmlID(), "max",
strconv.FormatFloat(GetProgressBarMax(progress), 'f', -1, 32))
func (progress *progressBarData) propertyChanged(tag string) {
if progress.created {
switch tag {
case ProgressBarMax:
progress.session.updateProperty(progress.htmlID(), Max,
strconv.FormatFloat(GetProgressBarMax(progress), 'f', -1, 32))
case ProgressBarValue:
progress.Session().updateProperty(progress.htmlID(), "value",
strconv.FormatFloat(GetProgressBarValue(progress), 'f', -1, 32))
case ProgressBarValue:
progress.session.updateProperty(progress.htmlID(), Value,
strconv.FormatFloat(GetProgressBarValue(progress), 'f', -1, 32))
}
default:
progress.viewData.propertyChanged(tag)
}
}
func (progress *progressBarData) Set(tag string, value any) bool {
return progress.set(progress.normalizeTag(tag), value)
}
func (progress *progressBarData) set(tag string, value any) bool {
if progress.viewData.set(tag, value) {
progress.propertyChanged(tag)
return true
}
return false
}
func (progress *progressBarData) Get(tag string) any {
return progress.get(progress.normalizeTag(tag))
}
func (progress *progressBarData) htmlTag() string {
return "progress"
}

View File

@ -9,71 +9,96 @@ import (
type Properties interface {
// Get returns a value of the property with name defined by the argument.
// The type of return value depends on the property. If the property is not set then nil is returned.
Get(tag string) any
getRaw(tag string) any
Get(tag PropertyName) any
getRaw(tag PropertyName) any
// Set 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
// a description of the error is written to the log
Set(tag string, value any) bool
setRaw(tag string, value any)
Set(tag PropertyName, value any) bool
setRaw(tag PropertyName, value any)
// Remove removes the property with name defined by the argument
Remove(tag string)
Remove(tag PropertyName)
// Clear removes all properties
Clear()
// AllTags returns an array of the set properties
AllTags() []string
AllTags() []PropertyName
empty() bool
}
type propertyList struct {
properties map[string]any
properties map[PropertyName]any
normalize func(PropertyName) PropertyName
//getFunc func(PropertyName) any
//set func(Properties, PropertyName, any) []PropertyName
//remove func(Properties, PropertyName) []PropertyName
}
type dataProperty struct {
propertyList
supportedProperties []PropertyName
get func(Properties, PropertyName) any
set func(Properties, PropertyName, any) []PropertyName
remove func(Properties, PropertyName) []PropertyName
}
func defaultNormalize(tag PropertyName) PropertyName {
return PropertyName(strings.ToLower(strings.Trim(string(tag), " \t")))
}
func (properties *propertyList) init() {
properties.properties = map[string]any{}
properties.properties = map[PropertyName]any{}
properties.normalize = defaultNormalize
//properties.getFunc = properties.getRaw
//properties.set = propertiesSet
//properties.remove = propertiesRemove
}
func (properties *propertyList) Get(tag string) any {
return properties.getRaw(strings.ToLower(tag))
func (properties *propertyList) empty() bool {
return len(properties.properties) == 0
}
func (properties *propertyList) getRaw(tag string) any {
func (properties *propertyList) getRaw(tag PropertyName) any {
if value, ok := properties.properties[tag]; ok {
return value
}
return nil
}
func (properties *propertyList) setRaw(tag string, value any) {
properties.properties[tag] = value
}
func (properties *propertyList) Remove(tag string) {
delete(properties.properties, strings.ToLower(tag))
}
func (properties *propertyList) remove(tag string) {
delete(properties.properties, tag)
}
func (properties *propertyList) Clear() {
properties.properties = map[string]any{}
}
func (properties *propertyList) AllTags() []string {
tags := make([]string, 0, len(properties.properties))
for t := range properties.properties {
tags = append(tags, t)
func (properties *propertyList) setRaw(tag PropertyName, value any) {
if value == nil {
delete(properties.properties, tag)
} else {
properties.properties[tag] = value
}
sort.Strings(tags)
}
/*
func (properties *propertyList) Remove(tag PropertyName) {
properties.remove(properties, properties.normalize(tag))
}
*/
func (properties *propertyList) Clear() {
properties.properties = map[PropertyName]any{}
}
func (properties *propertyList) AllTags() []PropertyName {
tags := make([]PropertyName, 0, len(properties.properties))
for tag := range properties.properties {
tags = append(tags, tag)
}
sort.Slice(tags, func(i, j int) bool {
return tags[i] < tags[j]
})
return tags
}
func (properties *propertyList) writeToBuffer(buffer *strings.Builder,
indent string, objectTag string, tags []string) {
indent string, objectTag string, tags []PropertyName) {
buffer.WriteString(objectTag)
buffer.WriteString(" {\n")
@ -83,7 +108,7 @@ func (properties *propertyList) writeToBuffer(buffer *strings.Builder,
for _, tag := range tags {
if value, ok := properties.properties[tag]; ok {
buffer.WriteString(indent2)
buffer.WriteString(tag)
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
writePropertyValue(buffer, tag, value, indent2)
buffer.WriteString(",\n")
@ -100,14 +125,41 @@ func parseProperties(properties Properties, object DataObject) {
if node := object.Property(i); node != nil {
switch node.Type() {
case TextNode:
properties.Set(node.Tag(), node.Text())
properties.Set(PropertyName(node.Tag()), node.Text())
case ObjectNode:
properties.Set(node.Tag(), node.Object())
properties.Set(PropertyName(node.Tag()), node.Object())
case ArrayNode:
properties.Set(node.Tag(), node.ArrayElements())
properties.Set(PropertyName(node.Tag()), node.ArrayElements())
}
}
}
}
func propertiesGet(properties Properties, tag PropertyName) any {
return properties.getRaw(tag)
}
func propertiesRemove(properties Properties, tag PropertyName) []PropertyName {
if properties.getRaw(tag) == nil {
return []PropertyName{}
}
properties.setRaw(tag, nil)
return []PropertyName{tag}
}
func (data *dataProperty) init() {
data.propertyList.init()
data.get = propertiesGet
data.set = propertiesSet
data.remove = propertiesRemove
}
func (data *dataProperty) Get(tag PropertyName) any {
return propertiesGet(data, data.normalize(tag))
}
func (data *dataProperty) Remove(tag PropertyName) {
data.remove(data, data.normalize(tag))
}

View File

@ -6,7 +6,7 @@ import (
"strings"
)
func stringProperty(properties Properties, tag string, session Session) (string, bool) {
func stringProperty(properties Properties, tag PropertyName, session Session) (string, bool) {
if value := properties.getRaw(tag); value != nil {
if text, ok := value.(string); ok {
return session.resolveConstants(text)
@ -15,7 +15,7 @@ func stringProperty(properties Properties, tag string, session Session) (string,
return "", false
}
func imageProperty(properties Properties, tag string, session Session) (string, bool) {
func imageProperty(properties Properties, tag PropertyName, session Session) (string, bool) {
if value := properties.getRaw(tag); value != nil {
if text, ok := value.(string); ok {
if text != "" && text[0] == '@' {
@ -61,11 +61,11 @@ func valueToSizeUnit(value any, session Session) (SizeUnit, bool) {
return AutoSize(), false
}
func sizeProperty(properties Properties, tag string, session Session) (SizeUnit, bool) {
func sizeProperty(properties Properties, tag PropertyName, session Session) (SizeUnit, bool) {
return valueToSizeUnit(properties.getRaw(tag), session)
}
func angleProperty(properties Properties, tag string, session Session) (AngleUnit, bool) {
func angleProperty(properties Properties, tag PropertyName, session Session) (AngleUnit, bool) {
if value := properties.getRaw(tag); value != nil {
switch value := value.(type) {
case AngleUnit:
@ -98,11 +98,11 @@ func valueToColor(value any, session Session) (Color, bool) {
return Color(0), false
}
func colorProperty(properties Properties, tag string, session Session) (Color, bool) {
func colorProperty(properties Properties, tag PropertyName, session Session) (Color, bool) {
return valueToColor(properties.getRaw(tag), session)
}
func valueToEnum(value any, tag string, session Session, defaultValue int) (int, bool) {
func valueToEnum(value any, tag PropertyName, session Session, defaultValue int) (int, bool) {
if value != nil {
values := enumProperties[tag].values
switch value := value.(type) {
@ -165,7 +165,7 @@ func enumStringToInt(value string, enumValues []string, logError bool) (int, boo
return 0, false
}
func enumProperty(properties Properties, tag string, session Session, defaultValue int) (int, bool) {
func enumProperty(properties Properties, tag PropertyName, session Session, defaultValue int) (int, bool) {
return valueToEnum(properties.getRaw(tag), tag, session, defaultValue)
}
@ -194,7 +194,7 @@ func valueToBool(value any, session Session) (bool, bool) {
return false, false
}
func boolProperty(properties Properties, tag string, session Session) (bool, bool) {
func boolProperty(properties Properties, tag PropertyName, session Session) (bool, bool) {
return valueToBool(properties.getRaw(tag), session)
}
@ -224,7 +224,7 @@ func valueToInt(value any, session Session, defaultValue int) (int, bool) {
return defaultValue, false
}
func intProperty(properties Properties, tag string, session Session, defaultValue int) (int, bool) {
func intProperty(properties Properties, tag PropertyName, session Session, defaultValue int) (int, bool) {
return valueToInt(properties.getRaw(tag), session, defaultValue)
}
@ -248,7 +248,7 @@ func valueToFloat(value any, session Session, defaultValue float64) (float64, bo
return defaultValue, false
}
func floatProperty(properties Properties, tag string, session Session, defaultValue float64) (float64, bool) {
func floatProperty(properties Properties, tag PropertyName, session Session, defaultValue float64) (float64, bool) {
return valueToFloat(properties.getRaw(tag), session, defaultValue)
}
@ -272,7 +272,7 @@ func valueToFloatText(value any, session Session, defaultValue float64) (string,
return fmt.Sprintf("%g", defaultValue), false
}
func floatTextProperty(properties Properties, tag string, session Session, defaultValue float64) (string, bool) {
func floatTextProperty(properties Properties, tag PropertyName, session Session, defaultValue float64) (string, bool) {
return valueToFloatText(properties.getRaw(tag), session, defaultValue)
}
@ -297,6 +297,6 @@ func valueToRange(value any, session Session) (Range, bool) {
return Range{}, false
}
func rangeProperty(properties Properties, tag string, session Session) (Range, bool) {
func rangeProperty(properties Properties, tag PropertyName, session Session) (Range, bool) {
return valueToRange(properties.getRaw(tag), session)
}

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@ import (
"strings"
)
var colorProperties = []string{
var colorProperties = []PropertyName{
ColorTag,
BackgroundColor,
TextColor,
@ -19,9 +19,10 @@ var colorProperties = []string{
OutlineColor,
TextLineColor,
ColorPickerValue,
AccentColor,
}
func isPropertyInList(tag string, list []string) bool {
func isPropertyInList(tag PropertyName, list []PropertyName) bool {
for _, prop := range list {
if prop == tag {
return true
@ -30,14 +31,11 @@ func isPropertyInList(tag string, list []string) bool {
return false
}
var angleProperties = []string{
Rotate,
SkewX,
SkewY,
var angleProperties = []PropertyName{
From,
}
var boolProperties = []string{
var boolProperties = []PropertyName{
Disabled,
Focusable,
Inset,
@ -64,9 +62,11 @@ var boolProperties = []string{
Repeating,
UserSelect,
ColumnSpanAll,
MoveToFrontAnimation,
HideSummaryMarker,
}
var intProperties = []string{
var intProperties = []PropertyName{
ZIndex,
TabSize,
HeadHeight,
@ -76,16 +76,12 @@ var intProperties = []string{
ColumnCount,
Order,
TabIndex,
MaxLength,
NumberPickerPrecision,
}
var floatProperties = map[string]struct{ min, max float64 }{
var floatProperties = map[PropertyName]struct{ min, max float64 }{
Opacity: {min: 0, max: 1},
ScaleX: {min: -math.MaxFloat64, max: math.MaxFloat64},
ScaleY: {min: -math.MaxFloat64, max: math.MaxFloat64},
ScaleZ: {min: -math.MaxFloat64, max: math.MaxFloat64},
RotateX: {min: 0, max: 1},
RotateY: {min: 0, max: 1},
RotateZ: {min: 0, max: 1},
NumberPickerMax: {min: -math.MaxFloat64, max: math.MaxFloat64},
NumberPickerMin: {min: -math.MaxFloat64, max: math.MaxFloat64},
NumberPickerStep: {min: -math.MaxFloat64, max: math.MaxFloat64},
@ -94,88 +90,88 @@ var floatProperties = map[string]struct{ min, max float64 }{
ProgressBarValue: {min: 0, max: math.MaxFloat64},
VideoWidth: {min: 0, max: 10000},
VideoHeight: {min: 0, max: 10000},
PushDuration: {min: 0, max: math.MaxFloat64},
}
var sizeProperties = map[string]string{
Width: Width,
Height: Height,
MinWidth: MinWidth,
MinHeight: MinHeight,
MaxWidth: MaxWidth,
MaxHeight: MaxHeight,
Left: Left,
Right: Right,
Top: Top,
Bottom: Bottom,
var sizeProperties = map[PropertyName]string{
Width: string(Width),
Height: string(Height),
MinWidth: string(MinWidth),
MinHeight: string(MinHeight),
MaxWidth: string(MaxWidth),
MaxHeight: string(MaxHeight),
Left: string(Left),
Right: string(Right),
Top: string(Top),
Bottom: string(Bottom),
TextSize: "font-size",
TextIndent: TextIndent,
LetterSpacing: LetterSpacing,
WordSpacing: WordSpacing,
LineHeight: LineHeight,
TextIndent: string(TextIndent),
LetterSpacing: string(LetterSpacing),
WordSpacing: string(WordSpacing),
LineHeight: string(LineHeight),
TextLineThickness: "text-decoration-thickness",
ListRowGap: "row-gap",
ListColumnGap: "column-gap",
GridRowGap: GridRowGap,
GridColumnGap: GridColumnGap,
ColumnWidth: ColumnWidth,
ColumnGap: ColumnGap,
Gap: Gap,
Margin: Margin,
MarginLeft: MarginLeft,
MarginRight: MarginRight,
MarginTop: MarginTop,
MarginBottom: MarginBottom,
Padding: Padding,
PaddingLeft: PaddingLeft,
PaddingRight: PaddingRight,
PaddingTop: PaddingTop,
PaddingBottom: PaddingBottom,
BorderWidth: BorderWidth,
BorderLeftWidth: BorderLeftWidth,
BorderRightWidth: BorderRightWidth,
BorderTopWidth: BorderTopWidth,
BorderBottomWidth: BorderBottomWidth,
OutlineWidth: OutlineWidth,
OutlineOffset: OutlineOffset,
XOffset: XOffset,
YOffset: YOffset,
BlurRadius: BlurRadius,
SpreadRadius: SpreadRadius,
Perspective: Perspective,
PerspectiveOriginX: PerspectiveOriginX,
PerspectiveOriginY: PerspectiveOriginY,
OriginX: OriginX,
OriginY: OriginY,
OriginZ: OriginZ,
TranslateX: TranslateX,
TranslateY: TranslateY,
TranslateZ: TranslateZ,
Radius: Radius,
RadiusX: RadiusX,
RadiusY: RadiusY,
RadiusTopLeft: RadiusTopLeft,
RadiusTopLeftX: RadiusTopLeftX,
RadiusTopLeftY: RadiusTopLeftY,
RadiusTopRight: RadiusTopRight,
RadiusTopRightX: RadiusTopRightX,
RadiusTopRightY: RadiusTopRightY,
RadiusBottomLeft: RadiusBottomLeft,
RadiusBottomLeftX: RadiusBottomLeftX,
RadiusBottomLeftY: RadiusBottomLeftY,
RadiusBottomRight: RadiusBottomRight,
RadiusBottomRightX: RadiusBottomRightX,
RadiusBottomRightY: RadiusBottomRightY,
ItemWidth: ItemWidth,
ItemHeight: ItemHeight,
CenterX: CenterX,
CenterY: CenterX,
GridRowGap: string(GridRowGap),
GridColumnGap: string(GridColumnGap),
ColumnWidth: string(ColumnWidth),
ColumnGap: string(ColumnGap),
Gap: string(Gap),
Margin: string(Margin),
MarginLeft: string(MarginLeft),
MarginRight: string(MarginRight),
MarginTop: string(MarginTop),
MarginBottom: string(MarginBottom),
Padding: string(Padding),
PaddingLeft: string(PaddingLeft),
PaddingRight: string(PaddingRight),
PaddingTop: string(PaddingTop),
PaddingBottom: string(PaddingBottom),
BorderWidth: string(BorderWidth),
BorderLeftWidth: string(BorderLeftWidth),
BorderRightWidth: string(BorderRightWidth),
BorderTopWidth: string(BorderTopWidth),
BorderBottomWidth: string(BorderBottomWidth),
OutlineWidth: string(OutlineWidth),
OutlineOffset: string(OutlineOffset),
XOffset: string(XOffset),
YOffset: string(YOffset),
BlurRadius: string(BlurRadius),
SpreadRadius: string(SpreadRadius),
Perspective: string(Perspective),
PerspectiveOriginX: string(PerspectiveOriginX),
PerspectiveOriginY: string(PerspectiveOriginY),
TransformOriginX: string(TransformOriginX),
TransformOriginY: string(TransformOriginY),
TransformOriginZ: string(TransformOriginZ),
Radius: string(Radius),
RadiusX: string(RadiusX),
RadiusY: string(RadiusY),
RadiusTopLeft: string(RadiusTopLeft),
RadiusTopLeftX: string(RadiusTopLeftX),
RadiusTopLeftY: string(RadiusTopLeftY),
RadiusTopRight: string(RadiusTopRight),
RadiusTopRightX: string(RadiusTopRightX),
RadiusTopRightY: string(RadiusTopRightY),
RadiusBottomLeft: string(RadiusBottomLeft),
RadiusBottomLeftX: string(RadiusBottomLeftX),
RadiusBottomLeftY: string(RadiusBottomLeftY),
RadiusBottomRight: string(RadiusBottomRight),
RadiusBottomRightX: string(RadiusBottomRightX),
RadiusBottomRightY: string(RadiusBottomRightY),
ItemWidth: string(ItemWidth),
ItemHeight: string(ItemHeight),
CenterX: string(CenterX),
CenterY: string(CenterX),
}
var enumProperties = map[string]struct {
type enumPropertyData struct {
values []string
cssTag string
cssValues []string
}{
}
var enumProperties = map[PropertyName]enumPropertyData{
Semantics: {
[]string{"default", "article", "section", "aside", "header", "main", "footer", "navigation", "figure", "figure-caption", "button", "p", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "code"},
"",
@ -188,17 +184,17 @@ var enumProperties = map[string]struct {
},
Overflow: {
[]string{"hidden", "visible", "scroll", "auto"},
Overflow,
string(Overflow),
[]string{"hidden", "visible", "scroll", "auto"},
},
TextAlign: {
[]string{"left", "right", "center", "justify"},
TextAlign,
string(TextAlign),
[]string{"left", "right", "center", "justify"},
},
TextTransform: {
[]string{"none", "capitalize", "lowercase", "uppercase"},
TextTransform,
string(TextTransform),
[]string{"none", "capitalize", "lowercase", "uppercase"},
},
TextWeight: {
@ -208,27 +204,27 @@ var enumProperties = map[string]struct {
},
WhiteSpace: {
[]string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"},
WhiteSpace,
string(WhiteSpace),
[]string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"},
},
WordBreak: {
[]string{"normal", "break-all", "keep-all", "break-word"},
WordBreak,
string(WordBreak),
[]string{"normal", "break-all", "keep-all", "break-word"},
},
TextOverflow: {
[]string{"clip", "ellipsis"},
TextOverflow,
string(TextOverflow),
[]string{"clip", "ellipsis"},
},
TextWrap: {
[]string{"wrap", "nowrap", "balance"},
TextWrap,
string(TextWrap),
[]string{"wrap", "nowrap", "balance"},
},
WritingMode: {
[]string{"horizontal-top-to-bottom", "horizontal-bottom-to-top", "vertical-right-to-left", "vertical-left-to-right"},
WritingMode,
string(WritingMode),
[]string{"horizontal-tb", "horizontal-bt", "vertical-rl", "vertical-lr"},
},
TextDirection: {
@ -248,7 +244,7 @@ var enumProperties = map[string]struct {
},
BorderStyle: {
[]string{"none", "solid", "dashed", "dotted", "double"},
BorderStyle,
string(BorderStyle),
[]string{"none", "solid", "dashed", "dotted", "double"},
},
TopStyle: {
@ -273,7 +269,7 @@ var enumProperties = map[string]struct {
},
OutlineStyle: {
[]string{"none", "solid", "dashed", "dotted", "double"},
OutlineStyle,
string(OutlineStyle),
[]string{"none", "solid", "dashed", "dotted", "double"},
},
Tabs: {
@ -348,7 +344,7 @@ var enumProperties = map[string]struct {
},
GridAutoFlow: {
[]string{"row", "column", "row-dense", "column-dense"},
GridAutoFlow,
string(GridAutoFlow),
[]string{"row", "column", "row dense", "column dense"},
},
ImageVerticalAlign: {
@ -388,7 +384,7 @@ var enumProperties = map[string]struct {
},
Cursor: {
[]string{"auto", "default", "none", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", "text", "vertical-text", "alias", "copy", "move", "no-drop", "not-allowed", "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out", "grab", "grabbing"},
Cursor,
string(Cursor),
[]string{"auto", "default", "none", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", "text", "vertical-text", "alias", "copy", "move", "no-drop", "not-allowed", "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out", "grab", "grabbing"},
},
Fit: {
@ -416,6 +412,21 @@ var enumProperties = map[string]struct {
"background-clip",
[]string{"border-box", "padding-box", "content-box"}, // "text"},
},
BackgroundOrigin: {
[]string{"border-box", "padding-box", "content-box"},
"background-origin",
[]string{"border-box", "padding-box", "content-box"},
},
MaskClip: {
[]string{"border-box", "padding-box", "content-box"},
"mask-clip",
[]string{"border-box", "padding-box", "content-box"},
},
MaskOrigin: {
[]string{"border-box", "padding-box", "content-box"},
"background-origin",
[]string{"border-box", "padding-box", "content-box"},
},
Direction: {
[]string{"to-top", "to-right-top", "to-right", "to-right-bottom", "to-bottom", "to-left-bottom", "to-left", "to-left-top"},
"",
@ -468,27 +479,27 @@ var enumProperties = map[string]struct {
},
MixBlendMode: {
[]string{"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"},
MixBlendMode,
string(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(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(ColumnFill),
[]string{"balance", "auto"},
},
}
func notCompatibleType(tag string, value any) {
ErrorLogF(`"%T" type not compatible with "%s" property`, value, tag)
func notCompatibleType(tag PropertyName, value any) {
ErrorLogF(`"%T" type not compatible with "%s" property`, value, string(tag))
}
func invalidPropertyValue(tag string, value any) {
ErrorLogF(`Invalid value "%v" of "%s" property`, value, tag)
func invalidPropertyValue(tag PropertyName, value any) {
ErrorLogF(`Invalid value "%v" of "%s" property`, value, string(tag))
}
func isConstantName(text string) bool {
@ -549,26 +560,48 @@ func isInt(value any) (int, bool) {
return n, true
}
func (properties *propertyList) setSimpleProperty(tag string, value any) bool {
func setSimpleProperty(properties Properties, tag PropertyName, value any) bool {
if value == nil {
delete(properties.properties, tag)
properties.setRaw(tag, nil)
return true
} else if text, ok := value.(string); ok {
text = strings.Trim(text, " \t\n\r")
if text == "" {
delete(properties.properties, tag)
properties.setRaw(tag, nil)
return true
}
if isConstantName(text) {
properties.properties[tag] = text
properties.setRaw(tag, text)
return true
}
}
return false
}
func (properties *propertyList) setSizeProperty(tag string, value any) bool {
if !properties.setSimpleProperty(tag, value) {
func setStringPropertyValue(properties Properties, tag PropertyName, text any) []PropertyName {
if text != "" {
properties.setRaw(tag, text)
} else if properties.getRaw(tag) != nil {
properties.setRaw(tag, nil)
} else {
return []PropertyName{}
}
return []PropertyName{tag}
}
func setArrayPropertyValue[T any](properties Properties, tag PropertyName, value []T) []PropertyName {
if len(value) > 0 {
properties.setRaw(tag, value)
} else if properties.getRaw(tag) != nil {
properties.setRaw(tag, nil)
} else {
return []PropertyName{}
}
return []PropertyName{tag}
}
func setSizeProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
var size SizeUnit
switch value := value.(type) {
case string:
@ -578,7 +611,7 @@ func (properties *propertyList) setSizeProperty(tag string, value any) bool {
size.Function = fn
} else if size, ok = StringToSizeUnit(value); !ok {
invalidPropertyValue(tag, value)
return false
return nil
}
case SizeUnit:
size = value
@ -601,29 +634,29 @@ func (properties *propertyList) setSizeProperty(tag string, value any) bool {
size.Value = float64(n)
} else {
notCompatibleType(tag, value)
return false
return nil
}
}
if size.Type == Auto {
delete(properties.properties, tag)
properties.setRaw(tag, nil)
} else {
properties.properties[tag] = size
properties.setRaw(tag, size)
}
}
return true
return []PropertyName{tag}
}
func (properties *propertyList) setAngleProperty(tag string, value any) bool {
if !properties.setSimpleProperty(tag, value) {
func setAngleProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
var angle AngleUnit
switch value := value.(type) {
case string:
var ok bool
if angle, ok = StringToAngleUnit(value); !ok {
invalidPropertyValue(tag, value)
return false
return nil
}
case AngleUnit:
angle = value
@ -639,24 +672,24 @@ func (properties *propertyList) setAngleProperty(tag string, value any) bool {
angle = Rad(float64(n))
} else {
notCompatibleType(tag, value)
return false
return nil
}
}
properties.properties[tag] = angle
properties.setRaw(tag, angle)
}
return true
return []PropertyName{tag}
}
func (properties *propertyList) setColorProperty(tag string, value any) bool {
if !properties.setSimpleProperty(tag, value) {
func setColorProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
var result Color
switch value := value.(type) {
case string:
var err error
if result, err = stringToColor(value); err != nil {
invalidPropertyValue(tag, value)
return false
return nil
}
case Color:
result = value
@ -666,105 +699,101 @@ func (properties *propertyList) setColorProperty(tag string, value any) bool {
result = Color(color)
} else {
notCompatibleType(tag, value)
return false
return nil
}
}
if result == 0 {
delete(properties.properties, tag)
} else {
properties.properties[tag] = result
}
properties.setRaw(tag, result)
}
return true
return []PropertyName{tag}
}
func (properties *propertyList) setEnumProperty(tag string, value any, values []string) bool {
if !properties.setSimpleProperty(tag, value) {
func setEnumProperty(properties Properties, tag PropertyName, value any, values []string) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
var n int
if text, ok := value.(string); ok {
if n, ok = enumStringToInt(text, values, false); !ok {
invalidPropertyValue(tag, value)
return false
return nil
}
} else if i, ok := isInt(value); ok {
if i < 0 || i >= len(values) {
invalidPropertyValue(tag, value)
return false
return nil
}
n = i
} else {
notCompatibleType(tag, value)
return false
return nil
}
properties.properties[tag] = n
properties.setRaw(tag, n)
}
return true
return []PropertyName{tag}
}
func (properties *propertyList) setBoolProperty(tag string, value any) bool {
if !properties.setSimpleProperty(tag, value) {
func setBoolProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
if text, ok := value.(string); ok {
switch strings.ToLower(strings.Trim(text, " \t")) {
case "true", "yes", "on", "1":
properties.properties[tag] = true
properties.setRaw(tag, true)
case "false", "no", "off", "0":
properties.properties[tag] = false
properties.setRaw(tag, false)
default:
invalidPropertyValue(tag, value)
return false
return nil
}
} else if n, ok := isInt(value); ok {
switch n {
case 1:
properties.properties[tag] = true
properties.setRaw(tag, true)
case 0:
properties.properties[tag] = false
properties.setRaw(tag, false)
default:
invalidPropertyValue(tag, value)
return false
return nil
}
} else if b, ok := value.(bool); ok {
properties.properties[tag] = b
properties.setRaw(tag, b)
} else {
notCompatibleType(tag, value)
return false
return nil
}
}
return true
return []PropertyName{tag}
}
func (properties *propertyList) setIntProperty(tag string, value any) bool {
if !properties.setSimpleProperty(tag, value) {
func setIntProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
if text, ok := value.(string); ok {
n, err := strconv.Atoi(strings.Trim(text, " \t"))
if err != nil {
invalidPropertyValue(tag, value)
ErrorLog(err.Error())
return false
return nil
}
properties.properties[tag] = n
properties.setRaw(tag, n)
} else if n, ok := isInt(value); ok {
properties.properties[tag] = n
properties.setRaw(tag, n)
} else {
notCompatibleType(tag, value)
return false
return nil
}
}
return true
return []PropertyName{tag}
}
func (properties *propertyList) setFloatProperty(tag string, value any, min, max float64) bool {
if !properties.setSimpleProperty(tag, value) {
func setFloatProperty(properties Properties, tag PropertyName, value any, min, max float64) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
f := float64(0)
switch value := value.(type) {
case string:
@ -772,14 +801,14 @@ func (properties *propertyList) setFloatProperty(tag string, value any, min, max
if f, err = strconv.ParseFloat(strings.Trim(value, " \t"), 64); err != nil {
invalidPropertyValue(tag, value)
ErrorLog(err.Error())
return false
return nil
}
if f < min || f > max {
ErrorLogF(`"%T" out of range of "%s" property`, value, tag)
return false
return nil
}
properties.properties[tag] = value
return true
properties.setRaw(tag, value)
return nil
case float32:
f = float64(value)
@ -792,64 +821,84 @@ func (properties *propertyList) setFloatProperty(tag string, value any, min, max
f = float64(n)
} else {
notCompatibleType(tag, value)
return false
return nil
}
}
if f >= min && f <= max {
properties.properties[tag] = f
properties.setRaw(tag, f)
} else {
ErrorLogF(`"%T" out of range of "%s" property`, value, tag)
return false
return nil
}
}
return true
return []PropertyName{tag}
}
func (properties *propertyList) Set(tag string, value any) bool {
return properties.set(strings.ToLower(tag), value)
}
func (properties *propertyList) set(tag string, value any) bool {
if value == nil {
delete(properties.properties, tag)
return true
}
func propertiesSet(properties Properties, tag PropertyName, value any) []PropertyName {
if _, ok := sizeProperties[tag]; ok {
return properties.setSizeProperty(tag, value)
return setSizeProperty(properties, tag, value)
}
if valuesData, ok := enumProperties[tag]; ok {
return properties.setEnumProperty(tag, value, valuesData.values)
return setEnumProperty(properties, tag, value, valuesData.values)
}
if limits, ok := floatProperties[tag]; ok {
return properties.setFloatProperty(tag, value, limits.min, limits.max)
return setFloatProperty(properties, tag, value, limits.min, limits.max)
}
if isPropertyInList(tag, colorProperties) {
return properties.setColorProperty(tag, value)
return setColorProperty(properties, tag, value)
}
if isPropertyInList(tag, angleProperties) {
return properties.setAngleProperty(tag, value)
return setAngleProperty(properties, tag, value)
}
if isPropertyInList(tag, boolProperties) {
return properties.setBoolProperty(tag, value)
return setBoolProperty(properties, tag, value)
}
if isPropertyInList(tag, intProperties) {
return properties.setIntProperty(tag, value)
return setIntProperty(properties, tag, value)
}
if text, ok := value.(string); ok {
properties.properties[tag] = text
return true
properties.setRaw(tag, text)
return []PropertyName{tag}
}
notCompatibleType(tag, value)
return nil
}
/*
func (properties *propertyList) Set(tag PropertyName, value any) bool {
tag = properties.normalize(tag)
if value == nil {
properties.remove(properties, tag)
return true
}
return properties.set(properties, tag, value) != nil
}
*/
func (data *dataProperty) Set(tag PropertyName, value any) bool {
if value == nil {
data.Remove(tag)
return true
}
tag = data.normalize(tag)
for _, supported := range data.supportedProperties {
if tag == supported {
return data.set(data, tag, value) != nil
}
}
ErrorLogF(`"%s" property is not supported`, string(tag))
return false
}

View File

@ -1,5 +1,6 @@
package rui
// Constants for various specific properties of a views
const (
// Visible - default value of the view Visibility property: View is visible
Visible = 0
@ -148,9 +149,9 @@ const (
WhiteSpacePreLine = 4
// WhiteSpaceBreakSpaces - the behavior is identical to that of WhiteSpacePreWrap, except that:
// * Any sequence of preserved white space always takes up space, including at the end of the line.
// * A line breaking opportunity exists after every preserved white space character, including between white space characters.
// * Such preserved spaces take up space and do not hang, and thus affect the boxs intrinsic sizes (min-content size and max-content size).
// - Any sequence of preserved white space always takes up space, including at the end of the line.
// - A line breaking opportunity exists after every preserved white space character, including between white space characters.
// - Such preserved spaces take up space and do not hang, and thus affect the boxs intrinsic sizes (min-content size and max-content size).
WhiteSpaceBreakSpaces = 5
// WordBreakNormal - use the default line break rule.

840
radius.go

File diff suppressed because it is too large Load Diff

75
range.go Normal file
View File

@ -0,0 +1,75 @@
package rui
import (
"fmt"
"strconv"
"strings"
)
// Range defines range limits. The First and Last value are included in the range
type Range struct {
First, Last int
}
// String returns a string representation of the Range struct
func (r Range) String() string {
if r.First == r.Last {
return fmt.Sprintf("%d", r.First)
}
return fmt.Sprintf("%d:%d", r.First, r.Last)
}
func (r *Range) setValue(value string) bool {
var err error
if strings.Contains(value, ":") {
values := strings.Split(value, ":")
if len(values) != 2 {
ErrorLog("Invalid range value: " + value)
return false
}
if r.First, err = strconv.Atoi(strings.Trim(values[0], " \t\n\r")); err != nil {
ErrorLog(`Invalid first range value "` + value + `" (` + err.Error() + ")")
return false
}
if r.Last, err = strconv.Atoi(strings.Trim(values[1], " \t\n\r")); err != nil {
ErrorLog(`Invalid last range value "` + value + `" (` + err.Error() + ")")
return false
}
return true
}
if r.First, err = strconv.Atoi(value); err != nil {
ErrorLog(`Invalid range value "` + value + `" (` + err.Error() + ")")
return false
}
r.Last = r.First
return true
}
func setRangeProperty(properties Properties, tag PropertyName, value any) []PropertyName {
switch value := value.(type) {
case string:
if setSimpleProperty(properties, tag, value) {
return []PropertyName{tag}
}
var r Range
if !r.setValue(value) {
invalidPropertyValue(tag, value)
return nil
}
properties.setRaw(tag, r)
case Range:
properties.setRaw(tag, value)
default:
if n, ok := isInt(value); ok {
properties.setRaw(tag, Range{First: n, Last: n})
} else {
notCompatibleType(tag, value)
return nil
}
}
return []PropertyName{tag}
}

View File

@ -6,16 +6,38 @@ import (
"strings"
)
// Constants for [Resizable] specific properties and events
const (
// Side is the constant for the "side" property tag.
// The "side" int property determines which side of the container is used to resize.
// The value of property is or-combination of TopSide (1), RightSide (2), BottomSide (4), and LeftSide (8)
Side = "side"
// Side is the constant for "side" property tag.
//
// Used by Resizable.
// Determines which side of the container is used to resize. The value of property is an or-combination of values listed.
// Default value is "all".
//
// Supported types: int, string.
//
// Values:
// - 1 (TopSide) or "top" - Top frame side.
// - 2 (RightSide) or "right" - Right frame side.
// - 4 (BottomSide) or "bottom" - Bottom frame side.
// - 8 (LeftSide) or "left" - Left frame side.
// - 15 (AllSides) or "all" - All frame sides.
Side PropertyName = "side"
// ResizeBorderWidth is the constant for the "resize-border-width" property tag.
// The "ResizeBorderWidth" SizeUnit property determines the width of the resizing border
ResizeBorderWidth = "resize-border-width"
// ResizeBorderWidth is the constant for "resize-border-width" property tag.
//
// Used by Resizable.
// Specifies the width of the resizing border.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
ResizeBorderWidth PropertyName = "resize-border-width"
)
// Constants for values of [Resizable] "side" property. These constants can be ORed if needed.
const (
// TopSide is value of the "side" property: the top side is used to resize
TopSide = 1
@ -32,7 +54,7 @@ const (
AllSides = TopSide | RightSide | BottomSide | LeftSide
)
// Resizable - grid-container of View
// Resizable represents a Resizable view
type Resizable interface {
View
ParentView
@ -40,7 +62,6 @@ type Resizable interface {
type resizableData struct {
viewData
content []View
}
// NewResizable create new Resizable object and return it
@ -52,97 +73,40 @@ func NewResizable(session Session, params Params) Resizable {
}
func newResizable(session Session) View {
return NewResizable(session, nil)
return new(resizableData)
}
func (resizable *resizableData) init(session Session) {
resizable.viewData.init(session)
resizable.tag = "Resizable"
resizable.systemClass = "ruiGridLayout"
resizable.content = []View{}
}
func (resizable *resizableData) String() string {
return getViewString(resizable, nil)
resizable.set = resizable.setFunc
resizable.changed = resizable.propertyChanged
}
func (resizable *resizableData) Views() []View {
return resizable.content
if view := resizable.content(); view != nil {
return []View{view}
}
return []View{}
}
func (resizable *resizableData) Remove(tag string) {
resizable.remove(strings.ToLower(tag))
func (resizable *resizableData) content() View {
if value := resizable.getRaw(Content); value != nil {
if content, ok := value.(View); ok {
return content
}
}
return nil
}
func (resizable *resizableData) remove(tag string) {
func (resizable *resizableData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case Side:
oldSide := resizable.getSide()
delete(resizable.properties, Side)
if oldSide != resizable.getSide() {
if resizable.created {
updateInnerHTML(resizable.htmlID(), resizable.Session())
resizable.updateResizeBorderWidth()
}
resizable.propertyChangedEvent(tag)
}
return resizableSetSide(resizable, value)
case ResizeBorderWidth:
w := resizable.resizeBorderWidth()
delete(resizable.properties, ResizeBorderWidth)
if !w.Equal(resizable.resizeBorderWidth()) {
resizable.updateResizeBorderWidth()
resizable.propertyChangedEvent(tag)
}
case Content:
if len(resizable.content) > 0 {
resizable.content = []View{}
if resizable.created {
updateInnerHTML(resizable.htmlID(), resizable.Session())
}
resizable.propertyChangedEvent(tag)
}
default:
resizable.viewData.remove(tag)
}
}
func (resizable *resizableData) Set(tag string, value any) bool {
return resizable.set(strings.ToLower(tag), value)
}
func (resizable *resizableData) set(tag string, value any) bool {
if value == nil {
resizable.remove(tag)
return true
}
switch tag {
case Side:
oldSide := resizable.getSide()
if !resizable.setSide(value) {
notCompatibleType(tag, value)
return false
}
if oldSide != resizable.getSide() {
if resizable.created {
updateInnerHTML(resizable.htmlID(), resizable.Session())
resizable.updateResizeBorderWidth()
}
resizable.propertyChangedEvent(tag)
}
return true
case ResizeBorderWidth:
w := resizable.resizeBorderWidth()
ok := resizable.setSizeProperty(tag, value)
if ok && !w.Equal(resizable.resizeBorderWidth()) {
resizable.updateResizeBorderWidth()
resizable.propertyChangedEvent(tag)
}
return ok
return setSizeProperty(resizable, tag, value)
case Content:
var newContent View = nil
@ -154,45 +118,54 @@ func (resizable *resizableData) set(tag string, value any) bool {
newContent = value
case DataObject:
if view := CreateViewFromObject(resizable.Session(), value); view != nil {
newContent = view
} else {
return false
if newContent = CreateViewFromObject(resizable.Session(), value); newContent == nil {
return nil
}
default:
notCompatibleType(tag, value)
return false
return nil
}
if len(resizable.content) == 0 {
resizable.content = []View{newContent}
} else {
resizable.content[0] = newContent
}
if resizable.created {
updateInnerHTML(resizable.htmlID(), resizable.Session())
}
resizable.propertyChangedEvent(tag)
return true
resizable.setRaw(Content, newContent)
return []PropertyName{}
case CellWidth, CellHeight, GridRowGap, GridColumnGap, CellVerticalAlign, CellHorizontalAlign:
ErrorLogF(`Not supported "%s" property`, tag)
return false
ErrorLogF(`Not supported "%s" property`, string(tag))
return nil
}
return resizable.viewData.set(tag, value)
return resizable.viewData.setFunc(tag, value)
}
func (resizable *resizableData) Get(tag string) any {
return resizable.get(strings.ToLower(tag))
func (resizable *resizableData) propertyChanged(tag PropertyName) {
switch tag {
case Side:
updateInnerHTML(resizable.htmlID(), resizable.Session())
fallthrough
case ResizeBorderWidth:
htmlID := resizable.htmlID()
session := resizable.Session()
column, row := resizableCellSizeCSS(resizable)
session.updateCSSProperty(htmlID, "grid-template-columns", column)
session.updateCSSProperty(htmlID, "grid-template-rows", row)
case Content:
updateInnerHTML(resizable.htmlID(), resizable.Session())
default:
resizable.viewData.propertyChanged(tag)
}
}
func (resizable *resizableData) getSide() int {
if value := resizable.getRaw(Side); value != nil {
func resizableSide(view View) int {
if value := view.getRaw(Side); value != nil {
switch value := value.(type) {
case string:
if value, ok := resizable.session.resolveConstants(value); ok {
if value, ok := view.Session().resolveConstants(value); ok {
validValues := map[string]int{
"top": TopSide,
"right": RightSide,
@ -236,15 +209,15 @@ func (resizable *resizableData) getSide() int {
return AllSides
}
func (resizable *resizableData) setSide(value any) bool {
func resizableSetSide(properties Properties, value any) []PropertyName {
switch value := value.(type) {
case string:
if n, err := strconv.Atoi(value); err == nil {
if n >= 1 && n <= AllSides {
resizable.properties[Side] = n
return true
properties.setRaw(Side, n)
return []PropertyName{Side}
}
return false
return nil
}
validValues := map[string]int{
"top": TopSide,
@ -265,13 +238,13 @@ func (resizable *resizableData) setSide(value any) bool {
hasConst = true
} else if n, err := strconv.Atoi(val); err == nil {
if n < 1 || n > AllSides {
return false
return nil
}
sides |= n
} else if n, ok := validValues[val]; ok {
sides |= n
} else {
return false
return nil
}
}
@ -280,69 +253,58 @@ func (resizable *resizableData) setSide(value any) bool {
for i := 1; i < len(values); i++ {
value += "|" + values[i]
}
resizable.properties[Side] = value
return true
properties.setRaw(Side, value)
return []PropertyName{Side}
}
if sides >= 1 && sides <= AllSides {
resizable.properties[Side] = sides
return true
properties.setRaw(Side, sides)
return []PropertyName{Side}
}
} else if value[0] == '@' {
resizable.properties[Side] = value
return true
properties.setRaw(Side, value)
return []PropertyName{Side}
} else if n, ok := validValues[value]; ok {
resizable.properties[Side] = n
return true
properties.setRaw(Side, n)
return []PropertyName{Side}
}
case int:
if value >= 1 && value <= AllSides {
resizable.properties[Side] = value
return true
properties.setRaw(Side, value)
return []PropertyName{Side}
} else {
ErrorLogF(`Invalid value %d of "side" property`, value)
return false
return nil
}
default:
if n, ok := isInt(value); ok {
if n >= 1 && n <= AllSides {
resizable.properties[Side] = n
return true
properties.setRaw(Side, n)
return []PropertyName{Side}
} else {
ErrorLogF(`Invalid value %d of "side" property`, n)
return false
return nil
}
}
}
return false
return nil
}
func (resizable *resizableData) resizeBorderWidth() SizeUnit {
result, _ := sizeProperty(resizable, ResizeBorderWidth, resizable.Session())
func resizableBorderWidth(view View) SizeUnit {
result, _ := sizeProperty(view, ResizeBorderWidth, view.Session())
if result.Type == Auto || result.Value == 0 {
return Px(4)
}
return result
}
func (resizable *resizableData) updateResizeBorderWidth() {
if resizable.created {
htmlID := resizable.htmlID()
session := resizable.Session()
column, row := resizable.cellSizeCSS()
session.updateCSSProperty(htmlID, "grid-template-columns", column)
session.updateCSSProperty(htmlID, "grid-template-rows", row)
}
}
func (resizable *resizableData) cellSizeCSS() (string, string) {
w := resizable.resizeBorderWidth().cssString("4px", resizable.Session())
side := resizable.getSide()
func resizableCellSizeCSS(view View) (string, string) {
w := resizableBorderWidth(view).cssString("4px", view.Session())
side := resizableSide(view)
column := "1fr"
row := "1fr"
@ -370,7 +332,7 @@ func (resizable *resizableData) cellSizeCSS() (string, string) {
}
func (resizable *resizableData) cssStyle(self View, builder cssBuilder) {
column, row := resizable.cellSizeCSS()
column, row := resizableCellSizeCSS(resizable)
builder.add("grid-template-columns", column)
builder.add("grid-template-rows", row)
@ -380,12 +342,12 @@ func (resizable *resizableData) cssStyle(self View, builder cssBuilder) {
func (resizable *resizableData) htmlSubviews(self View, buffer *strings.Builder) {
side := resizable.getSide()
side := resizableSide(resizable)
left := 1
top := 1
leftSide := (side & LeftSide) != 0
rightSide := (side & RightSide) != 0
w := resizable.resizeBorderWidth().cssString("4px", resizable.Session())
w := resizableBorderWidth(resizable).cssString("4px", resizable.Session())
if leftSide {
left = 2
@ -462,14 +424,13 @@ func (resizable *resizableData) htmlSubviews(self View, buffer *strings.Builder)
}
}
if len(resizable.content) > 0 {
view := resizable.content[0]
if view := resizable.content(); view != nil {
view.addToCSSStyle(map[string]string{
"grid-column-start": strconv.Itoa(left),
"grid-column-end": strconv.Itoa(left + 1),
"grid-row-start": strconv.Itoa(top),
"grid-row-end": strconv.Itoa(top + 1),
})
viewHTML(view, buffer)
viewHTML(view, buffer, "")
}
}

View File

@ -1,15 +1,24 @@
package rui
// ResizeEvent is the constant for "resize-event" property tag.
// The "resize-event" is fired when the view changes its size.
// The main listener format:
//
// func(View, Frame).
// Used by View.
// Is fired when the view changes its size.
//
// The additional listener formats:
// General listener format:
//
// func(Frame), func(View), and func().
const ResizeEvent = "resize-event"
// func(view rui.View, frame rui.Frame)
//
// where:
// - view - Interface of a view which generated this event,
// - frame - New offset and size of the view's visible area.
//
// Allowed listener formats:
//
// func(frame rui.Frame)
// func(view rui.View)
// func()
const ResizeEvent PropertyName = "resize-event"
func (view *viewData) onResize(self View, x, y, width, height float64) {
view.frame.Left = x
@ -24,21 +33,20 @@ func (view *viewData) onResize(self View, x, y, width, height float64) {
func (view *viewData) onItemResize(self View, index string, x, y, width, height float64) {
}
func (view *viewData) setFrameListener(tag string, value any) bool {
listeners, ok := valueToEventListeners[View, Frame](value)
if !ok {
notCompatibleType(tag, value)
return false
/*
func setFrameListener(properties Properties, tag PropertyName, value any) bool {
if listeners, ok := valueToOneArgEventListeners[View, Frame](value); ok {
if len(listeners) == 0 {
properties.setRaw(tag, nil)
} else {
properties.setRaw(tag, listeners)
}
return true
}
if listeners == nil {
delete(view.properties, tag)
} else {
view.properties[tag] = listeners
}
view.propertyChangedEvent(tag)
return true
notCompatibleType(tag, value)
return false
}
*/
func (view *viewData) setNoResizeEvent() {
view.noResizeEvent = true
@ -67,9 +75,7 @@ func (view *viewData) Frame() Frame {
// GetViewFrame returns the size and location of view's viewport.
// If the second argument (subviewID) is not specified or it is "" then the value of the first argument (view) is returned
func GetViewFrame(view View, subviewID ...string) Frame {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
view = getSubview(view, subviewID)
if view == nil {
return Frame{}
}
@ -79,5 +85,5 @@ func GetViewFrame(view View, subviewID ...string) Frame {
// GetResizeListeners returns the list of "resize-event" listeners. If there are no listeners then the empty list is returned
// If the second argument (subviewID) is not specified or it is "" then the listeners list of the first argument (view) is returned
func GetResizeListeners(view View, subviewID ...string) []func(View, Frame) {
return getEventListeners[View, Frame](view, subviewID, ResizeEvent)
return getOneArgEventListeners[View, Frame](view, subviewID, ResizeEvent)
}

View File

@ -3,6 +3,7 @@ package rui
import (
"embed"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
@ -44,6 +45,7 @@ var resources = resourceManager{
imageSrcSets: map[string][]scaledImage{},
}
// AddEmbedResources adds embedded resources to the list of application resources
func AddEmbedResources(fs *embed.FS) {
resources.embedFS = append(resources.embedFS, fs)
rootDirs := resources.embedRootDirs(fs)
@ -329,7 +331,7 @@ func ReadRawResource(filename string) []byte {
rootDirs := resources.embedRootDirs(fs)
for _, dir := range rootDirs {
switch dir {
case imageDir, themeDir, viewDir:
case imageDir, themeDir, viewDir, stringsDir:
// do nothing
case rawDir:
@ -361,6 +363,44 @@ func ReadRawResource(filename string) []byte {
return nil
}
// OpenRawResource returns the contents of the raw resource with the specified name
func OpenRawResource(filename string) fs.File {
for _, fs := range resources.embedFS {
rootDirs := resources.embedRootDirs(fs)
for _, dir := range rootDirs {
switch dir {
case imageDir, themeDir, viewDir, stringsDir:
// do nothing
case rawDir:
if file, err := fs.Open(dir + "/" + filename); err == nil {
return file
}
default:
if file, err := fs.Open(dir + "/" + rawDir + "/" + filename); err == nil {
return file
}
}
}
}
if resources.path != "" {
if file, err := os.Open(resources.path + rawDir + "/" + filename); err == nil {
return file
}
}
if exe, err := os.Executable(); err == nil {
if file, err := os.Open(filepath.Dir(exe) + "/resources/" + rawDir + "/" + filename); err == nil {
return file
}
}
ErrorLogF(`The "%s" raw file don't found`, filename)
return nil
}
// AllRawResources returns the list of all raw resouces
func AllRawResources() []string {
result := []string{}
@ -418,6 +458,7 @@ func AllImageResources() []string {
return result
}
// AddTheme adds theme to application
func AddTheme(theme Theme) {
if theme != nil {
name := theme.Name()

View File

@ -1,15 +1,24 @@
package rui
// ScrollEvent is the constant for "scroll-event" property tag.
// The "scroll-event" is fired when the content of the view is scrolled.
// The main listener format:
//
// func(View, Frame).
// Used by View.
// Is fired when the content of the view is scrolled.
//
// The additional listener formats:
// General listener format:
//
// func(Frame), func(View), and func().
const ScrollEvent = "scroll-event"
// func(view rui.View, frame rui.Frame)
//
// where:
// - view - Interface of a view which generated this event,
// - frame - New offset and size of the view's visible area.
//
// Allowed listener formats:
//
// func(frame rui.Frame)
// func(view rui.View)
// func()
const ScrollEvent PropertyName = "scroll-event"
func (view *viewData) onScroll(self View, x, y, width, height float64) {
view.scroll.Left = x
@ -35,9 +44,7 @@ func (view *viewData) setScroll(x, y, width, height float64) {
// GetViewScroll returns ...
// If the second argument (subviewID) is not specified or it is "" then a value of the first argument (view) is returned
func GetViewScroll(view View, subviewID ...string) Frame {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
view = getSubview(view, subviewID)
if view == nil {
return Frame{}
}
@ -47,7 +54,7 @@ func GetViewScroll(view View, subviewID ...string) Frame {
// GetScrollListeners returns the list of "scroll-event" listeners. If there are no listeners then the empty list is returned
// If the second argument (subviewID) is not specified or it is "" then the listeners list of the first argument (view) is returned
func GetScrollListeners(view View, subviewID ...string) []func(View, Frame) {
return getEventListeners[View, Frame](view, subviewID, ResizeEvent)
return getOneArgEventListeners[View, Frame](view, subviewID, ResizeEvent)
}
// ScrollTo scrolls the view's content to the given position.
@ -64,10 +71,7 @@ func ScrollViewTo(view View, subviewID string, x, y float64) {
// ScrollViewToEnd scrolls the view's content to the start of view.
// If the second argument (subviewID) is not specified or it is "" then the first argument (view) is used
func ScrollViewToStart(view View, subviewID ...string) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
view.Session().callFunc("scrollToStart", view.htmlID())
}
}
@ -75,10 +79,7 @@ func ScrollViewToStart(view View, subviewID ...string) {
// ScrollViewToEnd scrolls the view's content to the end of view.
// If the second argument (subviewID) is not specified or it is "" then the first argument (view) is used
func ScrollViewToEnd(view View, subviewID ...string) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
view.Session().callFunc("scrollToEnd", view.htmlID())
}
}

View File

@ -24,6 +24,7 @@ type bridge interface {
callCanvasVarFunc(v any, funcName string, args ...any)
callCanvasImageFunc(url string, property string, funcName string, args ...any)
createCanvasVar(funcName string, args ...any) any
createPath2D(arg string) any
updateCanvasProperty(property string, value any)
canvasFinish()
canvasTextMetrics(htmlID, font, text string) TextMetrics
@ -35,6 +36,8 @@ type bridge interface {
// SessionContent is the interface of a session content
type SessionContent interface {
// CreateRootView will be called by the library to create a root view of the application
CreateRootView(session Session) View
}
@ -86,11 +89,11 @@ type Session interface {
RootView() View
// Get returns a value of the view (with id defined by the first argument) property with name defined by the second argument.
// The type of return value depends on the property. If the property is not set then nil is returned.
Get(viewID, tag string) any
Get(viewID string, tag PropertyName) any
// Set sets the value (third argument) of the property (second argument) of the view with id defined by the first argument.
// Return "true" if the value has been set, in the opposite case "false" are returned and
// a description of the error is written to the log
Set(viewID, tag string, value any) bool
Set(viewID string, tag PropertyName, value any) bool
// DownloadFile downloads (saves) on the client side the file located at the specified path on the server.
DownloadFile(path string)
@ -103,6 +106,8 @@ type Session interface {
ClientItem(key string) (string, bool)
// SetClientItem stores a key-value pair in the client-side storage
SetClientItem(key, value string)
// RemoveClientItem removes a key-value pair in the client-side storage
RemoveClientItem(key string)
// RemoveAllClientItems removes all key-value pair from the client-side storage
RemoveAllClientItems()
@ -122,14 +127,14 @@ type Session interface {
registerAnimation(props []AnimatedProperty) string
resolveConstants(value string) (string, bool)
checkboxOffImage() string
checkboxOnImage() string
checkboxOffImage(accentColor Color) string
checkboxOnImage(accentColor Color) string
radiobuttonOffImage() string
radiobuttonOnImage() string
radiobuttonOnImage(accentColor Color) string
viewByHTMLID(id string) View
nextViewID() string
styleProperty(styleTag, property string) any
styleProperty(styleTag string, propertyTag PropertyName) any
setBridge(events chan DataObject, bridge bridge)
writeInitScript(writer *strings.Builder)
@ -143,16 +148,19 @@ type Session interface {
finishUpdateScript(htmlID string)
sendResponse()
addAnimationCSS(css string)
clearAnimation()
canvasStart(htmlID string)
removeAnimation(keyframe string)
htmlPropertyValue(htmlID, name string) string
canvasStart(htmlID string) bool
callCanvasFunc(funcName string, args ...any)
createCanvasVar(funcName string, args ...any) any
createPath(arg string) any
callCanvasVarFunc(v any, funcName string, args ...any)
callCanvasImageFunc(url string, property string, funcName string, args ...any)
updateCanvasProperty(property string, value any)
canvasFinish()
canvasTextMetrics(htmlID, font, text string) TextMetrics
htmlPropertyValue(htmlID, name string) string
addToEventsQueue(data DataObject)
handleAnswer(command string, data DataObject) bool
handleRootSize(data DataObject)
@ -262,7 +270,7 @@ func (session *sessionData) close() {
}
}
func (session *sessionData) styleProperty(styleTag, propertyTag string) any {
func (session *sessionData) styleProperty(styleTag string, propertyTag PropertyName) any {
if style := session.getCurrentTheme().style(styleTag); style != nil {
return style.getRaw(propertyTag)
}
@ -321,7 +329,7 @@ func (session *sessionData) writeInitScript(writer *strings.Builder) {
writer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`)
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
viewHTML(session.rootView, buffer)
viewHTML(session.rootView, buffer, "")
text := strings.ReplaceAll(buffer.String(), "'", `\'`)
writer.WriteString(text)
writer.WriteString("';\nscanElementsSize();")
@ -352,7 +360,7 @@ func (session *sessionData) reload() {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
viewHTML(session.rootView, buffer)
viewHTML(session.rootView, buffer, "")
session.bridge.updateInnerHTML("ruiRootView", buffer.String())
session.bridge.callFunc("scanElementsSize")
}
@ -368,14 +376,14 @@ func (session *sessionData) setIgnoreViewUpdates(ignore bool) {
session.ignoreUpdates = ignore
}
func (session *sessionData) Get(viewID, tag string) any {
func (session *sessionData) Get(viewID string, tag PropertyName) any {
if view := ViewByID(session.RootView(), viewID); view != nil {
return view.Get(tag)
}
return nil
}
func (session *sessionData) Set(viewID, tag string, value any) bool {
func (session *sessionData) Set(viewID string, tag PropertyName, value any) bool {
if view := ViewByID(session.RootView(), viewID); view != nil {
return view.Set(tag, value)
}
@ -427,20 +435,32 @@ func (session *sessionData) appendToInnerHTML(htmlID, html string) {
}
func (session *sessionData) updateCSSProperty(htmlID, property, value string) {
if !session.ignoreViewUpdates() && session.bridge != nil {
session.bridge.updateCSSProperty(htmlID, property, value)
if !session.ignoreViewUpdates() {
if session.bridge != nil {
session.bridge.updateCSSProperty(htmlID, property, value)
} else {
ErrorLog("No connection")
}
}
}
func (session *sessionData) updateProperty(htmlID, property string, value any) {
if !session.ignoreViewUpdates() && session.bridge != nil {
session.bridge.updateProperty(htmlID, property, value)
if !session.ignoreViewUpdates() {
if session.bridge != nil {
session.bridge.updateProperty(htmlID, property, value)
} else {
ErrorLog("No connection")
}
}
}
func (session *sessionData) removeProperty(htmlID, property string) {
if !session.ignoreViewUpdates() && session.bridge != nil {
session.bridge.removeProperty(htmlID, property)
if !session.ignoreViewUpdates() {
if session.bridge != nil {
session.bridge.removeProperty(htmlID, property)
} else {
ErrorLog("No connection")
}
}
}
@ -448,37 +468,82 @@ func (session *sessionData) startUpdateScript(htmlID string) bool {
if session.bridge != nil {
return session.bridge.startUpdateScript(htmlID)
}
ErrorLog("No connection")
return false
}
func (session *sessionData) finishUpdateScript(htmlID string) {
if session.bridge != nil {
session.bridge.finishUpdateScript(htmlID)
} else {
ErrorLog("No connection")
}
}
func (session *sessionData) sendResponse() {
if session.bridge != nil {
session.bridge.sendResponse()
} else {
ErrorLog("No connection")
}
}
func (session *sessionData) addAnimationCSS(css string) {
session.animationCSS += css
if session.bridge != nil {
session.bridge.appendAnimationCSS(css)
} else {
ErrorLog("No connection")
}
}
func (session *sessionData) clearAnimation() {
if session.bridge != nil {
session.bridge.setAnimationCSS("")
func (session *sessionData) removeAnimation(keyframe string) {
css := session.animationCSS
index := strings.Index(css, "@keyframes "+keyframe)
if index < 0 {
return
}
start := strings.IndexRune(css[index:], '{')
if start < 0 {
return
}
n := 1
end := -1
for i := start + index + 1; i < len(css); i++ {
if css[i] == '}' {
n--
if n == 0 {
end = i + 1
if end < len(css) && css[end] == '\n' {
end++
}
break
}
} else if css[i] == '{' {
n++
}
}
if end > index {
session.animationCSS = strings.Trim(css[:index]+css[end:], "\n")
if session.bridge != nil {
session.bridge.setAnimationCSS(session.animationCSS)
} else {
ErrorLog("No connection")
}
}
}
func (session *sessionData) canvasStart(htmlID string) {
func (session *sessionData) canvasStart(htmlID string) bool {
if session.bridge != nil {
session.bridge.canvasStart(htmlID)
return true
}
ErrorLog("No connection")
return false
}
func (session *sessionData) callCanvasFunc(funcName string, args ...any) {
@ -500,6 +565,13 @@ func (session *sessionData) createCanvasVar(funcName string, args ...any) any {
return nil
}
func (session *sessionData) createPath(arg string) any {
if session.bridge != nil {
return session.bridge.createPath2D(arg)
}
return nil
}
func (session *sessionData) callCanvasVarFunc(v any, funcName string, args ...any) {
if session.bridge != nil && v != nil {
session.bridge.callCanvasVarFunc(v, funcName, args...)
@ -555,6 +627,8 @@ func (session *sessionData) handleAnswer(command string, data DataObject) bool {
if session.bridge != nil {
session.bridge.sendResponse()
} else {
ErrorLog("No connection")
}
return true
}
@ -711,10 +785,10 @@ func (session *sessionData) handleEvent(command string, data DataObject) {
if viewID, ok := data.PropertyValue("id"); ok {
if viewID != "body" {
if view := session.viewByHTMLID(viewID); view != nil {
view.handleCommand(view, command, data)
view.handleCommand(view, PropertyName(command), data)
}
}
if command == KeyDownEvent {
if command == string(KeyDownEvent) {
var event KeyEvent
event.init(data)
session.hotKey(event)
@ -818,6 +892,11 @@ func (session *sessionData) SetClientItem(key, value string) {
session.bridge.callFunc("localStorageSet", key, value)
}
func (session *sessionData) RemoveClientItem(key string) {
delete(session.clientStorage, key)
session.bridge.callFunc("localStorageRemove", key)
}
func (session *sessionData) RemoveAllClientItems() {
session.clientStorage = map[string]string{}
session.bridge.callFunc("localStorageClear")

View File

@ -4,31 +4,42 @@ import "time"
// SessionStartListener is the listener interface of a session start event
type SessionStartListener interface {
// OnStart is a function that is called by the library after the creation of the root view of the application
OnStart(session Session)
}
// SessionFinishListener is the listener interface of a session start event
type SessionFinishListener interface {
// OnFinish is a function that is called by the library when the user closes the application page in the browser
OnFinish(session Session)
}
// SessionResumeListener is the listener interface of a session resume event
type SessionResumeListener interface {
// OnResume is a function that is called by the library when the application page in the client's browser becomes
// active and is also called immediately after OnStart
OnResume(session Session)
}
// SessionPauseListener is the listener interface of a session pause event
type SessionPauseListener interface {
// OnPause is a function that is called by the library when the application page in the client's browser becomes
// inactive and is also called when the user switches to a different browser tab/window, minimizes the browser,
// or switches to another application
OnPause(session Session)
}
// SessionPauseListener is the listener interface of a session disconnect event
type SessionDisconnectListener interface {
// OnDisconnect is a function that is called by the library if the server loses connection with the client and
// this happens when the connection is broken
OnDisconnect(session Session)
}
// SessionPauseListener is the listener interface of a session reconnect event
type SessionReconnectListener interface {
// OnReconnect is a function that is called by the library after the server reconnects with the client
// and this happens when the connection is restored
OnReconnect(session Session)
}

View File

@ -203,7 +203,7 @@ func (session *sessionData) SetCustomTheme(name string) bool {
const checkImage = `<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m4 8 3 4 5-8" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"/></svg>`
func (session *sessionData) checkboxImage(checked bool) string {
func (session *sessionData) checkboxImage(checked bool, accentColor Color) string {
var borderColor, backgroundColor Color
var ok bool
@ -217,7 +217,9 @@ func (session *sessionData) checkboxImage(checked bool) string {
}
if checked {
if backgroundColor, ok = session.Color("ruiHighlightColor"); !ok {
if accentColor != 0 {
backgroundColor = accentColor
} else if backgroundColor, ok = session.Color("ruiHighlightColor"); !ok {
backgroundColor = 0xFF1A74E8
}
} else if backgroundColor, ok = session.Color("ruiBackgroundColor"); !ok {
@ -244,16 +246,22 @@ func (session *sessionData) checkboxImage(checked bool) string {
return buffer.String()
}
func (session *sessionData) checkboxOffImage() string {
func (session *sessionData) checkboxOffImage(accentColor Color) string {
if accentColor != 0 {
return session.checkboxImage(false, accentColor)
}
if session.checkboxOff == "" {
session.checkboxOff = session.checkboxImage(false)
session.checkboxOff = session.checkboxImage(false, accentColor)
}
return session.checkboxOff
}
func (session *sessionData) checkboxOnImage() string {
func (session *sessionData) checkboxOnImage(accentColor Color) string {
if accentColor != 0 {
return session.checkboxImage(true, accentColor)
}
if session.checkboxOn == "" {
session.checkboxOn = session.checkboxImage(true)
session.checkboxOn = session.checkboxImage(true, accentColor)
}
return session.checkboxOn
}
@ -285,12 +293,14 @@ func (session *sessionData) radiobuttonOffImage() string {
return session.radiobuttonOff
}
func (session *sessionData) radiobuttonOnImage() string {
func (session *sessionData) radiobuttonOnImage(accentColor Color) string {
if session.radiobuttonOn == "" {
var borderColor, backgroundColor Color
var ok bool
if borderColor, ok = session.Color("ruiHighlightColor"); !ok {
if accentColor != 0 {
borderColor = accentColor
} else if borderColor, ok = session.Color("ruiHighlightColor"); !ok {
borderColor = 0xFF1A74E8
}
@ -313,7 +323,7 @@ func (session *sessionData) Language() string {
return session.language
}
if session.languages != nil && len(session.languages) > 0 {
if len(session.languages) > 0 {
return session.languages[0]
}
@ -329,7 +339,7 @@ func (session *sessionData) SetLanguage(lang string) {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
viewHTML(session.rootView, buffer)
viewHTML(session.rootView, buffer, "")
session.bridge.updateInnerHTML("ruiRootView", buffer.String())
}
}

283
shadow.go
View File

@ -5,30 +5,110 @@ import (
"strings"
)
// Constants for [ShadowProperty] specific properties
const (
// ColorTag is the name of the color property of the shadow.
ColorTag = "color"
// Inset is the name of bool property of the shadow. If it is set to "false" (default) then the shadow
// is assumed to be a drop shadow (as if the box were raised above the content).
// If it is set to "true" then the shadow to one inside the frame (as if the content was depressed inside the box).
// Inset shadows are drawn inside the border (even transparent ones), above the background, but below content.
Inset = "inset"
// XOffset is the name of the SizeUnit property of the shadow that determines the shadow horizontal offset.
// Negative values place the shadow to the left of the element.
XOffset = "x-offset"
// YOffset is the name of the SizeUnit property of the shadow that determines the shadow vertical offset.
// Negative values place the shadow above the element.
YOffset = "y-offset"
// BlurRadius is the name of the SizeUnit property of the shadow that determines the radius of the blur effect.
// The larger this value, the bigger the blur, so the shadow becomes bigger and lighter. Negative values are not allowed.
BlurRadius = "blur"
// SpreadRadius is the name of the SizeUnit property of the shadow. Positive values will cause the shadow to expand
// and grow bigger, negative values will cause the shadow to shrink.
SpreadRadius = "spread-radius"
// ColorTag is the constant for "color" property tag.
//
// Used by ColumnSeparatorProperty, BorderProperty, OutlineProperty, ShadowProperty.
//
// # Usage in ColumnSeparatorProperty
//
// Line color.
//
// Supported types: Color, string.
//
// Internal type is Color, other types converted to it during assignment.
// See Color description for more details.
//
// # Usage in BorderProperty
//
// Border line color.
//
// Supported types: Color, string.
//
// Internal type is Color, other types converted to it during assignment.
// See Color description for more details.
//
// # Usage in OutlineProperty
//
// Outline line color.
//
// Supported types: Color, string.
//
// Internal type is Color, other types converted to it during assignment.
// See Color description for more details.
//
// # Usage in ShadowProperty
//
// Color property of the shadow.
//
// Supported types: Color, string.
//
// Internal type is Color, other types converted to it during assignment.
// See Color description for more details.
ColorTag PropertyName = "color"
// Inset is the constant for "inset" property tag.
//
// Used by ShadowProperty.
// Controls whether to draw shadow inside the frame or outside. Inset shadows are drawn inside the border(even transparent
// ones), above the background, but below content.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", "1" - Drop shadow inside the frame(as if the content was depressed inside the box).
// - false, 0, "false", "no", "off", "0" - Shadow is assumed to be a drop shadow(as if the box were raised above the content).
Inset PropertyName = "inset"
// XOffset is the constant for "x-offset" property tag.
//
// Used by ShadowProperty.
// Determines the shadow horizontal offset. Negative values place the shadow to the left of the element.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
XOffset PropertyName = "x-offset"
// YOffset is the constant for "y-offset" property tag.
//
// Used by ShadowProperty.
// Determines the shadow vertical offset. Negative values place the shadow above the element.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
YOffset PropertyName = "y-offset"
// BlurRadius is the constant for "blur" property tag.
//
// Used by ShadowProperty.
// Determines the radius of the blur effect. The larger this value, the bigger the blur, so the shadow becomes bigger and
// lighter. Negative values are not allowed.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
BlurRadius PropertyName = "blur"
// SpreadRadius is the constant for "spread-radius" property tag.
//
// Used by ShadowProperty.
// Positive values will cause the shadow to expand and grow bigger, negative values will cause the shadow to shrink.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
SpreadRadius PropertyName = "spread-radius"
)
// ViewShadow contains attributes of the view shadow
type ViewShadow interface {
// ShadowProperty contains attributes of the view shadow
type ShadowProperty interface {
Properties
fmt.Stringer
stringWriter
@ -37,34 +117,36 @@ type ViewShadow interface {
visible(session Session) bool
}
type viewShadowData struct {
propertyList
type shadowPropertyData struct {
dataProperty
}
// NewViewShadow create the new shadow for a view. Arguments:
// offsetX, offsetY - x and y offset of the shadow
// blurRadius - the blur radius of the shadow
// spreadRadius - the spread radius of the shadow
// color - the color of the shadow
func NewViewShadow(offsetX, offsetY, blurRadius, spreadRadius SizeUnit, color Color) ViewShadow {
return NewShadowWithParams(Params{
XOffset: offsetX,
YOffset: offsetY,
// NewShadow create the new shadow property for a view. Arguments:
// - offsetX, offsetY is x and y offset of the shadow (if the argument is specified as int or float64, the value is considered to be in pixels);
// - blurRadius is the blur radius of the shadow (if the argument is specified as int or float64, the value is considered to be in pixels);
// - spreadRadius is the spread radius of the shadow (if the argument is specified as int or float64, the value is considered to be in pixels);
// - color is the color of the shadow.
func NewShadow[xOffsetType SizeUnit | int | float64, yOffsetType SizeUnit | int | float64, blurType SizeUnit | int | float64, spreadType SizeUnit | int | float64](
xOffset xOffsetType, yOffset yOffsetType, blurRadius blurType, spreadRadius spreadType, color Color) ShadowProperty {
return NewShadowProperty(Params{
XOffset: xOffset,
YOffset: yOffset,
BlurRadius: blurRadius,
SpreadRadius: spreadRadius,
ColorTag: color,
})
}
// NewInsetViewShadow create the new inset shadow for a view. Arguments:
// offsetX, offsetY - x and y offset of the shadow
// blurRadius - the blur radius of the shadow
// spreadRadius - the spread radius of the shadow
// color - the color of the shadow
func NewInsetViewShadow(offsetX, offsetY, blurRadius, spreadRadius SizeUnit, color Color) ViewShadow {
return NewShadowWithParams(Params{
XOffset: offsetX,
YOffset: offsetY,
// NewInsetShadow create the new inset shadow property for a view. Arguments:
// - offsetX, offsetY is x and y offset of the shadow (if the argument is specified as int or float64, the value is considered to be in pixels);
// - blurRadius is the blur radius of the shadow (if the argument is specified as int or float64, the value is considered to be in pixels);
// - spreadRadius is the spread radius of the shadow (if the argument is specified as int or float64, the value is considered to be in pixels);
// - color is the color of the shadow.
func NewInsetShadow[xOffsetType SizeUnit | int | float64, yOffsetType SizeUnit | int | float64, blurType SizeUnit | int | float64, spreadType SizeUnit | int | float64](
xOffset xOffsetType, yOffset yOffsetType, blurRadius blurType, spreadRadius spreadType, color Color) ShadowProperty {
return NewShadowProperty(Params{
XOffset: xOffset,
YOffset: yOffset,
BlurRadius: blurRadius,
SpreadRadius: spreadRadius,
ColorTag: color,
@ -72,66 +154,57 @@ func NewInsetViewShadow(offsetX, offsetY, blurRadius, spreadRadius SizeUnit, col
})
}
// NewTextShadow create the new text shadow. Arguments:
// offsetX, offsetY - x and y offset of the shadow
// blurRadius - the blur radius of the shadow
// color - the color of the shadow
func NewTextShadow(offsetX, offsetY, blurRadius SizeUnit, color Color) ViewShadow {
return NewShadowWithParams(Params{
XOffset: offsetX,
YOffset: offsetY,
// NewTextShadow create the new text shadow property. Arguments:
// - offsetX, offsetY is the x- and y-offset of the shadow (if the argument is specified as int or float64, the value is considered to be in pixels);
// - blurRadius is the blur radius of the shadow (if the argument is specified as int or float64, the value is considered to be in pixels);
// - color is the color of the shadow.
func NewTextShadow[xOffsetType SizeUnit | int | float64, yOffsetType SizeUnit | int | float64, blurType SizeUnit | int | float64](
xOffset xOffsetType, yOffset yOffsetType, blurRadius blurType, color Color) ShadowProperty {
return NewShadowProperty(Params{
XOffset: xOffset,
YOffset: yOffset,
BlurRadius: blurRadius,
ColorTag: color,
})
}
// NewShadowWithParams create the new shadow for a view.
func NewShadowWithParams(params Params) ViewShadow {
shadow := new(viewShadowData)
shadow.propertyList.init()
// NewShadowProperty create the new shadow property for a view.
//
// The following properties can be used:
// - "color" (ColorTag). Determines the color of the shadow (Color);
// - "x-offset" (XOffset). Determines the shadow horizontal offset (SizeUnit);
// - "y-offset" (YOffset). Determines the shadow vertical offset (SizeUnit);
// - "blur" (BlurRadius). Determines the radius of the blur effect (SizeUnit);
// - "spread-radius" (SpreadRadius). Positive values (SizeUnit) will cause the shadow to expand and grow bigger, negative values will cause the shadow to shrink;
// - "inset" (Inset). Controls (bool) whether to draw shadow inside the frame or outside.
func NewShadowProperty(params Params) ShadowProperty {
shadow := new(shadowPropertyData)
shadow.init()
if params != nil {
for _, tag := range []string{ColorTag, Inset, XOffset, YOffset, BlurRadius, SpreadRadius} {
for _, tag := range []PropertyName{ColorTag, Inset, XOffset, YOffset, BlurRadius, SpreadRadius} {
if value, ok := params[tag]; ok && value != nil {
shadow.Set(tag, value)
shadow.set(shadow, tag, value)
}
}
}
return shadow
}
// parseViewShadow parse DataObject and create ViewShadow object
func parseViewShadow(object DataObject) ViewShadow {
shadow := new(viewShadowData)
shadow.propertyList.init()
// parseShadowProperty parse DataObject and create ShadowProperty object
func parseShadowProperty(object DataObject) ShadowProperty {
shadow := new(shadowPropertyData)
shadow.init()
parseProperties(shadow, object)
return shadow
}
func (shadow *viewShadowData) Remove(tag string) {
delete(shadow.properties, strings.ToLower(tag))
func (shadow *shadowPropertyData) init() {
shadow.dataProperty.init()
shadow.supportedProperties = []PropertyName{ColorTag, Inset, XOffset, YOffset, BlurRadius, SpreadRadius}
}
func (shadow *viewShadowData) Set(tag string, value any) bool {
if value == nil {
shadow.Remove(tag)
return true
}
tag = strings.ToLower(tag)
switch tag {
case ColorTag, Inset, XOffset, YOffset, BlurRadius, SpreadRadius:
return shadow.propertyList.Set(tag, value)
}
ErrorLogF(`"%s" property is not supported by Shadow`, tag)
return false
}
func (shadow *viewShadowData) Get(tag string) any {
return shadow.propertyList.Get(strings.ToLower(tag))
}
func (shadow *viewShadowData) cssStyle(buffer *strings.Builder, session Session, lead string) bool {
func (shadow *shadowPropertyData) cssStyle(buffer *strings.Builder, session Session, lead string) bool {
color, _ := colorProperty(shadow, ColorTag, session)
offsetX, _ := sizeProperty(shadow, XOffset, session)
offsetY, _ := sizeProperty(shadow, YOffset, session)
@ -163,7 +236,7 @@ func (shadow *viewShadowData) cssStyle(buffer *strings.Builder, session Session,
return true
}
func (shadow *viewShadowData) cssTextStyle(buffer *strings.Builder, session Session, lead string) bool {
func (shadow *shadowPropertyData) cssTextStyle(buffer *strings.Builder, session Session, lead string) bool {
color, _ := colorProperty(shadow, ColorTag, session)
offsetX, _ := sizeProperty(shadow, XOffset, session)
offsetY, _ := sizeProperty(shadow, YOffset, session)
@ -187,7 +260,7 @@ func (shadow *viewShadowData) cssTextStyle(buffer *strings.Builder, session Sess
return true
}
func (shadow *viewShadowData) visible(session Session) bool {
func (shadow *shadowPropertyData) visible(session Session) bool {
color, _ := colorProperty(shadow, ColorTag, session)
offsetX, _ := sizeProperty(shadow, XOffset, session)
offsetY, _ := sizeProperty(shadow, YOffset, session)
@ -204,11 +277,11 @@ func (shadow *viewShadowData) visible(session Session) bool {
return true
}
func (shadow *viewShadowData) String() string {
func (shadow *shadowPropertyData) String() string {
return runStringWriter(shadow)
}
func (shadow *viewShadowData) writeString(buffer *strings.Builder, indent string) {
func (shadow *shadowPropertyData) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("_{ ")
comma := false
for _, tag := range shadow.AllTags() {
@ -216,7 +289,7 @@ func (shadow *viewShadowData) writeString(buffer *strings.Builder, indent string
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
writePropertyValue(buffer, tag, value, indent)
comma = true
@ -225,41 +298,41 @@ func (shadow *viewShadowData) writeString(buffer *strings.Builder, indent string
buffer.WriteString(" }")
}
func (properties *propertyList) setShadow(tag string, value any) bool {
func setShadowProperty(properties Properties, tag PropertyName, value any) bool {
if value == nil {
delete(properties.properties, tag)
properties.setRaw(tag, nil)
return true
}
switch value := value.(type) {
case ViewShadow:
properties.properties[tag] = []ViewShadow{value}
case ShadowProperty:
properties.setRaw(tag, []ShadowProperty{value})
case []ViewShadow:
case []ShadowProperty:
if len(value) == 0 {
delete(properties.properties, tag)
properties.setRaw(tag, nil)
} else {
properties.properties[tag] = value
properties.setRaw(tag, value)
}
case DataValue:
if !value.IsObject() {
return false
}
properties.properties[tag] = []ViewShadow{parseViewShadow(value.Object())}
properties.setRaw(tag, []ShadowProperty{parseShadowProperty(value.Object())})
case []DataValue:
shadows := []ViewShadow{}
shadows := []ShadowProperty{}
for _, data := range value {
if data.IsObject() {
shadows = append(shadows, parseViewShadow(data.Object()))
shadows = append(shadows, parseShadowProperty(data.Object()))
}
}
if len(shadows) == 0 {
return false
}
properties.properties[tag] = shadows
properties.setRaw(tag, shadows)
case string:
obj := NewDataObject(value)
@ -267,7 +340,7 @@ func (properties *propertyList) setShadow(tag string, value any) bool {
notCompatibleType(tag, value)
return false
}
properties.properties[tag] = []ViewShadow{parseViewShadow(obj)}
properties.setRaw(tag, []ShadowProperty{parseShadowProperty(obj)})
default:
notCompatibleType(tag, value)
@ -277,20 +350,20 @@ func (properties *propertyList) setShadow(tag string, value any) bool {
return true
}
func getShadows(properties Properties, tag string) []ViewShadow {
func getShadows(properties Properties, tag PropertyName) []ShadowProperty {
if value := properties.Get(tag); value != nil {
switch value := value.(type) {
case []ViewShadow:
case []ShadowProperty:
return value
case ViewShadow:
return []ViewShadow{value}
case ShadowProperty:
return []ShadowProperty{value}
}
}
return []ViewShadow{}
return []ShadowProperty{}
}
func shadowCSS(properties Properties, tag string, session Session) string {
func shadowCSS(properties Properties, tag PropertyName, session Session) string {
shadows := getShadows(properties, tag)
if len(shadows) == 0 {
return ""

View File

@ -7,11 +7,15 @@ import (
)
// SizeFunc describes a function that calculates the SizeUnit size.
//
// Used as the value of the SizeUnit properties.
// "min", "max", "clamp", "sum", "sub", "mul", and "div" functions are available.
//
// "min", "max", "clamp", "sum", "sub", "mul", "div", mod,
// "round", "round-up", "round-down" and "round-to-zero" functions are available.
type SizeFunc interface {
fmt.Stringer
// Name() returns the function name: "min", "max", "clamp", "sum", "sub", "mul", or "div"
// Name() returns the function name: "min", "max", "clamp", "sum", "sub", "mul",
// "div", "mod", "rem", "round", "round-up", "round-down" or "round-to-zero"
Name() string
// Args() returns a list of function arguments
Args() []any
@ -28,7 +32,9 @@ type sizeFuncData struct {
func parseSizeFunc(text string) SizeFunc {
text = strings.Trim(text, " ")
for _, tag := range []string{"min", "max", "sum", "sub", "mul", "div", "clamp"} {
for _, tag := range []string{
"min", "max", "sum", "sub", "mul", "div", "mod", "rem", "clamp",
"round-up", "round-down", "round-to-zero", "round"} {
if strings.HasPrefix(text, tag) {
text = strings.Trim(strings.TrimPrefix(text, tag), " ")
last := len(text) - 1
@ -59,7 +65,7 @@ func parseSizeFunc(text string) SizeFunc {
args = append(args, text[start:])
switch tag {
case "sub", "mul", "div":
case "sub", "mul", "div", "mod", "rem", "round-up", "round-down", "round-to-zero", "round":
if len(args) != 2 {
ErrorLogF(`"%s" function needs 2 arguments`, tag)
return nil
@ -73,7 +79,9 @@ func parseSizeFunc(text string) SizeFunc {
data := new(sizeFuncData)
data.tag = tag
if data.parseArgs(args, tag == "mul" || tag == "div") {
if data.parseArgs(args, tag == "mul" || tag == "div" || tag == "mod" ||
tag == "rem" || tag == "round-up" || tag == "round-down" ||
tag == "round-to-zero" || tag == "round") {
return data
}
}
@ -92,19 +100,25 @@ func (data *sizeFuncData) parseArgs(args []any, allowNumber bool) bool {
numberArg := func(index int, value float64) bool {
if allowNumber {
if index == 1 {
if value == 0 && data.tag == "div" {
ErrorLog(`Division by 0 in div function`)
return false
if value == 0 {
if data.tag == "div" || data.tag == "mod" {
ErrorLogF(`Division by 0 in "%s" function`, data.tag)
return false
}
if data.tag == "round" || data.tag == "round-up" ||
data.tag == "round-down" || data.tag == "round-to-zero" {
ErrorLogF(`The rounding interval is 0 in "%s" function`, data.tag)
return false
}
}
data.args = append(data.args, value)
return true
} else {
ErrorLogF(`Only the second %s function argument can be a number`, data.tag)
return false
}
} else {
ErrorLogF(`The %s function argument can't be a number`, data.tag)
}
ErrorLogF(`The %s function argument can't be a number`, data.tag)
return false
}
@ -219,7 +233,7 @@ func (data *sizeFuncData) writeCSS(topFunc string, buffer *strings.Builder, sess
case "":
buffer.WriteString("calc(")
case "min", "max", "clamp":
case "min", "max", "clamp", "mod", "rem", "round", "round-up", "round-down", "round-to-zero":
bracket = false
default:
@ -228,10 +242,22 @@ func (data *sizeFuncData) writeCSS(topFunc string, buffer *strings.Builder, sess
}
switch data.tag {
case "min", "max", "clamp":
case "min", "max", "clamp", "mod", "rem":
buffer.WriteString(data.tag)
buffer.WriteRune('(')
case "round":
buffer.WriteString("round(nearest, ")
case "round-up":
buffer.WriteString("round(up, ")
case "round-down":
buffer.WriteString("round(down, ")
case "round-to-zero":
buffer.WriteString("round(to-zero, ")
case "sum":
mathFunc(" + ")
@ -287,7 +313,7 @@ func (data *sizeFuncData) writeCSS(topFunc string, buffer *strings.Builder, sess
}
// MaxSize creates a SizeUnit function that calculates the maximum argument.
// Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc
func MaxSize(arg0, arg1 any, args ...any) SizeFunc {
data := new(sizeFuncData)
data.tag = "max"
@ -298,7 +324,7 @@ func MaxSize(arg0, arg1 any, args ...any) SizeFunc {
}
// MinSize creates a SizeUnit function that calculates the minimum argument.
// Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
func MinSize(arg0, arg1 any, args ...any) SizeFunc {
data := new(sizeFuncData)
data.tag = "min"
@ -309,7 +335,7 @@ func MinSize(arg0, arg1 any, args ...any) SizeFunc {
}
// SumSize creates a SizeUnit function that calculates the sum of arguments.
// Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
func SumSize(arg0, arg1 any, args ...any) SizeFunc {
data := new(sizeFuncData)
data.tag = "sum"
@ -320,7 +346,7 @@ func SumSize(arg0, arg1 any, args ...any) SizeFunc {
}
// SumSize creates a SizeUnit function that calculates the result of subtracting the arguments (arg1 - arg2).
// Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
func SubSize(arg0, arg1 any) SizeFunc {
data := new(sizeFuncData)
data.tag = "sub"
@ -331,7 +357,7 @@ func SubSize(arg0, arg1 any) SizeFunc {
}
// MulSize creates a SizeUnit function that calculates the result of multiplying the arguments (arg1 * arg2).
// Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64)
// or a string which is a text representation of a number.
func MulSize(arg0, arg1 any) SizeFunc {
@ -344,7 +370,7 @@ func MulSize(arg0, arg1 any) SizeFunc {
}
// DivSize creates a SizeUnit function that calculates the result of dividing the arguments (arg1 / arg2).
// Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64)
// or a string which is a text representation of a number.
func DivSize(arg0, arg1 any) SizeFunc {
@ -356,13 +382,103 @@ func DivSize(arg0, arg1 any) SizeFunc {
return data
}
// RemSize creates a SizeUnit function that calculates the remainder of a division operation
// with the same sign as the dividend (arg1 % arg2).
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64)
// or a string which is a text representation of a number.
func RemSize(arg0, arg1 any) SizeFunc {
data := new(sizeFuncData)
data.tag = "rem"
if !data.parseArgs([]any{arg0, arg1}, true) {
return nil
}
return data
}
// ModSize creates a SizeUnit function that calculates the remainder of a division operation
// with the same sign as the divisor (arg1 % arg2).
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64)
// or a string which is a text representation of a number.
func ModSize(arg0, arg1 any) SizeFunc {
data := new(sizeFuncData)
data.tag = "mod"
if !data.parseArgs([]any{arg0, arg1}, true) {
return nil
}
return data
}
// RoundSize creates a SizeUnit function that calculates a rounded number.
// The function rounds valueToRound (first argument) to the nearest integer multiple
// of roundingInterval (second argument), which may be either above or below the value.
// If the valueToRound is half way between the rounding targets above and below (neither is "nearest"), it will be rounded up.
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64)
// or a string which is a text representation of a number.
func RoundSize(valueToRound, roundingInterval any) SizeFunc {
data := new(sizeFuncData)
data.tag = "round"
if !data.parseArgs([]any{valueToRound, roundingInterval}, true) {
return nil
}
return data
}
// RoundUpSize creates a SizeUnit function that calculates a rounded number.
// The function rounds valueToRound (first argument) up to the nearest integer multiple
// of roundingInterval (second argument) (if the value is negative, it will become "more positive").
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64)
// or a string which is a text representation of a number.
func RoundUpSize(valueToRound, roundingInterval any) SizeFunc {
data := new(sizeFuncData)
data.tag = "round-up"
if !data.parseArgs([]any{valueToRound, roundingInterval}, true) {
return nil
}
return data
}
// RoundDownSize creates a SizeUnit function that calculates a rounded number.
// The function rounds valueToRound (first argument) down to the nearest integer multiple
// of roundingInterval (second argument) (if the value is negative, it will become "more negative").
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64)
// or a string which is a text representation of a number.
func RoundDownSize(valueToRound, roundingInterval any) SizeFunc {
data := new(sizeFuncData)
data.tag = "round-down"
if !data.parseArgs([]any{valueToRound, roundingInterval}, true) {
return nil
}
return data
}
// RoundToZeroSize creates a SizeUnit function that calculates a rounded number.
// The function rounds valueToRound (first argument) to the nearest integer multiple
// of roundingInterval (second argument), which may be either above or below the value.
// If the valueToRound is half way between the rounding targets above and below.
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64)
// or a string which is a text representation of a number.
func RoundToZeroSize(valueToRound, roundingInterval any) SizeFunc {
data := new(sizeFuncData)
data.tag = "round-to-zero"
if !data.parseArgs([]any{valueToRound, roundingInterval}, true) {
return nil
}
return data
}
// ClampSize creates a SizeUnit function whose the result is calculated as follows:
//
// min ≤ value ≤ max -> value;
// value < min -> min;
// max < value -> max;
//
// Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
func ClampSize(min, value, max any) SizeFunc {
data := new(sizeFuncData)
data.tag = "clamp"

View File

@ -13,6 +13,7 @@ import (
// SizeInDIP, SizeInPt, SizeInInch, SizeInMM, SizeInFraction
type SizeUnitType uint8
// Constants which represent values of a [SizeUnitType]
const (
// Auto is the SizeUnit type: default value.
Auto SizeUnitType = 0
@ -44,8 +45,14 @@ const (
// SizeUnit describe a size (Value field) and size unit (Type field).
type SizeUnit struct {
Type SizeUnitType
Value float64
// Type or dimension of the value
Type SizeUnitType
// Value of the size in Type units
Value float64
// Function representation of a size unit.
// When setting this value type should be set to SizeFunction
Function SizeFunc
}
@ -55,53 +62,53 @@ func AutoSize() SizeUnit {
}
// Px creates SizeUnit with SizeInPixel type
func Px(value float64) SizeUnit {
return SizeUnit{SizeInPixel, value, nil}
func Px[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInPixel, Value: float64(value), Function: nil}
}
// Em creates SizeUnit with SizeInEM type
func Em(value float64) SizeUnit {
return SizeUnit{SizeInEM, value, nil}
func Em[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInEM, Value: float64(value), Function: nil}
}
// Ex creates SizeUnit with SizeInEX type
func Ex(value float64) SizeUnit {
return SizeUnit{SizeInEX, value, nil}
func Ex[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInEX, Value: float64(value), Function: nil}
}
// Percent creates SizeUnit with SizeInDIP type
func Percent(value float64) SizeUnit {
return SizeUnit{SizeInPercent, value, nil}
func Percent[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInPercent, Value: float64(value), Function: nil}
}
// Pt creates SizeUnit with SizeInPt type
func Pt(value float64) SizeUnit {
return SizeUnit{SizeInPt, value, nil}
func Pt[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInPt, Value: float64(value), Function: nil}
}
// Pc creates SizeUnit with SizeInPc type
func Pc(value float64) SizeUnit {
return SizeUnit{SizeInPc, value, nil}
func Pc[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInPc, Value: float64(value), Function: nil}
}
// Mm creates SizeUnit with SizeInMM type
func Mm(value float64) SizeUnit {
return SizeUnit{SizeInMM, value, nil}
func Mm[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInMM, Value: float64(value), Function: nil}
}
// Cm creates SizeUnit with SizeInCM type
func Cm(value float64) SizeUnit {
return SizeUnit{SizeInCM, value, nil}
func Cm[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInCM, Value: float64(value), Function: nil}
}
// Inch creates SizeUnit with SizeInInch type
func Inch(value float64) SizeUnit {
return SizeUnit{SizeInInch, value, nil}
func Inch[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInInch, Value: float64(value), Function: nil}
}
// Fr creates SizeUnit with SizeInFraction type
func Fr(value float64) SizeUnit {
return SizeUnit{SizeInFraction, value, nil}
func Fr[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{SizeInFraction, float64(value), nil}
}
// Equal compare two SizeUnit. Return true if SizeUnit are equal

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ import (
"strings"
)
// SvgImageView - image View
// SvgImageView represents an SvgImageView view
type SvgImageView interface {
View
}
@ -25,7 +25,7 @@ func NewSvgImageView(session Session, params Params) SvgImageView {
}
func newSvgImageView(session Session) View {
return NewSvgImageView(session, nil)
return new(svgImageViewData) // NewSvgImageView(session, nil)
}
// Init initialize fields of imageView by default values
@ -33,14 +33,14 @@ func (imageView *svgImageViewData) init(session Session) {
imageView.viewData.init(session)
imageView.tag = "SvgImageView"
imageView.systemClass = "ruiSvgImageView"
imageView.normalize = normalizeSvgImageViewTag
imageView.set = imageView.setFunc
imageView.changed = imageView.propertyChanged
}
func (imageView *svgImageViewData) String() string {
return getViewString(imageView, nil)
}
func (imageView *svgImageViewData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeSvgImageViewTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Source, "source":
tag = Content
@ -54,57 +54,63 @@ func (imageView *svgImageViewData) normalizeTag(tag string) string {
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
}
func (imageView *svgImageViewData) setFunc(tag PropertyName, value any) []PropertyName {
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
imageView.setRaw(Content, text)
return []PropertyName{tag}
}
notCompatibleType(Source, value)
return false
return nil
default:
return imageView.viewData.set(tag, value)
return imageView.viewData.setFunc(tag, value)
}
}
func (imageView *svgImageViewData) Get(tag string) any {
return imageView.viewData.get(imageView.normalizeTag(tag))
func (imageView *svgImageViewData) propertyChanged(tag PropertyName) {
switch tag {
case Content:
updateInnerHTML(imageView.htmlID(), imageView.Session())
default:
imageView.viewData.propertyChanged(tag)
}
}
func (imageView *svgImageViewData) htmlTag() string {
return "div"
}
func (imageView *svgImageViewData) writeSvg(data []byte, buffer *strings.Builder) {
text := string(data)
index := strings.Index(text, "<svg")
if index > 0 {
text = text[index:]
}
index = strings.Index(text, "\n")
for index >= 0 {
if index > 0 && text[index-1] == '\r' {
buffer.WriteString(text[:index-1])
} else {
buffer.WriteString(text[:index])
}
end := len(text)
index++
for index < end && (text[index] == ' ' || text[index] == '\t' || text[index] == '\r' || text[index] == '\n') {
index++
}
text = text[index:]
index = strings.Index(text, "\n")
}
buffer.WriteString(text)
}
func (imageView *svgImageViewData) htmlSubviews(self View, buffer *strings.Builder) {
if value := imageView.getRaw(Content); value != nil {
if content, ok := value.(string); ok && content != "" {
@ -117,13 +123,13 @@ func (imageView *svgImageViewData) htmlSubviews(self View, buffer *strings.Build
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))
imageView.writeSvg(data, buffer)
return
} else {
DebugLog(err.Error())
}
} else if data, err := os.ReadFile(image.path); err == nil {
buffer.WriteString(string(data))
imageView.writeSvg(data, buffer)
return
} else {
DebugLog(err.Error())
@ -135,7 +141,7 @@ func (imageView *svgImageViewData) htmlSubviews(self View, buffer *strings.Build
if err == nil {
defer resp.Body.Close()
if body, err := io.ReadAll(resp.Body); err == nil {
buffer.WriteString(string(body))
imageView.writeSvg(body, buffer)
return
}
}

View File

@ -1,6 +1,6 @@
package rui
// TableAdapter describes the TableView content
// TableAdapter describes the [TableView] content
type TableAdapter interface {
// RowCount returns number of rows in the table
RowCount() int
@ -9,57 +9,70 @@ type TableAdapter interface {
ColumnCount() int
// Cell returns the contents of a table cell. The function can return elements of the following types:
// * string
// * rune
// * float32, float64
// * integer values: int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64
// * bool
// * rui.Color
// * rui.View
// * fmt.Stringer
// * rui.VerticalTableJoin, rui.HorizontalTableJoin
// - string
// - rune
// - float32, float64
// - integer values: int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64
// - bool
// - rui.Color
// - rui.View
// - fmt.Stringer
// - rui.VerticalTableJoin, rui.HorizontalTableJoin
Cell(row, column int) any
}
// TableColumnStyle describes the style of TableView columns.
// To set column styles, you must either implement the TableColumnStyle interface in the table adapter
// TableColumnStyle describes the style of [TableView] columns.
//
// To set column styles, you must either implement the [TableColumnStyle] interface in the table adapter
// or assign its separate implementation to the "column-style" property.
type TableColumnStyle interface {
// ColumnStyle returns a map of properties which describe the style of the column
ColumnStyle(column int) Params
}
// TableRowStyle describes the style of TableView rows.
// To set row styles, you must either implement the TableRowStyle interface in the table adapter
// TableRowStyle describes the style of [TableView] rows.
//
// To set row styles, you must either implement the [TableRowStyle] interface in the table adapter
// or assign its separate implementation to the "row-style" property.
type TableRowStyle interface {
// RowStyle returns a map of properties which describe the style of the row
RowStyle(row int) Params
}
// TableCellStyle describes the style of TableView cells.
// To set row cells, you must either implement the TableCellStyle interface in the table adapter
// TableCellStyle describes the style of [TableView] cells.
//
// To set row cells, you must either implement the [TableCellStyle] interface in the table adapter
// or assign its separate implementation to the "cell-style" property.
type TableCellStyle interface {
// CellStyle returns a map of properties which describe the style of the cell
CellStyle(row, column int) Params
}
// TableAllowCellSelection determines whether TableView cell selection is allowed.
// TableAllowCellSelection determines whether [TableView] cell selection is allowed.
//
// It is only used if the "selection-mode" property is set to CellSelection (1).
//
// To set cell selection allowing, you must either implement the TableAllowCellSelection interface
// in the table adapter or assign its separate implementation to the "allow-selection" property.
type TableAllowCellSelection interface {
// AllowCellSelection returns "true" if we allow the user to select particular cell at specific rows and columns
AllowCellSelection(row, column int) bool
}
// TableAllowRowSelection determines whether TableView row selection is allowed.
// TableAllowRowSelection determines whether [TableView] row selection is allowed.
//
// It is only used if the "selection-mode" property is set to RowSelection (2).
//
// To set row selection allowing, you must either implement the TableAllowRowSelection interface
// in the table adapter or assign its separate implementation to the "allow-selection" property.
type TableAllowRowSelection interface {
// AllowRowSelection returns "true" if we allow the user to select particular row in the table
AllowRowSelection(row int) bool
}
// SimpleTableAdapter is implementation of TableAdapter where the content
// SimpleTableAdapter is implementation of [TableAdapter] where the content
// defines as [][]any.
//
// When you assign [][]any value to the "content" property, it is converted to SimpleTableAdapter
type SimpleTableAdapter interface {
TableAdapter
@ -71,7 +84,7 @@ type simpleTableAdapter struct {
columnCount int
}
// TextTableAdapter is implementation of TableAdapter where the content
// TextTableAdapter is implementation of [TableAdapter] where the content
// defines as [][]string.
// When you assign [][]string value to the "content" property, it is converted to TextTableAdapter
type TextTableAdapter interface {
@ -251,78 +264,3 @@ func (style *simpleTableLineStyle) RowStyle(row int) Params {
}
return nil
}
func (table *tableViewData) setLineStyle(tag string, value any) bool {
switch value := value.(type) {
case []Params:
if len(value) > 0 {
style := new(simpleTableLineStyle)
style.params = value
table.properties[tag] = style
} else {
delete(table.properties, tag)
}
case DataNode:
if params := value.ArrayAsParams(); len(params) > 0 {
style := new(simpleTableLineStyle)
style.params = params
table.properties[tag] = style
} else {
delete(table.properties, tag)
}
default:
return false
}
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 {
for _, tag := range []string{RowStyle, Content} {
if value := table.getRaw(tag); value != nil {
if style, ok := value.(TableRowStyle); ok {
return style
}
}
}
return nil
}
func (table *tableViewData) setColumnStyle(value any) bool {
switch value := value.(type) {
case TableColumnStyle:
table.properties[ColumnStyle] = value
}
return table.setLineStyle(ColumnStyle, value)
}
func (table *tableViewData) getColumnStyle() TableColumnStyle {
for _, tag := range []string{ColumnStyle, Content} {
if value := table.getRaw(tag); value != nil {
if style, ok := value.(TableColumnStyle); ok {
return style
}
}
}
return nil
}
func (table *tableViewData) getCellStyle() TableCellStyle {
for _, tag := range []string{CellStyle, Content} {
if value := table.getRaw(tag); value != nil {
if style, ok := value.(TableCellStyle); ok {
return style
}
}
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,19 @@
package rui
import "strings"
func (cell *tableCellView) Set(tag string, value any) bool {
return cell.set(strings.ToLower(tag), value)
func newTableCellView(session Session) *tableCellView {
view := new(tableCellView)
view.init(session)
return view
}
func (cell *tableCellView) set(tag string, value any) bool {
switch tag {
case VerticalAlign:
tag = TableVerticalAlign
func (cell *tableCellView) init(session Session) {
cell.viewData.init(session)
cell.normalize = func(tag PropertyName) PropertyName {
if tag == VerticalAlign {
return TableVerticalAlign
}
return tag
}
return cell.viewData.set(tag, value)
}
func (cell *tableCellView) cssStyle(self View, builder cssBuilder) {
@ -26,13 +28,11 @@ func (cell *tableCellView) cssStyle(self View, builder cssBuilder) {
// GetTableContent returns a TableAdapter which defines the TableView content.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetTableContent(view View, subviewID ...string) TableAdapter {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if tableView, ok := view.(TableView); ok {
return tableView.content()
if view = getSubview(view, subviewID); view != nil {
if content := view.getRaw(Content); content != nil {
if adapter, ok := content.(TableAdapter); ok {
return adapter
}
}
}
@ -42,13 +42,13 @@ func GetTableContent(view View, subviewID ...string) TableAdapter {
// GetTableRowStyle returns a TableRowStyle which defines styles of TableView rows.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetTableRowStyle(view View, subviewID ...string) TableRowStyle {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if tableView, ok := view.(TableView); ok {
return tableView.getRowStyle()
if view = getSubview(view, subviewID); view != nil {
for _, tag := range []PropertyName{RowStyle, Content} {
if value := view.getRaw(tag); value != nil {
if style, ok := value.(TableRowStyle); ok {
return style
}
}
}
}
@ -58,13 +58,13 @@ func GetTableRowStyle(view View, subviewID ...string) TableRowStyle {
// GetTableColumnStyle returns a TableColumnStyle which defines styles of TableView columns.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetTableColumnStyle(view View, subviewID ...string) TableColumnStyle {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if tableView, ok := view.(TableView); ok {
return tableView.getColumnStyle()
if view = getSubview(view, subviewID); view != nil {
for _, tag := range []PropertyName{ColumnStyle, Content} {
if value := view.getRaw(tag); value != nil {
if style, ok := value.(TableColumnStyle); ok {
return style
}
}
}
}
@ -74,14 +74,15 @@ func GetTableColumnStyle(view View, subviewID ...string) TableColumnStyle {
// GetTableCellStyle returns a TableCellStyle which defines styles of TableView cells.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetTableCellStyle(view View, subviewID ...string) TableCellStyle {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if tableView, ok := view.(TableView); ok {
return tableView.getCellStyle()
if view = getSubview(view, subviewID); view != nil {
for _, tag := range []PropertyName{CellStyle, Content} {
if value := view.getRaw(tag); value != nil {
if style, ok := value.(TableCellStyle); ok {
return style
}
}
}
return nil
}
return nil
@ -119,15 +120,9 @@ func GetTableFootHeight(view View, subviewID ...string) int {
// If the selection mode is RowSelection (2) then the returned column index is less than 0.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetTableCurrent(view View, subviewID ...string) CellIndex {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
if selectionMode := GetTableSelectionMode(view); selectionMode != NoneSelection {
if tableView, ok := view.(TableView); ok {
return tableView.getCurrent()
}
return tableViewCurrent(view)
}
}
return CellIndex{Row: -1, Column: -1}
@ -137,84 +132,50 @@ func GetTableCurrent(view View, subviewID ...string) CellIndex {
// 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.
func GetTableCellClickedListeners(view View, subviewID ...string) []func(TableView, int, int) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.Get(TableCellClickedEvent); value != nil {
if result, ok := value.([]func(TableView, int, int)); ok {
return result
}
}
}
return []func(TableView, int, int){}
return getTwoArgEventListeners[TableView, int](view, subviewID, TableCellClickedEvent)
}
// GetTableCellSelectedListeners returns listeners of event which occurs when a table cell becomes selected.
// 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.
func GetTableCellSelectedListeners(view View, subviewID ...string) []func(TableView, int, int) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.Get(TableCellSelectedEvent); value != nil {
if result, ok := value.([]func(TableView, int, int)); ok {
return result
}
}
}
return []func(TableView, int, int){}
return getTwoArgEventListeners[TableView, int](view, subviewID, TableCellSelectedEvent)
}
// GetTableRowClickedListeners returns listeners of event which occurs when the user clicks on a table row.
// 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.
func GetTableRowClickedListeners(view View, subviewID ...string) []func(TableView, int) {
return getEventListeners[TableView, int](view, subviewID, TableRowClickedEvent)
return getOneArgEventListeners[TableView, int](view, subviewID, TableRowClickedEvent)
}
// GetTableRowSelectedListeners returns listeners of event which occurs when a table row becomes selected.
// 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.
func GetTableRowSelectedListeners(view View, subviewID ...string) []func(TableView, int) {
return getEventListeners[TableView, int](view, subviewID, TableRowSelectedEvent)
return getOneArgEventListeners[TableView, int](view, subviewID, TableRowSelectedEvent)
}
// 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 {
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
if view = getSubview(view, subviewID); view != nil {
if tableView, ok := view.(TableView); ok {
tableView.ReloadTableData()
return true
}
}
tableView.ReloadTableData()
return true
return false
}
// 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
if view = getSubview(view, subviewID); view != nil {
if tableView, ok := view.(TableView); ok {
tableView.ReloadCell(row, column)
return true
}
}
tableView.ReloadCell(row, column)
return true
return false
}

View File

@ -5,47 +5,120 @@ import (
"strings"
)
// Constants for [TabsLayout] specific view and events
const (
// CurrentTabChangedEvent is the constant for "current-tab-changed" property tag.
// The "current-tab-changed" event occurs when the new tab becomes active.
// The main listener format: func(TabsLayout, int, int), where
// the second argument is the index of the new active tab,
// the third argument is the index of the old active tab.
CurrentTabChangedEvent = "current-tab-changed"
//
// Used by TabsLayout.
// Occur when the new tab becomes active.
//
// General listener format:
//
// func(tabsLayout rui.TabsLayout, newTab, oldTab int)
//
// where:
// - tabsLayout - Interface of a tabs layout which generated this event,
// - newTab - Index of a new active tab,
// - oldTab - Index of an old active tab.
//
// Allowed listener formats:
//
// func(tabsLayout rui.TabsLayout, newTab int)
// func(newTab, oldTab int)
// func(newTab int)
// func()
CurrentTabChangedEvent PropertyName = "current-tab-changed"
// Icon is the constant for "icon" property tag.
// The string "icon" property defines the icon name that is displayed in the tab.
Icon = "icon"
//
// Used by TabsLayout.
// Defines the icon name that is displayed in the tab. The property is set for the child view of TabsLayout.
//
// Supported types: string.
Icon PropertyName = "icon"
// TabCloseButton is the constant for "tab-close-button" property tag.
// The "tab-close-button" is the bool property. If it is "true" then a close button is displayed within the tab.
TabCloseButton = "tab-close-button"
//
// Used by TabsLayout.
// Controls whether to add close button to a tab(s). This property can be set separately for each child view or for tabs
// layout itself. Property set for child view takes precedence over the value set for tabs layout. Default value is
// false.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", "1" - Tab(s) has close button.
// - false, 0, "false", "no", "off", "0" - No close button in tab(s).
TabCloseButton PropertyName = "tab-close-button"
// TabCloseEvent is the constant for "tab-close-event" property tag.
// The "tab-close-event" occurs when when the user clicks on the tab close button.
// The main listener format: func(TabsLayout, int), where the second argument is the index of the tab.
TabCloseEvent = "tab-close-event"
//
// Used by TabsLayout.
// Occurs when the user clicks on the tab close button.
//
// General listener format:
//
// func(tabsLayout rui.TabsLayout, tab int)
//
// where:
// - tabsLayout - Interface of a tabs layout which generated this event,
// - tab - Index of the tab.
//
// Allowed listener formats:
//
// func(tab int)
// func(tabsLayout rui.TabsLayout)
// func()
TabCloseEvent PropertyName = "tab-close-event"
// Tabs is the constant for the "tabs" property tag.
// The "tabs" is the int property that sets where the tabs are located.
// Valid values: TopTabs (0), BottomTabs (1), LeftTabs (2), RightTabs (3), LeftListTabs (4), RightListTabs (5), and HiddenTabs (6).
Tabs = "tabs"
// Tabs is the constant for "tabs" property tag.
//
// Used by TabsLayout.
// Sets where the tabs are located. Default value is "top".
//
// Supported types: int, string.
//
// Values:
// - 0 (TopTabs) or "top" - Tabs on the top.
// - 1 (BottomTabs) or "bottom" - Tabs on the bottom.
// - 2 (LeftTabs) or "left" - Tabs on the left. Each tab is rotated 90° counterclockwise.
// - 3 (RightTabs) or "right" - Tabs located on the right. Each tab is rotated 90° clockwise.
// - 4 (LeftListTabs) or "left-list" - Tabs on the left. The tabs are displayed as a list.
// - 5 (RightListTabs) or "right-list" - Tabs on the right. The tabs are displayed as a list.
// - 6 (HiddenTabs) or "hidden" - Tabs are hidden.
Tabs PropertyName = "tabs"
// TabBarStyle is the constant for the "tab-bar-style" property tag.
// The "tab-bar-style" is the string property that sets the style for the display of the tab bar.
// The default value is "ruiTabBar".
TabBarStyle = "tab-bar-style"
// TabBarStyle is the constant for "tab-bar-style" property tag.
//
// Used by TabsLayout.
// Set the style for the display of the tab bar. The default value is "ruiTabBar".
//
// Supported types: string.
TabBarStyle PropertyName = "tab-bar-style"
// TabStyle is the constant for the "tab-style" property tag.
// The "tab-style" is the string property that sets the style for the display of the tab.
// The default value is "ruiTab" or "ruiVerticalTab".
TabStyle = "tab-style"
// TabStyle is the constant for "tab-style" property tag.
//
// Used by TabsLayout.
// Set the style for the display of the tab. The default value is "ruiTab" or "ruiVerticalTab".
//
// Supported types: string.
TabStyle PropertyName = "tab-style"
// CurrentTabStyle is the constant for the "current-tab-style" property tag.
// The "current-tab-style" is the string property that sets the style for the display of the current (selected) tab.
// The default value is "ruiCurrentTab" or "ruiCurrentVerticalTab".
CurrentTabStyle = "current-tab-style"
// CurrentTabStyle is the constant for "current-tab-style" property tag.
//
// Used by TabsLayout.
// Set the style for the display of the current(selected) tab. The default value is "ruiCurrentTab" or
// "ruiCurrentVerticalTab".
//
// Supported types: string.
CurrentTabStyle PropertyName = "current-tab-style"
inactiveTabStyle = "data-inactiveTabStyle"
activeTabStyle = "data-activeTabStyle"
)
// Constants that are the values of the "tabs" property of a [TabsLayout]
const (
// TopTabs - tabs of TabsLayout are on the top
TopTabs = 0
// BottomTabs - tabs of TabsLayout are on the bottom
@ -60,12 +133,9 @@ const (
RightListTabs = 5
// HiddenTabs - tabs of TabsLayout are hidden
HiddenTabs = 6
inactiveTabStyle = "data-inactiveTabStyle"
activeTabStyle = "data-activeTabStyle"
)
// TabsLayout - multi-tab container of View
// TabsLayout represents a TabsLayout view
type TabsLayout interface {
ViewsContainer
ListAdapter
@ -73,8 +143,6 @@ type TabsLayout interface {
type tabsLayoutData struct {
viewsContainerData
tabListener []func(TabsLayout, int, int)
tabCloseListener []func(TabsLayout, int)
}
// NewTabsLayout create new TabsLayout object and return it
@ -86,7 +154,8 @@ func NewTabsLayout(session Session, params Params) TabsLayout {
}
func newTabsLayout(session Session) View {
return NewTabsLayout(session, nil)
//return NewTabsLayout(session, nil)
return new(tabsLayoutData)
}
// Init initialize fields of ViewsContainer by default values
@ -94,345 +163,229 @@ func (tabsLayout *tabsLayoutData) init(session Session) {
tabsLayout.viewsContainerData.init(session)
tabsLayout.tag = "TabsLayout"
tabsLayout.systemClass = "ruiTabsLayout"
tabsLayout.tabListener = []func(TabsLayout, int, int){}
tabsLayout.tabCloseListener = []func(TabsLayout, int){}
tabsLayout.set = tabsLayout.setFunc
tabsLayout.changed = tabsLayout.propertyChanged
}
func (tabsLayout *tabsLayoutData) String() string {
return getViewString(tabsLayout, nil)
}
func (tabsLayout *tabsLayoutData) currentItem(defaultValue int) int {
result, _ := intProperty(tabsLayout, Current, tabsLayout.session, defaultValue)
func tabsLayoutCurrent(view View, defaultValue int) int {
result, _ := intProperty(view, Current, view.Session(), defaultValue)
return result
}
func (tabsLayout *tabsLayoutData) Get(tag string) any {
return tabsLayout.get(strings.ToLower(tag))
}
func (tabsLayout *tabsLayoutData) get(tag string) any {
func (tabsLayout *tabsLayoutData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case CurrentTabChangedEvent:
return tabsLayout.tabListener
return setTwoArgEventListener[TabsLayout, int](tabsLayout, tag, value)
case TabCloseEvent:
return tabsLayout.tabCloseListener
}
return tabsLayout.viewsContainerData.get(tag)
}
func (tabsLayout *tabsLayoutData) Remove(tag string) {
tabsLayout.remove(strings.ToLower(tag))
}
func (tabsLayout *tabsLayoutData) remove(tag string) {
switch tag {
case CurrentTabChangedEvent:
if len(tabsLayout.tabListener) > 0 {
tabsLayout.tabListener = []func(TabsLayout, int, int){}
tabsLayout.propertyChangedEvent(tag)
}
return
case TabCloseEvent:
if len(tabsLayout.tabCloseListener) > 0 {
tabsLayout.tabCloseListener = []func(TabsLayout, int){}
tabsLayout.propertyChangedEvent(tag)
}
return
return setOneArgEventListener[TabsLayout, int](tabsLayout, tag, value)
case Current:
oldCurrent := tabsLayout.currentItem(0)
delete(tabsLayout.properties, Current)
if oldCurrent == 0 {
return
}
if tabsLayout.created {
tabsLayout.session.callFunc("activateTab", tabsLayout.htmlID(), 0)
for _, listener := range tabsLayout.tabListener {
listener(tabsLayout, 0, oldCurrent)
}
}
tabsLayout.setRaw("old-current", tabsLayoutCurrent(tabsLayout, -1))
case Tabs:
delete(tabsLayout.properties, Tabs)
if tabsLayout.created {
htmlID := tabsLayout.htmlID()
tabsLayout.session.updateProperty(htmlID, inactiveTabStyle, tabsLayout.inactiveTabStyle())
tabsLayout.session.updateProperty(htmlID, activeTabStyle, tabsLayout.activeTabStyle())
updateCSSStyle(htmlID, tabsLayout.session)
updateInnerHTML(htmlID, tabsLayout.session)
}
case TabStyle, CurrentTabStyle:
delete(tabsLayout.properties, tag)
if tabsLayout.created {
htmlID := tabsLayout.htmlID()
tabsLayout.session.updateProperty(htmlID, inactiveTabStyle, tabsLayout.inactiveTabStyle())
tabsLayout.session.updateProperty(htmlID, activeTabStyle, tabsLayout.activeTabStyle())
updateInnerHTML(htmlID, tabsLayout.session)
}
case TabCloseButton:
delete(tabsLayout.properties, tag)
if tabsLayout.created {
updateInnerHTML(tabsLayout.htmlID(), tabsLayout.session)
}
default:
tabsLayout.viewsContainerData.remove(tag)
return
}
tabsLayout.propertyChangedEvent(tag)
}
func (tabsLayout *tabsLayoutData) Set(tag string, value any) bool {
return tabsLayout.set(strings.ToLower(tag), value)
}
func (tabsLayout *tabsLayoutData) set(tag string, value any) bool {
if value == nil {
tabsLayout.remove(tag)
return true
}
switch tag {
case CurrentTabChangedEvent:
listeners := tabsLayout.valueToTabListeners(value)
if listeners == nil {
notCompatibleType(tag, value)
return false
}
tabsLayout.tabListener = listeners
case TabCloseEvent:
listeners, ok := valueToEventListeners[TabsLayout, int](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(TabsLayout, int){}
}
tabsLayout.tabCloseListener = listeners
case Current:
if current, ok := value.(int); ok && current < 0 {
tabsLayout.remove(Current)
return true
tabsLayout.setRaw(Current, nil)
return []PropertyName{tag}
}
oldCurrent := tabsLayout.currentItem(-1)
if !tabsLayout.setIntProperty(Current, value) {
return false
}
return setIntProperty(tabsLayout, Current, value)
current := tabsLayout.currentItem(0)
if oldCurrent == current {
return true
case TabStyle, CurrentTabStyle, TabBarStyle:
if text, ok := value.(string); ok {
return setStringPropertyValue(tabsLayout, tag, text)
}
if tabsLayout.created {
tabsLayout.session.callFunc("activateTab", tabsLayout.htmlID(), current)
for _, listener := range tabsLayout.tabListener {
notCompatibleType(tag, value)
return nil
}
return tabsLayout.viewsContainerData.setFunc(tag, value)
}
func (tabsLayout *tabsLayoutData) propertyChanged(tag PropertyName) {
switch tag {
case Current:
session := tabsLayout.Session()
current := GetCurrent(tabsLayout)
session.callFunc("activateTab", tabsLayout.htmlID(), current)
if listeners := getTwoArgEventListeners[TabsLayout, int](tabsLayout, nil, CurrentTabChangedEvent); len(listeners) > 0 {
oldCurrent, _ := intProperty(tabsLayout, "old-current", session, -1)
for _, listener := range listeners {
listener(tabsLayout, current, oldCurrent)
}
}
case Tabs:
if !tabsLayout.setEnumProperty(Tabs, value, enumProperties[Tabs].values) {
return false
}
if tabsLayout.created {
htmlID := tabsLayout.htmlID()
tabsLayout.session.updateProperty(htmlID, inactiveTabStyle, tabsLayout.inactiveTabStyle())
tabsLayout.session.updateProperty(htmlID, activeTabStyle, tabsLayout.activeTabStyle())
updateCSSStyle(htmlID, tabsLayout.session)
updateInnerHTML(htmlID, tabsLayout.session)
}
htmlID := tabsLayout.htmlID()
session := tabsLayout.Session()
session.updateProperty(htmlID, inactiveTabStyle, tabsLayoutInactiveTabStyle(tabsLayout))
session.updateProperty(htmlID, activeTabStyle, tabsLayoutActiveTabStyle(tabsLayout))
updateCSSStyle(htmlID, session)
updateInnerHTML(htmlID, session)
case TabStyle, CurrentTabStyle, TabBarStyle:
if text, ok := value.(string); ok {
if text == "" {
delete(tabsLayout.properties, tag)
} else {
tabsLayout.properties[tag] = text
}
} else {
notCompatibleType(tag, value)
return false
}
if tabsLayout.created {
htmlID := tabsLayout.htmlID()
tabsLayout.session.updateProperty(htmlID, inactiveTabStyle, tabsLayout.inactiveTabStyle())
tabsLayout.session.updateProperty(htmlID, activeTabStyle, tabsLayout.activeTabStyle())
updateInnerHTML(htmlID, tabsLayout.session)
}
htmlID := tabsLayout.htmlID()
session := tabsLayout.Session()
session.updateProperty(htmlID, inactiveTabStyle, tabsLayoutInactiveTabStyle(tabsLayout))
session.updateProperty(htmlID, activeTabStyle, tabsLayoutActiveTabStyle(tabsLayout))
updateInnerHTML(htmlID, session)
case TabCloseButton:
if !tabsLayout.setBoolProperty(tag, value) {
return false
}
if tabsLayout.created {
updateInnerHTML(tabsLayout.htmlID(), tabsLayout.session)
}
updateInnerHTML(tabsLayout.htmlID(), tabsLayout.Session())
default:
return tabsLayout.viewsContainerData.set(tag, value)
tabsLayout.viewsContainerData.propertyChanged(tag)
}
tabsLayout.propertyChangedEvent(tag)
return true
}
func (tabsLayout *tabsLayoutData) valueToTabListeners(value any) []func(TabsLayout, int, int) {
if value == nil {
return []func(TabsLayout, int, int){}
}
switch value := value.(type) {
case func(TabsLayout, int, int):
return []func(TabsLayout, int, int){value}
case func(TabsLayout, int):
fn := func(view TabsLayout, current, _ int) {
value(view, current)
/*
func (tabsLayout *tabsLayoutData) valueToTabListeners(value any) []func(TabsLayout, int, int) {
if value == nil {
return []func(TabsLayout, int, int){}
}
return []func(TabsLayout, int, int){fn}
case func(TabsLayout):
fn := func(view TabsLayout, _, _ int) {
value(view)
}
return []func(TabsLayout, int, int){fn}
switch value := value.(type) {
case func(TabsLayout, int, int):
return []func(TabsLayout, int, int){value}
case func(int, int):
fn := func(_ TabsLayout, current, old int) {
value(current, old)
}
return []func(TabsLayout, int, int){fn}
case func(int):
fn := func(_ TabsLayout, current, _ int) {
value(current)
}
return []func(TabsLayout, int, int){fn}
case func():
fn := func(TabsLayout, int, int) {
value()
}
return []func(TabsLayout, int, int){fn}
case []func(TabsLayout, int, int):
return value
case []func(TabsLayout, int):
listeners := make([]func(TabsLayout, int, int), len(value))
for i, val := range value {
if val == nil {
return nil
case func(TabsLayout, int):
fn := func(view TabsLayout, current, _ int) {
value(view, current)
}
listeners[i] = func(view TabsLayout, current, _ int) {
val(view, current)
}
}
return listeners
return []func(TabsLayout, int, int){fn}
case []func(TabsLayout):
listeners := make([]func(TabsLayout, int, int), len(value))
for i, val := range value {
if val == nil {
return nil
case func(TabsLayout):
fn := func(view TabsLayout, _, _ int) {
value(view)
}
listeners[i] = func(view TabsLayout, _, _ int) {
val(view)
}
}
return listeners
return []func(TabsLayout, int, int){fn}
case []func(int, int):
listeners := make([]func(TabsLayout, int, int), len(value))
for i, val := range value {
if val == nil {
return nil
case func(int, int):
fn := func(_ TabsLayout, current, old int) {
value(current, old)
}
listeners[i] = func(_ TabsLayout, current, old int) {
val(current, old)
}
}
return listeners
return []func(TabsLayout, int, int){fn}
case []func(int):
listeners := make([]func(TabsLayout, int, int), len(value))
for i, val := range value {
if val == nil {
return nil
case func(int):
fn := func(_ TabsLayout, current, _ int) {
value(current)
}
listeners[i] = func(_ TabsLayout, current, _ int) {
val(current)
}
}
return listeners
return []func(TabsLayout, int, int){fn}
case []func():
listeners := make([]func(TabsLayout, int, int), len(value))
for i, val := range value {
if val == nil {
return nil
case func():
fn := func(TabsLayout, int, int) {
value()
}
listeners[i] = func(TabsLayout, int, int) {
val()
}
}
return listeners
return []func(TabsLayout, int, int){fn}
case []any:
listeners := make([]func(TabsLayout, int, int), len(value))
for i, val := range value {
if val == nil {
return nil
}
switch val := val.(type) {
case func(TabsLayout, int, int):
listeners[i] = val
case []func(TabsLayout, int, int):
return value
case func(TabsLayout, int):
case []func(TabsLayout, int):
listeners := make([]func(TabsLayout, int, int), len(value))
for i, val := range value {
if val == nil {
return nil
}
listeners[i] = func(view TabsLayout, current, _ int) {
val(view, current)
}
}
return listeners
case func(TabsLayout):
case []func(TabsLayout):
listeners := make([]func(TabsLayout, int, int), len(value))
for i, val := range value {
if val == nil {
return nil
}
listeners[i] = func(view TabsLayout, _, _ int) {
val(view)
}
}
return listeners
case func(int, int):
case []func(int, int):
listeners := make([]func(TabsLayout, int, int), len(value))
for i, val := range value {
if val == nil {
return nil
}
listeners[i] = func(_ TabsLayout, current, old int) {
val(current, old)
}
}
return listeners
case func(int):
case []func(int):
listeners := make([]func(TabsLayout, int, int), len(value))
for i, val := range value {
if val == nil {
return nil
}
listeners[i] = func(_ TabsLayout, current, _ int) {
val(current)
}
}
return listeners
case func():
case []func():
listeners := make([]func(TabsLayout, int, int), len(value))
for i, val := range value {
if val == nil {
return nil
}
listeners[i] = func(TabsLayout, int, int) {
val()
}
default:
return nil
}
}
return listeners
}
return listeners
return nil
}
case []any:
listeners := make([]func(TabsLayout, int, int), len(value))
for i, val := range value {
if val == nil {
return nil
}
switch val := val.(type) {
case func(TabsLayout, int, int):
listeners[i] = val
case func(TabsLayout, int):
listeners[i] = func(view TabsLayout, current, _ int) {
val(view, current)
}
case func(TabsLayout):
listeners[i] = func(view TabsLayout, _, _ int) {
val(view)
}
case func(int, int):
listeners[i] = func(_ TabsLayout, current, old int) {
val(current, old)
}
case func(int):
listeners[i] = func(_ TabsLayout, current, _ int) {
val(current)
}
case func():
listeners[i] = func(TabsLayout, int, int) {
val()
}
default:
return nil
}
}
return listeners
}
return nil
}
*/
func (tabsLayout *tabsLayoutData) tabsLocation() int {
tabs, _ := enumProperty(tabsLayout, Tabs, tabsLayout.session, 0)
@ -453,36 +406,42 @@ func (tabsLayout *tabsLayoutData) tabBarStyle() string {
return "ruiTabBar"
}
func (tabsLayout *tabsLayoutData) inactiveTabStyle() string {
if style, ok := stringProperty(tabsLayout, TabStyle, tabsLayout.session); ok {
func tabsLayoutInactiveTabStyle(view View) string {
session := view.Session()
if style, ok := stringProperty(view, TabStyle, session); ok {
return style
}
if value := valueFromStyle(tabsLayout, TabStyle); value != nil {
if value := valueFromStyle(view, TabStyle); value != nil {
if style, ok := value.(string); ok {
if style, ok = tabsLayout.session.resolveConstants(style); ok {
if style, ok = session.resolveConstants(style); ok {
return style
}
}
}
switch tabsLayout.tabsLocation() {
tabs, _ := enumProperty(view, Tabs, session, 0)
switch tabs {
case LeftTabs, RightTabs:
return "ruiVerticalTab"
}
return "ruiTab"
}
func (tabsLayout *tabsLayoutData) activeTabStyle() string {
if style, ok := stringProperty(tabsLayout, CurrentTabStyle, tabsLayout.session); ok {
func tabsLayoutActiveTabStyle(view View) string {
session := view.Session()
if style, ok := stringProperty(view, CurrentTabStyle, session); ok {
return style
}
if value := valueFromStyle(tabsLayout, CurrentTabStyle); value != nil {
if value := valueFromStyle(view, CurrentTabStyle); value != nil {
if style, ok := value.(string); ok {
if style, ok = tabsLayout.session.resolveConstants(style); ok {
if style, ok = session.resolveConstants(style); ok {
return style
}
}
}
switch tabsLayout.tabsLocation() {
tabs, _ := enumProperty(view, Tabs, session, 0)
switch tabs {
case LeftTabs, RightTabs:
return "ruiCurrentVerticalTab"
}
@ -544,7 +503,7 @@ func (tabsLayout *tabsLayoutData) ListItem(index int, session Session) View {
Column: 2,
Content: "✕",
ClickEvent: func() {
for _, listener := range tabsLayout.tabCloseListener {
for _, listener := range getOneArgEventListeners[TabsLayout, int](tabsLayout, nil, TabCloseEvent) {
listener(tabsLayout, index)
}
},
@ -566,7 +525,7 @@ func (tabsLayout *tabsLayoutData) IsListItemEnabled(index int) bool {
return true
}
func (tabsLayout *tabsLayoutData) updateTitle(view View, tag string) {
func (tabsLayout *tabsLayoutData) updateTitle(view View, tag PropertyName) {
session := tabsLayout.session
title, _ := stringProperty(view, Title, session)
if !GetNotTranslate(tabsLayout) {
@ -575,13 +534,13 @@ func (tabsLayout *tabsLayoutData) updateTitle(view View, tag string) {
session.updateInnerHTML(view.htmlID()+"-title", title)
}
func (tabsLayout *tabsLayoutData) updateIcon(view View, tag string) {
func (tabsLayout *tabsLayoutData) updateIcon(view View, tag PropertyName) {
session := tabsLayout.session
icon, _ := stringProperty(view, Icon, session)
session.updateProperty(view.htmlID()+"-icon", "src", icon)
}
func (tabsLayout *tabsLayoutData) updateTabCloseButton(view View, tag string) {
func (tabsLayout *tabsLayoutData) updateTabCloseButton(view View, tag PropertyName) {
updateInnerHTML(tabsLayout.htmlID(), tabsLayout.session)
}
@ -596,11 +555,8 @@ func (tabsLayout *tabsLayoutData) Append(view View) {
view.SetChangeListener(Icon, tabsLayout.updateIcon)
view.SetChangeListener(TabCloseButton, tabsLayout.updateTabCloseButton)
if len(tabsLayout.views) == 1 {
tabsLayout.properties[Current] = 0
for _, listener := range tabsLayout.tabListener {
listener(tabsLayout, 0, -1)
}
defer tabsLayout.propertyChangedEvent(Current)
tabsLayout.setRaw(Current, nil)
tabsLayout.Set(Current, 0)
}
}
}
@ -611,9 +567,9 @@ func (tabsLayout *tabsLayoutData) Insert(view View, index int) {
tabsLayout.views = []View{}
}
if view != nil {
if current := tabsLayout.currentItem(0); current >= index {
tabsLayout.properties[Current] = current + 1
defer tabsLayout.propertyChangedEvent(Current)
if current := GetCurrent(tabsLayout); current >= index {
tabsLayout.setRaw(Current, current+1)
defer tabsLayout.currentChanged(current+1, current)
}
tabsLayout.viewsContainerData.Insert(view, index)
view.SetChangeListener(Title, tabsLayout.updateTitle)
@ -622,61 +578,95 @@ func (tabsLayout *tabsLayoutData) Insert(view View, index int) {
}
}
func (tabsLayout *tabsLayoutData) currentChanged(newCurrent, oldCurrent int) {
for _, listener := range getTwoArgEventListeners[TabsLayout, int](tabsLayout, nil, CurrentTabChangedEvent) {
listener(tabsLayout, newCurrent, oldCurrent)
}
if listener, ok := tabsLayout.changeListener[Current]; ok {
listener(tabsLayout, Current)
}
}
// Remove removes view from list and return it
func (tabsLayout *tabsLayoutData) RemoveView(index int) View {
if tabsLayout.views == nil {
tabsLayout.views = []View{}
if index < 0 || index >= len(tabsLayout.views) {
return nil
}
count := len(tabsLayout.views)
if index < 0 || index >= count {
return nil
oldCurrent := GetCurrent(tabsLayout)
newCurrent := oldCurrent
if index < oldCurrent || (index == oldCurrent && oldCurrent > 0) {
newCurrent--
}
view := tabsLayout.views[index]
view.setParentID("")
view.SetChangeListener(Title, nil)
view.SetChangeListener(Icon, nil)
view.SetChangeListener(TabCloseButton, nil)
if view := tabsLayout.viewsContainerData.RemoveView(index); view != nil {
view.SetChangeListener(Title, nil)
view.SetChangeListener(Icon, nil)
view.SetChangeListener(TabCloseButton, nil)
current := tabsLayout.currentItem(0)
if index < current || (index == current && current > 0) {
current--
if newCurrent != oldCurrent {
tabsLayout.setRaw(Current, newCurrent)
tabsLayout.currentChanged(newCurrent, oldCurrent)
}
}
return nil
if len(tabsLayout.views) == 1 {
tabsLayout.views = []View{}
current = -1
} else if index == 0 {
tabsLayout.views = tabsLayout.views[1:]
} else if index == count-1 {
tabsLayout.views = tabsLayout.views[:index]
} else {
tabsLayout.views = append(tabsLayout.views[:index], tabsLayout.views[index+1:]...)
}
/*
if tabsLayout.views == nil {
tabsLayout.views = []View{}
return nil
}
updateInnerHTML(tabsLayout.parentHTMLID(), tabsLayout.session)
tabsLayout.propertyChangedEvent(Content)
count := len(tabsLayout.views)
if index < 0 || index >= count {
return nil
}
delete(tabsLayout.properties, Current)
tabsLayout.set(Current, current)
return view
view := tabsLayout.views[index]
view.setParentID("")
view.SetChangeListener(Title, nil)
view.SetChangeListener(Icon, nil)
view.SetChangeListener(TabCloseButton, nil)
current := GetCurrent(tabsLayout)
if index < current || (index == current && current > 0) {
current--
}
if len(tabsLayout.views) == 1 {
tabsLayout.views = []View{}
current = -1
} else if index == 0 {
tabsLayout.views = tabsLayout.views[1:]
} else if index == count-1 {
tabsLayout.views = tabsLayout.views[:index]
} else {
tabsLayout.views = append(tabsLayout.views[:index], tabsLayout.views[index+1:]...)
}
updateInnerHTML(tabsLayout.parentHTMLID(), tabsLayout.session)
tabsLayout.propertyChangedEvent(Content)
delete(tabsLayout.view, Current)
tabsLayout.Set(Current, current)
return view
*/
}
func (tabsLayout *tabsLayoutData) htmlProperties(self View, buffer *strings.Builder) {
tabsLayout.viewsContainerData.htmlProperties(self, buffer)
buffer.WriteString(` data-inactiveTabStyle="`)
buffer.WriteString(tabsLayout.inactiveTabStyle())
buffer.WriteString(tabsLayoutInactiveTabStyle(tabsLayout))
buffer.WriteString(`" data-activeTabStyle="`)
buffer.WriteString(tabsLayout.activeTabStyle())
buffer.WriteString(tabsLayoutActiveTabStyle(tabsLayout))
buffer.WriteString(`" data-current="`)
buffer.WriteString(strconv.Itoa(tabsLayout.currentItem(0)))
buffer.WriteString(strconv.Itoa(GetCurrent(tabsLayout)))
buffer.WriteRune('"')
}
func (tabsLayout *tabsLayoutData) cssStyle(self View, builder cssBuilder) {
tabsLayout.viewsContainerData.cssStyle(self, builder)
tabsLayout.viewData.cssStyle(self, builder)
switch tabsLayout.tabsLocation() {
case TopTabs:
builder.add(`grid-template-rows`, `auto 1fr`)
@ -697,8 +687,7 @@ func (tabsLayout *tabsLayoutData) htmlSubviews(self View, buffer *strings.Builde
return
}
//viewCount := len(tabsLayout.views)
current := tabsLayout.currentItem(0)
current := GetCurrent(tabsLayout)
location := tabsLayout.tabsLocation()
tabsLayoutID := tabsLayout.htmlID()
@ -730,8 +719,8 @@ func (tabsLayout *tabsLayoutData) htmlSubviews(self View, buffer *strings.Builde
buffer.WriteString(`">`)
inactiveStyle := tabsLayout.inactiveTabStyle()
activeStyle := tabsLayout.activeTabStyle()
inactiveStyle := tabsLayoutInactiveTabStyle(tabsLayout)
activeStyle := tabsLayoutActiveTabStyle(tabsLayout)
notTranslate := GetNotTranslate(tabsLayout)
closeButton, _ := boolProperty(tabsLayout, TabCloseButton, tabsLayout.session)
@ -859,40 +848,38 @@ func (tabsLayout *tabsLayoutData) htmlSubviews(self View, buffer *strings.Builde
buffer.WriteString(`-page`)
buffer.WriteString(strconv.Itoa(n))
if current != n {
buffer.WriteString(`" style="display: grid; align-items: stretch; justify-items: stretch; visibility: hidden; `)
} else {
buffer.WriteString(`" style="display: grid; align-items: stretch; justify-items: stretch; `)
}
switch location {
case LeftTabs, LeftListTabs:
buffer.WriteString(`" style="position: relative; grid-row-start: 1; grid-row-end: 2; grid-column-start: 2; grid-column-end: 3;`)
buffer.WriteString(`grid-row-start: 1; grid-row-end: 2; grid-column-start: 2; grid-column-end: 3;">`)
case TopTabs:
buffer.WriteString(`" style="position: relative; grid-row-start: 2; grid-row-end: 3; grid-column-start: 1; grid-column-end: 2;`)
buffer.WriteString(`grid-row-start: 2; grid-row-end: 3; grid-column-start: 1; grid-column-end: 2;">`)
default:
buffer.WriteString(`" style="position: relative; grid-row-start: 1; grid-row-end: 2; grid-column-start: 1; grid-column-end: 2;`)
buffer.WriteString(`grid-row-start: 1; grid-row-end: 2; grid-column-start: 1; grid-column-end: 2;">`)
}
if current != n {
buffer.WriteString(` display: none;`)
}
buffer.WriteString(`">`)
view.addToCSSStyle(map[string]string{`position`: `absolute`, `left`: `0`, `right`: `0`, `top`: `0`, `bottom`: `0`})
viewHTML(tabsLayout.views[n], buffer)
viewHTML(view, buffer, "")
buffer.WriteString(`</div>`)
}
}
func (tabsLayout *tabsLayoutData) handleCommand(self View, command string, data DataObject) bool {
func (tabsLayout *tabsLayoutData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command {
case "tabClick":
if numberText, ok := data.PropertyValue("number"); ok {
if number, err := strconv.Atoi(numberText); err == nil {
current := tabsLayout.currentItem(0)
current := GetCurrent(tabsLayout)
if current != number {
tabsLayout.properties[Current] = number
for _, listener := range tabsLayout.tabListener {
listener(tabsLayout, number, current)
}
tabsLayout.propertyChangedEvent(Current)
tabsLayout.setRaw(Current, number)
tabsLayout.currentChanged(number, current)
}
}
}
@ -901,7 +888,7 @@ func (tabsLayout *tabsLayoutData) handleCommand(self View, command string, data
case "tabCloseClick":
if numberText, ok := data.PropertyValue("number"); ok {
if number, err := strconv.Atoi(numberText); err == nil {
for _, listener := range tabsLayout.tabCloseListener {
for _, listener := range getOneArgEventListeners[TabsLayout, int](tabsLayout, nil, TabCloseEvent) {
listener(tabsLayout, number)
}
}

View File

@ -5,7 +5,7 @@ import (
"strings"
)
// TextView - text View
// TextView represents a TextView view
type TextView interface {
View
}
@ -23,116 +23,79 @@ func NewTextView(session Session, params Params) TextView {
}
func newTextView(session Session) View {
return NewTextView(session, nil)
return new(textViewData)
}
// Init initialize fields of TextView by default values
func (textView *textViewData) init(session Session) {
textView.viewData.init(session)
textView.tag = "TextView"
textView.set = textView.setFunc
textView.changed = textView.propertyChanged
}
func (textView *textViewData) String() string {
return getViewString(textView, nil)
}
func (textView *textViewData) propertyChanged(tag PropertyName) {
switch tag {
case Text:
updateInnerHTML(textView.htmlID(), textView.Session())
func (textView *textViewData) Get(tag string) any {
return textView.get(strings.ToLower(tag))
}
func (textView *textViewData) Remove(tag string) {
textView.remove(strings.ToLower(tag))
}
func (textView *textViewData) remove(tag string) {
textView.viewData.remove(tag)
if textView.created {
switch tag {
case Text:
updateInnerHTML(textView.htmlID(), textView.session)
case TextOverflow:
textView.textOverflowUpdated()
case TextOverflow:
session := textView.Session()
if n, ok := enumProperty(textView, TextOverflow, session, 0); ok {
values := enumProperties[TextOverflow].cssValues
if n >= 0 && n < len(values) {
session.updateCSSProperty(textView.htmlID(), string(TextOverflow), values[n])
return
}
}
session.updateCSSProperty(textView.htmlID(), string(TextOverflow), "")
case NotTranslate:
updateInnerHTML(textView.htmlID(), textView.Session())
default:
textView.viewData.propertyChanged(tag)
}
}
func (textView *textViewData) Set(tag string, value any) bool {
return textView.set(strings.ToLower(tag), value)
}
func (textView *textViewData) set(tag string, value any) bool {
func (textView *textViewData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case Text:
switch value := value.(type) {
case string:
textView.properties[Text] = value
textView.setRaw(Text, value)
case fmt.Stringer:
textView.properties[Text] = value.String()
textView.setRaw(Text, value.String())
case float32:
textView.properties[Text] = fmt.Sprintf("%g", float64(value))
textView.setRaw(Text, fmt.Sprintf("%g", float64(value)))
case float64:
textView.properties[Text] = fmt.Sprintf("%g", value)
textView.setRaw(Text, fmt.Sprintf("%g", value))
case []rune:
textView.properties[Text] = string(value)
textView.setRaw(Text, string(value))
case bool:
if value {
textView.properties[Text] = "true"
textView.setRaw(Text, "true")
} else {
textView.properties[Text] = "false"
textView.setRaw(Text, "false")
}
default:
if n, ok := isInt(value); ok {
textView.properties[Text] = fmt.Sprintf("%d", n)
textView.setRaw(Text, fmt.Sprintf("%d", n))
} else {
notCompatibleType(tag, value)
return false
return nil
}
}
if textView.created {
updateInnerHTML(textView.htmlID(), textView.session)
}
case TextOverflow:
if !textView.viewData.set(tag, value) {
return false
}
if textView.created {
textView.textOverflowUpdated()
}
case NotTranslate:
if !textView.viewData.set(tag, value) {
return false
}
if textView.created {
updateInnerHTML(textView.htmlID(), textView.Session())
}
default:
return textView.viewData.set(tag, value)
return []PropertyName{Text}
}
textView.propertyChangedEvent(tag)
return true
}
func (textView *textViewData) textOverflowUpdated() {
session := textView.Session()
if n, ok := enumProperty(textView, TextOverflow, session, 0); ok {
values := enumProperties[TextOverflow].cssValues
if n >= 0 && n < len(values) {
session.updateCSSProperty(textView.htmlID(), TextOverflow, values[n])
return
}
}
session.updateCSSProperty(textView.htmlID(), TextOverflow, "")
return textView.viewData.setFunc(tag, value)
}
func (textView *textViewData) htmlSubviews(self View, buffer *strings.Builder) {

View File

@ -7,18 +7,34 @@ import (
"strings"
)
// Constants used as a values for [MediaStyleParams] member Orientation
const (
DefaultMedia = 0
PortraitMedia = 1
// DefaultMedia means that style appliance will not be related to client's window orientation
DefaultMedia = 0
// PortraitMedia means that style apply on clients with portrait window orientation
PortraitMedia = 1
// PortraitMedia means that style apply on clients with landscape window orientation
LandscapeMedia = 2
)
// MediaStyleParams define rules when particular style will be applied
type MediaStyleParams struct {
// Orientation for which particular style will be applied
Orientation int
MinWidth int
MaxWidth int
MinHeight int
MaxHeight int
// MinWidth for which particular style will be applied
MinWidth int
// MaxWidth for which particular style will be applied
MaxWidth int
// MinHeight for which particular style will be applied
MinHeight int
// MaxHeight for which particular style will be applied
MaxHeight int
}
type mediaStyle struct {
@ -38,31 +54,65 @@ type theme struct {
mediaStyles []mediaStyle
}
// Theme interface to describe application's theme
type Theme interface {
fmt.Stringer
// Name returns a name of the theme
Name() string
// Constant returns normal and touch theme constant value with specific tag
Constant(tag string) (string, string)
// SetConstant sets a value for a constant
SetConstant(tag string, value, touchUIValue string)
// ConstantTags returns the list of all available constants
ConstantTags() []string
// Color returns normal and dark theme color constant value with specific tag
Color(tag string) (string, string)
// SetColor sets normal and dark theme color constant value with specific tag
SetColor(tag, color, darkUIColor string)
// ColorTags returns the list of all available color constants
ColorTags() []string
// Image returns normal and dark theme image constant value with specific tag
Image(tag string) (string, string)
// SetImage sets normal and dark theme image constant value with specific tag
SetImage(tag, image, darkUIImage string)
// ImageConstantTags returns the list of all available image constants
ImageConstantTags() []string
// Style returns view style by its tag
Style(tag string) ViewStyle
// SetStyle sets style for a tag
SetStyle(tag string, style ViewStyle)
// RemoveStyle removes style with provided tag
RemoveStyle(tag string)
// MediaStyle returns media style which correspond to provided media style parameters
MediaStyle(tag string, params MediaStyleParams) ViewStyle
// SetMediaStyle sets media style with provided media style parameters and a tag
SetMediaStyle(tag string, params MediaStyleParams, style ViewStyle)
// StyleTags returns all tags which describe a style
StyleTags() []string
// MediaStyles returns all media style settings which correspond to a style tag
MediaStyles(tag string) []struct {
Selectors string
Params MediaStyleParams
}
// Append theme to a list of themes
Append(anotherTheme Theme)
constant(tag string, touchUI bool) string
@ -196,6 +246,7 @@ func parseMediaRule(text string) (mediaStyle, bool) {
var defaultTheme = NewTheme("")
// NewTheme creates a new theme with specific name and return its interface.
func NewTheme(name string) Theme {
result := new(theme)
result.init()
@ -203,6 +254,7 @@ func NewTheme(name string) Theme {
return result
}
// CreateThemeFromText creates a new theme from text and return its interface on success.
func CreateThemeFromText(text string) (Theme, bool) {
result := new(theme)
result.init()
@ -589,7 +641,7 @@ func (theme *theme) cssText(session Session) string {
}
var builder cssStyleBuilder
builder.init()
builder.init(16)
styleList := func(styles map[string]ViewStyle) []string {
ruiStyles := []string{}
@ -647,13 +699,13 @@ func (theme *theme) addText(themeText string) bool {
if node := obj.Property(i); node != nil {
switch node.Type() {
case ArrayNode:
params[node.Tag()] = node.ArrayElements()
params[PropertyName(node.Tag())] = node.ArrayElements()
case ObjectNode:
params[node.Tag()] = node.Object()
params[PropertyName(node.Tag())] = node.Object()
default:
params[node.Tag()] = node.Text()
params[PropertyName(node.Tag())] = node.Text()
}
}
}

View File

@ -6,24 +6,101 @@ import (
"time"
)
// Constants for [TimePicker] specific properties and events.
const (
TimeChangedEvent = "time-changed"
TimePickerMin = "time-picker-min"
TimePickerMax = "time-picker-max"
TimePickerStep = "time-picker-step"
TimePickerValue = "time-picker-value"
timeFormat = "15:04:05"
// TimeChangedEvent is the constant for "time-changed" property tag.
//
// Used by TimePicker.
// Occur when current time of the time picker has been changed.
//
// General listener format:
// func(picker rui.TimePicker, newTime time.Time, oldTime time.Time).
//
// where:
// - picker - Interface of a time picker which generated this event,
// - newTime - New time value,
// - oldTime - Old time value.
//
// Allowed listener formats:
// func(picker rui.TimePicker, newTime time.Time),
// func(newTime time.Time, oldTime time.Time),
// func(newTime time.Time),
// func(picker rui.TimePicker),
// func().
TimeChangedEvent PropertyName = "time-changed"
// TimePickerMin is the constant for "time-picker-min" property tag.
//
// Used by TimePicker.
// The minimum value of the time.
//
// Supported types: time.Time, string.
//
// Internal type is time.Time, other types converted to it during assignment.
//
// Conversion rules:
// string - values of this type parsed and converted to time.Time. The following formats are supported:
// - "HH:MM:SS" - "08:15:00".
// - "HH:MM:SS PM" - "08:15:00 AM".
// - "HH:MM" - "08:15".
// - "HH:MM PM" - "08:15 AM".
TimePickerMin PropertyName = "time-picker-min"
// TimePickerMax is the constant for "time-picker-max" property tag.
//
// Used by TimePicker.
// The maximum value of the time.
//
// Supported types: time.Time, string.
//
// Internal type is time.Time, other types converted to it during assignment.
//
// Conversion rules:
// string - values of this type parsed and converted to time.Time. The following formats are supported:
// - "HH:MM:SS" - "08:15:00".
// - "HH:MM:SS PM" - "08:15:00 AM".
// - "HH:MM" - "08:15".
// - "HH:MM PM" - "08:15 AM".
TimePickerMax PropertyName = "time-picker-max"
// TimePickerStep is the constant for "time-picker-step" property tag.
//
// Used by TimePicker.
// Time step in seconds.
//
// Supported types: int, string.
//
// Values:
// positive value - Step value in seconds used to increment or decrement time.
TimePickerStep PropertyName = "time-picker-step"
// TimePickerValue is the constant for "time-picker-value" property tag.
//
// Used by TimePicker.
// Current value.
//
// Supported types: time.Time, string.
//
// Internal type is time.Time, other types converted to it during assignment.
//
// Conversion rules:
// string - values of this type parsed and converted to time.Time. The following formats are supported:
// - "HH:MM:SS" - "08:15:00".
// - "HH:MM:SS PM" - "08:15:00 AM".
// - "HH:MM" - "08:15".
// - "HH:MM PM" - "08:15 AM".
TimePickerValue PropertyName = "time-picker-value"
timeFormat = "15:04:05"
)
// TimePicker - TimePicker view
// TimePicker represents a TimePicker view
type TimePicker interface {
View
}
type timePickerData struct {
viewData
dataList
timeChangedListeners []func(TimePicker, time.Time, time.Time)
}
// NewTimePicker create new TimePicker object and return it
@ -35,236 +112,154 @@ func NewTimePicker(session Session, params Params) TimePicker {
}
func newTimePicker(session Session) View {
return NewTimePicker(session, nil)
return new(timePickerData)
}
func (picker *timePickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "TimePicker"
picker.hasHtmlDisabled = true
picker.timeChangedListeners = []func(TimePicker, time.Time, time.Time){}
picker.dataListInit()
}
func (picker *timePickerData) String() string {
return getViewString(picker, nil)
picker.normalize = normalizeTimePickerTag
picker.set = picker.setFunc
picker.changed = picker.propertyChanged
}
func (picker *timePickerData) Focusable() bool {
return true
}
func (picker *timePickerData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeTimePickerTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Type, Min, Max, Step, Value:
return "time-picker-" + tag
}
return tag
return normalizeDataListTag(tag)
}
func (picker *timePickerData) Remove(tag string) {
picker.remove(picker.normalizeTag(tag))
}
func stringToTime(value string) (time.Time, bool) {
lowText := strings.ToUpper(value)
pm := strings.HasSuffix(lowText, "PM") || strings.HasSuffix(lowText, "AM")
func (picker *timePickerData) remove(tag string) {
switch tag {
case TimeChangedEvent:
if len(picker.timeChangedListeners) > 0 {
picker.timeChangedListeners = []func(TimePicker, time.Time, time.Time){}
picker.propertyChangedEvent(tag)
}
return
case TimePickerMin:
delete(picker.properties, TimePickerMin)
if picker.created {
picker.session.removeProperty(picker.htmlID(), Min)
}
case TimePickerMax:
delete(picker.properties, TimePickerMax)
if picker.created {
picker.session.removeProperty(picker.htmlID(), Max)
}
case TimePickerStep:
delete(picker.properties, TimePickerStep)
if picker.created {
picker.session.removeProperty(picker.htmlID(), Step)
}
case TimePickerValue:
if _, ok := picker.properties[TimePickerValue]; ok {
oldTime := GetTimePickerValue(picker)
delete(picker.properties, TimePickerValue)
time := GetTimePickerValue(picker)
if picker.created {
picker.session.callFunc("setInputValue", picker.htmlID(), time.Format(timeFormat))
}
for _, listener := range picker.timeChangedListeners {
listener(picker, time, oldTime)
}
var format string
switch len(strings.Split(value, ":")) {
case 2:
if pm {
format = "3:04 PM"
} else {
return
}
case DataList:
if len(picker.dataList.dataList) > 0 {
picker.setDataList(picker, []string{}, true)
format = "15:04"
}
default:
picker.viewData.remove(tag)
return
}
picker.propertyChangedEvent(tag)
}
func (picker *timePickerData) Set(tag string, value any) bool {
return picker.set(picker.normalizeTag(tag), value)
}
func (picker *timePickerData) set(tag string, value any) bool {
if value == nil {
picker.remove(tag)
return true
if pm {
format = "03:04:05 PM"
} else {
format = "15:04:05"
}
}
setTimeValue := func(tag string) (time.Time, bool) {
result, err := time.Parse(format, value)
if err != nil {
ErrorLog(err.Error())
return time.Now(), false
}
return result, true
}
func (picker *timePickerData) setFunc(tag PropertyName, value any) []PropertyName {
setTimeValue := func(tag PropertyName) []PropertyName {
switch value := value.(type) {
case time.Time:
picker.properties[tag] = value
return value, true
picker.setRaw(tag, value)
return []PropertyName{tag}
case string:
if text, ok := picker.Session().resolveConstants(value); ok {
lowText := strings.ToLower(text)
pm := strings.HasSuffix(lowText, "pm") || strings.HasSuffix(lowText, "am")
if isConstantName(value) {
picker.setRaw(tag, value)
return []PropertyName{tag}
}
var format string
switch len(strings.Split(text, ":")) {
case 2:
if pm {
format = "3:04 PM"
} else {
format = "15:04"
}
default:
if pm {
format = "03:04:05 PM"
} else {
format = "15:04:05"
}
}
if time, err := time.Parse(format, text); err == nil {
picker.properties[tag] = value
return time, true
} else {
ErrorLog(err.Error())
}
return time.Now(), false
if time, ok := stringToTime(value); ok {
picker.setRaw(tag, time)
return []PropertyName{tag}
}
}
notCompatibleType(tag, value)
return time.Now(), false
return nil
}
switch tag {
case TimePickerMin:
old, oldOK := getTimeProperty(picker, TimePickerMin, Min)
if time, ok := setTimeValue(TimePickerMin); ok {
if !oldOK || time != old {
if picker.created {
picker.session.updateProperty(picker.htmlID(), Min, time.Format(timeFormat))
}
picker.propertyChangedEvent(tag)
}
return true
return setTimeValue(TimePickerMin)
case TimePickerMax:
return setTimeValue(TimePickerMax)
case TimePickerStep:
return setIntProperty(picker, TimePickerStep, value)
case TimePickerValue:
picker.setRaw("old-time", GetTimePickerValue(picker))
return setTimeValue(tag)
case TimeChangedEvent:
return setTwoArgEventListener[TimePicker, time.Time](picker, tag, value)
case DataList:
return setDataList(picker, value, timeFormat)
}
return picker.viewData.setFunc(tag, value)
}
func (picker *timePickerData) propertyChanged(tag PropertyName) {
session := picker.Session()
switch tag {
case TimePickerMin:
if time, ok := GetTimePickerMin(picker); ok {
session.updateProperty(picker.htmlID(), "min", time.Format(timeFormat))
} else {
session.removeProperty(picker.htmlID(), "min")
}
case TimePickerMax:
old, oldOK := getTimeProperty(picker, TimePickerMax, Max)
if time, ok := setTimeValue(TimePickerMax); ok {
if !oldOK || time != old {
if picker.created {
picker.session.updateProperty(picker.htmlID(), Max, time.Format(timeFormat))
}
picker.propertyChangedEvent(tag)
}
return true
if time, ok := GetTimePickerMax(picker); ok {
session.updateProperty(picker.htmlID(), "max", time.Format(timeFormat))
} else {
session.removeProperty(picker.htmlID(), "max")
}
case TimePickerStep:
oldStep := GetTimePickerStep(picker)
if picker.setIntProperty(TimePickerStep, value) {
if step := GetTimePickerStep(picker); oldStep != step {
if picker.created {
if step > 0 {
picker.session.updateProperty(picker.htmlID(), Step, strconv.Itoa(step))
} else {
picker.session.removeProperty(picker.htmlID(), Step)
}
}
picker.propertyChangedEvent(tag)
}
return true
if step := GetTimePickerStep(picker); step > 0 {
session.updateProperty(picker.htmlID(), "step", strconv.Itoa(step))
} else {
session.removeProperty(picker.htmlID(), "step")
}
case TimePickerValue:
oldTime := GetTimePickerValue(picker)
if time, ok := setTimeValue(TimePickerValue); ok {
if time != oldTime {
if picker.created {
picker.session.callFunc("setInputValue", picker.htmlID(), time.Format(timeFormat))
value := GetTimePickerValue(picker)
session.callFunc("setInputValue", picker.htmlID(), value.Format(timeFormat))
if listeners := GetTimeChangedListeners(picker); len(listeners) > 0 {
oldTime := time.Now()
if val := picker.getRaw("old-time"); val != nil {
if time, ok := val.(time.Time); ok {
oldTime = time
}
for _, listener := range picker.timeChangedListeners {
listener(picker, time, oldTime)
}
picker.propertyChangedEvent(tag)
}
return true
for _, listener := range listeners {
listener(picker, value, oldTime)
}
}
case TimeChangedEvent:
listeners, ok := valueToEventWithOldListeners[TimePicker, time.Time](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(TimePicker, time.Time, time.Time){}
}
picker.timeChangedListeners = listeners
picker.propertyChangedEvent(tag)
return true
case DataList:
return picker.setDataList(picker, value, picker.created)
default:
return picker.viewData.set(tag, value)
}
return false
}
func (picker *timePickerData) Get(tag string) any {
return picker.get(picker.normalizeTag(tag))
}
func (picker *timePickerData) get(tag string) any {
switch tag {
case TimeChangedEvent:
return picker.timeChangedListeners
case DataList:
return picker.dataList.dataList
default:
return picker.viewData.get(tag)
picker.viewData.propertyChanged(tag)
}
}
@ -273,7 +268,13 @@ func (picker *timePickerData) htmlTag() string {
}
func (picker *timePickerData) htmlSubviews(self View, buffer *strings.Builder) {
picker.dataListHtmlSubviews(self, buffer)
dataListHtmlSubviews(self, buffer, func(text string, session Session) string {
text, _ = session.resolveConstants(text)
if time, ok := stringToTime(text); ok {
return time.Format(timeFormat)
}
return text
})
}
func (picker *timePickerData) htmlProperties(self View, buffer *strings.Builder) {
@ -308,20 +309,24 @@ func (picker *timePickerData) htmlProperties(self View, buffer *strings.Builder)
buffer.WriteString(` onclick="stopEventPropagation(this, event)"`)
}
picker.dataListHtmlProperties(picker, buffer)
dataListHtmlProperties(picker, buffer)
}
func (picker *timePickerData) handleCommand(self View, command string, data DataObject) bool {
func (picker *timePickerData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command {
case "textChanged":
if text, ok := data.PropertyValue("text"); ok {
if value, err := time.Parse(timeFormat, text); err == nil {
if value, ok := stringToTime(text); ok {
oldValue := GetTimePickerValue(picker)
picker.properties[TimePickerValue] = value
if value != oldValue {
for _, listener := range picker.timeChangedListeners {
for _, listener := range GetTimeChangedListeners(picker) {
listener(picker, value, oldValue)
}
if listener, ok := picker.changeListener[TimePickerValue]; ok {
listener(picker, TimePickerValue)
}
}
}
}
@ -331,7 +336,7 @@ func (picker *timePickerData) handleCommand(self View, command string, data Data
return picker.viewData.handleCommand(self, command, data)
}
func getTimeProperty(view View, mainTag, shortTag string) (time.Time, bool) {
func getTimeProperty(view View, mainTag, shortTag PropertyName) (time.Time, bool) {
valueToTime := func(value any) (time.Time, bool) {
if value != nil {
switch value := value.(type) {
@ -340,7 +345,7 @@ func getTimeProperty(view View, mainTag, shortTag string) (time.Time, bool) {
case string:
if text, ok := view.Session().resolveConstants(value); ok {
if result, err := time.Parse(timeFormat, text); err == nil {
if result, ok := stringToTime(text); ok {
return result, true
}
}
@ -354,9 +359,11 @@ func getTimeProperty(view View, mainTag, shortTag string) (time.Time, bool) {
return result, true
}
if value := valueFromStyle(view, shortTag); value != nil {
if result, ok := valueToTime(value); ok {
return result, true
for _, tag := range []PropertyName{mainTag, shortTag} {
if value := valueFromStyle(view, tag); value != nil {
if result, ok := valueToTime(value); ok {
return result, true
}
}
}
}
@ -368,10 +375,7 @@ func getTimeProperty(view View, mainTag, shortTag string) (time.Time, bool) {
// "false" as the second value otherwise.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetTimePickerMin(view View, subviewID ...string) (time.Time, bool) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
return getTimeProperty(view, TimePickerMin, Min)
}
return time.Now(), false
@ -381,10 +385,7 @@ func GetTimePickerMin(view View, subviewID ...string) (time.Time, bool) {
// "false" as the second value otherwise.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetTimePickerMax(view View, subviewID ...string) (time.Time, bool) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
return getTimeProperty(view, TimePickerMax, Max)
}
return time.Now(), false
@ -399,10 +400,7 @@ func GetTimePickerStep(view View, subviewID ...string) int {
// GetTimePickerValue returns the time of TimePicker subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetTimePickerValue(view View, subviewID ...string) time.Time {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view == nil {
if view = getSubview(view, subviewID); view == nil {
return time.Now()
}
time, _ := getTimeProperty(view, TimePickerValue, Value)
@ -413,5 +411,5 @@ func GetTimePickerValue(view View, subviewID ...string) time.Time {
// 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.
func GetTimeChangedListeners(view View, subviewID ...string) []func(TimePicker, time.Time, time.Time) {
return getEventWithOldListeners[TimePicker, time.Time](view, subviewID, TimeChangedEvent)
return getTwoArgEventListeners[TimePicker, time.Time](view, subviewID, TimeChangedEvent)
}

View File

@ -2,34 +2,90 @@ package rui
import (
"strconv"
"strings"
)
// Constants which represent [View] specific touch events properties
const (
// TouchStart is the constant for "touch-start" property tag.
// The "touch-start" event is fired when one or more touch points are placed on the touch surface.
// The main listener format: func(View, TouchEvent).
// The additional listener formats: func(TouchEvent), func(View), and func().
TouchStart = "touch-start"
//
// Used by View.
// Is fired when one or more touch points are placed on the touch surface.
//
// General listener format:
//
// func(view rui.View, event rui.TouchEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Touch event.
//
// Allowed listener formats:
//
// func(event rui.TouchEvent)
// func(view rui.View)
// func()
TouchStart PropertyName = "touch-start"
// TouchEnd is the constant for "touch-end" property tag.
// The "touch-end" event fires when one or more touch points are removed from the touch surface.
// The main listener format: func(View, TouchEvent).
// The additional listener formats: func(TouchEvent), func(View), and func().
TouchEnd = "touch-end"
//
// Used by View.
// Fired when one or more touch points are removed from the touch surface.
//
// General listener format:
//
// func(view rui.View, event rui.TouchEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Touch event.
//
// Allowed listener formats:
//
// func(event rui.TouchEvent)
// func(view rui.View)
// func()
TouchEnd PropertyName = "touch-end"
// TouchMove is the constant for "touch-move" property tag.
// The "touch-move" event is fired when one or more touch points are moved along the touch surface.
// The main listener format: func(View, TouchEvent).
// The additional listener formats: func(TouchEvent), func(View), and func().
TouchMove = "touch-move"
//
// Used by View.
// Is fired when one or more touch points are moved along the touch surface.
//
// General listener format:
//
// func(view rui.View, event rui.TouchEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Touch event.
//
// Allowed listener formats:
//
// func(event rui.TouchEvent)
// func(view rui.View)
// func()
TouchMove PropertyName = "touch-move"
// TouchCancel is the constant for "touch-cancel" property tag.
// The "touch-cancel" event is fired when one or more touch points have been disrupted
// in an implementation-specific manner (for example, too many touch points are created).
// The main listener format: func(View, TouchEvent).
// The additional listener formats: func(TouchEvent), func(View), and func().
TouchCancel = "touch-cancel"
//
// Used by View.
// Is fired when one or more touch points have been disrupted in an implementation-specific manner (for example, too many
// touch points are created).
//
// General listener format:
//
// func(view rui.View, event rui.TouchEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Touch event.
//
// Allowed listener formats:
//
// func(event rui.TouchEvent)
// func(view rui.View)
// func()
TouchCancel PropertyName = "touch-cancel"
)
// Touch contains parameters of a single touch of a touch event
@ -41,22 +97,26 @@ type Touch struct {
// X provides the horizontal coordinate within the view's viewport.
X float64
// Y provides the vertical coordinate within the view's viewport.
Y float64
// ClientX provides the horizontal coordinate within the application's viewport at which the event occurred.
ClientX float64
// ClientY provides the vertical coordinate within the application's viewport at which the event occurred.
ClientY float64
// ScreenX provides the horizontal coordinate (offset) of the touch pointer in global (screen) coordinates.
ScreenX float64
// ScreenY provides the vertical coordinate (offset) of the touch pointer in global (screen) coordinates.
ScreenY float64
// RadiusX is the X radius of the ellipse that most closely circumscribes the area of contact with the screen.
// The value is in pixels of the same scale as screenX.
RadiusX float64
// RadiusY is the Y radius of the ellipse that most closely circumscribes the area of contact with the screen.
// The value is in pixels of the same scale as screenX.
RadiusY float64
@ -90,55 +150,6 @@ type TouchEvent struct {
MetaKey bool
}
var touchEvents = map[string]struct{ jsEvent, jsFunc string }{
TouchStart: {jsEvent: "ontouchstart", jsFunc: "touchStartEvent"},
TouchEnd: {jsEvent: "ontouchend", jsFunc: "touchEndEvent"},
TouchMove: {jsEvent: "ontouchmove", jsFunc: "touchMoveEvent"},
TouchCancel: {jsEvent: "ontouchcancel", jsFunc: "touchCancelEvent"},
}
func (view *viewData) setTouchListener(tag string, value any) bool {
listeners, ok := valueToEventListeners[View, TouchEvent](value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeTouchListener(tag)
} else if js, ok := touchEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
view.session.updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)")
}
} else {
return false
}
return true
}
func (view *viewData) removeTouchListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := touchEvents[tag]; ok {
view.session.removeProperty(view.htmlID(), js.jsEvent)
}
}
}
func touchEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range touchEvents {
if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View, TouchEvent)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
}
}
}
}
func (event *TouchEvent) init(data DataObject) {
event.Touches = []Touch{}
@ -172,8 +183,8 @@ func (event *TouchEvent) init(data DataObject) {
event.MetaKey = dataBoolProperty(data, "metaKey")
}
func handleTouchEvents(view View, tag string, data DataObject) {
listeners := getEventListeners[View, TouchEvent](view, nil, tag)
func handleTouchEvents(view View, tag PropertyName, data DataObject) {
listeners := getOneArgEventListeners[View, TouchEvent](view, nil, tag)
if len(listeners) == 0 {
return
}
@ -189,23 +200,23 @@ func handleTouchEvents(view View, tag string, data DataObject) {
// GetTouchStartListeners returns the "touch-start" 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.
func GetTouchStartListeners(view View, subviewID ...string) []func(View, TouchEvent) {
return getEventListeners[View, TouchEvent](view, subviewID, TouchStart)
return getOneArgEventListeners[View, TouchEvent](view, subviewID, TouchStart)
}
// GetTouchEndListeners returns the "touch-end" 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.
func GetTouchEndListeners(view View, subviewID ...string) []func(View, TouchEvent) {
return getEventListeners[View, TouchEvent](view, subviewID, TouchEnd)
return getOneArgEventListeners[View, TouchEvent](view, subviewID, TouchEnd)
}
// GetTouchMoveListeners returns the "touch-move" 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.
func GetTouchMoveListeners(view View, subviewID ...string) []func(View, TouchEvent) {
return getEventListeners[View, TouchEvent](view, subviewID, TouchMove)
return getOneArgEventListeners[View, TouchEvent](view, subviewID, TouchMove)
}
// GetTouchCancelListeners returns the "touch-cancel" 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.
func GetTouchCancelListeners(view View, subviewID ...string) []func(View, TouchEvent) {
return getEventListeners[View, TouchEvent](view, subviewID, TouchCancel)
return getOneArgEventListeners[View, TouchEvent](view, subviewID, TouchCancel)
}

739
transform.go Normal file
View File

@ -0,0 +1,739 @@
package rui
import (
"fmt"
"math"
"strings"
)
// Constants for [TransformProperty] specific properties
const (
// Transform is the constant for "transform" property tag.
//
// Used by View.
// Specify translation, scale and rotation over x, y and z axes as well as a distortion of a view along x and y axes.
//
// Supported types: TransformProperty, string.
//
// See TransformProperty description for more details.
//
// Conversion rules:
// - TransformProperty - stored as is, no conversion performed.
// - string - string representation of TransformProperty interface. Example: "_{translate-x = 10px, scale-y = 1.1}".
Transform PropertyName = "transform"
// Perspective is the constant for "perspective" property tag.
//
// Used by View, TransformProperty.
// Distance between the z-plane and the user in order to give a 3D-positioned element some perspective. Each 3D element
// with z > 0 becomes larger, each 3D-element with z < 0 becomes smaller. The default value is 0 (no 3D effects).
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
Perspective PropertyName = "perspective"
// PerspectiveOriginX is the constant for "perspective-origin-x" property tag.
//
// Used by View.
// x-coordinate of the position at which the viewer is looking. It is used as the vanishing point by the "perspective"
// property. The default value is 50%.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
PerspectiveOriginX PropertyName = "perspective-origin-x"
// PerspectiveOriginY is the constant for "perspective-origin-y" property tag.
//
// Used by View.
// y-coordinate of the position at which the viewer is looking. It is used as the vanishing point by the "perspective"
// property. The default value is 50%.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
PerspectiveOriginY PropertyName = "perspective-origin-y"
// BackfaceVisible is the constant for "backface-visibility" property tag.
//
// Used by View.
// Controls whether the back face of a view is visible when turned towards the user. Default value is true.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", "1" - Back face is visible when turned towards the user.
// - false, 0, "false", "no", "off", "0" - Back face is hidden, effectively making the view invisible when turned away from the user.
BackfaceVisible PropertyName = "backface-visibility"
// TransformOriginX is the constant for "transform-origin-x" property tag.
//
// Used by View.
// x-coordinate of the point around which a view transformation is applied. The default value is 50%.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
TransformOriginX PropertyName = "transform-origin-x"
// TransformOriginY is the constant for "transform-origin-y" property tag.
//
// Used by View.
// y-coordinate of the point around which a view transformation is applied. The default value is 50%.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
TransformOriginY PropertyName = "transform-origin-y"
// TransformOriginZ is the constant for "transform-origin-z" property tag.
//
// Used by View.
// z-coordinate of the point around which a view transformation is applied. The default value is 50%.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
TransformOriginZ PropertyName = "transform-origin-z"
// TranslateX is the constant for "translate-x" property tag.
//
// Used by View, TransformProperty.
//
// x-axis translation value of a 2D/3D translation.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
TranslateX PropertyName = "translate-x"
// TranslateY is the constant for "translate-y" property tag.
//
// Used by View, TransformProperty.
//
// x-axis translation value of a 2D/3D translation.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
TranslateY PropertyName = "translate-y"
// TranslateZ is the constant for "translate-z" property tag.
//
// Used by View, TransformProperty.
//
// z-axis translation value of a 3D translation.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
TranslateZ PropertyName = "translate-z"
// ScaleX is the constant for "scale-x" property tag.
//
// Used by View, TransformProperty.
//
// x-axis scaling value of a 2D/3D scale. The original scale is 1. Values between 0 and 1 are used to decrease original
// scale, more than 1 - to increase. The default value is 1.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
ScaleX PropertyName = "scale-x"
// ScaleY is the constant for "scale-y" property tag.
//
// Used by View, TransformProperty.
//
// y-axis scaling value of a 2D/3D scale. The original scale is 1. Values between 0 and 1 are used to decrease original
// scale, more than 1 - to increase. The default value is 1.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
ScaleY PropertyName = "scale-y"
// ScaleZ is the constant for "scale-z" property tag.
//
// Used by View, TransformProperty.
//
// z-axis scaling value of a 3D scale. The original scale is 1. Values between 0 and 1 are used to decrease original
// scale, more than 1 - to increase. The default value is 1.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
ScaleZ PropertyName = "scale-z"
// Rotate is the constant for "rotate" property tag.
//
// Used by View, TransformProperty.
//
// Angle of the view rotation. A positive angle denotes a clockwise rotation, a negative angle a counter-clockwise.
//
// Supported types: AngleUnit, string, float, int.
//
// Internal type is AngleUnit, other types will be converted to it during assignment.
// See AngleUnit description for more details.
//
// Conversion rules:
// - AngleUnit - stored as is, no conversion performed.
// - string - must contain string representation of AngleUnit. If numeric value will be provided without any suffix then AngleUnit with value and Radian value type will be created.
// - float - a new AngleUnit value will be created with Radian as a type.
// - int - a new AngleUnit value will be created with Radian as a type.
Rotate PropertyName = "rotate"
// RotateX is the constant for "rotate-x" property tag.
//
// Used by View, TransformProperty.
//
// x-coordinate of the vector denoting the axis of rotation in range 0 to 1.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
RotateX PropertyName = "rotate-x"
// RotateY is the constant for "rotate-y" property tag.
//
// Used by View, TransformProperty.
//
// y-coordinate of the vector denoting the axis of rotation in range 0 to 1.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
RotateY PropertyName = "rotate-y"
// RotateZ is the constant for "rotate-z" property tag.
//
// Used by View, TransformProperty.
//
// z-coordinate of the vector denoting the axis of rotation in range 0 to 1.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
RotateZ PropertyName = "rotate-z"
// SkewX is the constant for "skew-x" property tag.
//
// Used by View, TransformProperty.
//
// Angle to use to distort the element along the abscissa. The default value is 0.
//
// Supported types: AngleUnit, string, float, int.
//
// Internal type is AngleUnit, other types will be converted to it during assignment.
// See AngleUnit description for more details.
//
// Conversion rules:
// - AngleUnit - stored as is, no conversion performed.
// - string - must contain string representation of AngleUnit. If numeric value will be provided without any suffix then AngleUnit with value and Radian value type will be created.
// - float - a new AngleUnit value will be created with Radian as a type.
// - int - a new AngleUnit value will be created with Radian as a type.
SkewX PropertyName = "skew-x"
// SkewY is the constant for "skew-y" property tag.
//
// Used by View, TransformProperty.
//
// Angle to use to distort the element along the ordinate. The default value is 0.
//
// Supported types: AngleUnit, string, float, int.
//
// Internal type is AngleUnit, other types will be converted to it during assignment.
// See AngleUnit description for more details.
//
// Conversion rules:
// - AngleUnit - stored as is, no conversion performed.
// - string - must contain string representation of AngleUnit. If numeric value will be provided without any suffix then AngleUnit with value and Radian value type will be created.
// - float - a new AngleUnit value will be created with Radian as a type.
// - int - a new AngleUnit value will be created with Radian as a type.
SkewY PropertyName = "skew-y"
)
// TransformProperty interface specifies view transformation parameters: the x-, y-, and z-axis translation values,
// the x-, y-, and z-axis scaling values, the angle to use to distort the element along the abscissa and ordinate,
// the angle of the view rotation.
//
// Valid property tags: Perspective ("perspective"), TranslateX ("translate-x"), TranslateY ("translate-y"), TranslateZ ("translate-z"),
// ScaleX ("scale-x"), ScaleY ("scale-y"), ScaleZ ("scale-z"), Rotate ("rotate"), RotateX ("rotate-x"),
// RotateY ("rotate-y"), RotateZ ("rotate-z"), SkewX ("skew-x"), and SkewY ("skew-y")
type TransformProperty interface {
Properties
fmt.Stringer
stringWriter
transformCSS(session Session) string
getSkew(session Session) (AngleUnit, AngleUnit, bool)
getTranslate(session Session) (SizeUnit, SizeUnit, SizeUnit)
}
type transformPropertyData struct {
dataProperty
}
// NewTransform creates a new transform property data and return its interface
//
// The following properties can be used:
//
// Perspective ("perspective"), TranslateX ("translate-x"), TranslateY ("translate-y"), TranslateZ ("translate-z"),
// ScaleX ("scale-x"), ScaleY ("scale-y"), ScaleZ ("scale-z"), Rotate ("rotate"), RotateX ("rotate-x"),
// RotateY ("rotate-y"), RotateZ ("rotate-z"), SkewX ("skew-x"), and SkewY ("skew-y")
func NewTransformProperty(params Params) TransformProperty {
transform := new(transformPropertyData)
transform.init()
for tag, value := range params {
transform.Set(tag, value)
}
return transform
}
func (transform *transformPropertyData) init() {
transform.dataProperty.init()
transform.normalize = normalizeTransformTag
transform.set = transformSet
transform.supportedProperties = []PropertyName{
RotateX, RotateY, RotateZ, Rotate, SkewX, SkewY, ScaleX, ScaleY, ScaleZ,
Perspective, TranslateX, TranslateY, TranslateZ,
}
}
func normalizeTransformTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
name := string(tag)
if strings.HasPrefix(name, "push-") {
tag = PropertyName(name[5:])
}
return tag
}
func (transform *transformPropertyData) String() string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
transform.writeString(buffer, "")
return buffer.String()
}
func (transform *transformPropertyData) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("_{ ")
comma := false
for _, tag := range transform.supportedProperties {
if value, ok := transform.properties[tag]; ok {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
writePropertyValue(buffer, tag, value, indent)
comma = true
}
}
buffer.WriteString(" }")
}
func transformSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case RotateX, RotateY, RotateZ:
return setFloatProperty(properties, tag, value, 0, 1)
case Rotate, SkewX, SkewY:
return setAngleProperty(properties, tag, value)
case ScaleX, ScaleY, ScaleZ:
return setFloatProperty(properties, tag, value, -math.MaxFloat64, math.MaxFloat64)
case Perspective, TranslateX, TranslateY, TranslateZ:
return setSizeProperty(properties, tag, value)
}
return nil
}
func valueToTransformProperty(value any) TransformProperty {
parseObject := func(obj DataObject) TransformProperty {
transform := NewTransformProperty(nil)
ok := true
for i := 0; i < obj.PropertyCount(); i++ {
if prop := obj.Property(i); prop.Type() == TextNode {
if !transform.Set(PropertyName(prop.Tag()), prop.Text()) {
ok = false
}
} else {
ok = false
}
}
if !ok && transform.empty() {
return nil
}
return transform
}
switch value := value.(type) {
case TransformProperty:
return value
case DataObject:
return parseObject(value)
case DataNode:
if obj := value.Object(); obj != nil {
return parseObject(obj)
}
case string:
if obj := ParseDataText(value); obj != nil {
return parseObject(obj)
}
}
return nil
}
func setTransformProperty(properties Properties, tag PropertyName, value any) bool {
if transform := valueToTransformProperty(value); transform != nil {
properties.setRaw(tag, transform)
return true
}
notCompatibleType(tag, value)
return false
}
func getTransformProperty(properties Properties, tag PropertyName) TransformProperty {
if val := properties.getRaw(tag); val != nil {
if transform, ok := val.(TransformProperty); ok {
return transform
}
}
return nil
}
func setTransformPropertyElement(properties Properties, tag, transformTag PropertyName, value any) []PropertyName {
srcTag := tag
tag = normalizeTransformTag(tag)
switch tag {
case Perspective, RotateX, RotateY, RotateZ, Rotate, SkewX, SkewY, ScaleX, ScaleY, ScaleZ, TranslateX, TranslateY, TranslateZ:
var result []PropertyName = nil
if transform := getTransformProperty(properties, transformTag); transform != nil {
if result = transformSet(transform, tag, value); result != nil {
result = []PropertyName{srcTag, transformTag}
}
} else {
transform := NewTransformProperty(nil)
if result = transformSet(transform, tag, value); result != nil {
properties.setRaw(transformTag, transform)
result = []PropertyName{srcTag, transformTag}
}
}
return result
}
ErrorLogF(`"TransformProperty" interface does not support the "%s" property`, tag)
return nil
}
func getPerspectiveOrigin(style Properties, session Session) (SizeUnit, SizeUnit) {
x, _ := sizeProperty(style, PerspectiveOriginX, session)
y, _ := sizeProperty(style, PerspectiveOriginY, session)
return x, y
}
func getTransformOrigin(style Properties, session Session) (SizeUnit, SizeUnit, SizeUnit) {
x, _ := sizeProperty(style, TransformOriginX, session)
y, _ := sizeProperty(style, TransformOriginY, session)
z, _ := sizeProperty(style, TransformOriginZ, session)
return x, y, z
}
func (transform *transformPropertyData) getSkew(session Session) (AngleUnit, AngleUnit, bool) {
skewX, okX := angleProperty(transform, SkewX, session)
skewY, okY := angleProperty(transform, SkewY, session)
return skewX, skewY, okX || okY
}
func (transform *transformPropertyData) getTranslate(session Session) (SizeUnit, SizeUnit, SizeUnit) {
x, _ := sizeProperty(transform, TranslateX, session)
y, _ := sizeProperty(transform, TranslateY, session)
z, _ := sizeProperty(transform, TranslateZ, session)
return x, y, z
}
func (transform *transformPropertyData) transformCSS(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if perspective, ok := sizeProperty(transform, Perspective, session); ok && perspective.Type != Auto && perspective.Value != 0 {
buffer.WriteString(`perspective(`)
buffer.WriteString(perspective.cssString("0", session))
buffer.WriteString(") ")
}
skewX, skewY, skewOK := transform.getSkew(session)
if skewOK {
buffer.WriteString(`skew(`)
buffer.WriteString(skewX.cssString())
buffer.WriteRune(',')
buffer.WriteString(skewY.cssString())
buffer.WriteString(") ")
}
x, y, z := transform.getTranslate(session)
if z.Type != Auto && z.Value != 0 {
buffer.WriteString(`translate3d(`)
buffer.WriteString(x.cssString("0px", session))
buffer.WriteRune(',')
buffer.WriteString(y.cssString("0px", session))
buffer.WriteRune(',')
buffer.WriteString(z.cssString("0px", session))
buffer.WriteString(") ")
} else if (x.Type != Auto && x.Value != 0) || (y.Type != Auto && y.Value != 0) {
buffer.WriteString(`translate(`)
buffer.WriteString(x.cssString("0px", session))
buffer.WriteRune(',')
buffer.WriteString(y.cssString("0px", session))
buffer.WriteString(") ")
}
scaleX, okScaleX := floatTextProperty(transform, ScaleX, session, 1)
scaleY, okScaleY := floatTextProperty(transform, ScaleY, session, 1)
scaleZ, okScaleZ := floatTextProperty(transform, ScaleZ, session, 1)
if okScaleZ {
buffer.WriteString(`scale3d(`)
buffer.WriteString(scaleX)
buffer.WriteRune(',')
buffer.WriteString(scaleY)
buffer.WriteRune(',')
buffer.WriteString(scaleZ)
buffer.WriteString(") ")
} else if okScaleX || okScaleY {
buffer.WriteString(`scale(`)
buffer.WriteString(scaleX)
buffer.WriteRune(',')
buffer.WriteString(scaleY)
buffer.WriteString(") ")
}
if angle, ok := angleProperty(transform, Rotate, session); ok {
rotateX, xOK := floatTextProperty(transform, RotateX, session, 1)
rotateY, yOK := floatTextProperty(transform, RotateY, session, 1)
rotateZ, zOK := floatTextProperty(transform, RotateZ, session, 1)
if xOK || yOK || zOK {
buffer.WriteString(`rotate3d(`)
buffer.WriteString(rotateX)
buffer.WriteRune(',')
buffer.WriteString(rotateY)
buffer.WriteRune(',')
buffer.WriteString(rotateZ)
buffer.WriteRune(',')
buffer.WriteString(angle.cssString())
buffer.WriteString(") ")
} else {
buffer.WriteString(`rotate(`)
buffer.WriteString(angle.cssString())
buffer.WriteString(") ")
}
}
length := buffer.Len()
if length == 0 {
return ""
}
result := buffer.String()
return result[:length-1]
}
func (style *viewStyle) writeViewTransformCSS(builder cssBuilder, session Session) {
x, y := getPerspectiveOrigin(style, session)
z := AutoSize()
if css := transformOriginCSS(x, y, z, session); css != "" {
builder.add(`perspective-origin`, css)
}
if backfaceVisible, ok := boolProperty(style, BackfaceVisible, session); ok {
if backfaceVisible {
builder.add(`backface-visibility`, `visible`)
} else {
builder.add(`backface-visibility`, `hidden`)
}
}
x, y, z = getTransformOrigin(style, session)
if css := transformOriginCSS(x, y, z, session); css != "" {
builder.add(`transform-origin`, css)
}
if transform := getTransformProperty(style, Transform); transform != nil {
builder.add(`transform`, transform.transformCSS(session))
}
}
func transformOriginCSS(x, y, z SizeUnit, session Session) string {
if z.Type == Auto && x.Type == Auto && y.Type == Auto {
return ""
}
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if x.Type == SizeInPercent {
switch x.Value {
case 0:
buffer.WriteString("left")
case 50:
buffer.WriteString("center")
case 100:
buffer.WriteString("right")
default:
buffer.WriteString(x.cssString("center", session))
}
} else {
buffer.WriteString(x.cssString("center", session))
}
buffer.WriteRune(' ')
if y.Type == SizeInPercent {
switch y.Value {
case 0:
buffer.WriteString("top")
case 50:
buffer.WriteString("center")
case 100:
buffer.WriteString("bottom")
default:
buffer.WriteString(y.cssString("center", session))
}
} else {
buffer.WriteString(y.cssString("center", session))
}
if z.Type != Auto && z.Value != 0 {
buffer.WriteRune(' ')
buffer.WriteString(z.cssString("0", session))
}
return buffer.String()
}
// GetTransform returns a view transform: translation, scale and rotation over x, y and z axes as well as a distortion of a view along x and y axes.
// The default value is nil (no transform)
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetTransform(view View, subviewID ...string) TransformProperty {
return transformStyledProperty(view, subviewID, Transform)
}
// GetPerspective returns a distance between the z = 0 plane and the user in order to give a 3D-positioned
// element some perspective. Each 3D element with z > 0 becomes larger; each 3D-element with z < 0 becomes smaller.
// The default value is 0 (no 3D effects).
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetPerspective(view View, subviewID ...string) SizeUnit {
return sizeStyledProperty(view, subviewID, Perspective, false)
}
// GetPerspectiveOrigin returns a x- and y-coordinate of the position at which the viewer is looking.
// It is used as the vanishing point by the Perspective property. The default value is (50%, 50%).
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetPerspectiveOrigin(view View, subviewID ...string) (SizeUnit, SizeUnit) {
view = getSubview(view, subviewID)
if view == nil {
return AutoSize(), AutoSize()
}
return getPerspectiveOrigin(view, view.Session())
}
// GetBackfaceVisible returns a bool property that sets whether the back face of an element is
// visible when turned towards the user. Values:
// true - the back face is visible when turned towards the user (default value).
// false - the back face is hidden, effectively making the element invisible when turned away from the user.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetBackfaceVisible(view View, subviewID ...string) bool {
return boolStyledProperty(view, subviewID, BackfaceVisible, false)
}
// GetTransformOrigin returns a x-, y-, and z-coordinate of the point around which a view transformation is applied.
// The default value is (50%, 50%, 50%).
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetTransformOrigin(view View, subviewID ...string) (SizeUnit, SizeUnit, SizeUnit) {
view = getSubview(view, subviewID)
if view == nil {
return AutoSize(), AutoSize(), AutoSize()
}
return getTransformOrigin(view, view.Session())
}
// GetTranslate returns a x-, y-, and z-axis translation value of a 2D/3D translation
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetTranslate(view View, subviewID ...string) (SizeUnit, SizeUnit, SizeUnit) {
if transform := GetTransform(view, subviewID...); transform != nil {
return transform.getTranslate(view.Session())
}
return AutoSize(), AutoSize(), AutoSize()
}
// GetSkew returns a angles to use to distort the element along the abscissa (x-axis)
// and the ordinate (y-axis). The default value is 0.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetSkew(view View, subviewID ...string) (AngleUnit, AngleUnit) {
if transform := GetTransform(view, subviewID...); transform != nil {
x, y, _ := transform.getSkew(view.Session())
return x, y
}
return AngleUnit{Value: 0, Type: Radian}, AngleUnit{Value: 0, Type: Radian}
}
// GetScale returns a x-, y-, and z-axis scaling value of a 2D/3D scale. The default value is 1.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetScale(view View, subviewID ...string) (float64, float64, float64) {
if transform := GetTransform(view, subviewID...); transform != nil {
session := view.Session()
x, _ := floatProperty(transform, ScaleX, session, 1)
y, _ := floatProperty(transform, ScaleY, session, 1)
z, _ := floatProperty(transform, ScaleZ, session, 1)
return x, y, z
}
return 1, 1, 1
}
// GetRotate returns a x-, y, z-coordinate of the vector denoting the axis of rotation, and the angle of the view rotation
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetRotate(view View, subviewID ...string) (float64, float64, float64, AngleUnit) {
if transform := GetTransform(view, subviewID...); transform != nil {
session := view.Session()
angle, _ := angleProperty(transform, Rotate, view.Session())
rotateX, _ := floatProperty(transform, RotateX, session, 1)
rotateY, _ := floatProperty(transform, RotateY, session, 1)
rotateZ, _ := floatProperty(transform, RotateZ, session, 1)
return rotateX, rotateY, rotateZ, angle
}
return 0, 0, 0, AngleUnit{Value: 0, Type: Radian}
}

View File

@ -6,38 +6,37 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
)
var stringBuilders []*strings.Builder = make([]*strings.Builder, 4096)
var stringBuilderCount = 0
const stringBuilderCap = 4096
var stringBuilderPool = sync.Pool{
New: func() any {
result := new(strings.Builder)
result.Grow(stringBuilderCap)
return result
},
}
func allocStringBuilder() *strings.Builder {
for stringBuilderCount > 0 {
stringBuilderCount--
result := stringBuilders[stringBuilderCount]
if result != nil {
stringBuilders[stringBuilderCount] = nil
result.Reset()
return result
}
if builder := stringBuilderPool.Get(); builder != nil {
return builder.(*strings.Builder)
}
result := new(strings.Builder)
result.Grow(4096)
result.Grow(stringBuilderCap)
return result
}
func freeStringBuilder(builder *strings.Builder) {
if builder != nil {
if stringBuilderCount == len(stringBuilders) {
stringBuilders = append(stringBuilders, builder)
} else {
stringBuilders[stringBuilderCount] = builder
}
stringBuilderCount++
if builder != nil && builder.Cap() == stringBuilderCap {
builder.Reset()
stringBuilderPool.Put(builder)
}
}
// GetLocalIP return IP address of the machine interface
func GetLocalIP() string {
addrs, err := net.InterfaceAddrs()
if err != nil {

View File

@ -4,22 +4,40 @@ import (
"strings"
)
// Constants for [VideoPlayer] specific properties and events
const (
// VideoWidth is the constant for the "video-width" property tag of VideoPlayer.
// The "video-width" float property defines the width of the video's display area in pixels.
VideoWidth = "video-width"
// VideoWidth is the constant for "video-width" property tag.
//
// Used by VideoPlayer.
// Defines the width of the video's display area in pixels.
//
// Supported types: float, int, string.
//
// Values:
// Internal type is float, other types converted to it during assignment.
VideoWidth PropertyName = "video-width"
// VideoHeight is the constant for the "video-height" property tag of VideoPlayer.
// The "video-height" float property defines the height of the video's display area in pixels.
VideoHeight = "video-height"
// VideoHeight is the constant for "video-height" property tag.
//
// Used by VideoPlayer.
// Defines the height of the video's display area in pixels.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
VideoHeight PropertyName = "video-height"
// Poster is the constant for the "poster" property tag of VideoPlayer.
// The "poster" property defines an URL for an image to be shown while the video is downloading.
// If this attribute isn't specified, nothing is displayed until the first frame is available,
// then the first frame is shown as the poster frame.
Poster = "poster"
// Poster is the constant for "poster" property tag.
//
// Used by VideoPlayer.
// Defines an URL for an image to be shown while the video is downloading. If this attribute isn't specified, nothing is
// displayed until the first frame is available, then the first frame is shown as the poster frame.
//
// Supported types: string.
Poster PropertyName = "poster"
)
// VideoPlayer is a type of a [View] which can play video files
type VideoPlayer interface {
MediaPlayer
}
@ -32,92 +50,56 @@ type videoPlayerData struct {
func NewVideoPlayer(session Session, params Params) VideoPlayer {
view := new(videoPlayerData)
view.init(session)
view.tag = "VideoPlayer"
setInitParams(view, params)
return view
}
func newVideoPlayer(session Session) View {
return NewVideoPlayer(session, nil)
return new(videoPlayerData) // NewVideoPlayer(session, nil)
}
func (player *videoPlayerData) init(session Session) {
player.mediaPlayerData.init(session)
player.tag = "VideoPlayer"
}
func (player *videoPlayerData) String() string {
return getViewString(player, nil)
player.changed = player.propertyChanged
}
func (player *videoPlayerData) htmlTag() string {
return "video"
}
func (player *videoPlayerData) Remove(tag string) {
player.remove(strings.ToLower(tag))
}
func (player *videoPlayerData) propertyChanged(tag PropertyName) {
session := player.Session()
updateSize := func(cssTag string) {
if size, ok := floatTextProperty(player, tag, session, 0); ok {
if size != "0" {
session.updateProperty(player.htmlID(), cssTag, size)
} else {
session.removeProperty(player.htmlID(), cssTag)
}
}
}
func (player *videoPlayerData) remove(tag string) {
switch tag {
case VideoWidth:
delete(player.properties, tag)
player.session.removeProperty(player.htmlID(), "width")
updateSize("width")
case VideoHeight:
delete(player.properties, tag)
player.session.removeProperty(player.htmlID(), "height")
updateSize("height")
case Poster:
delete(player.properties, tag)
player.session.removeProperty(player.htmlID(), Poster)
if url, ok := stringProperty(player, Poster, session); ok {
session.updateProperty(player.htmlID(), string(Poster), url)
} else {
session.removeProperty(player.htmlID(), string(Poster))
}
default:
player.mediaPlayerData.remove(tag)
player.mediaPlayerData.propertyChanged(tag)
}
}
func (player *videoPlayerData) Set(tag string, value any) bool {
return player.set(strings.ToLower(tag), value)
}
func (player *videoPlayerData) set(tag string, value any) bool {
if value == nil {
player.remove(tag)
return true
}
if player.mediaPlayerData.set(tag, value) {
session := player.Session()
updateSize := func(cssTag string) {
if size, ok := floatTextProperty(player, tag, session, 0); ok {
if size != "0" {
session.updateProperty(player.htmlID(), cssTag, size)
} else {
session.removeProperty(player.htmlID(), cssTag)
}
}
}
switch tag {
case VideoWidth:
updateSize("width")
case VideoHeight:
updateSize("height")
case Poster:
if url, ok := stringProperty(player, Poster, session); ok {
session.updateProperty(player.htmlID(), Poster, url)
}
}
return true
}
return false
}
func (player *videoPlayerData) htmlProperties(self View, buffer *strings.Builder) {
player.mediaPlayerData.htmlProperties(self, buffer)

744
view.go

File diff suppressed because it is too large Load Diff

View File

@ -2,34 +2,45 @@ package rui
import "strings"
// ViewByID return a View with id equal to the argument of the function or nil if there is no such View
func ViewByID(rootView View, id string) View {
// ViewByID returns the child View path to which is specified using the arguments id, ids. Example
//
// view := ViewByID(rootView, "id1", "id2", "id3")
// view := ViewByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found, the function will return nil
func ViewByID(rootView View, id string, ids ...string) View {
if rootView == nil {
ErrorLog(`ViewByID(nil, "` + id + `"): rootView is nil`)
return nil
}
if rootView.ID() == id {
return rootView
path := []string{id}
if len(ids) > 0 {
path = append(path, ids...)
}
if container, ok := rootView.(ParentView); ok {
if view := viewByID(container, id); view != nil {
return view
}
}
if index := strings.IndexRune(id, '/'); index > 0 {
if view2 := ViewByID(rootView, id[:index]); view2 != nil {
if view := ViewByID(view2, id[index+1:]); view != nil {
return view
result := rootView
for _, id := range path {
if result.ID() != id {
if container, ok := result.(ParentView); ok {
if view := viewByID(container, id); view != nil {
result = view
} else if index := strings.IndexRune(id, '/'); index > 0 {
if view := ViewByID(result, id[:index], id[index+1:]); view != nil {
result = view
} else {
ErrorLog(`ViewByID(_, "` + id + `"): View not found`)
return nil
}
} else {
ErrorLog(`ViewByID(_, "` + id + `"): View not found`)
return nil
}
}
return nil
}
return nil
}
ErrorLog(`ViewByID(_, "` + id + `"): View not found`)
return nil
return result
}
func viewByID(rootView ParentView, id string) View {
@ -49,10 +60,15 @@ func viewByID(rootView ParentView, id string) View {
return nil
}
// ViewsContainerByID return a ViewsContainer with id equal to the argument of the function or
// nil if there is no such View or View is not ViewsContainer
func ViewsContainerByID(rootView View, id string) ViewsContainer {
if view := ViewByID(rootView, id); view != nil {
// ViewsContainerByID return the ViewsContainer path to which is specified using the arguments id, ids. Example
//
// view := ViewsContainerByID(rootView, "id1", "id2", "id3")
// view := ViewsContainerByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not ViewsContainer, the function will return nil
func ViewsContainerByID(rootView View, id string, ids ...string) ViewsContainer {
if view := ViewByID(rootView, id, ids...); view != nil {
if list, ok := view.(ViewsContainer); ok {
return list
}
@ -61,10 +77,15 @@ func ViewsContainerByID(rootView View, id string) ViewsContainer {
return nil
}
// ListLayoutByID return a ListLayout with id equal to the argument of the function or
// nil if there is no such View or View is not ListLayout
func ListLayoutByID(rootView View, id string) ListLayout {
if view := ViewByID(rootView, id); view != nil {
// ListLayoutByID return the ListLayout path to which is specified using the arguments id, ids. Example
//
// view := ListLayoutByID(rootView, "id1", "id2", "id3")
// view := ListLayoutByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not ListLayout, the function will return nil
func ListLayoutByID(rootView View, id string, ids ...string) ListLayout {
if view := ViewByID(rootView, id, ids...); view != nil {
if list, ok := view.(ListLayout); ok {
return list
}
@ -73,10 +94,15 @@ func ListLayoutByID(rootView View, id string) ListLayout {
return nil
}
// StackLayoutByID return a StackLayout with id equal to the argument of the function or
// nil if there is no such View or View is not StackLayout
func StackLayoutByID(rootView View, id string) StackLayout {
if view := ViewByID(rootView, id); view != nil {
// StackLayoutByID return the StackLayout path to which is specified using the arguments id, ids. Example
//
// view := StackLayoutByID(rootView, "id1", "id2", "id3")
// view := StackLayoutByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not StackLayout, the function will return nil
func StackLayoutByID(rootView View, id string, ids ...string) StackLayout {
if view := ViewByID(rootView, id, ids...); view != nil {
if list, ok := view.(StackLayout); ok {
return list
}
@ -85,10 +111,15 @@ func StackLayoutByID(rootView View, id string) StackLayout {
return nil
}
// GridLayoutByID return a GridLayout with id equal to the argument of the function or
// nil if there is no such View or View is not GridLayout
func GridLayoutByID(rootView View, id string) GridLayout {
if view := ViewByID(rootView, id); view != nil {
// GridLayoutByID return the GridLayout path to which is specified using the arguments id, ids. Example
//
// view := GridLayoutByID(rootView, "id1", "id2", "id3")
// view := GridLayoutByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not GridLayout, the function will return nil
func GridLayoutByID(rootView View, id string, ids ...string) GridLayout {
if view := ViewByID(rootView, id, ids...); view != nil {
if list, ok := view.(GridLayout); ok {
return list
}
@ -97,10 +128,15 @@ func GridLayoutByID(rootView View, id string) GridLayout {
return nil
}
// ColumnLayoutByID return a ColumnLayout with id equal to the argument of the function or
// nil if there is no such View or View is not ColumnLayout
func ColumnLayoutByID(rootView View, id string) ColumnLayout {
if view := ViewByID(rootView, id); view != nil {
// ColumnLayoutByID return the ColumnLayout path to which is specified using the arguments id, ids. Example
//
// view := ColumnLayoutByID(rootView, "id1", "id2", "id3")
// view := ColumnLayoutByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not ColumnLayout, the function will return nil
func ColumnLayoutByID(rootView View, id string, ids ...string) ColumnLayout {
if view := ViewByID(rootView, id, ids...); view != nil {
if list, ok := view.(ColumnLayout); ok {
return list
}
@ -109,10 +145,15 @@ func ColumnLayoutByID(rootView View, id string) ColumnLayout {
return nil
}
// DetailsViewByID return a ColumnLayout with id equal to the argument of the function or
// nil if there is no such View or View is not DetailsView
func DetailsViewByID(rootView View, id string) DetailsView {
if view := ViewByID(rootView, id); view != nil {
// DetailsViewByID return the ColumnLayout path to which is specified using the arguments id, ids. Example
//
// view := DetailsViewByID(rootView, "id1", "id2", "id3")
// view := DetailsViewByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not DetailsView, the function will return nil
func DetailsViewByID(rootView View, id string, ids ...string) DetailsView {
if view := ViewByID(rootView, id, ids...); view != nil {
if details, ok := view.(DetailsView); ok {
return details
}
@ -121,10 +162,15 @@ func DetailsViewByID(rootView View, id string) DetailsView {
return nil
}
// DropDownListByID return a DropDownListView with id equal to the argument of the function or
// nil if there is no such View or View is not DropDownListView
func DropDownListByID(rootView View, id string) DropDownList {
if view := ViewByID(rootView, id); view != nil {
// DropDownListByID return the DropDownListView path to which is specified using the arguments id, ids. Example
//
// view := DropDownListByID(rootView, "id1", "id2", "id3")
// view := DropDownListByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not DropDownList, the function will return nil
func DropDownListByID(rootView View, id string, ids ...string) DropDownList {
if view := ViewByID(rootView, id, ids...); view != nil {
if list, ok := view.(DropDownList); ok {
return list
}
@ -133,10 +179,15 @@ func DropDownListByID(rootView View, id string) DropDownList {
return nil
}
// TabsLayoutByID return a TabsLayout with id equal to the argument of the function or
// nil if there is no such View or View is not TabsLayout
func TabsLayoutByID(rootView View, id string) TabsLayout {
if view := ViewByID(rootView, id); view != nil {
// TabsLayoutByID return the TabsLayout path to which is specified using the arguments id, ids. Example
//
// view := TabsLayoutByID(rootView, "id1", "id2", "id3")
// view := TabsLayoutByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not TabsLayout, the function will return nil
func TabsLayoutByID(rootView View, id string, ids ...string) TabsLayout {
if view := ViewByID(rootView, id, ids...); view != nil {
if list, ok := view.(TabsLayout); ok {
return list
}
@ -145,10 +196,15 @@ func TabsLayoutByID(rootView View, id string) TabsLayout {
return nil
}
// ListViewByID return a ListView with id equal to the argument of the function or
// nil if there is no such View or View is not ListView
func ListViewByID(rootView View, id string) ListView {
if view := ViewByID(rootView, id); view != nil {
// ListViewByID return the ListView path to which is specified using the arguments id, ids. Example
//
// view := ListViewByID(rootView, "id1", "id2", "id3")
// view := ListViewByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not ListView, the function will return nil
func ListViewByID(rootView View, id string, ids ...string) ListView {
if view := ViewByID(rootView, id, ids...); view != nil {
if list, ok := view.(ListView); ok {
return list
}
@ -157,10 +213,15 @@ func ListViewByID(rootView View, id string) ListView {
return nil
}
// TextViewByID return a TextView with id equal to the argument of the function or
// nil if there is no such View or View is not TextView
func TextViewByID(rootView View, id string) TextView {
if view := ViewByID(rootView, id); view != nil {
// TextViewByID return the TextView path to which is specified using the arguments id, ids. Example
//
// view := TextViewByID(rootView, "id1", "id2", "id3")
// view := TextViewByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not TextView, the function will return nil
func TextViewByID(rootView View, id string, ids ...string) TextView {
if view := ViewByID(rootView, id, ids...); view != nil {
if text, ok := view.(TextView); ok {
return text
}
@ -169,10 +230,15 @@ func TextViewByID(rootView View, id string) TextView {
return nil
}
// ButtonByID return a Button with id equal to the argument of the function or
// nil if there is no such View or View is not Button
func ButtonByID(rootView View, id string) Button {
if view := ViewByID(rootView, id); view != nil {
// ButtonByID return the Button path to which is specified using the arguments id, ids. Example
//
// view := ButtonByID(rootView, "id1", "id2", "id3")
// view := ButtonByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not Button, the function will return nil
func ButtonByID(rootView View, id string, ids ...string) Button {
if view := ViewByID(rootView, id, ids...); view != nil {
if button, ok := view.(Button); ok {
return button
}
@ -181,10 +247,15 @@ func ButtonByID(rootView View, id string) Button {
return nil
}
// CheckboxByID return a Checkbox with id equal to the argument of the function or
// nil if there is no such View or View is not Checkbox
func CheckboxByID(rootView View, id string) Checkbox {
if view := ViewByID(rootView, id); view != nil {
// CheckboxByID return the Checkbox path to which is specified using the arguments id, ids. Example
//
// view := CheckboxByID(rootView, "id1", "id2", "id3")
// view := CheckboxByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not Checkbox, the function will return nil
func CheckboxByID(rootView View, id string, ids ...string) Checkbox {
if view := ViewByID(rootView, id, ids...); view != nil {
if checkbox, ok := view.(Checkbox); ok {
return checkbox
}
@ -193,10 +264,15 @@ func CheckboxByID(rootView View, id string) Checkbox {
return nil
}
// EditViewByID return a EditView with id equal to the argument of the function or
// nil if there is no such View or View is not EditView
func EditViewByID(rootView View, id string) EditView {
if view := ViewByID(rootView, id); view != nil {
// EditViewByID return the EditView path to which is specified using the arguments id, ids. Example
//
// view := EditViewByID(rootView, "id1", "id2", "id3")
// view := EditViewByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not EditView, the function will return nil
func EditViewByID(rootView View, id string, ids ...string) EditView {
if view := ViewByID(rootView, id, ids...); view != nil {
if buttons, ok := view.(EditView); ok {
return buttons
}
@ -205,10 +281,15 @@ func EditViewByID(rootView View, id string) EditView {
return nil
}
// ProgressBarByID return a ProgressBar with id equal to the argument of the function or
// nil if there is no such View or View is not ProgressBar
func ProgressBarByID(rootView View, id string) ProgressBar {
if view := ViewByID(rootView, id); view != nil {
// ProgressBarByID return the ProgressBar path to which is specified using the arguments id, ids. Example
//
// view := ProgressBarByID(rootView, "id1", "id2", "id3")
// view := ProgressBarByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not ProgressBar, the function will return nil
func ProgressBarByID(rootView View, id string, ids ...string) ProgressBar {
if view := ViewByID(rootView, id, ids...); view != nil {
if buttons, ok := view.(ProgressBar); ok {
return buttons
}
@ -217,10 +298,15 @@ func ProgressBarByID(rootView View, id string) ProgressBar {
return nil
}
// ColorPickerByID return a ColorPicker with id equal to the argument of the function or
// nil if there is no such View or View is not ColorPicker
func ColorPickerByID(rootView View, id string) ColorPicker {
if view := ViewByID(rootView, id); view != nil {
// ColorPickerByID return the ColorPicker path to which is specified using the arguments id, ids. Example
//
// view := ColorPickerByID(rootView, "id1", "id2", "id3")
// view := ColorPickerByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not ColorPicker, the function will return nil
func ColorPickerByID(rootView View, id string, ids ...string) ColorPicker {
if view := ViewByID(rootView, id, ids...); view != nil {
if input, ok := view.(ColorPicker); ok {
return input
}
@ -229,10 +315,15 @@ func ColorPickerByID(rootView View, id string) ColorPicker {
return nil
}
// NumberPickerByID return a NumberPicker with id equal to the argument of the function or
// nil if there is no such View or View is not NumberPicker
func NumberPickerByID(rootView View, id string) NumberPicker {
if view := ViewByID(rootView, id); view != nil {
// NumberPickerByID return the NumberPicker path to which is specified using the arguments id, ids. Example
//
// view := NumberPickerByID(rootView, "id1", "id2", "id3")
// view := NumberPickerByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not NumberPicker, the function will return nil
func NumberPickerByID(rootView View, id string, ids ...string) NumberPicker {
if view := ViewByID(rootView, id, ids...); view != nil {
if input, ok := view.(NumberPicker); ok {
return input
}
@ -241,10 +332,15 @@ func NumberPickerByID(rootView View, id string) NumberPicker {
return nil
}
// TimePickerByID return a TimePicker with id equal to the argument of the function or
// nil if there is no such View or View is not TimePicker
func TimePickerByID(rootView View, id string) TimePicker {
if view := ViewByID(rootView, id); view != nil {
// TimePickerByID return the TimePicker path to which is specified using the arguments id, ids. Example
//
// view := TimePickerByID(rootView, "id1", "id2", "id3")
// view := TimePickerByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not TimePicker, the function will return nil
func TimePickerByID(rootView View, id string, ids ...string) TimePicker {
if view := ViewByID(rootView, id, ids...); view != nil {
if input, ok := view.(TimePicker); ok {
return input
}
@ -253,10 +349,15 @@ func TimePickerByID(rootView View, id string) TimePicker {
return nil
}
// DatePickerByID return a DatePicker with id equal to the argument of the function or
// nil if there is no such View or View is not DatePicker
func DatePickerByID(rootView View, id string) DatePicker {
if view := ViewByID(rootView, id); view != nil {
// DatePickerByID return the DatePicker path to which is specified using the arguments id, ids. Example
//
// view := DatePickerByID(rootView, "id1", "id2", "id3")
// view := DatePickerByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not DatePicker, the function will return nil
func DatePickerByID(rootView View, id string, ids ...string) DatePicker {
if view := ViewByID(rootView, id, ids...); view != nil {
if input, ok := view.(DatePicker); ok {
return input
}
@ -265,10 +366,15 @@ func DatePickerByID(rootView View, id string) DatePicker {
return nil
}
// FilePickerByID return a FilePicker with id equal to the argument of the function or
// nil if there is no such View or View is not FilePicker
func FilePickerByID(rootView View, id string) FilePicker {
if view := ViewByID(rootView, id); view != nil {
// FilePickerByID return the FilePicker path to which is specified using the arguments id, ids. Example
//
// view := FilePickerByID(rootView, "id1", "id2", "id3")
// view := FilePickerByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not FilePicker, the function will return nil
func FilePickerByID(rootView View, id string, ids ...string) FilePicker {
if view := ViewByID(rootView, id, ids...); view != nil {
if input, ok := view.(FilePicker); ok {
return input
}
@ -277,10 +383,15 @@ func FilePickerByID(rootView View, id string) FilePicker {
return nil
}
// CanvasViewByID return a CanvasView with id equal to the argument of the function or
// nil if there is no such View or View is not CanvasView
func CanvasViewByID(rootView View, id string) CanvasView {
if view := ViewByID(rootView, id); view != nil {
// CanvasViewByID return the CanvasView path to which is specified using the arguments id, ids. Example
//
// view := CanvasViewByID(rootView, "id1", "id2", "id3")
// view := CanvasViewByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not CanvasView, the function will return nil
func CanvasViewByID(rootView View, id string, ids ...string) CanvasView {
if view := ViewByID(rootView, id, ids...); view != nil {
if canvas, ok := view.(CanvasView); ok {
return canvas
}
@ -289,24 +400,15 @@ func CanvasViewByID(rootView View, id string) CanvasView {
return nil
}
/*
// TableViewByID return a TableView with id equal to the argument of the function or
// nil if there is no such View or View is not TableView
func TableViewByID(rootView View, id string) TableView {
if view := ViewByID(rootView, id); view != nil {
if canvas, ok := view.(TableView); ok {
return canvas
}
ErrorLog(`TableViewByID(_, "` + id + `"): The found View is not TableView`)
}
return nil
}
*/
// AudioPlayerByID return a AudioPlayer with id equal to the argument of the function or
// nil if there is no such View or View is not AudioPlayer
func AudioPlayerByID(rootView View, id string) AudioPlayer {
if view := ViewByID(rootView, id); view != nil {
// AudioPlayerByID return the AudioPlayer path to which is specified using the arguments id, ids. Example
//
// view := AudioPlayerByID(rootView, "id1", "id2", "id3")
// view := AudioPlayerByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not AudioPlayer, the function will return nil
func AudioPlayerByID(rootView View, id string, ids ...string) AudioPlayer {
if view := ViewByID(rootView, id, ids...); view != nil {
if canvas, ok := view.(AudioPlayer); ok {
return canvas
}
@ -315,10 +417,15 @@ func AudioPlayerByID(rootView View, id string) AudioPlayer {
return nil
}
// VideoPlayerByID return a VideoPlayer with id equal to the argument of the function or
// nil if there is no such View or View is not VideoPlayer
func VideoPlayerByID(rootView View, id string) VideoPlayer {
if view := ViewByID(rootView, id); view != nil {
// VideoPlayerByID return the VideoPlayer path to which is specified using the arguments id, ids. Example
//
// view := VideoPlayerByID(rootView, "id1", "id2", "id3")
// view := VideoPlayerByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not VideoPlayer, the function will return nil
func VideoPlayerByID(rootView View, id string, ids ...string) VideoPlayer {
if view := ViewByID(rootView, id, ids...); view != nil {
if canvas, ok := view.(VideoPlayer); ok {
return canvas
}
@ -327,10 +434,15 @@ func VideoPlayerByID(rootView View, id string) VideoPlayer {
return nil
}
// ImageViewByID return a ImageView with id equal to the argument of the function or
// nil if there is no such View or View is not ImageView
func ImageViewByID(rootView View, id string) ImageView {
if view := ViewByID(rootView, id); view != nil {
// ImageViewByID return the ImageView path to which is specified using the arguments id, ids. Example
//
// view := ImageViewByID(rootView, "id1", "id2", "id3")
// view := ImageViewByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not ImageView, the function will return nil
func ImageViewByID(rootView View, id string, ids ...string) ImageView {
if view := ViewByID(rootView, id, ids...); view != nil {
if canvas, ok := view.(ImageView); ok {
return canvas
}
@ -339,10 +451,15 @@ func ImageViewByID(rootView View, id string) ImageView {
return nil
}
// TableViewByID return a TableView with id equal to the argument of the function or
// nil if there is no such View or View is not TableView
func TableViewByID(rootView View, id string) TableView {
if view := ViewByID(rootView, id); view != nil {
// TableViewByID return the TableView path to which is specified using the arguments id, ids. Example
//
// view := TableViewByID(rootView, "id1", "id2", "id3")
// view := TableViewByID(rootView, "id1/id2/id3")
//
// These two function calls are equivalent.
// If a View with this path is not found or View is not TableView, the function will return nil
func TableViewByID(rootView View, id string, ids ...string) TableView {
if view := ViewByID(rootView, id, ids...); view != nil {
if canvas, ok := view.(TableView); ok {
return canvas
}

View File

@ -1,584 +0,0 @@
package rui
import (
"fmt"
"strings"
)
// ClipShape defines a View clipping area
type ClipShape interface {
Properties
fmt.Stringer
stringWriter
cssStyle(session Session) string
valid(session Session) bool
}
type insetClip struct {
propertyList
}
type ellipseClip struct {
propertyList
}
type circleClip struct {
propertyList
}
type polygonClip struct {
points []any
}
// InsetClip creates a rectangle View clipping area.
// top - offset from the top border of a View;
// right - offset from the right border of a View;
// bottom - offset from the bottom border of a View;
// left - offset from the left border of a View;
// radius - corner radius, pass nil if you don't need to round corners
func InsetClip(top, right, bottom, left SizeUnit, radius RadiusProperty) ClipShape {
clip := new(insetClip)
clip.init()
clip.Set(Top, top)
clip.Set(Right, right)
clip.Set(Bottom, bottom)
clip.Set(Left, left)
if radius != nil {
clip.Set(Radius, radius)
}
return clip
}
// CircleClip creates a circle View clipping area.
func CircleClip(x, y, radius SizeUnit) ClipShape {
clip := new(circleClip)
clip.init()
clip.Set(X, x)
clip.Set(Y, y)
clip.Set(Radius, radius)
return clip
}
// EllipseClip creates a ellipse View clipping area.
func EllipseClip(x, y, rx, ry SizeUnit) ClipShape {
clip := new(ellipseClip)
clip.init()
clip.Set(X, x)
clip.Set(Y, y)
clip.Set(RadiusX, rx)
clip.Set(RadiusY, ry)
return clip
}
// PolygonClip creates a polygon View clipping area.
// The elements of the function argument can be or text constants,
// or the text representation of SizeUnit, or elements of SizeUnit type.
func PolygonClip(points []any) ClipShape {
clip := new(polygonClip)
clip.points = []any{}
if clip.Set(Points, points) {
return clip
}
return nil
}
// PolygonPointsClip creates a polygon View clipping area.
func PolygonPointsClip(points []SizeUnit) ClipShape {
clip := new(polygonClip)
clip.points = []any{}
if clip.Set(Points, points) {
return clip
}
return nil
}
func (clip *insetClip) Set(tag string, value any) bool {
switch strings.ToLower(tag) {
case Top, Right, Bottom, Left:
if value == nil {
clip.Remove(tag)
return true
}
return clip.setSizeProperty(tag, value)
case Radius:
return clip.setRadius(value)
case RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY,
RadiusTopRight, RadiusTopRightX, RadiusTopRightY,
RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY,
RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY:
return clip.setRadiusElement(tag, value)
}
ErrorLogF(`"%s" property is not supported by the inset clip shape`, tag)
return false
}
func (clip *insetClip) String() string {
return runStringWriter(clip)
}
func (clip *insetClip) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("inset { ")
comma := false
for _, tag := range []string{Top, Right, Bottom, Left, Radius} {
if value, ok := clip.properties[tag]; ok {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(" = ")
writePropertyValue(buffer, tag, value, indent)
comma = true
}
}
buffer.WriteString(" }")
}
func (clip *insetClip) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
leadText := "inset("
for _, tag := range []string{Top, Right, Bottom, Left} {
value, _ := sizeProperty(clip, tag, session)
buffer.WriteString(leadText)
buffer.WriteString(value.cssString("0px", session))
leadText = " "
}
if radius := getRadiusProperty(clip); radius != nil {
buffer.WriteString(" round ")
buffer.WriteString(radius.BoxRadius(session).cssString(session))
}
buffer.WriteRune(')')
return buffer.String()
}
func (clip *insetClip) valid(session Session) bool {
for _, tag := range []string{Top, Right, Bottom, Left, Radius, RadiusX, RadiusY} {
if value, ok := sizeProperty(clip, tag, session); ok && value.Type != Auto && value.Value != 0 {
return true
}
}
return false
}
func (clip *circleClip) Set(tag string, value any) bool {
if value == nil {
clip.Remove(tag)
}
switch strings.ToLower(tag) {
case X, Y, Radius:
return clip.setSizeProperty(tag, value)
}
ErrorLogF(`"%s" property is not supported by the circle clip shape`, tag)
return false
}
func (clip *circleClip) String() string {
return runStringWriter(clip)
}
func (clip *circleClip) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("circle { ")
comma := false
for _, tag := range []string{Radius, X, Y} {
if value, ok := clip.properties[tag]; ok {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(" = ")
writePropertyValue(buffer, tag, value, indent)
comma = true
}
}
buffer.WriteString(" }")
}
func (clip *circleClip) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString("circle(")
r, _ := sizeProperty(clip, Radius, session)
buffer.WriteString(r.cssString("50%", session))
buffer.WriteString(" at ")
x, _ := sizeProperty(clip, X, session)
buffer.WriteString(x.cssString("50%", session))
buffer.WriteRune(' ')
y, _ := sizeProperty(clip, Y, session)
buffer.WriteString(y.cssString("50%", session))
buffer.WriteRune(')')
return buffer.String()
}
func (clip *circleClip) valid(session Session) bool {
if value, ok := sizeProperty(clip, Radius, session); ok && value.Value == 0 {
return false
}
return true
}
func (clip *ellipseClip) Set(tag string, value any) bool {
if value == nil {
clip.Remove(tag)
}
switch strings.ToLower(tag) {
case X, Y, RadiusX, RadiusY:
return clip.setSizeProperty(tag, value)
case Radius:
return clip.setSizeProperty(RadiusX, value) &&
clip.setSizeProperty(RadiusY, value)
}
ErrorLogF(`"%s" property is not supported by the ellipse clip shape`, tag)
return false
}
func (clip *ellipseClip) String() string {
return runStringWriter(clip)
}
func (clip *ellipseClip) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("ellipse { ")
comma := false
for _, tag := range []string{RadiusX, RadiusY, X, Y} {
if value, ok := clip.properties[tag]; ok {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(" = ")
writePropertyValue(buffer, tag, value, indent)
comma = true
}
}
buffer.WriteString(" }")
}
func (clip *ellipseClip) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
rx, _ := sizeProperty(clip, RadiusX, session)
ry, _ := sizeProperty(clip, RadiusX, session)
buffer.WriteString("ellipse(")
buffer.WriteString(rx.cssString("50%", session))
buffer.WriteRune(' ')
buffer.WriteString(ry.cssString("50%", session))
buffer.WriteString(" at ")
x, _ := sizeProperty(clip, X, session)
buffer.WriteString(x.cssString("50%", session))
buffer.WriteRune(' ')
y, _ := sizeProperty(clip, Y, session)
buffer.WriteString(y.cssString("50%", session))
buffer.WriteRune(')')
return buffer.String()
}
func (clip *ellipseClip) valid(session Session) bool {
rx, _ := sizeProperty(clip, RadiusX, session)
ry, _ := sizeProperty(clip, RadiusY, session)
return rx.Value != 0 && ry.Value != 0
}
func (clip *polygonClip) Get(tag string) any {
if Points == strings.ToLower(tag) {
return clip.points
}
return nil
}
func (clip *polygonClip) getRaw(tag string) any {
return clip.Get(tag)
}
func (clip *polygonClip) Set(tag string, value any) bool {
if Points == strings.ToLower(tag) {
switch value := value.(type) {
case []any:
result := true
clip.points = make([]any, len(value))
for i, val := range value {
switch val := val.(type) {
case string:
if isConstantName(val) {
clip.points[i] = val
} else if size, ok := StringToSizeUnit(val); ok {
clip.points[i] = size
} else {
notCompatibleType(tag, val)
result = false
}
case SizeUnit:
clip.points[i] = val
default:
notCompatibleType(tag, val)
clip.points[i] = AutoSize()
result = false
}
}
return result
case []SizeUnit:
clip.points = make([]any, len(value))
for i, point := range value {
clip.points[i] = point
}
return true
case string:
result := true
values := strings.Split(value, ",")
clip.points = make([]any, len(values))
for i, val := range values {
val = strings.Trim(val, " \t\n\r")
if isConstantName(val) {
clip.points[i] = val
} else if size, ok := StringToSizeUnit(val); ok {
clip.points[i] = size
} else {
notCompatibleType(tag, val)
result = false
}
}
return result
}
}
return false
}
func (clip *polygonClip) setRaw(tag string, value any) {
clip.Set(tag, value)
}
func (clip *polygonClip) Remove(tag string) {
if Points == strings.ToLower(tag) {
clip.points = []any{}
}
}
func (clip *polygonClip) Clear() {
clip.points = []any{}
}
func (clip *polygonClip) AllTags() []string {
return []string{Points}
}
func (clip *polygonClip) String() string {
return runStringWriter(clip)
}
func (clip *polygonClip) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("inset { ")
if clip.points != nil {
buffer.WriteString(Points)
buffer.WriteString(` = "`)
for i, value := range clip.points {
if i > 0 {
buffer.WriteString(", ")
}
writePropertyValue(buffer, "", value, indent)
}
buffer.WriteString(`" `)
}
buffer.WriteRune('}')
}
func (clip *polygonClip) cssStyle(session Session) string {
count := len(clip.points)
if count < 2 {
return ""
}
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
writePoint := func(value any) {
switch value := value.(type) {
case string:
if val, ok := session.resolveConstants(value); ok {
if size, ok := StringToSizeUnit(val); ok {
buffer.WriteString(size.cssString("0px", session))
return
}
}
case SizeUnit:
buffer.WriteString(value.cssString("0px", session))
return
}
buffer.WriteString("0px")
}
leadText := "polygon("
for i := 1; i < count; i += 2 {
buffer.WriteString(leadText)
writePoint(clip.points[i-1])
buffer.WriteRune(' ')
writePoint(clip.points[i])
leadText = ", "
}
buffer.WriteRune(')')
return buffer.String()
}
func (clip *polygonClip) valid(session Session) bool {
if clip.points == nil || len(clip.points) == 0 {
return false
}
return true
}
func parseClipShape(obj DataObject) ClipShape {
switch obj.Tag() {
case "inset":
clip := new(insetClip)
for _, tag := range []string{Top, Right, Bottom, Left, Radius, RadiusX, RadiusY} {
if value, ok := obj.PropertyValue(tag); ok {
clip.Set(tag, value)
}
}
return clip
case "circle":
clip := new(ellipseClip)
for _, tag := range []string{X, Y, Radius} {
if value, ok := obj.PropertyValue(tag); ok {
clip.Set(tag, value)
}
}
return clip
case "ellipse":
clip := new(ellipseClip)
for _, tag := range []string{X, Y, RadiusX, RadiusY} {
if value, ok := obj.PropertyValue(tag); ok {
clip.Set(tag, value)
}
}
return clip
case "polygon":
clip := new(ellipseClip)
if value, ok := obj.PropertyValue(Points); ok {
clip.Set(Points, value)
}
return clip
}
return nil
}
func (style *viewStyle) setClipShape(tag string, value any) bool {
switch value := value.(type) {
case ClipShape:
style.properties[tag] = value
return true
case string:
if isConstantName(value) {
style.properties[tag] = value
return true
}
if obj := NewDataObject(value); obj == nil {
if clip := parseClipShape(obj); clip != nil {
style.properties[tag] = clip
return true
}
}
case DataObject:
if clip := parseClipShape(value); clip != nil {
style.properties[tag] = clip
return true
}
case DataValue:
if value.IsObject() {
if clip := parseClipShape(value.Object()); clip != nil {
style.properties[tag] = clip
return true
}
}
}
notCompatibleType(tag, value)
return false
}
func getClipShape(prop Properties, tag string, session Session) ClipShape {
if value := prop.getRaw(tag); value != nil {
switch value := value.(type) {
case ClipShape:
return value
case string:
if text, ok := session.resolveConstants(value); ok {
if obj := NewDataObject(text); obj == nil {
return parseClipShape(obj)
}
}
}
}
return nil
}
// GetClip returns a View clipping area.
// If the second argument (subviewID) is not specified or it is "" then a top position of the first argument (view) is returned
func GetClip(view View, subviewID ...string) ClipShape {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
return getClipShape(view, Clip, view.Session())
}
return nil
}
// GetShapeOutside returns a shape around which adjacent inline content.
// If the second argument (subviewID) is not specified or it is "" then a top position of the first argument (view) is returned
func GetShapeOutside(view View, subviewID ...string) ClipShape {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
return getClipShape(view, ShapeOutside, view.Session())
}
return nil
}

View File

@ -85,6 +85,7 @@ func CreateViewFromObject(session Session, object DataObject) View {
defer session.setIgnoreViewUpdates(false)
}
view := creator(session)
view.init(session)
if customView, ok := view.(CustomView); ok {
if !InitCustomView(customView, tag, session, nil) {
return nil

View File

@ -1,305 +0,0 @@
package rui
import (
"fmt"
"strings"
)
const (
// Blur is the constant for the "blur" property tag of the ViewFilter interface.
// The "blur" float64 property applies a Gaussian blur. The value of radius defines the value
// of the standard deviation to the Gaussian function, or how many pixels on the screen blend
// into each other, so a larger value will create more blur. The lacuna value for interpolation is 0.
// The parameter is specified as a length in pixels.
Blur = "blur"
// Brightness is the constant for the "brightness" property tag of the ViewFilter interface.
// The "brightness" float64 property applies a linear multiplier to input image, making it appear more
// or less bright. A value of 0% will create an image that is completely black.
// A value of 100% leaves the input unchanged. Other values are linear multipliers on the effect.
// Values of an amount over 100% are allowed, providing brighter results.
Brightness = "brightness"
// Contrast is the constant for the "contrast" property tag of the ViewFilter interface.
// The "contrast" float64 property adjusts the contrast of the input.
// A value of 0% will create an image that is completely black. A value of 100% leaves the input unchanged.
// Values of amount over 100% are allowed, providing results with less contrast.
Contrast = "contrast"
// DropShadow is the constant for the "drop-shadow" property tag of the ViewFilter interface.
// The "drop-shadow" property applies a drop shadow effect to the input image.
// A drop shadow is effectively a blurred, offset version of the input image's alpha mask
// drawn in a particular color, composited below the image.
// Shadow parameters are set using the ViewShadow interface
DropShadow = "drop-shadow"
// Grayscale is the constant for the "grayscale" property tag of the ViewFilter interface.
// The "grayscale" float64 property converts the input image to grayscale.
// The value of amount defines the proportion of the conversion.
// A value of 100% is completely grayscale. A value of 0% leaves the input unchanged.
// Values between 0% and 100% are linear multipliers on the effect.
Grayscale = "grayscale"
// HueRotate is the constant for the "hue-rotate" property tag of the ViewFilter interface.
// The "hue-rotate" AngleUnit property applies a hue rotation on the input image.
// The value of angle defines the number of degrees around the color circle the input samples will be adjusted.
// A value of 0deg leaves the input unchanged. If the angle parameter is missing, a value of 0deg is used.
// Though there is no maximum value, the effect of values above 360deg wraps around.
HueRotate = "hue-rotate"
// Invert is the constant for the "invert" property tag of the ViewFilter interface.
// The "invert" float64 property inverts the samples in the input image.
// The value of amount defines the proportion of the conversion.
// A value of 100% is completely inverted. A value of 0% leaves the input unchanged.
// Values between 0% and 100% are linear multipliers on the effect.
Invert = "invert"
// Saturate is the constant for the "saturate" property tag of the ViewFilter interface.
// The "saturate" float64 property saturates the input image.
// The value of amount defines the proportion of the conversion.
// A value of 0% is completely un-saturated. A value of 100% leaves the input unchanged.
// Other values are linear multipliers on the effect.
// Values of amount over 100% are allowed, providing super-saturated results.
Saturate = "saturate"
// Sepia is the constant for the "sepia" property tag of the ViewFilter interface.
// The "sepia" float64 property converts the input image to sepia.
// The value of amount defines the proportion of the conversion.
// A value of 100% is completely sepia. A value of 0% leaves the input unchanged.
// Values between 0% and 100% are linear multipliers on the effect.
Sepia = "sepia"
//Opacity = "opacity"
)
// ViewFilter defines an applied to a View a graphical effects like blur or color shift.
// Allowable properties are Blur, Brightness, Contrast, DropShadow, Grayscale, HueRotate, Invert, Opacity, Saturate, and Sepia
type ViewFilter interface {
Properties
fmt.Stringer
stringWriter
cssStyle(session Session) string
}
type viewFilter struct {
propertyList
}
// NewViewFilter creates the new ViewFilter
func NewViewFilter(params Params) ViewFilter {
if params != nil {
filter := new(viewFilter)
filter.init()
for tag, value := range params {
filter.Set(tag, value)
}
if len(filter.properties) > 0 {
return filter
}
}
return nil
}
func newViewFilter(obj DataObject) ViewFilter {
filter := new(viewFilter)
filter.init()
for i := 0; i < obj.PropertyCount(); i++ {
if node := obj.Property(i); node != nil {
tag := node.Tag()
switch node.Type() {
case TextNode:
filter.Set(tag, node.Text())
case ObjectNode:
if tag == HueRotate {
// TODO
} else {
ErrorLog(`Invalid value of "` + tag + `"`)
}
default:
ErrorLog(`Invalid value of "` + tag + `"`)
}
}
}
if len(filter.properties) > 0 {
return filter
}
ErrorLog("Empty view filter")
return nil
}
func (filter *viewFilter) Set(tag string, value any) bool {
if value == nil {
filter.Remove(tag)
return true
}
switch strings.ToLower(tag) {
case Blur, Brightness, Contrast, Saturate:
return filter.setFloatProperty(tag, value, 0, 10000)
case Grayscale, Invert, Opacity, Sepia:
return filter.setFloatProperty(tag, value, 0, 100)
case HueRotate:
return filter.setAngleProperty(tag, value)
case DropShadow:
return filter.setShadow(tag, value)
}
ErrorLogF(`"%s" property is not supported by the view filter`, tag)
return false
}
func (filter *viewFilter) String() string {
return runStringWriter(filter)
}
func (filter *viewFilter) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("filter { ")
comma := false
tags := filter.AllTags()
for _, tag := range tags {
if value, ok := filter.properties[tag]; ok {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(" = ")
writePropertyValue(buffer, tag, value, indent)
comma = true
}
}
buffer.WriteString(" }")
}
func (filter *viewFilter) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if value, ok := floatTextProperty(filter, Blur, session, 0); ok {
buffer.WriteString(Blur)
buffer.WriteRune('(')
buffer.WriteString(value)
buffer.WriteString("px)")
}
for _, tag := range []string{Brightness, Contrast, Saturate, Grayscale, Invert, Opacity, Sepia} {
if value, ok := floatTextProperty(filter, tag, session, 0); ok {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString(tag)
buffer.WriteRune('(')
buffer.WriteString(value)
buffer.WriteString("%)")
}
}
if value, ok := angleProperty(filter, HueRotate, session); ok {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString(HueRotate)
buffer.WriteRune('(')
buffer.WriteString(value.cssString())
buffer.WriteRune(')')
}
var lead string
if buffer.Len() > 0 {
lead = " drop-shadow("
} else {
lead = "drop-shadow("
}
for _, shadow := range getShadows(filter, DropShadow) {
if shadow.cssTextStyle(buffer, session, lead) {
buffer.WriteRune(')')
lead = " drop-shadow("
}
}
return buffer.String()
}
func (style *viewStyle) setFilter(tag string, value any) bool {
switch value := value.(type) {
case ViewFilter:
style.properties[tag] = value
return true
case string:
if obj := NewDataObject(value); obj == nil {
if filter := newViewFilter(obj); filter != nil {
style.properties[tag] = filter
return true
}
}
case DataObject:
if filter := newViewFilter(value); filter != nil {
style.properties[tag] = filter
return true
}
case DataValue:
if value.IsObject() {
if filter := newViewFilter(value.Object()); filter != nil {
style.properties[tag] = filter
return true
}
}
}
notCompatibleType(tag, value)
return false
}
// GetFilter returns a View graphical effects like blur or color shift.
// If the second argument (subviewID) is not specified or it is "" then a top position of the first argument (view) is returned
func GetFilter(view View, subviewID ...string) ViewFilter {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.getRaw(Filter); value != nil {
if filter, ok := value.(ViewFilter); ok {
return filter
}
}
if value := valueFromStyle(view, Filter); value != nil {
if filter, ok := value.(ViewFilter); ok {
return filter
}
}
}
return nil
}
// GetBackdropFilter returns the area behind a View graphical effects like blur or color shift.
// If the second argument (subviewID) is not specified or it is "" then a top position of the first argument (view) is returned
func GetBackdropFilter(view View, subviewID ...string) ViewFilter {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.getRaw(BackdropFilter); value != nil {
if filter, ok := value.(ViewFilter); ok {
return filter
}
}
if value := valueFromStyle(view, BackdropFilter); value != nil {
if filter, ok := value.(ViewFilter); ok {
return filter
}
}
}
return nil
}

View File

@ -12,72 +12,31 @@ type ViewStyle interface {
Properties
// Transition returns the transition animation of the property. Returns nil is there is no transition animation.
Transition(tag string) Animation
Transition(tag PropertyName) AnimationProperty
// Transitions returns the map of transition animations. The result is always non-nil.
Transitions() map[string]Animation
Transitions() map[PropertyName]AnimationProperty
// SetTransition sets the transition animation for the property if "animation" argument is not nil, and
// removes the transition animation of the property if "animation" argument is nil.
// The "tag" argument is the property name.
SetTransition(tag string, animation Animation)
SetTransition(tag PropertyName, animation AnimationProperty)
cssViewStyle(buffer cssBuilder, session Session)
}
type viewStyle struct {
propertyList
transitions map[string]Animation
}
// Range defines range limits. The First and Last value are included in the range
type Range struct {
First, Last int
//transitions map[PropertyName]Animation
}
type stringWriter interface {
writeString(buffer *strings.Builder, indent string)
}
// String returns a string representation of the Range struct
func (r Range) String() string {
if r.First == r.Last {
return fmt.Sprintf("%d", r.First)
}
return fmt.Sprintf("%d:%d", r.First, r.Last)
}
func (r *Range) setValue(value string) bool {
var err error
if strings.Contains(value, ":") {
values := strings.Split(value, ":")
if len(values) != 2 {
ErrorLog("Invalid range value: " + value)
return false
}
if r.First, err = strconv.Atoi(strings.Trim(values[0], " \t\n\r")); err != nil {
ErrorLog(`Invalid first range value "` + value + `" (` + err.Error() + ")")
return false
}
if r.Last, err = strconv.Atoi(strings.Trim(values[1], " \t\n\r")); err != nil {
ErrorLog(`Invalid last range value "` + value + `" (` + err.Error() + ")")
return false
}
return true
}
if r.First, err = strconv.Atoi(value); err != nil {
ErrorLog(`Invalid range value "` + value + `" (` + err.Error() + ")")
return false
}
r.Last = r.First
return true
}
func (style *viewStyle) init() {
style.propertyList.init()
//style.shadows = []ViewShadow{}
style.transitions = map[string]Animation{}
style.normalize = normalizeViewStyleTag
}
// NewViewStyle create new ViewStyle object
@ -90,19 +49,19 @@ func NewViewStyle(params Params) ViewStyle {
return style
}
func (style *viewStyle) cssTextDecoration(session Session) string {
func textDecorationCSS(properties Properties, session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
noDecoration := false
if strikethrough, ok := boolProperty(style, Strikethrough, session); ok {
if strikethrough, ok := boolProperty(properties, Strikethrough, session); ok {
if strikethrough {
buffer.WriteString("line-through")
}
noDecoration = true
}
if overline, ok := boolProperty(style, Overline, session); ok {
if overline, ok := boolProperty(properties, Overline, session); ok {
if overline {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
@ -112,7 +71,7 @@ func (style *viewStyle) cssTextDecoration(session Session) string {
noDecoration = true
}
if underline, ok := boolProperty(style, Underline, session); ok {
if underline, ok := boolProperty(properties, Underline, session); ok {
if underline {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
@ -149,40 +108,27 @@ func split4Values(text string) []string {
return []string{}
}
func (style *viewStyle) backgroundCSS(session Session) string {
if value, ok := style.properties[Background]; ok {
if backgrounds, ok := value.([]BackgroundElement); ok {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for _, background := range backgrounds {
if value := background.cssStyle(session); value != "" {
if buffer.Len() > 0 {
buffer.WriteString(", ")
}
buffer.WriteString(value)
}
}
if buffer.Len() > 0 {
return buffer.String()
}
}
}
return ""
}
func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session) {
if margin, ok := boundsProperty(style, Margin, session); ok {
if visibility, ok := enumProperty(style, Visibility, session, Visible); ok {
switch visibility {
case Invisible:
builder.add(`visibility`, `hidden`)
case Gone:
builder.add(`display`, `none`)
}
}
if margin, ok := getBounds(style, Margin, session); ok {
margin.cssValue(Margin, builder, session)
}
if padding, ok := boundsProperty(style, Padding, session); ok {
if padding, ok := getBounds(style, Padding, session); ok {
padding.cssValue(Padding, builder, session)
}
if border := getBorder(style, Border); border != nil {
if border := getBorderProperty(style, Border); border != nil {
border.cssStyle(builder, session)
border.cssWidth(builder, session)
border.cssColor(builder, session)
@ -191,27 +137,27 @@ func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session) {
radius := getRadius(style, session)
radius.cssValue(builder, session)
if outline := getOutline(style); outline != nil {
if outline := getOutlineProperty(style); outline != nil {
outline.ViewOutline(session).cssValue(builder, session)
}
for _, tag := range []string{ZIndex, Order} {
for _, tag := range []PropertyName{ZIndex, Order} {
if value, ok := intProperty(style, tag, session, 0); ok {
builder.add(tag, strconv.Itoa(value))
builder.add(string(tag), strconv.Itoa(value))
}
}
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(string(Opacity), strconv.FormatFloat(opacity, 'f', 3, 32))
}
for _, tag := range []string{ColumnCount, TabSize} {
for _, tag := range []PropertyName{ColumnCount, TabSize} {
if value, ok := intProperty(style, tag, session, 0); ok && value > 0 {
builder.add(tag, strconv.Itoa(value))
builder.add(string(tag), strconv.Itoa(value))
}
}
for _, tag := range []string{
for _, tag := range []PropertyName{
Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight, Left, Right, Top, Bottom,
TextSize, TextIndent, LetterSpacing, WordSpacing, LineHeight, TextLineThickness,
ListRowGap, ListColumnGap, GridRowGap, GridColumnGap, ColumnGap, ColumnWidth, OutlineOffset} {
@ -219,18 +165,22 @@ func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session) {
if size, ok := sizeProperty(style, tag, session); ok && size.Type != Auto {
cssTag, ok := sizeProperties[tag]
if !ok {
cssTag = tag
cssTag = string(tag)
}
builder.add(cssTag, size.cssString("", session))
}
}
colorProperties := []struct{ property, cssTag string }{
{BackgroundColor, BackgroundColor},
type propertyCss struct {
property PropertyName
cssTag string
}
colorProperties := []propertyCss{
//{BackgroundColor, string(BackgroundColor)},
{TextColor, "color"},
{TextLineColor, "text-decoration-color"},
{CaretColor, CaretColor},
{AccentColor, AccentColor},
{CaretColor, string(CaretColor)},
{AccentColor, string(AccentColor)},
}
for _, p := range colorProperties {
if color, ok := colorProperty(style, p.property, session); ok && color != 0 {
@ -238,12 +188,25 @@ func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session) {
}
}
if value, ok := enumProperty(style, BackgroundClip, session, 0); ok {
builder.add(BackgroundClip, enumProperties[BackgroundClip].values[value])
for _, tag := range []PropertyName{BackgroundClip, BackgroundOrigin, MaskClip, MaskOrigin} {
if value, ok := enumProperty(style, tag, session, 0); ok {
if data, ok := enumProperties[tag]; ok {
builder.add(data.cssTag, data.cssValues[value])
}
}
}
if background := style.backgroundCSS(session); background != "" {
if background := backgroundCSS(style, session); background != "" {
builder.add("background", background)
} else {
backgroundColor, _ := colorProperty(style, BackgroundColor, session)
if backgroundColor != 0 {
builder.add("background-color", backgroundColor.cssString())
}
}
if mask := maskCSS(style, session); mask != "" {
builder.add("mask", mask)
}
if font, ok := stringProperty(style, FontName, session); ok && font != "" {
@ -251,7 +214,7 @@ func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session) {
}
writingMode := 0
for _, tag := range []string{
for _, tag := range []PropertyName{
Overflow, TextAlign, TextTransform, TextWeight, TextLineStyle, WritingMode, TextDirection,
VerticalTextOrientation, CellVerticalAlign, CellHorizontalAlign, GridAutoFlow, Cursor,
WhiteSpace, WordBreak, TextOverflow, Float, TableVerticalAlign, Resize, MixBlendMode, BackgroundBlendMode} {
@ -272,7 +235,11 @@ func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session) {
}
}
for _, prop := range []struct{ tag, cssTag, off, on string }{
type boolPropertyCss struct {
tag PropertyName
cssTag, off, on string
}
for _, prop := range []boolPropertyCss{
{tag: Italic, cssTag: "font-style", off: "normal", on: "italic"},
{tag: SmallCaps, cssTag: "font-variant", off: "normal", on: "small-caps"},
} {
@ -285,7 +252,7 @@ func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session) {
}
}
if text := style.cssTextDecoration(session); text != "" {
if text := textDecorationCSS(style, session); text != "" {
builder.add("text-decoration", text)
}
@ -406,46 +373,46 @@ func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session) {
if r, ok := rangeProperty(style, Column, session); ok {
builder.add("grid-column", fmt.Sprintf("%d / %d", r.First+1, r.Last+2))
}
if text := style.gridCellSizesCSS(CellWidth, session); text != "" {
if text := gridCellSizesCSS(style, CellWidth, session); text != "" {
builder.add(`grid-template-columns`, text)
}
if text := style.gridCellSizesCSS(CellHeight, session); text != "" {
if text := gridCellSizesCSS(style, CellHeight, session); text != "" {
builder.add(`grid-template-rows`, text)
}
style.writeViewTransformCSS(builder, session)
if clip := getClipShape(style, Clip, session); clip != nil && clip.valid(session) {
if clip := getClipShapeProperty(style, Clip, session); clip != nil && clip.valid(session) {
builder.add(`clip-path`, clip.cssStyle(session))
}
if clip := getClipShape(style, ShapeOutside, session); clip != nil && clip.valid(session) {
if clip := getClipShapeProperty(style, ShapeOutside, session); clip != nil && clip.valid(session) {
builder.add(`shape-outside`, clip.cssStyle(session))
}
if value := style.getRaw(Filter); value != nil {
if filter, ok := value.(ViewFilter); ok {
if filter, ok := value.(FilterProperty); ok {
if text := filter.cssStyle(session); text != "" {
builder.add(Filter, text)
builder.add(string(Filter), text)
}
}
}
if value := style.getRaw(BackdropFilter); value != nil {
if filter, ok := value.(ViewFilter); ok {
if filter, ok := value.(FilterProperty); ok {
if text := filter.cssStyle(session); text != "" {
builder.add(`-webkit-backdrop-filter`, text)
builder.add(BackdropFilter, text)
builder.add(string(BackdropFilter), text)
}
}
}
if transition := style.transitionCSS(session); transition != "" {
if transition := transitionCSS(style, session); transition != "" {
builder.add(`transition`, transition)
}
if animation := style.animationCSS(session); animation != "" {
builder.add(AnimationTag, animation)
if animation := animationCSS(style, session); animation != "" {
builder.add(string(Animation), animation)
}
if pause, ok := boolProperty(style, AnimationPaused, session); ok {
@ -494,20 +461,58 @@ func valueToOrientation(value any, session Session) (int, bool) {
return 0, false
}
func (style *viewStyle) Get(tag string) any {
return style.get(strings.ToLower(tag))
func normalizeViewStyleTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case "top-margin":
return MarginTop
case "right-margin":
return MarginRight
case "bottom-margin":
return MarginBottom
case "left-margin":
return MarginLeft
case "top-padding":
return PaddingTop
case "right-padding":
return PaddingRight
case "bottom-padding":
return PaddingBottom
case "left-padding":
return PaddingLeft
case "origin-x":
return TransformOriginX
case "origin-y":
return TransformOriginY
case "origin-z":
return TransformOriginZ
}
return tag
}
func (style *viewStyle) get(tag string) any {
func (style *viewStyle) Get(tag PropertyName) any {
return viewStyleGet(style, normalizeViewStyleTag(tag))
}
func viewStyleGet(style Properties, tag PropertyName) any {
switch tag {
case Border, CellBorder:
return getBorder(&style.propertyList, tag)
case BorderLeft, BorderRight, BorderTop, BorderBottom,
BorderStyle, BorderLeftStyle, BorderRightStyle, BorderTopStyle, BorderBottomStyle,
BorderColor, BorderLeftColor, BorderRightColor, BorderTopColor, BorderBottomColor,
BorderWidth, BorderLeftWidth, BorderRightWidth, BorderTopWidth, BorderBottomWidth:
if border := getBorder(style, Border); border != nil {
if border := getBorderProperty(style, Border); border != nil {
return border.Get(tag)
}
return nil
@ -516,7 +521,7 @@ func (style *viewStyle) get(tag string) any {
CellBorderStyle, CellBorderLeftStyle, CellBorderRightStyle, CellBorderTopStyle, CellBorderBottomStyle,
CellBorderColor, CellBorderLeftColor, CellBorderRightColor, CellBorderTopColor, CellBorderBottomColor,
CellBorderWidth, CellBorderLeftWidth, CellBorderRightWidth, CellBorderTopWidth, CellBorderBottomWidth:
if border := getBorder(style, CellBorder); border != nil {
if border := getBorderProperty(style, CellBorder); border != nil {
return border.Get(tag)
}
return nil
@ -527,39 +532,22 @@ func (style *viewStyle) get(tag string) any {
RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY:
return getRadiusElement(style, tag)
case ColumnSeparator:
if val, ok := style.properties[ColumnSeparator]; ok {
return val.(ColumnSeparatorProperty)
}
return nil
case ColumnSeparatorStyle, ColumnSeparatorWidth, ColumnSeparatorColor:
if val, ok := style.properties[ColumnSeparator]; ok {
if val := style.getRaw(ColumnSeparator); val != nil {
separator := val.(ColumnSeparatorProperty)
return separator.Get(tag)
}
return nil
case Transition:
if len(style.transitions) == 0 {
return nil
case RotateX, RotateY, RotateZ, Rotate, SkewX, SkewY, ScaleX, ScaleY, ScaleZ,
TranslateX, TranslateY, TranslateZ:
if transform := getTransformProperty(style, Transform); transform != nil {
return transform.Get(tag)
}
result := map[string]Animation{}
for tag, animation := range style.transitions {
result[tag] = animation
}
return result
return nil
}
return style.propertyList.getRaw(tag)
}
func (style *viewStyle) AllTags() []string {
result := style.propertyList.AllTags()
if len(style.transitions) > 0 {
result = append(result, Transition)
}
return result
return style.getRaw(tag)
}
func supportedPropertyValue(value any) bool {
@ -572,20 +560,20 @@ func supportedPropertyValue(value any) bool {
case int:
case stringWriter:
case fmt.Stringer:
case []ViewShadow:
case []ShadowProperty:
case []View:
case []any:
case []BackgroundElement:
case []BackgroundGradientPoint:
case []BackgroundGradientAngle:
case map[string]Animation:
case map[PropertyName]AnimationProperty:
default:
return false
}
return true
}
func writePropertyValue(buffer *strings.Builder, tag string, value any, indent string) {
func writePropertyValue(buffer *strings.Builder, tag PropertyName, value any, indent string) {
writeString := func(text string) {
simple := (tag != Text && tag != Title && tag != Summary)
@ -595,8 +583,8 @@ func writePropertyValue(buffer *strings.Builder, tag string, value any, indent s
} else {
for _, ch := range text {
if (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') ||
ch == '+' || ch == '-' || ch == '@' || ch == '/' || ch == '_' || ch == ':' ||
ch == '#' || ch == '%' || ch == 'π' || ch == '°' {
ch == '+' || ch == '-' || ch == '@' || ch == '/' || ch == '_' || ch == '.' ||
ch == ':' || ch == '#' || ch == '%' || ch == 'π' || ch == '°' {
} else {
simple = false
break
@ -683,7 +671,7 @@ func writePropertyValue(buffer *strings.Builder, tag string, value any, indent s
case fmt.Stringer:
writeString(value.String())
case []ViewShadow:
case []ShadowProperty:
switch len(value) {
case 0:
// do nothing
@ -787,7 +775,7 @@ func writePropertyValue(buffer *strings.Builder, tag string, value any, indent s
}
buffer.WriteRune('"')
case map[string]Animation:
case map[PropertyName]AnimationProperty:
switch count := len(value); count {
case 0:
buffer.WriteString("[]")
@ -799,11 +787,13 @@ func writePropertyValue(buffer *strings.Builder, tag string, value any, indent s
}
default:
tags := make([]string, 0, len(value))
tags := make([]PropertyName, 0, len(value))
for tag := range value {
tags = append(tags, tag)
}
sort.Strings(tags)
sort.Slice(tags, func(i, j int) bool {
return tags[i] < tags[j]
})
buffer.WriteString("[\n")
indent2 := indent + "\t"
for _, tag := range tags {
@ -819,12 +809,12 @@ func writePropertyValue(buffer *strings.Builder, tag string, value any, indent s
}
}
func writeViewStyle(name string, view ViewStyle, buffer *strings.Builder, indent string, excludeTags []string) {
func writeViewStyle(name string, view Properties, buffer *strings.Builder, indent string, excludeTags []PropertyName) {
buffer.WriteString(name)
buffer.WriteString(" {\n")
indent += "\t"
writeProperty := func(tag string, value any) {
writeProperty := func(tag PropertyName, value any) {
for _, exclude := range excludeTags {
if exclude == tag {
return
@ -833,7 +823,7 @@ func writeViewStyle(name string, view ViewStyle, buffer *strings.Builder, indent
if supportedPropertyValue(value) {
buffer.WriteString(indent)
buffer.WriteString(tag)
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
writePropertyValue(buffer, tag, value, indent)
buffer.WriteString(",\n")
@ -841,7 +831,7 @@ func writeViewStyle(name string, view ViewStyle, buffer *strings.Builder, indent
}
tags := view.AllTags()
removeTag := func(tag string) {
removeTag := func(tag PropertyName) {
for i, t := range tags {
if t == tag {
if i == 0 {
@ -856,10 +846,11 @@ func writeViewStyle(name string, view ViewStyle, buffer *strings.Builder, indent
}
}
tagOrder := []string{
tagOrder := []PropertyName{
ID, Row, Column, Top, Right, Bottom, Left, Semantics, Cursor, Visibility,
Opacity, ZIndex, Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight,
Margin, Padding, BackgroundClip, BackgroundColor, Background, Border, Radius, Outline, Shadow,
Margin, Padding, BackgroundColor, Background, BackgroundClip, BackgroundOrigin,
Mask, MaskClip, MaskOrigin, Border, Radius, Outline, Shadow,
Orientation, ListWrap, VerticalAlign, HorizontalAlign, CellWidth, CellHeight,
CellVerticalAlign, CellHorizontalAlign, ListRowGap, ListColumnGap, GridRowGap, GridColumnGap,
ColumnCount, ColumnWidth, ColumnSeparator, ColumnGap, AvoidBreak,
@ -877,10 +868,10 @@ func writeViewStyle(name string, view ViewStyle, buffer *strings.Builder, indent
}
}
finalTags := []string{
Perspective, PerspectiveOriginX, PerspectiveOriginY, BackfaceVisible, OriginX, OriginY, OriginZ,
TranslateX, TranslateY, TranslateZ, ScaleX, ScaleY, ScaleZ, Rotate, RotateX, RotateY, RotateZ,
SkewX, SkewY, Clip, Filter, BackdropFilter, Summary, Content, Transition}
finalTags := []PropertyName{
PerspectiveOriginX, PerspectiveOriginY, BackfaceVisible,
TransformOriginX, TransformOriginY, TransformOriginZ,
Transform, Clip, Filter, BackdropFilter, Summary, Content, Transition}
for _, tag := range finalTags {
removeTag(tag)
}
@ -902,14 +893,6 @@ func writeViewStyle(name string, view ViewStyle, buffer *strings.Builder, indent
buffer.WriteString("}")
}
func getViewString(view View, excludeTags []string) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
writeViewStyle(view.Tag(), view, buffer, "", excludeTags)
return buffer.String()
}
func runStringWriter(writer stringWriter) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)

Some files were not shown because too many files have changed in this diff Show More