diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97610c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +.DS_Store +demo/__debug_bin diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e6df305 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceRoot}/demo", + //"program": "${workspaceRoot}/editor", + "env": {}, + "args": [] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..60f919b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "cSpell.words": [ + "anoshenko", + "helvetica", + "htmlid", + "nesw", + "nwse", + "onclick", + "onkeydown", + "onmousedown", + "upgrader" + ] +} \ No newline at end of file diff --git a/README-ru.md b/README-ru.md new file mode 100644 index 0000000..32228f2 --- /dev/null +++ b/README-ru.md @@ -0,0 +1,4350 @@ +# Библиотека RUI + +Библиотека RUI (Remoute User Interface) предназначена для создания web приложений на языке go. + +Особенностью библиотеки заключается в том, что вся обработка данных осуществляется на сервере, +а браузер используется как тонкий клиент. Для связи клиента и сервера используется WebSocket. + +## Hello world + + type helloWorldSession struct { + } + + func (content *helloWorldSession) CreateRootView(session rui.Session) rui.View { + return rui.NewTextView(session, rui.Params { + rui.Text : "Hello world!!!", + }) + } + + func createHelloWorldSession(session rui.Session) rui.SessionContent { + return new(helloWorldSession) + } + + func main() { + app := rui.NewApplication("Hello world", "icon.svg", createHelloWorldSession) + app.Start("localhost:8000") + } + +В функции main создается rui приложение и запускается основной цикл. +При создании приложения задаются 3 параметра: имя приложения, имя иконки и функция createHelloWorldSession. +Функция createHelloWorldSession создает структуру реализующую интерфейс SessionContent: + + type SessionContent interface { + CreateRootView(session rui.Session) rui.View + } + +Для каждой новой сессии создается свой экземпляр структуры. + +Функция CreateRootView интерфейса SessionContent создает корневой элемент. +Когда пользователь обращается к приложению набрав в браузере адрес "localhost:8000", то создается новая сессия, +для нее создается новый экземпляр структуры helloWorldSession и в конце вызывается функция CreateRootView. +Функция createRootView возвращает представление строки текста, создаваемое с помощью функции NewTextView. + +Если вы хотите чтобы приложение было видно вне вашего компьютера, то поменяйте адрес в функции Start: + + app.Start(rui.GetLocalIP() + ":80") + +## Используемые типы данных + +### SizeUnit + +Структура SizeUnit используется для задания различных размеров элементов интерфейса, таких как ширина, высота, отступы, размер шрифта и т.п. +SizeUnit объявлена как + + type SizeUnit struct { + Type SizeUnitType + Value float64 + } + +где Type - тип размера; +Value - размер + +Тип может принимать следующие значения: + +| Значение | Константа | Описание | +|:--------:|----------------|-----------------------------------------------------------------------------| +| 0 | Auto | значение по умолчанию. Значение поля Value игнорируется | +| 1 | SizeInPixel | поле Value определяет размер в пикселях. | +| 2 | SizeInEM | поле Value определяет размер в em единицах. 1em равен базовому размеру шрифта, который задается в настройках браузера | +| 3 | SizeInEX | поле Value определяет размер в ex единицах. | +| 4 | SizeInPercent | поле Value определяет размер в процентах от размера родительского элемента. | +| 5 | SizeInPt | поле Value определяет размер в pt единицах (1pt = 1/72”). | +| 6 | SizeInPc | поле Value определяет размер в pc единицах (1pc = 12pt). | +| 7 | SizeInInch | поле Value определяет размер в дюймах. | +| 8 | SizeInMM | поле Value определяет размер в миллиметрах. | +| 9 | SizeInCM | поле Value определяет размер в сантиметрах. | +| 10 | SizeInFraction | поле Value определяет размер в частях. Используется только для задания размеров ячеек в GridLayout. | + +Для более наглядного и простого задания переменных типа SizeUnit могут использоваться функции приведенные ниже + +| Функция | Эквивалентное определение | +|----------------|----------------------------------------------------| +| rui.AutoSize() | rui.SizeUnit{ Type: rui.Auto, Value: 0 } | +| rui.Px(n) | rui.SizeUnit{ Type: rui.SizeInPixel, Value: n } | +| rui.Em(n) | rui.SizeUnit{ Type: rui.SizeInEM, Value: n } | +| rui.Ex(n) | rui.SizeUnit{ Type: rui.SizeInEX, Value: n } | +| rui.Percent(n) | rui.SizeUnit{ Type: rui.SizeInPercent, Value: n } | +| rui.Pt(n) | rui.SizeUnit{ Type: rui.SizeInPt, Value: n } | +| rui.Pc(n) | rui.SizeUnit{ Type: rui.SizeInPc, Value: n } | +| rui.Inch(n) | rui.SizeUnit{ Type: rui.SizeInInch, Value: n } | +| rui.Mm(n) | rui.SizeUnit{ Type: rui.SizeInMM, Value: n } | +| rui.Cm(n) | rui.SizeUnit{ Type: rui.SizeInCM, Value: n } | +| rui.Fr(n) | rui.SizeUnit{ Type: rui.SizeInFraction, Value: n } | + +Переменные типа SizeUnit имеют текстовое представление (зачем оно нужно будет описано ниже). Текстовое представление состоит из числа (равному значению поля Value) и следующим за ним суффиксом определяющим тип. Исключением является значение типа Auto, которое имеет представление “auto”. Суффиксы перечислены в следующей таблице: + +| Суффикс | Тип | +|:-------:|----------------| +| px | SizeInPixel | +| em | SizeInEM | +| ex | SizeInEX | +| % | SizeInPercent | +| pt | SizeInPt | +| pc | SizeInPc | +| in | SizeInInch | +| mm | SizeInMM | +| cm | SizeInCM | +| fr | SizeInFraction | + +Примеры: auto, 50%, 32px, 1.5in, 0.8em + +Чтобы преобразовать текстовое представление в структуру SizeUnit используется функция: + + func StringToSizeUnit(value string) (SizeUnit, bool) + +Получить текстовое представление структуры можно свойством String() + +### Color + +Тип Color описывает 32-битный цвет в формате ARGB: + + type Color uint32 + +Тип Color имеет три типа текстовых представлений: + +1) #AARRGGBB, #RRGGBB, #ARGB, #RGB + +где A, R, G, B это шестнадцатеричная цифра описывающая соответствующую компоненту. Если альфа канал не задается, то он считается равным FF. Если цветовая компонента задается одной цифрой, то она удваивается. Например “#48AD” эквивалентно “#4488AADD” + +2) argb(A, R, G, B), rgb(R, G, B) + +где A, R, G, B это представление цветовой компоненты. Компонента может быть запада в виде дробного числа в диапазоне [0…1] или в виде целого числа в диапазоне [0…255] или в виде процентов от 0% до 100%. +Примеры: + + “argb(255, 128, 96, 0)” + “rgb(1.0, .5, .8)” + “rgb(0%, 50%, 25%)” + “argb(50%, 128, .5, 100%)” + +Для преобразования Color в строку используется метод String. +Для преобразования строки в Color используется функция: + + func StringToColor(value string) (Color, bool) + +3) Имя цвета. В библиотеке определены следующие цвета + +| Имя | Значение | +|-----------------------|-----------| +| black | #ff000000 | +| silver | #ffc0c0c0 | +| gray | #ff808080 | +| white | #ffffffff | +| maroon | #ff800000 | +| red | #ffff0000 | +| purple | #ff800080 | +| fuchsia | #ffff00ff | +| green | #ff008000 | +| lime | #ff00ff00 | +| olive | #ff808000 | +| yellow | #ffffff00 | +| navy | #ff000080 | +| blue | #ff0000ff | +| teal | #ff008080 | +| aqua | #ff00ffff | +| orange | #ffffa500 | +| aliceblue | #fff0f8ff | +| antiquewhite | #fffaebd7 | +| aquamarine | #ff7fffd4 | +| azure | #fff0ffff | +| beige | #fff5f5dc | +| bisque | #ffffe4c4 | +| blanchedalmond | #ffffebcd | +| blueviolet | #ff8a2be2 | +| brown | #ffa52a2a | +| burlywood | #ffdeb887 | +| cadetblue | #ff5f9ea0 | +| chartreuse | #ff7fff00 | +| chocolate | #ffd2691e | +| coral | #ffff7f50 | +| cornflowerblue | #ff6495ed | +| cornsilk | #fffff8dc | +| crimson | #ffdc143c | +| cyan | #ff00ffff | +| darkblue | #ff00008b | +| darkcyan | #ff008b8b | +| darkgoldenrod | #ffb8860b | +| darkgray | #ffa9a9a9 | +| darkgreen | #ff006400 | +| darkgrey | #ffa9a9a9 | +| darkkhaki | #ffbdb76b | +| darkmagenta | #ff8b008b | +| darkolivegreen | #ff556b2f | +| darkorange | #ffff8c00 | +| darkorchid | #ff9932cc | +| darkred | #ff8b0000 | +| darksalmon | #ffe9967a | +| darkseagreen | #ff8fbc8f | +| darkslateblue | #ff483d8b | +| darkslategray | #ff2f4f4f | +| darkslategrey | #ff2f4f4f | +| darkturquoise | #ff00ced1 | +| darkviolet | #ff9400d3 | +| deeppink | #ffff1493 | +| deepskyblue | #ff00bfff | +| dimgray | #ff696969 | +| dimgrey | #ff696969 | +| dodgerblue | #ff1e90ff | +| firebrick | #ffb22222 | +| floralwhite | #fffffaf0 | +| forestgreen | #ff228b22 | +| gainsboro | #ffdcdcdc | +| ghostwhite | #fff8f8ff | +| gold | #ffffd700 | +| goldenrod | #ffdaa520 | +| greenyellow | #ffadff2f | +| grey | #ff808080 | +| honeydew | #fff0fff0 | +| hotpink | #ffff69b4 | +| indianred | #ffcd5c5c | +| indigo | #ff4b0082 | +| ivory | #fffffff0 | +| khaki | #fff0e68c | +| lavender | #ffe6e6fa | +| lavenderblush | #fffff0f5 | +| lawngreen | #ff7cfc00 | +| lemonchiffon | #fffffacd | +| lightblue | #ffadd8e6 | +| lightcoral | #fff08080 | +| lightcyan | #ffe0ffff | +| lightgoldenrodyellow | #fffafad2 | +| lightgray | #ffd3d3d3 | +| lightgreen | #ff90ee90 | +| lightgrey | #ffd3d3d3 | +| lightpink | #ffffb6c1 | +| lightsalmon | #ffffa07a | +| lightseagreen | #ff20b2aa | +| lightskyblue | #ff87cefa | +| lightslategray | #ff778899 | +| lightslategrey | #ff778899 | +| lightsteelblue | #ffb0c4de | +| lightyellow | #ffffffe0 | +| limegreen | #ff32cd32 | +| linen | #fffaf0e6 | +| magenta | #ffff00ff | +| mediumaquamarine | #ff66cdaa | +| mediumblue | #ff0000cd | +| mediumorchid | #ffba55d3 | +| mediumpurple | #ff9370db | +| mediumseagreen | #ff3cb371 | +| mediumslateblue | #ff7b68ee | +| mediumspringgreen | #ff00fa9a | +| mediumturquoise | #ff48d1cc | +| mediumvioletred | #ffc71585 | +| midnightblue | #ff191970 | +| mintcream | #fff5fffa | +| mistyrose | #ffffe4e1 | +| moccasin | #ffffe4b5 | +| navajowhite | #ffffdead | +| oldlace | #fffdf5e6 | +| olivedrab | #ff6b8e23 | +| orangered | #ffff4500 | +| orchid | #ffda70d6 | +| palegoldenrod | #ffeee8aa | +| palegreen | #ff98fb98 | +| paleturquoise | #ffafeeee | +| palevioletred | #ffdb7093 | +| papayawhip | #ffffefd5 | +| peachpuff | #ffffdab9 | +| peru | #ffcd853f | +| pink | #ffffc0cb | +| plum | #ffdda0dd | +| powderblue | #ffb0e0e6 | +| rosybrown | #ffbc8f8f | +| royalblue | #ff4169e1 | +| saddlebrown | #ff8b4513 | +| salmon | #fffa8072 | +| sandybrown | #fff4a460 | +| seagreen | #ff2e8b57 | +| seashell | #fffff5ee | +| sienna | #ffa0522d | +| skyblue | #ff87ceeb | +| slateblue | #ff6a5acd | +| slategray | #ff708090 | +| slategrey | #ff708090 | +| snow | #fffffafa | +| springgreen | #ff00ff7f | +| steelblue | #ff4682b4 | +| tan | #ffd2b48c | +| thistle | #ffd8bfd8 | +| tomato | #ffff6347 | +| turquoise | #ff40e0d0 | +| violet | #ffee82ee | +| wheat | #fff5deb3 | +| whitesmoke | #fff5f5f5 | +| yellowgreen | #ff9acd32 | + + +### AngleUnit + +Тип AngleUnit используется для задания угловых величин. +AngleUnit объявлена как + + type AngleUnit struct { + Type AngleUnitType + Value float64 + } + +где Type - тип угловой величины; +Value - угловая величина + +Тип может принимать следующие значения: +* Radian (0) - поле Value определяет угловую величину в радианах. +* PiRadian (1) - поле Value определяет угловую величину в радианах умноженных на π. +* Degree (2) - поле Value определяет угловую величину в градусах. +* Gradian (3) - поле Value определяет угловую величину в градах (градианах). +* Turn (4) - поле Value определяет угловую величину в оборотах (1 оборот == 360°). + +Для более наглядного и простого задания переменных типа AngleUnit могут использоваться функции приведенные ниже + +| Функция | Эквивалентное определение | +|---------------|-------------------------------------------------| +| rui.Rad(n) | rui.AngleUnit{ Type: rui.Radian, Value: n } | +| rui.PiRad(n) | rui.AngleUnit{ Type: rui.PiRadian, Value: n } | +| rui.Deg(n) | rui.AngleUnit{ Type: rui.Degree, Value: n } | +| rui.Grad(n) | rui.AngleUnit{ Type: rui.Gradian, Value: n } | + +Переменные типа AngleUnit имеют текстовое представление состоящее из числа (равному значению поля Value) и следующим за ним суффиксом определяющим тип. Суффиксы перечислены в следующей таблице: + +| Суффикс | Тип | +|:-------:|----------| +| deg | Degree | +| ° | Degree | +| rad | Radian | +| π | PiRadian | +| pi | PiRadian | +| grad | Gradian | +| turn | Turn | + +Примеры: “45deg”, “90°”, “3.14rad”, “2π”, “0.5pi” + +Для преобразования AngleUnit в строку используется метод String. +Для преобразования строки в AngleUnit используется функция: + + func StringToAngleUnit(value string) (AngleUnit, bool) + +## View + +View это интерфейс для доступа к элементу типа "View". View это прямоугольная область экрана. +Все элементы интерфейса расширяют интерфейс View, т.е. View является базовым элементом для всех +других элементов библиотеки. + +View имеет ряд свойств, таких как высота, ширина, цвет, параметры текста и т.д. Каждое свойство +имеет текстовое имя. Для чтения и записи значения свойства используются интерфейс Properties +(View реализует данный интерфейс): + + type Properties interface { + Get(tag string) interface{} + Set(tag string, value interface{}) bool + Remove(tag string) + Clear() + AllTags() []string + } + +Функция Get возвращает значение свойства или nil если свойство не установлено. + +Функция Set устанавливает значение свойства. Если значение свойства установлено успешно, то +функция возвращает true, если нет то false и в лог записывается описание возникшей ошибки. + +Функция Remove удаляет значение свойства, эквивалентно Set(nil) + +Для упрощения установки/чтения свойств имеются также две глобальные функции Get и Set: + + func Get(rootView View, viewID, tag string) interface{} + func Set(rootView View, viewID, tag string, value interface{}) bool + +Данные функции возвращают/устанавливают значение дочернего View + +### События + +При взаимодействии с приложением возникаю различные события: клики, изменение размеров, +изменение вводимых данных и т.п. + +Для реакции на события предназначены слушатели событий. Слушатель это функция которая вызывается +каждый раз когда возникает событие. У каждого события может быть несколько слушателей. Разберем +слушателей на примере события изменения текста "edit-text-changed" в редакторе "EditView". + +Слушателем события является функция вида + + func([, <параметры>]) + +где первый аргумент это View в котором произошло событие. Далее идут дополнительные параметры события. + +Для "edit-text-changed" основной слушатель будет иметь следующий вид: + + func(EditView, string) + +где второй аргумент это новое значение текста + +Если вы не планируте использовать первый аргумент, то его можно опустить. Это будет дополнительный слушатель + + func(string) + +Для того чтобы назначить слушателя необходимо его присвоить свойству с именем события + + view.Set(rui.EditTextChanged, func(edit EditView, newText string) { + // do something + }) + +или + + view.Set(rui.EditTextChanged, func(newText string) { + // do something + }) + +У каждого события может быть несколько слушателей. В связи с этим в качестве слушателей могут +использоваться пять типов данных + +* функция func(< View >[, <параметры>]) +* функция func([<параметры>]) +* массив функций []func(< View >[, <параметры>]) +* массив функций []func([<параметры>]) +* []interface{} содержащий только func(< View >[, <параметры>]) и func([<параметры>]) + +После присваивания свойству все эти типы преобразуются в массив функций []func(, [<параметры>]). +Соответственно функция Get всегда возвращает массив функций []func(, [<параметры>]). +В случае отсутствия слушателей этот массив будет пуст + +Для события "edit-text-changed" это + +* func(editor EditView, newText string) +* func(newText string) +* []func(editor EditView, newText string) +* []func(newText string) +* []interface{} содержащий только func(editor EditView, newText string) и func(newText string) + +А свойство "edit-text-changed" всегда хранит и возвращает []func(EditView, string). + +В дальнейшем при описании конкретных событий будет приводиться только формат основного слушателя. + +### Свойство "id" + +Свойство "id" это необязательный текстовый идентификатор View. С его помощью можно найти +дочерний View. Для этого используется функция ViewByID + + func ViewByID(rootView View, id string) View + +Данная функция ищет дочерний View с идентификатором id. Поиск начинается с rootView. +Если View не найден, то функция возвращает nil и в лог записывается сообщение об ошибке. + +Обычно id устанавливается при создании View и в дальнейшем не меняется. +Но это необязательное условие. Вы можете поменять id в любой момент. + +Для установки нового значения id используется функция Set. Например + + view.Set(rui.ID, "myView") + view.Set("id", "myView") + +Получить id можно двумя способами. Первый - используя функцию Get: +Например + + if value := view.Get(rui.ID); value != nil { + id = value.(string) + } + +И второй - используя функцию ID(): + + id = view.ID() + + +### Свойства "width", "height", "min-width", "min-height", "max-width", "max-height" + +Данные свойства устанавливают: + +| Свойство | Константа | Описание | +|--------------|---------------|--------------------------| +| "width" | rui.Width | Ширина View | +| "height" | rui.Height | Высота View | +| "min-width" | rui.MinWidth | Минимальная ширина View | +| "min-height" | rui.MinHeight | Минимальная высота View | +| "max-width" | rui.MaxWidth | Максимальная ширина View | +| "max-height" | rui.MaxHeight | Максимальная высота View | + +Данные свойства имеют тип SizeUnit. +Если значение "width"/"height" не установлены или установлены в Auto, то высота/ширина +View определяется его содержимым и ограничено минимальной и максимальной высотой/шириной. +В качестве значения данных свойств можно установить SizeUnit структуру, текстовое представление +SizeUnit или имя константы (о константах ниже): + + view.Set("width", rui.Px(8)) + view.Set(rui.MaxHeight, "80%") + view.Set(rui.Height, "@viewHeight") + +После получения значения функцией Get вы должны выполнить приведение типов: + + if value := view.Get(rui.Width); value != nil { + switch value.(type) { + case string: + text := value.(string) + // TODO + + case SizeUnit: + size := value.(SizeUnit) + // TODO + } + } + +Это довольно громоздко поэтому для каждого свойства существует одноимённая глобальная функция с префиксом Get, +которая выполняет данное приведение типа, получает значение константы, если необходимо, и +возвращает его. Все функции данного типа имеют два аргумента: View и subviewID string. +Первый аргумент это корневой View, второй - ID дочернего View. Если ID дочернего View передать как "", +то возвращается значение корневого View. +Для свойств "width", "height", "min-width", "min-height", "max-width", "max-height" это функции: + + func GetWidth(view View, subviewID string) SizeUnit + func GetHeight(view View, subviewID string) SizeUnit + func GetMinWidth(view View, subviewID string) SizeUnit + func GetMinHeight(view View, subviewID string) SizeUnit + func GetMaxWidth(view View, subviewID string) SizeUnit + func GetMaxHeight(view View, subviewID string) SizeUnit + +### Свойства "margin" и "padding" + +Свойство "margin" определяет внешние отступы от данного View до соседних. +Свойство "padding" устанавливает внутренние отступы от границы View до контента. +Значение свойств "margin" и "padding" хранятся в виде интерфейса BoundsProperty, +реализующего интерфейс Properties (см. выше). BoundsProperty имеет 4 свойства типа SizeUnit: + +| Свойство | Константа | Описание | +|-----------|--------------|------------------| +| "top" | rui.Top | Верхний отступ | +| "right" | rui.Right | Правый отступ | +| "bottom" | rui.Bottom | Нижний отступ | +| "left" | rui.Left | Левый отступ | + +Для создания интерфейса BoundsProperty используется функция NewBoundsProperty. Пример + + view.Set(rui.Margin, NewBoundsProperty(rui.Params { + rui.Top: rui.Px(8), + rui.Left: "@topMargin", + "right": "1.5em", + "bottom": rui.Inch(0.3), + }))) + +Соотвественно если вы запросите свойство "margin" или "padding" с помощью метода Get, +то вернется интерфейс BoundsProperty: + + if value := view.Get(rui.Margin); value != nil { + margin := value.(BoundsProperty) + } + +BoundsProperty с помощью функции "Bounds(session Session) Bounds" интерфейса BoundsProperty +может быть преобразовано в более удобную структуру Bounds: + + type Bounds struct { + Top, Right, Bottom, Left SizeUnit + } + +Для этого используется также могут использоваться глобальные функции: + + func GetMargin(view View, subviewID string) Bounds + func GetPadding(view View, subviewID string) Bounds + +Текстовое представление BoundsProperty имеет следующий вид: + + "_{ top = <верхний отступ>, right = <правый отступ>, bottom = <нижний отступ>, left = <левый отступ> }" + +В качестве значения свойств "margin" и "padding" методу Set может быть передано: +* интерфейс BoundsProperty или его текстовое представление; +* структура Bounds; +* SizeUnit или имя константы типа SizeUnit, в этом случай это значение устанавливается во все отступы. Т.е. + + view.Set(rui.Margin, rui.Px(8)) + +эквивалентно + + view.Set(rui.Margin, rui.Bounds{Top: rui.Px(8), Right: rui.Px(8), Bottom: rui.Px(8), Left: rui.Px(8)}) + +Так как значение свойства "margin" и "padding" всегда хранятся в виде интерфейса BoundsProperty, +то если вы прочитаете функцией Get свойство "margin" или "padding" установленное значением Bounds +или SizeUnit, то вы получите BoundsProperty, а не Bounds или SizeUnit. + +Свойства "margin" и "padding" используются для установки сразу четырех отступов. Для установки +отдельных отступов используются следующие свойства: + +| Свойство | Константа | Описание | +|------------------|--------------------|--------------------------| +| "margin-top" | rui.MarginTop | Верхний внешний отступ | +| "margin-right" | rui.MarginRight | Правый внешний отступ | +| "margin-bottom" | rui.MarginBottom | Нижний внешний отступ | +| "margin-left" | rui.MarginLeft | Левый внешний отступ | +| "padding-top" | rui.PaddingTop | Верхний внутренний отступ | +| "padding-right" | rui.PaddingRight | Правый внутренний отступ | +| "padding-bottom" | rui.PaddingBottom | Нижний внутренний отступ | +| "padding-left" | rui.PaddingLeft | Левый внутренний отступ | + +Например + + view.Set(rui.Margin, rui.Px(8)) + view.Set(rui.TopMargin, rui.Px(12)) + +эквивалентно + + view.Set(rui.Margin, rui.Bounds{Top: rui.Px(12), Right: rui.Px(8), Bottom: rui.Px(8), Left: rui.Px(8)}) + +### Свойство "border" + +Свойство "border" определяет рамку вокруг View. Линия рамки описывается тремя атрибутами: +стиль линии, толщина и цвет. + +Значение свойства "border" хранится в виде интерфейса BorderProperty, +реализующего интерфейс Properties (см. выше). BorderProperty может содержать следующие свойства: + +| Свойство | Константа | Тип | Описание | +|----------------|-------------|----------|-----------------------------| +| "left-style" | LeftStyle | int | Стиль левой линии рамки | +| "right-style" | RightStyle | int | Стиль правой линии рамки | +| "top-style" | TopStyle | int | Стиль верхней линии рамки | +| "bottom-style" | BottomStyle | int | Стиль нижней линии рамки | +| "left-width" | LeftWidth | SizeUnit | Толщина левой линии рамки | +| "right-width" | RightWidth | SizeUnit | Толщина правой линии рамки | +| "top-width" | TopWidth | SizeUnit | Толщина верхней линии рамки | +| "bottom-width" | BottomWidth | SizeUnit | Толщина нижней линии рамки | +| "left-color" | LeftColor | Color | Цвет левой линии рамки | +| "right-color" | RightColor | Color | Цвет правой линии рамки | +| "top-color" | TopColor | Color | Цвет верхней линии рамки | +| "bottom-color" | BottomColor | Color | Цвет нижней линии рамки | + +Стиль линии может принимать следующие значения: + +| Значение | Константа | Имя | Описание | +|:--------:|------------|----------|--------------------------| +| 0 | NoneLine | "none" | Нет рамки | +| 1 | SolidLine | "solid" | Сплошная линия | +| 2 | DashedLine | "dashed" | Пунктирная линия | +| 3 | DottedLine | "dotted" | Линия состоящая из точек | +| 4 | DoubleLine | "double" | Двойная сплошная линия | + +Все другие значения стиля игнорируются. + +Для создания интерфейса BorderProperty используется функция NewBorder. + +Если все линии рамки одинаковы, то для задания стиля, толщины и цвета могут использоваться следующие свойства: + +| Свойство | Константа | Тип | Описание | +|----------|---------------|----------|-----------------------| +| "style" | Style | int | Стиль линии рамки | +| "width" | Width | SizeUnit | Толщина линии рамки | +| "color" | ColorProperty | Color | Цвет линии рамки | + +Пример + + view.Set(rui.Border, NewBorder(rui.Params{ + rui.LeftStyle: rui.SolidBorder, + rui.RightStyle: rui.SolidBorder, + rui.TopStyle: rui.SolidBorder, + rui.BottomStyle: rui.SolidBorder, + rui.LeftWidth: rui.Px(1), + rui.RightWidth: rui.Px(1), + rui.TopWidth: rui.Px(1), + rui.BottomWidth: rui.Px(1), + rui.LeftColor: rui.Black, + rui.RightColor: rui.Black, + rui.TopColor: rui.Black, + rui.BottomColor: rui.Black, + })) + +эквивалентно + + view.Set(rui.Border, NewBorder(rui.Params{ + rui.Style: rui.SolidBorder, + rui.Width: rui.Px(1), + rui.ColorProperty: rui.Black, + })) + +Интерфейс BorderProperty может быть преобразован в структуру ViewBorders с помощью функции Border. +При преобразовании все текстовые константы заменяются реальными значениями. ViewBorders описана как + + type ViewBorders struct { + Top, Right, Bottom, Left ViewBorder + } + +где структура ViewBorder описана как + + type ViewBorder struct { + Style int + Color Color + Width SizeUnit + } + +Структура ViewBorders может быть передана в качестве параметра функции Set при установке значения свойства "border". +При этом ViewBorders преобразуется в BorderProperty. Поэтому при чтении свойства функцией Get будет возвращен интерфейс +BorderProperty, а не структура ViewBorders. Получить структуру ViewBorders без дополнительных преобразований можно +с помощью глобальной функции + + func GetBorder(view View, subviewID string) ViewBorders + +Кроме вспомогательных свойств "style", "width" и "color" есть еще 4: "left", "right", "top" и "bottom". +В качестве значения эти свойства могут принимать только структуру ViewBorder и позволяю установить все +атрибуты линии одноименной стороны. + +Вы также можете устанавливать отдельные атрибуты рамки использую функцию Set интерфейса View. +Для этого используются следующие свойства + +| Свойство | Константа | Тип | Описание | +|-----------------------|-------------------|------------|-----------------------------| +| "border-left-style" | BorderLeftStyle | int | Стиль левой линии рамки | +| "border-right-style" | BorderRightStyle | int | Стиль правой линии рамки | +| "border-top-style" | BorderTopStyle | int | Стиль верхней линии рамки | +| "border-bottom-style" | BorderBottomStyle | int | Стиль нижней линии рамки | +| "border-left-width" | BorderLeftWidth | SizeUnit | Толщина левой линии рамки | +| "border-right-width" | BorderRightWidth | SizeUnit | Толщина правой линии рамки | +| "border-top-width" | BorderTopWidth | SizeUnit | Толщина верхней линии рамки | +| "border-bottom-width" | BorderBottomWidth | SizeUnit | Толщина нижней линии рамки | +| "border-left-color" | BorderLeftColor | Color | Цвет левой линии рамки | +| "border-right-color" | BorderRightColor | Color | Цвет правой линии рамки | +| "border-top-color" | BorderTopColor | Color | Цвет верхней линии рамки | +| "border-bottom-color" | BorderBottomColor | Color | Цвет нижней линии рамки | +| "border-style" | BorderStyle | int | Стиль линии рамки | +| "border-width" | BorderWidth | SizeUnit | Толщина линии рамки | +| "border-color" | BorderColor | Color | Цвет линии рамки | +| "border-left" | BorderLeft | ViewBorder | Левая линия рамки | +| "border-right" | BorderRight | ViewBorder | Правая линия рамки | +| "border-top" | BorderTop | ViewBorder | Верхняя линия рамки | +| "border-bottom" | BorderBottom | ViewBorder | Нижняя линия рамки | + +Например + + view.Set(rui.BorderStyle, rui.SolidBorder) + view.Set(rui.BorderWidth, rui.Px(1)) + view.Set(rui.BorderColor, rui.Black) + +эквивалентно + + view.Set(rui.Border, NewBorder(rui.Params{ + rui.Style: rui.SolidBorder, + rui.Width: rui.Px(1), + rui.ColorProperty: rui.Black, + })) + +### Свойство "radius" + +Свойство "radius" задает эллиптический радиус скругления углов View. Радиусы задаются интерфейсом +RadiusProperty реализующим интерфейс Properties (см. выше). +Для этого используются следующие свойства типа SizeUnit: + +| Свойство | Константа | Описание | +|------------------|--------------|--------------------------------| +| "top-left-x" | TopLeftX | x-радиус верхнего левого угла | +| "top-left-y" | TopLeftY | y-радиус верхнего левого угла | +| "top-right-x" | TopRightX | x-радиус верхнего правого угла | +| "top-right-y" | TopRightY | y-радиус верхнего правого угла | +| "bottom-left-x" | BottomLeftX | x-радиус нижнего левого угла | +| "bottom-left-y" | BottomLeftY | y-радиус нижнего левого угла | +| "bottom-right-x" | BottomRightX | x-радиус нижнего правого угла | +| "bottom-right-y" | BottomRightY | y-радиус нижнего правого угла | + +Если x- и y-радиусы одинаковы то можно воспользоваться вспомогательными свойствами + +| Свойство | Константа | Описание | +|----------------|--------------|------------------------------| +| "top-left" | TopLeft | радиус верхнего левого угла | +| "top-right" | TopRight | радиус верхнего правого угла | +| "bottom-left" | BottomLeft | радиус нижнего левого угла | +| "bottom-right" | BottomRight | радиус нижнего правого угла | + +Для установки всех радиусов одинаковыми значениями используются свойства "x" и "y" + +Интерфейс RadiusProperty создается с помощью функции NewRadiusProperty. Пример + + view.Set(rui.Radius, NewRadiusProperty(rui.Params{ + rui.X: rui.Px(16), + rui.Y: rui.Px(8), + rui.TopLeft: rui.Px(0), + rui.BottomRight: rui.Px(0), + })) + +эквивалентно + + view.Set(rui.Radius, NewRadiusProperty(rui.Params{ + rui.TopRightX: rui.Px(16), + rui.TopRightY: rui.Px(8), + rui.BottomLeftX: rui.Px(16), + rui.BottomLeftY: rui.Px(8), + rui.TopLeftX: rui.Px(0), + rui.TopLeftX: rui.Px(0), + rui.BottomRightX: rui.Px(0), + rui.BottomRightY: rui.Px(0), + })) + +Если все радиусы одинаковы, то данное значение типа SizeUnit может быть напрямую присвоено свойству "radius" + + view.Set(rui.Radius, rui.Px(4)) + +RadiusProperty имеет текстовое представление следующего вида: + + _{ = [/ ] [, = [/ ]] … } + +где может принимать следующие значения: + +| Свойство | Описание | +|------------------|-------------------------------------| +| "x" | Все x-радиусы | +| "y" | Все y-радиусы | +| "top-left" | x- и y-радиус верхнего левого угла | +| "top-left-x" | x-радиус верхнего левого угла | +| "top-left-y" | y-радиус верхнего левого угла | +| "top-right" | x- и y-радиус верхнего правого угла | +| "top-right-x" | x-радиус верхнего правого угла | +| "top-right-y" | y-радиус верхнего правого угла | +| "bottom-left" | x- и y-радиус нижнего левого угла | +| "bottom-left-x" | x-радиус нижнего левого угла | +| "bottom-left-y" | y-радиус нижнего левого угла | +| "bottom-right" | x- и y-радиус нижнего правого угла | +| "bottom-right-x" | x-радиус нижнего правого угла | +| "bottom-right-y" | y-радиус нижнего правого угла | + +Значения вида " / " можно присваивать только +свойствам "top-left", "top-right", "bottom-left" и "bottom-right". + +Примеры: + + _{ x = 4px, y = 4px, top-left = 8px, bottom-right = 8px } + +эквивалентно + + _{ top-left = 8px, top-right = 4px, bottom-left = 4px, bottom-right = 8px } + +или + + _{ top-left = 8px / 8px, top-right = 4px / 4px, bottom-left = 4px / 4px, bottom-right = 8px / 8px } + +или + + _{ top-left-x = 8px, top-left-y = 8px, top-right-x = 4px, top-right-y = 4px, + bottom-left-x = 4px, bottom-left-y = 4px, bottom-right-x = 8px, bottom-right-y = 8px } + +Интерфейс RadiusProperty может быть преобразован в структуру BoxRadius с помощью функции BoxRadius. +При преобразовании все текстовые константы заменяются реальными значениями. BoxRadius описана как + + type BoxRadius struct { + TopLeftX, TopLeftY, TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY SizeUnit + } + +Структура BoxRadius может быть передана в качестве параметра функции Set при установке значения свойства "radius". +При этом BoxRadius преобразуется в RadiusProperty. Поэтому при чтении свойства функцией Get будет возвращен интерфейс +RadiusProperty, а не структура BoxRadius. Получить структуру BoxRadius без дополнительных преобразований можно +с помощью глобальной функции + + func GetRadius(view View, subviewID string) BoxRadius + +Вы также можете устанавливать отдельные радиусы использую функцию Set интерфейса View. +Для этого используются следующие свойства + +| Свойство | Константа | Описание | +|-------------------------|--------------------|-------------------------------------| +| "radius-x" | RadiusX | Все x-радиусы | +| "radius-y" | RadiusY | Все y-радиусы | +| "radius-top-left" | RadiusTopLeft | x- и y-радиус верхнего левого угла | +| "radius-top-left-x" | RadiusTopLeftX | x-радиус верхнего левого угла | +| "radius-top-left-y" | RadiusTopLeftY | y-радиус верхнего левого угла | +| "radius-top-right" | RadiusTopRight | x- и y-радиус верхнего правого угла | +| "radius-top-right-x" | RadiusTopRightX | x-радиус верхнего правого угла | +| "radius-top-right-y" | RadiusTopRightY | y-радиус верхнего правого угла | +| "radius-bottom-left" | RadiusBottomLeft | x- и y-радиус нижнего левого угла | +| "radius-bottom-left-x" | RadiusBottomLeftX | x-радиус нижнего левого угла | +| "radius-bottom-left-y" | RadiusBottomLeftY | y-радиус нижнего левого угла | +| "radius-bottom-right" | RadiusBottomRight | x- и y-радиус нижнего правого угла | +| "radius-bottom-right-x" | RadiusBottomRightX | x-радиус нижнего правого угла | +| "radius-bottom-right-y" | RadiusBottomRightY | y-радиус нижнего правого угла | + +Например + + view.Set(rui.RadiusX, rui.Px(4)) + view.Set(rui.RadiusY, rui.Px(32)) + +эквивалентно + + view.Set(rui.Border, NewRadiusProperty(rui.Params{ + rui.X: rui.Px(4), + rui.Y: rui.Px(32), + })) + +### Свойство "shadow" + +Свойство "shadow" позволяет задать тени для View. Теней может быть несколько. Тень описывается +с помощью интерфейса ViewShadow расширяющего интерфейс Properties (см. выше). У тени имеются следующие свойства: + +| Свойство | Константа | Тип | Описание | +|-----------------|---------------|----------|---------------------------------------------------------------| +| "color" | ColorProperty | Color | Цвет тени | +| "inset" | Inset | bool | true - тень внутри View, false - снаружи | +| "x-offset" | XOffset | SizeUnit | Смещение тени по оси X | +| "y-offset" | YOffset | SizeUnit | Смещение тени по оси Y | +| "blur" | BlurRadius | float | Радиус размытия тени. Значение должно быть >= 0 | +| "spread-radius" | SpreadRadius | float | Увеличение тени. Значение > 0 увеличивает тень, < 0 уменьшает | + +Для создания ViewShadow используются три функции: + + 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 + +Функция NewViewShadow создает внешнюю тень (Inset == false), NewInsetViewShadow - внутреннюю +(Inset == true). +Функция NewShadowWithParams используется когда в качестве параметров необходимо использовать +константы. Например: + + shadow := NewShadowWithParams(rui.Params{ + rui.ColorProperty : "@shadowColor", + rui.BlurRadius : 8.0, + rui.Dilation : 16.0, + }) + +В качестве значения свойству "shadow" может быть присвоено ViewShadow, массив ViewShadow, +текстовое представление ViewShadow. + +Текстовое представление ViewShadow имеет следующий формат: + + _{ color = <цвет> [, x-offset = <смещение>] [, y-offset = <смещение>] [, blur = <радиус>] + [, spread-radius = <увеличени>] [, inset = <тип>] } + + +Получить значение данного свойства можно с помощью функции + + func GetViewShadows(view View, subviewID string) []ViewShadow + +Если тень не задана, то данная функция вернет пустой массив + +### Свойство "background-color" + +Константа: rui.BackgroundColor. Get функция: BackgroundColor() Color + +Свойство "background-color" устанавливает цвет фона. Допустимые значения: Color, целое число, текстовой представление Color и имя константы начинающееся с '@'. +Целое число должно кодировать цвет в формате AARRGGBB + +Кроме цвета в качестве фона можно также использовать изображения и градиенты (см. ниже). +В этом случае "background-color" используется для прозрачных участков изображений. + +### Свойство "background-clip" + +Свойство "background-clip" определяет как цвет фона и/или фоновое изображение будут выводиться под границами блока. + +Если фоновое изображение или цвет не заданы, это свойство будет иметь визуальный эффект, только если у границы есть прозрачные области или частично непрозрачные области; в противном случае граница скрывает разницу. + +Свойство может принимать следующие значения: + +| Значение | Константа | Имя | Описание | +|:--------:|----------------|---------------|------------------------------------------------| +| 0 | BorderBoxClip | "border-box" | Фон распространяется до внешнего края границы (но под границей в z-порядке). | +| 1 | PaddingBoxClip | "padding-box" | Фон распространяется до внешнего края отступа. Под границей фон не рисуется. | +| 2 | ContentBoxClip | "content-box" | Фон закрашивается внутри (обрезается) поля содержимого. | + +### Свойство "background" + +В качестве фона View, помимо цвета, можно задать также картинки и/или градиентные заливки. +Для этого используется свойство "background". Фон может содержать несколько картинок и градиентов. +Каждый элемент фона описывается интерфейсом BackgroundElement. BackgroundElement может быть трех +типов: линейный градиент, радиальный градиент и изображение. + +#### Линейный градиент + +Линейный градиент создается с помощью функции + + func NewBackgroundLinearGradient(params Params) BackgroundElement + +Линейный градиент имеет следующие параметры: + +* Direction ("direction") - определяет направление линии градиента (линии вдоль которой меняется цвет). +Необязательный параметр. Направление по умолчанию - снизу вверх. +Может принимать или значение типа AngleUnit (угол наклона линии относительно вертикали) +или одно из следующих значений типа Int: + +| Значение | Константа | Имя | Описание | +|:--------:|-----------------------|-------------------|------------------------------------------------| +| 0 | ToTopGradient | "to-top" | Линия идет снизу вверх (значение по умолчанию) | +| 1 | ToRightTopGradient | "to-right-top" | Из левого нижнего угла в правый верхний | +| 2 | ToRightGradient | "to-right" | Слева направо | +| 3 | ToRightBottomGradient | "to-right-bottom" | Из левого верхнего угла в правый нижний | +| 4 | ToBottomGradient | "to-bottom" | Сверху вних | +| 5 | ToLeftBottomGradient | "to-left-bottom" | Из правого верхнего угла в левый нижний | +| 6 | ToLeftGradient | "to-left" | Справа налево | +| 7 | ToLeftTopGradient | "to-left-top" | Из правого нижнего угла в левый верхний | + +* Gradient ("gradient") - массив ключевых точек градиента (обязательный параметр). Каждая точка +описывается структурой BackgroundGradientPoint, которая имеет два поля: Pos типа SizeUnit и Color. +Pos определяет положение точки относительно начала линии градиента. Массив должен иметь не менее 2 точек. +В качестве значения градиента можно также передать массив Color. В этом случае точки равномерно +распределяются вдоль линии градиента. +Также в качестве массива ключевых точек можно использовать массив типа []interface{}. +Элементами этого массива могут быть BackgroundGradientPoint, Color, текстовое представление BackgroundGradientPoint +или Color и имя константы + +* Repeat ("repeat") - булево значение, определяющее будет ли повторяться градиент после последней +ключевой точки. Необязательный параметр. Значение по умолчанию - false (не повторять) + +Текстовое представление линейного градиента имеет следующий вид: + + linear-gradient { gradient = <значение> [, direction = <значение>] [, repeat = <значение>] } + +#### Радиальный градиент + +Радиальный градиент создается с помощью функции + + func NewBackgroundRadialGradient(params Params) BackgroundElement + +Радиальный градиент имеет следующие параметры: + +* Gradient ("gradient") - массив ключевых точек градиента (обязательный параметр). Идентичен одноименному +параметру линейного градиента. + +* Repeat ("repeat") - булево значение, определяющее будет ли повторяться градиент после последней +ключевой точки. Необязательный параметр. Значение по умолчанию - false (не повторять) + +* RadialGradientShape ("radial-gradient-shape") или Shape ("shape") - определяет форму градиента. +Может принимать одно из двух значений типа Int: + +| Значение | Константа | Имя | Описание | +|:--------:|-----------------|-----------|--------------------------------------------| +| 0 | EllipseGradient | "ellipse" | Формой является эллипс, выровненный по оси | +| 1 | CircleGradient | "circle" | Формой является круг с постоянным радиусом | + +Необязательный параметр. Значение по умолчанию EllipseGradient + +* RadialGradientRadius ("radial-gradient-radius") или Radius ("radius") - задает радиус градиента. +Может принимать или значение типа SizeUnit или одно из следующих значений типа Int: + +| Константа | Значение | Имя | Описание | +|------------------------|:--------:|-------------------|--------------------------------------------| +| ClosestSideGradient | 0 | "closest-side" | Конечная форма градиента соответствует стороне прямоугольника, ближайшей к его центру (для окружностей), или обеим вертикальным и горизонтальным сторонам, ближайшим к центру (для эллипсов) | +| ClosestCornerGradient | 1 | "closest-corner" | Конечная форма градиента определяется таким образом, чтобы он точно соответствовал ближайшему углу окна от его центра | +| FarthestSideGradient | 2 | "farthest-side" | Схоже с ClosestSideGradient, кроме того что, размер формы определяется самой дальней стороной от своего центра (или вертикальных и горизонтальных сторон) | +| FarthestCornerGradient | 3 | "farthest-corner" | Конечная форма градиента определяется таким образом, чтобы он точно соответствовал самому дальнему углу прямоугольника от его центра | + +Необязательный параметр. Значение по умолчанию ClosestSideGradient + +* CenterX ("center-x"), CenterY ("center-y") - задает центр градиента относительно левого верхнего +угла View. Принимает значение типа SizeUnit. Необязательный параметр. +Значение по умолчанию "50%", т.е. центр градиента совпадает с центром View. + +Текстовое представление линейного градиента имеет следующий вид: + + radial-gradient { gradient = <значение> [, repeat = <значение>] [, shape = <значение>] + [, radius = <значение>][, center-x = <значение>][, center-y = <значение>]} + +#### Изображение + +Изображение имеет следующие параметры: + +* Source ("src") - задает URL изображения + +* Fit ("fit") - необязательный параметр определяющий масштабирование изображения. +Может принимать одно из следующих значений типа Int: + +| Константа | Значение | Имя | Описание | +|------------|:--------:|-----------|------------------------------------------------------------------| +| NoneFit | 0 | "none" | Нет масштабирования (значение по умолчанию). Размеры изображения определяются параметрами Width и Height | +| ContainFit | 1 | "contain" | Изображение масштабирует с сохранением пропорций так, чтобы его ширина или высота равнялась ширине или высоте области фона. Изображение может обрезаться по ширине или высоте | +| CoverFit | 2 | "cover" | Изображение масштабирует с сохранением пропорций так, чтобы картинка целиком поместилась внутрь области фона | + +* Width ("width"), Height (height) - необязательные SizeUnit параметры задающие высоту и ширину +изображения. Используется только если параметр Fit равен NoneFit. Значение по умолчанию Auto (исходный +размер). Значение в процентах задает размер относительно высоты и ширины области фона соответственно + +* Attachment - ??? + +* Repeat (repeat) - необязательный параметр задающий повтор изображения. +Может принимать одно из следующих значений типа Int: + +| Константа | Значение | Имя | Описание | +|-------------|:--------:|-------------|---------------------------------------------------------------| +| NoRepeat | 0 | "no-repeat" | Изображение не повторяется (значение по умолчанию) | +| RepeatXY | 1 | "repeat" | Изображение повторяется по горизонтали и вертикали | +| RepeatX | 2 | "repeat-x" | Изображение повторяется только по горизонтали | +| RepeatY | 3 | "repeat-y" | Изображение повторяется только по вертикали | +| RepeatRound | 4 | "round" | Изображение повторяется так, чтобы в область фона поместилось целое число рисунков; если это не удаётся сделать, то фоновые рисунки масштабируются | +| RepeatSpace | 5 | "space" | Изображение повторяется столько раз, сколько требуется для заполнения области фона; если это не удаётся, между картинками добавляется пустое пространство | + +* ImageHorizontalAlign, +* ImageVerticalAlign, + +### Свойство "clip" + +Свойство "clip" (константа Clip) типа ClipShape задает задает область образки. +Есть 4 типа областей обрезки + +#### inset + +Прямоугольная область обрезки. Создается с помощью функции: + + func InsetClip(top, right, bottom, left SizeUnit, radius RadiusProperty) ClipShape + +где top, right, bottom, left это расстояние от соответственно верхней, правой, нижней и левой границы +View до одноименной границы обрезки; radius - задает радиусы скругления углов области обрезки +(описание типа RadiusProperty смотри выше). Если скругления углов не должно быть, то в качестве +radius необходимо передать nil + +Текстовое описание прямоугольной области обрезки имеет следующий формат + + inset{ top = , right = , bottom = , left = , + [radius = ] } + } + +#### circle + +Круглая область обрезки. Создается с помощью функции: + + func CircleClip(x, y, radius SizeUnit) ClipShape + +где x, y - координаты центра окружности; radius - радиус + +Текстовое описание круглой области обрезки имеет следующий формат + + circle{ x = , y = , radius = } + +#### ellipse + +Эллиптическая область обрезки. Создается с помощью функции: + + func EllipseClip(x, y, rx, ry SizeUnit) ClipShape + +где x, y - координаты центра эллипса; rх - радиус эллипса по оси X; ry - радиус эллипса по оси Y. + +Текстовое описание эллиптической области обрезки имеет следующий формат + + ellipse{ x = , y = , radius-x = , radius-y = } + +#### polygon + +Многоугольная область обрезки. Создается с помощью функций: + + func PolygonClip(points []interface{}) ClipShape + func PolygonPointsClip(points []SizeUnit) ClipShape + +в качестве аргумента передается массив угловых точек многоугольника в следующем порядке: x1, y1, x2, y2, … +В качестве элементов аргумента функции PolygonClip могут быть или текстовые константы, или +текстовое представление SizeUnit, или элементы типа SizeUnit. + +Текстовое описание многоугольной области обрезки имеет следующий формат + + polygon{ points = ", , , ,…" } + +### Свойство "оpacity" + +Свойство "оpacity" (константа Opacity) типа float64 задает прозрачность View. Допустимые значения от 0 до 1. +Где 1 - View полностью непрозрачен, 0 - полностью прозрачен. + +Получить значение данного свойства можно с помощью функции + + func GetOpacity(view View, subviewID string) float64 + +### Свойство "z-index" + +Свойство "z-index" (константа ZIndex) типа int определяет положение элемента и нижестоящих элементов по оси z. +В случае перекрытия элементов, это значение определяет порядок наложения. В общем случае, элементы +с большим z-index перекрывают элементы с меньшим. + +Получить значение данного свойства можно с помощью функции + + func GetZIndex(view View, subviewID string) int + +### Свойство "visibility" + +Свойство "visibility" (константа Visibility) типа int задает видимость View. Допустимые значения + +| Значение | Константа | Имя | Видимость | +|:--------:|-----------|-------------|------------------------------------| +| 0 | Visible | "visible" | View видим. Значение по умолчанию. | +| 1 | Invisible | "invisible" | View невидим, но занимает место. | +| 2 | Gone | "gone" | View невидим и не занимает место. | + +Получить значение данного свойства можно с помощью функции + + func GetVisibility(view View, subviewID string) int + +### Свойство "filter" + +Свойство "filter" (константа Filter) применяет ко View такие графические эффекты, как размытие и смещение цвета. +В качестве значения свойства "filter" используется только интерфейс ViewFilter. ViewFilter создается с помощью +функции + + func NewViewFilter(params Params) ViewFilter + +В аргументе перечисляются применяемые эффекты. Возможны следующие эффекты: + +| Эффект | Константа | Тип | Описание | +|---------------|------------|--------------------|----------------------------------| +| "blur" | Blur | float64 0…10000px | Размытие по Гауссу | +| "brightness" | Brightness | float64 0…10000% | Изменение яркости | +| "contrast" | Contrast | float64 0…10000% | Изменение контрастности | +| "drop-shadow" | DropShadow | []ViewShadow | Добавление тени | +| "grayscale" | Grayscale | float64 0…100% | Преобразование к оттенкам серого | +| "hue-rotate" | HueRotate | AngleUnit | Вращение оттенка | +| "invert" | Invert | float64 0…100% | Инвертирование цветов | +| "opacity" | Opacity | float64 0…100% | Изменение прозрачности | +| "saturate" | Saturate | float64 0…10000% | Изменение насыщености | +| "sepia" | Sepia | float64 0…100% | Преобразование в серпию | + +Получить значение текущего фильтра можно с помощью функции + + func GetFilter(view View, subviewID string) ViewFilter + +### Свойство "semantics" + +Свойство "semantics" (константа Semantics) типа string определяет семантический смысл View. +Данное свойство может не иметь видимого эффекта, но позволяет поисковикам понимать структуру вашего приложения. +Так же оно помогает озвучивать интерфейс системам для людей с ограниченными возможностями: + +| Значение | Имя | Семантика | +|:--------:|------------------|------------------------------------------------------| +| 0 | "default" | Не определена. Значение по умолчанию. | +| 1 | "article" | Самостоятельная часть приложения предназначенная для независимого распространения или повторного использования. | +| 2 | "section" | Автономный раздел который не может быть представлен более точным по семантике элементом | +| 3 | "aside" | Часть документа, чьё содержимое только косвенно связанно с основным содержимым (сноска, метка) | +| 4 | "header" | Заголовок приложения | +| 5 | "main" | Основной контент (содержимое) приложения | +| 6 | "footer" | Нижний колонтитул | +| 7 | "navigation" | Панель навигации | +| 8 | "figure" | Изображение | +| 9 | "figure-caption" | Заголовок Изображения. Должно быть внутри "figure" | +| 10 | "button" | Кнопка | +| 11 | "p" | Параграф | +| 12 | "h1" | Заголовок текста 1-го уровня. Изменяет стиль текста | +| 13 | "h2" | Заголовок текста 2-го уровня. Изменяет стиль текста | +| 14 | "h3" | Заголовок текста 3-го уровня. Изменяет стиль текста | +| 15 | "h4" | Заголовок текста 4-го уровня. Изменяет стиль текста | +| 16 | "h5" | Заголовок текста 5-го уровня. Изменяет стиль текста | +| 17 | "h6" | Заголовок текста 6-го уровня. Изменяет стиль текста | +| 18 | "blockquote" | Цитата. Изменяет стиль текста | +| 19 | "code" | Программный код. Изменяет стиль текста | + +### Свойства текста + +Все перечисленные в этом разделе свойства являются наследуемыми, т.е. свойство будет применяться не только ко View +для которого оно установлено, но и ко всем View вложенным в него. + +Имеются следующие свойства для настройки параметров отображения текста: + +#### Свойство "font-name" + +Свойство "font-name" (константа FontName) - текстовое свойство определяет имя используемого шрифта. +Может задаваться несколько шрифтов. В этом случае они разделяются пробелом. +Шрифты применяются в том порядке в котором они перечислены. Т.е. сначала +применяется первый, если он недоступен, то второй, третий и т.д. + +Получить значение данного свойства можно с помощью функции + + func GetFontName(view View, subviewID string) string + +#### Свойство "text-color" + +Свойство "text-color" (константа TextColor) - свойство типа Color определяет цвет текста. + +Получить значение данного свойства можно с помощью функции + + func GetTextColor(view View, subviewID string) Color + +#### Свойство "text-size" + +Свойство "text-size" (константа TextSize) - свойство типа SizeUnit определяет размер шрифта. + +Получить значение данного свойства можно с помощью функции + + func GetTextSize(view View, subviewID string) SizeUnit + +#### Свойство "italic" + +Свойство "italic" (константа Italic) - свойство типа bool. Если значение равно true, то к тексту применяется курсивное начертание + +Получить значение данного свойства можно с помощью функции + + func IsItalic(view View, subviewID string) bool + +#### Свойство "small-caps" + +Свойство "small-caps" (константа SmallCaps) - свойство типа bool. Если значение равно true, то к тексту применяется начертание капителью + +Получить значение данного свойства можно с помощью функции + + func IsSmallCaps(view View, subviewID string) bool + +#### Свойство "white-space" + +Свойство "white-space" (константа WhiteSpace) типа int управляет тем, как обрабатываются пробельные +символы внутри View. Свойство "white-space" может принимать следующие значения: + +0 (константа WhiteSpaceNormal, имя "normal") - последовательности пробелов объединяются в один пробел. +Символы новой строки в источнике обрабатываются, как отдельный пробел. Применение данного значения +при необходимости разбивает строки для того, чтобы заполнить строчные боксы. + +1 (константа WhiteSpaceNowrap, имя "nowrap") - объединяет последовательности пробелов в один пробел, +как значение normal, но не переносит строки (оборачивание текста) внутри текста. + +2 (константа WhiteSpacePre, имя "pre") - последовательности пробелов сохраняются так, как они указаны +в источнике. Строки переносятся только там, где в источнике указаны символы новой строки и там, +где в источнике указаны элементы "br". + +3 (константа WhiteSpacePreWrap, имя "pre-wrap") - последовательности пробелов сохраняются так, как они +указаны в источнике. Строки переносятся только там, где в источнике указаны символы новой строки и там, +где в источнике указаны элементы "br", и при необходимости для заполнения строчных боксов. + +4 (константа WhiteSpacePreLine, имя "pre-line") - последовательности пробелов объединяются в один пробел. +Строки разбиваются по символам новой строки, по элементам "br", и при необходимости для заполнения строчных боксов. + +5 (константа WhiteSpaceBreakSpaces, имя "break-spaces") - поведение идентично pre-wrap со следующими отличиями: +* Последовательности пробелов сохраняются так, как они указаны в источнике, включая пробелы на концах строк. +* Строки переносятся по любым пробелам, в том числе в середине последовательности пробелов. +* Пробелы занимают место и не висят на концах строк, а значит влияют на внутренние размеры (min-content и max-content). + +В приведённой ниже таблице указано поведение различных значений свойства "white-space" + +| | Новые строки | Пробелы и табуляция | Перенос по словам | Пробелы в конце строки | +|-----------------------|-----------------------------|-----------------------------|-------------------|-----------------------------| +| WhiteSpaceNormal | Объединяются в одну | Объединяются в один пробел | Переносится | Удаляются | +| WhiteSpaceNowrap | Объединяются в одну | Объединяются в один пробел | Не переносится | Удаляются | +| WhiteSpacePre | Сохраняются как в источнике | Сохраняются как в источнике | Не переносится | Сохраняются как в источнике | +| WhiteSpacePreWrap | Сохраняются как в источнике | Сохраняются как в источнике | Переносится | Висят | +| WhiteSpacePreLine | Сохраняются как в источнике | Объединяются в один пробел | Переносится | Удаляются | +| WhiteSpaceBreakSpaces | Сохраняются как в источнике | Сохраняются как в источнике | Переносится | Переносятся | + +#### Свойство "word-break" + +Свойство "word-break" (константа WordBreak) типа int определяет, где будет установлен перевод +на новую строку в случае превышения текстом границ блока. +Свойство "white-space" может принимать следующие значения: + +0 (константа WordBreak, имя "normal) - поведение по умолчанию для расстановки перевода строк. + +1 (константа WordBreakAll, имя "break-all) - при превышении границ блока, перевод строки будет +вставлен между любыми двумя символами (за исключением текста на китайском/японском/корейском языке). + +2 (константа WordBreakKeepAll, имя "keep-all) - перевод строки не будет использован в тексте на +китайском/японском/корейском языке. Для текста на других языках будет применено поведение по умолчанию (normal). + +3 (константа WordBreakWord, имя "break-word) - при превышении границ блока, остающиеся целыми слова +могут быть разбиты в произвольном месте, если не будет найдено более подходящее для переноса строки место. + +#### Свойства "strikethrough", "overline" и "underline" + +Данные свойства устанавливают декоративные линии на тексте: + +| Свойство | Константа | Тип декоративной линии | +|-----------------|----------------|-----------------------------| +| "strikethrough" | Strikethrough | Линия перечеркивающая текст | +| "overline" | Overline | Линия над текстом | +| "underline" | Underline | Линия под текстом | + +Получить значение данных свойств можно с помощью функций + + func IsStrikethrough(view View, subviewID string) bool + func IsOverline(view View, subviewID string) bool + func IsUnderline(view View, subviewID string) bool + +#### Свойство "text-line-thickness" + +Свойство "text-line-thickness" (константа TextLineThickness) - свойство типа SizeUnit. +Свойство устанавливает толщину декоративных линий на тексте заданных с помощью свойств "strikethrough", "overline" и "underline". + +Получить значение данного свойства можно с помощью функции + + GetTextLineThickness(view View, subviewID string) SizeUnit + +#### Свойство "text-line-style" + +Свойство "text-line-style" (константа TextLineStyle) - свойство типа int. +Свойство устанавливает стиль декоративных линий на тексте заданных с помощью свойств "strikethrough", "overline" и "underline". +Возможны следующие значения: + +| Значение | Константа | Имя | Описание | +|:--------:|------------|----------|--------------------------| +| 1 | SolidLine | "solid" | Сплошная линия | +| 2 | DashedLine | "dashed" | Пунктирная линия | +| 3 | DottedLine | "dotted" | Линия состоящая из точек | +| 4 | DoubleLine | "double" | Двойная сплошная линия | +| 5 | WavyLine | "wavy" | Волнистая линия | + +Если свойство не определено то используется сплошная линия (SolidLine (1)). + +Получить значение данного свойства можно с помощью функции + + func GetTextLineStyle(view View, subviewID string) int + +#### Свойство "text-line-color" + +Свойство "text-line-color" (константа TextLineColor) - свойство типа Color. +Свойство устанавливает цвет декоративных линий на тексте заданных с помощью свойств "strikethrough", "overline" и "underline". +Если свойство не определено то для линий используется цвет текста заданный с помощью свойства "text-color". + +Получить значение данного свойства можно с помощью функции + + func GetTextLineColor(view View, subviewID string) Color + +#### Свойство "text-weight" + +Свойство "text-weight" (константа TextWeight) - свойство типа int устанавливает начертание шрифта. Допустимые значения: + +| Значение | Константа | Общее название начертания | +|:--------:|----------------|-----------------------------------------------------------------| +| 1 | ThinFont | Тонкий (Волосяной) Thin (Hairline) | +| 2 | ExtraLightFont | Дополнительный светлый (Сверхсветлый) Extra Light (Ultra Light) | +| 3 | LightFont | Светлый Light | +| 4 | NormalFont | Нормальный Normal. Значение по умолчанию | +| 5 | MediumFont | Средний Medium | +| 6 | SemiBoldFont | Полужирный Semi Bold (Demi Bold) | +| 7 | BoldFont | Жирный Bold | +| 8 | ExtraBoldFont | Дополнительный жирный (Сверхжирный) Extra Bold (Ultra Bold) | +| 9 | BlackFont | Чёрный (Густой) Black (Heavy) | + +Некоторые шрифты доступны только в нормальном или полужирном начертании. В этом случае значение данного свойства игнорируется + +Получить значение данного свойства можно с помощью функции + + func GetTextWeight(view View, subviewID string) int + +#### Свойство "text-shadow" + +Свойство "text-shadow" позволяет задать тени для текста. Теней может быть несколько. Тень описывается +с помощью интерфейса ViewShadow (см. выше, раздел "Свойство 'shadow'"). Для тени текста используются только +свойства "color", "x-offset", "y-offset" и "blur". Свойства "inset" и "spread-radius" игнорируются (т.е. их +задание не является ошибкой, просто никакого влияния на тень текста они не имеют). + +Для создания ViewShadow для тени текста используются функции: + + func NewTextShadow(offsetX, offsetY, blurRadius SizeUnit, color Color) ViewShadow + func NewShadowWithParams(params Params) ViewShadow + +Функция NewShadowWithParams используется когда в качестве параметров необходимо использовать +константы. Например: + + shadow := NewShadowWithParams(rui.Params{ + rui.ColorProperty : "@shadowColor", + rui.BlurRadius : 8.0, + }) + +В качестве значения свойству "text-shadow" может быть присвоено ViewShadow, массив ViewShadow, +текстовое представление ViewShadow (см. выше, раздел "Свойство 'shadow'"). + +Получить значение данного свойства можно с помощью функции + + func GetTextShadows(view View, subviewID string) []ViewShadow + +Если тень не задана, то данная функция вернет пустой массив + +#### Свойство "text-align" + +Свойство "text-align" (константа TextAlign) - свойство типа int устанавливает выравнивание текста. Допустимые значения: + +| Значение | Константа | Имя | Значение | +|:--------:|--------------|-----------|------------------------------| +| 0 | LeftAlign | "left" | Выравнивание по левому краю | +| 1 | RightAlign | "right" | Выравнивание по правому краю | +| 2 | CenterAlign | "center" | Выравнивание по центру | +| 3 | JustifyAlign | "justify" | Выравнивание по ширине | + +Получить значение данного свойства можно с помощью функции + + func GetTextAlign(view View, subviewID string) int + +#### Свойство "text-indent" + +Свойство "text-indent" (TextIndent) - свойство типа SizeUnit определяет размер отступа (пустого места) перед первой строкой текста. + +Получить значение данного свойства можно с помощью функции + + func GetTextIndent(view View, subviewID string) SizeUnit + +#### Свойство "letter-spacing" + +Свойство "letter-spacing" (LetterSpacing) - свойство типа SizeUnit определяет межбуквенное расстояние в тексте. +Значение может быть отрицательным, но при этом могут быть ограничения, зависящие от конкретной реализации. +Агент пользователя может не увеличивать или уменьшать межбуквенное расстояние для выравнивания текста. + +Получить значение данного свойства можно с помощью функции + + func GetLetterSpacing(view View, subviewID string) SizeUnit + +#### Свойство "word-spacing" + +Свойство "word-spacing" (константа WordSpacing) - свойство типа SizeUnit определяет длину пробела между словами. +Если величина задана в процентах, то она определяет дополнительный интервал как процент от предварительной ширины символа. +В остальных случаях она определяет дополнительный интервал в дополнение к внутреннему интервалу между словами, определяемому шрифтом. + +Получить значение данного свойства можно с помощью функции + + func GetWordSpacing(view View, subviewID string) SizeUnit + +#### Свойство "line-height" + +Свойство "line-height" (константа LineHeight) - свойство типа SizeUnit устанавливает величину пространства между строками. + +Получить значение данного свойства можно с помощью функции + + func GetLineHeight(view View, subviewID string) SizeUnit + +#### Свойство "text-transform" + +Свойство "text-transform" (константа TextTransform) - свойство типа int определяет регистр символов. Допустимые значения: + +| Значение | Константа | Преобразование регистра | +|:--------:|-------------------------|-----------------------------------------| +| 0 | NoneTextTransform | Оригинальный регистр символов | +| 1 | CapitalizeTextTransform | Каждое слово начинается с большой буквы | +| 2 | LowerCaseTextTransform | Все символы строчные | +| 3 | UpperCaseTextTransform | Все символы заглавные | + +Получить значение данного свойства можно с помощью функции + + func GetTextTransform(view View, subviewID string) int + +#### Свойство "text-direction" + +Свойство "text-direction" (константа TextDirection) - свойство типа int определяет направление вывода текста. Допустимые значения: + +| Значение | Константа | Направление вывода текста | +|:--------:|-------------------------|--------------------------------------------------------------------------| +| 0 | SystemTextDirection | Системное направление. Определяется языком операционной системы. | +| 1 | LeftToRightDirection | Слева направо. Используется для английского и большинства других языков. | +| 2 | RightToLeftDirection | Справа налево. Используется для иврит, арабский и некоторых других. | + +Получить значение данного свойства можно с помощью функции + + func GetTextDirection(view View, subviewID string) int + +#### Свойство "writing-mode" + +Свойство "writing-mode" (константа WritingMode) - свойство типа int определяет как располагаются строки текста +вертикально или горизонтально, а также направление в котором выводятся строки. +Возможны следующие значения: + +| Значение | Константа | Значение | +|:--------:|-----------------------|--------------------------------------------------------------------| +| 0 | HorizontalTopToBottom | Горизонтальные строки выводятся сверху сниз. Значение по умолчанию | +| 1 | HorizontalBottomToTop | Горизонтальные строки выводятся снизу вверх. | +| 2 | VerticalRightToLeft | Вертикальные строки выводятся справа налево. | +| 3 | VerticalLeftToRight | Вертикальные строки выводятся слева направо. | + +Получить значение данного свойства можно с помощью функции + + func GetWritingMode(view View, subviewID string) int + +#### Свойство "vertical-text-orientation" + +Свойство "vertical-text-orientation" (константа VerticalTextOrientation) - свойство типа int используется, только +если "writing-mode" установлено в VerticalRightToLeft (2) или VerticalLeftToRight (3) и определяет положение +символов вертикальной строки. Возможны следующие значения: + +| Значение | Константа | Значение | +|:--------:|-------------------------|--------------------------------------------------------------------| +| 0 | MixedTextOrientation | Символы повернуты на 90 по часовой стрелке. Значение по умолчанию. | +| 1 | UprightTextOrientation | Символы расположены нормально (вертикально). | + +Получить значение данного свойства можно с помощью функции + + func GetVerticalTextOrientation(view View, subviewID string) int + +### Свойства трансформации + +Данные свойства используются для трансформации (наклон, масштабирование и т.п.) содержимого View. + +#### Свойство "perspective" + +Свойство "perspective" (константа Perspective) определяет расстояние между плоскостью z = 0 и пользователем +для того чтобы придать 3D-позиционируемому элементу эффект перспективы. Каждый трансформируемый элемент с z > 0 +станет больше, с z < 0 соответственно меньше. + +Элементы части которые находятся за пользователем, т.е. z-координата этих элементов больше чем значение свойства perspective, не отрисовываются. + +Точка схождения по умолчанию расположена в центре элемента, но её можно переместить используя свойства +"perspective-origin-x" и "perspective-origin-y". + +Получить значение данного свойства можно с помощью функции + + func GetPerspective(view View, subviewID string) SizeUnit + +#### Свойства "perspective-origin-x" и "perspective-origin-y" + +Свойства "perspective-origin-x" и "perspective-origin-y" (константы PerspectiveOriginX и PerspectiveOriginY) +типа SizeUnit определяют позицию, с которой смотрит зритель. Она используется свойством "perspective" как точка схода. + +По умолчанию свойства "perspective-origin-x" и "perspective-origin-y" имеют значение 50%, т.е. указывают на центр View. + +Получить значение данных свойств можно с помощью функции + + func GetPerspectiveOrigin(view View, subviewID string) (SizeUnit, SizeUnit) + +#### Свойство "backface-visibility" + +Свойство "backface-visibility" (константа BackfaceVisible) типа bool определяет, видно ли заднюю грань элемента, +когда он повёрнут к пользователю. + +Задняя поверхность элемента является зеркальным отражением его передней поверхности. Однако невидимая в 2D, +задняя грань может быть видимой, когда преобразование вызывает вращение элемента в 3D пространстве. +(Это свойство не влияет на 2D-преобразования, которые не имеют перспективы.) + +Получить значение данного свойства можно с помощью функции + + func GetBackfaceVisible(view View, subviewID string) bool + +#### Свойства "origin-x", "origin-y" и "origin-z" + +Свойства "origin-x", "origin-y" и "origin-z" (константа OriginX, OriginY и OriginZ) типа SizeUnit устанавливают +исходную точку для преобразований элемента. + +Исходная точка преобразования - это точка, вокруг которой происходит преобразование. Например, вращение. + +Свойство "origin-z" игнорируется если не установлено свойство "perspective". + +Получить значение данных свойств можно с помощью функции + + func GetOrigin(view View, subviewID string) (SizeUnit, SizeUnit, SizeUnit) + +#### Свойства "translate-x", "translate-y" и "translate-z" + +Свойства "translate-x", "translate-y" и "translate-z" (константа TranslateX, TranslateY и TranslateZ) типа SizeUnit +позволяют задать смещение содержимого View. + +Свойство "translate-z" игнорируется если не установлено свойство "perspective". + +Получить значение данных свойств можно с помощью функции + + func GetTranslate(view View, subviewID string) (SizeUnit, SizeUnit, SizeUnit) + +#### Свойства "scale-x", "scale-y" и "scale-z" + +Свойства "scale-x", "scale-y" и "scale-z" (константа ScaleX, ScaleY и ScaleZ) типа float64 устанавливает +коэффициент масштабирования соответственно по осям x, y и z. +Исходный масштаб равен 1. Значение от 0 до 1 используется для уменьшения. Больше 1 - для увеличения. +Значения меньше или равное 0 являются недопустимыми (функция Set будет возвращать значение false) + +Свойство "scale-z" игнорируется если не установлено свойство "perspective". + +Получить значение данных свойств можно с помощью функции + + func GetScale(view View, subviewID string) (float64, float64, float64) + +#### Свойства "rotate" + +Свойство "rotate" (константа Rotate) типа AngleUnit задает угол поворота содержимого вокруг +вектора задаваемого свойствами "rotate-x", "rotate-y" и "rotate-z". + +#### Свойства "rotate-x", "rotate-y" и "rotate-z" + +Свойства "rotate-x", "rotate-y" и "rotate-z" (константа RotateX, RotateY и RotateZ) типа float64 +задают вектор вокруг которого осуществляется вращение на угол заданный свойством "rotate". +Данный вектор проходит через точку заданную свойствами "origin-x", "origin-y" и "origin-z" + +Свойство "rotate-z" игнорируется если не установлено свойство "perspective". + +Получить значение данных свойств, а также свойства "rotate" можно с помощью функции + + func GetRotate(view View, subviewID string) (float64, float64, float64, AngleUnit) + +#### Свойства "skew-x" и "skew-y" + +Свойства "skew-x" и "skew-y" (константа SkewX и SkewY) типа AngleUnit задают скос (наклон) содержимого, +превращая тем самым его из прямоугольника в параллелограмм. Скос осуществляется вокруг точки, +задаваемой свойствами transform-origin-x и transform-origin-y. + +Получить значение данных свойств можно с помощью функции + + func GetSkew(view View, subviewID string) (AngleUnit, AngleUnit) + +### События клавиатуры + +Для View получившего фокус ввода могут генерироваться два вида событий клавиатуры + +| Событие | Константа | Описание | +|------------------|--------------|------------------------| +| "key-down-event" | KeyDownEvent | Клавиша была нажата. | +| "key-up-event" | KeyUpEvent | Клавиша была отпущена. | + +Основной слушатель данных событий имеет следующий формат: + + func(View, KeyEvent) + +где второй аргумент описывает параметры нажатых клавиш. Структура KeyEvent имеет следующие поля: + +| Поле | Тип | Описание | +|-----------|--------|----------------------------------------------------------------------------------------------------------------------| +| TimeStamp | uint64 | Время, когда событие было создано (в миллисекундах). Точка отсчета зависит от реализации браузера (ЭПОХА, запуск браузера и т.п.). | +| Key | string | Значение клавиши, на которой возникло событие. Значение выдается с учетом текущего языка и регистра. | +| Code | string | Код клавиши, представленного события. Значение не зависит от текущего языка и регистра. | +| Repeat | bool | Повторное нажатие: клавиша была нажата до тех пор, пока её ввод не начал автоматически повторяться. | +| CtrlKey | bool | Клавиша Ctrl была активна, когда возникло событие. | +| ShiftKey | bool | Клавиша Shift была активна, когда возникло событие. | +| AltKey | bool | Kлавиша Alt ( Option или ⌥ в OS X) была активна, когда возникло событие. | +| MetaKey | bool | Kлавиша Meta (для Mac это клавиша ⌘ Command; для Windows - клавиша "Windows" ⊞) была активна, когда возникло событие.| + +Можно также использовать слушателей следующих форматов: + +* func(KeyEvent) +* func(View) +* func() + +Получить списки слушателей событий клавиатуры можно с помощью функций: + + func GetKeyDownListeners(view View, subviewID string) []func(View, KeyEvent) + func GetKeyUpListeners(view View, subviewID string) []func(View, KeyEvent) + +### События фокуса + +События фокуса возникают когда View получает или теряет фокус ввода. Соответственно возможны два событий: + +| Событие | Константа | Описание | +|--------------------|----------------|-------------------------------------------------| +| "focus-event" | FocusEvent | View получает фокус ввода (становится активным) | +| "lost-focus-event" | LostFocusEvent | View теряет фокус ввода (становится неактивным) | + +Основной слушатель данных событий имеет следующий формат: + + func(View). + +Можно также использовать слушателя следующего формата: + + func() + +Получить списки слушателей событий фокуса можно с помощью функций: + + func GetFocusListeners(view View, subviewID string) []func(View) + func GetLostFocusListeners(view View, subviewID string) []func(View) + +### События мыши + +Для View могут генерироваться несколько вида событий мыши + +| Событие | Константа | Описание | +|----------------------|------------------|------------------------| +| "mouse-down" | MouseDown | Клавиша мыши была нажата. | +| "mouse-up" | MouseUp | Клавиша мыши была отпущена. | +| "mouse-move" | MouseMove | Переместился курсор мыши | +| "mouse-out" | MouseOut | Курсор мыши вышел за пределы View, или зашел в дочерной View | +| "mouse-over" | MouseOver | Курсор мыши зашел в пределы View | +| "click-event" | ClickEvent | Произошел клик мышкой | +| "double-click-event" | DoubleClickEvent | Произошел двойной клик мышкой | +| "context-menu-event" | ContextMenuEvent | Нажета клавиша вызова контекстного меню (правая кнопка мыши) | + +Основной слушатель данных событий имеет следующий формат: + + func(View, MouseEvent) + +где второй аргумент описывает параметры нажатых клавиш. Структура MouseEvent имеет следующие поля: + +| Поле | Тип | Описание | +|-----------|---------|----------------------------------------------------------------------------------------------------------------------| +| TimeStamp | uint64 | Время, когда событие было создано (в миллисекундах). Точка отсчета зависит от реализации браузера (ЭПОХА, запуск браузера и т.п.). | +| Button | int | Номер кнопки мыши, нажатие на которую инициировало событие | +| Buttons | int | Битовая маска, показывающия какие кнопки мыши были нажаты в момент возникновения события | +| X | float64 | Горизонтальная позиция мыши относительно начала координат View | +| Y | float64 | Вертикальная позиция мыши относительно начала координат View | +| ClientX | float64 | Горизонтальная позиция мыши относительно левого верхнего угда приложения | +| ClientY | float64 | Вертикальная позиция мыши относительно левого верхнего угда приложения | +| ScreenX | float64 | Горизонтальная позиция мыши относительно левого верхнего угда экрана | +| ScreenY | float64 | Вертикальная позиция мыши относительно левого верхнего угда экрана | +| CtrlKey | bool | Клавиша Ctrl была активна, когда возникло событие. | +| ShiftKey | bool | Клавиша Shift была активна, когда возникло событие. | +| AltKey | bool | Kлавиша Alt ( Option или ⌥ в OS X) была активна, когда возникло событие. | +| MetaKey | bool | Kлавиша Meta (для Mac это клавиша ⌘ Command; для Windows - клавиша "Windows" ⊞) была активна, когда возникло событие.| + +Поле Button может принимать следующие значения + +| Значение | Константа | Описание | +|:--------:|----------------------|----------------------------------------------------------------------------------| +| <0 | | Не нажата ни одна кнопка | +| 0 | PrimaryMouseButton | Основная кнопка. Обычно левая кнопка мыши (может быть изменена в настройках ОС) | +| 1 | AuxiliaryMouseButton | Вспомогательная кнопка. Колёсико или средняя кнопка мыши, если она есть | +| 2 | SecondaryMouseButton | Вторичная кнопка. Обычно правая кнопка мыши (может быть изменена в настройках ОС)| +| 3 | MouseButton4 | Четвёртая кнопка мыши. Обычно кнопка браузера Назад | +| 4 | MouseButton5 | Пятая кнопка мыши. Обычно кнопка браузера Вперёд | + +Поле Button преставляет собой битовую маску объединающую (с помощью ИЛИ) следующие значения + +| Значение | Константа | Описание | +|:--------:|--------------------|------------------------| +| 1 | PrimaryMouseMask | Основная кнопка | +| 2 | SecondaryMouseMask | Вторичная кнопка | +| 4 | AuxiliaryMouseMask | Вспомогательная кнопка | +| 8 | MouseMask4 | Четвёртая кнопка | +| 16 | MouseMask5 | Пятая кнопка | + +Можно также использовать слушателей следующих форматов: + +* func(MouseEvent) +* func(View) +* func() + +Получить списки слушателей событий мыши можно с помощью функций: + + func GetMouseDownListeners(view View, subviewID string) []func(View, MouseEvent) + func GetMouseUpListeners(view View, subviewID string) []func(View, MouseEvent) + func GetMouseMoveListeners(view View, subviewID string) []func(View, MouseEvent) + func GetMouseOverListeners(view View, subviewID string) []func(View, MouseEvent) + func GetMouseOutListeners(view View, subviewID string) []func(View, MouseEvent) + func GetClickListeners(view View, subviewID string) []func(View, MouseEvent) + func GetDoubleClickListeners(view View, subviewID string) []func(View, MouseEvent) + func GetContextMenuListeners(view View, subviewID string) []func(View, MouseEvent) + +## События указателя + +Указатель - это аппаратно-независимое представление устройств ввода (таких как мышь, перо +или точка контакта на сенсорной поверхности). Указатель может указывать на конкретную координату +(или набор координат) на контактной поверхности, например на экране. + +Все указатели могут генерироваться несколько вида событий + +| Событие | Константа | Описание | +|------------------|---------------|------------------------------------------------------------| +| "pointer-down" | PointerDown | Указатель был нажат. | +| "pointer-up" | PointerUp | Указатель был отпущен. | +| "pointer-move" | PointerMove | Указатель перемещен | +| "pointer-cancel" | PointerCancel | События указателя прерваны. | +| "pointer-out" | PointerOut | Указатель вышел за пределы View, или зашел в дочерной View | +| "pointer-over" | PointerOver | Указатель зашел в пределы View | + +Основной слушатель данных событий имеет следующий формат: + + func(View, PointerEvent) + +где второй аргумент описывает параметры указателя. Структура PointerEvent расширяет структуру MouseEvent +и имеет следующие дополнительные поля: + +| Поле | Тип | Описание | +|--------------------|---------|-----------------------------------------------------------------------| +| PointerID | int | Уникальный идентификатор указателя, вызвавшего событие. | +| Width | float64 | Ширина (величина по оси X) в пикселях контактной геометрии указателя. | +| Height | float64 | Высота (величина по оси Y) в пикселях контактной геометрии указателя. | +| Pressure | float64 | Нормализованное давление на входе указателя в диапазоне от 0 до 1, где 0 и 1 представляют минимальное и максимальное давление, которое аппаратное обеспечение способно обнаруживать, соответственно.| +| TangentialPressure | float64 | Нормализованное тангенциальное давление на входе указателя (также известное как давление в цилиндре или напряжение цилиндра) в диапазоне от -1 до 1, где 0 - нейтральное положение элемента управления.| +| TiltX | float64 | Плоский угол (в градусах в диапазоне от -90 до 90) между плоскостью Y–Z и плоскостью, содержащей как ось указателя (например, стилуса), так и ось Y.| +| TiltY | float64 | Плоский угол (в градусах в диапазоне от -90 до 90) между плоскостью X–Z и плоскостью, содержащей как ось указателя (например, стилуса), так и ось X.| +| Twist | float64 | Вращение указателя (например, стилуса) по часовой стрелке вокруг своей главной оси в градусах со значением в диапазоне от 0 до 359.| +| PointerType | string | тип устройства, вызвавшего событие: "mouse", "pen", "touch" и т.п. | +| IsPrimary | bool | указатель является первичным указателем этого типа. | + +Можно также использовать слушателей следующих форматов: + +* func(PointerEvent) +* func(View) +* func() + +Получить списки слушателей событий указателя можно с помощью функций: + + func GetPointerDownListeners(view View, subviewID string) []func(View, PointerEvent) + func GetPointerUpListeners(view View, subviewID string) []func(View, PointerEvent) + func GetPointerMoveListeners(view View, subviewID string) []func(View, PointerEvent) + func GetPointerCancelListeners(view View, subviewID string) []func(View, PointerEvent) + func GetPointerOverListeners(view View, subviewID string) []func(View, PointerEvent) + func GetPointerOutListeners(view View, subviewID string) []func(View, PointerEvent) + +### Touch события + +Данные события используются для отслеживания многоточечных касаний. Одиночные касания эмулируют события мыши. +Если у вас нет необходимости отслеживания многоточечных касаний, то проще использовать события мыши + +| Событие | Константа | Описание | +|----------------|-------------|-----------------------------------------------| +| "touch-start" | TouchStart | Произошло касание поверхности. | +| "touch-end" | TouchEnd | Завершено касание поверхности. | +| "touch-move" | TouchMove | Одно или несколько касаний изменили положение | +| "touch-cancel" | TouchCancel | Касание прервано. | + +Основной слушатель данных событий имеет следующий формат: + + func(View, TouchEvent) + +где второй аргумент описывает параметры касаний. Структура TouchEvent имеет следующие поля: + +| Поле | Тип | Описание | +|-----------|---------|----------------------------------------------------------------------------------------------------------------------| +| TimeStamp | uint64 | Время, когда событие было создано (в миллисекундах). Точка отсчета зависит от реализации браузера (ЭПОХА, запуск браузера и т.п.). | +| Touches | []Touch | Массив структур Touch, каждая из которых описывает одно касание | +| CtrlKey | bool | Клавиша Ctrl была активна, когда возникло событие. | +| ShiftKey | bool | Клавиша Shift была активна, когда возникло событие. | +| AltKey | bool | Kлавиша Alt ( Option или ⌥ в OS X) была активна, когда возникло событие. | +| MetaKey | bool | Kлавиша Meta (для Mac это клавиша ⌘ Command; для Windows - клавиша "Windows" ⊞) была активна, когда возникло событие.| + +Структура Touch описывает одиночное касание и имеет следующие поля + +| Поле | Тип | Описание | +|---------------|---------|--------------------------------------------------------------------------------------------------------------------------| +| Identifier | int | Уникальный идентификатор присваюваемый каждому касанию и не меняющийся до его завершения. | +| X | float64 | Горизонтальная позиция мыши относительно начала координат View | +| Y | float64 | Вертикальная позиция мыши относительно начала координат View | +| ClientX | float64 | Горизонтальная позиция мыши относительно левого верхнего угда приложения | +| ClientY | float64 | Вертикальная позиция мыши относительно левого верхнего угда приложения | +| ScreenX | float64 | Горизонтальная позиция мыши относительно левого верхнего угда экрана | +| ScreenY | float64 | Вертикальная позиция мыши относительно левого верхнего угда экрана | +| RadiusX | float64 | x-радиус эллипса в пикселях, который наиболее точно ограничивает область контакта с экраном. | +| RadiusY | float64 | y-радиус эллипса в пикселях, который наиболее точно ограничивает область контакта с экраном. | +| RotationAngle | float64 | Угол (в градусах), на который нужно повернуть по часовой стрелке эллипс, описываемый параметрами radiusX и radiusY, чтобы наиболее точно покрыть область контакта между пользователем и поверхностью. | +| Force | float64 | Величина давления от 0,0 (без давления) до 1,0 (максимальное давление), которое пользователь прикладывает к поверхности. | + +Можно также использовать слушателей следующих форматов: + +* func(TouchEvent) +* func(View) +* func() + +Получить списки слушателей событий касания можно с помощью функций: + + func GetTouchStartListeners(view View, subviewID string) []func(View, TouchEvent) + func GetTouchEndListeners(view View, subviewID string) []func(View, TouchEvent) + func GetTouchMoveListeners(view View, subviewID string) []func(View, TouchEvent) + func GetTouchCancelListeners(view View, subviewID string) []func(View, TouchEvent) + +### Событие "resize-event" + +Событие "resize-event" (константа ResizeEvent) вызывается когда View меняет свои положение и/или размеры. +Основной слушатель данных событий имеет следующий формат: + + func(View, Frame) + +где структура объявлена как + + type Frame struct { + Left, Top, Width, Height float64 + } + +Соотвественно элементы Frame содержат следующие данные +* Left - новое смещение в пикселях по горизонтали относительно родительского View (левая позиция); +* Top - новое смещение в пикселях по вертикали относительно родительского View (верхняя позиция) +* Width - новая ширина видимой части View в пикселях; +* Height - новая высота видимой части View в пикселях. + +Можно также использовать слушателей следующих форматов: + +* func(Frame) +* func(View) +* func() + +Получить список слушателей данного события можно с помощью функции: + + func GetResizeListeners(view View, subviewID string) []func(View, Frame) + +Текущие положение и размеры видимой части View можно получить с помощью функции интерфейса View: + + Frame() Frame + +или глобальной функции + + func GetViewFrame(view View, subviewID string) Frame + +### Событие прокрутки + +Событие "scroll-event" (константа ScrollEvent) созникает при прокрутке содержимого View. +Основной слушатель данных событий имеет следующий формат: + + func(View, Frame) + +где элементы Frame содержат следующие данные +* Left - новое смещение видимой области по горизонтали (в пикселях); +* Top - новое смещение видимой области по вертикали (в пикселях); +* Width - общая ширина View в пикселях; +* Height - общая высота View в пикселях. + +Можно также использовать слушателей следующих форматов: + +* func(Frame) +* func(View) +* func() + +Получить список слушателей данного события можно с помощью функции: + + func GetScrollListeners(view View) []func(View, Frame) + +Текущие положение видимой области и общие размеры View можно получить с помощью функции интерфейса View: + + Scroll() Frame + +или глобальной функции + + func GetViewScroll(view View, subviewID string) Frame + +Для программной прокрутки могут использоваться следующие глобальные функции + + func ScrollViewTo(view View, subviewID string, x, y float64) + func ScrollViewToStart(view View, subviewID string) + func ScrollViewToEnd(view View, subviewID string) + +которые прокручивают view, соответственно, в заданную позицию, начало и конец + +## ViewsContainer + +Интерфейс ViewsContainer, реализующий View, описывает контейнер содержащий несколько +дочерних элементов интерфейса (View). ViewsContainer является базовым для других контейнеров +(ListLayout, GridLayout, StackLayout и т.д.) и самостоятельно не используется. + +Помимо всех свойств View данный элемент имеет всего одно дополнительное свойство "content" + +### "content" + +Свойство "content" (константа Сontent) определяет массив дочерних View. Функция Get интерфейса +для данного свойства всегда возвращает []View. + +В качестве значения свойства "content" могут быть переданы следующие 5 типов данных: + +* View - преобразуется во []View, содержащий один элемент; + +* []View - nil-элементы запрещены, если массив будет содержать nil, то свойство не будет +установлено, а функция Set вернет false и в лог запишется сообщение об ошибке; + +* string - если строка является текстовым представление View, то создается соответствующий View, +иначе создается TextView, которому в качестве текста передается данная строка. +Далее создается []View, содержащий полученный View; + +* []string - каждый элемент массива преобразуется во View как описано в предыдущем пункте; + +* []interface{} - данный массив должен содержать только View и string. Каждый string-элемент +преобразуется во View, как описано выше. Если массив будет содержать недопустимае значения, +то свойство "content" не будет установлено, а функция Set вернет false и в лог запишется сообщение об ошибке. + +Поучить значение свойства "content" можно с помощи функции интерфейса ViewsContainer + + Views() []View + +Для редактирования свойства "content" можно использовать следующие функции интерфейса ViewsContainer: + + Append(view View) + +Данная функция добавляет аргумент в конец списка View + + Insert(view View, index uint) + +Данная функция вставляет аргумент в заданную позицию списка View. Если index больше длины +списка, то View добавляется в конец списка. Если index меньше 0, то в начало списка. + + RemoveView(index uint) View + +Данная функция удаляет View из заданной позиции и возвращает его. Если index указывает за +границы списка, то ничего не удаляется, а функция возвращает nil. + +## ListLayout + +ListLayout является контейнером, реализующим интерфейс ViewsContainer. Для его создания используется функция + + func NewListLayout(session Session, params Params) ListLayout + +Элементы в данном контейнере располагаются в виде списка. Расположением дочерних элементов можно управлять. +Для этого ListLayout имеет ряд свойств + +### "orientation" + +Свойство "orientation" (константа Orientation) типа int задает то как дочерние элементы будут +располагаться друг относительно друга. Свойство может принимать следующие значения: + +| Значение | Константа | Расположение | +|:--------:|-----------------------|------------------------------------------------------------| +| 0 | TopDownOrientation | Дочерние элементы располагаются в столбец сверху вниз. | +| 1 | StartToEndOrientation | Дочерние элементы располагаются в строку с начала в конец. | +| 2 | BottomUpOrientation | Дочерние элементы располагаются в столбец снизу вверх. | +| 3 | EndToStartOrientation | Дочерние элементы располагаются в строку с конца в начала. | + +Положение начала и конца для StartToEndOrientation и EndToStartOrientation зависит от значения +свойства "text-direction". Для языков с письмом справа налево (арабский, иврит) начало находится +справа, для остальных языков - слева. + +### "wrap" + +Свойство "wrap" (константа Wrap) типа int определяет расположения элементов в случае достижения +границы контейнера. Возможны три варианта: + +* WrapOff (0) - колонка/строка элементов продолжается и выходит за границы видимой области. + +* WrapOn (1) - начинается новая колонка/строка элементов. Новая колонка располагается по направлению +к концу (о положении начала и конца см. выше), новая строка - снизу. + +* WrapReverse (2) - начинается новая колонка/строка элементов. Новая колонка располагается по направлению +к началу (о положении начала и конца см. выше), новая строка - сверху. + +### "vertical-align" + +Свойство "vertical-align" (константа VerticalAlign) типа int устанавливает вертикальное +выравнивание элементов в контейнере. Допустимые значения: + +| Значение | Константа | Имя | Значение | +|:--------:|--------------|-----------|-------------------------------| +| 0 | TopAlign | "top" | Выравнивание по верхнему краю | +| 1 | BottomAlign | "bottom" | Выравнивание по нижнему краю | +| 2 | CenterAlign | "center" | Выравнивание по центру | +| 3 | StretchAlign | "stretch" | Выравнивание по высоте | + +### "horizontal-align" + +Свойство "horizontal-align" (константа HorizontalAlign) типа int устанавливет +горизонтальное выравнивание элементов в списке. Допустимые значения: + +| Значение | Константа | Имя | Значение | +|:--------:|--------------|-----------|------------------------------| +| 0 | LeftAlign | "left" | Выравнивание по левому краю | +| 1 | RightAlign | "right" | Выравнивание по правому краю | +| 2 | CenterAlign | "center" | Выравнивание по центру | +| 3 | StretchAlign | "stretch" | Выравнивание по ширине | + +## GridLayout + +GridLayout является контейнером, реализующим интерфейс ViewsContainer. Для его создания используется функция + + func NewGridLayout(session Session, params Params) GridLayout + +Пространство контейнера данного контейнера разбито на ячейки в виде таблицы. +Все дочерние элементы располагаются в ячейках таблицы. Ячейка адресуется по номеру строки и +столбца. Номера строк и столбцов начинаются с 0. + +### "column" и "row" + +Расположение View внутри GridLayout определяется с помощью свойств "column" и "row". +Данные свойства должны устанавливаться для каждого из дочерних View. +Дочерний View может занимать несколько ячеек внутри GridLayout. При этом они могут +перекрываться. + +В качестве значения "column" и "row" можно установить: +* целое число большее или равное 0; +* текстовое представление целого числа большего или равного 0 или константу; +* структуру Range, задающую диапазон строк/столбцов: + + type Range struct { + First, Last int + } + +где First - номер первого столбца/строки, Last - номер последнего столбца/строки; +* строка вида "<номер первого столбца/строки>:<номер последнего столбца/строки>", являющуюся +текстовым представление структуры Range + +Пример + + grid := rui.NewGridLayout(session, rui.Params { + rui.Content : []View{ + NewView(session, rui.Params { + rui.ID : "view1", + rui.Row : 0, + rui.Column : rui.Range{ First: 1, Last: 2 }, + }), + NewView(session, rui.Params { + rui.ID : "view2", + rui.Row : "0:2", + rui.Column : "0", + }), + }, + }) + +В данном примере view1 занимает в нулевой строке столбцы 1 и 2, а view1 занимает +в нулевом стобце строки 0, 1 и 2. + +### "cell-width" и "cell-height" + +По умолчанию размеры ячеек вычисляются на основе размеров помещенных в них дочерних View. +Свойства "cell-width" и "cell-height" (константы CellWidth и CellHeight) позволяют установить +фиксированную ширину и высоту ячеек независимо от размеров дочерних элементов. +Данные свойства имеют тип []SizeUnit. Каждый элемент массива определяет размер соответствующего +столбца или строки. + +Данным свойствам могут быть присвоены следующие типы данных: + +* SizeUnit или текстовое представление SizeUnit (или SizeUnit константа). В этом случае +соотвествующие размеры всех ячеек устанавливаются одинаковыми; + +* []SizeUnit; + +* string содержащая текстовые представления SizeUnit (или SizeUnit константы) разделенные запятой; + +* []string. Каждый элемент должен быть текстовым представлением SizeUnit (или SizeUnit константой) + +* []interface{}. Каждый элемент должен или иметь тип SizeUnit или быть текстовым +представлением SizeUnit (или SizeUnit константой) + +Если количество элементов в свойствах "cell-width" и "cell-height" меньше, чем используемое число +столбцов и строк, то недостающие элементы устанавливаются в Auto. + +В значениях свойств "cell-width" и "cell-height" может использоваться SizeUnit тип SizeInFraction. +Этот тип означает 1 часть. Часть вычисляется так: из размера контейнера вычитается размер всех +ячеек имеющих тип не SizeInFraction, а затем оставшийся размер делится на количество частей. +Значение SizeUnit типа SizeInFraction может быть как целым, так и дробным. + +### "grid-row-gap" и "grid-column-gap" + +Свойства "grid-row-gap" и "grid-column-gap" (константы GridRowGap и GridColumnGap) типа SizeUnit позволяют +установить соответственно расстояния между строками и столбцами контейнера. Значение по умолчанию 0. + +### "cell-vertical-align" + +Свойство "cell-vertical-align" (константа CellVerticalAlign) типа int устанавливает вертикальное +выравнивание дочерних элементов внутри занимаемой ячейки. Допустимые значения: + +| Значение | Константа | Имя | Значение | +|:--------:|--------------|-----------|-------------------------------| +| 0 | TopAlign | "top" | Выравнивание по верхнему краю | +| 1 | BottomAlign | "bottom" | Выравнивание по нижнему краю | +| 2 | CenterAlign | "center" | Выравнивание по центру | +| 3 | StretchAlign | "stretch" | Растягивание на всю высоту | + +Значение по умолчанию StretchAlign (3) + +### "cell-horizontal-align" + +Свойство "cell-horizontal-align" (константа CellHorizontalAlign) типа int устанавливет +горизонтальное выравнивание дочерних элементов внутри занимаемой ячейки. Допустимые значения: + +| Значение | Константа | Имя | Значение | +|:--------:|--------------|-----------|------------------------------| +| 0 | LeftAlign | "left" | Выравнивание по левому краю | +| 1 | RightAlign | "right" | Выравнивание по правому краю | +| 2 | CenterAlign | "center" | Выравнивание по центру | +| 3 | StretchAlign | "stretch" | Растягивание на всю ширину | + +Значение по умолчанию StretchAlign (3) + +## ColumnLayout + +ColumnLayout является контейнером, реализующим интерфейс ViewsContainer. Все дочерние View +располагаются в виде вертикального списка выровненные по левому или правому краю и разбитого +на несколько колонок. Выравнивание зависит от свойства "text-direction". + +Для создания ColumnLayout используется функция + + func NewColumnLayout(session Session, params Params) ColumnLayout + +### Свойство "column-count" + +Свойство "column-count" (константа ColumnCount) типа int устанавливет количество колонок. + +Если данное свойство равно 0 и не задано свойство "column-width", то разбитие на колонки +не выполняется, а контейнер прокручивается вниз. + +Если значение данного свойства больше 0, то список разбивается на колонки. Высота колонки +равна высоте ColumnLayout, а ширина вычисляется как ширина ColumnLayout делённая на +"column-count". Каждая следующая колонка располагается в зависимости от свойства +"text-direction" справа или слева от предыдущей, а контейнер прокручивается по горизонтали. + +Получить значение данного свойства можно с помощью функции + + func GetColumnCount(view View, subviewID string) int + +### Свойство "column-width" + +Свойство "column-width" (константа ColumnWidth) типа SizeUnit используется только если +"column-count" равно 0 и устанавливет ширину колонки. + +ВАЖНО! В качестве значения "column-width" нельзя использовать проценты (т.е. если вы зададите +значение в процентах, то это проигнорируется системой) + +Получить значение данного свойства можно с помощью функции + + func GetColumnWidth(view View, subviewID string) SizeUnit + +### Свойство "column-gap" + +Свойство "column-gap" (константа ColumnGap) типа SizeUnit устанавливает ширину разрыва между колонками. + +Получить значение данного свойства можно с помощью функции + + func GetColumnGap(view View, subviewID string) SizeUnit + +### Свойство "column-separator" + +Свойство "column-separator" (константа ColumnSeparator) позволяет задать линию которая будет +рисоваться в разрывах колонок. Линия рамки описывается тремя атрибутами: стиль линии, толщина и цвет. + +Значение свойства "column-separator" хранится в виде интерфейса ColumnSeparatorProperty, +реализующего интерфейс Properties (см. выше). ColumnSeparatorProperty может содержать следующие свойства: + +| Свойство | Константа | Тип | Описание | +|----------|---------------|----------|------------------| +| "style" | Style | int | Стиль линии | +| "width" | Width | SizeUnit | Толщина линии | +| "color" | ColorProperty | Color | Цвет линии | + +Стиль линии может принимать следующие значения: + +| Значение | Константа | Имя | Описание | +|:--------:|------------|----------|--------------------------| +| 0 | NoneLine | "none" | Нет рамки | +| 1 | SolidLine | "solid" | Сплошная линия | +| 2 | DashedLine | "dashed" | Пунктирная линия | +| 3 | DottedLine | "dotted" | Линия состоящая из точек | +| 4 | DoubleLine | "double" | Двойная сплошная линия | + +Все другие значения стиля игнорируются. + +Для создания интерфейса ColumnSeparatorProperty используется функция + + func NewColumnSeparator(params Params) ColumnSeparatorProperty + +Интерфейс ColumnSeparatorProperty может быть преобразован в структуру ViewBorder с помощью +функции ViewBorder. При преобразовании все текстовые константы заменяются реальными значениями. +ViewBorder описана как + + type ViewBorder struct { + Style int + Color Color + Width SizeUnit + } + +Структура ViewBorder может быть передана в качестве параметра функции Set при установке значения +свойства "column-separator". При этом ViewBorder преобразуется в ColumnSeparatorProperty. +Поэтому при чтении свойства функцией Get будет возвращен интерфейс ColumnSeparatorProperty, +а не структура ViewBorder. Получить структуру ViewBorders без дополнительных преобразований можно +с помощью глобальной функции + + func GetColumnSeparator(view View, subviewID string) ViewBorder + +Вы также можете устанавливать отдельные атрибуты линии использую функцию Set интерфейса View. +Для этого используются следующие свойства + +| Свойство | Константа | Тип | Описание | +|--------------------------|----------------------|----------|---------------| +| "column-separator-style" | ColumnSeparatorStyle | int | Стиль линии | +| "column-separator-width" | ColumnSeparatorWidth | SizeUnit | Толщина линии | +| "column-separator-color" | ColumnSeparatorColor | Color | Цвет линии | + +Например + + view.Set(rui.ColumnSeparatorStyle, rui.SolidBorder) + view.Set(rui.ColumnSeparatorWidth, rui.Px(1)) + view.Set(rui.ColumnSeparatorColor, rui.Black) + +эквивалентно + + view.Set(rui.ColumnSeparator, ColumnSeparatorProperty(rui.Params{ + rui.Style: rui.SolidBorder, + rui.Width: rui.Px(1), + rui.ColorProperty: rui.Black, + })) + +### Свойство "avoid-break" + +При формировании колонок ColumnLayout может разрывать некоторые типы View, так что начало +будет в конце одной колонки, а окончание в следующей. Например, разрывается TextView, +заголовок картинки и сама картинки и т.д. + +Свойство "avoid-break" (константа AvoidBreak) типа bool позволяет избежать этого эффекта. +Необходимо установить для View, который нельзя разрывать, данное свойство со значением "true". +Соответственно значение "false" данного свойства позволяет разрывать View. +Значение по умолчанию "false". + +Получить значение данного свойства можно с помощью функции + + func GetAvoidBreak(view View, subviewID string) bool + +## StackLayout + +StackLayout является контейнером, реализующим интерфейс ViewsContainer. Все дочерние View +располагаются друг над другом и каждый занимает все пространство контейнера. В каждый момент времени +доступен только один дочерний View (текущий). + +Для создания StackLayout используется функция + + func NewStackLayout(session Session, params Params) StackLayout + +Помимо свойств Append, Insert, RemoveView и свойства "content" интерфейса ViewsContainer +контейнер StackLayout имеет еще две функции интерфейса для управления дочерними View: Push и Pop + + Push(view View, animation int, onPushFinished func()) + +Данная функция добавляет новый View в контейнер и делает его текущим. Она похожа на Append, +но в отличие от нее дабавление выполняется с использованием эффекта анимации. Вид анимации +задается вторым аргументом и может принимать следующие значения: + +| Значение | Константа | Анимация | +|:--------:|---------------------|-----------------------------| +| 0 | DefaultAnimation | Анимация по умолчанию. Для функции Push это EndToStartAnimation, для Pop - StartToEndAnimation | +| 1 | StartToEndAnimation | Анимация из начала в конец. Начало и конец определются направлением вывода текста | +| 2 | EndToStartAnimation | Анимация из конца в начало. | +| 3 | TopDownAnimation | Анимация сверху вниз. | +| 4 | BottomUpAnimation | Анимация снизу вверх. | + +Третий аргумент onPushFinished - функция вызываемая по окончании анимации. Может быть nil. + + Pop(animation int, onPopFinished func(View)) bool + +Данная функция удаляет текущий View из контейнера используя анимацию. +Второй аргумент onPopFinished - функция вызываемая по окончании анимации. Может быть nil. +Функция вернёт false если StackLayout пуст и true если текущий элемени был удален. + + Получить текущий (видимый) View можно с помощью функции интерфейса + + Peek() View + +Для того чтобы сделать любой дочерний View текущим (видимым) используются функции интерфейса: + + MoveToFront(view View) bool + MoveToFrontByID(viewID string) bool + +Данная функция вернет true в случае успеха и false если дочерний View или View с таким id не существует и в +лог будет записано сообщение об ошибке. + +## AbsoluteLayout + +AbsoluteLayout является контейнером, реализующим интерфейс ViewsContainer. Дочерние View +могут располагатся в произвольных позициях пространства контейнера. + +Для создания AbsoluteLayout используется функция + + func NewAbsoluteLayout(session Session, params Params) AbsoluteLayout + +Дочерние View позиционируюся с помощью свойств типа SizeUnit: "left", "right", "top" и +"bottom" (соответственно константы Left, Right, Top и Bottom). Можно задавать любые из +этих свойств для дочернего View. Если ни "left" ни "right" не заданы, то дочерний View +будет прижат к левому краю контейнера. Если ни "top" ни "bottom" не заданы, то дочерний View +будет прижат к верхнему краю контейнера. + +## DetailsView + +DetailsView является контейнером, реализующим интерфейс ViewsContainer. +Для создания DetailsView используется функция + + func NewDetailsView(session Session, params Params) DetailsView + +Помимо дочерних View данный контейнер имеет свойство "summary" (константа Summary). +В качестве значения свойства "summary" может быть или View или строка текста. + +DetailsView может находиться в одном из двух состояний: + +* отображается только содержимое свойства "summary". Дочерние View скрыты и не занимают место на экране + +* отображается сначала содержимое свойства "summary", а ниже дочерние View. +Размещение дочерних View, аналогично ColumnLayout с "column-count" равным 0. + +DetailsView переключается между состояниями по клику по "summary". + +Для принудительного переключения состояний DetailsView используется bool свойство +"expanded" (константа Expanded). Соответственно значение "true" показывает дочерние +View, "false" - скрывает. + +Получить значение свойства "expanded" можно с помощью функции + + func IsDetailsExpanded(view View, subviewID string) bool + +а значение свойства "summary" можно получить с помощью функции + + func GetDetailsSummary(view View, subviewID string) View + +## Resizable + +Resizable является контейнером в который можно поместить только один View. Resizable позволяет +интерактивно менять размеры вложенного View. + +Вокруг влоденного View создается рамка, потянув за которую можно менять размеры. + +Resizable не реализует интерфейс ViewsContainer. Для управлением вложенным View используется +только свойство Content. Данному свойству может быть присвоено значение типа View или +строка текста. Во втором случае создается TextView. + +Рамка вокруг вложенного View может быть как со всех сторон, так и только с отдельных. +Для задания сторон рамки используется свойство "side" (константа Side) типа int. +Оно может принимать следующие значения: + +| Значение | Константа | Имя | Сторона рамки | +|:--------:|--------------|----------|------------------------------------| +| 1 | TopSide | "top" | Верхняя | +| 2 | RightSide | "right" | Правая | +| 4 | BottomSide | "bottom" | Нижняя | +| 8 | LeftSide | "left" | Левая | +| 15 | AllSides | "all" | Все стороны. Значение по умолчанию | + +Кроме этих значений может также использоваться or-комбинация TopSide, RightSide, BottomSide и LeftSide. +AllSides определено как + + AllSides = TopSide | RightSide | BottomSide | LeftSide + +Для установки ширины рамки используется SizeUnit свойство "resize-border-width" (константа ResizeBorderWidth). +Значение по умолчанию для "resize-border-width" равно 4px. + +## TextView + +Элемент TextView расширяющий интерфейс View предназначен для вывода текста. + +Для создания TextView используется функция: + + func NewTextView(session Session, params Params) TextView + +Выводимый текст задается string свойством "text" (константа Text). +Помимо метода Get значение свойства "text" может быть получено с помощью функции + + func GetText(view View, subviewID string) string + +TextView наследует от View все свойства параметров текста ("font-name", "text-size", "text-color" и т.д.). +Кроме них добавляется еще один "text-overflow" (константа TextOverflow). Он определяет как обрезается +текст если он выходит за границы. Данное свойство типа int может принимать следующие значения + +| Значение | Константа | Имя | Обрезка текста | +|:--------:|----------------------|------------|--------------------------------------------| +| 0 | TextOverflowClip | "clip" | Текст обрезается по границе (по умолчанию) | +| 1 | TextOverflowEllipsis | "ellipsis" | В конце видимой части текста выводится '…' | + +## EditView + +Элемент EditView является редактором теста и расширяет интерфейс View. + +Для создания EditView используется функция: + + func NewEditView(session Session, params Params) EditView + +Возможно несколько вариантов редактируемого текста. Тип редактируемого текста устанвливается +с помощью int свойства "edit-view-type" (константа EditViewType). +Данное свойство может принимать следующие значения: + +| Значение | Константа | Имя | Тип редактора | +|:--------:|----------------|-------------|-----------------------------------------------------| +| 0 | SingleLineText | "text" | Однострочный редактор текста. Значение по умолчанию | +| 1 | PasswordText | "password" | Радактор пароля. Текст скрывается звездочками | +| 2 | EmailText | "email" | Редактор для ввода одиночного e-mail | +| 3 | EmailsText | "emails" | Редактор для ввода нескольких e-mail | +| 4 | URLText | "url" | Редактор для ввода интернет адреса | +| 5 | PhoneText | "phone" | Редактор для ввода телефонного номера | +| 6 | MultiLineText | "multiline" | Многострочный редактор текста | + +Для упрощения текста программы можно использовать свойства "type" (константа Type) вместо "edit-view-type". +Эти имена свойств синонимы. Но при описании стиля "type" использовать нельзя + +Для установки/получения редактируемого текста используется string свойство "text" (константа Text) + +Максимальная длина редактируемого текста устанавливается с помощью int свойства "max-length" +(константа MaxLength). + +Вы можете ограничить вводимый текст с помощью регулярного выражения. Для этого используется +string свойство "edit-view-pattern" (константа EditViewPattern). Вместо "edit-view-pattern" +можно использовать синоним "pattern" (константа Pattern), за исключением описания стиля. + +Для запрещения редактирования текста используется bool свойство "readonly" (константа ReadOnly). + +Для включения/выключения встроенной проверки орфографии используется bool свойство "spellcheck" +(константа Spellcheck). Проверка орфографии можно включить только если тип редактора установлен +в SingleLineText или MultiLineText. + +Для редактора можно установить подсказку которая будет показываться пока редактор пуст. +Для этого используется string свойство "hint" (константа Hint). + +Для многострочного редактора может быть включен режим автоматического переноса. Для +этого используется bool свойство "wrap" (константа Wrap). Если "wrap" выключен (значение по умолчанию), +то используется горизонтальная прокрутка. Если включен, то по достижении границы EditView +текст переносится на новую строку. + +Для получения значений свойств EditView могут использоваться следующие функции: + + func GetText(view View, subviewID string) string + func GetHint(view View, subviewID string) string + func GetMaxLength(view View, subviewID string) int + func GetEditViewType(view View, subviewID string) int + func GetEditViewPattern(view View, subviewID string) string + func IsReadOnly(view View, subviewID string) bool + func IsEditViewWrap(view View, subviewID string) bool + func IsSpellcheck(view View, subviewID string) bool + + +Для отслеживания изменения текста используется событие "edit-text-changed" (константа +EditTextChangedEvent). Основной слушатель события имеет следующий формат: + + func(EditView, string) + +где второй аргумент это новое значение текста + +Получить текущий список слушателей изменения текста можно с помощью функции + + func GetTextChangedListeners(view View, subviewID string) []func(EditView, string) + +## NumberPicker + +Элемент NumberPicker расширяет интерфейс View и предназначен для ввода чисел. + +Для создания NumberPicker используется функция: + + func NewNumberPicker(session Session, params Params) NumberPicker + +NumberPicker может работать в двух режимах: редактор текста и слайдер. +Режим устанавливает int свойство "date-picker-type" (константа NumberPickerType). +Свойство "date-picker-type" может принимать следующие значения: + +| Значение | Константа | Имя | Тип редактора | +|:--------:|--------------|----------|----------------------------------------| +| 0 | NumberEditor | "editor" | Редактор текста. Значение по умолчанию | +| 1 | NumberSlider | "slider" | Слайдер | + +Установить/прочитать текущее значение можно с помощью свойства "date-picker-value" +(константа NumberPickerValue). В качестве значения свойству "date-picker-value" могут быть переданы: + +* float64 +* float32 +* int +* int8…int64 +* uint +* uint8…uint64 +* текстовое представление любых из выше перечисленых типов + +Все эти типы приводятся к float64. Соответственно функция Get всегда возвращает float64 значение. +Прочитано значение свойства "date-picker-value" может быть также с помощью функции: + + func GetNumberPickerValue(view View, subviewID string) float64 + +На вводимые значения могут быть наложены ограничения. Для этого используются следующие свойства: + +| Свойство | Константа | Ограничение | +|----------------------|------------------|------------------------| +| "date-picker-min" | NumberPickerMin | Минимальное значение | +| "date-picker-max" | NumberPickerMax | Максимальное значение | +| "date-picker-step" | NumberPickerStep | Шаг изменения значения | + +Присвоины данным свойствам могут те же типы значений, что и "date-picker-value". + +По умолчанию, в случае если "date-picker-type" равно NumberSlider, минимальное значение равно 0, +максимальное - 1. Если же "date-picker-type" равно NumberEditor то вводимые числа, по умолчанию, +ограничены лишь диапазоном значений float64. + +Прочитать значения данных свойств можно с помощью функций: + + func GetNumberPickerMinMax(view View, subviewID string) (float64, float64) + func GetNumberPickerStep(view View, subviewID string) float64 + +Для отслеживания изменения вводимого значения используется событие "date-changed" (константа +NumberChangedEvent). Основной слушатель события имеет следующий формат: + + func(picker NumberPicker, newValue float64) + +где второй аргумент это новое значение + +Получить текущий список слушателей изменения значения можно с помощью функции + + func GetNumberChangedListeners(view View, subviewID string) []func(NumberPicker, float64) + +## DatePicker + +Элемент DatePicker расширяет интерфейс View и предназначен для ввода дат. + +Для создания DatePicker используется функция: + + func NewDatePicker(session Session, params Params) DatePicker + +Установить/прочитать текущее значение можно с помощью свойства "date-picker-value" +(константа DatePickerValue). В качестве значения свойству "date-picker-value" могут быть переданы: + +* time.Time +* константа +* текст, который может быть преобразован в time.Time функцией + func time.Parse(layout string, value string) (time.Time, error) + +Текст преобразуется в time.Time. Соответственно функция Get всегда возвращает time.Time значение. +Прочитано значение свойства "date-picker-value" может быть также с помощью функции: + + func GetDatePickerValue(view View, subviewID string) time.Time + +На вводимые даты могут быть наложены ограничения. Для этого используются следующие свойства: + +| Свойство | Константа | Тип данных | Ограничение | +|--------------------|----------------|------------|----------------------------| +| "date-picker-min" | DatePickerMin | time.Time | Минимальное значение даты | +| "date-picker-max" | DatePickerMax | time.Time | Максимальное значение даты | +| "date-picker-step" | DatePickerStep | int | Шаг изменения даты в днях | + +Прочитать значения данных свойств можно с помощью функций: + + func GetDatePickerMin(view View, subviewID string) (time.Time, bool) + func GetDatePickerMax(view View, subviewID string) (time.Time, bool) + func GetDatePickerStep(view View, subviewID string) int + +Для отслеживания изменения вводимого значения используется событие "date-changed" (константа +DateChangedEvent). Основной слушатель события имеет следующий формат: + + func(picker DatePicker, newDate time.Time) + +где второй аргумент это новое значение даты + +Получить текущий список слушателей изменения даты можно с помощью функции + + func GetDateChangedListeners(view View, subviewID string) []func(DatePicker, time.Time) + +## TimePicker + +Элемент TimePicker расширяет интерфейс View и предназначен для ввода времени. + +Для создания TimePicker используется функция: + + func NewTimePicker(session Session, params Params) TimePicker + +Установить/прочитать текущее значение можно с помощью свойства "time-picker-value" +(константа TimePickerValue). В качестве значения свойству "time-picker-value" могут быть переданы: + +* time.Time +* константа +* текст, который может быть преобразован в time.Time функцией + func time.Parse(layout string, value string) (time.Time, error) + +Текст преобразуется в time.Time. Соответственно функция Get всегда возвращает time.Time значение. +Прочитано значение свойства "time-picker-value" может быть также с помощью функции: + + func GetTimePickerValue(view View, subviewID string) time.Time + +На вводимое время могут быть наложены ограничения. Для этого используются следующие свойства: + +| Свойство | Константа | Тип данных | Ограничение | +|--------------------|----------------|------------|----------------------------------| +| "time-picker-min" | TimePickerMin | time.Time | Минимальное значение времени | +| "time-picker-max" | TimePickerMax | time.Time | Максимальное значение времени | +| "time-picker-step" | TimePickerStep | int | Шаг изменения времени в секундах | + +Прочитать значения данных свойств можно с помощью функций: + + func GetTimePickerMin(view View, subviewID string) (time.Time, bool) + func GetTimePickerMax(view View, subviewID string) (time.Time, bool) + func GetTimePickerStep(view View, subviewID string) int + +Для отслеживания изменения вводимого значения используется событие "time-changed" (константа +TimeChangedEvent). Основной слушатель события имеет следующий формат: + + func(picker TimePicker, newTime time.Time) + +где второй аргумент это новое значение времени + +Получить текущий список слушателей изменения даты можно с помощью функции + + func GetTimeChangedListeners(view View, subviewID string) []func(TimePicker, time.Time) + +## ColorPicker + +Элемент ColorPicker расширяет интерфейс View и предназначен для выбора цвета в формате RGB без альфа канала. + +Для создания ColorPicker используется функция: + + func NewColorPicker(session Session, params Params) ColorPicker + +Установить/прочитать текущее значение можно с помощью свойства "color-picker-value" +(константа ColorPickerValue). В качестве значения свойству "color-picker-value" могут быть переданы: + +* Color +* текстовое представление Color +* константа + +Прочитано значение свойства "color-picker-value" может быть также с помощью функции: + + func GetColorPickerValue(view View, subviewID string) Color + +Для отслеживания изменения выбранного цвета используется событие "color-changed" (константа +ColorChangedEvent). Основной слушатель события имеет следующий формат: + + func(picker ColorPicker, newColor Color) + +где второй аргумент это новое значение цвета + +Получить текущий список слушателей изменения даты можно с помощью функции + + func GetColorChangedListeners(view View, subviewID string) []func(ColorPicker, Color) + +## DropDownList + +Элемент DropDownList расширяет интерфейс View и предназначен для выбора значения из выпадающего списка. + +Для создания DropDownList используется функция: + + func NewDropDownList(session Session, params Params) DropDownList + +Список возможных значений задается с помощью свойства "items" (константа Items). +В качестве значения свойству "items" могут быть переданы следующие типы данных + +* []string +* []fmt.Stringer +* []interface{} содержащий в качестве элементов только: string, fmt.Stringer, bool, rune, +float32, float64, int, int8…int64, uint, uint8…uint64. + +Все эти типы данных преопразуются в []string и присваиваются свойству "items". +Прочитать значение свойства "items" можно с помощью функции + + func GetDropDownItems(view View, subviewID string) []string + +Выбранное значение определяется int свойством "current" (константа Current). Значение по умолчанию 0. +Прочитать значение данного свойства можно с помощью функции + + func GetDropDownCurrent(view View, subviewID string) int + +Для отслеживания изменения свойства "current" используется событие "drop-down-event" (константа +DropDownEvent). Основной слушатель события имеет следующий формат: + + func(list DropDownList, newCurrent int) + +где второй аргумент это индекс выбранного элемента + +Получить текущий список слушателей изменения даты можно с помощью функции + + func GetDropDownListeners(view View, subviewID string) []func(DropDownList, int) + +## ProgressBar + +Элемент DropDownList расширяет интерфейс View и предназначен для отображение прогресса в виде +заполняемой полоски. + +Для создания ProgressBar используется функция: + + func NewProgressBar(session Session, params Params) ProgressBar + +ProgressBar имеет два свойства типа float64: +* "progress-max" (константа ProgressBarMax) - максимальное значение (по умолчанию 1); +* "progress-value" (константа ProgressBarValue) - текущее значение (по умолчанию 0). + +Минимальное всегда 0. +В качестве значений этим свойствам может быть присвоино кроме float64 также float32, int, +int8…int64, uint, uint8…uint64 + +Прочитать значение данных свойств можно с помощью функций + + func GetProgressBarMax(view View, subviewID string) float64 + func GetProgressBarValue(view View, subviewID string) float64 + +## Button + +Элемент Button реализует нажимаемую кнопку. Это CustomView (о нем ниже) на базе ListLayout и, +соответсвенно, обладает всеми свойствами ListLayout. Но в отличие от ListLayout может получать +фокус ввода. + +Контент, по умолчанию, выровнен по центру. + +Для создания Button используется функция: + + func NewButton(session Session, params Params) Button + +## ListView + +Элемент ListView реализует список. +Для создания ListView используется функция: + + func NewListView(session Session, params Params) ListView + +### Свойство "items" + +Элементы списка задаются с помощью свойства "items" (константа Items). Основным значением +свойства "items" является интерфейс ListAdapter: + + type ListAdapter interface { + ListSize() int + ListItem(index int, session Session) View + IsListItemEnabled(index int) bool + } + +Соостветственно функции этого интерфейса должны возвращать количество элементов, View i-го элемента и +статус i-го элемента разрешен/запрещен. + +Вы можете реализовать этот интерфейс сами или воспользоваться вспомогательными функциями: + + func NewTextListAdapter(items []string, params Params) ListAdapter + func NewViewListAdapter(items []View) ListAdapter + +NewTextListAdapter создает адаптер из массива строк, второй аргумент это параметры TextView используемого +для отобрыжения текста. NewViewListAdapter создает адаптер из массива View. + +Cвойствy "items" могут быть присвоены следующие типы данных: + +* ListAdapter; +* []View при присваиваниии преобразуется в ListAdapter с помощью функции NewViewListAdapter; +* []string при присваиваниии преобразуется в ListAdapter с помощью функции NewTextListAdapter; +* []interface{} который может содержать элементы типа View, string, fmt.Stringer, bool, rune, +float32, float64, int, int8…int64, uint, uint8…uint64. При присваиваниии все типы кроме +View и string преобразуется в string, далее все string в TextView и из получившегося массива View +с помощью функции NewViewListAdapter получается ListAdapter. + +Если элементы списка меняются в ходе работы, то после изменения необходимо вызывать или функцию +ReloadListViewData() интерфейса ListView или глобальную функцию ReloadListViewData(view View, subviewID string). +Данные функции обновляют отображаемые элементы списка. + +### Свойство "orientation" + +Элементы списка могут располагаться как вертикально (колонками), так и горизонтально (строками). +Свойство "orientation" (константа Orientation) типа int задает то как элементы списка будут +располагаться друг относительно друга. Свойство может принимать следующие значения: + +| Значение | Константа | Расположение | +|:--------:|-----------------------|---------------------------------------------------| +| 0 | TopDownOrientation | Элементы располагаются в столбец сверху вниз. | +| 1 | StartToEndOrientation | Элементы располагаются в строку с начала в конец. | +| 2 | BottomUpOrientation | Элементы располагаются в столбец снизу вверх. | +| 3 | EndToStartOrientation | Элементы располагаются в строку с конца в начала. | + +Положение начала и конца для StartToEndOrientation и EndToStartOrientation зависит от значения +свойства "text-direction". Для языков с письмом справа налево (арабский, иврит) начало находится +справа, для остальных языков - слева. + +Получить значение данного свойства можно с помощью функции + + func GetListOrientation(view View, subviewID string) int + +### Свойство "wrap" + +Свойство "wrap" (константа Wrap) типа int определяет расположения элементов в случае достижения +границы контейнера. Возможны три варианта: + +* WrapOff (0) - колонка/строка элементов продолжается и выходит за границы видимой области. + +* WrapOn (1) - начинается новая колонка/строка элементов. Новая колонка располагается по направлению +к концу (о положении начала и конца см. выше), новая строка - снизу. + +* WrapReverse (2) - начинается новая колонка/строка элементов. Новая колонка располагается по направлению +к началу (о положении начала и конца см. выше), новая строка - сверху. + +Получить значение данного свойства можно с помощью функции + + func GetListWrap(view View, subviewID string) int + +### Свойства "item-width" и "item-height" + +По умолчанию высота и ширина элементов списка вычисляется на основе их содержимого. +Это приводит к тому что элементы вертикального списка могут иметь разную высоту, а элементы +горизонтального - разную ширину. + +Вы можете установить фиксированную высоту и ширину элемента списка. Для этого используются SizeUnit +свойства "item-width" и "item-height" + +Получить значения данных свойств можно с помощью функций + + func GetListItemWidth(view View, subviewID string) SizeUnit + func GetListItemHeight(view View, subviewID string) SizeUnit + +### Свойство "item-vertical-align" + +Свойство "item-vertical-align" (константа ItemVerticalAlign) типа int устанавливает вертикальное +выравнивание содержимого элементов списка. Допустимые значения: + +| Значение | Константа | Имя | Значение | +|:--------:|--------------|-----------|-------------------------------| +| 0 | TopAlign | "top" | Выравнивание по верхнему краю | +| 1 | BottomAlign | "bottom" | Выравнивание по нижнему краю | +| 2 | CenterAlign | "center" | Выравнивание по центру | +| 3 | StretchAlign | "stretch" | Выравнивание по высоте | + +Получить значение данного свойства можно с помощью функции + + func GetListItemVerticalAlign(view View, subviewID string) int + +### Свойство "item-horizontal-align" + +Свойство "item-horizontal-align" (константа ItemHorizontalAlign) типа int устанавливет +горизонтальное выравнивание содержимого элементов списка. Допустимые значения: + +| Значение | Константа | Имя | Значение | +|:--------:|--------------|-----------|------------------------------| +| 0 | LeftAlign | "left" | Выравнивание по левому краю | +| 1 | RightAlign | "right" | Выравнивание по правому краю | +| 2 | CenterAlign | "center" | Выравнивание по центру | +| 3 | StretchAlign | "stretch" | Выравнивание по ширине | + +Получить значение данного свойства можно с помощью функции + + GetListItemHorizontalAlign(view View, subviewID string) int + +### Свойство "current" + +ListView позволяет выбирать пункты списка имеющие статус "разрешен" (см. ListAdapter). +Элемент может быть выбран как интерактивно, так и программно. Для этого используется +int свойство "current" (константа Current). Значение "current" меньше 0 означает что +не выбран ни один пункт + +Получить значение данного свойства можно с помощью функции + + func GetListViewCurrent(view View, subviewID string) int + +### Свойства "list-item-style", "current-style" и "current-inactive-style" + +Данные три свойства отвечают за стиль фона и свойства текста каждого элемента списка. + +| Свойство | Константа | Стиль | +|--------------------------|----------------------|-------------------------------------------------| +| "list-item-style" | ListItemStyle | Стиль невыбранного элемента | +| "current-style" | CurrentStyle | Стиль выбранного элемента. ListView в фокусе | +| "current-inactive-style" | CurrentInactiveStyle | Стиль выбранного элемента. ListView не в фокусе | + +### Свойства "checkbox", "checked", "checkbox-horizontal-align" и "checkbox-vertical-align" + +Свойство "current" позволяет выбрать один пункт списка. +Свойства "checkbox" позволяет добавить к каждому элементу списка чекбокс с помощью которого +можно выбрать несколько элементов списка. Свойство "checkbox" (константа ItemCheckbox) имеет тип int +и может принимать следующие значения + +| Значение | Константа | Имя | Вид чекбокса | +|:--------:|------------------|------------|--------------------------------------------------| +| 0 | NoneCheckbox | "none" | Нет чекбокса. Значение по умолчанию | +| 1 | SingleCheckbox | "single" | ◉ Чекбокс позволяющий пометить только один пункт | +| 2 | MultipleCheckbox | "multiple" | ☑ Чекбокс позволяющий пометить несколько пунктов | + + +Получить значение данного свойства можно с помощью функции + + func GetListViewCheckbox(view View, subviewID string) int + +Получить/установить список помеченных пунктов можно с помощью свойства "checked" (константа Checked). +Данное свойство имеет тип []int и хранит индексы помеченных элементов. +Получить значение данного свойства можно с помощью функции + + func GetListViewCheckedItems(view View, subviewID string) []int + +Проверить помечен ли конкретный элемент можно с помощью функции + + func IsListViewCheckedItem(view View, subviewID string, index int) bool + +По умолчанию чекбокс расположен в верхнем левом углу элемента. Изменить его положение можно +с помощью int свойств "checkbox-horizontal-align" и "checkbox-vertical-align" (константы +CheckboxHorizontalAlign и CheckboxVerticalAlign) + +Свойство "checkbox-horizontal-align" (константа СheckboxHorizontalAlign) может принимать следующие значения: + +| Значение | Константа | Имя | Расположение чекбокса | +|:--------:|--------------|----------|-------------------------------------------------| +| 0 | LeftAlign | "left" | У левого края. Контент справа | +| 1 | RightAlign | "right" | У правого края. Контент слева | +| 2 | CenterAlign | "center" | По центру по горизонтали. Контент ниже или выше | + +Свойство "checkbox-vertical-align" (константа CheckboxVerticalAlign) может принимать следующие значения: + +| Значение | Константа | Имя | Значение | +|:--------:|--------------|----------|-------------------------------| +| 0 | TopAlign | "top" | Выравнивание по верхнему краю | +| 1 | BottomAlign | "bottom" | Выравнивание по нижнему краю | +| 2 | CenterAlign | "center" | Выравнивание по центру | + +Особый случай когда и "checkbox-horizontal-align" и "checkbox-vertical-align" равны CenterAlign (2). +В этом случае чекбокс расположен по центру по горизонтали, контент ниже + +Получить значения свойств можно "checkbox-horizontal-align" и "checkbox-vertical-align" с помощью функций + + func GetListViewCheckboxHorizontalAlign(view View, subviewID string) int + func GetListViewCheckboxVerticalAlign(view View, subviewID string) int + +### События ListView + +Для ListView есть три характерных события + +* "list-item-clicked" (константа ListItemClickedEvent) - возникает когда пользователь кликает по элементу +списка. Основной слушатель данного события имеет следующий формат: func(ListView, int). Где второй +аргумент это индекс элемента. + +* "list-item-selected" (константа ListItemSelectedEvent) - возникает когда пользователь элемент списка +становится выбраным. Основной слушатель данного события имеет следующий формат: func(ListView, int). +Где второй аргумент это индекс элемента. + +* "list-item-checked" (константа ListItemCheckedEvent) - возникает когда пользователь ставит/снимает +пометку чекбокса элемента списка. Основной слушатель данного события имеет следующий формат: func(ListView, []int). +Где второй аргумент это массив индексов помеченных элементов. + +Получить списки слушателей данных событий можно с помощью функций: + + func GetListItemClickedListeners(view View, subviewID string) []func(ListView, int) + func GetListItemSelectedListeners(view View, subviewID string) []func(ListView, int) + func GetListItemCheckedListeners(view View, subviewID string) []func(ListView, []int) + +## TableView + +Элемент TableView реализует таблицу. Для создания TableView используется функция: + + func NewTableView(session Session, params Params) TableView + +### Свойство "content" + +Свойство "content" определяет содержимое таблицы. Для описания содержимого необходимо реализовать +интерфейс TableAdapter объявленный как + + type TableAdapter interface { + RowCount() int + ColumnCount() int + Cell(row, column int) interface{} + } + +где функции RowCount() и ColumnCount() должны возврацать количество строк и столбцов в таблице; +Cell(row, column int) возвращает содержимое ячейки таблицы. Функция Cell() может возвращать +элементы следующих типов: + +* string +* rune +* float32, float64 +* целочисленные значения: int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64 +* bool +* rui.Color +* rui.View +* fmt.Stringer + +Свойству "content" можно также присваивать следующие типы данных + +* TableAdapter +* [][]interface{} +* [][]string + +[][]interface{} и [][]string при присвоении преобразуются к TableAdapter. + +### Свойство "cell-style" + +Свойство "cell-style" (константа CellStyle) предназначено для настройки оформления ячейки таблицы. Данному свойству +может быть присвоено только реализация интерфейса TableCellStyle. + + type TableCellStyle interface { + CellStyle(row, column int) Params + } + +Данный интерфейс содержит только одну функцию CellStyle, которая возвращает параметры оформления +заданной ячейки таблицы. Можно использовать любые свойства интерфейса View. Например + + func (style *myTableCellStyle) CellStyle(row, column int) rui.Params { + if row == 0 { + return rui.Params { + rui.BackgroundColor: rui.Gray, + rui.Italic: true, + } + } + return nil + } + +Если не надо менять оформление какой-то ячейки, то для нее можно вернуть nil. + +#### Свойства "row-span" и "column-span" + +Помимо свойств интерфейса View, функцией CellStyle могут возвращаться еще два свойства типа int: +"row-span" (константа RowSpan) и "column-span" (константа ColumnSpan). +Данные свойства используются для объединения ячеек таблицы. + +Свойство "row-span" указавает сколько ячеек надо объединить по вертикали, +а "column-span" - по горизонтали. Например + + func (style *myTableCellStyle) CellStyle(row, column int) rui.Params { + if row == 0 && column == 0 { + return rui.Params { rui.RowSpan: 2 } + } + if row == 0 && column == 1 { + return rui.Params { rui.ColumnSpan: 2 } + } + return nil + } + +В этом случае таблица будет иметь следующий вид + +|------|----------------| +| | | +| |-------|--------| +| | | | +|------|-------|--------| + +Если в качестве значения свойства "content" используется [][]interface{}, то для объединения +ячеек используются пустые структуры + + type VerticalTableJoin struct { + } + type HorizontalTableJoin struct { + } + +Данные структуры присоединяют ячейку, соответсвенно, к верхней/левой. Описание приведенной выше таблицы будет +иметь следующий вид + + content := [][]interface{} { + {"", "", rui.HorizontalTableJoin{}}, + {rui.VerticalTableJoin{}, "", ""}, + } + +### Свойство "row-style" + +Свойство "row-style" (константа RowStyle) предназначено для настройки оформления строки таблицы. +Данному свойству может быть присвоены или реализация интерфейса TableRowStyle или []Params. +TableRowStyle объявлена как + + type TableRowStyle interface { + RowStyle(row int) Params + } + +Функция RowStyle возвращает параметры применяемые ко всей строке таблицы. +Свойство "row-style" имеет более низкий приоритет по сравнению со сойством "cell-style", +т.е. свойства заданные в "cell-style" будут использоваться вместо заданных в "row-style" + +### Свойство "column-style" + +Свойство "column-style" (константа ColumnStyle) предназначено для настройки оформления столбца таблицы. +Данному свойству может быть присвоены или реализация интерфейса TableColumnStyle или []Params. +TableColumnStyle объявлена как + + type TableColumnStyle interface { + ColumnStyle(column int) Params + } + +Функция ColumnStyle возвращает параметры применяемые ко всему столбцу таблицы. +Свойство "column-style" имеет более низкий приоритет по сравнению со сойствами "cell-style" и "row-style". + +### Свойства "head-height" и "head-style" + +Таблица может иметь "шапку". +Свойство "head-height" (константа HeadHeight) типа int указывает сколько первых строк таблицы образуют "шапку". +Свойство "head-style" (константа HeadStyle) задает стиль шапки. Свойству "head-style" может быть +присвоено, значение типа: + +* string - имя стиля; +* []Params - перечисление свойств "шапки". + +### Свойства "foot-height" и "foot-style" + +Таблица может иметь в конце финализирующие строки (например строка "Итого"). +Свойство "foot-height" (константа FootHeight) типа int указывает количество этих финализирующих строк. +Свойство "foot-style" (константа FootStyle) задает их стиль. Значения свойства "foot-style" аналогичны свойству "head-style". + +### Свойство "cell-padding" + +Свойство "cell-padding" (константа CellPadding) типа SizeUnit задает отступы от границ ячейки до +контента. Данное свойство эквивалентно + + func (style *myTableCellStyle) CellStyle(row, column int) rui.Params { + return rui.Params { rui.Padding: } + } + +И введено для удобства, чтобы не надо было писать адаптер для задания отступов. +Свойство "cell-padding" имеет более низкий приоритет по сравнению со сойством "cell-style". + +"cell-padding" может также использоваться при задании параметров в свойствах +"row-style", "column-style", "foot-style" и "head-style" + +### Свойство "cell-border" + +Свойство "cell-border" (константа CellBorder) задает памку для всех ячеек таблицы. +Данное свойство эквивалентно + + func (style *myTableCellStyle) CellStyle(row, column int) rui.Params { + return rui.Params { rui.Border: } + } + +И введено для удобства, чтобы не надо было писать адаптер для рамки. +Свойство "cell-border" имеет более низкий приоритет по сравнению со сойством "cell-style". + +"cell-border" может также использоваться при задании параметров в свойствах +"row-style", "column-style", "foot-style" и "head-style" + +### Свойство "table-vertical-align" + +Свойство "table-vertical-align" (константа TableVerticalAlign) типа int задает вертикальное выравание +данных внутри ячейки таблицы. Допустимые значения: + +| Значение | Константа | Имя | Значение | +|:--------:|---------------|------------|-------------------------------| +| 0 | TopAlign | "top" | Выравнивание по верхнему краю | +| 1 | BottomAlign | "bottom" | Выравнивание по нижнему краю | +| 2 | CenterAlign | "center" | Выравнивание по центру | +| 3, 4 | BaselineAlign | "baseline" | Выравнивание по базовой линии | + +Для горизонтального выравнивания используется свойство "text-align" + +## Пользовательский View + +Пользовательский View должен реализовывать интерфейс CustomView, который в свою очередь +расширяет интерфейсы ViewsContainer и View. Пользовательский View создается на основе другого, +который назавается Super View. + +Для упрощение задачи уже имеется базовая реализация CustomView в виде структуры CustomViewData. + +Создание пользовательского View рассмотрим на примере встроенного элемента Buttom: + +1) объявляем интерфейс Button, как расширяющий CustomView, и структуру buttonData как расширяющую CustomViewData + + type Button interface { + rui.CustomView + } + + type buttonData struct { + rui.CustomViewData + } + +2) реализуем функцию CreateSuperView + + func (button *buttonData) CreateSuperView(session Session) View { + return rui.NewListLayout(session, rui.Params{ + rui.Semantics: rui.ButtonSemantics, + rui.Style: "ruiButton", + rui.StyleDisabled: "ruiDisabledButton", + rui.HorizontalAlign: rui.CenterAlign, + rui.VerticalAlign: rui.CenterAlign, + rui.Orientation: rui.StartToEndOrientation, + }) + } + +3) если надо, то переопределяем методы интерфейса CustomView, для Button это +функция Focusable() (так как кнопка может получать фокус, а ListLayout не получает) + + func (button *buttonData) Focusable() bool { + return true + } + +4) пишем функцию для создания Button: + + func NewButton(session rui.Session, params rui.Params) Button { + button := new(buttonData) + rui.InitCustomView(button, "Button", session, params) + return button + } + +При создании CustomView обязательным является вызов функции InitCustomView. +Данная функция инициализирует структуру CustomViewData. Первым аргументом +является указатель на инициализируемую структуру, вторым - имя присвоенное +вашему View, третьим - сессия и четвертым - параметры + +5) регистрируем элемент. Регистрацию рекомендуется осуществлять в методе init пакета + + rui.RegisterViewCreator("Button", func(session rui.Session) rui.View { + return NewButton(session, nil) + }) + +Все! Новый элемент готов + +## CanvasView + +CanvasView это область в которой вы можете рисовать. Для создания CanvasView используется функция: + + func NewCanvasView(session Session, params Params) CanvasView + +CanvasView имеет всего одно дополнительное свойство: "draw-function" (константа DrawFunction). +С помощью данного свойства задается функция рисования имеющая следующее описание + + func(Canvas) + +где Canvas это контекст рисования с помощью которого осуществляется рисование + +Интерфей Canvas сожержит ряд функция для настройки стилей, текста и непосредственно самого рисования. + +Все координаты и размеры задаются только в пикселях, поэтому при рисовании SizeUnit не используется. +Везде используется float64 + +### Настройка стиля линий + +Для настройки цвета линий используются следующие функции интерфейса Canvas: + +* SetSolidColorStrokeStyle(color Color) - линия будет рисоваться сплошным цветом + +* SetLinearGradientStrokeStyle(x0, y0 float64, color0 Color, x1, y1 float64, color1 Color, stopPoints []GradientPoint) - +линия будет рисоваться с помощью линейного градиента. Начальная точка градиента задается с помощью x0, y0 и color0, +конечная - x1, y1 и color1. Массив []GradientPoint задает промежуточные точки градиента. Если промежуточных точек нет, +то в качестве последнего параметра можно передать nil + +* SetRadialGradientStrokeStyle(x0, y0, r0 float64, color0 Color, x1, y1, r1 float64, color1 Color, stopPoints []GradientPoint) - +линия будет рисоваться с помощью радиального градиента. x0, y0, r0, color0 - координаты центра, радиус и цвет начальной окружности. +x1, y1, r1, color1 - координаты центра, радиус и цвет конечной окружности. Массив []GradientPoint задает промежуточные точки градиента + +Структура GradientPoint описана как + + type GradientPoint struct { + Offset float64 + Color Color + } + +где Offset - значение в диапазоне от 0 до 1 задает относительное положение промежуточной точки, Color - цвет этой точки. + +Толщина линии в пикселях задается функцией + + SetLineWidth(width float64) + +Вид концов линии задается с помощью функции + + SetLineCap(cap int) + +где cap может принимать следующие значения + +| Значение | Константа | Вид | +|:--------:|-----------|----------------------------------------------------------------------------------| +| 0 | ButtCap | Окончании линии обрезано в конечной точке. Значение по умолчанию. | +| 1 | RoundCap | Окончании линии скруглено. Центр окружности находится в конечной точке. | +| 2 | SquareCap | В конец линии добавляется прямоугольник с шириной равной половине толщины линии. | + +Форма, используемая для соединения двух отрезков линии в месте их пересечения, задается функцией + + SetLineJoin(join int) + +где join может принимать следующие значения + +| Значение | Константа | Вид | +|:--------:|-----------|--------| +| 0 | MiterJoin | Сегменты соединяются путем удлинения их внешних краев для соединения в одной точке с эффектом заполнения дополнительной области в форме ромба. | +| 1 | RoundJoin | Закругляет углы фигуры, заполняя дополнительный сектор диском с центром в общей конечной точке соединенных сегментов. Радиус этих закругленных углов равен ширине линии. | +| 2 | BevelJoin | Заполняет дополнительную треугольную область между общей конечной точкой соединенных сегментов и отдельными внешними прямоугольными углами каждого сегмента. | + +По умолчанию рисуется сплошная линия. Если необходимо нарисовать прерывистую линию, то +необходимо сначала задать шаблон с помощью функции + + SetLineDash(dash []float64, offset float64) + +где dash []float64 задает шаблон линиии в виде чередования длин отрезков и пропусков. Второй аргумент - +смещение шаблона относительно начала линии. + +Пример + + canvas.SetLineDash([]float64{16, 8, 4, 8}, 0) + +Линия рисуется следующим образом: отрезок длиной 16 пикселей, затеп пропуск длиной 8 пикселей, +отрезок длиной 4 пикселя, затеп пропуск длиной 8 пикселей, затем снова отрезок длиной 16 пикселей и т.д. + +### Настройка стиля заливки + +Для настройки стиля заливки используются следующие функции интерфейса Canvas: + +* SetSolidColorFillStyle(color Color) - фигура будет заливаться сплошным цветом + +* SetLinearGradientFillStyle(x0, y0 float64, color0 Color, x1, y1 float64, color1 Color, stopPoints []GradientPoint) - +фигура будет заливаться с помощью линейного градиента. Начальная точка градиента задается с помощью x0, y0 и color0, +конечная - x1, y1 и color1. Массив []GradientPoint задает промежуточные точки градиента. Если промежуточных точек нет, +то в качестве последнего параметра можно передать nil + +* SetRadialGradientFillStyle(x0, y0, r0 float64, color0 Color, x1, y1, r1 float64, color1 Color, stopPoints []GradientPoint) - +фигура будет заливаться с помощью радиального градиента. x0, y0, r0, color0 - координаты центра, радиус и цвет начальной окружности. +x1, y1, r1, color1 - координаты центра, радиус и цвет конечной окружности. Массив []GradientPoint задает промежуточные точки градиента + +### Рисование геометрических фигур + +#### Прямоугольник + +Для рисования прямоугольников могут использоваться три функции: + + FillRect(x, y, width, height float64) + StrokeRect(x, y, width, height float64) + FillAndStrokeRect(x, y, width, height float64) + +FillRect рисует закрашеный прямоугольник. + +StrokeRect рисует контур прямоугольника. + +FillAndStrokeRect рисует контур и закрашивает внутриности. + +#### Прямоугольник с закругленными углами + +Аналогично прямоуголику есть три функции рисования + + FillRoundedRect(x, y, width, height, r float64) + StrokeRoundedRect(x, y, width, height, r float64) + FillAndStrokeRoundedRect(x, y, width, height, r float64) + +где r это радиус скругления + +#### Эллипс + +Для рисования эллипсов также могут использоваться три функции: + + FillEllipse(x, y, radiusX, radiusY, rotation float64) + StrokeEllipse(x, y, radiusX, radiusY, rotation float64) + FillAndStrokeEllipse(x, y, radiusX, radiusY, rotation float64) + +где x, y - центр эллипса, radiusX, radiusY - радиусы эллипса по оси X и Y, +rotation - угол поворота элилпса относительно центра в радианах + +#### Path + +Интерфейс Path позволяет описать сложную фигуру. Создается Path с помощью функции NewPath(). + +После создания вы должны описать фигуру. Для этого могут использоваться следующие функции интерфейса: + +* MoveTo(x, y float64) - переместить текущую точке в заданные координаты; + +* LineTo(x, y float64) - добавить линию из текущей точки в заданную; + +* ArcTo(x0, y0, x1, y1, radius float64) - добавить дугу окружности, используя заданные контрольные точки и радиус. +При необходимости дуга автоматически соединяется с последней точкой пути прямой линией. +x0, y0 - координаты первой контрольной точки; +x1, y1 - координаты второй контрольной точки; +radius - радиус дуги. Должен быть неотрицательным. + +* Arc(x, y, radius, startAngle, endAngle float64, clockwise bool) - добавить дугу окружности. +x, y - координаты центра дуги; +radius - радиус дуги. Должен быть неотрицательным; +startAngle - угол в радианах, под которым начинается дуга, измеряется по часовой стрелке от положительной оси X. +endAngle - угол в радианах, под которым заканчивается дуга, измеряется по часовой стрелке от положительной оси X. +clockwise - если true, дуга будет нарисована по часовой стрелке между начальным и конечным углами, иначе - против часовой стрелки + +* BezierCurveTo(cp0x, cp0y, cp1x, cp1y, x, y float64) - добавить кубическую кривую Безье из текущей точки. +cp0x, cp0y - координаты первой контрольной точки; +cp1x, cp1y - координаты второй контрольной точки; +x, y - координаты конечной точки. + +* QuadraticCurveTo(cpx, cpy, x, y float64) - добавить квадратичную кривую Безье из текущей точки. +cpx, cpy - координаты контрольной точки; +x, y - координаты конечной точки. + +* Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, clockwise bool) - добавить эллиптическую дугу. +x, y - координаты центра эллипса; +radiusX - радиус большой оси эллипса. Должен быть неотрицательным; +radiusY - радиус малой оси эллипса. Должен быть неотрицательным; +rotation - вращение эллипса, выраженное в радианах; +startAngle - угол начала эллипса в радианах, измеренный по часовой стрелке от положительной оси x; +endAngle - угол в радианах, под которым заканчивается эллипс, измеренный по часовой стрелке от положительной оси x. +clockwise - если true, рисует эллипс по часовой стрелке, иначе - против часовой стрелки. + +Функция Close() вызывается в конце и соединяет начальную и конечную точку фигуры. Используется только для +замкнутых фигур. + +После того как Path сформирован его можно нарисовать использую следующие 3 функции + + FillPath(path Path) + StrokePath(path Path) + FillAndStrokePath(path Path) + +#### Линия + +Для рисования линии используется функция + + DrawLine(x0, y0, x1, y1 float64) + +### Текст + +Для вывода текста в заданных координатах используются две функции + + FillText(x, y float64, text string) + StrokeText(x, y float64, text string) + +Функция StrokeText рисует контур текста, FillText - рисует сам текст. + +Горизонтальное выравнивание текста относительно заданных координат устанавливается с помощью функции + + SetTextAlign(align int) + +где align может принимать одно из следующих значений: + +| Значение | Константа | Выравнивание | +|:--------:|-------------|----------------------------------------------------| +| 0 | LeftAlign | Заданная точка является самой левой точкой текста | +| 1 | RightAlign | Заданная точка является самой правой точкой текста | +| 2 | CenterAlign | Текст центрируется относительно заданной точки | +| 3 | StartAlign | Если текст выводиться слева направо, то вывод текста эквивалентен LeftAlign, иначе RightAlign | +| 4 | EndAlign | Если текст выводиться слева направо, то вывод текста эквивалентен RightAlign, иначе LeftAlign | + +Вертикальное выравнивание текста относительно заданных координат устанавливается с помощью функции + + SetTextBaseline(baseline int) + +где baseline может принимать одно из следующих значений: + +| Значение | Константа | Выравнивание | +|:--------:|---------------------|---------------------------------------------------| +| 0 | AlphabeticBaseline | Относительно нормальной базовой линии текста | +| 1 | TopBaseline | Относительно верхней границы текста | +| 2 | MiddleBaseline | Относительно середины текста | +| 3 | BottomBaseline | Относительно нижней границы текста | +| 4 | HangingBaseline | Относительно подвешенной базовой линии текста (используется тибетскими и другими индийскими шрифтами) | +| 5 | IdeographicBaseline | Относительно идеографической базовой линии текста | + +Идеографическая базовая линия это нижняя часть изображения символов, если основная часть символов выступает за базовую линию алфавита +(Используется китайскими, японскими и корейскими шрифтами). + +Для установки параметров шрифта выводимого текста используются функции + + SetFont(name string, size SizeUnit) + SetFontWithParams(name string, size SizeUnit, params FontParams) + +где FontParams определена как + + type FontParams struct { + // Italic - if true then a font is italic + Italic bool + // SmallCaps - if true then a font uses small-caps glyphs + SmallCaps bool + // Weight - a font weight. Valid values: 0...9, there + // 0 - a weight does not specify; + // 1 - a minimal weight; + // 4 - a normal weight; + // 7 - a bold weight; + // 9 - a maximal weight. + Weight int + // LineHeight - the height (relative to the font size of the element itself) of a line box. + LineHeight SizeUnit + } + +Функция TextWidth позволяет узнать ширину выводимого текста в пикселях + + TextWidth(text string, fontName string, fontSize SizeUnit) float64 + +### Изображение + +Перед рисованием изображения его необходимо сначала загрузить. Для этого используется глобальная функция: + + func LoadImage(url string, onLoaded func(Image), session Session) Image { + +Изображение загружается асинхронно. После окончания загрузки будет вызвана функция передаваемая во втором аргументе. +Если изображение было загружено успешно, то функция LoadingStatus() интерфейса Image будет возвращать значение +ImageReady (1), если при загрузке произошла ошибка, то данная функция будет возвращать ImageLoadingError (2). +Текстовое описание ошибки возвращает функция LoadingError() + +В отличие от ImageView призагрузке Image не учитывается плотность пикселей. Вы должны сами определить какое изображение +загружать. Это можно сделать так: + + var url string + if session.PixelRatio() == 2 { + url = "image@2x.png" + } else { + url = "image.png" + } + +Для рисования изображения используются следующие функции: + + DrawImage(x, y float64, image Image) + DrawImageInRect(x, y, width, height float64, image Image) + DrawImageFragment(srcX, srcY, srcWidth, srcHeight, dstX, dstY, dstWidth, dstHeight float64, image Image) + +Функция DrawImage выводит изображение как есть (без масштабирования): x, y - координаты левого верхнего угла изображения + +Функция DrawImageInRect выводит изображение с масштабированием: x, y - координаты левого верхнего угла изображения, +width, height - ширина и высота результата + +Функция DrawImageFragment выводит фрагмент изображения с масштабированием: srcX, srcY, srcWidth, srcHeight описывают исходную +область изображения, dstX, dstY, dstWidth, dstHeight - результирующая область. + +Изображение можно также использовать в стиле заливке + + SetImageFillStyle(image Image, repeat int) + +где repeat может принимать следующие значения: + +| Значение | Константа | Описание | +|:--------:|-----------|----------------------------------------------------| +| 0 | NoRepeat | Изображение не повторяется | +| 1 | RepeatXY | Изображение повторяется по вертикали и горизонтали | +| 2 | RepeatX | Изображение повторяется только по горизонтали | +| 3 | RepeatY | Изображение повторяется только по вертикали | + +## AudioPlayer, VideoPlayer, MediaPlayer + +AudioPlayer и VideoPlayer это элементы которые предназначены для воспроизведения аудио и видео. +Оба элемента реализуют интефейс MediaPlayer. Большинство свойств и все события AudioPlayer и VideoPlayer +являются общими и реализуются через MediaPlayer. + +### Свойство "src" + +Свойство "src" (константа Source) задает один или несколько источников медиафайлов. Свойство "src" может принимать +значение следующих типов: + +* string, +* MediaSource, +* []MediaSource. + +Структура MediaSource объявлена как + + type MediaSource struct { + Url string + MimeType string + } + +где Url - обязательный параметр, MimeType - необязательный mime тип файла + +Так как разные браузеры поддерживают разные форматы файлов и кодеков, то рекомендуется задавать несколько +источников в разных форматах. Плеер сам выбирает из списка источников наиболее подходящий. Задание mime +типов облегчает браузеру этот процесс + +### Свойство "controls" + +Свойство "controls" (константа Controls) типа bool указывает, должны ли отображаться элементы пользовательского +интерфейса для управления воспроизведения медиа ресурса. Значение по умолчанию false. + +Если свойство "controls" равно false для AudioPlayer, то он будет невидим и не будет занимаеть место на экране. + +### Свойство "loop" + +Свойство "loop" (константа Loop) типа bool. Если оно установлено в true, то медиа-файл начинаться сначала, +когда он достигает конца. Значение по умолчанию false. + +### Свойство "muted" + +Свойство "muted" (константа Muted) типа bool включает (true) / выключает (false) беззвучный режим. Значение по умолчанию false. + +### Свойство "preload" + +Свойство "preload" (константа Preload) типа int определяет какие данные должны быть предварительно загружены, если таковые имеются. +Допустимые значения: + +| Значение | Константа | Имя | Значение | +|:--------:|-----------------|------------|----------------------------------------------------------------------------------------| +| 0 | PreloadNone | "none" | Медиа файл не должен быть предварительно загружен | +| 1 | PreloadMetadata | "metadata" | Предварительно загружаются только метаданные | +| 2 | PreloadAuto | "auto" | Весь медиафайл может быть загружен, даже если пользователь не должен его использовать. | + +Значение по умолчанию PreloadAuto (2) + +### Свойство "poster" + +Свойство "poster" (константа Poster) типа string используется только для VideoPlayer. +Оно задает url картинки которая будет показываться пока видео не загрузится. +Если данное свойство не задано, то будет сначала показываться черный экран, а затем первый кадр (как только он загрузится). + +### Свойства "video-width" и "video-height" + +Свойства "video-width" (константа VideoWidth) и "video-height" (константа VideoHeight) типа float64 используется только для VideoPlayer. +Оно определяет ширину и высоту выводимого видео в пикселях. + +Если "video-width" и "video-height" не заданы, то используются реальные размеры видео, при этом размеры контейнера в +который помещено видео игнорируются и видео может перекрывать другие элементы интерфейса. Поэтому рекоментуется задавать +эти величины, например, так + + rui.Set(view, "videoPlayerContainer", rui.ResizeEvent, func(frame rui.Frame) { + rui.Set(view, "videoPlayer", rui.VideoWidth, frame.Width) + rui.Set(view, "videoPlayer", rui.VideoHeight, frame.Height) + }) + +Если задано только одно из свойств "video-width" или "video-height", то второе вычисляется на основе пропорций видео + +### События + +MediaPlayer имеет две группы событий: + +1) имеет обработчик вида func(MediaPlayer) (также можно использовать func()). в эту группу входят следующие события + +* "abort-event" (константа AbortEvent) - Срабатывает, когда ресурс загружен не полностью, но не в результате ошибки. + +* "can-play-event" (константа CanPlayEvent) - Запускается, когда пользовательский агент может воспроизводить +мультимедиа, но оценивает, что загружено недостаточно данных для воспроизведения мультимедиа до его конца +без необходимости остановки для дальнейшей буферизации контента. + +* "can-play-through-event" (константа CanPlayThroughEvent) - Запускается, когда пользовательский агент может воспроизводить мультимедиа, +и оценивает, что было загружено достаточно данных для воспроизведения мультимедиа до его конца, без необходимости +остановки для дальнейшей буферизации контента. + +* "complete-event" (константа CompleteEvent) - + +* "emptied-event" (константа EmptiedEvent) - Запускается, когда носитель становится пустым; например, когда носитель +уже загружен (или частично загружен) + +* "ended-event" (константа EndedEvent) - Срабатывает, когда воспроизведение останавливается, когда достигнут конец носителя +или если дальнейшие данные недоступны. + +* "loaded-data-event" (константа LoadedDataEvent) - Запускается, когда первый кадр носителя завершил загрузку. + +* "loaded-metadata-event" (константа LoadedMetadataEvent) - Запускается, когда метаданные были загружены. + +* "loadstart-event" (константа LoadstartEvent) - Запускается, когда браузер начал загружать ресурс. + +* "pause-event" (константа PauseEvent) - Вызывается, когда обрабатывается запрос на приостановку воспроизведения, +и действие переходит в состояние паузы, чаще всего это происходит, когда вызывается метод Pause(). + +* "play-event" (константа PlayEvent) - Срабатывает, когда начинается воспроизведение медиа файла, например, +в результате использования метода Play() + +* "playing-event" (константа PlayingEvent) - Запускается, когда воспроизведение готово начать после приостановки +или задержки из-за отсутствия данных. + +* "progress-event" (константа ProgressPvent) - Периодически запускается, когда браузер загружает ресурс. + +* "seeked-event" (константа SeekedEvent) - Запускается, когда скорость воспроизведения изменилась. + +* "seeking-event" (константа SeekingEvent) - Запускается, когда начинается операция поиска. + +* "stalled-event" (константа StalledEvent) - Запускается, когда пользовательский агент пытается извлечь данные мультимедиа, +но данные неожиданно не поступают. + +* "suspend-event" (константа SuspendEvent) - Запускается, когда загрузка медиа-данных была приостановлена. + +* "waiting-event" (константа WaitingEvent) - Срабатывает, когда воспроизведение остановлено из-за временной нехватки данных + +2) имеет обработчик вида func(MediaPlayer, float64) (также можно использовать func(float64), func(MediaPlayer) и func()). +В эту группу входят события связанные с измененим парамеров плеера. В качестве второго аргумента передается +новое значение измененного параметра. + +* "duration-changed-event" (константа DurationChangedEvent) - запускается, когда атрибут продолжительности был обновлён. + +* "time-updated-event" (константа TimeUpdatedEvent) - запускается, когда текущее время было обновлено. + +* "volume-changed-event" (константа VolumeChangedEvent) - запускается при изменении громкости. + +* "rate-changed-event" (константа RateChangedEvent) - запускается, когда скорость воспроизведения изменилась. + +Отдельное событие, не относящееся к этим двум группам, "player-error-event" (константа PlayerErrorEvent) срабатывает, +когда ресурс не может быть загружен из-за ошибки (например, ошибки сети). + +Обработчик данного события имеет вида func(player MediaPlayer, code int, message string) (также можно использовать +func(int, string), func(MediaPlayer) и func()). Где аргумент "message" это сообщение об ошибке, "code" - код ошибки: + +| Код ошибки | Константа | Значение | +|:----------:|-------------------------------|------------------------------------------------------------------------------| +| 0 | PlayerErrorUnknown | Неизвестная ошибка | +| 1 | PlayerErrorAborted | Извлечение связанного ресурса было прервано запросом пользователя. | +| 2 | PlayerErrorNetwork | Произошла какая-то сетевая ошибка, которая помешала успешному извлечению носителя, несмотря на то, что он был ранее доступен. | +| 3 | PlayerErrorDecode | Несмотря на то, что ранее ресурс был определён, как используемый, при попытке декодировать медиаресурс произошла ошибка. | +| 4 | PlayerErrorSourceNotSupported | Связанный объект ресурса или поставщик мультимедиа был признан неподходящим. | + +### Методы + +MediaPlayer имеет ряд методов для управления параметрами плеера: + +* Play() - запускает воспроизведение медиа файла; + +* Pause() - ставит воспроизведение на паузу; + +* SetCurrentTime(seconds float64) - устанавливает текущее время воспроизведения в секундах; + +* CurrentTime() float64 - возвращает текущее время воспроизведения в секундах; + +* Duration() float64 - возвращает длительность медиа файла в секундах; + +* SetPlaybackRate(rate float64) - устанавливает скорость воспроизведения. Нормальная скорость равна 1.0; + +* PlaybackRate() float64 - возвращает текущую скорость воспроизведения; + +* SetVolume(volume float64) - устанавливает скорост громкость в диапазоне от 0 (тишина) до 1 (максимальная громкость); + +* Volume() float64 - возвращает текущую громкость; + +* IsEnded() bool - возвращает true если достигнут конец медиа файла; + +* IsPaused() bool - возвращает true если воспроизведение поставлено на паузу. + +Для быстрого доступа к этим методам имеются глобальные функции: + + func MediaPlayerPlay(view View, playerID string) + func MediaPlayerPause(view View, playerID string) + func SetMediaPlayerCurrentTime(view View, playerID string, seconds float64) + func MediaPlayerCurrentTime(view View, playerID string) float64 + func MediaPlayerDuration(view View, playerID string) float64 + func SetMediaPlayerVolume(view View, playerID string, volume float64) + func MediaPlayerVolume(view View, playerID string) float64 + func SetMediaPlayerPlaybackRate(view View, playerID string, rate float64) + func MediaPlayerPlaybackRate(view View, playerID string) float64 + func IsMediaPlayerEnded(view View, playerID string) bool + func IsMediaPlayerPaused(view View, playerID string) bool + +где view - корневой View, playerID - id of AudioPlayer or VideoPlayer + +## Сессия + +Когда клиент создает соединение с сервером, то для этого соединения создается интерфейс Session. +Этот интерфейс используется для взаимодействия с клиентом. +Получить текущий интерфейс Session можно вызвав метод Session() интерфейса View. + +При создани сессии она получает пользовательскую реализацию интерфейса SessionContent. + + type SessionContent interface { + CreateRootView(session rui.Session) rui.View + } + +Данный интерфейс создается функцией передаваемой в качестве параметра при создании приложения +функцией NewApplication. + +Кроме обязательной функции CreateRootView() SessionContent может иметь несколько опциональных +функций: + + OnStart(session rui.Session) + OnFinish(session rui.Session) + OnResume(session rui.Session) + OnPause(session rui.Session) + OnDisconnect(session rui.Session) + OnReconnect(session rui.Session) + +Сразу после создания сессии вызывается функция CreateRootView. После создания корневого View +вызывается функцию OnStart (если она реализована) + +Функция OnFinish (если она реализована) вызывается когда пользователь закрывет страницу приложения в браузере + +Функция OnPause вызывается когда страница приложения в браузере клиента становится неактивной. +Это происходит если пользователь переключается на другую вкладку/окно браузера, сворачивает браузер +или переключается на другое приложение. + +Функция OnResume вызывается когда страница приложения в браузере клиента становится активной. Так же +эта функция вызывается сразу после OnStart + +Функция OnDisconnect вызывается если сервер теряет соединение с клиентом. Это происходит либо при +обрыве связи. + +Функция OnReconnect вызывается после того как сервер восстанавливает соединение с клиентом. + +Интерфейс Session предоставляет следующие методы: + +* DarkTheme() bool - возвращает true, если используется темная тема. Определяется настройками на стороне клиента + +* TouchScreen() bool - возвращает true, если клиент поддерживает touch screen + +* PixelRatio() float64 - возвращает размер логического пикселя, т.е. сколько физических пикселей образуют логический. Например, для iPhone это значение будет 2 или 3 + +* TextDirection() int - возвращает напрвление письма: LeftToRightDirection (1) или RightToLeftDirection (2) + +* Constant(tag string) (string, bool) - возвращает значение константы + +* Color(tag string) (Color, bool) - возвращает значение константы цвета + +* SetCustomTheme(name string) bool - устанавливает тему заданым именем в качестве текущей. Возвращает false если тема с таким именем не найдена. Темы с именем "" это тема по умолчанию. + +* Language() string - возвращает текущий язык интерфейса, например: "en", "ru", "ptBr" + +* SetLanguage(lang string) - устанавливает текущий язык интерфейса (см. "Поддержка нескольких языков") + +* GetString(tag string) (string, bool) - возвращает текстовое текстовое значение для текущего языка +(см. "Поддержка нескольких языков") + +* Content() SessionContent - возвращает текущий экземпляр SessionContent + +* RootView() View - возвращает корневой View сессии + +* Get(viewID, tag string) interface{} - возвращает значение свойства View с именем tag. Эквивалентно + + rui.Get(session.RootView(), viewID, tag) + +* Set(viewID, tag string, value interface{}) bool - устанавливает значение свойства View с именем tag. + + rui.Set(session.RootView(), viewID, tag, value) + +## Формат описания ресурсов + +Ресурсы приложения (темы, View, переводы) могут быть описаны в виде текста (utf-8). Данный текст помещается +в файл с расширением ".rui". + +Корневым элементом файла ресурса должен быть объект. Он имеет следующий формат: + + <имя объекта> { + <данные объекта> + } + +если имя объекта содержит следующие символы: '=', '{', '}', '[', ']', ',', ' ', '\t', '\n', '\'', '"', +'`', '/' и любые пробелы, то имя объекта необходимо брать в кавычки. Если эти символы не используются, +то кавычки не обязательны. + +Можно использовать три вида ковычек: + +* "…" - эквивалентна такой же строке в языке go, т.е. внутри можно использовать escape последовательности: +\n, \r, \\, \", \', \0, \t, \x00, \u0000 + +* '…' - аналогична строке "…" + +* `…` - эквивалентна такой же строке в языке go, т.е. текст внутри этой строки остается как есть. Внутри +нельзя использовать символ `. + +Данные объяекта представляют собой множество пар <ключ> = <значение> разделенные запятой. + +Ключ это строка текста. Правила оформления такие же как и у имени объекта. + +Значения могут быть 3 видов: + +* Простое значение - строка текста оформленная по тем же правилам, что и имя объекта + +* Объект + +* Массив значений + +Массив значений заключается в квадратные скобки. Элементы массива разделяются запятыми. +Элементами могут быть простые значения или объекты. + +В текста могут быть комментарии. Правила оформления такие же как в языке go: // и /* … */ + +Пример: + + GridLayout { + id = gridLayout, width = 100%, height = 100%, + cell-width = "150px, 1fr, 30%", cell-height = "25%, 200px, 1fr", + content = [ + // Subviews + TextView { row = 0, column = 0:1, + text = "View 1", text-align = center, vertical-align = center, + background-color = #DDFF0000, radius = 8px, padding = 32px, + border = _{ style = solid, width = 1px, color = #FFA0A0A0 } + }, + TextView { row = 0:1, column = 2, + text = "View 2", text-align = center, vertical-align = center, + background-color = #DD00FF00, radius = 8px, padding = 32px, + border = _{ style = solid, width = 1px, color = #FFA0A0A0 } + }, + TextView { row = 1:2, column = 0, + text = "View 3", text-align = center, vertical-align = center, + background-color = #DD0000FF, radius = 8px, padding = 32px, + border = _{ style = solid, width = 1px, color = #FFA0A0A0 } + }, + TextView { row = 1, column = 1, + text = "View 4", text-align = center, vertical-align = center, + background-color = #DDFF00FF, radius = 8px, padding = 32px, + border = _{ style = solid, width = 1px, color = #FFA0A0A0 } + }, + TextView { row = 2, column = 1:2, + text = "View 5", text-align = center, vertical-align = center, + background-color = #DD00FFFF, radius = 8px, padding = 32px, + border = _{ style = solid, width = 1px, color = #FFA0A0A0 } + }, + ] + } + +Для работы с текстовыми ресурсами используется интерфейс DataNode + + type DataNode interface { + Tag() string + Type() int + Text() string + Object() DataObject + ArraySize() int + ArrayElement(index int) DataValue + ArrayElements() []DataValue + } + +Данный элемент описывает базовый элемент данных. + +Метод Tag возвращает значение ключа. + +Тип данных возвращается методом Type. Он возвращает одно из 3 значений + +| Значение | Константа | Тип данных | +|:--------:|------------|--------------------| +| 0 | TextNode | Простое значение | +| 1 | ObjectNode | Объект | +| 2 | ArrayNode | Массив | + +Для получения простого значения используется метод Text. +Для получения объекта используется метод Object. +Для получения элементов массива используюся методы ArraySize, ArrayElement и ArrayElements + +## Ресурсы + +Ресурсы (картинки, темы, переводы и т.д.) с которыми работает приложение должны размещаться по +поддиректориям внутри одного директория ресурсов. Ресурсы должны располагаться в следующих поддерикториях: + +* images - в данную поддиректорию помещаются все изображения. Здесь можно делать вложенные поддиректории. +В этом случае их надо включать в имя файла. Например, "subdir/image1.png" + +* themes - в данную поддиректорию помещаются темы приложения (см. ниже) + +* views - в данную поддиректорию помещаются описания View + +* strings - в данную поддиректорию помещаются переводы текстовых ресурсов (см. Поддержка нескольких языков) + +* raw - в данную поддиректорию помещаются все остальные ресурсы: звуки, видео, двоичные данные и т.п. + +Директория с ресурсами может или включаться в исполняемый файл или располагаться отдельно. + +Если ресурсы необходимо включить в исполняемый файл, то имя директории должно быть "resources" и +подключаться она должны следующим образом: + + import ( + "embed" + + "github.com/anoshenko/rui" + ) + + //go:embed resources + var resources embed.FS + + func main() { + rui.AddEmbedResources(&resources) + + app := rui.NewApplication("Hello world", createHelloWorldSession) + app.Start("localhost:8000") + } + +Если ресурсы поставляются в виде отдельной директории, то ее необходимо зарегистрировать +с помощью функции SetResourcePath до создания Application: + + func main() { + rui.SetResourcePath(path) + + app := rui.NewApplication("Hello world", createHelloWorldSession) + app.Start("localhost:8000") + } + +## Изображения для экранов с разной плотностью пикселей + +Если вам необходимо добавить в ресурсы отдельные изображения для экранов с разной плотностью пикселей, +то это делается в стиле iOS. Т.е. к имени файла добавляется '@<плотность>x'. Например + + image@2x.png + image@3x.jpg + image@1.5x.gif + +Например, у вас есть изображения для трех плотностей: image.png, image@2x.png и image@3x.png. +В этом случае полю "src" ImageView вы присваиваете только значение "image.png". Библиотека +сама найдет остальные в директории "images" и передаст клиенту изображение с +требуемой плотностью + +## Темы + +Тема включает в сябя три вида данных: +* константы +* константы цвета +* Стили View + +Темы оформляются в виде rui файла и помещаются в папку themes. + +Корневым объектом темы является объект с именем 'theme'. Данный объект может содержать следующие свойства: + +* name - текстовое свойство задающее имя темы. Если данное свойство не задано или оно равно пустой строке, +то это тема по умолчанию. + +* constants - свойство-объект определяющий константы. Имя объекта может быть любым. Рекомендуется использовать "_". +Объект может иметь любое количиство текстовых свойств задающих пару "имя константы" = "значение". +В данном разделе помещаются константы типа SizeUnit, AngleUnit, текстовые и числовые. Для того чтобы +присвоить константу какому либо свойству View надо свойству присвоить имя константы добавив вначале символ '@'. +Например + + theme { + constants = _{ + defaultPadding = 4px, + buttonPadding = @defaultPadding, + angle = 30deg, + } + } + + rui.Set(view, "subView", rui.Padding, "@defaultPadding") + +* constants:touch - свойство-объект определяющий константы используемые только для touch screen. +Например, как сделать отступы больше на touch screen: + + theme { + constants = _{ + defaultPadding = 4px, + }, + constants:touch = _{ + defaultPadding = 12px, + }, + } + +* colors - свойство-объект определяющий цветовые константы для светлой темы оформления (тема по умолчанию). +Объект может иметь любое количиство текстовых свойств задающих пару "имя цвета" = "цвет". Аналогично +константам, при присваивании необходимо вначале имени цвета добавить '@'. Например + + theme { + colors = _{ + textColor = #FF101010, + borderColor = @textColor, + backgroundColor = white, + } + } + + rui.Set(view, "subView", rui.TextColor, "@textColor") + +Имена цветов, такие как "black", "white", "red" и т.д., используются без символа '@'. При этом вы можете +задавать цветовые константы с такими же именами. Например + + theme { + colors = _{ + red = blue, + } + } + + rui.Set(view, "subView", rui.TextColor, "@red") // blue text + rui.Set(view, "subView", rui.TextColor, "red") // red text + +* colors:dark - свойство-объект определяющий цветовые константы для темной темы оформления + +* styles - массив общих стилей. Каждый элемент массива должен быть объектом. Имя объекта является ивляется +именем стиля. Например, + + theme { + styles = [ + demoPage { + width = 100%, + height = 100%, + cell-width = "1fr, auto", + }, + demoPanel { + width = 100%, + height = 100%, + orientation = start-to-end, + }, + ] + } + +Для использования стилей у View есть два текстовых свойства "style" (константа Style) и "style-disabled" +(константа StyleDisabled). Свойству "style" присваивается имя свойства которое применяется ко View при +значении свойства "disabled" равного false. Свойству "style-disabled" присваивается имя свойства +которое применяется ко View при значении свойства "disabled" равного true. Если "style-disabled" +не определен, то всойство "style" используется в обоих режимах. + +Внимание! Символ '@' к имени стиля добавлять НЕ НАДО. Если вы добавите символ '@' к имени, то имя +стиля будет извлекаться из одноименной константы. Например + + theme { + constants = _{ + @demoPanel = demoPage + }, + styles = [ + demoPage { + width = 100%, + height = 100%, + cell-width = "1fr, auto", + }, + demoPanel { + width = 100%, + height = 100%, + orientation = start-to-end, + }, + ] + } + + rui.Set(view, "subView", rui.Style, "demoPanel") // style == demoPanel + rui.Set(view, "subView", rui.Style, "@demoPanel") // style == demoPage + +Помимо общих стилей можно затать стили для отдельных режимов работы. Для этого к имени "styles" добавляются +следующие модификаторы: + +* ":portrait" или ":landscape" - соответственно стили для портретного или ладшафтного режима программы. +Внимание имеется ввиду соотношение сторон окна программы, а не экрана. + +* ":width<размер>" - стили для экрана ширина которого не превышает заданный размер в логических пикселях. + +* ":height<размер>" - стили для экрана высота которого не превышает заданный размер в логических пикселях. + +Например + + theme { + styles = [ + demoPage { + width = 100%, + height = 100%, + cell-width = "1fr, auto", + }, + demoPage2 { + row = 0, + column = 1, + } + ], + styles:landscape = [ + demoPage { + width = 100%, + height = 100%, + cell-height = "1fr, auto", + }, + demoPage2 { + row = 1, + column = 0, + } + ], + styles:portrait:width320 = [ + sapmplePage { + width = 100%, + height = 50%, + }, + ] + } + +## Стандартные константы и стили + +В библиотеке определен ряд констант и стилей. Вы их можете переопределять в своих темах. + +Системные стили которые вы можете переопределять: + +| Имя стиля | Описание | +|---------------------|-----------------------------------------------------------------------------| +| ruiApp | Данный стиль используется для назначения стиля текста (шрифт, размер и т.д.) по умолчанию | +| ruiView | Стиль View по умолчанию | +| ruiArticle | Стиль используемый если свойство "semantics" установлено в "article" | +| ruiSection | Стиль используемый если свойство "semantics" установлено в "section" | +| ruiAside | Стиль используемый если свойство "semantics" установлено в "aside" | +| ruiHeader | Стиль используемый если свойство "semantics" установлено в "header" | +| ruiMain | Стиль используемый если свойство "semantics" установлено в "main" | +| ruiFooter | Стиль используемый если свойство "semantics" установлено в "footer" | +| ruiNavigation | Стиль используемый если свойство "semantics" установлено в "navigation" | +| ruiFigure | Стиль используемый если свойство "semantics" установлено в "figure" | +| ruiFigureCaption | Стиль используемый если свойство "semantics" установлено в "figure-caption" | +| ruiButton | Стиль используемый если свойство "semantics" установлено в "button" | +| ruiParagraph | Стиль используемый если свойство "semantics" установлено в "paragraph" | +| ruiH1 | Стиль используемый если свойство "semantics" установлено в "h1" | +| ruiH2 | Стиль используемый если свойство "semantics" установлено в "h2" | +| ruiH3 | Стиль используемый если свойство "semantics" установлено в "h3" | +| ruiH4 | Стиль используемый если свойство "semantics" установлено в "h4" | +| ruiH5 | Стиль используемый если свойство "semantics" установлено в "h5" | +| ruiH6 | Стиль используемый если свойство "semantics" установлено в "h6" | +| ruiBlockquote | Стиль используемый если свойство "semantics" установлено в "blockquote" | +| ruiCode | Стиль используемый если свойство "semantics" установлено в "code" | +| ruiTable | Стиль TableView по умолчанию | +| ruiTableHead | Стиль заголовка TableView по умолчанию | +| ruiTableFoot | Стиль итого TableView по умолчанию | +| ruiTableRow | Стиль строки TableView по умолчанию | +| ruiTableColumn | Стиль колонки TableView по умолчанию | +| ruiTableCell | Стиль ячейки TableView по умолчанию | +| ruiDisabledButton | Стиль Button если свойство "disabled" установлено в true | +| ruiCheckbox | Стиль Checkbox | +| ruiListItem | Стиль пункта ListView | +| ruiListItemSelected | Стиль выбранного пункта ListView когда ListView не владеет фокусом | +| ruiListItemFocused | Стиль выбранного пункта ListView когда ListView владеет фокусом | +| ruiPopup | Стиль всплывающего окна | +| ruiPopupTitle | Стиль заголовка всплывающего окна | +| ruiMessageText | Стиль текста всплывающего окна (Message, Question) | +| ruiPopupMenuItem | Стиль пункта всплывающего меню | + +Системные цвета которые вы можете переопределять: + +| Имя константы цвета | Описание | +|----------------------------|----------------------------------------------------| +| ruiBackgroundColor | Цвет фона | +| ruiTextColor | Цвет текста | +| ruiDisabledTextColor | Цвет запрещенного текста | +| ruiHighlightColor | Цвет подсветки | +| ruiHighlightTextColor | Цвет подсвеченного текста | +| ruiButtonColor | Цвет кнопки | +| ruiButtonActiveColor | Цвет кнопки в фокусе | +| ruiButtonTextColor | Цвет текста кнопки | +| ruiButtonDisabledColor | Цвет запрещенной кнопки | +| ruiButtonDisabledTextColor | Цвет текста запрещенной кнопки | +| ruiSelectedColor | Цвет фона неактивного выбранного пункта ListView | +| ruiSelectedTextColor | Цвет текста неактивного выбранного пункта ListView | +| ruiPopupBackgroundColor | Цвет фона всплывающего окна | +| ruiPopupTextColor | Цвет текста всплывающего окна | +| ruiPopupTitleColor | Цвет фона заголовка всплывающего окна | +| ruiPopupTitleTextColor | Цвет текста заголовка всплывающего окна | + +Константы которые вы можете переопределять: + +| Имя константы | Описание | +|------------------------------|-----------------------------------------------| +| ruiButtonHorizontalPadding | Горизонтальный отступ внутри кнопки | +| ruiButtonVerticalPadding | Вертикальный отступ внутри кнопки | +| ruiButtonMargin | Внешний тоступ кнопки | +| ruiButtonRadius | Радиус скругления углов кнопки | +| ruiButtonHighlightDilation | Ширина внешней рамки активной кнопки | +| ruiButtonHighlightBlur | Размытие рамки активной кнопки | +| ruiCheckboxGap | Разрыв между checkbox и содержимым | +| ruiListItemHorizontalPadding | Горизонтальный отступ внутри пункта ListView | +| ruiListItemVerticalPadding | Вертикальный отступ внутри пункта ListView | +| ruiPopupTitleHeight | Высота заголовка всплывающего окна | +| ruiPopupTitlePadding | Внутренний отступ заголовка всплывающего окна | +| ruiPopupButtonGap | Разрыв между кнопками всплывающего окна | + +## Поддержка нескольких языков + +Если вы хотите добавить в программу поддержку нескольких языков, то необходимо поместить в папку "strings" ресурсов +файлы с переводом. Файлы перевода должны иметь расширение "rui" и следующий формат + + strings { + <язык 1> = _{ + <Исходный текст 1> = <Перевод 1>, + <Исходный текст 2> = <Перевод 2>, + … + }, + <язык 2> = _{ + <Исходный текст 1> = <Перевод 1>, + <Исходный текст 2> = <Перевод 2>, + … + }, + … + } + +Если перевод на каждый язык помещается в отдельный файл, то можно использовать следующий формат + + strings:<язык> { + <Исходный текст 1> = <Перевод 1>, + <Исходный текст 2> = <Перевод 2>, + … + } + +Например, если все переводы в одном файле strings.rui + + strings { + ru = _{ + "Yes" = "Да", + "No" = "Нет", + }, + de = _{ + "Yes" = "Ja", + "No" = "Nein", + }, + } + +Если в разных. Файл ru.rui + + strings:ru { + "Yes" = "Да", + "No" = "Нет", + } + +Файл de.rui + + strings:de { + "Yes" = "Ja", + "No" = "Nein", + } + +Перевод можно также разбивать на несколько файлов. + +Переводы автоматически подставляются во всех View. + +Однако если вы рисуете текст в CanvasView, то вы должны запрашивать перевод сами. Для этого в интерфейсе Session +есть метод: + + GetString(tag string) (string, bool) + +Если перевода данной строки нет, то метод вернет исходную строку и false в качестве второго параметра. + +Получить текущий язык можно с помощью метода Language() интерфейса Session. Текущий язык определяется настройками +браузера пользователя. Поменять язык сессии можно спомощью метода SetLanguage(lang string) интерфейса Session. + diff --git a/README.md b/README.md index cc02696..6909455 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4311 @@ -# rui -golang library for creating web application +# RUI library + +The RUI (Remoute User Interface) library is designed to create web applications in the go language. + +The peculiarity of the library is that all data processing is carried out on the server, +and the browser is used as a thin client. WebSocket is used for client-server communication. + +## Hello world + + type helloWorldSession struct { + } + + func (content *helloWorldSession) CreateRootView(session rui.Session) rui.View { + return rui.NewTextView(session, rui.Params { + rui.Text : "Hello world!!!", + }) + } + + func createHelloWorldSession(session rui.Session) rui.SessionContent { + return new(helloWorldSession) + } + + func main() { + app := rui.NewApplication("Hello world", "icon.svg", createHelloWorldSession) + app.Start("localhost:8000") + } + +In the main function, a rui application is created and the main loop is started. +When creating an application, 3 parameters are set: the name of the application, the name of the icon, and the createHelloWorldSession function. +The createHelloWorldSession function creates a structure that implements the SessionContent interface: + + type SessionContent interface { + CreateRootView(session rui.Session) rui.View + } + +A new instance of the helloWorldSession structure is created for each new session, + +The CreateRootView function of the SessionContent interface creates a root element. +When the user accesses the application by typing the address "localhost: 8000" in the browser, a new session is created. +A new instance of the helloWorldSession structure is created for it, and at the end the CreateRootView function is called. +The createRootView function returns a representation of a text that is created using the NewTextView function. + +If you want the application to be visible outside your computer, then change the address in the Start function: + + app.Start(rui.GetLocalIP() + ":80") + +## Used data types + +### SizeUnit + +The SizeUnit structure is used to set various sizes of interface elements such as width, height, padding, font size, etc. +SizeUnit is declared as + + type SizeUnit struct { + Type SizeUnitType + Value float64 + } + +where Type is the type of size; Value is the size. + +The Type can take the following values: + +| Value | Constant | Description | +|:--------:|----------------|--------------------------------------------------------------------------------| +| 0 | Auto | default value. The Value field is ignored | +| 1 | SizeInPixel | the Value field specifies the size in pixels. | +| 2 | SizeInEM | the Value field specifies the size in em units. 1em is equal to the base font size, which is set in the browser settings | +| 3 | SizeInEX | the Value field specifies the size in ex units. | +| 4 | SizeInPercent | the Value field specifies the size as a percentage of the parent element size. | +| 5 | SizeInPt | the Value field specifies the size in pt units (1pt = 1/72"). | +| 6 | SizeInPc | the Value field specifies the size in pc units (1pc = 12pt). | +| 7 | SizeInInch | the Value field specifies the size in inches. | +| 8 | SizeInMM | the Value field specifies the size in millimeters. | +| 9 | SizeInCM | the Value field defines the size in centimeters. | +| 10 | SizeInFraction | the Value field specifies the size in parts. Used only for sizing cells of the GridLayout. | + +For a more visual and simple setting of variables of the SizeUnit type, the functions below can be used. + +| Function | Equivalent definition | +|----------------|----------------------------------------------------| +| rui.AutoSize() | rui.SizeUnit{ Type: rui.Auto, Value: 0 } | +| rui.Px(n) | rui.SizeUnit{ Type: rui.SizeInPixel, Value: n } | +| rui.Em(n) | rui.SizeUnit{ Type: rui.SizeInEM, Value: n } | +| rui.Ex(n) | rui.SizeUnit{ Type: rui.SizeInEX, Value: n } | +| rui.Percent(n) | rui.SizeUnit{ Type: rui.SizeInPercent, Value: n } | +| rui.Pt(n) | rui.SizeUnit{ Type: rui.SizeInPt, Value: n } | +| rui.Pc(n) | rui.SizeUnit{ Type: rui.SizeInPc, Value: n } | +| rui.Inch(n) | rui.SizeUnit{ Type: rui.SizeInInch, Value: n } | +| rui.Mm(n) | rui.SizeUnit{ Type: rui.SizeInMM, Value: n } | +| rui.Cm(n) | rui.SizeUnit{ Type: rui.SizeInCM, Value: n } | +| rui.Fr(n) | rui.SizeUnit{ Type: rui.SizeInFraction, Value: n } | + +Variables of the SizeUnit type have a textual representation (why you need it will be described below). +The textual representation consists of a number (equal to the value of the Value field) followed by +a suffix defining the type. An exception is a value of type Auto, which has the representation “auto”. +The suffixes are listed in the following table: + +| Suffix | Type | +|:------:|----------------| +| px | SizeInPixel | +| em | SizeInEM | +| ex | SizeInEX | +| % | SizeInPercent | +| pt | SizeInPt | +| pc | SizeInPc | +| in | SizeInInch | +| mm | SizeInMM | +| cm | SizeInCM | +| fr | SizeInFraction | + +Examples: auto, 50%, 32px, 1.5in, 0.8em + +To convert the textual representation to the SizeUnit structure, is used the function: + + func StringToSizeUnit(value string) (SizeUnit, bool) + +You can get a textual representation of the structure using the String() function of SizeUnit structure + +### Color + +The Color type describes a 32-bit ARGB color: + + type Color uint32 + +The Color type has three types of text representations: + +1) #AARRGGBB, #RRGGBB, #ARGB, #RGB + +where A, R, G, B is a hexadecimal digit describing the corresponding component. If the alpha channel is not specified, +then it is considered equal to FF. If the color component is specified by one digit, then it is doubled. +For example, “# 48AD” is equivalent to “# 4488AADD” + +2) argb(A, R, G, B), rgb(R, G, B) + +where A, R, G, B is the representation of the color component. The component can be west as a float number in the range [0 … 1], +or as an integer in the range [0 … 255], or as a percentage from 0% to 100%. + +Examples: + + “argb(255, 128, 96, 0)” + “rgb(1.0, .5, .8)” + “rgb(0%, 50%, 25%)” + “argb(50%, 128, .5, 100%)” + +The String function is used to convert a Color to a string. +To convert a string to Color, is used the function: + + func StringToColor(value string) (Color, bool) + +3) The name of the color. The RUI library defines the following colors + +| Name | Color | +|-----------------------|-----------| +| black | #ff000000 | +| silver | #ffc0c0c0 | +| gray | #ff808080 | +| white | #ffffffff | +| maroon | #ff800000 | +| red | #ffff0000 | +| purple | #ff800080 | +| fuchsia | #ffff00ff | +| green | #ff008000 | +| lime | #ff00ff00 | +| olive | #ff808000 | +| yellow | #ffffff00 | +| navy | #ff000080 | +| blue | #ff0000ff | +| teal | #ff008080 | +| aqua | #ff00ffff | +| orange | #ffffa500 | +| aliceblue | #fff0f8ff | +| antiquewhite | #fffaebd7 | +| aquamarine | #ff7fffd4 | +| azure | #fff0ffff | +| beige | #fff5f5dc | +| bisque | #ffffe4c4 | +| blanchedalmond | #ffffebcd | +| blueviolet | #ff8a2be2 | +| brown | #ffa52a2a | +| burlywood | #ffdeb887 | +| cadetblue | #ff5f9ea0 | +| chartreuse | #ff7fff00 | +| chocolate | #ffd2691e | +| coral | #ffff7f50 | +| cornflowerblue | #ff6495ed | +| cornsilk | #fffff8dc | +| crimson | #ffdc143c | +| cyan | #ff00ffff | +| darkblue | #ff00008b | +| darkcyan | #ff008b8b | +| darkgoldenrod | #ffb8860b | +| darkgray | #ffa9a9a9 | +| darkgreen | #ff006400 | +| darkgrey | #ffa9a9a9 | +| darkkhaki | #ffbdb76b | +| darkmagenta | #ff8b008b | +| darkolivegreen | #ff556b2f | +| darkorange | #ffff8c00 | +| darkorchid | #ff9932cc | +| darkred | #ff8b0000 | +| darksalmon | #ffe9967a | +| darkseagreen | #ff8fbc8f | +| darkslateblue | #ff483d8b | +| darkslategray | #ff2f4f4f | +| darkslategrey | #ff2f4f4f | +| darkturquoise | #ff00ced1 | +| darkviolet | #ff9400d3 | +| deeppink | #ffff1493 | +| deepskyblue | #ff00bfff | +| dimgray | #ff696969 | +| dimgrey | #ff696969 | +| dodgerblue | #ff1e90ff | +| firebrick | #ffb22222 | +| floralwhite | #fffffaf0 | +| forestgreen | #ff228b22 | +| gainsboro | #ffdcdcdc | +| ghostwhite | #fff8f8ff | +| gold | #ffffd700 | +| goldenrod | #ffdaa520 | +| greenyellow | #ffadff2f | +| grey | #ff808080 | +| honeydew | #fff0fff0 | +| hotpink | #ffff69b4 | +| indianred | #ffcd5c5c | +| indigo | #ff4b0082 | +| ivory | #fffffff0 | +| khaki | #fff0e68c | +| lavender | #ffe6e6fa | +| lavenderblush | #fffff0f5 | +| lawngreen | #ff7cfc00 | +| lemonchiffon | #fffffacd | +| lightblue | #ffadd8e6 | +| lightcoral | #fff08080 | +| lightcyan | #ffe0ffff | +| lightgoldenrodyellow | #fffafad2 | +| lightgray | #ffd3d3d3 | +| lightgreen | #ff90ee90 | +| lightgrey | #ffd3d3d3 | +| lightpink | #ffffb6c1 | +| lightsalmon | #ffffa07a | +| lightseagreen | #ff20b2aa | +| lightskyblue | #ff87cefa | +| lightslategray | #ff778899 | +| lightslategrey | #ff778899 | +| lightsteelblue | #ffb0c4de | +| lightyellow | #ffffffe0 | +| limegreen | #ff32cd32 | +| linen | #fffaf0e6 | +| magenta | #ffff00ff | +| mediumaquamarine | #ff66cdaa | +| mediumblue | #ff0000cd | +| mediumorchid | #ffba55d3 | +| mediumpurple | #ff9370db | +| mediumseagreen | #ff3cb371 | +| mediumslateblue | #ff7b68ee | +| mediumspringgreen | #ff00fa9a | +| mediumturquoise | #ff48d1cc | +| mediumvioletred | #ffc71585 | +| midnightblue | #ff191970 | +| mintcream | #fff5fffa | +| mistyrose | #ffffe4e1 | +| moccasin | #ffffe4b5 | +| navajowhite | #ffffdead | +| oldlace | #fffdf5e6 | +| olivedrab | #ff6b8e23 | +| orangered | #ffff4500 | +| orchid | #ffda70d6 | +| palegoldenrod | #ffeee8aa | +| palegreen | #ff98fb98 | +| paleturquoise | #ffafeeee | +| palevioletred | #ffdb7093 | +| papayawhip | #ffffefd5 | +| peachpuff | #ffffdab9 | +| peru | #ffcd853f | +| pink | #ffffc0cb | +| plum | #ffdda0dd | +| powderblue | #ffb0e0e6 | +| rosybrown | #ffbc8f8f | +| royalblue | #ff4169e1 | +| saddlebrown | #ff8b4513 | +| salmon | #fffa8072 | +| sandybrown | #fff4a460 | +| seagreen | #ff2e8b57 | +| seashell | #fffff5ee | +| sienna | #ffa0522d | +| skyblue | #ff87ceeb | +| slateblue | #ff6a5acd | +| slategray | #ff708090 | +| slategrey | #ff708090 | +| snow | #fffffafa | +| springgreen | #ff00ff7f | +| steelblue | #ff4682b4 | +| tan | #ffd2b48c | +| thistle | #ffd8bfd8 | +| tomato | #ffff6347 | +| turquoise | #ff40e0d0 | +| violet | #ffee82ee | +| wheat | #fff5deb3 | +| whitesmoke | #fff5f5f5 | +| yellowgreen | #ff9acd32 | + +### AngleUnit + +The AngleUnit type is used to set angular values. AngleUnit is declared as + + type AngleUnit struct { + Type AngleUnitType + Value float64 + } + +where Type is the type of angular value; Value is the angular value + +The Type can take the following values: + +* Radian (0) - the Value field defines the angular value in radians. +* PiRadian (1) - the Value field defines the angular value in radians multiplied by π. +* Degree (2) - the Value field defines the angular value in degrees. +* Gradian (3) - the Value field defines the angular value in grades (gradians). +* Turn (4) - the Value field defines the angular value in turns (1 turn == 360°). + +For a more visual and simple setting of variables of the AngleUnit type, the functions below can be used. + +| Function | Equivalent definition | +|--------------|-----------------------------------------------| +| rui.Rad(n) | rui.AngleUnit{ Type: rui.Radian, Value: n } | +| rui.PiRad(n) | rui.AngleUnit{ Type: rui.PiRadian, Value: n } | +| rui.Deg(n) | rui.AngleUnit{ Type: rui.Degree, Value: n } | +| rui.Grad(n) | rui.AngleUnit{ Type: rui.Gradian, Value: n } | + +Variables of type AngleUnit have a textual representation consisting of a number (equal to the value of the Value field) +followed by a suffix defining the type. The suffixes are listed in the following table: + +| Suffix | Type | +|:-------:|----------| +| deg | Degree | +| ° | Degree | +| rad | Radian | +| π | PiRadian | +| pi | PiRadian | +| grad | Gradian | +| turn | Turn | + +Examples: “45deg”, “90°”, “3.14rad”, “2π”, “0.5pi” + +The String function is used to convert AngleUnit to a string. +To convert a string to AngleUnit is used the function: + + func StringToAngleUnit(value string) (AngleUnit, bool) + +## View + +View is an interface for accessing an element of "View" type. View is a rectangular area of the screen. +All interface elements extend the View interface, i.e. View is the base element for all other elements in the library. + +View has a number of properties like height, width, color, text parameters, etc. Each property has a text name. +The Properties interface is used to read and write the property value (View implements this interface): + + type Properties interface { + Get(tag string) interface{} + Set(tag string, value interface{}) bool + Remove(tag string) + Clear() + AllTags() []string + } + +The Get function returns the value of the property, or nil if the property is not set. + +The Set function sets the value of a property. If the property value is set successfully, then +the function returns true, if not, then false and a description of the error that occurred is written to the log. + +The Remove function removes property value, equivalent to Set(nil) + +To simplify setting / reading properties, there are also two global functions Get and Set: + + func Get(rootView View, viewID, tag string) interface{} + func Set(rootView View, viewID, tag string, value interface{}) bool + +These functions get/set the value of the child View + +### Events + +When interacting with the application, various events arise: clicks, resizing, changing input data, etc. + +Event listeners are designed to respond to events. A listener is a function that is called every time an event occurs. +Each event can have multiple listeners. Let's analyze the listeners using the example of the "edit-text-changed" +text change event in the "EditView" editor. + +The event listener is a function of the form + + func([, ]) + +where the first argument is the View in which the event occurred. Further there are additional parameters of the event. + +For "edit-text-changed", the main listener will look like this: + + func(EditView, string) + +where the second argument is the new text value + +If you do not plan to use the first argument, you can omit it. This will be an additional listener + + func(string) + +In order to assign a listener, you must assign it to a property with the event name + + view.Set(rui.EditTextChanged, func(edit EditView, newText string) { + // do something + }) + +or + + view.Set(rui.EditTextChanged, func(newText string) { + // do something + }) + +Each event can have multiple listeners. In this regard, five data types can be used as listeners: + +* func(< View >[, < parameters >]) +* func([< parameters>]) +* []func(< View >[, < parameters >]) +* []func([< parameters >]) +* []interface{} which only contains func(< View >[, < parameters >]) and func([< parameters >]) + +After being assigned to a property, all these types are converted to an array of []func(< View >, [< parameters >]). +Accordingly, the Get function always returns an array of []func(< View >, [< parameters >]). +If there are no listeners, this array will be empty. + +For the "edit-text-changed" event, this + +* func(editor EditView, newText string) +* func(newText string) +* []func(editor EditView, newText string) +* []func(newText string) +* []interface{} содержащий только func(editor EditView, newText string) и func(newText string) + +And the "edit-text-changed" property always stores and returns []func(EditView, string). + +In what follows, when describing specific events, only the format of the main listener will be presented. + +### "id" property + +The "id" property is an optional textual identifier for the View. With it, you can find the child View. +To do this, use the ViewByID function + + func ViewByID(rootView View, id string) View + +This function looks for a child View with id. The search starts from rootView. +If View is not found, the function returns nil and an error message is written to the log. + +Usually id is set when the View is created and is not changed later. +But this is an optional condition. You can change the id at any time. + +The Set function is used to set a new value for id. For example + + view.Set(rui.ID, "myView") + view.Set("id", "myView") + +There are two ways to get the id. The first is using the Get function: + + if value := view.Get(rui.ID); value != nil { + id = value.(string) + } + +And the second one is using the ID() function: + + id = view.ID() + +### "width", "height", "min-width", "min-height", "max-width", "max-height" properties + +These properties are set: + +| Property | Constant | Description | +|--------------|---------------|----------------------------| +| "width" | rui.Width | The width of View | +| "height" | rui.Height | The height of View | +| "min-width" | rui.MinWidth | The minimum width of View | +| "min-height" | rui.MinHeight | The minimum height of View | +| "max-width" | rui.MaxWidth | The maximum width of View | +| "max-height" | rui.MaxHeight | The maximum height of View | + +These properties are of type SizeUnit. +If the "width" / "height" value is not set or is set to Auto, then the height/width of the View +is determined by its content and limited to the minimum and maximum height/width. +As the value of these properties, you can set the SizeUnit structure, the textual representation of the SizeUnit, +or the name of the constant (about the constants below): + + view.Set("width", rui.Px(8)) + view.Set(rui.MaxHeight, "80%") + view.Set(rui.Height, "@viewHeight") + +After getting the value with the Get function, you must typecast: + + if value := view.Get(rui.Width); value != nil { + switch value.(type) { + case string: + text := value.(string) + // TODO + + case SizeUnit: + size := value.(SizeUnit) + // TODO + } + } + +This is quite cumbersome, therefore for each property there is a global function of the same name with the Get prefix, +which performs the given cast, gets the value of the constant, if necessary, and returns it. +All functions of this type have two arguments: View and subviewID string. +The first argument is the root View, the second is the ID of the child View. +If the ID of the child View is passed as "", then the value of the root View is returned. +For the properties "width", "height", "min-width", "min-height", "max-width", "max-height" these are functions: + + func GetWidth(view View, subviewID string) SizeUnit + func GetHeight(view View, subviewID string) SizeUnit + func GetMinWidth(view View, subviewID string) SizeUnit + func GetMinHeight(view View, subviewID string) SizeUnit + func GetMaxWidth(view View, subviewID string) SizeUnit + func GetMaxHeight(view View, subviewID string) SizeUnit + +### "margin" и "padding" properties + +The "margin" property determines the outer margins from this View to its neighbors. +The "padding" property sets the padding from the border of the View to the content. +The values of the "margin" and "padding" properties are stored as the BoundsProperty interface, +which implements the Properties interface (see above). BoundsProperty has 4 SizeUnit properties: + +| Property | Constant | Description | +|-----------|-------------|-------------------| +| "top" | rui.Top | Top padding | +| "right" | rui.Right | Right padding | +| "bottom" | rui.Bottom | Bottom padding | +| "left" | rui.Left | Дуае padding | + +The NewBoundsProperty function is used to create the BoundsProperty interface. Example + + view.Set(rui.Margin, NewBoundsProperty(rui.Params { + rui.Top: rui.Px(8), + rui.Left: "@topMargin", + "right": "1.5em", + "bottom": rui.Inch(0.3), + }))) + +Accordingly, if you request the "margin" or "padding" property using the Get method, the BoundsProperty interface will return: + + if value := view.Get(rui.Margin); value != nil { + margin := value.(BoundsProperty) + } + +BoundsProperty using the "Bounds (session Session) Bounds" function of the BoundsProperty interface +can be converted to a more convenient Bounds structure: + + type Bounds struct { + Top, Right, Bottom, Left SizeUnit + } + +Global functions can also be used for this: + + func GetMargin(view View, subviewID string) Bounds + func GetPadding(view View, subviewID string) Bounds + +The textual representation of the BoundsProperty is as follows: + + "_{ top = , right = , bottom = , left = }" + +The value of the "margin" and "padding" properties can be passed to the Set method: +* BoundsProperty interface or its textual representation; +* Bounds structure; +* SizeUnit or the name of a constant of type SizeUnit, in which case this value is set to all indents. Those. + + view.Set(rui.Margin, rui.Px(8)) + +equivalent to + + view.Set(rui.Margin, rui.Bounds{Top: rui.Px(8), Right: rui.Px(8), Bottom: rui.Px(8), Left: rui.Px(8)}) + +Since the value of the "margin" and "padding" property is always stored as the BoundsProperty interface, +if you read the "margin" or "padding" property set by the Bounds or SizeUnit with the Get function, +then you get the BoundsProperty, not the Bounds or SizeUnit. + +The "margin" and "padding" properties are used to set four margins at once. +The following properties are used to set individual paddings: + +| Property | Constant | Description | +|------------------|-------------------|--------------------| +| "margin-top" | rui.MarginTop | The top margin | +| "margin-right" | rui.MarginRight | The right margin | +| "margin-bottom" | rui.MarginBottom | The bottom margin | +| "margin-left" | rui.MarginLeft | The left margin | +| "padding-top" | rui.PaddingTop | The top padding | +| "padding-right" | rui.PaddingRight | The right padding | +| "padding-bottom" | rui.PaddingBottom | The bottom padding | +| "padding-left" | rui.PaddingLeft | The left padding | + +Example + + view.Set(rui.Margin, rui.Px(8)) + view.Set(rui.TopMargin, rui.Px(12)) + +equivalent to + + view.Set(rui.Margin, rui.Bounds{Top: rui.Px(12), Right: rui.Px(8), Bottom: rui.Px(8), Left: rui.Px(8)}) + +### Свойство "border" + +The "border" property defines a border around the View. The frame line is described by three attributes: +line style, thickness and color. + +The value of the "border" property is stored as the BorderProperty interface, +which implements the Properties interface (see above). BorderProperty can contain the following properties: + +| Property | Constant | Type | Description | +|----------------|-------------|----------|--------------------------| +| "left-style" | LeftStyle | int | Left border line style | +| "right-style" | RightStyle | int | Right border line style | +| "top-style" | TopStyle | int | Top border line style | +| "bottom-style" | BottomStyle | int | Bottom border line style | +| "left-width" | LeftWidth | SizeUnit | Left border line width | +| "right-width" | RightWidth | SizeUnit | Right border line width | +| "top-width" | TopWidth | SizeUnit | Top border line width | +| "bottom-width" | BottomWidth | SizeUnit | Bottom border line width | +| "left-color" | LeftColor | Color | Left border line color | +| "right-color" | RightColor | Color | Right border line color | +| "top-color" | TopColor | Color | Top border line color | +| "bottom-color" | BottomColor | Color | Bottom border line color | + +Line style can take the following values: + +| Value | Constant | Name | Description | +|:-----:|------------|----------|---------------------| +| 0 | NoneLine | "none" | No frame | +| 1 | SolidLine | "solid" | Solid line | +| 2 | DashedLine | "dashed" | Dashed line | +| 3 | DottedLine | "dotted" | Dotted line | +| 4 | DoubleLine | "double" | Double solid line | + +All other style values are ignored. + +The NewBorder function is used to create the BorderProperty interface. + +If all the lines of the frame are the same, then the following properties can be used to set the style, thickness and color: + +| Property | Constant | Type | Description | +|-----------|----------|----------|---------------------| +| "style" | Style | int | Border line style | +| "width" | Width | SizeUnit | Border line width | +| "color" | Color | Color | Border line color | + +Example + + view.Set(rui.Border, NewBorder(rui.Params{ + rui.LeftStyle: rui.SolidBorder, + rui.RightStyle: rui.SolidBorder, + rui.TopStyle: rui.SolidBorder, + rui.BottomStyle: rui.SolidBorder, + rui.LeftWidth: rui.Px(1), + rui.RightWidth: rui.Px(1), + rui.TopWidth: rui.Px(1), + rui.BottomWidth: rui.Px(1), + rui.LeftColor: rui.Black, + rui.RightColor: rui.Black, + rui.TopColor: rui.Black, + rui.BottomColor: rui.Black, + })) + +equivalent to + + view.Set(rui.Border, NewBorder(rui.Params{ + rui.Style: rui.SolidBorder, + rui.Width: rui.Px(1), + rui.ColorProperty: rui.Black, + })) + +The BorderProperty interface can be converted to a ViewBorders structure using the Border function. +When converted, all text constants are replaced with real values. ViewBorders is described as + + type ViewBorders struct { + Top, Right, Bottom, Left ViewBorder + } + +where the ViewBorder structure is described as + + type ViewBorder struct { + Style int + Color Color + Width SizeUnit + } + +The ViewBorders structure can be passed as a parameter to the Set function when setting the value of the "border" property. +This converts the ViewBorders to BorderProperty. Therefore, when the property is read, +the Get function will return the BorderProperty interface, not the ViewBorders structure. +You can get the ViewBorders structure without additional transformations using the global function + + func GetBorder(view View, subviewID string) ViewBorders + +Besides the auxiliary properties "style", "width" and "color" there are 4 more: "left", "right", "top" and "bottom". +As a value, these properties can only take the ViewBorder structure and allow you to set all the attributes of the line of the side of the same name. + +You can also set individual frame attributes using the Set function of the View interface. +For this, the following properties are used + +| Property | Constant | Type | Description | +|-----------------------|-------------------|------------|--------------------------| +| "border-left-style" | BorderLeftStyle | int | Left border line style | +| "border-right-style" | BorderRightStyle | int | Right border line style | +| "border-top-style" | BorderTopStyle | int | Top border line style | +| "border-bottom-style" | BorderBottomStyle | int | Bottom border line style | +| "border-left-width" | BorderLeftWidth | SizeUnit | Left border line width | +| "border-right-width" | BorderRightWidth | SizeUnit | Right border line width | +| "border-top-width" | BorderTopWidth | SizeUnit | Top border line width | +| "border-bottom-width" | BorderBottomWidth | SizeUnit | Bottom border line width | +| "border-left-color" | BorderLeftColor | Color | Left border line color | +| "border-right-color" | BorderRightColor | Color | Right border line color | +| "border-top-color" | BorderTopColor | Color | Top border line color | +| "border-bottom-color" | BorderBottomColor | Color | Bottom border line color | +| "border-style" | BorderStyle | int | Border line style | +| "border-width" | BorderWidth | SizeUnit | Border line width | +| "border-color" | BorderColor | Color | Border line color | +| "border-left" | BorderLeft | ViewBorder | Left border line | +| "border-right" | BorderRight | ViewBorder | Right border line | +| "border-top" | BorderTop | ViewBorder | Top border line | +| "border-bottom" | BorderBottom | ViewBorder | Bottom border line | + +Example + + view.Set(rui.BorderStyle, rui.SolidBorder) + view.Set(rui.BorderWidth, rui.Px(1)) + view.Set(rui.BorderColor, rui.Black) + +equivalent to + + view.Set(rui.Border, NewBorder(rui.Params{ + rui.Style: rui.SolidBorder, + rui.Width: rui.Px(1), + rui.ColorProperty: rui.Black, + })) + +### "radius" property + +The "radius" property sets the elliptical corner radius of the View. Radii are specified by the RadiusProperty +interface that implements the Properties interface (see above). +For this, the following properties of the SizeUnit type are used: + +| Property | Constant | Description | +|------------------|--------------|-------------------------------------| +| "top-left-x" | TopLeftX | x-radius of the top left corner | +| "top-left-y" | TopLeftY | y-radius of the top left corner | +| "top-right-x" | TopRightX | x-radius of the top right corner | +| "top-right-y" | TopRightY | y-radius of the top right corner | +| "bottom-left-x" | BottomLeftX | x-radius of the bottom left corner | +| "bottom-left-y" | BottomLeftY | y-radius of the bottom left corner | +| "bottom-right-x" | BottomRightX | x-radius of the bottom right corner | +| "bottom-right-y" | BottomRightY | y-radius of the bottom right corner | + +If the x- and y-radii are the same, then you can use the auxiliary properties + +| Property | Constant | Description | +|----------------|--------------|----------------------------| +| "top-left" | TopLeft | top left corner radius | +| "top-right" | TopRight | top right corner radius | +| "bottom-left" | BottomLeft | bottom left corner radius | +| "bottom-right" | BottomRight | bottom right corner radius | + +To set all radii to the same values, use the "x" and "y" properties + +The RadiusProperty interface is created using the NewRadiusProperty function. Example + + view.Set(rui.Radius, NewRadiusProperty(rui.Params{ + rui.X: rui.Px(16), + rui.Y: rui.Px(8), + rui.TopLeft: rui.Px(0), + rui.BottomRight: rui.Px(0), + })) + +equivalent to + + view.Set(rui.Radius, NewRadiusProperty(rui.Params{ + rui.TopRightX: rui.Px(16), + rui.TopRightY: rui.Px(8), + rui.BottomLeftX: rui.Px(16), + rui.BottomLeftY: rui.Px(8), + rui.TopLeftX: rui.Px(0), + rui.TopLeftX: rui.Px(0), + rui.BottomRightX: rui.Px(0), + rui.BottomRightY: rui.Px(0), + })) + +If all radii are the same, then the given SizeUnit value can be directly assigned to the "radius" property + + view.Set(rui.Radius, rui.Px(4)) + +RadiusProperty has a textual representation of the following form: + + _{ = [/ ] [, = [/ ]] … } + +where can take the following values: "x", "y", "top-left", "top-left-x", "top-left-y", "top-right", +"top-right-x", "top-right-y", "bottom-left", "bottom-left-x", "bottom-left-y", "bottom-right", "bottom-right-x", "bottom-right-y". + +Values like " / " can only be assigned to the "top-left", "top-right", "bottom-left" and "bottom-right" properties. + +Examples: + + _{ x = 4px, y = 4px, top-left = 8px, bottom-right = 8px } + +equivalent to + + _{ top-left = 8px, top-right = 4px, bottom-left = 4px, bottom-right = 8px } + +or + + _{ top-left = 8px / 8px, top-right = 4px / 4px, bottom-left = 4px / 4px, bottom-right = 8px / 8px } + +or + + _{ top-left-x = 8px, top-left-y = 8px, top-right-x = 4px, top-right-y = 4px, + bottom-left-x = 4px, bottom-left-y = 4px, bottom-right-x = 8px, bottom-right-y = 8px } + +The RadiusProperty interface can be converted to a BoxRadius structure using the BoxRadius function. +When converted, all text constants are replaced with real values. BoxRadius is described as + + type BoxRadius struct { + TopLeftX, TopLeftY, TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY SizeUnit + } + +The BoxRadius structure can be passed as a parameter to the Set function by setting the value of the "radius" property. +This converts BoxRadius to RadiusProperty. Therefore, when the property is read, +the Get function will return the RadiusProperty interface, not the BoxRadius structure. +You can get the BoxRadius structure without additional transformations using the global function + + func GetRadius(view View, subviewID string) BoxRadius + +You can also set individual radii using the Set function of the View interface. +For this, the following properties are used + +| Property | Constant | Description | +|-------------------------|--------------------|-------------------------------------| +| "radius-x" | RadiusX | All x-radii | +| "radius-y" | RadiusY | All y-radii | +| "radius-top-left-x" | RadiusTopLeftX | x-radius of the top left corner | +| "radius-top-left-y" | RadiusTopLeftY | y-radius of the top left corner | +| "radius-top-right-x" | RadiusTopRightX | x-radius of the top right corner | +| "radius-top-right-y" | RadiusTopRightY | y-radius of the top right corner | +| "radius-bottom-left-x" | RadiusBottomLeftX | x-radius of the bottom left corner | +| "radius-bottom-left-y" | RadiusBottomLeftY | y-radius of the bottom left corner | +| "radius-bottom-right-x" | RadiusBottomRightX | x-radius of the bottom right corner | +| "radius-bottom-right-y" | RadiusBottomRightY | y-radius of the bottom right corner | +| "radius-top-left" | RadiusTopLeft | top left corner radius | +| "radius-top-right" | RadiusTopRight | top right corner radius | +| "radius-bottom-left" | RadiusBottomLeft | bottom left corner radius | +| "radius-bottom-right" | RadiusBottomRight | bottom right corner radius | + +Example + + view.Set(rui.RadiusX, rui.Px(4)) + view.Set(rui.RadiusY, rui.Px(32)) + +equivalent to + + view.Set(rui.Border, NewRadiusProperty(rui.Params{ + rui.X: rui.Px(4), + rui.Y: rui.Px(32), + })) + +### "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 has the following properties: + +| Property | Constant | Type | Description | +|-----------------|---------------|----------|-----------------------------------------------------------------------| +| "color" | ColorProperty | Color | Shadow color | +| "inset" | Inset | bool | true - the shadow inside the View, false - outside | +| "x-offset" | XOffset | SizeUnit | Offset the shadow along the X axis | +| "y-offset" | YOffset | SizeUnit | Offset the shadow along the Y axis | +| "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: + + 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 + +The NewViewShadow function creates an outer shadow (Inset == false), +NewInsetViewShadow - an inner one (Inset == true). +The NewShadowWithParams function is used when constants must be used as parameters. +For example: + + shadow := NewShadowWithParams(rui.Params{ + rui.ColorProperty : "@shadowColor", + rui.BlurRadius : 8.0, + rui.Dilation : 16.0, + }) + +ViewShadow, ViewShadow array, and ViewShadow textual representation can be assigned as a value to the "shadow" property. + +The ViewShadow text representation has the following format: + + _{ color = [, x-offset = ] [, y-offset = ] [, blur = ] + [, spread-radius = ] [, inset = ] } + +You can get the value of "shadow" property using the function + + func GetViewShadows(view View, subviewID string) []ViewShadow + +If no shadow is specified, then this function will return an empty array + +### "background-color" property + +Constant: rui.BackgroundColor. Get function: GetBackgroundColor() Color + +The "background-color" property sets the background color. Valid values are Color, an integer, the textual representation of Color, +and a constant name starting with '@'. An integer must encode the color in the AARRGGBB format + +In addition to color, images and gradients can also be used as backgrounds (see below). +In this case, "background-color" is used for transparent areas of images. + +### "background-clip" property + +The "background-clip" property determines how the background color and / or background image will be displayed below the box borders. + +If no background image or color is specified, this property will have a visual effect only +if the border has transparent areas or partially opaque areas; otherwise, the border hides the difference. + +The property can take the following values: + +| Value | Constant | Name | Description | +|:-----:|----------------|---------------|------------------------------------------------| +| 0 | BorderBoxClip | "border-box" | The background extends to the outer edge of the border (but below the border in z-order). | +| 1 | PaddingBoxClip | "padding-box" | The background extends to the outer edge of the padding. No background is drawn below the border. | +| 2 | ContentBoxClip | "content-box" | The background is painted inside (clipped) of the content box. | + +### "background" property + +In addition to color, pictures and / or gradient fills can also be specified as the background of the View. +The property "background" is used for this. The background can contain multiple images and gradients. +Each background element is described by the BackgroundElement interface. +BackgroundElement can be of three types: linear gradient, radial gradient, and image. + +#### Linear gradient + +A linear gradient is created using the function + + func NewBackgroundLinearGradient(params Params) BackgroundElement + +The linear gradient has the following options: + +* Direction ("direction") - defines the direction of the gradient line (the line along which the color changes). +Optional parameter. The default direction is from bottom to top. It can be either AngleUnit +(the angle of inclination of the line relative to the vertical) or one of the following int values: + +| Value | Constant | Name | Description | +|:-----:|-----------------------|-------------------|-----------------------------------------------| +| 0 | ToTopGradient | "to-top" | Line goes from bottom to top (default) | +| 1 | ToRightTopGradient | "to-right-top" | From bottom left to top right | +| 2 | ToRightGradient | "to-right" | From left to right | +| 3 | ToRightBottomGradient | "to-right-bottom" | From top left to bottom right | +| 4 | ToBottomGradient | "to-bottom" | From top to bottom | +| 5 | ToLeftBottomGradient | "to-left-bottom" | From the upper right corner to the lower left | +| 6 | ToLeftGradient | "to-left" | From right to left | +| 7 | ToLeftTopGradient | "to-left-top" | From the bottom right corner to the top left | + +* Gradient ("gradient") - array of gradient key points (required parameter). +Each point is described by a BackgroundGradientPoint structure, which has two fields: Pos of type SizeUnit and Color. +Pos defines the position of the point relative to the start of the gradient line. The array must have at least 2 points. +You can also pass a Color array as the gradient value. In this case, the points are evenly distributed along the gradient line. +You can also use an array of []interface{} as an array of cue points. +The elements of this array can be BackgroundGradientPoint, Color, BackgroundGradientPoint or Color text representation, and the name of the constant + +* Repeat ("repeat") - a boolean value that determines whether the gradient will repeat after the last key point. +Optional parameter. The default is false (do not repeat) + +The linear gradient text representation is as follows: + + linear-gradient { gradient = [, direction = ] [, repeat = ] } + +#### Radial gradient + +A radial gradient is created using the function + + func NewBackgroundRadialGradient(params Params) BackgroundElement + +The radial gradient has the following parameters: + +* Gradient ("gradient") - array of gradient key points (required parameter). Identical to the linear gradient parameter of the same name. + +* Repeat ("repeat") - a boolean value that determines whether the gradient will repeat after the last key point. +Optional parameter. The default is false (do not repeat) + +* RadialGradientShape ("radial-gradient-shape") or Shape ("shape") - defines the shape of the gradient. +It can take one of two int values: + +| Value | Constant | Name | Description | +|:-----:|-----------------|-----------|----------------------------------------------| +| 0 | EllipseGradient | "ellipse" | The shape is an axis-aligned ellipse | +| 1 | CircleGradient | "circle" | The shape is a circle with a constant radius | + +Optional parameter. The default is EllipseGradient + +* RadialGradientRadius ("radial-gradient-radius") or Radius ("radius") - sets the radius of the gradient. +Can be either SizeUnit or one of the following int values: + +| Value | Constant | Name | Description | +|:-----:|------------------------|-------------------|--------------------------------------------| +| 0 | ClosestSideGradient | "closest-side" | The final shape of the gradient corresponds to the side of the rectangle closest to its center (for circles), or both vertical and horizontal sides closest to the center (for ellipses) | +| 1 | ClosestCornerGradient | "closest-corner" | The final shape of the gradient is defined so that it exactly matches the closest corner of the window from its center | +| 2 | FarthestSideGradient | "farthest-side" | Similar to ClosestSideGradient, except that the size of the shape is determined by the farthest side from its center (or vertical and horizontal sides) | +| 3 | FarthestCornerGradient | "farthest-corner" | The final shape of the gradient is defined so that it exactly matches the farthest corner of the rectangle from its center | + +Optional parameter. The default is ClosestSideGradient + +* CenterX ("center-x"), CenterY ("center-y") - sets the center of the gradient relative to the upper left corner of the View. Takes in a SizeUnit value. Optional parameter. +The default value is "50%", i.e. the center of the gradient is the center of the View. + +The linear gradient text representation is as follows: + + radial-gradient { gradient = [, repeat = ] [, shape = ] + [, radius = ][, center-x = ][, center-y = ]} + +#### Image + +The image has the following parameters: + +* Source ("src") - Specifies the URL of the image + +* Fit ("fit") - an optional parameter that determines the scaling of the image. +Can be one of the following Int values: + +| Constant | Value | Name | Description | +|------------|:-----:|-----------|---------------------------------------------------------------------------------------------------------| +| NoneFit | 0 | "none" | No scaling (default). The dimensions of the image are determined by the Width and Height parameters. | +| ContainFit | 1 | "contain" | The image is scaled proportionally so that its width or height is equal to the width or height of the background area. Image can be cropped to width or height | +| CoverFit | 2 | "cover" | The image is scaled with the same proportions so that the whole picture fits inside the background area | + +* Width ("width"), Height (height) - optional SizeUnit parameters that specify the height and width of the image. +Used only if Fit is NoneFit. The default is Auto (original size). The percentage value sets the size relative +to the height and width of the background area, respectively + +* Attachment - + +* Repeat (repeat) - an optional parameter specifying the repetition of the image. +Can be one of the following int values: + +| Constant | Value | Name | Description | +|-------------|:-----:|-------------|-------------------------------------------------| +| NoRepeat | 0 | "no-repeat" | Image does not repeat (default) | +| RepeatXY | 1 | "repeat" | The image repeats horizontally and vertically | +| RepeatX | 2 | "repeat-x" | The image repeats only horizontally | +| RepeatY | 3 | "repeat-y" | Image repeats vertically only | +| RepeatRound | 4 | "round" | The image is repeated so that an integer number of images fit into the background area; if this fails, then the background images are scaled | +| RepeatSpace | 5 | "space" | The image is repeated as many times as necessary to fill the background area; if this fails, an empty space is added between the pictures | + +* ImageHorizontalAlign, + +* ImageVerticalAlign, + +### "clip" property + +The "clip" property (Clip constant) of the ClipShape 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 + +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 +(see the description of the RadiusProperty type above). If there should be no rounding of corners, then nil must be passed as radius + +The textual description of the rectangular cropping area is in the following format + + inset{ top = , right = , bottom = , left = , + [radius = ] } + } + +#### circle + +Round cropping area. Created with the function: + + func CircleClip(x, y, radius SizeUnit) ClipShape + +where x, y - coordinates of the center of the circle; radius - radius + +The textual description of the circular cropping area is in the following format + + circle{ x = , y = , radius = } + +#### ellipse + +Elliptical cropping area. Created with the function: + + func EllipseClip(x, y, rx, ry SizeUnit) ClipShape + +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. + +The textual description of the elliptical clipping region is in the following format + + ellipse{ x = , y = , radius-x = , radius-y = } + +#### polygon + +Polygonal cropping area. Created using functions: + + func PolygonClip(points []interface{}) ClipShape + func PolygonPointsClip(points []SizeUnit) ClipShape + +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, +or the text representation of SizeUnit, or elements of type SizeUnit. + +The textual description of the polygonal cropping area is in the following format + + polygon{ points = ", , , ,…" } + +### "оpacity" property + +The "opacity" property (constant Opacity) of the float64 type sets the transparency of the View. Valid values are from 0 to 1. +Where 1 - View is fully opaque, 0 - fully transparent. + +You can get the value of this property using the function + + func GetOpacity(view View, subviewID string) float64 + +### "z-index" property + +The "z-index" property (constant ZIndex) of type int defines the position of the element and its children along the z-axis. +In the case of overlapping elements, this value determines the stacking order. In general, the elements +higher z-indexes overlap elements with lower. + +You can get the value of this property using the function + + func GetZIndex(view View, subviewID string) int + +### "visibility" property + +The "visibility" property (constant Visibility) of type int specifies the visibility of the View. Valid values + +| Value | Constant | Name | Visibility | +|:-----:|-----------|-------------|------------------------------------------------| +| 0 | Visible | "visible" | View is visible. Default value. | +| 1 | Invisible | "invisible" | View is invisible but takes up space. | +| 2 | Gone | "gone" | View is invisible and does not take up space. | + +You can get the value of this property using the function + + func GetVisibility(view View, subviewID string) int + +### "filter" property + +The "filter" property (Filter constant) applies graphical effects such as blur and color shift to the View. +Only the ViewFilter interface is used as the value of the "filter" property. +ViewFilter is created using the function + + func NewViewFilter(params Params) ViewFilter + +The argument lists the effects to apply. The following effects are possible: + +| Effect | Constant | Type | Description | +|---------------|------------|--------------------|-------------------------| +| "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 | +| "grayscale" | Grayscale | float64 0…100% | Converting to grayscale | +| "hue-rotate" | HueRotate | AngleUnit | Hue rotation | +| "invert" | Invert | float64 0…100% | Invert colors | +| "opacity" | Opacity | float64 0…100% | Changing transparency | +| "saturate" | Saturate | float64 0…10000% | Saturation change | +| "sepia" | Sepia | float64 0…100% | Conversion to serpia | + +Example + + rui.Set(view, "subview", rui.Filter, rui.NewFilter(rui.Params{ + rui.Brightness: 200, + rui.Contrast: 150, + })) + +You can get the value of the current filter using the function + + func GetFilter(view View, subviewID string) ViewFilter + +### "semantics" property + +The "semantics" string property (Semantics constant) defines the semantic meaning of the View. +This property may have no visible effect, but it allows search engines to understand the structure of your application. +It also helps to voice the interface to systems for people with disabilities: + +| Value | Name | Semantics | +|:-----:|------------------|-----------------------------------------------------| +| 0 | "default" | Unspecified. Default value. | +| 1 | "article" | A stand-alone part of the application intended for independent distribution or reuse. | +| 2 | "section" | A stand-alone section that cannot be represented by a more precise semantically element | +| 3 | "aside" | A part of a document whose content is only indirectly related to the main content (footnote, label) | +| 4 | "header" | Application Title | +| 5 | "main" | Main content (content) of the application | +| 6 | "footer" | Footer | +| 7 | "navigation" | Navigation bar | +| 8 | "figure" | Image | +| 9 | "figure-caption" | Image Title. Should be inside "figure" | +| 10 | "button" | Button | +| 11 | "p" | Paragraph | +| 12 | "h1" | Level 1 text heading. Changes the style of the text | +| 13 | "h2" | Level 2 text heading. Changes the style of the text | +| 14 | "h3" | Level 3 text heading. Changes the style of the text | +| 15 | "h4" | Level 4 text heading. Changes the style of the text | +| 16 | "h5" | Level 5 text heading. Changes the style of the text | +| 17 | "h6" | Level 6 text heading. Changes the style of the text | +| 18 | "blockquote" | Quote. Changes the style of the text | +| 19 | "code" | Program code. Changes the style of the text | + +### Text properties + +All properties listed in this section are inherited, i.e. the property will apply +not only to the View for which it is set, but also to all Views nested in it. + +The following properties are available to customize the text display options: + +#### "font-name" property + +Property "font-name" (constant FontName) - the text property specifies the name of the font to use. +Multiple fonts can be specified. In this case, they are separated by a space. +Fonts are applied in the order in which they are listed. Those, the first is applied first, +if it is not available, then the second, third, etc. + +You can get the value of this property using the function + + func GetFontName(view View, subviewID string) string + +#### "text-color" property + +Property "text-color" (constant TextColor) - the Color property determines the color of the text. + +You can get the value of this property using the function + + func GetTextColor(view View, subviewID string) Color + +#### "text-size" property + +Property "text-size" (constant TextSize) - the SizeUnit property determines the size of the font. + +You can get the value of this property using the function + + func GetTextSize(view View, subviewID string) SizeUnit + +#### "italic" property + +The "italic" property (constant Italic) is the bool property. If the value is true, then italics are applied to the text + +You can get the value of this property using the function + + func IsItalic(view View, subviewID string) bool + +#### "small-caps" property + +The "small-caps" property (SmallCaps constant) is the bool property. If the value is true, then small-caps is applied to the text. + +You can get the value of this property using the function + + func IsSmallCaps(view View, subviewID string) bool + +#### "white-space" property + +The "white-space" (WhiteSpace constant) int property controls how whitespace is handled within the View. +The "white-space" property can take the following values: + +0 (constant WhiteSpaceNormal, name "normal") - sequences of spaces are concatenated into one space. +Newlines in the source are treated as a single space. Applying this value optionally splits lines to fill inline boxes. + +1 (constant WhiteSpaceNowrap, name "nowrap") - Concatenates sequences of spaces into one space, +like a normal value, but does not wrap lines (text wrapping) within the text. + +2 (constant WhiteSpacePre, name "pre") - sequences of spaces are saved as they are specified in the source. +Lines are wrapped only where newlines are specified in the source and where "br" elements are specified in the source. + +3 (constant WhiteSpacePreWrap, name "pre-wrap") - sequences of spaces are saved as they are +indicated in the source. Lines are wrapped only where newlines are specified in the source and there, +where "br" elements are specified in the source, and optionally to fill inline boxes. + +4 (constant WhiteSpacePreLine, name "pre-line") - sequences of spaces are concatenated into one space. +Lines are split on newlines, on "br" elements, and optionally to fill inline boxes. + +5 (constant WhiteSpaceBreakSpaces, name "break-spaces") - the behavior is identical to pre-wrap with the following differences: +* Sequences of spaces are preserved as specified in the source, including spaces at the end of lines. +* Lines are wrapped on any spaces, including in the middle of a sequence of spaces. +* Spaces take up space and do not hang at the ends of lines, which means they affect the internal dimensions (min-content and max-content). + +The table below shows the behavior of various values ​​of the "white-space" property. + +| | New lines | Spaces and Tabs | Text wrapping | End of line spaces | End-of-line other space separators | +|-----------------------|-----------|-----------------|---------------|--------------------|------------------------------------| +| WhiteSpaceNormal | Collapse | Collapse | Wrap | Remove | Hang | +| WhiteSpaceNowrap | Collapse | Collapse | No wrap | Remove | Hang | +| WhiteSpacePre | Preserve | Preserve | No wrap | Preserve | No wrap | +| WhiteSpacePreWrap | Preserve | Preserve | Wrap | Hang | Hang | +| WhiteSpacePreLine | Preserve | Collapse | Wrap | Remove | Hang | +| WhiteSpaceBreakSpaces | Preserve | Preserve | Wrap | Wrap | Wrap | + +#### "word-break" property + +The "word-break" int property (WordBreak constant) determines where the newline will be set if the text exceeds the block boundaries. +The "white-space" property can take the following values: + +0 (constant WordBreak, name "normal) - default behavior for linefeed placement. + +1 (constant WordBreakAll, name "break-all) - if the block boundaries are exceeded, +a line break will be inserted between any two characters (except for Chinese/Japanese/Korean text). + +2 (constant WordBreakKeepAll, name "keep-all) - Line break will not be used in Chinese/Japanese/ Korean text. +For text in other languages, the default behavior (normal) will be applied. + +3 (constant WordBreakWord, name "break-word) - when the block boundaries are exceeded, +the remaining whole words can be broken in an arbitrary place, if a more suitable place for line break is not found. + +#### "strikethrough", "overline", "underline" properties + +These bool properties set decorative lines on the text: + +| Property | Constant | Decorative line type | +|-----------------|---------------|-------------------------| +| "strikethrough" | Strikethrough | Strikethrough line text | +| "overline" | Overline | Line above the text | +| "underline" | Underline | Line under the text | + +You can get the value of these properties using the functions + + func IsStrikethrough(view View, subviewID string) bool + func IsOverline(view View, subviewID string) bool + func IsUnderline(view View, subviewID string) bool + +#### "text-line-thickness" property + +The "text-line-thickness" SizeUnit property (TextLineThickness constant) sets the thickness +of decorative lines on the text set using the "strikethrough", "overline" and "underline" properties. + +You can get the value of this property using the function + + GetTextLineThickness(view View, subviewID string) SizeUnit + +#### "text-line-style" property + +The "text-line-style" int property (constant TextLineStyle) sets the style of decorative lines +on the text set using the "strikethrough", "overline" and "underline" properties. + +Possible values are: + +| Value | Constant | Name | Description | +|:-----:|------------|----------|----------------------------| +| 1 | SolidLine | "solid" | Solid line (default value) | +| 2 | DashedLine | "dashed" | Dashed line | +| 3 | DottedLine | "dotted" | Dotted line | +| 4 | DoubleLine | "double" | Double solid line | +| 5 | WavyLine | "wavy" | Wavy line | + +You can get the value of this property using the function + + func GetTextLineStyle(view View, subviewID string) int + +#### "text-line-color" property + +The "text-line-color" Color property (constant TextLineColor) sets the color of decorative lines +on the text set using the "strikethrough", "overline" and "underline" properties. +If the property is not defined, then the text color specified by the "text-color" property is used for lines. + +You can get the value of this property using the function + + func GetTextLineColor(view View, subviewID string) Color + +#### "text-weight" property + +Свойство "text-weight" (константа TextWeight) - свойство типа int устанавливает начертание шрифта. Допустимые значения: + +| Value | Constant | Common name of the face | +|:-----:|----------------|---------------------------| +| 1 | ThinFont | Thin (Hairline) | +| 2 | ExtraLightFont | Extra Light (Ultra Light) | +| 3 | LightFont | Light | +| 4 | NormalFont | Normal. Default value | +| 5 | MediumFont | Medium | +| 6 | SemiBoldFont | Semi Bold (Demi Bold) | +| 7 | BoldFont | Bold | +| 8 | ExtraBoldFont | Extra Bold (Ultra Bold) | +| 9 | BlackFont | Black (Heavy) | + +Some fonts are only available in normal or bold style. In this case, the value of this property is ignored. + +You can get the value of this property using the function + + func GetTextWeight(view View, subviewID string) int + +#### "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"). +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: + + func NewTextShadow(offsetX, offsetY, blurRadius SizeUnit, color Color) ViewShadow + func NewShadowWithParams(params Params) ViewShadow + +The NewShadowWithParams function is used when constants must be used as parameters. For example: + + shadow := NewShadowWithParams(rui.Params{ + rui.ColorProperty : "@shadowColor", + 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"). + +You can get the value of this property using the function + + func GetTextShadows(view View, subviewID string) []ViewShadow + +If no shadow is specified, then this function will return an empty array + +#### "text-align" property + +The "text-align" int property (constant TextAlign) sets the alignment of the text. Valid values: + +| Value | Constant | Name | Value | +|:-----:|--------------|-----------|-------------------| +| 0 | LeftAlign | "left" | Left alignment | +| 1 | RightAlign | "right" | Right alignment | +| 2 | CenterAlign | "center" | Center alignment | +| 3 | JustifyAlign | "justify" | Justify alignment | + +You can get the value of this property using the function + + func GetTextAlign(view View, subviewID string) int + +#### "text-indent" property + +The "text-indent" (TextIndent constant) SizeUnit property determines the size of the indent (empty space) before the first line of text. + +You can get the value of this property using the function + + func GetTextIndent(view View, subviewID string) SizeUnit + +#### "letter-spacing" property + +The "Letter-spacing" (LetterSpacing constant) SizeUnit property determines the letter spacing in the text. +The value can be negative, but there can be implementation-specific restrictions. +The user agent can choose not to increase or decrease the letter spacing to align the text. + +You can get the value of this property using the function + + func GetLetterSpacing(view View, subviewID string) SizeUnit + +#### "word-spacing" property + +The "word-spacing" (WordSpacing constant) SizeUnit property determines the length of the space between words. +If the value is specified as a percentage, then it defines the extra spacing as a percentage of the preliminary character width. +Otherwise, it specifies additional spacing in addition to the inner word spacing as defined by the font. + +You can get the value of this property using the function + + func GetWordSpacing(view View, subviewID string) SizeUnit + +#### "line-height" property + +The "line-height" (LineHeight constant) SizeUnit property sets the amount of space between lines. + +You can get the value of this property using the function + + func GetLineHeight(view View, subviewID string) SizeUnit + +#### "text-transform" property + +The "text-transform" (TextTransform constant) int property defines the case of characters. Valid values: + +| Value | Constant | Case conversion | +|:-----:|-------------------------|-----------------------------------------| +| 0 | NoneTextTransform | Original case of characters | +| 1 | CapitalizeTextTransform | Every word starts with a capital letter | +| 2 | LowerCaseTextTransform | All characters are lowercase | +| 3 | UpperCaseTextTransform | All characters are uppercase | + +You can get the value of this property using the function + + func GetTextTransform(view View, subviewID string) int + +#### "text-direction" property + +The "text-direction" (TextDirection constant) int property determines the direction of text output. Valid values: + +| Value | Constant | Text output direction | +|:-----:|----------------------|-------------------------------------------------------------------------| +| 0 | SystemTextDirection | Systemic direction. Determined by the language of the operating system. | +| 1 | LeftToRightDirection | From left to right. Used for English and most other languages. | +| 2 | RightToLeftDirection | From right to left. Used for Hebrew, Arabic and some others. | + +You can get the value of this property using the function + + func GetTextDirection(view View, subviewID string) int + +#### "writing-mode" property +The "writing-mode" (WritingMode constant) int property defines how the lines of text are arranged +vertically or horizontally, as well as the direction in which the lines are displayed. +Possible values ​​are: + +| Value | Constant | Description | +|:-----:|-----------------------|------------------------------------------------------------------| +| 0 | HorizontalTopToBottom | Horizontal lines are displayed from top to bottom. Default value | +| 1 | HorizontalBottomToTop | Horizontal lines are displayed from bottom to top. | +| 2 | VerticalRightToLeft | Vertical lines are output from right to left. | +| 3 | VerticalLeftToRight | Vertical lines are output from left to right. | + +You can get the value of this property using the function + + func GetWritingMode(view View, subviewID string) int + +#### "vertical-text-orientation" property + +The "vertical-text-orientation" (VerticalTextOrientation constant) int property is used only if "writing-mode" +is set to VerticalRightToLeft (2) or VerticalLeftToRight (3) and determines the position of the vertical line characters. +Possible values are: + +| Value | Constant | Value | +|:-----:|------------------------|----------------------------------------------| +| 0 | MixedTextOrientation | Symbols rotated 90 clockwise. Default value. | +| 1 | UprightTextOrientation | Symbols are arranged normally (vertically). | + +You can get the value of this property using the function + + func GetVerticalTextOrientation(view View, subviewID string) int + +### Transformation properties + +These properties are used to transform (skew, scale, etc.) the content of the View. + +#### "perspective" property + +The "perspective" SizeUnit property (Perspective constant) defines the distance between the z = 0 plane and +the user in order to give the 3D positioned element a perspective effect. Each transformed element with +z > 0 will become larger, with z < 0, respectively, less. + +Elements of the part that are behind the user, i.e. the z-coordinate of these elements is greater than the value of the perspective property, and are not rendered. + +The vanishing point is by default located in the center of the element, but it can be moved using the "perspective-origin-x" and "perspective-origin-y" properties. + +You can get the value of this property using the function + + func GetPerspective(view View, subviewID string) SizeUnit + +#### "perspective-origin-x" and "perspective-origin-y" properties + +The "Perspective-origin-x" and "perspective-origin-y" SizeUnit properties (PerspectiveOriginX and PerspectiveOriginY constants) +determine the position from which the viewer is looking. It is used by the perspective property as a vanishing point. + +By default, the "perspective-origin-x" and "perspective-origin-y" properties are set to 50%. point to the center of the View. + +You can get the value of these properties using the function + + func GetPerspectiveOrigin(view View, subviewID string) (SizeUnit, SizeUnit) + +#### "backface-visibility" property + +The "backface-visibility" bool property (BackfaceVisible constant) determines whether +the back face of an element is visible when it is facing the user. + +The back surface of an element is a mirror image of its front surface. However, invisible in 2D, +the back face can be visible when the transformation causes the element to rotate in 3D space. +(This property has no effect on 2D transforms that have no perspective.) + +You can get the value of this property using the function + + func GetBackfaceVisible(view View, subviewID string) bool + +#### "origin-x", "origin-y", and "origin-z" properties + +The "origin-x", "origin-y", and "origin-z" SizeUnit properties (OriginX, OriginY, and OriginZ constants) set the origin for element transformations. + +The origin of the transformation is the point around which the transformation takes place. For example, rotation. + +The "origin-z" property is ignored if the perspective property is not set. + +You can get the value of these properties using the function + + func GetOrigin(view View, subviewID string) (SizeUnit, SizeUnit, SizeUnit) + +#### "translate-x", "translate-y", and "translate-z" properties + +The "translate-x", "translate-y" and "translate-z" SizeUnit properties (TranslateX, TranslateY, and TranslateZ constants) +set the offset of the content of the View. + +The translate-z property is ignored if the perspective property is not set. + +You can get the value of these properties using the function + + func GetTranslate(view View, subviewID string) (SizeUnit, SizeUnit, SizeUnit) + +#### "scale-x", "scale-y" and "scale-z" properties + +The "scale-x", "scale-y" and "scale-z" float64 properties (ScaleX, ScaleY and ScaleZ constants) set +the scaling factor along the x, y and z axes, respectively. +The original scale is 1. A value between 0 and 1 is used to zoom out. More than 1 - to increase. +Values less than or equal to 0 are invalid (the Set function will return false) + +The "scale-z" property is ignored if the "perspective" property is not set. + +You can get the value of these properties using the function + + func GetScale(view View, subviewID string) (float64, float64, float64) + +#### "rotate" property + +The "rotate" AngleUnit property (Rotate constant) sets the angle of rotation of the content +around the vector specified by the "rotate-x", "rotate-y" and "rotate-z" properties. + +#### "rotate-x", "rotate-y", and "rotate-z" properties + +The "rotate-x", "rotate-y" and "rotate-z" float64 properties (constant RotateX, RotateY and RotateZ) set +the vector around which the rotation is performed by the angle specified by the "rotate" property. +This vector passes through the point specified by the "origin-x", "origin-y" and "origin-z" properties. + +The "rotate-z" property is ignored if the "perspective" property is not set. + +You can get the value of these properties, as well as the "rotate" property, using the function + + func GetRotate(view View, subviewID string) (float64, float64, float64, AngleUnit) + +#### "skew-x" and "skew-y" properties + +The "skew-x" and "skew-y" AngleUnit properties (SkewX and SkewY constants) set the skew (skew) of the content, +thus turning it from a rectangle into a parallelogram. The bevel is carried out around the point +specified by the transform-origin-x and transform-origin-y properties. + +You can get the value of these properties using the function + + func GetSkew(view View, subviewID string) (AngleUnit, AngleUnit) + +### Keyboard events + +Two kinds of keyboard events can be generated for a View that has received input focus. + +| Event | Constant | Description | +|------------------|--------------|----------------------------| +| "key-down-event" | KeyDownEvent | The key has been pressed. | +| "key-up-event" | KeyUpEvent | The key has been released. | + +The main event data listener has the following format: + + func(View, KeyEvent) + +where the second argument describes the parameters of the keys pressed. The KeyEvent structure has the following fields: + +| Field | Type | Description | +|-----------|--------|-------------------------------------------------------------------------------------------------------------------------------------------| +| TimeStamp | uint64 | The time the event was created (in milliseconds). The starting point depends on the browser implementation (EPOCH, browser launch, etc.). | +| Key | string | The value of the key on which the event occurred. The value is returned taking into account the current language and case. | +| Code | string | The key code of the represented event. The value is independent of the current language and case. | +| Repeat | bool | Repeated pressing: the key was pressed until its input began to be automatically repeated. | +| CtrlKey | bool | The Ctrl key was active when the event occurred. | +| ShiftKey | bool | The Shift key was active when the event occurred. | +| AltKey | bool | The Alt (Option or ⌥ in OS X) key was active when the event occurred. | +| MetaKey | bool | The Meta key (for Mac, this is the ⌘ Command key; for Windows, the Windows key ⊞) was active when the event occurred. | + +You can also use listeners in the following formats: + +* func(KeyEvent) +* func(View) +* func() + +You can get lists of listeners for keyboard events using the functions: + + func GetKeyDownListeners(view View, subviewID string) []func(View, KeyEvent) + func GetKeyUpListeners(view View, subviewID string) []func(View, KeyEvent) + +### Focus events + +Focus events are fired when a View gains or loses input focus. Accordingly, two events are possible: + +| Event | Constant | Description | +|--------------------|----------------|--------------------------------------------| +| "focus-event" | FocusEvent | View receives input focus (becomes active) | +| "lost-focus-event" | LostFocusEvent | View loses input focus (becomes inactive) | + +The main event data listener has the following format: + + func(View). + +You can also use a listener in the following format: + + func() + +You can get lists of listeners for focus events using the functions: + + func GetFocusListeners(view View, subviewID string) []func(View) + func GetLostFocusListeners(view View, subviewID string) []func(View) + +### Mouse events + +Several kinds of mouse events can be generated for the View + +| Event | Constant | Description | +|----------------------|------------------|------------------------------------------------------------------------| +| "mouse-down" | MouseDown | The mouse button was pressed. | +| "mouse-up" | MouseUp | The mouse button has been released. | +| "mouse-move" | MouseMove | Mouse cursor moved | +| "mouse-out" | MouseOut | The mouse cursor has moved outside the View, or entered the child View | +| "mouse-over" | MouseOver | The mouse cursor has moved within the arrea of View | +| "click-event" | ClickEvent | There was a mouse click | +| "double-click-event" | DoubleClickEvent | There was a double mouse click | +| "context-menu-event" | ContextMenuEvent | The key for calling the context menu (right mouse button) is pressed | + +The main event data listener has the following format: + + func(View, MouseEvent) + +where the second argument describes the parameters of the mouse event. The MouseEvent structure has the following fields: + +| Field | Type | Description | +|-----------|---------|-----------------------------------------------------------------------------------------| +| TimeStamp | uint64 | The time the event was created (in milliseconds). The starting point depends on the browser implementation (EPOCH, browser launch, etc.). | +| Button | int | The number of the mouse button clicked on which triggered the event | +| Buttons | int | Bitmask showing which mouse buttons were pressed when the event occurred | +| X | float64 | The horizontal position of the mouse relative to the origin View | +| Y | float64 | The vertical position of the mouse relative to the origin View | +| ClientX | float64 | Horizontal position of the mouse relative to the upper left corner of the application | +| ClientY | float64 | The vertical position of the mouse relative to the upper left corner of the application | +| ScreenX | float64 | Horizontal position of the mouse relative to the upper left corner of the screen | +| ScreenY | float64 | Vertical position of the mouse relative to the upper left corner of the screen | +| CtrlKey | bool | The Ctrl key was active when the event occurred. | +| ShiftKey | bool | The Shift key was active when the event occurred. | +| AltKey | bool | The Alt (Option or ⌥ in OS X) key was active when the event occurred. | +| MetaKey | bool | The Meta key (for Mac this is the ⌘ Command key, for Windows is the Windows key ⊞) was active when the event occurred. | + +Button field can take the following values + +| Value | Constant | Description | +|:-----:|----------------------|--------------------------------------------------------------------------------------| +| <0 | | No buttons pressed | +| 0 | PrimaryMouseButton | Main button. Usually the left mouse button (can be changed in the OS settings) | +| 1 | AuxiliaryMouseButton | Auxiliary button (wheel or middle mouse button) | +| 2 | SecondaryMouseButton | Secondary button. Usually the right mouse button (can be changed in the OS settings) | +| 3 | MouseButton4 | Fourth mouse button. Usually the browser's Back button | +| 4 | MouseButton5 | Fifth mouse button. Usually the browser button Forward | + +The Button field is a bit mask combining (using OR) the following values + +| Value | Constant | Description | +|:-----:|--------------------|------------------| +| 1 | PrimaryMouseMask | Main button | +| 2 | SecondaryMouseMask | Secondary button | +| 4 | AuxiliaryMouseMask | Auxiliary button | +| 8 | MouseMask4 | Fourth button | +| 16 | MouseMask5 | Fifth button | + +You can also use listeners in the following formats: + +* func(MouseEvent) +* func(View) +* func() + +You can get lists of listeners for mouse events using the functions: + + func GetMouseDownListeners(view View, subviewID string) []func(View, MouseEvent) + func GetMouseUpListeners(view View, subviewID string) []func(View, MouseEvent) + func GetMouseMoveListeners(view View, subviewID string) []func(View, MouseEvent) + func GetMouseOverListeners(view View, subviewID string) []func(View, MouseEvent) + func GetMouseOutListeners(view View, subviewID string) []func(View, MouseEvent) + func GetClickListeners(view View, subviewID string) []func(View, MouseEvent) + func GetDoubleClickListeners(view View, subviewID string) []func(View, MouseEvent) + func GetContextMenuListeners(view View, subviewID string) []func(View, MouseEvent) + +## Pointer Events + +A pointer is a device-independent representation of input devices (such as a mouse, pen, +or point of contact on a touch surface). A pointer can point to a specific coordinate +(or set of coordinates) on a contact surface such as a screen. + +All pointers can generate several kinds of events + +| Event | Constant | Description | +|------------------|---------------|-------------------------------------------------------------------------| +| "pointer-down" | PointerDown | The pointer was pressed. | +| "pointer-up" | PointerUp | The pointer was released. | +| "pointer-move" | PointerMove | The pointer has been moved | +| "pointer-cancel" | PointerCancel | Pointer events aborted. | +| "pointer-out" | PointerOut | The pointer went out of bounds of the View, or went into the child View | +| "pointer-over" | PointerOver | The pointer is within the limits of View | + +The main event data listener has the following format: + + func(View, PointerEvent) + +where the second argument describes the parameters of the pointer. PointerEvent structure extends MouseEvent structure +and has the following additional fields: + +| Field | Type | Description | +|--------------------|---------|------------------------------------------------------------------------| +| PointerID | int | The unique identifier of the pointer that raised the event. | +| Width | float64 | The width (X-axis value) in pixels of the pointer's contact geometry. | +| Height | float64 | The height (Y-axis value) in pixels of the pointer's contact geometry. | +| Pressure | float64 | Normalized gauge inlet pressure ranging from 0 to 1, where 0 and 1 represent the minimum and maximum pressure that the hardware is capable of detecting, respectively. | +| TangentialPressure | float64 | Normalized gauge inlet tangential pressure (also known as cylinder pressure or cylinder voltage) ranges from -1 to 1, where 0 is the neutral position of the control. | +| TiltX | float64 | The planar angle (in degrees, ranging from -90 to 90) between the Y – Z plane and the plane that contains both the pointer (such as a stylus) axis and the Y axis. | +| TiltY | float64 | The planar angle (in degrees, ranging from -90 to 90) between the X – Z plane and the plane containing both the pointer (such as a stylus) axis and the X axis. | +| Twist | float64 | Rotation of a pointer (for example, a stylus) clockwise around its main axis in degrees with a value in the range from 0 to 359. | +| PointerType | string | the type of device that triggered the event: "mouse", "pen", "touch", etc. | +| IsPrimary | bool | a pointer is the primary pointer of this type. | + +You can also use listeners in the following formats: + +* func(PointerEvent) +* func(View) +* func() + +You can get lists of pointer event listeners using the functions: + + func GetPointerDownListeners(view View, subviewID string) []func(View, PointerEvent) + func GetPointerUpListeners(view View, subviewID string) []func(View, PointerEvent) + func GetPointerMoveListeners(view View, subviewID string) []func(View, PointerEvent) + func GetPointerCancelListeners(view View, subviewID string) []func(View, PointerEvent) + func GetPointerOverListeners(view View, subviewID string) []func(View, PointerEvent) + func GetPointerOutListeners(view View, subviewID string) []func(View, PointerEvent) + +### Touch events + +These events are used to track multipoint touches. Single touches emulate mouse events. +If you do not need to track multi-point touches, then it is easier to use mouse events + +| Event | Constant | Description | +|----------------|-------------|--------------------------------------| +| "touch-start" | TouchStart | The surface touched. | +| "touch-end" | TouchEnd | Surface touch completed. | +| "touch-move" | TouchMove | One or more touches changed position | +| "touch-cancel" | TouchCancel | The touch is interrupted. | + +The main event data listener has the following format: + + func(View, TouchEvent) + +where the second argument describes the touch parameters. The TouchEvent structure has the following fields: + +| Field | Type | Description | +|-----------|---------|-----------------------------------------------------------------------| +| TimeStamp | uint64 | The time the event was created (in milliseconds). The starting point depends on the browser implementation (EPOCH, browser launch, etc.). | +| Touches | []Touch | Array of Touch structures, each describing one touch | +| CtrlKey | bool | The Ctrl key was active when the event occurred. | +| ShiftKey | bool | The Shift key was active when the event occurred. | +| AltKey | bool | The Alt (Option or ⌥ in OS X) key was active when the event occurred. | +| MetaKey | bool | The Meta key (for Mac, this is the ⌘ Command key; for Windows, the Windows key ⊞) was active when the event occurred. | + +The Touch structure describes a single touch and has the following fields + +| Field | Type | Description | +|---------------|---------|---------------------------------------------------------------------------------------------------------| +| Identifier | int | A unique identifier assigned to each touch and does not change until it is completed. | +| X | float64 | The horizontal position of the mouse relative to the origin View | +| Y | float64 | The vertical position of the mouse relative to the origin View | +| ClientX | float64 | Horizontal position of the mouse relative to the upper left corner of the application | +| ClientY | float64 | The vertical position of the mouse relative to the upper left corner of the application | +| ScreenX | float64 | Horizontal position of the mouse relative to the upper left corner of the screen | +| ScreenY | float64 | Vertical position of the mouse relative to the upper left corner of the screen | +| RadiusX | float64 | The x-radius of the ellipse, in pixels, that most closely delimits the area of ​​contact with the screen. | +| RadiusY | float64 | The y-radius of the ellipse, in pixels, that most closely delimits the area of ​​contact with the screen. | +| RotationAngle | float64 | The angle (in degrees) to rotate the ellipse clockwise, described by the radiusX and radiusY parameters, to best cover the contact area between the user and the surface. | +| Force | float64 | The amount of pressure from 0.0 (no pressure) to 1.0 (maximum pressure) that the user applies to the surface. | + +You can also use listeners in the following formats: + +* func(TouchEvent) +* func(View) +* func() + +You can get lists of listeners for touch events using the functions: + + func GetTouchStartListeners(view View, subviewID string) []func(View, TouchEvent) + func GetTouchEndListeners(view View, subviewID string) []func(View, TouchEvent) + func GetTouchMoveListeners(view View, subviewID string) []func(View, TouchEvent) + func GetTouchCancelListeners(view View, subviewID string) []func(View, TouchEvent) + +### Resize-event + +The "resize-event" (ResizeEvent constant) is called when the View changes its position and/or size. +The main event data listener has the following format: + + func(View, Frame) + +where the structure is declared as + + type Frame struct { + Left, Top, Width, Height float64 + } + +Frame elements contain the following data +* Left - the new horizontal offset in pixels relative to the parent View (left position); +* Top - the new vertical offset in pixels relative to the parent View (top position) +* Width - the new width of the visible part of the View in pixels; +* Height - the new height of the visible part of the View in pixels. + +You can also use listeners in the following formats: + +* func(Frame) +* func(View) +* func() + +You can get a list of listeners for this event using the function: + + func GetResizeListeners(view View, subviewID string) []func(View, Frame) + +The current position and dimensions of the visible part of the View can be obtained using the View interface function: + + Frame() Frame + +or global function + + func GetViewFrame(view View, subviewID string) Frame + +### Scroll event + +The "scroll-event" (ScrollEvent constant) is raised when the contents of the View are scrolled. +The main event data listener has the following format: + + func(View, Frame) + +where the Frame elements contain the following data +* Left - the new horizontal shift of the visible area (in pixels); +* Top - the new vertical offset of the visible area (in pixels); +* Width - the total width of the View in pixels; +* Height - the total height of the View in pixels. + +You can also use listeners in the following formats: + +* func(Frame) +* func(View) +* func() + +You can get a list of listeners for this event using the function: + + func GetScrollListeners(view View) []func(View, Frame) + +The current position of the viewable area and the overall dimensions of the View can be obtained using the View interface function: + + Scroll() Frame + +or global function + + func GetViewScroll(view View, subviewID string) Frame + +The following global functions can be used for manual scrolling + + func ScrollViewTo(view View, subviewID string, x, y float64) + func ScrollViewToStart(view View, subviewID string) + func ScrollViewToEnd(view View, subviewID string) + +which scroll the view, respectively, to the given position, start and end + +## ViewsContainer + +The ViewsContainer interface, which implements View, describes a container that contains +several child interface elements (View). ViewsContainer is the base for other containers +(ListLayout, GridLayout, StackLayout, etc.) and is not used on its own. + +In addition to all View properties, this element has only one additional property "content" + +### "content" property + +The "content" property (constant Сontent) defines an array of child Views. Interface Get function +always returns []View for the given property. + +The following 5 data types can be passed as the value of the "content" property: + +* View - converted to []View containing one element; + +* []View - nil-elements are prohibited, if the array contains nil, then the property will not be set, +and the Set function will return false and an error message will be written to the log; + +* string - if the string is a text representation of the View, then the corresponding View is created, +otherwise a TextView is created, to which the given string is passed as text. +Next, a []View is created containing the resulting View; + +* []string - each element of the array is converted to View as described in the previous paragraph; + +* []interface{} - this array must contain only View and string. Each string element is converted to +a View as described above. If the array contains invalid values, the "content" property will not be set, +and the Set function will return false and an error message will be written to the log. + +You can learn the value of the "content" property using the ViewsContainer interface function + + Views() []View + +The following functions of the ViewsContainer interface can be used to edit the "content" property: + + Append(view View) + +This function adds an argument to the end of the View list. + + Insert(view View, index uint) + +This function inserts an argument at the specified position in the View list. +If index is greater than the length of the list, then the View is added to the end of the list. +If index is less than 0, then to the beginning of the list. + + RemoveView(index uint) View + +This function removes the View from the given position and returns it. +If index points outside the bounds of the list, then nothing is removed, and the function returns nil. + +## ListLayout + +ListLayout is a container that implements the ViewsContainer interface. To create it, use the function + + func NewListLayout(session Session, params Params) ListLayout + +Items in this container are arranged as a list. The position of the children can be controlled. +For this, ListLayout has a number of properties + +### "orientation" property + +The "orientation" int property (Orientation constant) specifies how the children will be positioned +relative to each other. The property can take the following values: + +| Value | Constant | Location | +|:-----:|-----------------------|--------------------------------------------------------------| +| 0 | TopDownOrientation | Child elements are arranged in a column from top to bottom. | +| 1 | StartToEndOrientation | Child elements are laid out in a row from beginning to end. | +| 2 | BottomUpOrientation | Child elements are arranged in a column from bottom to top. | +| 3 | EndToStartOrientation | Child elements are laid out in a line from end to beginning. | + +The start and end positions for StartToEndOrientation and EndToStartOrientation depend on the value +of the "text-direction" property. For languages ​​written from right to left (Arabic, Hebrew), +the beginning is on the right, for other languages ​​- on the left. + +### "wrap" property + +The "wrap" int property (Wrap constant) defines the position of elements in case of reaching +the border of the container. There are three options: + +* WrapOff (0) - the column / row of elements continues and goes beyond the bounds of the visible area. + +* WrapOn (1) - starts a new column / row of items. The new column is positioned towards the end +(for the position of the beginning and end, see above), the new line is at the bottom. + +* WrapReverse (2) - starts a new column / row of elements. The new column is positioned towards the beginning +(for the position of the beginning and end, see above), the new line is at the top. + +### "vertical-align" property + +The "vertical-align" property (VerticalAlign constant) of type int sets the vertical +alignment of items in the container. Valid values: + +| Value | Constant | Name | Alignment | +|:-----:|--------------|-----------|------------------| +| 0 | TopAlign | "top" | Top alignment | +| 1 | BottomAlign | "bottom" | Bottom alignment | +| 2 | CenterAlign | "center" | Center alignment | +| 3 | StretchAlign | "stretch" | Height alignment | + +### "horizontal-align" property + +The "horizontal-align" int property (HorizontalAlign constant) sets the horizontal +alignment of items in the list. Valid values: + +| Value | Constant | Name | Alignment | +|:-----:|--------------|-----------|------------------| +| 0 | LeftAlign | "left" | Left alignment | +| 1 | RightAlign | "right" | Right alignment | +| 2 | CenterAlign | "center" | Center alignment | +| 3 | StretchAlign | "stretch" | Width alignment | + +## GridLayout + +GridLayout is a container that implements the ViewsContainer interface. To create it, use the function + + func NewGridLayout(session Session, params Params) GridLayout + +The container space of this container is split into cells in the form of a table. +All children are located in the cells of the table. A cell is addressed by row and column number. +Row and column numbers start at 0. + +### "column" and "row" properties + +The location of the View inside the GridLayout is determined using the "column" and "row" properties. +These properties must be set for each of the child Views. +Child View can span multiple cells within the GridLayout and they can overlap. + +The values "column" and "row" can be set by: + +* an integer greater than or equal to 0; + +* textual representation of an integer greater than or equal to 0 or a constant; + +* a Range structure specifying a range of rows / columns: + + type Range struct { + First, Last int + } + +where First is the number of the first column / row, Last is the number of the last column / row; + +* a line of the form "< number of the first column / row >: < number of the last column / row >", +which is a textual representation of the Range structure + +Example + + grid := rui.NewGridLayout(session, rui.Params { + rui.Content : []View{ + NewView(session, rui.Params { + rui.ID : "view1", + rui.Row : 0, + rui.Column : rui.Range{ First: 1, Last: 2 }, + }), + NewView(session, rui.Params { + rui.ID : "view2", + rui.Row : "0:2", + rui.Column : "0", + }), + }, + }) + +In this example, view1 occupies columns 1 and 2 in row 0, and view1 occupies rows 0, 1, and 2 in column 0. + +### "cell-width" and "cell-height" properties + +By default, the sizes of the cells are calculated based on the sizes of the child Views placed in them. +The "cell-width" and "cell-height" properties (CellWidth and CellHeight constants) allow you to set +a fixed width and height of cells regardless of the size of the child elements. +These properties are of type []SizeUnit. Each element in the array determines the size of the corresponding column or row. + +These properties can be assigned the following data types: + +* SizeUnit or textual representation of SizeUnit (or SizeUnit constant). In this case, the corresponding dimensions of all cells are set to the same; + +* [] SizeUnit; + +* string containing textual representations of SizeUnit (or SizeUnit constants) separated by commas; + +* [] string. Each element must be a textual representation of a SizeUnit (or a SizeUnit constant) + +* [] interface {}. Each element must either be of type SizeUnit or be a textual representation of SizeUnit (or a SizeUnit constant) + +If the number of elements in the "cell-width" and "cell-height" properties is less than the number of columns and rows used, then the missing elements are set to Auto. + +The values of the "cell-width" and "cell-height" properties can use the SizeUnit type SizeInFraction. +This type means 1 part. The part is calculated as follows: the size of all cells +that are not of type SizeInFraction is subtracted from the size of the container, +and then the remaining size is divided by the number of parts. +The SizeUnit value of type SizeInFraction can be either integer or fractional. + +### "grid-row-gap" and "grid-column-gap" properties + +The "grid-row-gap" and "grid-column-gap" SizeUnit properties (GridRowGap and GridColumnGap constants) +allow you to set the distance between the rows and columns of the container, respectively. The default is 0px. + +### "cell-vertical-align" property + +The "cell-vertical-align" property (constant CellVerticalAlign) of type int sets the vertical alignment of children within the cell they are occupying. Valid values: + +| Value | Constant | Name | Alignment | +|:-----:|--------------|-----------|---------------------| +| 0 | TopAlign | "top" | Top alignment | +| 1 | BottomAlign | "bottom" | Bottom alignment | +| 2 | CenterAlign | "center" | Center alignment | +| 3 | StretchAlign | "stretch" | Full height stretch | + +The default value is StretchAlign (3) + +### "cell-horizontal-align" property + +The "cell-horizontal-align" property (constant CellHorizontalAlign) of type int sets the horizontal alignment of children within the occupied cell. Valid values: + +| Value | Constant | Name | Alignment | +|:-----:|--------------|-----------|--------------------| +| 0 | LeftAlign | "left" | Left alignment | +| 1 | RightAlign | "right" | Right alignment | +| 2 | CenterAlign | "center" | Center alignment | +| 3 | StretchAlign | "stretch" | Full width stretch | + +The default value is StretchAlign (3) + +## ColumnLayout + +ColumnLayout is a container that implements the ViewsContainer interface. +All child Views are arranged in a vertical list aligned to the left or right and split into several columns. +The alignment depends on the "text-direction" property. + +To create the ColumnLayout, use the function + + func NewColumnLayout(session Session, params Params) ColumnLayout + +### "column-count" property + +The "column-count" int property (ColumnCount constant) sets the number of columns. + +If this property is 0 and the "column-width" property is not set, +then no column splitting is performed and the container is scrolled down. + +If the value of this property is greater than 0, then the list is split into columns. +The column height is equal to the ColumnLayout height, and the width is calculated +as the ColumnLayout width divided by "column-count". Each next column is located depending +on the "text-direction" property to the right or left of the previous one, and the container is scrolled horizontally. + +You can get the value of this property using the function + + func GetColumnCount(view View, subviewID string) int + +### "column-width" property + +The "column-width" SizeUnit property (ColumnWidth constant) sets the column width. +This property is used only if "column-count" is 0, otherwise it is ignored. + +IMPORTANT! Percentages cannot be used as the "column-width" value (i.e. if you specify a value in percent, the system will ignore it) + +You can get the value of this property using the function + + func GetColumnWidth(view View, subviewID string) SizeUnit + +### "column-gap" property + +The "column-gap" SizeUnit property (ColumnGap constant) sets the width of the gap between columns. + +You can get the value of this property using the function + + func GetColumnGap(view View, subviewID string) SizeUnit + +### "column-separator" property + +The "column-separator" property (ColumnSeparator constant) allows you to set a line that will be drawn at column breaks. +The separator line is described by three attributes: line style, thickness, and color. + +The value of the "column-separator" property is stored as the ColumnSeparatorProperty interface, +which implements the Properties interface (see above). ColumnSeparatorProperty can contain the following properties: + +| Property | Constant | Type | Description | +|----------|---------------|----------|----------------| +| "style" | Style | int | Line style | +| "width" | Width | SizeUnit | Line thickness | +| "color" | ColorProperty | Color | Line color | + +Line style can take the following values: + +| Value | Constant | Name | Description | +|:-----:|------------|----------| ------------------| +| 0 | NoneLine | "none" | No frame | +| 1 | SolidLine | "solid" | Solid line | +| 2 | DashedLine | "dashed" | Dashed line | +| 3 | DottedLine | "dotted" | Dotted line | +| 4 | DoubleLine | "double" | Double solid line | + +All other style values are ignored. + +To create the ColumnSeparatorProperty interface, use the function + + func NewColumnSeparator(params Params) ColumnSeparatorProperty + +The ColumnSeparatorProperty interface can be converted to a ViewBorder structure using the ViewBorder function. +When converted, all text constants are replaced with real values. ViewBorder is described as + + type ViewBorder struct { + Style int + Color Color + Width SizeUnit + } + +The ViewBorder structure can be passed as a parameter to the Set function when setting +the value of the "column-separator" property. This converts the ViewBorder to ColumnSeparatorProperty. +Therefore, when reading the property, the Get function will return the ColumnSeparatorProperty interface, +not the ViewBorder structure. + +You can get the ViewBorders structure without additional transformations using the global function + + func GetColumnSeparator(view View, subviewID string) ViewBorder + +You can also set individual line attributes using the Set function of the View interface. +For this, the following properties are used + +| Property | Constant | Type | Description | +|--------------------------|----------------------|----------|----------------| +| "column-separator-style" | ColumnSeparatorStyle | int | Line style | +| "column-separator-width" | ColumnSeparatorWidth | SizeUnit | Line thickness | +| "column-separator-color" | ColumnSeparatorColor | Color | Line color | + +For example + + view.Set(rui.ColumnSeparatorStyle, rui.SolidBorder) + view.Set(rui.ColumnSeparatorWidth, rui.Px(1)) + view.Set(rui.ColumnSeparatorColor, rui.Black) + +equivalent to + + view.Set(rui.ColumnSeparator, ColumnSeparatorProperty(rui.Params{ + rui.Style: rui.SolidBorder, + rui.Width: rui.Px(1), + rui.ColorProperty: rui.Black, + })) + +### "avoid-break" property + +When forming columns, ColumnLayout can break some types of View, so that the beginning +will be at the end of one column and the end in the next. For example, the TextView, +the title of the picture and the picture itself are broken, etc. + +The "avoid-break" bool property (AvoidBreak constant) avoids this effect. +You must set this property to "true" for a non-breakable View. +Accordingly, the value "false" of this property allows the View to be broken. +The default is "false". + +You can get the value of this property using the function + + func GetAvoidBreak(view View, subviewID string) bool + +## StackLayout + +StackLayout is a container that implements the ViewsContainer interface. +All child Views are stacked on top of each other and each takes up the entire container space. +Only one child View (current) is available at a time. + +To create a StackLayout, use the function + + func NewStackLayout(session Session, params Params) StackLayout + +In addition to the Append, Insert, RemoveView properties and the "content" property of the ViewsContainer, +the StackLayout container has two other interface functions for manipulating child Views: Push and Pop. + + Push(view View, animation int, onPushFinished func()) + +This function adds a new View to the container and makes it current. +It is similar to Append, but the addition is done using an animation effect. +The animation type is specified by the second argument and can take the following values: + +| Value | Constant | Animation | +|:-----:|---------------------|-----------------------------| +| 0 | DefaultAnimation | Default animation. For the Push function it is EndToStartAnimation, for Pop - StartToEndAnimation | +| 1 | StartToEndAnimation | Animation from beginning to end. The beginning and the end are determined by the direction of the text output | +| 2 | EndToStartAnimation | End-to-Beginning animation. | +| 3 | TopDownAnimation | Top-down animation. | +| 4 | BottomUpAnimation | Bottom up animation. | + +The third argument onPushFinished is the function to be called when the animation ends. It may be nil. + + Pop(animation int, onPopFinished func(View)) bool + +This function removes the current View from the container using animation. +The second argument onPopFinished is the function to be called when the animation ends. It may be nil. +The function will return false if the StackLayout is empty and true if the current item has been removed. + + You can get the current (visible) View using the interface function + + Peek() View + +In order to make any child View current (visible), the interface functions are used: + + MoveToFront(view View) bool + MoveToFrontByID(viewID string) bool + +This function will return true if successful and false if the child View or +View with id does not exist and an error message will be written to the log. + +## AbsoluteLayout + +AbsoluteLayout is a container that implements the ViewsContainer interface. +Child Views can be positioned at arbitrary positions in the container space. + +To create an AbsoluteLayout, use the function + + func NewAbsoluteLayout(session Session, params Params) AbsoluteLayout + +The child View is positioned using the properties of the SizeUnit type: "left", "right", "top" and "bottom" +(respectively, the constants Left, Right, Top and Bottom). You can set any of these properties on the child View. +If neither "left" or "right" is specified, then the child View will be pinned to the left edge of the container. +If neither top nor bottom is specified, then the child View will be pinned to the top edge of the container. + +## DetailsView + +DetailsView is a container that implements the ViewsContainer interface. +To create a DetailsView, the function is used + + func NewDetailsView(session Session, params Params) DetailsView + +In addition to child Views, this container has a "summary" property (Summary constant). +The value of the "summary" property can be either View or a string of text. + +The DetailsView can be in one of two states: + +* only the content of the "summary" property is displayed. Child Views are hidden and do not take up screen space + +* the content of the "summary" property is displayed first, and below the child Views. +The layout of the child Views is the same as ColumnLayout with "column-count" equal to 0. + +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 + + func IsDetailsExpanded(view View, subviewID string) bool + +and the value of the "summary" property can be obtained using the function + + func GetDetailsSummary(view View, subviewID string) View + +## Resizable + +Resizable is a container in which only one View can be placed. Resizable allows you to interactively resize the content View. +To create a Resizable view, the function is used + + func NewResizable(session Session, params Params) Resizable + +A frame is created around the content View, and you can drag it to resize. + +Resizable does not implement the ViewsContainer interface. Only the Content property is used to control the content View. +This property can be assigned a value of type View or a string of text. In the second case, a TextView is created. + +The frame around the content View can be either from all sides, or only from separate ones. +To set the sides of the frame, use the "side" int property (Side constant). +It can take the following values: + +| Value | Constant | Name | Frame side | +|:-----:|------------|----------|--------------------------| +| 1 | TopSide | "top" | Top | +| 2 | RightSide | "right" | Right | +| 4 | BottomSide | "bottom" | Bottom | +| 8 | LeftSide | "left" | Left | +| 15 | AllSides | "all" | All sides. Default value | + +In addition to these values, an or-combination of TopSide, RightSide, BottomSide and LeftSide can also be used. +AllSides is defined as + + AllSides = TopSide | RightSide | BottomSide | LeftSide + +To set the border width, use the SizeUnit property "resize-border-width" (ResizeBorderWidth constant). +The default value of "resize-border-width" is 4px. + +## TextView + +The TextView element, which extends the View interface, is intended for displaying text. + +To create a TextView, the function is used: + + func NewTextView(session Session, params Params) TextView + +The displayed text is set by the string property "text" (Text constant). +In addition to the Get method, the value of the "text" property can be obtained using the function + + func GetText (view View, subviewID string) string + +TextView inherits from View all properties of text parameters ("font-name", "text-size", "text-color", etc.). +In addition to them, the "text-overflow" int property (TextOverflow constant) is added. +It determines how the text is cut if it goes out of bounds. +This property of type int can take the following values + +| Value | Constant | Name | Cropping Text | +|:-----:|----------------------| -----------|-------------------------------------------------------------| +| 0 | TextOverflowClip | "clip" | Text is clipped at the border (default) | +| 1 | TextOverflowEllipsis | "ellipsis" | At the end of the visible part of the text '…' is displayed | + +## EditView + +The EditView element is a test editor and extends the View interface. + +To create an EditView, the function is used: + + func NewEditView(session Session, params Params) EditView + +Several options for editable text are possible. The type of the edited text is set using +the int property "edit-view-type" (EditViewType constant). +This property can take the following values: + +| Value | Constant | Name | Editor type | +|:-----:|----------------|-------------|--------------------------------------------------| +| 0 | SingleLineText | "text" | One-line text editor. Default value | +| 1 | PasswordText | "password" | Password editor. The text is hidden by asterisks | +| 2 | EmailText | "email" | Single e-mail editor | +| 3 | EmailsText | "emails" | Multiple e-mail editor | +| 4 | URLText | "url" | Internet address input editor | +| 5 | PhoneText | "phone" | Phone number editor | +| 6 | MultiLineText | "multiline" | Multi-Line Text Editor | + +To simplify the text of the program, you can use the "type" properties (Type constant) instead of the "edit-view-type". +These property names are synonymous. But when describing the style, "type" cannot be used. + +To set/get edited text, use the string property "text" (Text constant) + +The maximum length of editable text is set using the "max-length" int property (MaxLength constant). + +You can limit the input text using a regular expression. To do this, use the string property +"edit-view-pattern" (EditViewPattern constant). Instead of "edit-view-pattern", you can use the synonym "pattern" +(Pattern constant), except for the style description. + +To prohibit text editing, use the bool property "readonly" (ReadOnly constant). + +To enable / disable the built-in spell checker, use the bool "spellcheck" property (Spellcheck constant). +Spell checking can only be enabled if the editor type is set to SingleLineText or MultiLineText. + +For the editor, you can set a hint that will be shown while the editor is empty. +To do this, use the string property "hint" (Hint constant). + +For a multi-line editor, auto-wrap mode can be enabled. The bool property "wrap" (constant Wrap) is used for this. +If "wrap" is off (default), then horizontal scrolling is used. +If enabled, the text wraps to a new line when the EditView border is reached. + +The following functions can be used to get the values ​​of the properties of an EditView: + + func GetText(view View, subviewID string) string + func GetHint(view View, subviewID string) string + func GetMaxLength(view View, subviewID string) int + func GetEditViewType(view View, subviewID string) int + func GetEditViewPattern(view View, subviewID string) string + func IsReadOnly(view View, subviewID string) bool + func IsEditViewWrap(view View, subviewID string) bool + func IsSpellcheck(view View, subviewID string) bool + +The "edit-text-changed" event (EditTextChangedEvent constant) is used to track changes to the text. +The main event listener has the following format: + + func(EditView, string) + +where the second argument is the new text value + +You can get the current list of text change listeners using the function + + func GetTextChangedListeners(view View, subviewID string) []func(EditView, string) + +## NumberPicker + +The NumberPicker element extends the View interface to enter numbers. + +To create a NumberPicker, the function is used: + + func NewNumberPicker(session Session, params Params) NumberPicker + +NumberPicker can work in two modes: text editor and slider. +The mode sets the int property "date-picker-type" (NumberPickerType constant). +The "date-picker-type" property can take the following values: + +| Value | Constant | Name | Editor type | +|:-----:|--------------|----------|----------------------------| +| 0 | NumberEditor | "editor" | Text editor. Default value | +| 1 | NumberSlider | "slider" | Slider | + +You can set/get the current value using the "date-picker-value" property (NumberPickerValue constant). +The following can be passed as a value to the "date-picker-value" property: + +* float64 +* float32 +* int +* int8 … int64 +* uint +* uint8 … uint64 +* textual representation of any of the above types + +All of these types are cast to float64. Accordingly, the Get function always returns a float64 value. +The value of the "date-picker-value" property can also be read using the function: + + func GetNumberPickerValue(view View, subviewID string) float64 + +The entered values ​​may be subject to restrictions. For this, the following properties are used: + +| Property | Constant | Restriction | +|--------------------|------------------|-------------------| +| "date-picker-min" | NumberPickerMin | Minimum value | +| "date-picker-max" | NumberPickerMax | Maximum value | +| "date-picker-step" | NumberPickerStep | Value change step | + +Assignments to these properties can be the same value types as "date-picker-value". + +By default, if "date-picker-type" is equal to NumberSlider, the minimum value is 0, maximum is 1. +If "date-picker-type" is equal to NumberEditor, then the entered numbers, by default, are limited only by the range of float64 values. + +You can read the values ​​of these properties using the functions: + + func GetNumberPickerMinMax(view View, subviewID string) (float64, float64) + func GetNumberPickerStep(view View, subviewID string) float64 + +The "date-changed" event (NumberChangedEvent constant) is used to track the change in the entered value. +The main event listener has the following format: + + func(picker NumberPicker, newValue float64) + +where the second argument is the new value + +You can get the current list of value change listeners using the function + + func GetNumberChangedListeners(view View, subviewID string) []func(NumberPicker, float64) + +## DatePicker + +The DatePicker element extends the View interface to enter dates. + +To create DatePicker function is used: + + func NewDatePicker(session Session, params Params) DatePicker + +You can set/get the current value using the "date-picker-value" property (the DatePickerValue constant). +The following can be passed as a value to the "date-picker-value" property: + +* time.Time +* constant +* text that can be converted to time.Time by function + + func time.Parse(layout string, value string) (time.Time, error) + +The text is converted to time.Time. Accordingly, the Get function always returns a time.Time value. +The value of the "date-picker-value" property can also be read using the function: + + func GetDatePickerValue(view View, subviewID string) time.Time + +The dates you enter may be subject to restrictions. For this, the following properties are used: + +| Property | Constant | Data type | Restriction | +|--------------------|----------------|-----------|--------------------------| +| "date-picker-min" | DatePickerMin | time.Time | Minimum date value | +| "date-picker-max" | DatePickerMax | time.Time | Maximum date value | +| "date-picker-step" | DatePickerStep | int | Date change step in days | + +You can read the values of these properties using the functions: + + func GetDatePickerMin(view View, subviewID string) (time.Time, bool) + func GetDatePickerMax(view View, subviewID string) (time.Time, bool) + func GetDatePickerStep(view View, subviewID string) int + +The "date-changed" event (DateChangedEvent constant) is used to track the change in the entered value. +The main event listener has the following format: + + func(picker DatePicker, newDate time.Time) + +where the second argument is the new date value + +You can get the current list of date change listeners using the function + + func GetDateChangedListeners(view View, subviewID string) []func(DatePicker, time.Time) + +## TimePicker + +The TimePicker element extends the View interface and is intended for entering time. + +To create a TimePicker, the function is used: + + func NewTimePicker(session Session, params Params) TimePicker + +You can set/get the current value using the "time-picker-value" property (TimePickerValue constant). +The following can be passed as a value to the "time-picker-value" property: + +* time.Time +* constant +* text that can be converted to time.Time by function + + func time.Parse(layout string, value string) (time.Time, error) + +The text is converted to time.Time. Accordingly, the Get function always returns a time.Time value. +The value of the "time-picker-value" property can also be read using the function: + + func GetTimePickerValue(view View, subviewID string) time.Time + +The time entered may be subject to restrictions. For this, the following properties are used: + +| Property | Constant | Data type | Restriction | +|--------------------|----------------|-----------|---------------------------| +| "time-picker-min" | TimePickerMin | time.Time | Minimum time value | +| "time-picker-max" | TimePickerMax | time.Time | The maximum value of time | +| "time-picker-step" | TimePickerStep | int | Time step in seconds | + +You can read the values of these properties using the functions: + + func GetTimePickerMin(view View, subviewID string) (time.Time, bool) + func GetTimePickerMax(view View, subviewID string) (time.Time, bool) + func GetTimePickerStep(view View, subviewID string) int + +The "time-changed" event (TimeChangedEvent constant) is used to track the change in the entered value. +The main event listener has the following format: + + func(picker TimePicker, newTime time.Time) + +where the second argument is the new time value + +You can get the current list of date change listeners using the function + + func GetTimeChangedListeners(view View, subviewID string) []func(TimePicker, time.Time) + +## ColorPicker + +The ColorPicker element extends the View interface and is designed to select a color in RGB format without an alpha channel. + +To create a ColorPicker, the function is used: + + func NewColorPicker(session Session, params Params) ColorPicker + +You can set/get the current color value using the "color-picker-value" property (ColorPickerValue constant). +The following can be passed as a value to the "color-picker-value" property: + +* Color +* text representation of Color +* constant + +The value of the property "color-picker-value" can also be read using the function: + + func GetColorPickerValue(view View, subviewID string) Color + +The "color-changed" event (ColorChangedEvent constant) is used to track the change in the selected color. +The main event listener has the following format: + + func(picker ColorPicker, newColor Color) + +where the second argument is the new color value + +You can get the current list of date change listeners using the function + + func GetColorChangedListeners(view View, subviewID string) []func(ColorPicker, Color) + +## DropDownList + +The DropDownList element extends the View interface and is designed to select a value from a drop-down list. + +To create a DropDownList, use the function: + + func NewDropDownList(session Session, params Params) DropDownList + +The list of possible values is set using the "items" property (Items constant). +The following data types can be passed as a value to the "items" property + +* []string +* []fmt.Stringer +* []interface{} containing as elements only: string, fmt.Stringer, bool, rune, +float32, float64, int, int8 … int64, uint, uint8 … uint64. + +All of these data types are converted to []string and assigned to the "items" property. +You can read the value of the "items" property using the function + + func GetDropDownItems(view View, subviewID string) []string + +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 + + func GetDropDownCurrent(view View, subviewID string) int + +To track the change of the "current" property, the "drop-down-event" event (DropDownEvent constant) is used. +The main event listener has the following format: + + func(list DropDownList, newCurrent int) + +where the second argument is the index of the selected item + +You can get the current list of date change listeners using the function + + func GetDropDownListeners(view View, subviewID string) []func(DropDownList, int) + +## ProgressBar + +The DropDownList element extends the View interface and is designed to display progress as a fillable bar. + +To create a ProgressBar, the function is used: + + func NewProgressBar(session Session, params Params) ProgressBar + +ProgressBar has two float64 properties: +* "progress-max" (ProgressBarMax constant) - maximum value (default 1); +* "progress-value" (ProgressBarValue constant) - current value (default 0). + +The minimum is always 0. +In addition to float64, float32, int, int8 … int64, uint, uint8 … uint64 + +You can read the value of these properties using the functions + + func GetProgressBarMax(view View, subviewID string) float64 + func GetProgressBarValue(view View, subviewID string) float64 + +## Button + +The Button element implements a clickable button. This is a CustomView (about it below) based on ListLayout and, +accordingly, has all the properties of ListLayout. But unlike ListLayout, it can receive input focus. + +Content is centered by default. + +To create a Button, use the function: + + func NewButton(session Session, params Params) Button + +## ListView + +The ListView element implements a list. +The ListView is created using the function: + + func NewListView(session Session, params Params) ListView + +### The "items" property + +List items are set using the "items" property (Items constant). +The main value of the "items" property is the ListAdapter interface: + + type ListAdapter interface { + ListSize() int + ListItem(index int, session Session) View + IsListItemEnabled(index int) bool + } + +Accordingly, the functions of this interface must return the number of elements, +the View of the i-th element and the status of the i-th element (allowed/denied). + +You can implement this interface yourself or use helper functions: + + func NewTextListAdapter(items []string, params Params) ListAdapter + func NewViewListAdapter(items []View) ListAdapter + +NewTextListAdapter creates an adapter from an array of strings, the second argument +is the parameters of the TextView used to display the text. +NewViewListAdapter creates an adapter from the View array. + +The "items" property can be assigned the following data types: + +* ListAdapter; +* [] View, when assigned, is converted to a ListAdapter using the NewViewListAdapter function; +* [] string, when assigned, is converted to a ListAdapter using the NewTextListAdapter function; +* [] interface {} which can contain elements of type View, string, fmt.Stringer, bool, rune, +float32, float64, int, int8 ... int64, uint, uint8 ... uint64. +When assigning, all types except View and string are converted to string, then all string in TextView +and from the resulting View array using the NewViewListAdapter function, a ListAdapter is obtained. + +If the list items change during operation, then after the change, either the ReloadListViewData() +function of the ListView interface or the global ReloadListViewData(view View, subviewID string) function must be called. +These functions update the displayed list items. + +### "Orientation" property + +List items can be arranged both vertically (in columns) and horizontally (in rows). +The "orientation" property (Orientation constant) of int type specifies how the list items +will be positioned relative to each other. The property can take the following values: + +| Value | Constant | Location | +|:-----:|-----------------------|--------------------------------------------------------| +| 0 | TopDownOrientation | Items are arranged in a column from top to bottom. | +| 1 | StartToEndOrientation | Elements are laid out on a line from beginning to end. | +| 2 | BottomUpOrientation | Items are arranged in a column from bottom to top. | +| 3 | EndToStartOrientation | Elements are arranged in a row from end to beginning. | + +The start and end positions for StartToEndOrientation and EndToStartOrientation depend +on the value of the "text-direction" property. For languages ​​written from right to left +(Arabic, Hebrew), the beginning is on the right, for other languages ​​- on the left. + +You can get the value of this property using the function + + func GetListOrientation(view View, subviewID string) int + +### "wrap" property + +The "wrap" int property (Wrap constant) defines the position of elements +in case of reaching the border of the container. There are three options: + +* WrapOff (0) - the column/row of elements continues and goes beyond the bounds of the visible area. + +* WrapOn (1) - starts a new column/row of items. The new column is positioned towards the end +(for the position of the beginning and end, see above), the new line is at the bottom. + +* WrapReverse (2) - starts a new column/row of elements. The new column is positioned towards +the beginning (for the position of the beginning and end, see above), the new line is at the top. + +You can get the value of this property using the function + + func GetListWrap(view View, subviewID string) int + +### "item-width" and "item-height" properties + +By default, the height and width of list items are calculated based on their content. +This leads to the fact that the elements of the vertical list can have different heights, +and the elements of the horizontal - different widths. + +You can set a fixed height and width of the list item. To do this, use the SizeUnit +properties "item-width" and "item-height" + +You can get the values of these properties using the functions + + func GetListItemWidth(view View, subviewID string) SizeUnit + func GetListItemHeight(view View, subviewID string) SizeUnit + +### "item-vertical-align" property + +The "item-vertical-align" int property (ItemVerticalAlign constant) sets the vertical alignment +of the contents of the list items. Valid values: + +| Value | Constant | Name | Alignment | +|:-----:|--------------|-----------|------------------| +| 0 | TopAlign | "top" | Top alignment | +| 1 | BottomAlign | "bottom" | Bottom alignment | +| 2 | CenterAlign | "center" | Center alignment | +| 3 | StretchAlign | "stretch" | Height alignment | + +You can get the value of this property using the function + + func GetListItemVerticalAlign(view View, subviewID string) int + +### "item-horizontal-align" property + +The "item-horizontal-align" int property (ItemHorizontalAlign constant) sets the +horizontal alignment of the contents of the list items. Valid values: + +| Value | Constant | Name | Alignment | +|:-----:|--------------|-----------|------------------| +| 0 | LeftAlign | "left" | Left alignment | +| 1 | RightAlign | "right" | Right alignment | +| 2 | CenterAlign | "center" | Center alignment | +| 3 | StretchAlign | "stretch" | Height alignment | + +You can get the value of this property using the function + + GetListItemHorizontalAlign(view View, subviewID string) int + +### "current" property + +ListView allows you to select list items with the "allowed" status (see ListAdapter). +The item can be selected both interactively and programmatically. +To do this, use the int property "current" (constant Current). +The value "current" is less than 0 means that no item is selected + +You can get the value of this property using the function + + func GetListViewCurrent(view View, subviewID string) int + +### "list-item-style", "current-style", and "current-inactive-style" properties + +These three properties are responsible for the background style and text properties of each list item. + +| Property | Constant | Style | +|--------------------------|----------------------|----------------------------------------------------------| +| "list-item-style" | ListItemStyle | Unselected element style | +| "current-style" | CurrentStyle | The style of the selected item. ListView in focus | +| "current-inactive-style" | CurrentInactiveStyle | The style of the selected item. ListView is out of focus | + +### "checkbox", "checked", "checkbox-horizontal-align", and "checkbox-vertical-align" properties + +The "current" property allows you to select one item in the list. +The "checkbox" properties allow you to add a checkbox to each item in the list with which +you can select several items in the list. The "checkbox" int property (ItemCheckbox constant) +can take the following values + +| Value | Constant | Name | Checkbox view | +|:--------:|------------------|------------|----------------------------------------------------| +| 0 | NoneCheckbox | "none" | There is no checkbox. Default value | +| 1 | SingleCheckbox | "single" | ◉ A checkbox that allows you to mark only one item | +| 2 | MultipleCheckbox | "multiple" | ☑ A checkbox that allows you to mark several items | + + +You can get the value of this property using the function + + func GetListViewCheckbox(view View, subviewID string) int + +You can get/set the list of checked items using the "checked" property (Checked constant). +This property is of type []int and stores the indexes of the marked elements. +You can get the value of this property using the function + + func GetListViewCheckedItems(view View, subviewID string) []int + +You can check if a specific element is marked using the function + + func IsListViewCheckedItem(view View, subviewID string, index int) bool + +By default, the checkbox is located in the upper left corner of the element. +You can change its position using int properties "checkbox-horizontal-align" and +"checkbox-vertical-align" (CheckboxHorizontalAlign and CheckboxVerticalAlign constants) + +The "checkbox-horizontal-align" int property can take the following values: + +| Value | Constant | Name | Checkbox location | +|:-----:|--------------|----------|---------------------------------------------| +| 0 | LeftAlign | "left" | At the left edge. Content on the right | +| 1 | RightAlign | "right" | At the right edge. Content on the left | +| 2 | CenterAlign | "center" | Center horizontally. Content below or above | + +The "checkbox-vertical-align" int property can take the following values: + +| Value | Constant | Name | Checkbox location | +|:-----:|--------------|----------|-------------------| +| 0 | TopAlign | "top" | Top alignment | +| 1 | BottomAlign | "bottom" | Bottom alignment | +| 2 | CenterAlign | "center" | Center alignment | + +Special case where both "checkbox-horizontal-align" and "checkbox-vertical-align" are CenterAlign (2). +In this case, the checkbox is centered horizontally, the content is below + +You can get property values for "checkbox-horizontal-align" and "checkbox-vertical-align" using the functions + + func GetListViewCheckboxHorizontalAlign(view View, subviewID string) int + func GetListViewCheckboxVerticalAlign(view View, subviewID string) int + +### ListView events + +There are three specific events for ListView + +* "list-item-clicked" (ListItemClickedEvent constant) event occurs when the user clicks on a list item. +The main listener for this event has the following format: func(ListView, int). +Where the second argument is the index of the element. + +* "list-item-selected" (ListItemSelectedEvent constant) event occurs when the user selects a list item. +The main listener for this event has the following format: func(ListView, int). +Where the second argument is the index of the element. + +* "list-item-checked" (ListItemCheckedEvent constant) event occurs when the user checks/unchecks the checkbox of a list item. +The main listener for this event has the following format: func(ListView, []int). +Where the second argument is an array of indexes of the tagged items. + +You can get lists of listeners for these events using the functions: + + func GetListItemClickedListeners(view View, subviewID string) []func(ListView, int) + func GetListItemSelectedListeners(view View, subviewID string) []func(ListView, int) + func GetListItemCheckedListeners(view View, subviewID string) []func(ListView, []int) + +## TableView + +The TableView element implements a table. To create a TableView, the function is used: + + func NewTableView(session Session, params Params) TableView + +### "content" property + +The "content" property defines the content of the table. +To describe the content, you need to implement the TableAdapter interface declared as + + type TableAdapter interface { + RowCount() int + ColumnCount() int + Cell(row, column int) interface{} + } + +where RowCount() and ColumnCount() functions must return the number of rows and columns in the table; +Cell(row, column int) returns the contents of a table cell. The Cell() 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 + +The "content" property can also be assigned the following data types + +* TableAdapter +* [][]interface{} +* [][]string + +[][]interface{} and [][]string are converted to a TableAdapter when assigned. + +### "cell-style" property + +The "cell-style" property (CellStyle constant) is used to customize the appearance of a table cell. +Only an implementation of the TableCellStyle interface can be assigned to this property. + + type TableCellStyle interface { + CellStyle(row, column int) Params + } + +This interface contains only one CellStyle function that returns the styling parameters for a given table cell. +Any properties of the View interface can be used. For example + + func (style *myTableCellStyle) CellStyle(row, column int) rui.Params { + if row == 0 { + return rui.Params { + rui.BackgroundColor: rui.Gray, + rui.Italic: true, + } + } + return nil + } + +If you don't need to change the appearance of a cell, you can return nil for it. + +#### "row-span" and "column-span" properties + +In addition to the properties of the View interface, the CellStyle function can return two more properties of type int: +"row-span" (RowSpan constant) and "column-span" (ColumnSpan constant). +These properties are used to combine table cells. + +The "row-span" property specifies how many cells to merge vertically, and the "column-span" property - horizontally. For example + + func (style *myTableCellStyle) CellStyle(row, column int) rui.Params { + if row == 0 && column == 0 { + return rui.Params { rui.RowSpan: 2 } + } + if row == 0 && column == 1 { + return rui.Params { rui.ColumnSpan: 2 } + } + return nil + } + +In this case, the table will look like this + +|------|----------------| +| | | +| |-------|--------| +| | | | +|------|-------|--------| + +If [][]interface{} is used as the value of the "content" property, then empty structures are used to merge cells + + type VerticalTableJoin struct { + } + + type HorizontalTableJoin struct { + } + +These structures attach the cell to the top/left, respectively. The description of the above table will be as follows + + content := [][]interface{} { + {"", "", rui.HorizontalTableJoin{}}, + {rui.VerticalTableJoin{}, "", ""}, + } + +### "row-style" property + +The "row-style" property (RowStyle constant) is used to customize the appearance of a table row. +This property can be assigned either an implementation of the TableRowStyle interface or []Params. +TableRowStyle is declared as + + type TableRowStyle interface { + RowStyle(row int) Params + } + +The RowStyle function returns parameters that apply to the entire row of the table. +The "row-style" property has a lower priority than the "cell-style" property, i.e. +properties set in "cell-style" will be used instead of those set in "row-style" + +### "column-style" property + +The "column-style" property (ColumnStyle constant) is used to customize the appearance of a table column. +This property can be assigned either an implementation of the TableColumnStyle interface or []Params. +TableColumnStyle is declared as + + type TableColumnStyle interface { + ColumnStyle(column int) Params + } + +The ColumnStyle function returns the parameters applied to the entire column of the table. +The "column-style" property has a lower precedence over the "cell-style" and "row-style" properties. + +### "head-height" and "head-style" properties + +The table can have a header. +The "head-height" int property (constant HeadHeight) indicates how many first rows of the table form the header. +The "head-style" property (constant HeadStyle) sets the style of the heading. The "head-style" property can be +assigned, value of type: + +* string - style name; +* []Params - enumeration of header properties. + +### "foot-height" and "foot-style" properties + +The table can have finalizing lines at the end (footer). For example, the "total" line. +The "foot-height" int property (the FootHeight constant) indicates the number of these footer lines. +The "foot-style" property (constant FootStyle) sets footer style. +The values for the "foot-style" property are the same as for the "head-style" property. + +### "cell-padding" property + +The "cell-padding" BoundsProperty property (CellPadding constant) sets the padding from the cell borders to the content. +This property is equivalent to + + func (style *myTableCellStyle) CellStyle(row, column int) rui.Params { + return rui.Params { rui.Padding: } + } + +And it was introduced for convenience, so that you do not have to write an adapter to set indents. +The cell-padding property has a lower priority than the "cell-style" property. + +"cell-padding" can also be used when setting parameters in the "row-style", "column-style", "foot-style", and "head-style" properties + +### "cell-border" property + +The "cell-border" property (CellBorder constant) sets the memory for all table cells. +This property is equivalent to + + func (style *myTableCellStyle) CellStyle(row, column int) rui.Params { + return rui.Params { rui.Border: } + } + +And it was introduced for convenience, so that it is not necessary to write an adapter for the frame. +The "cell-border" property has a lower precedence over the "cell-style" property. + +"cell-border" can also be used when setting parameters in properties +"row-style", "column-style", "foot-style" and "head-style" + +### "table-vertical-align" property + +The "table-vertical-align" int property (TableVerticalAlign constant) specifies +the vertical alignment of data within a table cell. Valid values: + +| Value | Constant | Name | Alignment | +|:-----:|---------------|------------|--------------------| +| 0 | TopAlign | "top" | Top alignment | +| 1 | BottomAlign | "bottom" | Bottom alignment | +| 2 | CenterAlign | "center" | Center alignment | +| 3, 4 | BaselineAlign | "baseline" | Baseline alignment | + +For horizontal alignment, use the "text-align" property + +## Custom View + +A custom View must implement the CustomView interface, which extends the ViewsContainer and View interfaces. +A custom View is created based on another, which is named Super View. + +To simplify the task, there is already a basic CustomView implementation in the form of a CustomViewData structure. + +Let's consider creating a custom View using the built-in Buttom element as an example: + +1) declare the Button interface as extending CustomView, and the buttonData structure as extending CustomViewData + + type Button interface { + rui.CustomView + } + + type buttonData struct { + rui.CustomViewData + } + +2) implement the CreateSuperView function + + func (button *buttonData) CreateSuperView(session Session) View { + return rui.NewListLayout(session, rui.Params{ + rui.Semantics: rui.ButtonSemantics, + rui.Style: "ruiButton", + rui.StyleDisabled: "ruiDisabledButton", + rui.HorizontalAlign: rui.CenterAlign, + rui.VerticalAlign: rui.CenterAlign, + rui.Orientation: rui.StartToEndOrientation, + }) + } + +3) if necessary, override the methods of the CustomView interface, for Button this is the Focusable() function +(since the button can receive focus, but ListLayout does not) + + func (button *buttonData) Focusable() bool { + return true + } + +4) write a function to create a Button: + + func NewButton(session rui.Session, params rui.Params) Button { + button := new(buttonData) + rui.InitCustomView(button, "Button", session, params) + return button + } + +When creating a CustomView, it is mandatory to call the InitCustomView function. +This function initializes the CustomViewData structure. +The first argument is a pointer to the structure to be initialized, +the second is the name assigned to your View, the third is the session and the fourth is the parameters + +5) registering the item. It is recommended to register in the init method of the package + + rui.RegisterViewCreator("Button", func(session rui.Session) rui.View { + return NewButton(session, nil) + }) + +All! The new element is ready + +## CanvasView + +CanvasView is an area in which you can draw. To create a CanvasView, the function is used: + + func NewCanvasView(session Session, params Params) CanvasView + +CanvasView has only one additional property: "draw-function" (DrawFunction constant). +Using this property, a drawing function is set with the following description + + func(Canvas) + +where Canvas is the drawing context with which to draw + +The Canvas interface contains a number of functions for customizing styles, text and drawing itself. + +All coordinates and sizes are set only in pixels, so SizeUnit is not used when drawing. +float64 used everywhere + +### Setting the line style + +The following functions of the Canvas interface are used to customize the line color: + +* SetSolidColorStrokeStyle(color Color) - the line will be drawn with a solid color + +* SetLinearGradientStrokeStyle(x0, y0 float64, color0 Color, x1, y1 float64, color1 Color, stopPoints []GradientPoint) - +the line will be drawn using a linear gradient. +The gradient starts at x0, y0, and color0, and the gradient ends at x1, y1, and color1. +The []GradientPoint array specifies the intermediate points of the gradient. +If there are no intermediate points, then nil can be passed as the last parameter + +* SetRadialGradientStrokeStyle(x0, y0, r0 float64, color0 Color, x1, y1, r1 float64, color1 Color, stopPoints []GradientPoint) - +the line will be drawn using a radial gradient. +x0, y0, r0, color0 - center coordinates, radius and color of the starting circle. +x1, y1, r1, color1 - center coordinates, radius and color of the end circle. +The []GradientPoint array specifies intermediate points of the gradient + +The GradientPoint structure is described as + + type GradientPoint struct { + Offset float64 + Color Color + } + +where Offset is a value in the range from 0 to 1 specifies the relative position of the intermediate point, Color is the color of this point. + +Line width in pixels is set by the function + + SetLineWidth(width float64) + +The type of line ends is set using the function + + SetLineCap(cap int) + +where cap can take the following values + +| Value | Constant | View | +|:-----:|-----------|------------------------------------------------------------------------------| +| 0 | ButtCap | The ends of lines are squared off at the endpoints. Default value. | +| 1 | RoundCap | The ends of lines are rounded. The center of the circle is at the end point. | +| 2 | 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. | + +The shape used to connect two line segments at their intersection is specified by the function + + SetLineJoin(join int) + +where join can take the following values + +| Value | Constant | View | +|:-----:|-----------|----------------------------------------| +| 0 | 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 | +| 1 | 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. | +| 2 | BevelJoin | Fills an additional triangular area between the common endpoint of connected segments, and the separate outside rectangular corners of each segment. | + +By default, a solid line is drawn. If you want to draw a broken line, you must first set the pattern using the function + + SetLineDash(dash []float64, offset float64) + +where dash []float64 specifies the line pattern in the form of alternating line lengths and gaps. +The second argument is the offset of the template relative to the beginning of the line. + +Example + + canvas.SetLineDash([]float64{16, 8, 4, 8}, 0) + +The line is drawn as follows: a 16-pixel segment, then an 8-pixel gap, then a 4-pixel segment, then an 8-pixel gap, then a 16-pixel segment again, and so on. + +### Setting the fill style + +The following functions of the Canvas interface are used to customize the fill style: + +* SetSolidColorFillStyle(color Color) - the shape will be filled with a solid color + +* SetLinearGradientFillStyle(x0, y0 float64, color0 Color, x1, y1 float64, color1 Color, stopPoints []GradientPoint) - +the shape will be filled with a linear gradient. +The gradient starts at x0, y0, and color0, and the gradient ends at x1, y1, and color1. +The []GradientPoint array specifies the intermediate points of the gradient. +If there are no intermediate points, then nil can be passed as the last parameter + +* SetRadialGradientFillStyle(x0, y0, r0 float64, color0 Color, x1, y1, r1 float64, color1 Color, stopPoints []GradientPoint) - +the shape will be filled with a radial gradient. +x0, y0, r0, color0 - center coordinates, radius and color of the starting circle. +x1, y1, r1, color1 - center coordinates, radius and color of the end circle. +Array []GradientPoint specifies intermediate points of the gradient + +### Drawing geometric shapes + +#### Rectangle + +Three functions can be used to draw rectangles: + + FillRect(x, y, width, height float64) + StrokeRect(x, y, width, height float64) + FillAndStrokeRect(x, y, width, height float64) + +FillRect draws a filled rectangle. + +StrokeRect draws the outline of a rectangle. + +FillAndStrokeRect draws a path and fills in the interior. + +#### Rounded Rectangle + +Similar to the rectangle, there are three drawing functions + + FillRoundedRect(x, y, width, height, r float64) + StrokeRoundedRect(x, y, width, height, r float64) + FillAndStrokeRoundedRect(x, y, width, height, r float64) + +where r is the radius of the rounding + +#### Ellipse + +Three functions can also be used to draw ellipses: + + FillEllipse(x, y, radiusX, radiusY, rotation float64) + StrokeEllipse(x, y, radiusX, radiusY, rotation float64) + FillAndStrokeEllipse(x, y, radiusX, radiusY, rotation float64) + +where x, y is the center of the ellipse, radiusX, radiusY are the radii of the ellipse along the X and Y axes, +rotation - the angle of rotation of the ellipse relative to the center in radians. + +#### Path + +The Path interface allows you to describe a complex shape. Path is created using the NewPath () function. + +Once created, you must describe the shape. For this, the following interface functions can be used: + +* MoveTo(x, y float64) - move the current point to the specified coordinates; + +* LineTo(x, y float64) - add a line from the current point to the specified one; + +* ArcTo(x0, y0, x1, y1, radius float64) - add a circular arc using the specified control points and radius. +If necessary, the arc is automatically connected to the last point of the path with a straight line. +x0, y0 - coordinates of the first control point; +x1, y1 - coordinates of the second control point; +radius - radius of the arc. Must be non-negative. + +* Arc(x, y, radius, startAngle, endAngle float64, clockwise bool) - add a circular arc. +x, y - coordinates of the arc center; +radius - radius of the arc. Must be non-negative; +startAngle - The angle, in radians, at which the arc begins, measured clockwise from the positive X-axis. +endAngle - The angle, in radians, at which the arc ends, measured clockwise from the positive X-axis. +clockwise - if true, the arc will be drawn clockwise between the start and end corners, otherwise counterclockwise + +* BezierCurveTo(cp0x, cp0y, cp1x, cp1y, x, y float64) - add a cubic Bezier curve from the current point. +cp0x, cp0y - coordinates of the first control point; +cp1x, cp1y - coordinates of the second control point; +x, y - coordinates of the end point. + +* QuadraticCurveTo(cpx, cpy, x, y float64) - add a quadratic Bezier curve from the current point. +cpx, cpy - coordinates of the control point; +x, y - coordinates of the end point. + +* Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, clockwise bool) - add an elliptical arc. +x, y - coordinates of the center of the ellipse; +radiusX is the radius of the major axis of the ellipse. Must be non-negative; +radiusY - radius of the minor axis of the ellipse. Must be non-negative; +rotation - the rotation of the ellipse, expressed in radians; +startAngle The angle of the start of the ellipse in radians, measured clockwise from the positive x-axis. +endAngle The angle, in radians, at which the ellipse ends, measured clockwise from the positive x-axis. +clockwise - if true, draws the ellipse clockwise, otherwise counterclockwise. + +The Close () function is called at the end and connects the start and end points of the shape. Used only for closed shapes. + +After the Path is formed, it can be drawn using the following 3 functions + + FillPath(path Path) + StrokePath(path Path) + FillAndStrokePath(path Path) + +#### Line + +To draw a line, use the function + + DrawLine(x0, y0, x1, y1 float64) + +### Text + +To display text in specified coordinates, two functions are used + + FillText(x, y float64, text string) + StrokeText(x, y float64, text string) + +The StrokeText function draws the outline of the text, FillText draws the text itself. + +The horizontal alignment of the text relative to the specified coordinates is set using the function + + SetTextAlign(align int) + +where align can be one of the following values: + +| Value | Constant | Alignment | +|:-----:|-------------|--------------------------------------------------------| +| 0 | LeftAlign | The specified point is the leftmost point of the text | +| 1 | RightAlign | The specified point is the rightmost point of the text | +| 2 | CenterAlign | The text is centered on the specified point | +| 3 | StartAlign | If the text is displayed from left to right, then the text output is equivalent to LeftAlign, otherwise RightAlign | +| 4 | EndAlign | If the text is displayed from left to right, then the text output is equivalent to RightAlign, otherwise LeftAlign | + +The vertical alignment of the text relative to the specified coordinates is set using the function + + SetTextBaseline(baseline int) + +where baseline can be one of the following values: + +| Value | Constant | Alignment | +|:-----:|---------------------|--------------------------------------------------| +| 0 | AlphabeticBaseline | Relatively normal baseline of text | +| 1 | TopBaseline | Relative to the top border of the text | +| 2 | MiddleBaseline | About the middle of the text | +| 3 | BottomBaseline | To the bottom of the text | +| 4 | HangingBaseline | Relative to the dangling baseline of the text (used in Tibetan and other Indian scripts) | +| 5 | IdeographicBaseline | Relative to the ideographic baseline of the text | + +An ideographic baseline is the bottom of a character display if the main character is +outside the alphabet baseline (Used in Chinese, Japanese, and Korean fonts). + +To set the font parameters of the displayed text, use the functions + + SetFont(name string, size SizeUnit) + SetFontWithParams(name string, size SizeUnit, params FontParams) + +where FontParams is defined as + + type FontParams struct { + // Italic - if true then a font is italic + Italic bool + // SmallCaps - if true then a font uses small-caps glyphs + SmallCaps bool + // Weight - a font weight. Valid values: 0…9, there + // 0 - a weight does not specify; + // 1 - a minimal weight; + // 4 - a normal weight; + // 7 - a bold weight; + // 9 - a maximal weight. + Weight int + // LineHeight - the height (relative to the font size of the element itself) of a line box. + LineHeight SizeUnit + } + +The TextWidth function allows you to find out the width of the displayed text in pixels + + TextWidth(text string, fontName string, fontSize SizeUnit) float64 + +### Image + +Before drawing an image, it must first be loaded. The global function is used for this: + + func LoadImage(url string, onLoaded func(Image), session Session) Image { + +The image is loaded asynchronously. After the download is finished, the function passed in the second argument will be called. +If the image was loaded successfully, then the LoadingStatus() function of the Image interface will +return the value ImageReady (1), if an error occurred while loading, then this function will return ImageLoadingError (2). +The textual description of the error is returned by the LoadingError() function + +Unlike an ImageView, loading an Image does not take into account the pixel density. +It is up to you to decide which image to upload. You can do it like this: + + var url string + if session.PixelRatio() == 2 { + url = "image@2x.png" + } else { + url = "image.png" + } + +The following functions are used to draw the image: + + DrawImage(x, y float64, image Image) + DrawImageInRect(x, y, width, height float64, image Image) + DrawImageFragment(srcX, srcY, srcWidth, srcHeight, dstX, dstY, dstWidth, dstHeight float64, image Image) + +The DrawImage function displays the image as it is (without scaling): x, y - coordinates of the upper left corner of the image + +The DrawImageInRect function displays the image with scaling: x, y are coordinates of the upper left corner of the image, +width, height are width and height of the result + +The DrawImageFragment function displays a fragment of the image with scaling: srcX, srcY, srcWidth, srcHeight describe +the original area of the image, dstX, dstY, dstWidth, dstHeight describe the resulting area. + +Image can also be used in fill style + + SetImageFillStyle(image Image, repeat int) + +where repeat can take on the following values: + +| Value | Constant | Description | +|:-----:|-----------|-----------------------------------------------| +| 0 | NoRepeat | Image is not repeated | +| 1 | RepeatXY | Image is repeated vertically and horizontally | +| 2 | RepeatX | The image is repeated horizontally only | +| 3 | RepeatY | The image is repeated vertically only | + +## AudioPlayer, VideoPlayer, MediaPlayer + +AudioPlayer and VideoPlayer are elements for audio and video playback. +Both elements implement the MediaPlayer interface. Most of the properties and all events +of AudioPlayer and VideoPlayer are implemented through the MediaPlayer. + +### Свойство "src" + +The "src" property (Source constant) specifies one or more media sources. The "src" property can take on the following types: + +* string, +* MediaSource, +* []MediaSource. + +The MediaSource structure is declared as + + type MediaSource struct { + Url string + MimeType string + } + +where Url is a required parameter, MimeType is an optional mime file type + +Since different browsers support different file formats and codecs, it is recommended +to specify multiple sources in different formats. The player chooses the most suitable +one from the list of sources. Setting mime types makes this process easier for the browser + +### "controls" property + +The "controls" bool property (Controls constant) specifies whether UI elements should be +displayed to control playback of the media resource. The default is false. + +If the "controls" property is false for the AudioPlayer, then it will be invisible and will not take up screen space. + +### "loop" property + +The "loop" bool property (Loop constant). If it set to true, then the media file will start over when it reaches the end. The default is false. + +### "muted" property + +The "muted" bool property (constant Muted) enables (true) / disables (false) silent mode. The default is false. + +### "preload" property + +The "preload" int property (constant Preload) defines what data should be preloaded. +Valid values: + +| Value | Constant | Name | Description | +|:-----:|-----------------|------------|----------------------------------------------------------------------------------| +| 0 | PreloadNone | "none" | Media file must not be pre-loaded | +| 1 | PreloadMetadata | "metadata" | Only metadata is preloaded | +| 2 | PreloadAuto | "auto" | The entire media file can be downloaded even if the user doesn't have to use it. | + +The default value is PreloadAuto (2) + +### "poster" property + +The "poster" string property (Poster constant) is used only for VideoPlayer. +It sets the url of the image that will be shown until the video is loaded. +If this property is not set, then a black screen will be shown first, and then the first frame (as soon as it is loaded). + +### "video-width" and "video-height" properties + +The "video-width" (VideoWidth constant) and "video-height" (VideoHeight constant) float64 properties are used only for VideoPlayer. +It defines the width and height of the rendered video in pixels. + +If "video-width" and "video-height" are not specified, then the actual dimensions of the video are used, +while the dimensions of the container in which the video is placed are ignored and the video may overlap +other interface elements. Therefore, it is recommended to set these values, for example, like this + + rui.Set(view, "videoPlayerContainer", rui.ResizeEvent, func(frame rui.Frame) { + rui.Set(view, "videoPlayer", rui.VideoWidth, frame.Width) + rui.Set(view, "videoPlayer", rui.VideoHeight, frame.Height) + }) + +If only one of the "video-width" or "video-height" properties is set, then the second is calculated based on the aspect ratio of the video + +### Developments + +MediaPlayer has two groups of events: + +1) has a handler like + + func(MediaPlayer) + +You can also use func(). This group includes the following events: + +* "abort-event" (constant AbortEvent) - Fires when the resource is not fully loaded, but not as a result of an error. + +* "can-play-event" (CanPlayEvent constant) is fired when the user agent is able to play media +but judges that there is not enough data loaded to play the media to the end without +having to stop to further buffer the content. + +* "can-play-through-event" (constant CanPlayThroughEvent) is fired when the user agent is able +to play the media and evaluates that enough data has been loaded to play the media to its end, +without having to stop to further buffer the content. + +* "complete-event" (CompleteEvent constant) - + +* "emptied-event" (EmptiedEvent constant) is fired when the media becomes empty; for example when the media is already loaded (or partially loaded) + +* "ended-event" (EndedEvent constant) - Fires when playback stops, when the end of media is reached, or if no further data is available. + +* "loaded-data-event" (LoadedDataEvent constant) is fired when the first frame of the media has finished loading. + +* "loaded-metadata-event" (LoadedMetadataEvent constant) is fired when the metadata has been loaded. + +* "loadstart-event" (LoadstartEvent constant) is fired when the browser starts loading the resource. + +* "pause-event" (PauseEvent constant) is fired when a pause request is processed and the action pauses, most often when the Pause () method is called. + +* "play-event" (PlayEvent constant) is fired when the media file starts playing, for example, as a result of using the Play () method + +* "playing-event" (PlayingEvent constant) is fired when playback is about to start after being paused or delayed due to lack of data. + +* "progress-event" (ProgressEvent constant) is fired periodically when the browser loads the resource. + +* "seeked-event" (SeekedEvent constant) is fired when the playback speed has changed. + +* "seeking-event" (SeekingEvent constant) is fired when a seeking operation begins. + +* "stalled-event" (StalledEvent constant) is fired when the user agent tries to retrieve media data, but no data arrives unexpectedly. + +* "suspend-event" (SuspendEvent constant) is fired when media loading has been suspended. + +* "waiting-event" (WaitingEvent constant) is fired when playback is stopped due to a temporary lack of data + +2) has a handler like + + func(MediaPlayer, float64) + +You can also use func(float64), func(MediaPlayer) and func(). +This group includes events related to changing the parameters of the player. +The new value of the changed parameter is passed as the second argument. + +* "duration-changed-event" (DurationChangedEvent constant) is fired when the duration attribute has been updated. + +* "time-updated-event" (TimeUpdatedEvent constant) is fired when the current time has been updated. + +* "volume-changed-event" (VolumeChangedEvent constant) is fired when the volume changes. + +* "rate-changed-event" (RateChangedEvent constant) is fired when the playback speed has changed. + +A separate event that does not belong to these two groups, "player-error-event" (PlayerErrorEvent constant) +is fired when the resource cannot be loaded due to an error (eg network error). + +The handler for this event looks like + + func(player MediaPlayer, code int, message string) + +You can also use func(int, string), func(MediaPlayer) and func(). +Where the argument "message" is the error message, "code" is the error code: + +| Error code | Constant | Value | +|:----------:|-------------------------------|---------------------------------------------------------------------------| +| 0 | PlayerErrorUnknown | Unknown error | +| 1 | PlayerErrorAborted | Fetching the associated resource was interrupted by a user request. | +| 2 | PlayerErrorNetwork | Some kind of network error has occurred that prevented the media from successfully ejecting, even though it was previously available. | +| 3 | PlayerErrorDecode | Although the resource was previously identified as being used, an error occurred while trying to decode the media resource. | +| 4 | PlayerErrorSourceNotSupported | The associated resource object or media provider was found to be invalid. | + +### Methods + +MediaPlayer has a number of methods for controlling player parameters: + +* Play() starts playback of a media file; + +* Pause() pauses playback; + +* SetCurrentTime(seconds float64) sets the current playback time in seconds; + +* CurrentTime() float64 returns the current playing time in seconds; + +* Duration() float64 returns the duration of the media file in seconds; + +* SetPlaybackRate(rate float64) sets the playback speed. Normal speed is 1.0; + +* PlaybackRate() float64 returns the current playback speed; + +* SetVolume(volume float64) sets the volume speed in the range from 0 (silence) to 1 (maximum volume); + +* Volume() float64 returns the current volume; + +* IsEnded() bool returns true if the end of the media file is reached; + +* IsPaused() bool returns true if playback is paused. + +For quick access to these methods, there are global functions: + + func MediaPlayerPlay(view View, playerID string) + func MediaPlayerPause(view View, playerID string) + func SetMediaPlayerCurrentTime(view View, playerID string, seconds float64) + func MediaPlayerCurrentTime(view View, playerID string) float64 + func MediaPlayerDuration(view View, playerID string) float64 + func SetMediaPlayerVolume(view View, playerID string, volume float64) + func MediaPlayerVolume(view View, playerID string) float64 + func SetMediaPlayerPlaybackRate(view View, playerID string, rate float64) + func MediaPlayerPlaybackRate(view View, playerID string) float64 + func IsMediaPlayerEnded(view View, playerID string) bool + func IsMediaPlayerPaused(view View, playerID string) bool + +where view is the root View, playerID is the id of AudioPlayer or VideoPlayer + +## Session + +When a client creates a connection to a server, a Session interface is created for that connection. +This interface is used to interact with the client. +You can get the current Session interface by calling the Session() method of the View interface. + +When a session is created, it gets a custom implementation of the SessionContent interface. + + type SessionContent interface { + CreateRootView(session rui.Session) rui.View + } + +This interface is created by the function passed as a parameter when creating an application by the NewApplication function. + +In addition to the mandatory CreateRootView() function, SessionContent can have several optional functions: + + OnStart(session rui.Session) + OnFinish(session rui.Session) + OnResume(session rui.Session) + OnPause(session rui.Session) + OnDisconnect(session rui.Session) + OnReconnect(session rui.Session) + +Immediately after creating a session, the CreateRootView function is called. After creating the root View, the OnStart function is called (if implemented) + +The OnFinish function (if implemented) is called when the user closes the application page in the browser + +The OnPause function is called when the application page in the client's browser becomes inactive. +This happens if the user switches to a different browser tab / window, minimizes the browser, or switches to another application. + +The OnResume function is called when the application page in the client's browser becomes active. Also, this function is called immediately after OnStart + +The OnDisconnect function is called if the server loses connection with the client. This happens either when the connection is broken. + +The OnReconnect function is called after the server reconnects with the client. + +The Session interface provides the following methods: + +* DarkTheme() bool returns true if a dark theme is used. Determined by client-side settings + +* TouchScreen() bool returns true if client supports touch screen + +* PixelRatio() float64 returns the size of a logical pixel, i.e. how many physical pixels form a logical. For example, for iPhone, this value will be 2 or 3 + +* TextDirection() int returns the direction of the letter: LeftToRightDirection (1) or RightToLeftDirection (2) + +* Constant(tag string) (string, bool) returns the value of a constant + +* Color(tag string) (Color, bool) returns the value of the color constant + +* SetCustomTheme(name string) bool sets the theme with the given name as the current one. +Returns false if no topic with this name was found. Themes named "" are the default theme. + +* Language() string returns the current interface language, for example: "en", "ru", "ptBr" + +* SetLanguage(lang string) sets the current interface language (see "Support for multiple languages") + +* GetString(tag string) (string, bool) returns a textual text value for the current language +(see "Support for multiple languages") + +* Content() SessionContent returns the current SessionContent instance + +* RootView() View returns the root View of the session + +* Get(viewID, tag string) interface{} returns the value of the View property named tag. Equivalent to + + rui.Get(session.RootView(), viewID, tag) + +* Set(viewID, tag string, value interface {}) bool sets the value of the View property named tag. + + rui.Set(session.RootView(), viewID, tag, value) + +## Resource description format + +Application resources (themes, views, translations) can be described as text (utf-8). +This text is placed in a file with the ".rui" extension. + +The root element of the resource file must be an object. It has the following format: + + < object name > { + < object data > + } + +if the object name contains the following characters: '=', '{', '}', '[', ']', ',', '', '\t', '\n', +'\' ',' "','` ',' / 'and any spaces, then the object name must be enclosed in quotation marks. +If these characters are not used, then quotation marks are optional. + +You can use three types of quotation marks: + +* "…" is equivalent to the same string in the go language, i.e. inside you can use escape sequences: +\n, \r, \\, \", \', \0, \t, \x00, \u0000 + +* '…' is similar to the line "…" + +* `…` is equivalent to the same string in the go language, i.e. the text within this line remains as is. Inside +you cannot use the ` character. + +Object data is a set of < key > = < value > pairs separated by commas. + +The key is a string of text. The design rules are the same as for the object name. + +Values can be of 3 types: + +* Simple value - a line of text formatted according to the same rules as the name of the object + +* An object + +* Array of values + +An array of values is enclosed in square brackets. Array elements are separated by commas. +Elements can be simple values or objects. + +There may be comments in the text. The design rules are the same as in the go language: // and / * ... * / + +Example: + + GridLayout { + id = gridLayout, width = 100%, height = 100%, + cell-width = "150px, 1fr, 30%", cell-height = "25%, 200px, 1fr", + content = [ + // Subviews + TextView { row = 0, column = 0:1, + text = "View 1", text-align = center, vertical-align = center, + background-color = #DDFF0000, radius = 8px, padding = 32px, + border = _{ style = solid, width = 1px, color = #FFA0A0A0 } + }, + TextView { row = 0:1, column = 2, + text = "View 2", text-align = center, vertical-align = center, + background-color = #DD00FF00, radius = 8px, padding = 32px, + border = _{ style = solid, width = 1px, color = #FFA0A0A0 } + }, + TextView { row = 1:2, column = 0, + text = "View 3", text-align = center, vertical-align = center, + background-color = #DD0000FF, radius = 8px, padding = 32px, + border = _{ style = solid, width = 1px, color = #FFA0A0A0 } + }, + TextView { row = 1, column = 1, + text = "View 4", text-align = center, vertical-align = center, + background-color = #DDFF00FF, radius = 8px, padding = 32px, + border = _{ style = solid, width = 1px, color = #FFA0A0A0 } + }, + TextView { row = 2, column = 1:2, + text = "View 5", text-align = center, vertical-align = center, + background-color = #DD00FFFF, radius = 8px, padding = 32px, + border = _{ style = solid, width = 1px, color = #FFA0A0A0 } + }, + ] + } + +To work with text resources, the DataNode interface is used + + type DataNode interface { + Tag() string + Type() int + Text() string + Object() DataObject + ArraySize() int + ArrayElement(index int) DataValue + ArrayElements() []DataValue + } + +This element describes the underlying data element. + +The Tag method returns the value of the key. + +The data type is returned by the Type method. It returns one of 3 values + +| Value | Constant | Data type | +|:-----:|------------|--------------| +| 0 | TextNode | Simple value | +| 1 | ObjectNode | Object | +| 2 | ArrayNode | Array | + +The Text() method is used to get a simple value. +To get an object, use the Object() method. +To get the elements of an array, use the ArraySize, ArrayElement and ArrayElements methods + +## Resources + +Resources (pictures, themes, translations, etc.) with which the application works should be placed +in subdirectories within one resource directory. Resources should be located in the following subdirectories: + +* images - all images are placed in this subdirectory. Here you can make nested subdirectories. +In this case, they must be included in the file name. For example, "subdir/image1.png" + +* themes - application themes are placed in this subdirectory (see below) + +* views - View descriptions are placed in this subdirectory + +* strings - translations of text resources are placed in this subdirectory (see Support for multiple languages) + +* raw - all other resources are placed in this subdirectory: sounds, video, binary data, etc. + +The resource directory can either be included in the executable file or located separately. + +If the resources need to be included in the executable file, then the name of the directory must be "resources" and it must be connected as follows: + + import ( + "embed" + + "github.com/anoshenko/rui" + ) + + //go:embed resources + var resources embed.FS + + func main() { + rui.AddEmbedResources(&resources) + + app := rui.NewApplication("Hello world", createHelloWorldSession) + app.Start("localhost:8000") + } + +If the resources are supplied as a separate directory, then it must be registered +using the SetResourcePath function before creating the Application: + + func main() { + rui.SetResourcePath(path) + + app := rui.NewApplication("Hello world", createHelloWorldSession) + app.Start("localhost:8000") + } + +## Images for screens with different pixel densities + +If you need to add separate images to the resources for screens with different pixel densities, +then this is done in the style of iOS, i.e. '@< density >x' is appended to the filename. For example + + image@2x.png + image@3x.jpg + image@1.5x.gif + +For example, you have images for three densities: image.png, image@2x.png, and image@3x.png. +In this case, you only assign the value "image.png" to the "src" field of the ImageView. +The library itself will find the rest in the "images" directory and transfer the image to the client with the required density + +## Themes + +The topic includes three types of data: + +* constants +* color constants +* View styles + +Themes are designed as a rui file and placed in the themes folder. + +The root of the theme is an object named 'theme'. This object can contain the following properties: + +* name - an optional text property that specifies the name of the theme. +If this property is not set or is equal to an empty string, then this is the default theme. + +* constants - property object defining constants. The name of the object can be anything. It is recommended to use "_". +An object can have any number of text properties specifying the "constant name" = "value" pair. +This section contains constants of type SizeUnit, AngleUnit, text and numeric. In order to assign a constant to any View property, +you need to assign the name of the constant to the property by adding the '@' symbol at the beginning. +For example + + theme { + constants = _{ + defaultPadding = 4px, + buttonPadding = @defaultPadding, + angle = 30deg, + } + } + + rui.Set(view, "subView", rui.Padding, "@defaultPadding") + +* constants:touch is property object defining constants used only for touch screen. +For example, how to make indents larger on a touch screen: + + theme { + constants = _{ + defaultPadding = 4px, + }, + constants:touch = _{ + defaultPadding = 12px, + }, + } + +* colors is an object property that defines color constants for a light skin (default theme). +An object can have any number of text properties specifying the "color name" = "color" pair. +Similar to constants, when assigning, you must add '@' at the beginning of the color name. For example + + theme { + colors = _{ + textColor = #FF101010, + borderColor = @textColor, + backgroundColor = white, + } + } + + rui.Set(view, "subView", rui.TextColor, "@textColor") + +Color names such as "black", "white", "red", etc. are used without the '@' character. +However, you can specify color constants with the same names. For example + + theme { + colors = _{ + red = blue, + } + } + + rui.Set(view, "subView", rui.TextColor, "@red") // blue text + rui.Set(view, "subView", rui.TextColor, "red") // red text + +* colors:dark is an object property that defines color constants for a dark theme + +* styles is an array of common styles. Each element of the array must be an object. +The object name is and is the name of the style. For example, + + theme { + styles = [ + demoPage { + width = 100%, + height = 100%, + cell-width = "1fr, auto", + }, + demoPanel { + width = 100%, + height = 100%, + orientation = start-to-end, + }, + ] + } + +To use styles, the View has two text properties "style" (Style constant) and "style-disabled" (StyleDisabled constant). +The "style" property is assigned the property name that is applied to the View when the "disabled" property is set to false. +The "style-disabled" property is assigned the property name that is applied to the View when the "disabled" property is set to true. +If "style-disabled" is not specified, then the "style" property is used in both modes. + +Attention! The '@' symbol should NOT be added to the style name. If you add the '@' symbol to the name, +then the style name will be extracted from the constant of the same name. For example + + theme { + constants = _{ + @demoPanel = demoPage + }, + styles = [ + demoPage { + width = 100%, + height = 100%, + cell-width = "1fr, auto", + }, + demoPanel { + width = 100%, + height = 100%, + orientation = start-to-end, + }, + ] + } + + rui.Set(view, "subView", rui.Style, "demoPanel") // style == demoPanel + rui.Set(view, "subView", rui.Style, "@demoPanel") // style == demoPage + +In addition to general styles, you can add styles for specific work modes. To do this, the following modifiers are added to the name "styles": + +* ":portrait" or ":landscape" are respectively styles for portrait or landscape mode of the program. +Attention means the aspect ratio of the program window, not the screen. + +* ":width< size >" are styles for a screen whose width does not exceed the specified size in logical pixels. + +* ":height< size >" are styles for a screen whose height does not exceed the specified size in logical pixels. + +For example + + theme { + styles = [ + demoPage { + width = 100%, + height = 100%, + cell-width = "1fr, auto", + }, + demoPage2 { + row = 0, + column = 1, + } + ], + styles:landscape = [ + demoPage { + width = 100%, + height = 100%, + cell-height = "1fr, auto", + }, + demoPage2 { + row = 1, + column = 0, + } + ], + styles:portrait:width320 = [ + sapmplePage { + width = 100%, + height = 50%, + }, + ] + } + +## Standard constants and styles + +The library defines a number of constants and styles. You can override them in your themes. + +System styles that you can override: + +| Style name | Описание | +|---------------------|---------------------------------------------------------------------| +| ruiApp | This style is used to set the default text style (font, size, etc.) | +| ruiView | Default View Style | +| ruiArticle | The style to use if the "semantics" property is set to "article" | +| ruiSection | The style used if the "semantics" property is set to "section" | +| ruiAside | The style used if the "semantics" property is set to "aside" | +| ruiHeader | The style used if the "semantics" property is set to "header" | +| ruiMain | The style used if the "semantics" property is set to "main" | +| ruiFooter | Style used if the "semantics" property is set to "footer" | +| ruiNavigation | Style used if property "semantics" is set to "navigation" | +| ruiFigure | Style used if property "semantics" is set to "figure" | +| ruiFigureCaption | Style used if property "semantics" is set to "figure-caption" | +| ruiButton | Style used if property "semantics" is set to "button" | +| ruiParagraph | The style used if the "semantics" property is set to "paragraph" | +| ruiH1 | Style used if property "semantics" is set to "h1" | +| ruiH2 | Style used if property "semantics" is set to "h2" | +| ruiH3 | Style used if property "semantics" is set to "h3" | +| ruiH4 | Style used if property "semantics" is set to "h4" | +| ruiH5 | Style used if property "semantics" is set to "h5" | +| ruiH6 | Style used if property "semantics" is set to "h6" | +| ruiBlockquote | Style used if the "semantics" property is set to "blockquote" | +| ruiCode | Style used if property "semantics" is set to "code" | +| ruiTable | Default TableView style | +| ruiTableHead | Default TableView header style | +| ruiTableFoot | Default TableView footer style | +| ruiTableRow | Default TableView row style | +| ruiTableColumn | Default TableView column style | +| ruiTableCell | Default TableView cell style | +| ruiDisabledButton | Button style if property "disabled" is set to true | +| ruiCheckbox | Checkbox style | +| ruiListItem | ListView item style | +| ruiListItemSelected | Style the selected ListView item when the ListView does not have focus | +| ruiListItemFocused | Style the selected ListView item when the ListView has focus | +| ruiPopup | Popup style | +| ruiPopupTitle | Popup title style | +| ruiMessageText | Popup text style (Message, Question) | +| ruiPopupMenuItem | Popup menu item style | + +System color constants that you can override: + +| Color constant name | Description | +|----------------------------|-----------------------------------------------------| +| ruiBackgroundColor | Background color | +| ruiTextColor | Text color | +| ruiDisabledTextColor | Banned text color | +| ruiHighlightColor | Backlight color | +| ruiHighlightTextColor | Highlighted text color | +| ruiButtonColor | Button color | +| ruiButtonActiveColor | Focus button color | +| ruiButtonTextColor | Button text color | +| ruiButtonDisabledColor | Denied button color | +| ruiButtonDisabledTextColor | Disabled button text color | +| ruiSelectedColor | Background color of inactive selected ListView item | +| ruiSelectedTextColor | Text color of inactive selected ListView item | +| ruiPopupBackgroundColor | Popup background color | +| ruiPopupTextColor | Popup text color | +| ruiPopupTitleColor | Popup title background color | +| ruiPopupTitleTextColor | Popup Title Text Color | + +Constants that you can override: + +| Constant name | Description | +|------------------------------|------------------------------------------------| +| ruiButtonHorizontalPadding | Horizontal padding inside the button | +| ruiButtonVerticalPadding | Vertical padding inside the button | +| ruiButtonMargin | External button access | +| ruiButtonRadius | Button corner radius | +| ruiButtonHighlightDilation | Width of the outer border of the active button | +| ruiButtonHighlightBlur | Blur the active button frame | +| ruiCheckboxGap | Break between checkbox and content | +| ruiListItemHorizontalPadding | Horizontal padding inside a ListView item | +| ruiListItemVerticalPadding | Vertical padding inside a ListView item | +| ruiPopupTitleHeight | Popup title height | +| ruiPopupTitlePadding | Popup title padding | +| ruiPopupButtonGap | Break between popup buttons | + +## Multi-language support + +If you want to add support for several languages to the program, you need to place +the translation files in the "strings" folder of the resources. +Translation files must have the "rui" extension and the following format + + strings { + = _{ + = , + = , + … + }, + <язык 2> = _{ + = , + = , + … + }, + … + } + +If the translation for each language is placed in a separate file, then the following format can be used + + strings: { + = , + = , + … + } + +For example, if all translations are in one file strings.rui + + strings { + ru = _{ + "Yes" = "Да", + "No" = "Нет", + }, + de = _{ + "Yes" = "Ja", + "No" = "Nein", + }, + } + +If in different. ru.rui file: + + strings:ru { + "Yes" = "Да", + "No" = "Нет", + } + +de.rui file: + + strings:de { + "Yes" = "Ja", + "No" = "Nein", + } + +The translation can also be split into multiple files. + +Translations are automatically inserted in all Views. + +However, if you are drawing text in a CanvasView, then you must request the translation yourself. +To do this, there is a method in the Session interface: + + GetString(tag string) (string, bool) + +If there is no translation of the given string, then the method will return the original string and false as the second parameter. + +You can get the current language using the Language() method of the Session interface. +The current language is determined by the user's browser settings. +You can change the session language using the SetLanguage(lang string) method of the Session interface. diff --git a/absoluteLayout.go b/absoluteLayout.go new file mode 100644 index 0000000..704e28c --- /dev/null +++ b/absoluteLayout.go @@ -0,0 +1,40 @@ +package rui + +import "strings" + +// AbsoluteLayout - list-container of View +type AbsoluteLayout interface { + ViewsContainer +} + +type absoluteLayoutData struct { + viewsContainerData +} + +// NewAbsoluteLayout create new AbsoluteLayout object and return it +func NewAbsoluteLayout(session Session, params Params) AbsoluteLayout { + view := new(absoluteLayoutData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newAbsoluteLayout(session Session) View { + return NewAbsoluteLayout(session, nil) +} + +// Init initialize fields of ViewsContainer by default values +func (layout *absoluteLayoutData) Init(session Session) { + layout.viewsContainerData.Init(session) + layout.tag = "AbsoluteLayout" + layout.systemClass = "ruiAbsoluteLayout" +} + +func (layout *absoluteLayoutData) htmlSubviews(self View, buffer *strings.Builder) { + if layout.views != nil { + for _, view := range layout.views { + view.addToCSSStyle(map[string]string{`position`: `absolute`}) + viewHTML(view, buffer) + } + } +} diff --git a/angleUnit.go b/angleUnit.go new file mode 100644 index 0000000..c2de20a --- /dev/null +++ b/angleUnit.go @@ -0,0 +1,212 @@ +package rui + +import ( + "fmt" + "math" + "strconv" + "strings" +) + +// AngleUnitType : type of enumerated constants for define a type of AngleUnit value. +// Can take the following values: Radian, Degree, Gradian, and Turn +type AngleUnitType uint8 + +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 (1⁄400 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). +type AngleUnit struct { + Type AngleUnitType + Value float64 +} + +// Deg creates AngleUnit with Degree type +func Deg(value float64) AngleUnit { + return AngleUnit{Type: Degree, Value: value} +} + +// Rad create AngleUnit with Radian type +func Rad(value float64) AngleUnit { + return AngleUnit{Type: Radian, Value: value} +} + +// PiRad create AngleUnit with PiRadian type +func PiRad(value float64) AngleUnit { + return AngleUnit{Type: PiRadian, Value: value} +} + +// Grad create AngleUnit with Gradian type +func Grad(value float64) AngleUnit { + return AngleUnit{Type: Gradian, Value: value} +} + +// Equal compare two AngleUnit. Return true if AngleUnit are equal +func (angle AngleUnit) Equal(size2 AngleUnit) bool { + return angle.Type == size2.Type && angle.Value == size2.Value +} + +func angleUnitSuffixes() map[AngleUnitType]string { + return map[AngleUnitType]string{ + Degree: "deg", + Radian: "rad", + PiRadian: "pi", + Gradian: "grad", + Turn: "turn", + } +} + +// StringToAngleUnit converts the string argument to AngleUnit +func StringToAngleUnit(value string) (AngleUnit, bool) { + var angle AngleUnit + ok, err := angle.setValue(value) + if !ok { + ErrorLog(err) + } + return angle, ok +} + +func (angle *AngleUnit) setValue(value string) (bool, string) { + value = strings.ToLower(strings.Trim(value, " \t\n\r")) + + setValue := func(suffix string, unitType AngleUnitType) (bool, string) { + val, err := strconv.ParseFloat(value[:len(value)-len(suffix)], 64) + if err != nil { + return false, `AngleUnit.SetValue("` + value + `") error: ` + err.Error() + } + angle.Value = val + angle.Type = unitType + return true, "" + } + + if value == "π" { + angle.Value = 1 + angle.Type = PiRadian + return true, "" + } + + if strings.HasSuffix(value, "π") { + return setValue("π", PiRadian) + } + + if strings.HasSuffix(value, "°") { + return setValue("°", Degree) + } + + for unitType, suffix := range angleUnitSuffixes() { + if strings.HasSuffix(value, suffix) { + return setValue(suffix, unitType) + } + } + + if val, err := strconv.ParseFloat(value, 64); err == nil { + angle.Value = val + angle.Type = Radian + return true, "" + } + + return false, `AngleUnit.SetValue("` + value + `") error: invalid argument` +} + +// String - convert AngleUnit to string +func (angle AngleUnit) String() string { + if suffix, ok := angleUnitSuffixes()[angle.Type]; ok { + return fmt.Sprintf("%g%s", angle.Value, suffix) + } + + return fmt.Sprintf("%g", angle.Value) +} + +// cssString - convert AngleUnit to string +func (angle AngleUnit) cssString() string { + if angle.Type == PiRadian { + return fmt.Sprintf("%grad", angle.Value*math.Pi) + } + + return angle.String() +} + +// ToDegree returns the angle in radians +func (angle AngleUnit) ToRadian() AngleUnit { + switch angle.Type { + case PiRadian: + return AngleUnit{Value: angle.Value * math.Pi, Type: Radian} + + case Degree: + return AngleUnit{Value: angle.Value * math.Pi / 180, Type: Radian} + + case Gradian: + return AngleUnit{Value: angle.Value * math.Pi / 200, Type: Radian} + + case Turn: + return AngleUnit{Value: angle.Value * 2 * math.Pi, Type: Radian} + } + + return angle +} + +// ToDegree returns the angle in degrees +func (angle AngleUnit) ToDegree() AngleUnit { + switch angle.Type { + case Radian: + return AngleUnit{Value: angle.Value * 180 / math.Pi, Type: Degree} + + case PiRadian: + return AngleUnit{Value: angle.Value * 180, Type: Degree} + + case Gradian: + return AngleUnit{Value: angle.Value * 360 / 400, Type: Degree} + + case Turn: + return AngleUnit{Value: angle.Value * 360, Type: Degree} + } + + return angle +} + +// ToGradian returns the angle in gradians (1⁄400 of a full circle) +func (angle AngleUnit) ToGradian() AngleUnit { + switch angle.Type { + case Radian: + return AngleUnit{Value: angle.Value * 200 / math.Pi, Type: Gradian} + + case PiRadian: + return AngleUnit{Value: angle.Value * 200, Type: Gradian} + + case Degree: + return AngleUnit{Value: angle.Value * 400 / 360, Type: Gradian} + + case Turn: + return AngleUnit{Value: angle.Value * 400, Type: Gradian} + } + + return angle +} + +// ToTurn returns the angle in turns (1 turn = 360 degree) +func (angle AngleUnit) ToTurn() AngleUnit { + switch angle.Type { + case Radian: + return AngleUnit{Value: angle.Value / (2 * math.Pi), Type: Turn} + + case PiRadian: + return AngleUnit{Value: angle.Value / 2, Type: Turn} + + case Degree: + return AngleUnit{Value: angle.Value / 360, Type: Turn} + + case Gradian: + return AngleUnit{Value: angle.Value / 400, Type: Turn} + } + + return angle +} diff --git a/animation.go b/animation.go new file mode 100644 index 0000000..2b3fcee --- /dev/null +++ b/animation.go @@ -0,0 +1,229 @@ +package rui + +/* +import ( + "fmt" + "strconv" +) + +type AnimationTags struct { + Tag string + Start, End interface{} +} + +type AnimationKeyFrame struct { + KeyFrame int + TimingFunction string + Params Params +} + +type AnimationScenario interface { + fmt.Stringer + ruiStringer + Name() string + cssString(session Session) string +} + +type animationScenario struct { + name string + tags []AnimationTags + keyFrames []AnimationKeyFrame + cssText string +} + +var animationScenarios = []string{} + +func addAnimationScenario(name string) string { + animationScenarios = append(animationScenarios, name) + return name +} + +func registerAnimationScenario() string { + find := func(text string) bool { + for _, scenario := range animationScenarios { + if scenario == text { + return true + } + } + return false + } + + n := 1 + name := fmt.Sprintf("scenario%08d", n) + for find(name) { + n++ + name = fmt.Sprintf("scenario%08d", n) + } + + animationScenarios = append(animationScenarios, name) + return name +} + +func NewAnimationScenario(tags []AnimationTags, keyFrames []AnimationKeyFrame) AnimationScenario { + if tags == nil { + ErrorLog(`Nil "tags" argument is not allowed.`) + return nil + } + + if len(tags) == 0 { + ErrorLog(`An empty "tags" argument is not allowed.`) + return nil + } + + animation := new(animationScenario) + animation.tags = tags + if keyFrames == nil && len(keyFrames) > 0 { + animation.keyFrames = keyFrames + } + animation.name = registerAnimationScenario() + + return animation +} + +func (animation *animationScenario) Name() string { + return animation.name +} + +func (animation *animationScenario) String() string { + writer := newRUIWriter() + animation.ruiString(writer) + return writer.finish() +} + +func (animation *animationScenario) ruiString(writer ruiWriter) { + // TODO +} + +func valueToCSS(tag string, value interface{}, session Session) string { + if value == nil { + return "" + } + + convertFloat := func(val float64) string { + if _, ok := sizeProperties[tag]; ok { + return fmt.Sprintf("%gpx", val) + } + return fmt.Sprintf("%g", val) + } + + switch value := value.(type) { + case string: + value, ok := session.resolveConstants(value) + if !ok { + return "" + } + if _, ok := sizeProperties[tag]; ok { + var size SizeUnit + if size.SetValue(value) { + return size.cssString("auto") + } + return "" + } + if isPropertyInList(tag, colorProperties) { + var color Color + if color.SetValue(value) { + return color.cssString() + } + return "" + } + if isPropertyInList(tag, angleProperties) { + var angle AngleUnit + if angle.SetValue(value) { + return angle.cssString() + } + return "" + } + if _, ok := enumProperties[tag]; ok { + var size SizeUnit + if size.SetValue(value) { + return size.cssString("auto") + } + return "" + } + return value + + case SizeUnit: + return value.cssString("auto") + + case AngleUnit: + return value.cssString() + + case Color: + return value.cssString() + + case float32: + return convertFloat(float64(value)) + + case float64: + return convertFloat(value) + + default: + if n, ok := isInt(value); ok { + if prop, ok := enumProperties[tag]; ok { + values := prop.cssValues + if n >= 0 && n < len(values) { + return values[n] + } + return "" + } + + return convertFloat(float64(n)) + } + } + return "" +} + +func (animation *animationScenario) cssString(session Session) string { + if animation.cssText != "" { + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + writeValue := func(tag string, value interface{}) { + if cssValue := valueToCSS(tag, value); cssValue != "" { + buffer.WriteString(" ") + buffer.WriteString(tag) + buffer.WriteString(": ") + buffer.WriteString(cssValue) + buffer.WriteString(";\n") + } + } + + buffer.WriteString(`@keyframes `) + buffer.WriteString(animation.name) + + buffer.WriteString(" {\n from {\n") + for _, property := range animation.tags { + writeValue(property.Tag, property.Start) + } + + buffer.WriteString(" }\n to {\n") + for _, property := range animation.tags { + writeValue(property.Tag, property.End) + } + buffer.WriteString(" }\n") + + if animation.keyFrames != nil { + for _, keyFrame := range animation.keyFrames { + if keyFrame.KeyFrame > 0 && keyFrame.KeyFrame < 100 && + keyFrame.Params != nil && len(keyFrame.Params) > 0 { + + buffer.WriteString(" ") + buffer.WriteString(strconv.Itoa(keyFrame.KeyFrame)) + buffer.WriteString("% {\n") + for tag, value := range keyFrame.Params { + writeValue(tag, value) + } + buffer.WriteString(" }\n") + + } + } + } + buffer.WriteString("}\n") + + animation.cssText = buffer.String() + } + + return animation.cssText +} +*/ diff --git a/appLog.go b/appLog.go new file mode 100644 index 0000000..7fe48a7 --- /dev/null +++ b/appLog.go @@ -0,0 +1,74 @@ +package rui + +import ( + "fmt" + "log" + "runtime" +) + +// ProtocolInDebugLog If it is set to true, then the protocol of the exchange between +// clients and the server is displayed in the debug log +var ProtocolInDebugLog = false + +var debugLogFunc func(string) = func(text string) { + log.Println("\033[34m" + text) +} + +var errorLogFunc = func(text string) { + log.Println("\033[31m" + text) + //println(text) +} + +// SetDebugLog sets a function for outputting debug info. +// The default value is nil (debug info is ignored) +func SetDebugLog(f func(string)) { + debugLogFunc = f +} + +// SetErrorLog sets a function for outputting error messages. +// The default value is log.Println(text) +func SetErrorLog(f func(string)) { + errorLogFunc = f +} + +// DebugLog print the text to the debug log +func DebugLog(text string) { + if debugLogFunc != nil { + debugLogFunc(text) + } +} + +// DebugLogF print the text to the debug log +func DebugLogF(format string, a ...interface{}) { + if debugLogFunc != nil { + debugLogFunc(fmt.Sprintf(format, a...)) + } +} + +// ErrorLog print the text to the error log +func ErrorLog(text string) { + if errorLogFunc != nil { + errorLogFunc(text) + errorStack() + } +} + +// ErrorLogF print the text to the error log +func ErrorLogF(format string, a ...interface{}) { + if errorLogFunc != nil { + errorLogFunc(fmt.Sprintf(format, a...)) + errorStack() + } +} + +func errorStack() { + if errorLogFunc != nil { + skip := 2 + _, file, line, ok := runtime.Caller(skip) + for ok { + errorLogFunc(fmt.Sprintf("\t%s: line %d", file, line)) + skip++ + _, file, line, ok = runtime.Caller(skip) + } + } +} diff --git a/app_scripts.js b/app_scripts.js new file mode 100644 index 0000000..f8047d1 --- /dev/null +++ b/app_scripts.js @@ -0,0 +1,1231 @@ +var sessionID = "0" +var socket +var socketUrl + +var images = new Map(); +var windowFocus = true + +function sendMessage(message) { + if (socket) { + socket.send(message) + } +} + +window.onload = function() { + socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://" + socketUrl += document.location.hostname + var port = document.location.port + if (port) { + socketUrl += ":" + port + } + socketUrl += window.location.pathname + "ws" + + socket = new WebSocket(socketUrl); + socket.onopen = socketOpen; + socket.onclose = socketClose; + socket.onerror = socketError; + socket.onmessage = function(event) { + window.execScript ? window.execScript(event.data) : window.eval(event.data); + }; +}; + +function socketOpen() { + + const touch_screen = (('ontouchstart' in document.documentElement) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)) ? "1" : "0"; + var message = "startSession{touch=" + touch_screen + + const style = window.getComputedStyle(document.body); + if (style) { + var direction = style.getPropertyValue('direction'); + if (direction) { + message += ",direction=" + direction + } + } + + const lang = window.navigator.languages; + if (lang) { + message += ",languages=\"" + lang + "\""; + } + + const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)"); + if (darkThemeMq.matches) { + message += ",dark=1"; + } + + const pixelRatio = window.devicePixelRatio; + if (pixelRatio) { + message += ",pixel-ratio=" + pixelRatio; + } + + sendMessage( message + "}" ); +} + +function socketReopen() { + sendMessage( "reconnect{session=" + sessionID + "}" ); +} + +function socketReconnect() { + if (!socket) { + socket = new WebSocket(socketUrl); + socket.onopen = socketReopen; + socket.onclose = socketClose; + socket.onerror = socketError; + socket.onmessage = function(event) { + window.execScript ? window.execScript(event.data) : window.eval(event.data); + }; + } +} + +function socketClose(event) { + console.log("socket closed") + socket = null; + if (!event.wasClean && windowFocus) { + window.setTimeout(socketReconnect, 10000); + } + /* + if (event.wasClean) { + alert('Connection was clean closed'); + } else { + alert('Connection was lost'); + } + alert('Code: ' + event.code + ' reason: ' + event.reason); + */ +} + +function socketError(error) { + console.log(error); +} + +window.onresize = function() { + scanElementsSize(); +} + +window.onbeforeunload = function(event) { + sendMessage( "session-close{session=" + sessionID +"}" ); +} + +window.onblur = function(event) { + windowFocus = false + sendMessage( "session-pause{session=" + sessionID +"}" ); +} + +window.onfocus = function(event) { + windowFocus = true + if (!socket) { + socketReconnect() + } else { + sendMessage( "session-resume{session=" + sessionID +"}" ); + } +} + +function getIntAttribute(element, tag) { + let value = element.getAttribute(tag); + if (value) { + return value; + } + return 0; +} + +function scanElementsSize() { + var views = document.getElementsByClassName("ruiView"); + if (views) { + var message = "resize{session=" + sessionID + ",views=[" + var count = 0 + for (var i = 0; i < views.length; i++) { + let element = views[i]; + let noresize = element.getAttribute("data-noresize"); + if (!noresize) { + let rect = element.getBoundingClientRect(); + let top = getIntAttribute(element, "data-top"); + let left = getIntAttribute(element, "data-left"); + let width = getIntAttribute(element, "data-width"); + let height = getIntAttribute(element, "data-height"); + if (rect.width > 0 && rect.height > 0 && + (width != rect.width || height != rect.height || left != rect.left || top != rect.top)) { + element.setAttribute("data-top", rect.top); + element.setAttribute("data-left", rect.left); + element.setAttribute("data-width", rect.width); + element.setAttribute("data-height", rect.height); + if (count > 0) { + message += ","; + } + message += "view{id=" + element.id + ",x=" + rect.left + ",y=" + rect.top + ",width=" + rect.width + ",height=" + rect.height + + ",scroll-x=" + element.scrollLeft + ",scroll-y=" + element.scrollTop + ",scroll-width=" + element.scrollWidth + ",scroll-height=" + element.scrollHeight + "}"; + count += 1; + } + } + } + + if (count > 0) { + sendMessage(message + "]}"); + } + } +} + +function scrollEvent(element, event) { + sendMessage("scroll{session=" + sessionID + ",id=" + element.id + ",x=" + element.scrollLeft + + ",y=" + element.scrollTop + ",width=" + element.scrollWidth + ",height=" + element.scrollHeight + "}"); +} + +function updateCSSRule(selector, ruleText) { + var styleSheet = document.styleSheets[0]; + var rules = styleSheet.cssRules ? styleSheet.cssRules : styleSheet.rules + selector = "." + selector + for (var i = 0; i < rules.length; i++) { + var rule = rules[i] + if (!rule.selectorText) { + continue; + } + if (rule.selectorText == selector) { + if (styleSheet.deleteRule) { + styleSheet.deleteRule(i) + } else if (styleSheet.removeRule) { + styleSheet.removeRule(i) + } + break; + } + } + if (styleSheet.insertRule) { + styleSheet.insertRule(selector + " { " + ruleText + "}") + } else if (styleSheet.addRule) { + styleSheet.addRule(selector, ruleText, rules.length) + } + scanElementsSize(); +} + +function updateCSSStyle(elementId, style) { + var element = document.getElementById(elementId); + if (element) { + element.style = style; + scanElementsSize(); + } +} + +function updateCSSProperty(elementId, property, value) { + var element = document.getElementById(elementId); + if (element) { + element.style[property] = value; + scanElementsSize(); + } +} + +function updateProperty(elementId, property, value) { + var element = document.getElementById(elementId); + if (element) { + element.setAttribute(property, value); + scanElementsSize(); + } +} + +function removeProperty(elementId, property, value) { + var element = document.getElementById(elementId); + if (element && element.hasAttribute(property)) { + element.removeAttribute(property); + scanElementsSize(); + } +} + +function updateInnerHTML(elementId, content) { + var element = document.getElementById(elementId); + if (element) { + element.innerHTML = content; + scanElementsSize(); + } +} + +function appendToInnerHTML(elementId, content) { + var element = document.getElementById(elementId); + if (element) { + element.innerHTML += content; + scanElementsSize(); + } +} + +function setDisabled(elementId, disabled) { + var element = document.getElementById(elementId); + if (element) { + if ('disabled' in element) { + element.disabled = disabled + } else { + element.setAttribute("data-disabled", disabled ? "1" : "0"); + } + scanElementsSize(); + } +} + +function focusEvent(element, event) { + event.stopPropagation(); + sendMessage("focus-event{session=" + sessionID + ",id=" + element.id + "}"); +} + +function blurEvent(element, event) { + event.stopPropagation(); + sendMessage("lost-focus-event{session=" + sessionID + ",id=" + element.id + "}"); +} + +function enterOrSpaceKeyClickEvent(event) { + if (event.key) { + return (event.key == " " || event.key == "Enter"); + } else if (event.keyCode) { + return (event.keyCode == 32 || event.keyCode == 13); + } + return false; +} + +function activateTab(layoutId, tabNumber) { + var element = document.getElementById(layoutId); + if (element) { + var currentTabId = element.getAttribute("data-current"); + var newTabId = layoutId + '-' + tabNumber; + if (currentTabId != newTabId) { + function setTab(tabId, styleProperty, display) { + var tab = document.getElementById(tabId); + if (tab) { + tab.className = element.getAttribute(styleProperty); + var page = document.getElementById(tab.getAttribute("data-view")); + if (page) { + page.style.display = display; + } + } + } + setTab(currentTabId, "data-inactiveTabStyle", "none") + setTab(newTabId, "data-activeTabStyle", ""); + element.setAttribute("data-current", newTabId); + scanElementsSize() + } + } +} + +function tabClickEvent(layoutId, tabNumber, event) { + event.stopPropagation(); + event.preventDefault(); + activateTab(layoutId, tabNumber) + sendMessage("tabClick{session=" + sessionID + ",id=" + layoutId + ",number=" + tabNumber + "}"); +} + +function tabKeyClickEvent(layoutId, tabNumber, event) { + if (enterOrSpaceKeyClickEvent(event)) { + tabClickEvent(layoutId, tabNumber, event) + } +} + +function keyEvent(element, event, tag) { + event.stopPropagation(); + + var message = tag + "{session=" + sessionID + ",id=" + element.id; + if (event.timeStamp) { + message += ",timeStamp=" + event.timeStamp; + } + if (event.key) { + message += ",key=\"" + event.key + "\""; + } + if (event.code) { + message += ",code=\"" + event.code + "\""; + } + if (event.repeat) { + message += ",repeat=1"; + } + if (event.ctrlKey) { + message += ",ctrlKey=1"; + } + if (event.shiftKey) { + message += ",shiftKey=1"; + } + if (event.altKey) { + message += ",altKey=1"; + } + if (event.metaKey) { + message += ",metaKey=1"; + } + + message += "}" + sendMessage(message); +} + +function keyDownEvent(element, event) { + keyEvent(element, event, "key-down-event") +} + +function keyUpEvent(element, event) { + keyEvent(element, event, "key-up-event") +} + +function mouseEventData(element, event) { + var message = "" + + if (event.timeStamp) { + message += ",timeStamp=" + event.timeStamp; + } + if (event.button) { + message += ",button=" + event.button; + } + if (event.buttons) { + message += ",buttons=" + event.buttons; + } + if (event.clientX) { + var x = event.clientX; + var el = element; + if (el.parentElement) { + x += el.parentElement.scrollLeft; + } + while (el) { + x -= el.offsetLeft + el = el.parentElement + } + + message += ",x=" + x + ",clientX=" + event.clientX; + } + if (event.clientY) { + var y = event.clientY; + var el = element; + if (el.parentElement) { + y += el.parentElement.scrollTop; + } + while (el) { + y -= el.offsetTop + el = el.parentElement + } + + message += ",y=" + y + ",clientY=" + event.clientY; + } + if (event.screenX) { + message += ",screenX=" + event.screenX; + } + if (event.screenY) { + message += ",screenY=" + event.screenY; + } + if (event.ctrlKey) { + message += ",ctrlKey=1"; + } + if (event.shiftKey) { + message += ",shiftKey=1"; + } + if (event.altKey) { + message += ",altKey=1"; + } + if (event.metaKey) { + message += ",metaKey=1"; + } + return message +} + +function mouseEvent(element, event, tag) { + event.stopPropagation(); + //event.preventDefault() + + var message = tag + "{session=" + sessionID + ",id=" + element.id + mouseEventData(element, event) + "}"; + sendMessage(message); +} + +function mouseDownEvent(element, event) { + mouseEvent(element, event, "mouse-down") +} + +function mouseUpEvent(element, event) { + mouseEvent(element, event, "mouse-up") +} + +function mouseMoveEvent(element, event) { + mouseEvent(element, event, "mouse-move") +} + +function mouseOverEvent(element, event) { + mouseEvent(element, event, "mouse-over") +} + +function mouseOutEvent(element, event) { + mouseEvent(element, event, "mouse-out") +} + +function clickEvent(element, event) { + mouseEvent(element, event, "click-event") + event.preventDefault(); +} + +function doubleClickEvent(element, event) { + mouseEvent(element, event, "double-click-event") + event.preventDefault(); +} + +function contextMenuEvent(element, event) { + mouseEvent(element, event, "context-menu-event") + event.preventDefault(); +} + +function pointerEvent(element, event, tag) { + event.stopPropagation(); + + var message = tag + "{session=" + sessionID + ",id=" + element.id + mouseEventData(element, event); + + if (event.pointerId) { + message += ",pointerId=" + event.pointerId; + } + if (event.width) { + message += ",width=" + event.width; + } + if (event.height) { + message += ",height=" + event.height; + } + if (event.pressure) { + message += ",pressure=" + event.pressure; + } + if (event.tangentialPressure) { + message += ",tangentialPressure=" + event.tangentialPressure; + } + if (event.tiltX) { + message += ",tiltX=" + event.tiltX; + } + if (event.tiltY) { + message += ",tiltY=" + event.tiltY; + } + if (event.twist) { + message += ",twist=" + event.twist; + } + if (event.pointerType) { + message += ",pointerType=" + event.pointerType; + } + if (event.isPrimary) { + message += ",isPrimary=1"; + } + + message += "}"; + sendMessage(message); +} + +function pointerDownEvent(element, event) { + pointerEvent(element, event, "pointer-down") +} + +function pointerUpEvent(element, event) { + pointerEvent(element, event, "pointer-up") +} + +function pointerMoveEvent(element, event) { + pointerEvent(element, event, "pointer-move") +} + +function pointerCancelEvent(element, event) { + pointerEvent(element, event, "pointer-cancel") +} + +function pointerOverEvent(element, event) { + pointerEvent(element, event, "pointer-over") +} + +function pointerOutEvent(element, event) { + pointerEvent(element, event, "pointer-out") +} + +function touchEvent(element, event, tag) { + event.stopPropagation(); + + var message = tag + "{session=" + sessionID + ",id=" + element.id; + if (event.timeStamp) { + message += ",timeStamp=" + event.timeStamp; + } + if (event.touches && event.touches.length > 0) { + message += ",touches=[" + for (var i = 0; i < event.touches.length; i++) { + var touch = event.touches.item(i) + if (touch) { + if (i > 0) { + message += "," + } + message += "touch{identifier=" + touch.identifier; + + var x = touch.clientX; + var y = touch.clientY; + var el = element; + if (el.parentElement) { + x += el.parentElement.scrollLeft; + y += el.parentElement.scrollTop; + } + while (el) { + x -= el.offsetLeft + y -= el.offsetTop + el = el.parentElement + } + + message += ",x=" + x + ",y=" + y + ",clientX=" + touch.clientX + ",clientY=" + touch.clientY + + ",screenX=" + touch.screenX + ",screenY=" + touch.screenY + ",radiusX=" + touch.radiusX + + ",radiusY=" + touch.radiusY + ",rotationAngle=" + touch.rotationAngle + ",force=" + touch.force + "}" + } + } + message += "]" + } + if (event.ctrlKey) { + message += ",ctrlKey=1"; + } + if (event.shiftKey) { + message += ",shiftKey=1"; + } + if (event.altKey) { + message += ",altKey=1"; + } + if (event.metaKey) { + message += ",metaKey=1"; + } + + message += "}"; + sendMessage(message); +} + +function touchStartEvent(element, event) { + touchEvent(element, event, "touch-start") +} + +function touchEndEvent(element, event) { + touchEvent(element, event, "touch-end") +} + +function touchMoveEvent(element, event) { + touchEvent(element, event, "touch-move") +} + +function touchCancelEvent(element, event) { + touchEvent(element, event, "touch-cancel") +} + +function dropDownListEvent(element, event) { + event.stopPropagation(); + var message = "itemSelected{session=" + sessionID + ",id=" + element.id + ",number=" + element.selectedIndex.toString() + "}" + sendMessage(message); +} + +function selectDropDownListItem(elementId, number) { + var element = document.getElementById(elementId); + if (element) { + element.selectedIndex = number; + scanElementsSize(); + } +} + +function listItemClickEvent(element, event) { + event.stopPropagation(); + var selected = false; + if (element.classList) { + selected = (element.classList.contains("ruiListItemFocused") || element.classList.contains("ruiListItemSelected")); + } else { + selected = element.className.indexOf("ruiListItemFocused") >= 0 || element.className.indexOf("ruiListItemSelected") >= 0; + } + + var list = element.parentNode + if (list) { + if (!selected) { + selectListItem(list, element, true) + } + + var message = "itemClick{session=" + sessionID + ",id=" + list.id + "}" + sendMessage(message); + } +} + +function getListItemNumber(itemId) { + var pos = itemId.indexOf("-") + if (pos >= 0) { + return parseInt(itemId.substring(pos+1)) + } +} + +function selectListItem(element, item, needSendMessage) { + var currentId = element.getAttribute("data-current"); + var message; + var focusStyle = element.getAttribute("data-focusitemstyle"); + var blurStyle = element.getAttribute("data-bluritemstyle"); + + if (!focusStyle) { + focusStyle = "ruiListItemFocused" + } + if (!blurStyle) { + blurStyle = "ruiListItemSelected" + } + + if (currentId) { + var current = document.getElementById(currentId); + if (current) { + if (current.classList) { + current.classList.remove(focusStyle, blurStyle); + } else { // IE < 10 + current.className = "ruiListItem"; + } + if (sendMessage) { + message = "itemUnselected{session=" + sessionID + ",id=" + element.id + "}"; + } + } + } + + if (item) { + if (element === document.activeElement) { + if (item.classList) { + item.classList.add(focusStyle); + } else { // IE < 10 + item.className = "ruiListItem " + focusStyle + } + } else { + if (item.classList) { + item.classList.add(blurStyle); + } else { // IE < 10 + item.className = "ruiListItem " + blurStyle + } + } + + element.setAttribute("data-current", item.id); + if (sendMessage) { + var number = getListItemNumber(item.id) + if (number != undefined) { + message = "itemSelected{session=" + sessionID + ",id=" + element.id + ",number=" + number + "}"; + } + } + + var left = item.offsetLeft - element.offsetLeft; + if (left < element.scrollLeft) { + element.scrollLeft = left; + } + + var top = item.offsetTop - element.offsetTop; + if (top < element.scrollTop) { + element.scrollTop = top; + } + + var right = left + item.offsetWidth; + if (right > element.scrollLeft + element.clientWidth) { + element.scrollLeft = right - element.clientWidth; + } + + var bottom = top + item.offsetHeight + if (bottom > element.scrollTop + element.clientHeight) { + element.scrollTop = bottom - element.clientHeight; + } + } + + if (needSendMessage && message != undefined) { + sendMessage(message); + } + scanElementsSize(); +} + +function findRightListItem(list, x, y) { + var result; + var count = list.childNodes.length; + for (var i = 0; i < count; i++) { + var item = list.childNodes[i]; + if (item.offsetLeft >= x) { + if (result) { + var result_dy = Math.abs(result.offsetTop - y); + var item_dy = Math.abs(item.offsetTop - y); + if (item_dy < result_dy || (item_dy == result_dy && (item.offsetLeft - x) < (result.offsetLeft - x))) { + result = item; + } + } else { + result = item; + } + } + } + return result +} + +function findLeftListItem(list, x, y) { + var result; + var count = list.childNodes.length; + for (var i = 0; i < count; i++) { + var item = list.childNodes[i]; + if (item.offsetLeft < x) { + if (result) { + var result_dy = Math.abs(result.offsetTop - y); + var item_dy = Math.abs(item.offsetTop - y); + if (item_dy < result_dy || (item_dy == result_dy && (x - item.offsetLeft) < (x - result.offsetLeft))) { + result = item; + } + } else { + result = item; + } + } + } + return result +} + +function findTopListItem(list, x, y) { + var result; + var count = list.childNodes.length; + for (var i = 0; i < count; i++) { + var item = list.childNodes[i]; + if (item.offsetTop < y) { + if (result) { + var result_dx = Math.abs(result.offsetLeft - x); + var item_dx = Math.abs(item.offsetLeft - x); + if (item_dx < result_dx || (item_dx == result_dx && (y - item.offsetTop) < (y - result.offsetTop))) { + result = item; + } + } else { + result = item; + } + } + } + return result +} + +function findBottomListItem(list, x, y) { + var result; + var count = list.childNodes.length; + for (var i = 0; i < count; i++) { + var item = list.childNodes[i]; + if (item.offsetTop >= y) { + if (result) { + var result_dx = Math.abs(result.offsetLeft - x); + var item_dx = Math.abs(item.offsetLeft - x); + if (item_dx < result_dx || (item_dx == result_dx && (item.offsetTop - y) < (result.offsetTop - y))) { + result = item; + } + } else { + result = item; + } + } + } + return result +} + +function listViewKeyDownEvent(element, event) { + var key; + if (event.key) { + key = event.key; + } else if (event.keyCode) { + switch (event.keyCode) { + case 13: key = "Enter"; break; + case 32: key = " "; break; + case 33: key = "PageUp"; break; + case 34: key = "PageDown"; break; + case 35: key = "End"; break; + case 36: key = "Home"; break; + case 37: key = "ArrowLeft"; break; + case 38: key = "ArrowUp"; break; + case 39: key = "ArrowRight"; break; + case 40: key = "ArrowDown"; break; + } + } + if (key) { + var currentId = element.getAttribute("data-current"); + var current + if (currentId) { + current = document.getElementById(currentId); + //number = getListItemNumber(currentId); + } + if (current) { + var item + switch (key) { + case " ": + case "Enter": + var message = "itemClick{session=" + sessionID + ",id=" + element.id + "}"; + sendMessage(message); + break; + + case "ArrowLeft": + item = findLeftListItem(element, current.offsetLeft, current.offsetTop); + break; + + case "ArrowRight": + item = findRightListItem(element, current.offsetLeft + current.offsetWidth, current.offsetTop); + break; + + case "ArrowDown": + item = findBottomListItem(element, current.offsetLeft, current.offsetTop + current.offsetHeight); + break; + + case "ArrowUp": + item = findTopListItem(element, current.offsetLeft, current.offsetTop); + break; + + case "Home": + item = element.childNodes[0]; + break; + + case "End": + item = element.childNodes[element.childNodes.length - 1]; + break; + + case "PageUp": + // TODO + break; + + case "PageDown": + // TODO + break; + + default: + return; + } + if (item && item !== current) { + selectListItem(element, item, true); + } + } + } + + event.stopPropagation(); + event.preventDefault(); +} + +function listViewFocusEvent(element, event) { + var currentId = element.getAttribute("data-current"); + if (currentId) { + var current = document.getElementById(currentId); + if (current) { + var focusStyle = element.getAttribute("data-focusitemstyle"); + var blurStyle = element.getAttribute("data-bluritemstyle"); + if (!focusStyle) { + focusStyle = "ruiListItemFocused" + } + if (!blurStyle) { + blurStyle = "ruiListItemSelected" + } + + if (current.classList) { + current.classList.remove(blurStyle); + current.classList.add(focusStyle); + } else { // IE < 10 + current.className = "ruiListItem " + focusStyle; + } + } + } +} + +function listViewBlurEvent(element, event) { + var currentId = element.getAttribute("data-current"); + if (currentId) { + var current = document.getElementById(currentId); + if (current) { + var focusStyle = element.getAttribute("data-focusitemstyle"); + var blurStyle = element.getAttribute("data-bluritemstyle"); + if (!focusStyle) { + focusStyle = "ruiListItemFocused" + } + if (!blurStyle) { + blurStyle = "ruiListItemSelected" + } + + if (current.classList) { + current.classList.remove(focusStyle); + current.classList.add(blurStyle); + } else { // IE < 10 + current.className = "ruiListItem " + blurStyle; + } + } + } +} + +function selectRadioButton(radioButtonId) { + var element = document.getElementById(radioButtonId); + if (element) { + var list = element.parentNode + if (list) { + var current = list.getAttribute("data-current"); + if (current) { + if (current === radioButtonId) { + return + } + + var mark = document.getElementById(current + "mark"); + if (mark) { + //mark.hidden = true + mark.style.visibility = "hidden" + } + } + + var mark = document.getElementById(radioButtonId + "mark"); + if (mark) { + //mark.hidden = false + mark.style.visibility = "visible" + } + list.setAttribute("data-current", radioButtonId); + var message = "radioButtonSelected{session=" + sessionID + ",id=" + list.id + ",radioButton=" + radioButtonId + "}" + sendMessage(message); + scanElementsSize(); + } + } +} + +function unselectRadioButtons(radioButtonsId) { + var list = document.getElementById(radioButtonsId); + if (list) { + var current = list.getAttribute("data-current"); + if (current) { + var mark = document.getElementById(current + "mark"); + if (mark) { + mark.style.visibility = "hidden" + } + + list.removeAttribute("data-current"); + } + + var message = "radioButtonUnselected{session=" + sessionID + ",id=" + list.id + "}" + sendMessage(message); + scanElementsSize(); + } +} + +function radioButtonClickEvent(element, event) { + event.stopPropagation(); + event.preventDefault(); + selectRadioButton(element.id) +} + +function radioButtonKeyClickEvent(element, event) { + if (enterOrSpaceKeyClickEvent(event)) { + radioButtonClickEvent(element, event); + } +} + +function editViewInputEvent(element) { + var text = element.value + text = text.replace(/\\/g, "\\\\") + text = text.replace(/\"/g, "\\\"") + var message = "textChanged{session=" + sessionID + ",id=" + element.id + ",text=\"" + text + "\"}" + sendMessage(message); +} + +function setInputValue(elementId, text) { + var element = document.getElementById(elementId); + if (element) { + element.value = text; + scanElementsSize(); + } +} + +function startResize(element, mx, my, event) { + var view = element.parentNode; + if (!view) { + return; + } + + var startX = event.clientX; + var startY = event.clientY; + var startWidth = view.offsetWidth + var startHeight = view.offsetHeight + + document.addEventListener("mousemove", moveHandler, true); + document.addEventListener("mouseup", upHandler, true); + + event.stopPropagation(); + event.preventDefault(); + + function moveHandler(e) { + if (mx != 0) { + var width = startWidth + (e.clientX - startX) * mx; + if (width <= 0) { + width = 1; + } + view.style.width = width + "px"; + sendMessage("widthChanged{session=" + sessionID + ",id=" + view.id + ",width=" + view.style.width + "}"); + } + + if (my != 0) { + var height = startHeight + (e.clientY - startY) * my; + if (height <= 0) { + height = 1; + } + view.style.height = height + "px"; + sendMessage("heightChanged{session=" + sessionID + ",id=" + view.id + ",height=" + view.style.height + "}"); + } + + event.stopPropagation(); + event.preventDefault(); + scanElementsSize(); + } + + function upHandler (e) { + document.removeEventListener("mouseup", upHandler, true); + document.removeEventListener("mousemove", moveHandler, true); + e.stopPropagation(); + } +} + +function transitionEndEvent(element, event) { + var message = "transitionEnd{session=" + sessionID + ",id=" + element.id; + if (event.propertyName) { + message += ",property=" + event.propertyName + } + sendMessage(message + "}"); + event.stopPropagation(); +} + +function transitionCancelEvent(element, event) { + var message = "transitionEnd{session=" + sessionID + ",id=" + element.id; + if (event.propertyName) { + message += ",property=" + event.propertyName + } + sendMessage(message + "}"); + event.stopPropagation(); +} + +function stackTransitionEndEvent(stackId, propertyName, event) { + sendMessage("transitionEnd{session=" + sessionID + ",id=" + stackId + ",property=" + propertyName + "}"); + event.stopPropagation(); +} + +function loadImage(url) { + var img = images.get(url); + if (img != undefined) { + return + } + + img = new Image(); + img.addEventListener("load", function() { + images.set(url, img) + var message = "imageLoaded{session=" + sessionID + ",url=\"" + url + "\""; + if (img.naturalWidth) { + message += ",width=" + img.naturalWidth + } + if (img.naturalHeight) { + message += ",height=" + img.naturalHeight + } + sendMessage(message + "}") + }, false); + + img.addEventListener("error", function(event) { + var message = "imageError{session=" + sessionID + ",url=\"" + url + "\""; + if (event && event.message) { + var text = event.message.replace(new RegExp("\"", 'g'), "\\\"") + message += ",message=\"" + text + "\""; + } + sendMessage(message + "}") + }, false); + + img.src = url; +} + +function clickOutsidePopup(e) { + sendMessage("clickOutsidePopup{session=" + sessionID + "}") + e.stopPropagation(); +} + +function clickClosePopup(element, e) { + var popupId = element.getAttribute("data-popupId"); + sendMessage("clickClosePopup{session=" + sessionID + ",id=" + popupId + "}") + e.stopPropagation(); +} + +function scrollTo(elementId, x, y) { + var element = document.getElementById(elementId); + if (element) { + element.scrollTo(x, y); + } +} + +function scrollToStart(elementId) { + var element = document.getElementById(elementId); + if (element) { + element.scrollTo(0, 0); + } +} + +function scrollToEnd(elementId) { + var element = document.getElementById(elementId); + if (element) { + element.scrollTo(0, element.scrollHeight - element.offsetHeight); + } +} + +function focus(elementId) { + var element = document.getElementById(elementId); + if (element) { + element.focus(); + } +} + +function playerEvent(element, tag) { + //event.stopPropagation(); + sendMessage(tag + "{session=" + sessionID + ",id=" + element.id + "}"); +} + +function playerTimeUpdatedEvent(element) { + var message = "time-update-event{session=" + sessionID + ",id=" + element.id + ",value="; + if (element.currentTime) { + message += element.currentTime; + } else { + message += "0"; + } + sendMessage(message + "}"); +} + +function playerDurationChangedEvent(element) { + var message = "duration-changed-event{session=" + sessionID + ",id=" + element.id + ",value="; + if (element.duration) { + message += element.duration; + } else { + message += "0"; + } + sendMessage(message + "}"); +} + +function playerVolumeChangedEvent(element) { + var message = "volume-changed-event{session=" + sessionID + ",id=" + element.id + ",value="; + if (element.volume && !element.muted) { + message += element.volume; + } else { + message += "0"; + } + sendMessage(message + "}"); +} + +function playerRateChangedEvent(element) { + var message = "rate-changed-event{session=" + sessionID + ",id=" + element.id + ",value="; + if (element.playbackRate) { + message += element.playbackRate; + } else { + message += "0"; + } + sendMessage(message + "}"); +} + +function playerErrorEvent(element) { + var message = "player-error-event{session=" + sessionID + ",id=" + element.id; + if (element.error) { + if (element.error.code) { + message += ",code=" + element.error.code; + } + if (element.error.message) { + message += ",message=`" + element.error.message + "`"; + } + } + sendMessage(message + "}"); +} + +function setMediaMuted(elementId, value) { + var element = document.getElementById(elementId); + if (element) { + element.muted = value + } +} + +function mediaPlay(elementId) { + var element = document.getElementById(elementId); + if (element && element.play) { + element.play() + } +} + +function mediaPause(elementId) { + var element = document.getElementById(elementId); + if (element && element.pause) { + element.pause() + } +} + +function mediaSetSetCurrentTime(elementId, time) { + var element = document.getElementById(elementId); + if (element) { + element.currentTime = time + } +} + +function mediaSetPlaybackRate(elementId, time) { + var element = document.getElementById(elementId); + if (element) { + element.playbackRate = time + } +} + +function mediaSetVolume(elementId, volume) { + var element = document.getElementById(elementId); + if (element) { + element.volume = volume + } +} diff --git a/app_styles.css b/app_styles.css new file mode 100644 index 0000000..8c327a9 --- /dev/null +++ b/app_styles.css @@ -0,0 +1,124 @@ +* { + box-sizing: border-box; + padding: 0; + margin: 0; + overflow: hidden; + min-width: 1px; + min-height: 1px; + text-overflow: ellipsis; +} + +div { + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +div:focus { + outline: none; +} + +input { + padding: 4px; + overflow: auto; +} + +textarea { + padding: 4px; + overflow: auto; +} + +ul:focus { + outline: none; +} + +body { + margin: 0 auto; + width: 100%; + height: 100vh; +} + +.ruiRoot { + position: absolute; + top: 0px; + bottom: 0px; + right: 0px; + left: 0px; +} + +.ruiPopupLayer { + background-color: rgba(128,128,128,0.1); + position: absolute; + top: 0px; + bottom: 0px; + right: 0px; + left: 0px; +} + +.ruiView { +} + +.ruiAbsoluteLayout { + position: relative; +} + +.ruiGridLayout { + display: grid; +} + +.ruiListLayout { + display: flex; +} + +.ruiStackLayout { + display: grid; +} + +.ruiStackPageLayout { + display: grid; + width: 100%; + height: 100%; + align-items: stretch; + justify-items: stretch; + grid-column-start: 1; + grid-column-end: 2; + grid-row-start: 1; + grid-row-end: 2; +} + +.ruiTabsLayout { + display: grid; +} + +.ruiImageView { + display: grid; +} + +.ruiListView { + overflow: auto; + display: flex; + align-content: stretch; +} +/* +@media (prefers-color-scheme: light) { + body { + background: #FFF; + color: #000; + } + .ruiRoot { + background-color: #FFFFFF; + } +} + +@media (prefers-color-scheme: dark) { + body { + background: #303030; + color: #F0F0F0; + } + .ruiRoot { + background-color: #303030; + } +} +*/ diff --git a/application.go b/application.go new file mode 100644 index 0000000..f459379 --- /dev/null +++ b/application.go @@ -0,0 +1,297 @@ +package rui + +import ( + _ "embed" + "fmt" + "io" + "log" + "math/rand" + "net/http" + "os/exec" + "runtime" + "strconv" +) + +//go:embed app_scripts.js +var defaultScripts string + +//go:embed app_styles.css +var appStyles string + +//go:embed defaultTheme.rui +var defaultThemeText string + +// Application - app interface +type Application interface { + // Start - start the application life cycle + Start(addr string) + Finish() + nextSessionID() int + removeSession(id int) +} + +type application struct { + name, icon string + createContentFunc func(Session) SessionContent + sessions map[int]Session +} + +func (app *application) getStartPage() string { + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + buffer.WriteString(` + + + + `) + buffer.WriteString(app.name) + buffer.WriteString("") + if app.icon != "" { + buffer.WriteString(` + `) + } + + buffer.WriteString(` + + + + + + +
+ + +`) + + return buffer.String() +} + +func (app *application) init(name, icon string) { + app.name = name + app.icon = icon + app.sessions = map[int]Session{} +} + +func (app *application) Start(addr string) { + http.Handle("/", app) + log.Fatal(http.ListenAndServe(addr, nil)) +} + +func (app *application) Finish() { + for _, session := range app.sessions { + session.close() + } + +} + +func (app *application) nextSessionID() int { + n := rand.Intn(0x7FFFFFFE) + 1 + _, ok := app.sessions[n] + for ok { + n = rand.Intn(0x7FFFFFFE) + 1 + _, ok = app.sessions[n] + } + return n +} + +func (app *application) removeSession(id int) { + delete(app.sessions, id) +} + +func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) { + + if ProtocolInDebugLog { + DebugLogF("%s %s", req.Method, req.URL.Path) + } + + switch req.Method { + case "GET": + switch req.URL.Path { + case "/": + w.WriteHeader(http.StatusOK) + io.WriteString(w, app.getStartPage()) + + case "/ws": + if brige := CreateSocketBrige(w, req); brige != nil { + go app.socketReader(brige) + } + + default: + filename := req.URL.Path[1:] + if size := len(filename); size > 0 && filename[size-1] == '/' { + filename = filename[:size-1] + } + + if !serveResourceFile(filename, w, req) { + w.WriteHeader(http.StatusNotFound) + } + } + } +} + +func (app *application) socketReader(brige WebBrige) { + var session Session + events := make(chan DataObject, 1024) + + for { + message, ok := brige.ReadMessage() + if !ok { + events <- NewDataObject("disconnect") + return + } + + if ProtocolInDebugLog { + DebugLog(message) + } + + if obj := ParseDataText(message); obj != nil { + command := obj.Tag() + switch command { + case "startSession": + answer := "" + if session, answer = app.startSession(obj, events, brige); session != nil { + if !brige.WriteMessage(answer) { + return + } + session.onStart() + go sessionEventHandler(session, events, brige) + } + + case "reconnect": + if sessionText, ok := obj.PropertyValue("session"); ok { + if sessionID, err := strconv.Atoi(sessionText); err == nil { + if session = app.sessions[sessionID]; session != nil { + session.setBrige(events, brige) + answer := allocStringBuilder() + defer freeStringBuilder(answer) + + session.writeInitScript(answer) + if !brige.WriteMessage(answer.String()) { + return + } + session.onReconnect() + go sessionEventHandler(session, events, brige) + return + } + DebugLogF("Session #%d not exists", sessionID) + } else { + ErrorLog(`strconv.Atoi(sessionText) error: ` + err.Error()) + } + } else { + ErrorLog(`"session" key not found`) + } + + answer := "" + if session, answer = app.startSession(obj, events, brige); session != nil { + if !brige.WriteMessage(answer) { + return + } + session.onStart() + go sessionEventHandler(session, events, brige) + } + + case "answer": + session.handleAnswer(obj) + + case "imageLoaded": + session.imageManager().imageLoaded(obj, session) + + case "imageError": + session.imageManager().imageLoadError(obj, session) + + default: + events <- obj + } + } + } +} + +func sessionEventHandler(session Session, events chan DataObject, brige WebBrige) { + for { + data := <-events + + switch command := data.Tag(); command { + case "disconnect": + session.onDisconnect() + return + + case "session-close": + session.onFinish() + session.App().removeSession(session.ID()) + brige.Close() + + case "session-pause": + session.onPause() + + case "session-resume": + session.onResume() + + case "resize": + session.handleResize(data) + + default: + session.handleViewEvent(command, data) + } + } +} + +func (app *application) startSession(params DataObject, events chan DataObject, brige WebBrige) (Session, string) { + if app.createContentFunc == nil { + return nil, "" + } + + session := newSession(app, app.nextSessionID(), "", params) + session.setBrige(events, brige) + if !session.setContent(app.createContentFunc(session), session) { + return nil, "" + } + + app.sessions[session.ID()] = session + + answer := allocStringBuilder() + defer freeStringBuilder(answer) + + answer.WriteString("sessionID = '") + answer.WriteString(strconv.Itoa(session.ID())) + answer.WriteString("';\n") + session.writeInitScript(answer) + answerText := answer.String() + + if ProtocolInDebugLog { + DebugLog("Start session:") + DebugLog(answerText) + } + return session, answerText +} + +// NewApplication - create the new application of the single view type. +func NewApplication(name, icon string, createContentFunc func(Session) SessionContent) Application { + app := new(application) + app.init(name, icon) + app.createContentFunc = createContentFunc + return app +} + +func OpenBrowser(url string) bool { + var err error + + switch runtime.GOOS { + case "linux": + err = exec.Command("xdg-open", url).Start() + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + err = exec.Command("open", url).Start() + default: + err = fmt.Errorf("unsupported platform") + } + + return err != nil +} diff --git a/audioPlayer.go b/audioPlayer.go new file mode 100644 index 0000000..3fd8053 --- /dev/null +++ b/audioPlayer.go @@ -0,0 +1,31 @@ +package rui + +type AudioPlayer interface { + MediaPlayer +} + +type audioPlayerData struct { + mediaPlayerData +} + +// NewAudioPlayer create new MediaPlayer object and return it +func NewAudioPlayer(session Session, params Params) MediaPlayer { + view := new(audioPlayerData) + view.Init(session) + view.tag = "AudioPlayer" + setInitParams(view, params) + return view +} + +func newAudioPlayer(session Session) View { + return NewAudioPlayer(session, nil) +} + +func (player *audioPlayerData) Init(session Session) { + player.mediaPlayerData.Init(session) + player.tag = "AudioPlayer" +} + +func (player *audioPlayerData) htmlTag() string { + return "audio" +} diff --git a/background.go b/background.go new file mode 100644 index 0000000..443bd04 --- /dev/null +++ b/background.go @@ -0,0 +1,716 @@ +package rui + +import "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 + + // 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 + + // 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 + + // 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 +) + +// BackgroundElement describes the background element. +type BackgroundElement interface { + Properties + cssStyle(view View) string + Tag() string +} + +type backgroundElement struct { + propertyList +} + +type backgroundImage struct { + backgroundElement +} + +// BackgroundGradientPoint define point on gradient straight line +type BackgroundGradientPoint struct { + // Pos - the distance from the start of the gradient straight line + Pos SizeUnit + // Color - the color of the point + Color Color +} + +type backgroundGradient struct { + backgroundElement +} + +type backgroundLinearGradient struct { + backgroundGradient +} + +type backgroundRadialGradient struct { + backgroundGradient +} + +// 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]interface{}{} + result = image + + case "linear-gradient": + gradient := new(backgroundLinearGradient) + gradient.properties = map[string]interface{}{} + result = gradient + + case "radial-gradient": + gradient := new(backgroundRadialGradient) + gradient.properties = map[string]interface{}{} + result = gradient + + default: + return nil + } + + count := obj.PropertyCount() + for i := 0; i < count; i++ { + if node := obj.Property(i); node.Type() == TextNode { + if value := node.Text(); value != "" { + result.Set(node.Tag(), value) + } + } + } + + return result +} + +// NewBackgroundImage creates the new background image +func NewBackgroundImage(params Params) BackgroundElement { + result := new(backgroundImage) + result.properties = map[string]interface{}{} + for tag, value := range params { + result.Set(tag, value) + } + return result +} + +// NewBackgroundLinearGradient creates the new background linear gradient +func NewBackgroundLinearGradient(params Params) BackgroundElement { + result := new(backgroundLinearGradient) + result.properties = map[string]interface{}{} + 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]interface{}{} + for tag, value := range params { + result.Set(tag, value) + } + return result +} + +func (image *backgroundImage) Tag() string { + return "image" +} + +func (image *backgroundImage) normalizeTag(tag string) string { + tag = strings.ToLower(tag) + switch tag { + case "source": + tag = Source + + case Fit: + tag = backgroundFit + + case HorizontalAlign: + tag = ImageHorizontalAlign + + case VerticalAlign: + tag = ImageVerticalAlign + } + + return tag +} + +func (image *backgroundImage) Set(tag string, value interface{}) 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) interface{} { + return image.backgroundElement.Get(image.normalizeTag(tag)) +} + +func (image *backgroundImage) cssStyle(view View) string { + session := view.Session() + if src, ok := stringProperty(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")) + buffer.WriteRune(' ') + buffer.WriteString(height.cssString("auto")) + } + } + + 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 (gradient *backgroundGradient) Set(tag string, value interface{}) bool { + + switch tag = strings.ToLower(tag); tag { + case Repeat: + return gradient.setBoolProperty(tag, value) + + case Gradient: + switch value := value.(type) { + case string: + if value != "" { + elements := strings.Split(value, `,`) + if count := len(elements); count > 1 { + points := make([]interface{}, count) + for i, element := range elements { + if strings.Contains(element, "@") { + points[i] = element + } else { + var point BackgroundGradientPoint + if point.setValue(element) { + points[i] = point + } else { + ErrorLogF("Invalid gradient element #%d: %s", i, element) + return false + } + } + } + gradient.properties[Gradient] = points + return true + } + + text := strings.Trim(value, " \n\r\t") + if text[0] == '@' { + gradient.properties[Gradient] = text + 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 + points[i].Pos = AutoSize() + } + 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 + } + + case []interface{}: + if count := len(value); count > 1 { + points := make([]interface{}, count) + for i, element := range value { + switch element := element.(type) { + case string: + if strings.Contains(element, "@") { + points[i] = element + } else { + var point BackgroundGradientPoint + if !point.setValue(element) { + ErrorLogF("Invalid gradient element #%d: %s", i, element) + return false + } + points[i] = point + } + + case BackgroundGradientPoint: + points[i] = element + + case GradientPoint: + points[i] = BackgroundGradientPoint{Color: element.Color, Pos: Percent(element.Offset * 100)} + + case Color: + points[i] = BackgroundGradientPoint{Color: element, Pos: AutoSize()} + + default: + ErrorLogF("Invalid gradient element #%d: %v", i, element) + return false + } + } + gradient.properties[Gradient] = points + return true + } + } + + default: + ErrorLogF("Invalid gradient %v", value) + return false + } + + return gradient.backgroundElement.Set(tag, value) +} + +func (point *BackgroundGradientPoint) setValue(value string) bool { + var ok bool + + switch elements := strings.Split(value, `:`); len(elements) { + case 2: + if point.Color, ok = StringToColor(elements[0]); !ok { + return false + } + if point.Pos, ok = StringToSizeUnit(elements[1]); !ok { + return false + } + + case 1: + if point.Color, ok = StringToColor(elements[0]); !ok { + return false + } + point.Pos = AutoSize() + + default: + return false + } + + return false +} + +func (gradient *backgroundGradient) writeGradient(view View, buffer *strings.Builder) bool { + + value, ok := gradient.properties[Gradient] + if !ok { + return false + } + + points := []BackgroundGradientPoint{} + + switch value := value.(type) { + case string: + if text, ok := view.Session().resolveConstants(value); ok && text != "" { + elements := strings.Split(text, `,`) + points := make([]BackgroundGradientPoint, len(elements)) + for i, element := range elements { + if !points[i].setValue(element) { + ErrorLogF(`Invalid gradient point #%d: "%s"`, i, element) + return false + } + } + } else { + ErrorLog(`Invalid gradient: ` + value) + return false + } + + case []BackgroundGradientPoint: + points = value + + case []interface{}: + points = make([]BackgroundGradientPoint, len(value)) + for i, element := range value { + switch element := element.(type) { + case string: + if text, ok := view.Session().resolveConstants(element); ok && text != "" { + if !points[i].setValue(text) { + ErrorLogF(`Invalid gradient point #%d: "%s"`, i, text) + return false + } + } else { + ErrorLogF(`Invalid gradient point #%d: "%s"`, i, text) + return false + } + + case BackgroundGradientPoint: + points[i] = element + } + } + } + + if len(points) > 0 { + for i, point := range points { + if i > 0 { + buffer.WriteString(`, `) + } + + buffer.WriteString(point.Color.cssString()) + if point.Pos.Type != Auto { + buffer.WriteRune(' ') + buffer.WriteString(point.Pos.cssString("")) + } + } + return true + } + + return false +} + +func (gradient *backgroundLinearGradient) Tag() string { + return "linear-gradient" +} + +func (gradient *backgroundLinearGradient) Set(tag string, value interface{}) bool { + if tag == Direction { + switch value := value.(type) { + case AngleUnit: + gradient.properties[Direction] = value + return true + + case string: + var angle AngleUnit + if ok, _ := angle.setValue(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(view View) string { + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + session := view.Session() + 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(view, buffer) { + return "" + } + + buffer.WriteString(") ") + return buffer.String() +} + +func (gradient *backgroundRadialGradient) Tag() string { + return "radial-gradient" +} + +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 interface{}) bool { + tag = gradient.normalizeTag(tag) + switch tag { + case RadialGradientRadius: + switch value := value.(type) { + case string, SizeUnit: + return gradient.propertyList.Set(RadialGradientRadius, value) + + 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: + return gradient.propertyList.Set(RadialGradientShape, value) + + case CenterX, CenterY: + return gradient.propertyList.Set(tag, value) + } + + return gradient.backgroundGradient.Set(tag, value) +} + +func (gradient *backgroundRadialGradient) Get(tag string) interface{} { + return gradient.backgroundGradient.Get(gradient.normalizeTag(tag)) +} + +func (gradient *backgroundRadialGradient) cssStyle(view View) string { + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + session := view.Session() + if repeating, _ := boolProperty(gradient, Repeating, session); repeating { + buffer.WriteString(`repeating-radial-gradient(`) + } else { + buffer.WriteString(`radial-gradient(`) + } + + if shape, ok := enumProperty(gradient, RadialGradientShape, session, EllipseGradient); ok && shape == CircleGradient { + buffer.WriteString(`circle `) + } else { + buffer.WriteString(`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(values.cssValues[n]) + buffer.WriteString(" ") + } else { + if r, ok := StringToSizeUnit(text); ok && r.Type != Auto { + buffer.WriteString(r.cssString("")) + buffer.WriteString(" ") + } else { + ErrorLog(`Invalid linear gradient radius: ` + text) + } + } + } else { + ErrorLog(`Invalid linear gradient radius: ` + value) + } + + case int: + values := enumProperties[RadialGradientRadius].cssValues + if value >= 0 && value < len(values) { + buffer.WriteString(values[value]) + buffer.WriteString(" ") + } else { + ErrorLogF(`Invalid linear gradient radius: %d`, value) + } + + case SizeUnit: + if value.Type != Auto { + buffer.WriteString(value.cssString("")) + buffer.WriteString(" ") + } + } + } + + x, _ := sizeProperty(gradient, CenterX, session) + y, _ := sizeProperty(gradient, CenterX, session) + if x.Type != Auto || y.Type != Auto { + buffer.WriteString("at ") + buffer.WriteString(x.cssString("50%")) + buffer.WriteString(" ") + buffer.WriteString(y.cssString("50%")) + } + + buffer.WriteString(", ") + if !gradient.writeGradient(view, buffer) { + return "" + } + + buffer.WriteString(") ") + + return buffer.String() +} diff --git a/border.go b/border.go new file mode 100644 index 0000000..d7d0644 --- /dev/null +++ b/border.go @@ -0,0 +1,710 @@ +package rui + +import ( + "fmt" + "strings" +) + +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" + // TopStyle is the constant for "top-style" property tag + TopStyle = "top-style" + // BottomStyle is the constant for "bottom-style" property tag + BottomStyle = "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" + // TopWidth is the constant for "top-width" property tag + TopWidth = "top-width" + // BottomWidth is the constant for "bottom-width" property tag + BottomWidth = "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" + // TopColor is the constant for "top-color" property tag + TopColor = "top-color" + // BottomColor is the constant for "bottom-color" property tag + BottomColor = "bottom-color" +) + +// BorderProperty is the interface of a view border data +type BorderProperty interface { + Properties + ruiStringer + fmt.Stringer + ViewBorders(session Session) ViewBorders + delete(tag string) + cssStyle(builder cssBuilder, session Session) + cssWidth(builder cssBuilder, session Session) + cssColor(builder cssBuilder, session Session) + cssStyleValue(session Session) string + cssWidthValue(session Session) string + cssColorValue(session Session) string +} + +type borderProperty struct { + propertyList +} + +func newBorderProperty(value interface{}) BorderProperty { + border := new(borderProperty) + border.properties = map[string]interface{}{} + + if value != nil { + switch value := value.(type) { + case BorderProperty: + return value + + case DataObject: + _ = border.setBorderObject(value) + + case ViewBorder: + border.properties[Style] = value.Style + border.properties[Width] = value.Width + border.properties[ColorProperty] = value.Color + + case ViewBorders: + if value.Left.Style == value.Right.Style && + value.Left.Style == value.Top.Style && + value.Left.Style == value.Bottom.Style { + border.properties[Style] = value.Left.Style + } else { + border.properties[LeftStyle] = value.Left.Style + border.properties[RightStyle] = value.Right.Style + border.properties[TopStyle] = value.Top.Style + border.properties[BottomStyle] = value.Bottom.Style + } + if value.Left.Width.Equal(value.Right.Width) && + value.Left.Width.Equal(value.Top.Width) && + value.Left.Width.Equal(value.Bottom.Width) { + border.properties[Width] = value.Left.Width + } else { + border.properties[LeftWidth] = value.Left.Width + border.properties[RightWidth] = value.Right.Width + border.properties[TopWidth] = value.Top.Width + border.properties[BottomWidth] = value.Bottom.Width + } + if value.Left.Color == value.Right.Color && + value.Left.Color == value.Top.Color && + value.Left.Color == value.Bottom.Color { + border.properties[ColorProperty] = value.Left.Color + } else { + border.properties[LeftColor] = value.Left.Color + border.properties[RightColor] = value.Right.Color + border.properties[TopColor] = value.Top.Color + border.properties[BottomColor] = value.Bottom.Color + } + + default: + invalidPropertyValue(Border, value) + return nil + } + } + return border +} + +// NewBorder creates the new BorderProperty +func NewBorder(params Params) BorderProperty { + border := new(borderProperty) + border.properties = map[string]interface{}{} + if params != nil { + for _, tag := range []string{Style, Width, ColorProperty, Left, Right, Top, Bottom, + LeftStyle, RightStyle, TopStyle, BottomStyle, + LeftWidth, RightWidth, TopWidth, BottomWidth, + LeftColor, RightColor, TopColor, BottomColor} { + if value, ok := params[tag]; ok && value != nil { + border.Set(tag, value) + } + } + } + return border +} + +func (border *borderProperty) normalizeTag(tag string) string { + tag = strings.ToLower(tag) + switch tag { + case BorderLeft, CellBorderLeft: + return Left + + case BorderRight, CellBorderRight: + return Right + + case BorderTop, CellBorderTop: + return Top + + case BorderBottom, CellBorderBottom: + return Bottom + + case BorderStyle, CellBorderStyle: + return Style + + case BorderLeftStyle, CellBorderLeftStyle, "style-left": + return LeftStyle + + case BorderRightStyle, CellBorderRightStyle, "style-right": + return RightStyle + + case BorderTopStyle, CellBorderTopStyle, "style-top": + return TopStyle + + case BorderBottomStyle, CellBorderBottomStyle, "style-bottom": + return BottomStyle + + case BorderWidth, CellBorderWidth: + return Width + + case BorderLeftWidth, CellBorderLeftWidth, "width-left": + return LeftWidth + + case BorderRightWidth, CellBorderRightWidth, "width-right": + return RightWidth + + case BorderTopWidth, CellBorderTopWidth, "width-top": + return TopWidth + + case BorderBottomWidth, CellBorderBottomWidth, "width-bottom": + return BottomWidth + + case BorderColor, CellBorderColor: + return ColorProperty + + case BorderLeftColor, CellBorderLeftColor, "color-left": + return LeftColor + + case BorderRightColor, CellBorderRightColor, "color-right": + return RightColor + + case BorderTopColor, CellBorderTopColor, "color-top": + return TopColor + + case BorderBottomColor, CellBorderBottomColor, "color-bottom": + return BottomColor + } + + return tag +} + +func (border *borderProperty) ruiString(writer ruiWriter) { + writer.startObject("_") + + for _, tag := range []string{Style, Width, ColorProperty} { + if value, ok := border.properties[tag]; ok { + writer.writeProperty(Style, value) + } + } + + for _, side := range []string{Top, Right, Bottom, Left} { + style, okStyle := border.properties[side+"-"+Style] + width, okWidth := border.properties[side+"-"+Width] + color, okColor := border.properties[side+"-"+ColorProperty] + if okStyle || okWidth || okColor { + writer.startObjectProperty(side, "_") + if okStyle { + writer.writeProperty(Style, style) + } + if okWidth { + writer.writeProperty(Width, width) + } + if okColor { + writer.writeProperty(ColorProperty, color) + } + writer.endObject() + } + } + // TODO + writer.endObject() +} + +func (border *borderProperty) String() string { + writer := newRUIWriter() + border.ruiString(writer) + return writer.finish() +} + +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(ColorProperty); 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.PropertyWithTag(side); node != nil { + if node.Type() == ObjectNode { + if !border.setSingleBorderObject(side, node.Object()) { + 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) { + result = false + } + } + + default: + notCompatibleType(Style, text) + result = false + } + } + + if text, ok := obj.PropertyValue(ColorProperty); ok { + values := split4Values(text) + switch len(values) { + case 1: + if !border.setColorProperty(ColorProperty, 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(ColorProperty, 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) + + switch tag { + case Style: + for _, t := range []string{tag, TopStyle, RightStyle, BottomStyle, LeftStyle} { + delete(border.properties, t) + } + + case Width: + for _, t := range []string{tag, TopWidth, RightWidth, BottomWidth, LeftWidth} { + delete(border.properties, t) + } + + case ColorProperty: + for _, t := range []string{tag, TopColor, RightColor, BottomColor, LeftColor} { + delete(border.properties, t) + } + + case Left, Right, Top, Bottom: + border.Remove(tag + "-style") + border.Remove(tag + "-width") + border.Remove(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} { + if t != tag { + if _, ok := border.properties[t]; !ok { + border.properties[t] = style + } + } + } + } + + 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} { + if t != tag { + if _, ok := border.properties[t]; !ok { + border.properties[t] = width + } + } + } + } + + case LeftColor, RightColor, TopColor, BottomColor: + delete(border.properties, tag) + if color, ok := border.properties[ColorProperty]; ok && color != nil { + for _, t := range []string{TopColor, RightColor, BottomColor, LeftColor} { + if t != tag { + if _, ok := border.properties[t]; !ok { + border.properties[t] = color + } + } + } + } + + default: + ErrorLogF(`"%s" property is not compatible with the BorderProperty`, tag) + } +} + +func (border *borderProperty) Set(tag string, value interface{}) bool { + if value == nil { + border.Remove(tag) + return true + } + + tag = border.normalizeTag(tag) + + switch tag { + case Style: + if border.setEnumProperty(Style, value, enumProperties[BorderStyle].values) { + for _, side := range []string{TopStyle, RightStyle, BottomStyle, LeftStyle} { + delete(border.properties, side) + } + return true + } + + case Width: + if border.setSizeProperty(Width, value) { + for _, side := range []string{TopWidth, RightWidth, BottomWidth, LeftWidth} { + delete(border.properties, side) + } + return true + } + + case ColorProperty: + if border.setColorProperty(ColorProperty, value) { + for _, side := range []string{TopColor, RightColor, BottomColor, LeftColor} { + delete(border.properties, side) + } + return true + } + + case LeftStyle, RightStyle, TopStyle, BottomStyle: + return border.setEnumProperty(tag, value, enumProperties[BorderStyle].values) + + case LeftWidth, RightWidth, TopWidth, BottomWidth: + return border.setSizeProperty(tag, value) + + case LeftColor, RightColor, TopColor, BottomColor: + return border.setColorProperty(tag, value) + + case Left, Right, Top, Bottom: + switch value := value.(type) { + case string: + if obj := ParseDataText(value); obj != nil { + return border.setSingleBorderObject(tag, obj) + } + + case DataObject: + return border.setSingleBorderObject(tag, value) + + case BorderProperty: + styleTag := tag + "-" + Style + if style := value.Get(styleTag); value != nil { + border.properties[styleTag] = style + } + colorTag := tag + "-" + ColorProperty + if color := value.Get(colorTag); value != nil { + border.properties[colorTag] = color + } + widthTag := tag + "-" + Width + if width := value.Get(widthTag); value != nil { + border.properties[widthTag] = width + } + return true + + case ViewBorder: + border.properties[tag+"-"+Style] = value.Style + border.properties[tag+"-"+Width] = value.Width + border.properties[tag+"-"+ColorProperty] = value.Color + return true + } + fallthrough + + default: + ErrorLogF(`"%s" property is not compatible with the BorderProperty`, tag) + } + + return false +} + +func (border *borderProperty) Get(tag string) interface{} { + tag = border.normalizeTag(tag) + + if result, ok := border.properties[tag]; ok { + return result + } + + switch tag { + case Left, Right, Top, Bottom: + result := newBorderProperty(nil) + if style, ok := border.properties[tag+"-"+Style]; ok { + result.Set(Style, style) + } else if style, ok := border.properties[Style]; ok { + result.Set(Style, style) + } + if width, ok := border.properties[tag+"-"+Width]; ok { + result.Set(Width, width) + } else if width, ok := border.properties[Width]; ok { + result.Set(Width, width) + } + if color, ok := border.properties[tag+"-"+ColorProperty]; ok { + result.Set(ColorProperty, color) + } else if color, ok := border.properties[ColorProperty]; ok { + result.Set(ColorProperty, color) + } + return result + + case LeftStyle, RightStyle, TopStyle, BottomStyle: + if style, ok := border.properties[tag]; ok { + return style + } + return border.properties[Style] + + case LeftWidth, RightWidth, TopWidth, BottomWidth: + if width, ok := border.properties[tag]; ok { + return width + } + return border.properties[Width] + + case LeftColor, RightColor, TopColor, BottomColor: + if color, ok := border.properties[tag]; ok { + return color + } + return border.properties[ColorProperty] + } + + return nil +} + +func (border *borderProperty) delete(tag string) { + tag = border.normalizeTag(tag) + remove := []string{} + + switch tag { + case Style: + remove = []string{Style, LeftStyle, RightStyle, TopStyle, BottomStyle} + + case Width: + remove = []string{Width, LeftWidth, RightWidth, TopWidth, BottomWidth} + + case ColorProperty: + remove = []string{ColorProperty, LeftColor, RightColor, TopColor, BottomColor} + + case Left, Right, Top, Bottom: + if border.Get(Style) != nil { + border.properties[tag+"-"+Style] = 0 + remove = []string{tag + "-" + ColorProperty, tag + "-" + Width} + } else { + remove = []string{tag + "-" + Style, tag + "-" + ColorProperty, tag + "-" + Width} + } + + case LeftStyle, RightStyle, TopStyle, BottomStyle: + if border.Get(Style) != nil { + border.properties[tag] = 0 + } else { + remove = []string{tag} + } + + case LeftWidth, RightWidth, TopWidth, BottomWidth: + if border.Get(Width) != nil { + border.properties[tag] = AutoSize() + } else { + remove = []string{tag} + } + + case LeftColor, RightColor, TopColor, BottomColor: + if border.Get(ColorProperty) != nil { + border.properties[tag] = 0 + } else { + remove = []string{tag} + } + } + + for _, tag := range remove { + delete(border.properties, tag) + } +} + +func (border *borderProperty) ViewBorders(session Session) ViewBorders { + + defStyle, _ := valueToEnum(border.getRaw(Style), BorderStyle, session, NoneLine) + defWidth, _ := sizeProperty(border, Width, session) + defColor, _ := colorProperty(border, ColorProperty, session) + + getBorder := func(prefix string) ViewBorder { + var result ViewBorder + var ok bool + if result.Style, ok = valueToEnum(border.getRaw(prefix+Style), BorderStyle, session, NoneLine); !ok { + result.Style = defStyle + } + if result.Width, ok = sizeProperty(border, prefix+Width, session); !ok { + result.Width = defWidth + } + if result.Color, ok = colorProperty(border, prefix+ColorProperty, session); !ok { + result.Color = defColor + } + return result + } + + return ViewBorders{ + Top: getBorder("top-"), + Left: getBorder("left-"), + Right: getBorder("right-"), + Bottom: getBorder("bottom-"), + } +} + +func (border *borderProperty) cssStyle(builder cssBuilder, session Session) { + borders := border.ViewBorders(session) + values := enumProperties[BorderStyle].cssValues + 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]) + } else { + builder.addValues(BorderStyle, " ", values[borders.Top.Style], + values[borders.Right.Style], values[borders.Bottom.Style], values[borders.Left.Style]) + } +} + +func (border *borderProperty) cssWidth(builder cssBuilder, session Session) { + borders := border.ViewBorders(session) + if borders.Top.Width == borders.Right.Width && + borders.Top.Width == borders.Left.Width && + borders.Top.Width == borders.Bottom.Width { + if borders.Top.Width.Type != Auto { + builder.add("border-width", borders.Top.Width.cssString("0")) + } + } else { + builder.addValues("border-width", " ", borders.Top.Width.cssString("0"), + borders.Right.Width.cssString("0"), borders.Bottom.Width.cssString("0"), borders.Left.Width.cssString("0")) + } +} + +func (border *borderProperty) cssColor(builder cssBuilder, session Session) { + borders := border.ViewBorders(session) + if borders.Top.Color == borders.Right.Color && + borders.Top.Color == borders.Left.Color && + borders.Top.Color == borders.Bottom.Color { + if borders.Top.Color != 0 { + builder.add("border-color", borders.Top.Color.cssString()) + } + } else { + builder.addValues("border-color", " ", borders.Top.Color.cssString(), + borders.Right.Color.cssString(), borders.Bottom.Color.cssString(), borders.Left.Color.cssString()) + } +} + +func (border *borderProperty) cssStyleValue(session Session) string { + var builder cssValueBuilder + border.cssStyle(&builder, session) + return builder.finish() +} + +func (border *borderProperty) cssWidthValue(session Session) string { + var builder cssValueBuilder + border.cssWidth(&builder, session) + return builder.finish() +} + +func (border *borderProperty) cssColorValue(session Session) string { + var builder cssValueBuilder + border.cssColor(&builder, session) + return builder.finish() +} + +// ViewBorder describes parameters of a view border +type ViewBorder struct { + Style int + Color Color + Width SizeUnit +} + +// ViewBorders describes the top, right, bottom, and left border of a view +type ViewBorders struct { + Top, Right, Bottom, Left ViewBorder +} + +// AllTheSame returns true if all borders are the same +func (border *ViewBorders) AllTheSame() bool { + return border.Top.Style == border.Right.Style && + border.Top.Style == border.Left.Style && + border.Top.Style == border.Bottom.Style && + border.Top.Color == border.Right.Color && + border.Top.Color == border.Left.Color && + border.Top.Color == border.Bottom.Color && + border.Top.Width.Equal(border.Right.Width) && + border.Top.Width.Equal(border.Left.Width) && + border.Top.Width.Equal(border.Bottom.Width) +} + +func getBorder(style Properties, tag string) BorderProperty { + if value := style.Get(tag); value != nil { + if border, ok := value.(BorderProperty); ok { + return border + } + } + return nil +} diff --git a/bounds.go b/bounds.go new file mode 100644 index 0000000..f4630f6 --- /dev/null +++ b/bounds.go @@ -0,0 +1,405 @@ +package rui + +import ( + "fmt" + "strings" +) + +// BorderProperty is the interface of a bounds property data +type BoundsProperty interface { + Properties + ruiStringer + fmt.Stringer + Bounds(session Session) Bounds +} + +type boundsPropertyData struct { + propertyList +} + +// NewBoundsProperty creates the new BoundsProperty object +func NewBoundsProperty(params Params) BoundsProperty { + bounds := new(boundsPropertyData) + bounds.properties = map[string]interface{}{} + if params != nil { + for _, tag := range []string{Top, Right, Bottom, Left} { + if value, ok := params[tag]; ok { + bounds.Set(tag, value) + } + } + } + return bounds +} + +func (bounds *boundsPropertyData) normalizeTag(tag string) string { + tag = strings.ToLower(tag) + switch tag { + case MarginTop, PaddingTop, CellPaddingTop, + "top-margin", "top-padding", "top-cell-padding": + tag = Top + + case MarginRight, PaddingRight, CellPaddingRight, + "right-margin", "right-padding", "right-cell-padding": + tag = Right + + case MarginBottom, PaddingBottom, CellPaddingBottom, + "bottom-margin", "bottom-padding", "bottom-cell-padding": + tag = Bottom + + case MarginLeft, PaddingLeft, CellPaddingLeft, + "left-margin", "left-padding", "left-cell-padding": + tag = Left + } + + return tag +} + +func (bounds *boundsPropertyData) ruiString(writer ruiWriter) { + writer.startObject("_") + + for _, tag := range []string{Top, Right, Bottom, Left} { + if value, ok := bounds.properties[tag]; ok { + writer.writeProperty(Style, value) + } + } + + writer.endObject() +} + +func (bounds *boundsPropertyData) String() string { + writer := newRUIWriter() + bounds.ruiString(writer) + return writer.finish() +} + +func (bounds *boundsPropertyData) Remove(tag string) { + bounds.propertyList.Remove(bounds.normalizeTag(tag)) +} + +func (bounds *boundsPropertyData) Set(tag string, value interface{}) 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) interface{} { + 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) + bottom, _ := sizeProperty(bounds, Bottom, session) + left, _ := sizeProperty(bounds, Left, session) + return Bounds{Top: top, Right: right, Bottom: bottom, Left: left} +} + +// Bounds describe bounds of rectangle. +type Bounds struct { + Top, Right, Bottom, Left SizeUnit +} + +// DefaultBounds return bounds with Top, Right, Bottom and Left fields set to Auto +func DefaultBounds() Bounds { + return Bounds{ + Top: SizeUnit{Type: Auto, Value: 0}, + Right: SizeUnit{Type: Auto, Value: 0}, + Bottom: SizeUnit{Type: Auto, Value: 0}, + Left: SizeUnit{Type: Auto, Value: 0}, + } +} + +// SetAll set the Top, Right, Bottom and Left field to the equal value +func (bounds *Bounds) SetAll(value SizeUnit) { + bounds.Top = value + bounds.Right = value + bounds.Bottom = value + bounds.Left = value +} + +func (bounds *Bounds) parse(value string, session Session) bool { + var ok bool + if value, ok = session.resolveConstants(value); !ok { + return false + } + + values := strings.Split(value, ",") + switch len(values) { + case 1: + if bounds.Left, ok = StringToSizeUnit(values[0]); !ok { + return false + } + bounds.Right.Type = bounds.Left.Type + bounds.Right.Value = bounds.Left.Value + bounds.Top.Type = bounds.Left.Type + bounds.Top.Value = bounds.Left.Value + bounds.Bottom.Type = bounds.Left.Type + bounds.Bottom.Value = bounds.Left.Value + return true + + case 5: + if values[4] != "" { + ErrorLog("invalid Bounds value '" + value + "' (needs 1 or 4 elements separeted by comma)") + return false + } + fallthrough + + case 4: + if bounds.Top, ok = StringToSizeUnit(values[0]); ok { + if bounds.Right, ok = StringToSizeUnit(values[1]); ok { + if bounds.Bottom, ok = StringToSizeUnit(values[2]); ok { + if bounds.Left, ok = StringToSizeUnit(values[3]); ok { + return true + } + } + } + } + return false + } + + ErrorLog("invalid Bounds value '" + value + "' (needs 1 or 4 elements separeted by comma)") + return false +} + +func (bounds *Bounds) setFromProperties(tag, topTag, rightTag, bottomTag, leftTag string, properties Properties, session Session) { + bounds.Top = AutoSize() + if size, ok := sizeProperty(properties, tag, session); ok { + bounds.Top = size + } + bounds.Right = bounds.Top + bounds.Bottom = bounds.Top + bounds.Left = bounds.Top + + if size, ok := sizeProperty(properties, topTag, session); ok { + bounds.Top = size + } + if size, ok := sizeProperty(properties, rightTag, session); ok { + bounds.Right = size + } + if size, ok := sizeProperty(properties, bottomTag, session); ok { + bounds.Bottom = size + } + if size, ok := sizeProperty(properties, leftTag, session); ok { + bounds.Left = size + } +} + +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 && + bounds.Left.Type == bounds.Bottom.Type { + return bounds.Left.Type == Auto || + (bounds.Left.Value == bounds.Top.Value && + bounds.Left.Value == bounds.Right.Value && + bounds.Left.Value == bounds.Bottom.Value) + } + + 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() { + return bounds.Top.String() + } + return bounds.Top.String() + "," + bounds.Right.String() + "," + + bounds.Bottom.String() + "," + bounds.Left.String() +} + +func (bounds *Bounds) cssValue(tag string, builder cssBuilder) { + if bounds.allFieldsEqual() { + builder.add(tag, bounds.Top.cssString("0")) + } else { + builder.addValues(tag, " ", bounds.Top.cssString("0"), bounds.Right.cssString("0"), + bounds.Bottom.cssString("0"), bounds.Left.cssString("0")) + } +} + +func (bounds *Bounds) cssString() string { + var builder cssValueBuilder + bounds.cssValue("", &builder) + return builder.finish() +} + +func (properties *propertyList) setBounds(tag string, value interface{}) bool { + if !properties.setSimpleProperty(tag, value) { + switch value := value.(type) { + case string: + if strings.Contains(value, ",") { + values := split4Values(value) + count := len(values) + switch count { + case 1: + value = values[0] + + case 4: + bounds := NewBoundsProperty(nil) + for i, tag := range []string{Top, Right, Bottom, Left} { + if !bounds.Set(tag, values[i]) { + notCompatibleType(tag, value) + return false + } + } + properties.properties[tag] = bounds + return true + + default: + notCompatibleType(tag, value) + return false + } + } + return properties.setSizeProperty(tag, value) + + case SizeUnit: + properties.properties[tag] = value + + case Bounds: + properties.properties[tag] = value + + case BoundsProperty: + properties.properties[tag] = value + + case DataObject: + bounds := NewBoundsProperty(nil) + for _, tag := range []string{Top, Right, Bottom, Left} { + if text, ok := value.PropertyValue(tag); ok { + if !bounds.Set(tag, text) { + notCompatibleType(tag, value) + return false + } + } + } + properties.properties[tag] = bounds + + default: + notCompatibleType(tag, value) + return false + } + } + + return true +} + +func (properties *propertyList) boundsProperty(tag string) BoundsProperty { + if value, ok := properties.properties[tag]; ok { + switch value := value.(type) { + case string: + bounds := NewBoundsProperty(nil) + for _, t := range []string{Top, Right, Bottom, Left} { + bounds.Set(t, value) + } + return bounds + + case SizeUnit: + bounds := NewBoundsProperty(nil) + for _, t := range []string{Top, Right, Bottom, Left} { + bounds.Set(t, value) + } + return bounds + + case BoundsProperty: + return value + + case Bounds: + return NewBoundsProperty(Params{ + Top: value.Top, + Right: value.Right, + Bottom: value.Bottom, + Left: value.Left}) + } + } + + return NewBoundsProperty(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 interface{}) 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) { + if value := properties.Get(tag); value != nil { + switch value := value.(type) { + case string: + if text, ok := session.resolveConstants(value); ok { + if size, ok := StringToSizeUnit(text); ok { + return Bounds{Left: size, Top: size, Right: size, Bottom: size}, true + } + } + + case SizeUnit: + return Bounds{Left: value, Top: value, Right: value, Bottom: value}, true + + case Bounds: + return value, true + + case BoundsProperty: + return value.Bounds(session), true + + default: + notCompatibleType(tag, value) + } + } + + return DefaultBounds(), false +} diff --git a/bounds_test.go b/bounds_test.go new file mode 100644 index 0000000..19ffcef --- /dev/null +++ b/bounds_test.go @@ -0,0 +1,99 @@ +package rui + +/* +import ( + "bytes" + "strconv" + "testing" +) + +func TestBoundsSet(t *testing.T) { + + session := createTestSession(t) + + obj := NewDataObject("Test") + obj.SetPropertyValue("x", "10") + obj.SetPropertyValue("padding", "8px") + obj.SetPropertyValue("margins", "16mm,10pt,12in,auto") + obj.SetPropertyValue("fail1", "x16mm") + obj.SetPropertyValue("fail2", "16mm,10pt,12in") + obj.SetPropertyValue("fail3", "x16mm,10pt,12in,auto") + obj.SetPropertyValue("fail4", "16mm,x10pt,12in,auto") + obj.SetPropertyValue("fail5", "16mm,10pt,x12in,auto") + obj.SetPropertyValue("fail6", "16mm,10pt,12in,autoo") + + const failAttrsCount = 6 + + var bounds Bounds + if bounds.setProperty(obj, "padding", session) { + if bounds.Left.Type != SizeInPixel || bounds.Left.Value != 8 || + bounds.Left != bounds.Right || + bounds.Left != bounds.Top || + bounds.Left != bounds.Bottom { + t.Errorf("set padding error, result %v", bounds) + } + } + + if bounds.setProperty(obj, "margins", session) { + if bounds.Top.Type != SizeInMM || bounds.Top.Value != 16 || + bounds.Right.Type != SizeInPt || bounds.Right.Value != 10 || + bounds.Bottom.Type != SizeInInch || bounds.Bottom.Value != 12 || + bounds.Left.Type != Auto { + t.Errorf("set margins error, result %v", bounds) + } + } + + ignoreTestLog = true + for i := 1; i <= failAttrsCount; i++ { + if bounds.setProperty(obj, "fail"+strconv.Itoa(i), session) { + t.Errorf("set 'fail' error, result %v", bounds) + } + } + ignoreTestLog = false + + obj.SetPropertyValue("padding-left", "10mm") + obj.SetPropertyValue("padding-top", "4pt") + obj.SetPropertyValue("padding-right", "12in") + obj.SetPropertyValue("padding-bottom", "8px") + + if bounds.setProperty(obj, "padding", session) { + if bounds.Left.Type != SizeInMM || bounds.Left.Value != 10 || + bounds.Top.Type != SizeInPt || bounds.Top.Value != 4 || + bounds.Right.Type != SizeInInch || bounds.Right.Value != 12 || + bounds.Bottom.Type != SizeInPixel || bounds.Bottom.Value != 8 { + t.Errorf("set margins error, result %v", bounds) + } + } + + for _, tag := range []string{"padding-left", "padding-top", "padding-right", "padding-bottom"} { + if old, ok := obj.PropertyValue(tag); ok { + ignoreTestLog = true + obj.SetPropertyValue(tag, "x") + if bounds.setProperty(obj, "padding", session) { + t.Errorf("set \"%s\" value \"x\": result %v ", tag, bounds) + } + ignoreTestLog = false + obj.SetPropertyValue(tag, old) + } + } +} + +func TestBoundsWriteData(t *testing.T) { + + _ = createTestSession(t) + + bounds := Bounds{ + SizeUnit{SizeInPixel, 8}, + SizeUnit{SizeInInch, 10}, + SizeUnit{SizeInPt, 12}, + SizeUnit{Auto, 0}, + } + + buffer := new(bytes.Buffer) + bounds.writeData(buffer) + str := buffer.String() + if str != `"8px,10in,12pt,auto"` { + t.Errorf("result `%s`, expected `\"8px,10dip,12pt,auto\"`", str) + } +} +*/ diff --git a/button.go b/button.go new file mode 100644 index 0000000..6ecb9c8 --- /dev/null +++ b/button.go @@ -0,0 +1,36 @@ +package rui + +// Button - button view +type Button interface { + CustomView +} + +type buttonData struct { + CustomViewData +} + +// NewButton create new Button object and return it +func NewButton(session Session, params Params) Button { + button := new(buttonData) + InitCustomView(button, "Button", session, params) + return button +} + +func newButton(session Session) View { + return NewButton(session, nil) +} + +func (button *buttonData) CreateSuperView(session Session) View { + return NewListLayout(session, Params{ + Semantics: ButtonSemantics, + Style: "ruiButton", + StyleDisabled: "ruiDisabledButton", + HorizontalAlign: CenterAlign, + VerticalAlign: CenterAlign, + Orientation: StartToEndOrientation, + }) +} + +func (button *buttonData) Focusable() bool { + return true +} diff --git a/canvas.go b/canvas.go new file mode 100644 index 0000000..a9b9e95 --- /dev/null +++ b/canvas.go @@ -0,0 +1,1011 @@ +package rui + +import ( + "fmt" + "strconv" + "strings" +) + +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 + // 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 + // BevelJoin - Fills an additional triangular area between the common endpoint + // of connected segments, and the separate outside rectangular corners of each segment. + BevelJoin = 2 + + // ButtCap - the ends of lines are squared off at the endpoints. Default value. + ButtCap = 0 + // RoundCap - the ends of lines are rounded. + RoundCap = 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 + + // 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.) + IdeographicBaseline = 5 + + // 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 +) + +// GradientPoint defined by an offset and a color, to a linear or radial gradient +type GradientPoint struct { + // Offset - a number between 0 and 1, inclusive, representing the position of the color stop + Offset float64 + // Color - the color of the stop + Color Color +} + +// FontParams defined optionally font properties +type FontParams struct { + // Italic - if true then a font is italic + Italic bool + // SmallCaps - if true then a font uses small-caps glyphs + SmallCaps bool + // Weight - a font weight. Valid values: 0...9, there + // 0 - a weight does not specify; + // 1 - a minimal weight; + // 4 - a normal weight; + // 7 - a bold weight; + // 9 - a maximal weight. + Weight int + // LineHeight - the height (relative to the font size of the element itself) of a line box. + LineHeight SizeUnit +} + +// Canvas is a drawing interface +type Canvas interface { + // View return the view for the drawing + View() CanvasView + // Width returns the width in pixels of the canvas area + Width() float64 + // Height returns the height in pixels of the canvas area + Height() float64 + + // Save saves the entire state of the canvas by pushing the current state onto a stack. + Save() + // Restore restores the most recently saved canvas state by popping the top entry + // in the drawing state stack. If there is no saved state, this method does nothing. + Restore() + + // ClipPath turns the rectangle into the current clipping region. It replaces any previous clipping region. + ClipRect(x, y, width, height float64) + // ClipPath turns the path into the current clipping region. It replaces any previous clipping region. + 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 + // 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 + // 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. + SetTranslation(x, y float64) + + // SetRotation adds a rotation to the transformation matrix. + // angle - the rotation angle, clockwise in radians + SetRotation(angle float64) + + // 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). + SetTransformation(xScale, yScale, xSkew, ySkew, dx, dy float64) + + // ResetTransformation resets the current transform to the identity matrix + ResetTransformation() + + // SetSolidColorFillStyle sets the color to use inside shapes + SetSolidColorFillStyle(color Color) + + // SetSolidColorStrokeStyle sets color to use for the strokes (outlines) around shapes + 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 + 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 + 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 + 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 + SetRadialGradientStrokeStyle(x0, y0, r0 float64, color0 Color, x1, y1, r1 float64, color1 Color, stopPoints []GradientPoint) + + // SetImageFillStyle set the image as the filling pattern. + // repeate - 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. + SetLineWidth(width float64) + + // 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 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) + + // 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. + // offset - the line dash offset + SetLineDash(dash []float64, offset float64) + + // SetFont sets the current text style to use when drawing text + SetFont(name string, size SizeUnit) + // SetFontWithParams sets the current text style to use when drawing text + SetFontWithParams(name string, size SizeUnit, params FontParams) + + // TextWidth calculates the width of the text drawn by a given font + TextWidth(text string, fontName string, fontSize SizeUnit) float64 + + // SetTextBaseline sets the current text baseline used when drawing text. Valid values: + // AlphabeticBaseline (0), TopBaseline (1), MiddleBaseline (2), BottomBaseline (3), + // HangingBaseline (4), and IdeographicBaseline (5). All other values are ignored. + SetTextBaseline(baseline int) + + // SetTextAlign sets the current text alignment used when drawing text. Valid values: + // LeftAlign (0), RightAlign (1), CenterAlign (2), StartAlign (3), and EndAlign(4). All other values are ignored. + 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. + 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. + 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) + + // 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) + + // DrawLine draws a line according to the current strokeStyle and other context settings + DrawLine(x0, y0, x1, y1 float64) + + // 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 frament (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) + + finishDraw() string +} + +type canvasData struct { + view CanvasView + script strings.Builder +} + +func newCanvas(view CanvasView) Canvas { + canvas := new(canvasData) + canvas.view = view + canvas.script.Grow(4096) + canvas.script.WriteString(`const canvas = document.getElementById('`) + canvas.script.WriteString(view.htmlID()) + canvas.script.WriteString(`'); +const ctx = canvas.getContext('2d'); +const dpr = window.devicePixelRatio || 1; +var gradient; +var path; +var img; +ctx.canvas.width = dpr * canvas.clientWidth; +ctx.canvas.height = dpr * canvas.clientHeight; +ctx.scale(dpr, dpr);`) + /* + canvas.script.WriteString(strconv.FormatFloat(view.canvasWidth(), 'g', -1, 64)) + canvas.script.WriteString(`; + ctx.canvas.height = dpr * `) + canvas.script.WriteString(strconv.FormatFloat(view.canvasHeight(), 'g', -1, 64)) + canvas.script.WriteString(";\nctx.scale(dpr, dpr);") + */ + return canvas +} + +func (canvas *canvasData) finishDraw() string { + canvas.script.WriteString("\n") + return canvas.script.String() +} + +func (canvas *canvasData) View() CanvasView { + return canvas.view +} + +func (canvas *canvasData) Width() float64 { + if canvas.view != nil { + return canvas.view.Frame().Width + } + return 0 +} + +func (canvas *canvasData) Height() float64 { + if canvas.view != nil { + return canvas.view.Frame().Height + } + return 0 +} + +func (canvas *canvasData) Save() { + canvas.script.WriteString("\nctx.save();") +} + +func (canvas *canvasData) Restore() { + canvas.script.WriteString("\nctx.restore();") +} + +func (canvas *canvasData) ClipRect(x, y, width, height float64) { + canvas.script.WriteString("\nctx.beginPath();\nctx.rect(") + canvas.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(width, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(height, 'g', -1, 64)) + canvas.script.WriteString(");\nctx.clip();") +} + +func (canvas *canvasData) ClipPath(path Path) { + canvas.script.WriteString(path.scriptText()) + canvas.script.WriteString("\nctx.clip();") +} + +func (canvas *canvasData) SetScale(x, y float64) { + canvas.script.WriteString("\nctx.scale(") + canvas.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64)) + canvas.script.WriteString(");") +} + +func (canvas *canvasData) SetTranslation(x, y float64) { + canvas.script.WriteString("\nctx.translate(") + canvas.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64)) + canvas.script.WriteString(");") +} + +func (canvas *canvasData) SetRotation(angle float64) { + canvas.script.WriteString("\nctx.rotate(") + canvas.script.WriteString(strconv.FormatFloat(angle, 'g', -1, 64)) + canvas.script.WriteString(");") +} + +func (canvas *canvasData) SetTransformation(xScale, yScale, xSkew, ySkew, dx, dy float64) { + canvas.script.WriteString("\nctx.transform(") + canvas.script.WriteString(strconv.FormatFloat(xScale, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(ySkew, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(xSkew, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(yScale, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(dx, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(dy, 'g', -1, 64)) + canvas.script.WriteString(");") +} + +func (canvas *canvasData) ResetTransformation() { + canvas.script.WriteString("\nctx.resetTransform();\nctx.scale(dpr, dpr);") +} + +func (canvas *canvasData) SetSolidColorFillStyle(color Color) { + canvas.script.WriteString("\nctx.fillStyle = \"") + canvas.script.WriteString(color.cssString()) + canvas.script.WriteString(`";`) +} + +func (canvas *canvasData) SetSolidColorStrokeStyle(color Color) { + canvas.script.WriteString("\nctx.strokeStyle = \"") + canvas.script.WriteString(color.cssString()) + canvas.script.WriteString(`";`) +} + +func (canvas *canvasData) setLinearGradient(x0, y0 float64, color0 Color, x1, y1 float64, color1 Color, stopPoints []GradientPoint) { + canvas.script.WriteString("\ngradient = ctx.createLinearGradient(") + canvas.script.WriteString(strconv.FormatFloat(x0, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(y0, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(x1, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(y1, 'g', -1, 64)) + canvas.script.WriteString(");\ngradient.addColorStop(0, '") + canvas.script.WriteString(color0.cssString()) + canvas.script.WriteString("');") + + for _, point := range stopPoints { + if point.Offset >= 0 && point.Offset <= 1 { + canvas.script.WriteString("\ngradient.addColorStop(") + canvas.script.WriteString(strconv.FormatFloat(point.Offset, 'g', -1, 64)) + canvas.script.WriteString(", '") + canvas.script.WriteString(point.Color.cssString()) + canvas.script.WriteString("');") + } + } + + canvas.script.WriteString("\ngradient.addColorStop(1, '") + canvas.script.WriteString(color1.cssString()) + canvas.script.WriteString("');") +} + +func (canvas *canvasData) SetLinearGradientFillStyle(x0, y0 float64, color0 Color, x1, y1 float64, color1 Color, stopPoints []GradientPoint) { + canvas.setLinearGradient(x0, y0, color0, x1, y1, color1, stopPoints) + canvas.script.WriteString("\nctx.fillStyle = gradient;") +} + +func (canvas *canvasData) SetLinearGradientStrokeStyle(x0, y0 float64, color0 Color, x1, y1 float64, color1 Color, stopPoints []GradientPoint) { + canvas.setLinearGradient(x0, y0, color0, x1, y1, color1, stopPoints) + canvas.script.WriteString("\nctx.strokeStyle = gradient;") +} + +func (canvas *canvasData) setRadialGradient(x0, y0, r0 float64, color0 Color, x1, y1, r1 float64, color1 Color, stopPoints []GradientPoint) { + canvas.script.WriteString("\ngradient = ctx.createRadialGradient(") + canvas.script.WriteString(strconv.FormatFloat(x0, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(y0, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(r0, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(x1, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(y1, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(r1, 'g', -1, 64)) + canvas.script.WriteString(");\ngradient.addColorStop(0, '") + canvas.script.WriteString(color0.cssString()) + canvas.script.WriteString("');") + + for _, point := range stopPoints { + if point.Offset >= 0 && point.Offset <= 1 { + canvas.script.WriteString("\ngradient.addColorStop(") + canvas.script.WriteString(strconv.FormatFloat(point.Offset, 'g', -1, 64)) + canvas.script.WriteString(", '") + canvas.script.WriteString(point.Color.cssString()) + canvas.script.WriteString("');") + } + } + + canvas.script.WriteString("\ngradient.addColorStop(1, '") + canvas.script.WriteString(color1.cssString()) + canvas.script.WriteString("');") +} + +func (canvas *canvasData) SetRadialGradientFillStyle(x0, y0, r0 float64, color0 Color, x1, y1, r1 float64, color1 Color, stopPoints []GradientPoint) { + canvas.setRadialGradient(x0, y0, r0, color0, x1, y1, r1, color1, stopPoints) + canvas.script.WriteString("\nctx.fillStyle = gradient;") +} + +func (canvas *canvasData) SetRadialGradientStrokeStyle(x0, y0, r0 float64, color0 Color, x1, y1, r1 float64, color1 Color, stopPoints []GradientPoint) { + canvas.setRadialGradient(x0, y0, r0, color0, x1, y1, r1, color1, stopPoints) + canvas.script.WriteString("\nctx.strokeStyle = gradient;") +} + +func (canvas *canvasData) SetImageFillStyle(image Image, repeat int) { + if image == nil || image.LoadingStatus() != ImageReady { + return + } + + var repeatText string + switch repeat { + case NoRepeat: + repeatText = "no-repeat" + + case RepeatXY: + repeatText = "repeat" + + case RepeatX: + repeatText = "repeat-x" + + case RepeatY: + repeatText = "repeat-y" + + default: + return + } + + canvas.script.WriteString("\nimg = images.get('") + canvas.script.WriteString(image.URL()) + canvas.script.WriteString("');\nif (img) {\nctx.fillStyle = ctx.createPattern(img,'") + canvas.script.WriteString(repeatText) + canvas.script.WriteString("');\n}") +} + +func (canvas *canvasData) SetLineWidth(width float64) { + if width > 0 { + canvas.script.WriteString("\nctx.lineWidth = '") + canvas.script.WriteString(strconv.FormatFloat(width, 'g', -1, 64)) + canvas.script.WriteString("';") + } +} + +func (canvas *canvasData) SetLineJoin(join int) { + switch join { + case MiterJoin: + canvas.script.WriteString("\nctx.lineJoin = 'miter';") + + case RoundJoin: + canvas.script.WriteString("\nctx.lineJoin = 'round';") + + case BevelJoin: + canvas.script.WriteString("\nctx.lineJoin = 'bevel';") + } +} + +func (canvas *canvasData) SetLineCap(cap int) { + switch cap { + case ButtCap: + canvas.script.WriteString("\nctx.lineCap = 'butt';") + + case RoundCap: + canvas.script.WriteString("\nctx.lineCap = 'round';") + + case SquareCap: + canvas.script.WriteString("\nctx.lineCap = 'square';") + } +} + +func (canvas *canvasData) SetLineDash(dash []float64, offset float64) { + canvas.script.WriteString("\nctx.setLineDash([") + for i, d := range dash { + if i > 0 { + canvas.script.WriteString(",") + } + canvas.script.WriteString(strconv.FormatFloat(d, 'g', -1, 64)) + } + + canvas.script.WriteString("]);") + if offset >= 0 { + canvas.script.WriteString("\nctx.lineDashOffset = '") + canvas.script.WriteString(strconv.FormatFloat(offset, 'g', -1, 64)) + canvas.script.WriteString("';") + } +} + +func (canvas *canvasData) writeFont(name string, script *strings.Builder) { + names := strings.Split(name, ",") + lead := " " + for _, font := range names { + font = strings.Trim(font, " \n\"'") + script.WriteString(lead) + lead = "," + if strings.Contains(font, " ") { + script.WriteRune('"') + script.WriteString(font) + script.WriteRune('"') + } else { + script.WriteString(font) + } + + } + script.WriteString("';") +} + +func (canvas *canvasData) SetFont(name string, size SizeUnit) { + canvas.script.WriteString("\nctx.font = '") + canvas.script.WriteString(size.cssString("1em")) + canvas.writeFont(name, &canvas.script) +} + +func (canvas *canvasData) setFontWithParams(name string, size SizeUnit, params FontParams, script *strings.Builder) { + script.WriteString("\nctx.font = '") + if params.Italic { + script.WriteString("italic ") + } + if params.SmallCaps { + script.WriteString("small-caps ") + } + if params.Weight > 0 && params.Weight <= 9 { + switch params.Weight { + case 4: + script.WriteString("normal ") + case 7: + script.WriteString("bold ") + default: + script.WriteString(strconv.Itoa(params.Weight * 100)) + script.WriteRune(' ') + } + } + + script.WriteString(size.cssString("1em")) + switch params.LineHeight.Type { + case Auto: + + case SizeInPercent: + if params.LineHeight.Value != 100 { + script.WriteString("/") + script.WriteString(strconv.FormatFloat(params.LineHeight.Value/100, 'g', -1, 64)) + } + + case SizeInFraction: + if params.LineHeight.Value != 1 { + script.WriteString("/") + script.WriteString(strconv.FormatFloat(params.LineHeight.Value, 'g', -1, 64)) + } + + default: + script.WriteString("/") + script.WriteString(params.LineHeight.cssString("")) + } + + canvas.writeFont(name, script) +} + +func (canvas *canvasData) SetFontWithParams(name string, size SizeUnit, params FontParams) { + canvas.setFontWithParams(name, size, params, &canvas.script) +} + +func (canvas *canvasData) TextWidth(text string, fontName string, fontSize SizeUnit) float64 { + script := allocStringBuilder() + defer freeStringBuilder(script) + + script.WriteString(`const canvas = document.getElementById('`) + script.WriteString(canvas.View().htmlID()) + script.WriteString(`'); +const ctx = canvas.getContext('2d'); +const dpr = window.devicePixelRatio || 1; +var gradient; +var path; +ctx.save() +ctx.scale(dpr, dpr);`) + + canvas.setFontWithParams(fontName, fontSize, FontParams{}, script) + script.WriteString("\nvar w = ctx.measureText('") + canvas.writeStringArgs(text, script) + script.WriteString(`').width; +sendMessage('answer{width=' + w + ', answerID=' + answerID + '}')`) + + result := canvas.View().Session().runGetterScript(script.String()) + switch result.Tag() { + case "answer": + if value, ok := result.PropertyValue("width"); ok { + w, err := strconv.ParseFloat(value, 32) + if err == nil { + return w + } + ErrorLog(err.Error()) + } + + case "error": + if text, ok := result.PropertyValue("errorText"); ok { + ErrorLog(text) + } else { + ErrorLog("error") + } + + default: + ErrorLog("Unknown answer: " + result.Tag()) + } + + return 0 +} + +func (canvas *canvasData) SetTextBaseline(baseline int) { + switch baseline { + case AlphabeticBaseline: + canvas.script.WriteString("\nctx.textBaseline = 'alphabetic';") + case TopBaseline: + canvas.script.WriteString("\nctx.textBaseline = 'top';") + case MiddleBaseline: + canvas.script.WriteString("\nctx.textBaseline = 'middle';") + case BottomBaseline: + canvas.script.WriteString("\nctx.textBaseline = 'bottom';") + case HangingBaseline: + canvas.script.WriteString("\nctx.textBaseline = 'hanging';") + case IdeographicBaseline: + canvas.script.WriteString("\nctx.textBaseline = 'ideographic';") + } +} + +func (canvas *canvasData) SetTextAlign(align int) { + switch align { + case LeftAlign: + canvas.script.WriteString("\nctx.textAlign = 'left';") + case RightAlign: + canvas.script.WriteString("\nctx.textAlign = 'right';") + case CenterAlign: + canvas.script.WriteString("\nctx.textAlign = 'center';") + case StartAlign: + canvas.script.WriteString("\nctx.textAlign = 'start';") + case EndAlign: + canvas.script.WriteString("\nctx.textAlign = 'end';") + } +} + +func (canvas *canvasData) SetShadow(offsetX, offsetY, blur float64, color Color) { + if color.Alpha() > 0 && blur >= 0 { + canvas.script.WriteString("\nctx.shadowColor = '") + canvas.script.WriteString(color.cssString()) + canvas.script.WriteString("';\nctx.shadowOffsetX = ") + canvas.script.WriteString(strconv.FormatFloat(offsetX, 'g', -1, 64)) + canvas.script.WriteString(";\nctx.shadowOffsetY = ") + canvas.script.WriteString(strconv.FormatFloat(offsetY, 'g', -1, 64)) + canvas.script.WriteString(";\nctx.shadowBlur = ") + canvas.script.WriteString(strconv.FormatFloat(blur, 'g', -1, 64)) + canvas.script.WriteString(";") + } +} + +func (canvas *canvasData) ResetShadow() { + canvas.script.WriteString("\nctx.shadowColor = 'rgba(0,0,0,0)';\nctx.shadowOffsetX = 0;\nctx.shadowOffsetY = 0;\nctx.shadowBlur = 0;") +} + +func (canvas *canvasData) writeRectArgs(x, y, width, height float64) { + canvas.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(width, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(height, 'g', -1, 64)) +} + +func (canvas *canvasData) ClearRect(x, y, width, height float64) { + canvas.script.WriteString("\nctx.clearRect(") + canvas.writeRectArgs(x, y, width, height) + canvas.script.WriteString(");") +} + +func (canvas *canvasData) FillRect(x, y, width, height float64) { + canvas.script.WriteString("\nctx.fillRect(") + canvas.writeRectArgs(x, y, width, height) + canvas.script.WriteString(");") +} + +func (canvas *canvasData) StrokeRect(x, y, width, height float64) { + canvas.script.WriteString("\nctx.strokeRect(") + canvas.writeRectArgs(x, y, width, height) + canvas.script.WriteString(");") +} + +func (canvas *canvasData) FillAndStrokeRect(x, y, width, height float64) { + canvas.FillRect(x, y, width, height) + canvas.StrokeRect(x, y, width, height) +} + +func (canvas *canvasData) writeRoundedRect(x, y, width, height, r float64) { + left := strconv.FormatFloat(x, 'g', -1, 64) + top := strconv.FormatFloat(y, 'g', -1, 64) + right := strconv.FormatFloat(x+width, 'g', -1, 64) + bottom := strconv.FormatFloat(y+height, 'g', -1, 64) + leftR := strconv.FormatFloat(x+r, 'g', -1, 64) + topR := strconv.FormatFloat(y+r, 'g', -1, 64) + rightR := strconv.FormatFloat(x+width-r, 'g', -1, 64) + bottomR := strconv.FormatFloat(y+height-r, 'g', -1, 64) + radius := strconv.FormatFloat(r, 'g', -1, 64) + canvas.script.WriteString("\nctx.beginPath();\nctx.moveTo(") + canvas.script.WriteString(left) + canvas.script.WriteRune(',') + canvas.script.WriteString(topR) + canvas.script.WriteString(");\nctx.arc(") + canvas.script.WriteString(leftR) + canvas.script.WriteRune(',') + canvas.script.WriteString(topR) + canvas.script.WriteRune(',') + canvas.script.WriteString(radius) + canvas.script.WriteString(",Math.PI,Math.PI*3/2);\nctx.lineTo(") + canvas.script.WriteString(rightR) + canvas.script.WriteRune(',') + canvas.script.WriteString(top) + canvas.script.WriteString(");\nctx.arc(") + canvas.script.WriteString(rightR) + canvas.script.WriteRune(',') + canvas.script.WriteString(topR) + canvas.script.WriteRune(',') + canvas.script.WriteString(radius) + canvas.script.WriteString(",Math.PI*3/2,Math.PI*2);\nctx.lineTo(") + canvas.script.WriteString(right) + canvas.script.WriteRune(',') + canvas.script.WriteString(bottomR) + canvas.script.WriteString(");\nctx.arc(") + canvas.script.WriteString(rightR) + canvas.script.WriteRune(',') + canvas.script.WriteString(bottomR) + canvas.script.WriteRune(',') + canvas.script.WriteString(radius) + canvas.script.WriteString(",0,Math.PI/2);\nctx.lineTo(") + canvas.script.WriteString(leftR) + canvas.script.WriteRune(',') + canvas.script.WriteString(bottom) + canvas.script.WriteString(");\nctx.arc(") + canvas.script.WriteString(leftR) + canvas.script.WriteRune(',') + canvas.script.WriteString(bottomR) + canvas.script.WriteRune(',') + canvas.script.WriteString(radius) + canvas.script.WriteString(",Math.PI/2,Math.PI);\nctx.closePath();") +} + +func (canvas *canvasData) FillRoundedRect(x, y, width, height, r float64) { + canvas.writeRoundedRect(x, y, width, height, r) + canvas.script.WriteString("\nctx.fill();") +} + +func (canvas *canvasData) StrokeRoundedRect(x, y, width, height, r float64) { + canvas.writeRoundedRect(x, y, width, height, r) + canvas.script.WriteString("\nctx.stroke();") +} + +func (canvas *canvasData) FillAndStrokeRoundedRect(x, y, width, height, r float64) { + canvas.writeRoundedRect(x, y, width, height, r) + canvas.script.WriteString("\nctx.fill();\nctx.stroke();") +} + +func (canvas *canvasData) writeEllipse(x, y, radiusX, radiusY, rotation float64) { + yText := strconv.FormatFloat(y, 'g', -1, 64) + canvas.script.WriteString("\nctx.beginPath();\nctx.moveTo(") + canvas.script.WriteString(strconv.FormatFloat(x+radiusX, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(yText) + canvas.script.WriteString(");\nctx.ellipse(") + canvas.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(yText) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(radiusX, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(radiusY, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(rotation, 'g', -1, 64)) + canvas.script.WriteString(",0,Math.PI*2);") +} + +func (canvas *canvasData) FillEllipse(x, y, radiusX, radiusY, rotation float64) { + if radiusX >= 0 && radiusY >= 0 { + canvas.writeEllipse(x, y, radiusX, radiusY, rotation) + canvas.script.WriteString("\nctx.fill();") + } +} + +func (canvas *canvasData) StrokeEllipse(x, y, radiusX, radiusY, rotation float64) { + if radiusX >= 0 && radiusY >= 0 { + canvas.writeEllipse(x, y, radiusX, radiusY, rotation) + canvas.script.WriteString("\nctx.stroke();") + } +} + +func (canvas *canvasData) FillAndStrokeEllipse(x, y, radiusX, radiusY, rotation float64) { + if radiusX >= 0 && radiusY >= 0 { + canvas.writeEllipse(x, y, radiusX, radiusY, rotation) + canvas.script.WriteString("\nctx.fill();\nctx.stroke();") + } +} + +func (canvas *canvasData) writePointArgs(x, y float64) { + canvas.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64)) +} + +func (canvas *canvasData) writeStringArgs(text string, script *strings.Builder) { + //rText := []rune(text) + for _, ch := range text { + switch ch { + case '\t': + script.WriteString(`\t`) + case '\n': + script.WriteString(`\n`) + case '\r': + script.WriteString(`\r`) + case '\\': + script.WriteString(`\\`) + case '"': + script.WriteString(`\"`) + case '\'': + script.WriteString(`\'`) + default: + if ch < ' ' { + script.WriteString(fmt.Sprintf("\\x%02X", int(ch))) + } else { + script.WriteRune(ch) + } + } + } +} + +func (canvas *canvasData) FillText(x, y float64, text string) { + canvas.script.WriteString("\nctx.fillText('") + canvas.writeStringArgs(text, &canvas.script) + canvas.script.WriteString(`',`) + canvas.writePointArgs(x, y) + canvas.script.WriteString(");") +} + +func (canvas *canvasData) StrokeText(x, y float64, text string) { + canvas.script.WriteString("\nctx.strokeText('") + canvas.writeStringArgs(text, &canvas.script) + canvas.script.WriteString(`',`) + canvas.writePointArgs(x, y) + canvas.script.WriteString(");") +} + +func (canvas *canvasData) FillPath(path Path) { + canvas.script.WriteString(path.scriptText()) + canvas.script.WriteString("\nctx.fill();") +} + +func (canvas *canvasData) StrokePath(path Path) { + canvas.script.WriteString(path.scriptText()) + canvas.script.WriteString("\nctx.stroke();") +} + +func (canvas *canvasData) FillAndStrokePath(path Path) { + canvas.script.WriteString(path.scriptText()) + canvas.script.WriteString("\nctx.fill();\nctx.stroke();") +} + +func (canvas *canvasData) DrawLine(x0, y0, x1, y1 float64) { + canvas.script.WriteString("\nctx.beginPath();\nctx.moveTo(") + canvas.script.WriteString(strconv.FormatFloat(x0, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(y0, 'g', -1, 64)) + canvas.script.WriteString(");\nctx.lineTo(") + canvas.script.WriteString(strconv.FormatFloat(x1, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(y1, 'g', -1, 64)) + canvas.script.WriteString(");\nctx.stroke();") +} + +func (canvas *canvasData) DrawImage(x, y float64, image Image) { + if image == nil || image.LoadingStatus() != ImageReady { + return + } + canvas.script.WriteString("\nimg = images.get('") + canvas.script.WriteString(image.URL()) + canvas.script.WriteString("');\nif (img) {\nctx.drawImage(img,") + canvas.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64)) + canvas.script.WriteString(");\n}") +} + +func (canvas *canvasData) DrawImageInRect(x, y, width, height float64, image Image) { + if image == nil || image.LoadingStatus() != ImageReady { + return + } + canvas.script.WriteString("\nimg = images.get('") + canvas.script.WriteString(image.URL()) + canvas.script.WriteString("');\nif (img) {\nctx.drawImage(img,") + canvas.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(width, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(height, 'g', -1, 64)) + canvas.script.WriteString(");\n}") +} + +func (canvas *canvasData) DrawImageFragment(srcX, srcY, srcWidth, srcHeight, dstX, dstY, dstWidth, dstHeight float64, image Image) { + if image == nil || image.LoadingStatus() != ImageReady { + return + } + canvas.script.WriteString("\nimg = images.get('") + canvas.script.WriteString(image.URL()) + canvas.script.WriteString("');\nif (img) {\nctx.drawImage(img,") + canvas.script.WriteString(strconv.FormatFloat(srcX, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(srcY, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(srcWidth, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(srcHeight, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(dstX, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(dstY, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(dstWidth, 'g', -1, 64)) + canvas.script.WriteRune(',') + canvas.script.WriteString(strconv.FormatFloat(dstHeight, 'g', -1, 64)) + canvas.script.WriteString(");\n}") +} diff --git a/canvasView.go b/canvasView.go new file mode 100644 index 0000000..7e5d02c --- /dev/null +++ b/canvasView.go @@ -0,0 +1,118 @@ +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" + +// CanvasView interface of a custom draw view +type CanvasView interface { + View + Redraw() +} + +type canvasViewData struct { + viewData + drawer func(Canvas) +} + +// NewCanvasView creates the new custom draw view +func NewCanvasView(session Session, params Params) CanvasView { + view := new(canvasViewData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newCanvasView(session Session) View { + return NewCanvasView(session, nil) +} + +// Init initialize fields of ViewsContainer by default values +func (canvasView *canvasViewData) Init(session Session) { + canvasView.viewData.Init(session) + canvasView.tag = "CanvasView" +} + +func (canvasView *canvasViewData) normalizeTag(tag string) string { + tag = strings.ToLower(tag) + switch tag { + case "draw-func": + tag = DrawFunction + } + return tag +} + +func (canvasView *canvasViewData) Remove(tag string) { + canvasView.remove(canvasView.normalizeTag(tag)) +} + +func (canvasView *canvasViewData) remove(tag string) { + if tag == DrawFunction { + canvasView.drawer = nil + canvasView.Redraw() + } else { + canvasView.viewData.remove(tag) + } +} + +func (canvasView *canvasViewData) Set(tag string, value interface{}) bool { + return canvasView.set(canvasView.normalizeTag(tag), value) +} + +func (canvasView *canvasViewData) set(tag string, value interface{}) bool { + if tag == DrawFunction { + if value == nil { + canvasView.drawer = nil + } else if fn, ok := value.(func(Canvas)); ok { + canvasView.drawer = fn + } else { + notCompatibleType(tag, value) + return false + } + canvasView.Redraw() + return true + } + + return canvasView.viewData.set(tag, value) +} + +func (canvasView *canvasViewData) Get(tag string) interface{} { + return canvasView.get(canvasView.normalizeTag(tag)) +} + +func (canvasView *canvasViewData) get(tag string) interface{} { + if tag == DrawFunction { + return canvasView.drawer + } + return canvasView.viewData.get(tag) +} + +func (canvasView *canvasViewData) htmlTag() string { + return "canvas" +} + +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) + } + canvasView.session.runScript(canvas.finishDraw()) + } +} + +func (canvasView *canvasViewData) onResize(self View, x, y, width, height float64) { + canvasView.viewData.onResize(self, x, y, width, height) + canvasView.Redraw() +} + +// RedrawCanvasView finds CanvasView with canvasViewID and redraws it +func RedrawCanvasView(rootView View, canvasViewID string) { + if canvas := CanvasViewByID(rootView, canvasViewID); canvas != nil { + canvas.Redraw() + } +} diff --git a/checkbox.go b/checkbox.go new file mode 100644 index 0000000..4ae2bfa --- /dev/null +++ b/checkbox.go @@ -0,0 +1,374 @@ +package rui + +import ( + "fmt" + "strings" +) + +// 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" + +// Checkbox - 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) +} + +func (button *checkboxData) Init(session Session) { + button.viewsContainerData.Init(session) + button.tag = "Checkbox" + button.systemClass = "ruiGridLayout ruiCheckbox" + button.checkedListeners = []func(Checkbox, bool){} +} + +func (button *checkboxData) Focusable() bool { + return true +} + +func (button *checkboxData) Get(tag string) interface{} { + switch strings.ToLower(tag) { + case CheckboxChangedEvent: + return button.checkedListeners + } + + return button.viewsContainerData.Get(tag) +} + +func (button *checkboxData) Set(tag string, value interface{}) bool { + switch strings.ToLower(tag) { + case CheckboxChangedEvent: + ok := button.setChangedListener(value) + if !ok { + notCompatibleType(tag, value) + } + return ok + + case Checked: + oldChecked := button.checked() + if !button.setBoolProperty(Checked, value) { + return false + } + if button.created { + checked := button.checked() + if checked != oldChecked { + button.changedCheckboxState(checked) + } + } + return true + + case CheckboxHorizontalAlign, CheckboxVerticalAlign: + if button.setEnumProperty(tag, value, enumProperties[tag].values) { + if button.created { + htmlID := button.htmlID() + updateCSSStyle(htmlID, button.session) + updateInnerHTML(htmlID, button.session) + } + return true + } + return false + + case VerticalAlign: + if button.setEnumProperty(tag, value, enumProperties[tag].values) { + if button.created { + updateCSSProperty(button.htmlID()+"content", "align-items", button.cssVerticalAlign(), button.session) + } + return true + } + return false + + case HorizontalAlign: + if button.setEnumProperty(tag, value, enumProperties[tag].values) { + if button.created { + updateCSSProperty(button.htmlID()+"content", "justify-items", button.cssHorizontalAlign(), button.session) + } + return true + } + return false + + case CellVerticalAlign, CellHorizontalAlign, CellWidth, CellHeight: + return false + } + + return button.viewsContainerData.Set(tag, value) +} + +func (button *checkboxData) Remove(tag string) { + switch strings.ToLower(tag) { + case CheckboxChangedEvent: + if len(button.checkedListeners) > 0 { + button.checkedListeners = []func(Checkbox, bool){} + } + + case Checked: + oldChecked := button.checked() + delete(button.properties, tag) + if oldChecked { + button.changedCheckboxState(false) + } + + case CheckboxHorizontalAlign, CheckboxVerticalAlign: + delete(button.properties, tag) + htmlID := button.htmlID() + updateCSSStyle(htmlID, button.session) + updateInnerHTML(htmlID, button.session) + + case VerticalAlign: + delete(button.properties, tag) + updateCSSProperty(button.htmlID()+"content", "align-items", button.cssVerticalAlign(), button.session) + + case HorizontalAlign: + delete(button.properties, tag) + updateCSSProperty(button.htmlID()+"content", "justify-items", button.cssHorizontalAlign(), button.session) + + default: + button.viewsContainerData.Remove(tag) + } +} + +func (button *checkboxData) checked() bool { + checked, _ := boolProperty(button, Checked, button.Session()) + return checked +} + +func (button *checkboxData) changedCheckboxState(state bool) { + for _, listener := range button.checkedListeners { + listener(button, state) + } + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + button.htmlCheckbox(buffer, state) + button.Session().runScript(fmt.Sprintf(`updateInnerHTML('%v', '%v');`, button.htmlID()+"checkbox", buffer.String())) +} + +func checkboxClickListener(view View) { + view.Set(Checked, !IsCheckboxChecked(view, "")) +} + +func checkboxKeyListener(view View, event KeyEvent) { + switch event.Code { + case "Enter", "Space": + view.Set(Checked, !IsCheckboxChecked(view, "")) + } +} + +func (button *checkboxData) setChangedListener(value interface{}) bool { + if value == nil { + if len(button.checkedListeners) > 0 { + button.checkedListeners = []func(Checkbox, bool){} + } + return true + } + + switch value := value.(type) { + case func(Checkbox, bool): + button.checkedListeners = []func(Checkbox, bool){value} + + case func(bool): + fn := func(view Checkbox, checked bool) { + value(checked) + } + button.checkedListeners = []func(Checkbox, bool){fn} + + case []func(Checkbox, bool): + button.checkedListeners = value + + case []func(bool): + listeners := make([]func(Checkbox, bool), len(value)) + for i, val := range value { + if val == nil { + return false + } + + listeners[i] = func(view Checkbox, checked bool) { + val(checked) + } + } + button.checkedListeners = listeners + + case []interface{}: + listeners := make([]func(Checkbox, bool), len(value)) + for i, val := range value { + if val == nil { + return false + } + + switch val := val.(type) { + case func(Checkbox, bool): + listeners[i] = val + + case func(bool): + listeners[i] = func(view Checkbox, date bool) { + val(date) + } + + default: + return false + } + } + button.checkedListeners = listeners + } + return true +} + +func (button *checkboxData) cssStyle(self View, builder cssBuilder) { + session := button.Session() + vAlign, _ := enumStyledProperty(button, CheckboxVerticalAlign, LeftAlign) + hAlign, _ := enumStyledProperty(button, CheckboxHorizontalAlign, TopAlign) + switch hAlign { + case CenterAlign: + if vAlign == BottomAlign { + builder.add("grid-template-rows", "1fr auto") + } else { + builder.add("grid-template-rows", "auto 1fr") + } + + case RightAlign: + builder.add("grid-template-columns", "1fr auto") + + default: + builder.add("grid-template-columns", "auto 1fr") + } + + if gap, ok := sizeConstant(session, "ruiCheckboxGap"); ok && gap.Type != Auto && gap.Value > 0 { + builder.add("gap", gap.cssString("0")) + } + + builder.add("align-items", "stretch") + builder.add("justify-items", "stretch") + + button.viewsContainerData.cssStyle(self, builder) +} + +func (button *checkboxData) htmlCheckbox(buffer *strings.Builder, checked bool) (int, int) { + vAlign, _ := enumStyledProperty(button, CheckboxVerticalAlign, LeftAlign) + hAlign, _ := enumStyledProperty(button, CheckboxHorizontalAlign, TopAlign) + + buffer.WriteString(`
`) + if checked { + buffer.WriteString(button.Session().checkboxOnImage()) + } else { + buffer.WriteString(button.Session().checkboxOffImage()) + } + buffer.WriteString(`
`) + + return vAlign, hAlign +} + +func (button *checkboxData) htmlSubviews(self View, buffer *strings.Builder) { + + vCheckboxAlign, hCheckboxAlign := button.htmlCheckbox(buffer, IsCheckboxChecked(button, "")) + + buffer.WriteString(`
`) + button.viewsContainerData.htmlSubviews(self, buffer) + buffer.WriteString(`
`) +} + +func (button *checkboxData) cssHorizontalAlign() string { + align, _ := enumStyledProperty(button, HorizontalAlign, TopAlign) + values := enumProperties[CellHorizontalAlign].cssValues + if align >= 0 && align < len(values) { + return values[align] + } + return values[0] +} + +func (button *checkboxData) cssVerticalAlign() string { + align, _ := enumStyledProperty(button, VerticalAlign, TopAlign) + values := enumProperties[CellVerticalAlign].cssValues + if align >= 0 && align < len(values) { + return values[align] + } + return values[0] +} + +// IsCheckboxChecked returns true if the Checkbox is checked, false otherwise. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func IsCheckboxChecked(view View, subviewID string) bool { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if checked := view.Get(Checked); checked != nil { + if b, ok := checked.(bool); ok { + return b + } + } + } + return false +} diff --git a/color.go b/color.go new file mode 100644 index 0000000..adea3a8 --- /dev/null +++ b/color.go @@ -0,0 +1,177 @@ +package rui + +import ( + "bytes" + "fmt" + "strconv" + "strings" +) + +// Color - represent color in argb format +type Color uint32 + +// ARGB - return alpha, red, green and blue components of the color +func (color Color) ARGB() (uint8, uint8, uint8, uint8) { + return uint8(color >> 24), + uint8((color >> 16) & 0xFF), + uint8((color >> 8) & 0xFF), + uint8(color & 0xFF) +} + +// Alpha - return the alpha component of the color +func (color Color) Alpha() int { + return int((color >> 24) & 0xFF) +} + +// Red - return the red component of the color +func (color Color) Red() int { + return int((color >> 16) & 0xFF) +} + +// Green - return the green component of the color +func (color Color) Green() int { + return int((color >> 8) & 0xFF) +} + +// Blue - return the blue component of the color +func (color Color) Blue() int { + return int(color & 0xFF) +} + +// String get a text representation of the color +func (color Color) String() string { + return fmt.Sprintf("#%08X", int(color)) +} + +func (color Color) rgbString() string { + return fmt.Sprintf("#%06X", int(color&0xFFFFFF)) +} + +// writeData write a text representation of the color to the buffer +func (color Color) writeData(buffer *bytes.Buffer) { + buffer.WriteString(color.String()) +} + +// cssString get the text representation of the color in CSS format +func (color Color) cssString() string { + red := color.Red() + green := color.Green() + blue := color.Blue() + + if alpha := color.Alpha(); alpha < 255 { + aText := fmt.Sprintf("%.2f", float64(alpha)/255.0) + if len(aText) > 1 { + aText = aText[1:] + } + return fmt.Sprintf("rgba(%d,%d,%d,%s)", red, green, blue, aText) + } + + return fmt.Sprintf("rgb(%d,%d,%d)", red, green, blue) +} + +// StringToColor converts the string argument to Color value +func StringToColor(text string) (Color, bool) { + + text = strings.Trim(text, " \t\r\n") + if text == "" { + ErrorLog(`Invalid color value: ""`) + return 0, false + } + + if text[0] == '#' { + c, err := strconv.ParseUint(text[1:], 16, 32) + if err != nil { + ErrorLog("Set color value error: " + err.Error()) + return 0, false + } + + switch len(text) - 1 { + case 8: + return Color(c), true + + case 6: + return Color(c | 0xFF000000), true + + case 4: + a := (c >> 12) & 0xF + r := (c >> 8) & 0xF + g := (c >> 4) & 0xF + b := c & 0xF + return Color((a << 28) | (a << 24) | (r << 20) | (r << 16) | (g << 12) | (g << 8) | (b << 4) | b), true + + case 3: + r := (c >> 8) & 0xF + g := (c >> 4) & 0xF + b := c & 0xF + return Color(0xFF000000 | (r << 20) | (r << 16) | (g << 12) | (g << 8) | (b << 4) | b), true + } + + ErrorLog(`Invalid color format: "` + text + `". Valid formats: #AARRGGBB, #RRGGBB, #ARGB, #RGB`) + return 0, false + } + + parseRGB := func(args string) []int { + args = strings.Trim(args, " \t") + count := len(args) + if count < 3 || args[0] != '(' || args[count-1] != ')' { + return []int{} + } + + arg := strings.Split(args[1:count-1], ",") + result := make([]int, len(arg)) + for i, val := range arg { + val = strings.Trim(val, " \t") + size := len(val) + if size == 0 { + return []int{} + } + if val[size-1] == '%' { + if n, err := strconv.Atoi(val[:size-1]); err == nil && n >= 0 && n <= 100 { + result[i] = n * 255 / 100 + } else { + return []int{} + } + } else if strings.ContainsRune(val, '.') { + if val[0] == '.' { + val = "0" + val + } + if f, err := strconv.ParseFloat(val, 32); err == nil && f >= 0 && f <= 1 { + result[i] = int(f * 255) + } else { + return []int{} + } + } else { + if n, err := strconv.Atoi(val); err == nil && n >= 0 && n <= 255 { + result[i] = n + } else { + return []int{} + } + } + } + return result + } + + text = strings.ToLower(text) + if strings.HasPrefix(text, "rgba") { + args := parseRGB(text[4:]) + if len(args) == 4 { + return Color((args[3] << 24) | (args[0] << 16) | (args[1] << 8) | args[2]), true + } + } + + if strings.HasPrefix(text, "rgb") { + args := parseRGB(text[3:]) + if len(args) == 3 { + return Color(0xFF000000 | (args[0] << 16) | (args[1] << 8) | args[2]), true + } + } + + // TODO hsl(360,100%,50%), hsla(360,100%,50%,.5) + + if color, ok := colorConstants[text]; ok { + return color, true + } + + ErrorLog(`Invalid color format: "` + text + `"`) + return 0, false +} diff --git a/colorConstants.go b/colorConstants.go new file mode 100644 index 0000000..dee5c48 --- /dev/null +++ b/colorConstants.go @@ -0,0 +1,448 @@ +package rui + +const ( + // Black color constant + Black Color = 0xff000000 + // Silver color constant + Silver Color = 0xffc0c0c0 + // Gray color constant + Gray Color = 0xff808080 + // White color constant + White Color = 0xffffffff + // Maroon color constant + Maroon Color = 0xff800000 + // Red color constant + Red Color = 0xffff0000 + // Purple color constant + Purple Color = 0xff800080 + // Fuchsia color constant + Fuchsia Color = 0xffff00ff + // Green color constant + Green Color = 0xff008000 + // Lime color constant + Lime Color = 0xff00ff00 + // Olive color constant + Olive Color = 0xff808000 + // Yellow color constant + Yellow Color = 0xffffff00 + // Navy color constant + Navy Color = 0xff000080 + // Blue color constant + Blue Color = 0xff0000ff + // Teal color constant + Teal Color = 0xff008080 + // Aqua color constant + Aqua Color = 0xff00ffff + // Orange color constant + Orange Color = 0xffffa500 + // AliceBlue color constant + AliceBlue Color = 0xfff0f8ff + // AntiqueWhite color constant + AntiqueWhite Color = 0xfffaebd7 + // Aquamarine color constant + Aquamarine Color = 0xff7fffd4 + // Azure color constant + Azure Color = 0xfff0ffff + // Beige color constant + Beige Color = 0xfff5f5dc + // Bisque color constant + Bisque Color = 0xffffe4c4 + // BlanchedAlmond color constant + BlanchedAlmond Color = 0xffffebcd + // BlueViolet color constant + BlueViolet Color = 0xff8a2be2 + // Brown color constant + Brown Color = 0xffa52a2a + // Burlywood color constant + Burlywood Color = 0xffdeb887 + // CadetBlue color constant + CadetBlue Color = 0xff5f9ea0 + // Chartreuse color constant + Chartreuse Color = 0xff7fff00 + // Chocolate color constant + Chocolate Color = 0xffd2691e + // Coral color constant + Coral Color = 0xffff7f50 + // CornflowerBlue color constant + CornflowerBlue Color = 0xff6495ed + // Cornsilk color constant + Cornsilk Color = 0xfffff8dc + // Crimson color constant + Crimson Color = 0xffdc143c + // Cyan color constant + Cyan Color = 0xff00ffff + // DarkBlue color constant + DarkBlue Color = 0xff00008b + // DarkCyan color constant + DarkCyan Color = 0xff008b8b + // DarkGoldenRod color constant + DarkGoldenRod Color = 0xffb8860b + // DarkGray color constant + DarkGray Color = 0xffa9a9a9 + // DarkGreen color constant + DarkGreen Color = 0xff006400 + // DarkGrey color constant + DarkGrey Color = 0xffa9a9a9 + // DarkKhaki color constant + DarkKhaki Color = 0xffbdb76b + // DarkMagenta color constant + DarkMagenta Color = 0xff8b008b + // DarkOliveGreen color constant + DarkOliveGreen Color = 0xff556b2f + // DarkOrange color constant + DarkOrange Color = 0xffff8c00 + // DarkOrchid color constant + DarkOrchid Color = 0xff9932cc + // DarkRed color constant + DarkRed Color = 0xff8b0000 + // DarkSalmon color constant + DarkSalmon Color = 0xffe9967a + // DarkSeaGreen color constant + DarkSeaGreen Color = 0xff8fbc8f + // DarkSlateBlue color constant + DarkSlateBlue Color = 0xff483d8b + // DarkSlateGray color constant + DarkSlateGray Color = 0xff2f4f4f + // Darkslategrey color constant + Darkslategrey Color = 0xff2f4f4f + // DarkTurquoise color constant + DarkTurquoise Color = 0xff00ced1 + // DarkViolet color constant + DarkViolet Color = 0xff9400d3 + // DeepPink color constant + DeepPink Color = 0xffff1493 + // DeepSkyBlue color constant + DeepSkyBlue Color = 0xff00bfff + // DimGray color constant + DimGray Color = 0xff696969 + // DimGrey color constant + DimGrey Color = 0xff696969 + // DodgerBlue color constant + DodgerBlue Color = 0xff1e90ff + // FireBrick color constant + FireBrick Color = 0xffb22222 + // FloralWhite color constant + FloralWhite Color = 0xfffffaf0 + // ForestGreen color constant + ForestGreen Color = 0xff228b22 + // Gainsboro color constant + Gainsboro Color = 0xffdcdcdc + // GhostWhite color constant + GhostWhite Color = 0xfff8f8ff + // Gold color constant + Gold Color = 0xffffd700 + // GoldenRod color constant + GoldenRod Color = 0xffdaa520 + // GreenyEllow color constant + GreenyEllow Color = 0xffadff2f + // Grey color constant + Grey Color = 0xff808080 + // Honeydew color constant + Honeydew Color = 0xfff0fff0 + // HotPink color constant + HotPink Color = 0xffff69b4 + // IndianRed color constant + IndianRed Color = 0xffcd5c5c + // Indigo color constant + Indigo Color = 0xff4b0082 + // Ivory color constant + Ivory Color = 0xfffffff0 + // Khaki color constant + Khaki Color = 0xfff0e68c + // Lavender color constant + Lavender Color = 0xffe6e6fa + // LavenderBlush color constant + LavenderBlush Color = 0xfffff0f5 + // LawnGreen color constant + LawnGreen Color = 0xff7cfc00 + // LemonChiffon color constant + LemonChiffon Color = 0xfffffacd + // LightBlue color constant + LightBlue Color = 0xffadd8e6 + // LightCoral color constant + LightCoral Color = 0xfff08080 + // LightCyan color constant + LightCyan Color = 0xffe0ffff + // LightGoldenrodYellow color constant + LightGoldenrodYellow Color = 0xfffafad2 + // LightGray color constant + LightGray Color = 0xffd3d3d3 + // LightGreen color constant + LightGreen Color = 0xff90ee90 + // LightGrey color constant + LightGrey Color = 0xffd3d3d3 + // LightPink color constant + LightPink Color = 0xffffb6c1 + // LightSalmon color constant + LightSalmon Color = 0xffffa07a + // LightSeaGreen color constant + LightSeaGreen Color = 0xff20b2aa + // LightSkyBlue color constant + LightSkyBlue Color = 0xff87cefa + // LightSlateGray color constant + LightSlateGray Color = 0xff778899 + // LightSlateGrey color constant + LightSlateGrey Color = 0xff778899 + // LightSteelBlue color constant + LightSteelBlue Color = 0xffb0c4de + // LightYellow color constant + LightYellow Color = 0xffffffe0 + // LimeGreen color constant + LimeGreen Color = 0xff32cd32 + // Linen color constant + Linen Color = 0xfffaf0e6 + // Magenta color constant + Magenta Color = 0xffff00ff + // MediumAquamarine color constant + MediumAquamarine Color = 0xff66cdaa + // MediumBlue color constant + MediumBlue Color = 0xff0000cd + // MediumOrchid color constant + MediumOrchid Color = 0xffba55d3 + // MediumPurple color constant + MediumPurple Color = 0xff9370db + // MediumSeaGreen color constant + MediumSeaGreen Color = 0xff3cb371 + // MediumSlateBlue color constant + MediumSlateBlue Color = 0xff7b68ee + // MediumSpringGreen color constant + MediumSpringGreen Color = 0xff00fa9a + // MediumTurquoise color constant + MediumTurquoise Color = 0xff48d1cc + // MediumVioletRed color constant + MediumVioletRed Color = 0xffc71585 + // MidnightBlue color constant + MidnightBlue Color = 0xff191970 + // MintCream color constant + MintCream Color = 0xfff5fffa + // MistyRose color constant + MistyRose Color = 0xffffe4e1 + // Moccasin color constant + Moccasin Color = 0xffffe4b5 + // NavajoWhite color constant + NavajoWhite Color = 0xffffdead + // OldLace color constant + OldLace Color = 0xfffdf5e6 + // OliveDrab color constant + OliveDrab Color = 0xff6b8e23 + // OrangeRed color constant + OrangeRed Color = 0xffff4500 + // Orchid color constant + Orchid Color = 0xffda70d6 + // PaleGoldenrod color constant + PaleGoldenrod Color = 0xffeee8aa + // PaleGreen color constant + PaleGreen Color = 0xff98fb98 + // PaleTurquoise color constant + PaleTurquoise Color = 0xffafeeee + // PaleVioletRed color constant + PaleVioletRed Color = 0xffdb7093 + // PapayaWhip color constant + PapayaWhip Color = 0xffffefd5 + // PeachPuff color constant + PeachPuff Color = 0xffffdab9 + // Peru color constant + Peru Color = 0xffcd853f + // Pink color constant + Pink Color = 0xffffc0cb + // Plum color constant + Plum Color = 0xffdda0dd + // PowderBlue color constant + PowderBlue Color = 0xffb0e0e6 + // RosyBrown color constant + RosyBrown Color = 0xffbc8f8f + // RoyalBlue color constant + RoyalBlue Color = 0xff4169e1 + // SaddleBrown color constant + SaddleBrown Color = 0xff8b4513 + // Salmon color constant + Salmon Color = 0xfffa8072 + // SandyBrown color constant + SandyBrown Color = 0xfff4a460 + // SeaGreen color constant + SeaGreen Color = 0xff2e8b57 + // SeaShell color constant + SeaShell Color = 0xfffff5ee + // Sienna color constant + Sienna Color = 0xffa0522d + // SkyBlue color constant + SkyBlue Color = 0xff87ceeb + // SlateBlue color constant + SlateBlue Color = 0xff6a5acd + // SlateGray color constant + SlateGray Color = 0xff708090 + // SlateGrey color constant + SlateGrey Color = 0xff708090 + // Snow color constant + Snow Color = 0xfffffafa + // SpringGreen color constant + SpringGreen Color = 0xff00ff7f + // SteelBlue color constant + SteelBlue Color = 0xff4682b4 + // Tan color constant + Tan Color = 0xffd2b48c + // Thistle color constant + Thistle Color = 0xffd8bfd8 + // Tomato color constant + Tomato Color = 0xffff6347 + // Turquoise color constant + Turquoise Color = 0xff40e0d0 + // Violet color constant + Violet Color = 0xffee82ee + // Wheat color constant + Wheat Color = 0xfff5deb3 + // Whitesmoke color constant + Whitesmoke Color = 0xfff5f5f5 + // YellowGreen color constant + YellowGreen Color = 0xff9acd32 +) + +var colorConstants = map[string]Color{ + "black": 0xff000000, + "silver": 0xffc0c0c0, + "gray": 0xff808080, + "white": 0xffffffff, + "maroon": 0xff800000, + "red": 0xffff0000, + "purple": 0xff800080, + "fuchsia": 0xffff00ff, + "green": 0xff008000, + "lime": 0xff00ff00, + "olive": 0xff808000, + "yellow": 0xffffff00, + "navy": 0xff000080, + "blue": 0xff0000ff, + "teal": 0xff008080, + "aqua": 0xff00ffff, + "orange": 0xffffa500, + "aliceblue": 0xfff0f8ff, + "antiquewhite": 0xfffaebd7, + "aquamarine": 0xff7fffd4, + "azure": 0xfff0ffff, + "beige": 0xfff5f5dc, + "bisque": 0xffffe4c4, + "blanchedalmond": 0xffffebcd, + "blueviolet": 0xff8a2be2, + "brown": 0xffa52a2a, + "burlywood": 0xffdeb887, + "cadetblue": 0xff5f9ea0, + "chartreuse": 0xff7fff00, + "chocolate": 0xffd2691e, + "coral": 0xffff7f50, + "cornflowerblue": 0xff6495ed, + "cornsilk": 0xfffff8dc, + "crimson": 0xffdc143c, + "cyan": 0xff00ffff, + "darkblue": 0xff00008b, + "darkcyan": 0xff008b8b, + "darkgoldenrod": 0xffb8860b, + "darkgray": 0xffa9a9a9, + "darkgreen": 0xff006400, + "darkgrey": 0xffa9a9a9, + "darkkhaki": 0xffbdb76b, + "darkmagenta": 0xff8b008b, + "darkolivegreen": 0xff556b2f, + "darkorange": 0xffff8c00, + "darkorchid": 0xff9932cc, + "darkred": 0xff8b0000, + "darksalmon": 0xffe9967a, + "darkseagreen": 0xff8fbc8f, + "darkslateblue": 0xff483d8b, + "darkslategray": 0xff2f4f4f, + "darkslategrey": 0xff2f4f4f, + "darkturquoise": 0xff00ced1, + "darkviolet": 0xff9400d3, + "deeppink": 0xffff1493, + "deepskyblue": 0xff00bfff, + "dimgray": 0xff696969, + "dimgrey": 0xff696969, + "dodgerblue": 0xff1e90ff, + "firebrick": 0xffb22222, + "floralwhite": 0xfffffaf0, + "forestgreen": 0xff228b22, + "gainsboro": 0xffdcdcdc, + "ghostwhite": 0xfff8f8ff, + "gold": 0xffffd700, + "goldenrod": 0xffdaa520, + "greenyellow": 0xffadff2f, + "grey": 0xff808080, + "honeydew": 0xfff0fff0, + "hotpink": 0xffff69b4, + "indianred": 0xffcd5c5c, + "indigo": 0xff4b0082, + "ivory": 0xfffffff0, + "khaki": 0xfff0e68c, + "lavender": 0xffe6e6fa, + "lavenderblush": 0xfffff0f5, + "lawngreen": 0xff7cfc00, + "lemonchiffon": 0xfffffacd, + "lightblue": 0xffadd8e6, + "lightcoral": 0xfff08080, + "lightcyan": 0xffe0ffff, + "lightgoldenrodyellow": 0xfffafad2, + "lightgray": 0xffd3d3d3, + "lightgreen": 0xff90ee90, + "lightgrey": 0xffd3d3d3, + "lightpink": 0xffffb6c1, + "lightsalmon": 0xffffa07a, + "lightseagreen": 0xff20b2aa, + "lightskyblue": 0xff87cefa, + "lightslategray": 0xff778899, + "lightslategrey": 0xff778899, + "lightsteelblue": 0xffb0c4de, + "lightyellow": 0xffffffe0, + "limegreen": 0xff32cd32, + "linen": 0xfffaf0e6, + "magenta": 0xffff00ff, + "mediumaquamarine": 0xff66cdaa, + "mediumblue": 0xff0000cd, + "mediumorchid": 0xffba55d3, + "mediumpurple": 0xff9370db, + "mediumseagreen": 0xff3cb371, + "mediumslateblue": 0xff7b68ee, + "mediumspringgreen": 0xff00fa9a, + "mediumturquoise": 0xff48d1cc, + "mediumvioletred": 0xffc71585, + "midnightblue": 0xff191970, + "mintcream": 0xfff5fffa, + "mistyrose": 0xffffe4e1, + "moccasin": 0xffffe4b5, + "navajowhite": 0xffffdead, + "oldlace": 0xfffdf5e6, + "olivedrab": 0xff6b8e23, + "orangered": 0xffff4500, + "orchid": 0xffda70d6, + "palegoldenrod": 0xffeee8aa, + "palegreen": 0xff98fb98, + "paleturquoise": 0xffafeeee, + "palevioletred": 0xffdb7093, + "papayawhip": 0xffffefd5, + "peachpuff": 0xffffdab9, + "peru": 0xffcd853f, + "pink": 0xffffc0cb, + "plum": 0xffdda0dd, + "powderblue": 0xffb0e0e6, + "rosybrown": 0xffbc8f8f, + "royalblue": 0xff4169e1, + "saddlebrown": 0xff8b4513, + "salmon": 0xfffa8072, + "sandybrown": 0xfff4a460, + "seagreen": 0xff2e8b57, + "seashell": 0xfffff5ee, + "sienna": 0xffa0522d, + "skyblue": 0xff87ceeb, + "slateblue": 0xff6a5acd, + "slategray": 0xff708090, + "slategrey": 0xff708090, + "snow": 0xfffffafa, + "springgreen": 0xff00ff7f, + "steelblue": 0xff4682b4, + "tan": 0xffd2b48c, + "thistle": 0xffd8bfd8, + "tomato": 0xffff6347, + "turquoise": 0xff40e0d0, + "violet": 0xffee82ee, + "wheat": 0xfff5deb3, + "whitesmoke": 0xfff5f5f5, + "yellowgreen": 0xff9acd32, +} diff --git a/colorPicker.go b/colorPicker.go new file mode 100644 index 0000000..2d0f6ba --- /dev/null +++ b/colorPicker.go @@ -0,0 +1,253 @@ +package rui + +import ( + "fmt" + "strings" +) + +const ( + ColorChangedEvent = "color-changed" + ColorPickerValue = "color-picker-value" +) + +// ColorPicker - ColorPicker view +type ColorPicker interface { + View +} + +type colorPickerData struct { + viewData + colorChangedListeners []func(ColorPicker, Color) +} + +// NewColorPicker create new ColorPicker object and return it +func NewColorPicker(session Session, params Params) ColorPicker { + view := new(colorPickerData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newColorPicker(session Session) View { + return NewColorPicker(session, nil) +} + +func (picker *colorPickerData) Init(session Session) { + picker.viewData.Init(session) + picker.tag = "ColorPicker" + picker.colorChangedListeners = []func(ColorPicker, Color){} + picker.properties[Padding] = Px(0) +} + +func (picker *colorPickerData) normalizeTag(tag string) string { + tag = strings.ToLower(tag) + switch tag { + case Value, ColorProperty: + return ColorPickerValue + } + + return tag +} + +func (picker *colorPickerData) Remove(tag string) { + picker.remove(picker.normalizeTag(tag)) +} + +func (picker *colorPickerData) remove(tag string) { + switch tag { + case ColorChangedEvent: + picker.colorChangedListeners = []func(ColorPicker, Color){} + + case ColorPickerValue: + oldColor := GetColorPickerValue(picker, "") + delete(picker.properties, ColorPickerValue) + picker.colorChanged(oldColor) + + default: + picker.viewData.remove(tag) + } +} + +func (picker *colorPickerData) Set(tag string, value interface{}) bool { + return picker.set(picker.normalizeTag(tag), value) +} + +func (picker *colorPickerData) set(tag string, value interface{}) bool { + if value == nil { + picker.remove(tag) + return true + } + + switch tag { + case ColorChangedEvent: + switch value := value.(type) { + case func(ColorPicker, Color): + picker.colorChangedListeners = []func(ColorPicker, Color){value} + + case func(Color): + fn := func(view ColorPicker, date Color) { + value(date) + } + picker.colorChangedListeners = []func(ColorPicker, Color){fn} + + case []func(ColorPicker, Color): + picker.colorChangedListeners = value + + case []func(Color): + listeners := make([]func(ColorPicker, Color), len(value)) + for i, val := range value { + if val == nil { + notCompatibleType(tag, val) + return false + } + + listeners[i] = func(view ColorPicker, date Color) { + val(date) + } + } + picker.colorChangedListeners = listeners + + case []interface{}: + listeners := make([]func(ColorPicker, Color), len(value)) + for i, val := range value { + if val == nil { + notCompatibleType(tag, val) + return false + } + + switch val := val.(type) { + case func(ColorPicker, Color): + listeners[i] = val + + case func(Color): + listeners[i] = func(view ColorPicker, date Color) { + val(date) + } + + default: + notCompatibleType(tag, val) + return false + } + } + picker.colorChangedListeners = listeners + } + return true + + case ColorPickerValue: + oldColor := GetColorPickerValue(picker, "") + if picker.setColorProperty(ColorPickerValue, value) { + newValue := GetColorPickerValue(picker, "") + if oldColor != newValue { + picker.colorChanged(oldColor) + } + return true + } + + default: + return picker.viewData.set(tag, value) + } + return false +} + +func (picker *colorPickerData) colorChanged(oldColor Color) { + newColor := GetColorPickerValue(picker, "") + if oldColor != newColor { + picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, picker.htmlID(), newColor.rgbString())) + for _, listener := range picker.colorChangedListeners { + listener(picker, newColor) + } + } +} + +func (picker *colorPickerData) Get(tag string) interface{} { + return picker.get(picker.normalizeTag(tag)) +} + +func (picker *colorPickerData) get(tag string) interface{} { + switch tag { + case ColorChangedEvent: + return picker.colorChangedListeners + + default: + return picker.viewData.get(tag) + } +} + +func (picker *colorPickerData) htmlTag() string { + return "input" +} + +func (picker *colorPickerData) htmlProperties(self View, buffer *strings.Builder) { + picker.viewData.htmlProperties(self, buffer) + + buffer.WriteString(` type="color" value="`) + buffer.WriteString(GetColorPickerValue(picker, "").rgbString()) + buffer.WriteByte('"') + + buffer.WriteString(` oninput="editViewInputEvent(this)"`) +} + +func (picker *colorPickerData) htmlDisabledProperties(self View, buffer *strings.Builder) { + if IsDisabled(self) { + buffer.WriteString(` disabled`) + } + picker.viewData.htmlDisabledProperties(self, buffer) +} + +func (picker *colorPickerData) handleCommand(self View, command string, data DataObject) bool { + switch command { + case "textChanged": + if text, ok := data.PropertyValue("text"); ok { + oldColor := GetColorPickerValue(picker, "") + if color, ok := StringToColor(text); ok { + picker.properties[ColorPickerValue] = color + if color != oldColor { + for _, listener := range picker.colorChangedListeners { + listener(picker, color) + } + } + } + } + return true + } + + return picker.viewData.handleCommand(self, command, data) +} + +// GetColorPickerValue returns the value of ColorPicker subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetColorPickerValue(view View, subviewID string) Color { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := colorStyledProperty(view, ColorPickerValue); ok { + return result + } + for _, tag := range []string{Value, ColorProperty} { + if value, ok := valueFromStyle(view, tag); ok { + if result, ok := valueToColor(value, view.Session()); ok { + return result + } + } + } + } + return 0 +} + +// GetColorChangedListeners returns the ColorChangedListener list of an ColorPicker subview. +// If there are no listeners then the empty list is returned +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetColorChangedListeners(view View, subviewID string) []func(ColorPicker, Color) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.Get(ColorChangedEvent); value != nil { + if listeners, ok := value.([]func(ColorPicker, Color)); ok { + return listeners + } + } + } + return []func(ColorPicker, Color){} +} diff --git a/color_test.go b/color_test.go new file mode 100644 index 0000000..011b8a4 --- /dev/null +++ b/color_test.go @@ -0,0 +1,120 @@ +package rui + +import ( + "bytes" + "testing" +) + +func TestColorARGB(t *testing.T) { + color := Color(0x7FFE8743) + a, r, g, b := color.ARGB() + if a != 0x7F { + t.Error("a != 0x7F") + } + if r != 0xFE { + t.Error("r != 0xFE") + } + if g != 0x87 { + t.Error("g != 0x87") + } + if b != 0x43 { + t.Error("b != 0x43") + } + + if color.Alpha() != 0x7F { + t.Error("color.Alpha() != 0x7F") + } + + if color.Red() != 0xFE { + t.Error("color.Red() != 0xFE") + } + + if color.Green() != 0x87 { + t.Error("color.Green() != 0x87") + } + + if color.Blue() != 0x43 { + t.Error("color.Blue() != 0x43") + } +} + +func TestColorSetValue(t *testing.T) { + createTestLog(t, true) + + testData := []struct{ src, result string }{ + {"#7F102040", "rgba(16,32,64,.50)"}, + {"#102040", "rgb(16,32,64)"}, + {"#8124", "rgba(17,34,68,.53)"}, + {"rgba(17,34,67,.5)", "rgba(17,34,67,.50)"}, + {"rgb(.25,50%,96)", "rgb(63,127,96)"}, + {"rgba(.25,50%,96,100%)", "rgb(63,127,96)"}, + } + + for _, data := range testData { + color, ok := StringToColor(data.src) + if !ok { + t.Errorf(`color.SetValue("%s") fail`, data.src) + } + result := color.cssString() + if result != data.result { + t.Errorf(`color.cssString() = "%s", expected: "%s"`, result, data.result) + } + } +} + +func TestColorWriteData(t *testing.T) { + testCSS := func(t *testing.T, color Color, result string) { + buffer := new(bytes.Buffer) + buffer.WriteString(color.cssString()) + str := buffer.String() + if str != result { + t.Errorf("color = %#X, expected = \"%s\", result = \"%s\"", color, result, str) + } + } + + buffer := new(bytes.Buffer) + color := Color(0x7FFE8743) + color.writeData(buffer) + str := buffer.String() + if str != "#7FFE8743" { + t.Errorf(`color = %#X, expected = "#7FFE8743", result = "%s"`, color, str) + } + + testCSS(t, Color(0x7FFE8743), "rgba(254,135,67,.50)") + testCSS(t, Color(0xFFFE8743), "rgb(254,135,67)") + testCSS(t, Color(0x05FE8743), "rgba(254,135,67,.02)") +} + +func TestColorSetData(t *testing.T) { + test := func(t *testing.T, data string, result Color) { + color, ok := StringToColor(data) + if !ok { + t.Errorf("data = \"%s\", fail result", data) + } else if color != result { + t.Errorf("data = \"%s\", expected = %#X, result = %#X", data, result, color) + } + } + + test(t, "#7Ffe8743", 0x7FFE8743) + test(t, "#fE8743", 0xFFFE8743) + test(t, "#AE43", 0xAAEE4433) + test(t, "#E43", 0xFFEE4433) + + failData := []string{ + "", + "7FfeG743", + "#7Ffe87439", + "#7FfeG743", + "#7Ffe874", + "#feG743", + "#7Ffe8", + "#fG73", + "#GF3", + } + + for _, data := range failData { + if color, ok := StringToColor(data); ok { + t.Errorf("data = \"%s\", success, result = %#X", data, color) + } + } +} diff --git a/columnLayout.go b/columnLayout.go new file mode 100644 index 0000000..1682d5a --- /dev/null +++ b/columnLayout.go @@ -0,0 +1,222 @@ +package rui + +import ( + "strconv" + "strings" +) + +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" + // ColumnWidth is the constant for the "column-width" property tag. + // The "column-width" SizeUnit property specifies the width of each column. + ColumnWidth = "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" + // 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" + // 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" + // 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" + // 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" +) + +// ColumnLayout - grid-container of View +type ColumnLayout interface { + ViewsContainer +} + +type columnLayoutData struct { + viewsContainerData +} + +// NewColumnLayout create new ColumnLayout object and return it +func NewColumnLayout(session Session, params Params) ColumnLayout { + view := new(columnLayoutData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newColumnLayout(session Session) View { + return NewColumnLayout(session, nil) +} + +// 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) normalizeTag(tag string) string { + tag = strings.ToLower(tag) + switch tag { + case Gap: + return ColumnGap + } + return tag +} + +func (columnLayout *columnLayoutData) Get(tag string) interface{} { + 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) + switch tag { + case ColumnCount, ColumnWidth, ColumnGap: + updateCSSProperty(columnLayout.htmlID(), tag, "", columnLayout.Session()) + + case ColumnSeparator: + updateCSSProperty(columnLayout.htmlID(), "column-rule", "", columnLayout.Session()) + } +} + +func (columnLayout *columnLayoutData) Set(tag string, value interface{}) bool { + return columnLayout.set(columnLayout.normalizeTag(tag), value) +} + +func (columnLayout *columnLayoutData) set(tag string, value interface{}) bool { + if value == nil { + columnLayout.remove(tag) + return true + } + + switch tag { + case ColumnCount: + if columnLayout.setIntProperty(tag, value) { + session := columnLayout.Session() + if count, ok := intProperty(columnLayout, tag, session, 0); ok && count > 0 { + updateCSSProperty(columnLayout.htmlID(), tag, strconv.Itoa(count), session) + } else { + updateCSSProperty(columnLayout.htmlID(), tag, "auto", session) + } + return true + } + return false + } + + ok := columnLayout.viewsContainerData.set(tag, value) + if ok { + switch tag { + case ColumnSeparator: + css := "" + session := columnLayout.Session() + if val, ok := columnLayout.properties[ColumnSeparator]; ok { + separator := val.(ColumnSeparatorProperty) + css = separator.cssValue(columnLayout.Session()) + } + updateCSSProperty(columnLayout.htmlID(), "column-rule", css, session) + } + } + return ok +} + +// GetColumnCount returns int value which specifies number of columns into which the content of +// ColumnLayout is break. If the return value is 0 then the number of columns is calculated +// based on the "column-width" property. +// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned +func GetColumnCount(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return 0 + } + result, _ := intStyledProperty(view, ColumnCount, 0) + return result +} + +// GetColumnWidth returns SizeUnit value which specifies the width of each column of ColumnLayout. +// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned +func GetColumnWidth(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return AutoSize() + } + result, _ := sizeStyledProperty(view, ColumnWidth) + return result +} + +// GetColumnGap returns SizeUnit property which specifies the size of the gap (gutter) between columns of ColumnLayout. +// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned +func GetColumnGap(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return AutoSize() + } + result, _ := sizeStyledProperty(view, ColumnGap) + return result +} + +// GetColumnSeparator returns ViewBorder struct which specifies the line drawn between +// columns in a multi-column ColumnLayout. +// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned +func GetColumnSeparator(view View, subviewID string) ViewBorder { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + + if view != nil { + value := view.Get(ColumnSeparator) + if value == nil { + value, _ = valueFromStyle(view, ColumnSeparator) + } + + if value != nil { + if separator, ok := value.(ColumnSeparatorProperty); ok { + return separator.ViewBorder(view.Session()) + } + } + } + + return ViewBorder{} +} + +// ColumnSeparatorStyle returns int value which specifies 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). +// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned +func GetColumnSeparatorStyle(view View, subviewID string) int { + border := GetColumnSeparator(view, subviewID) + return border.Style +} + +// ColumnSeparatorWidth returns SizeUnit value which specifies the width of the line drawn between +// columns in a multi-column layout. +// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned +func GetColumnSeparatorWidth(view View, subviewID string) SizeUnit { + border := GetColumnSeparator(view, subviewID) + return border.Width +} + +// ColumnSeparatorColor returns Color value which specifies the color of the line drawn between +// columns in a multi-column layout. +// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned +func GetColumnSeparatorColor(view View, subviewID string) Color { + border := GetColumnSeparator(view, subviewID) + return border.Color +} diff --git a/columnSeparator.go b/columnSeparator.go new file mode 100644 index 0000000..8b86d13 --- /dev/null +++ b/columnSeparator.go @@ -0,0 +1,184 @@ +package rui + +import ( + "fmt" + "strings" +) + +// ColumnSeparatorProperty is the interface of a view separator data +type ColumnSeparatorProperty interface { + Properties + ruiStringer + fmt.Stringer + ViewBorder(session Session) ViewBorder + cssValue(session Session) string +} + +type columnSeparatorProperty struct { + propertyList +} + +func newColumnSeparatorProperty(value interface{}) ColumnSeparatorProperty { + + if value == nil { + separator := new(columnSeparatorProperty) + separator.properties = map[string]interface{}{} + return separator + } + + switch value := value.(type) { + case ColumnSeparatorProperty: + return value + + case DataObject: + separator := new(columnSeparatorProperty) + separator.properties = map[string]interface{}{} + for _, tag := range []string{Style, Width, ColorProperty} { + if val, ok := value.PropertyValue(tag); ok && val != "" { + separator.set(tag, value) + } + } + return separator + + case ViewBorder: + separator := new(columnSeparatorProperty) + separator.properties = map[string]interface{}{ + Style: value.Style, + Width: value.Width, + ColorProperty: value.Color, + } + return separator + } + + invalidPropertyValue(Border, value) + return nil +} + +// NewColumnSeparator creates the new ColumnSeparatorProperty +func NewColumnSeparator(params Params) ColumnSeparatorProperty { + separator := new(columnSeparatorProperty) + separator.properties = map[string]interface{}{} + if params != nil { + for _, tag := range []string{Style, Width, ColorProperty} { + if value, ok := params[tag]; ok && value != nil { + separator.Set(tag, value) + } + } + } + return separator +} + +func (separator *columnSeparatorProperty) normalizeTag(tag string) string { + tag = strings.ToLower(tag) + switch tag { + case ColumnSeparatorStyle, "separator-style": + return Style + + case ColumnSeparatorWidth, "separator-width": + return Width + + case ColumnSeparatorColor, "separator-color": + return ColorProperty + } + + return tag +} + +func (separator *columnSeparatorProperty) ruiString(writer ruiWriter) { + writer.startObject("_") + for _, tag := range []string{Style, Width, ColorProperty} { + if value, ok := separator.properties[tag]; ok { + writer.writeProperty(Style, value) + } + } + writer.endObject() +} + +func (separator *columnSeparatorProperty) String() string { + writer := newRUIWriter() + separator.ruiString(writer) + return writer.finish() +} + +func (separator *columnSeparatorProperty) Remove(tag string) { + + switch tag = separator.normalizeTag(tag); tag { + case Style, Width, ColorProperty: + delete(separator.properties, tag) + + default: + ErrorLogF(`"%s" property is not compatible with the ColumnSeparatorProperty`, tag) + } +} + +func (separator *columnSeparatorProperty) Set(tag string, value interface{}) 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 ColorProperty: + return separator.setColorProperty(ColorProperty, value) + } + + ErrorLogF(`"%s" property is not compatible with the ColumnSeparatorProperty`, tag) + return false +} + +func (separator *columnSeparatorProperty) Get(tag string) interface{} { + tag = separator.normalizeTag(tag) + + if result, ok := separator.properties[tag]; ok { + return result + } + + return nil +} + +func (separator *columnSeparatorProperty) ViewBorder(session Session) ViewBorder { + style, _ := valueToEnum(separator.getRaw(Style), BorderStyle, session, NoneLine) + width, _ := sizeProperty(separator, Width, session) + color, _ := colorProperty(separator, ColorProperty, session) + + return ViewBorder{ + Style: style, + Width: width, + Color: color, + } +} + +func (separator *columnSeparatorProperty) cssValue(session Session) string { + value := separator.ViewBorder(session) + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + if value.Width.Type != Auto && value.Width.Type != SizeInFraction && value.Width.Value > 0 { + buffer.WriteString(value.Width.cssString("")) + } + + styles := enumProperties[BorderStyle].cssValues + if value.Style > 0 && value.Style < len(styles) { + if buffer.Len() > 0 { + buffer.WriteRune(' ') + } + buffer.WriteString(styles[value.Style]) + } + + if value.Color != 0 { + if buffer.Len() > 0 { + buffer.WriteRune(' ') + } + buffer.WriteString(value.Color.cssString()) + } + + return buffer.String() +} diff --git a/cssBuilder.go b/cssBuilder.go new file mode 100644 index 0000000..bd2ac71 --- /dev/null +++ b/cssBuilder.go @@ -0,0 +1,258 @@ +package rui + +import ( + "strings" +) + +var systemStyles = map[string]string{ + "ruiApp": "body", + "ruiDefault": "div", + "ruiArticle": "article", + "ruiSection": "section", + "ruiAside": "aside", + "ruiHeader": "header", + "ruiMain": "main", + "ruiFooter": "footer", + "ruiNavigation": "nav", + "ruiFigure": "figure", + "ruiFigureCaption": "figcaption", + "ruiButton": "button", + "ruiP": "p", + "ruiParagraph": "p", + "ruiH1": "h1", + "ruiH2": "h2", + "ruiH3": "h3", + "ruiH4": "h4", + "ruiH5": "h5", + "ruiH6": "h6", + "ruiBlockquote": "blockquote", + "ruiCode": "code", + "ruiTable": "table", + "ruiTableHead": "thead", + "ruiTableFoot": "tfoot", + "ruiTableRow": "tr", + "ruiTableColumn": "col", + "ruiTableCell": "td", + "ruiDropDownList": "select", + "ruiDropDownListItem": "option", +} + +var disabledStyles = []string{ + "ruiRoot", + "ruiPopupLayer", + "ruiAbsoluteLayout", + "ruiGridLayout", + "ruiListLayout", + "ruiStackLayout", + "ruiStackPageLayout", + "ruiTabsLayout", + "ruiImageView", + "ruiListView", +} + +type cssBuilder interface { + add(key, value string) + addValues(key, separator string, values ...string) +} + +type viewCSSBuilder struct { + buffer *strings.Builder +} + +type cssValueBuilder struct { + buffer *strings.Builder +} + +type cssStyleBuilder struct { + buffer *strings.Builder + media bool +} + +func (builder *viewCSSBuilder) finish() string { + if builder.buffer == nil { + return "" + } + + result := builder.buffer.String() + freeStringBuilder(builder.buffer) + builder.buffer = nil + return result +} + +func (builder *viewCSSBuilder) add(key, value string) { + if value != "" { + if builder.buffer == nil { + builder.buffer = allocStringBuilder() + } else if builder.buffer.Len() > 0 { + builder.buffer.WriteRune(' ') + } + + builder.buffer.WriteString(key) + builder.buffer.WriteString(": ") + builder.buffer.WriteString(value) + builder.buffer.WriteRune(';') + } +} + +func (builder *viewCSSBuilder) addValues(key, separator string, values ...string) { + if len(values) == 0 { + return + } + + if builder.buffer == nil { + builder.buffer = allocStringBuilder() + } else if builder.buffer.Len() > 0 { + builder.buffer.WriteRune(' ') + } + + builder.buffer.WriteString(key) + builder.buffer.WriteString(": ") + for i, value := range values { + if i > 0 { + builder.buffer.WriteString(separator) + } + builder.buffer.WriteString(value) + } + builder.buffer.WriteRune(';') +} + +func (builder *cssValueBuilder) finish() string { + if builder.buffer == nil { + return "" + } + + result := builder.buffer.String() + freeStringBuilder(builder.buffer) + builder.buffer = nil + return result +} + +func (builder *cssValueBuilder) add(key, value string) { + if value != "" { + if builder.buffer == nil { + builder.buffer = allocStringBuilder() + } + builder.buffer.WriteString(value) + } +} + +func (builder *cssValueBuilder) addValues(key, separator string, values ...string) { + if len(values) > 0 { + if builder.buffer == nil { + builder.buffer = allocStringBuilder() + } + for i, value := range values { + if i > 0 { + builder.buffer.WriteString(separator) + } + builder.buffer.WriteString(value) + } + } +} + +func (builder *cssStyleBuilder) init() { + builder.buffer = allocStringBuilder() + builder.buffer.Grow(16 * 1024) +} + +func (builder *cssStyleBuilder) finish() string { + if builder.buffer == nil { + return "" + } + + result := builder.buffer.String() + freeStringBuilder(builder.buffer) + builder.buffer = nil + return result +} + +func (builder *cssStyleBuilder) startMedia(rule string) { + if builder.buffer == nil { + builder.init() + } + builder.buffer.WriteString(`@media screen`) + builder.buffer.WriteString(rule) + builder.buffer.WriteString(` {\n`) + builder.media = true +} + +func (builder *cssStyleBuilder) endMedia() { + if builder.buffer == nil { + builder.init() + } + builder.buffer.WriteString(`}\n`) + builder.media = false +} + +func (builder *cssStyleBuilder) startStyle(name string) { + for _, disabledName := range disabledStyles { + if name == disabledName { + return + } + } + + if builder.buffer == nil { + builder.init() + } + if builder.media { + builder.buffer.WriteString(`\t`) + } + + if sysName, ok := systemStyles[name]; ok { + builder.buffer.WriteString(sysName) + } else { + builder.buffer.WriteRune('.') + builder.buffer.WriteString(name) + } + + builder.buffer.WriteString(` {\n`) +} + +func (builder *cssStyleBuilder) endStyle() { + if builder.buffer == nil { + builder.init() + } + if builder.media { + builder.buffer.WriteString(`\t`) + } + builder.buffer.WriteString(`}\n`) +} + +func (builder *cssStyleBuilder) add(key, value string) { + if value != "" { + if builder.buffer == nil { + builder.init() + } + if builder.media { + builder.buffer.WriteString(`\t`) + } + builder.buffer.WriteString(`\t`) + builder.buffer.WriteString(key) + builder.buffer.WriteString(`: `) + builder.buffer.WriteString(value) + builder.buffer.WriteString(`;\n`) + } +} + +func (builder *cssStyleBuilder) addValues(key, separator string, values ...string) { + if len(values) == 0 { + return + } + + if builder.buffer == nil { + builder.init() + } + if builder.media { + builder.buffer.WriteString(`\t`) + } + builder.buffer.WriteString(`\t`) + builder.buffer.WriteString(key) + builder.buffer.WriteString(`: `) + for i, value := range values { + if i > 0 { + builder.buffer.WriteString(separator) + } + builder.buffer.WriteString(value) + } + builder.buffer.WriteString(`;\n`) +} diff --git a/customView.go b/customView.go new file mode 100644 index 0000000..a4dc457 --- /dev/null +++ b/customView.go @@ -0,0 +1,261 @@ +package rui + +import "strings" + +// CustomView defines a custom view interface +type CustomView interface { + ViewsContainer + CreateSuperView(session Session) View + SuperView() View + setSuperView(view View) + setTag(tag string) +} + +// CustomViewData defines a data of a basic custom view +type CustomViewData struct { + tag string + superView View +} + +// InitCustomView initializes fields of CustomView by default values +func InitCustomView(customView CustomView, tag string, session Session, params Params) bool { + customView.setTag(tag) + if view := customView.CreateSuperView(session); view != nil { + customView.setSuperView(view) + setInitParams(customView, params) + } else { + ErrorLog(`nil SuperView of "` + tag + `" view`) + return false + } + return true +} + +// SuperView returns a super view +func (customView *CustomViewData) SuperView() View { + return customView.superView +} + +func (customView *CustomViewData) setSuperView(view View) { + customView.superView = view +} + +func (customView *CustomViewData) setTag(tag string) { + customView.tag = tag +} + +// 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) interface{} { + return customView.superView.Get(tag) +} + +func (customView *CustomViewData) getRaw(tag string) interface{} { + return customView.superView.getRaw(tag) +} + +func (customView *CustomViewData) setRaw(tag string, value interface{}) { + customView.superView.setRaw(tag, value) +} + +// 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 interface{}) bool { + return customView.superView.Set(tag, value) +} + +func (customView *CustomViewData) SetAnimated(tag string, value interface{}, animation Animation) bool { + return customView.superView.SetAnimated(tag, value, animation) +} + +// Remove removes the property with name defined by the argument +func (customView *CustomViewData) Remove(tag string) { + customView.superView.Remove(tag) +} + +// AllTags returns an array of the set properties +func (customView *CustomViewData) AllTags() []string { + return customView.superView.AllTags() +} + +// Clear removes all properties +func (customView *CustomViewData) Clear() { + customView.superView.Clear() +} + +// Init initializes fields of View by default values +func (customView *CustomViewData) Init(session Session) { +} + +// Session returns a current Session interface +func (customView *CustomViewData) Session() Session { + return customView.superView.Session() +} + +// Parent returns a parent view +func (customView *CustomViewData) Parent() View { + return customView.superView.Parent() +} + +func (customView *CustomViewData) parentHTMLID() string { + return customView.superView.parentHTMLID() +} + +func (customView *CustomViewData) setParentID(parentID string) { + customView.superView.setParentID(parentID) +} + +// Tag returns a tag of View interface +func (customView *CustomViewData) Tag() string { + if customView.tag != "" { + return customView.tag + } + return customView.superView.Tag() +} + +// ID returns a id of the view +func (customView *CustomViewData) ID() string { + return customView.superView.ID() +} + +// Focusable returns true if the view receives the focus +func (customView *CustomViewData) Focusable() bool { + return customView.superView.Focusable() +} + +/* +// SetTransitionEndListener sets the new listener of the transition end event +func (customView *CustomViewData) SetTransitionEndListener(property string, listener TransitionEndListener) { + customView.superView.SetTransitionEndListener(property, listener) +} + +// SetTransitionEndFunc sets the new listener function of the transition end event +func (customView *CustomViewData) SetTransitionEndFunc(property string, listenerFunc func(View, string)) { + customView.superView.SetTransitionEndFunc(property, listenerFunc) +} +*/ + +// Frame returns a location and size of the view in pixels +func (customView *CustomViewData) Frame() Frame { + return customView.superView.Frame() +} + +func (customView *CustomViewData) Scroll() Frame { + return customView.superView.Scroll() +} + +func (customView *CustomViewData) onResize(self View, x, y, width, height float64) { + customView.superView.onResize(customView.superView, x, y, width, height) +} + +func (customView *CustomViewData) onItemResize(self View, index int, x, y, width, height float64) { + customView.superView.onItemResize(customView.superView, index, x, y, width, height) +} + +func (customView *CustomViewData) handleCommand(self View, command string, data DataObject) bool { + return customView.superView.handleCommand(customView.superView, command, data) +} + +func (customView *CustomViewData) htmlClass(disabled bool) string { + return customView.superView.htmlClass(disabled) +} + +func (customView *CustomViewData) htmlTag() string { + return customView.superView.htmlTag() +} + +func (customView *CustomViewData) closeHTMLTag() bool { + return customView.superView.closeHTMLTag() +} + +func (customView *CustomViewData) htmlID() string { + return customView.superView.htmlID() +} + +func (customView *CustomViewData) htmlSubviews(self View, buffer *strings.Builder) { + customView.superView.htmlSubviews(customView.superView, buffer) +} + +func (customView *CustomViewData) htmlProperties(self View, buffer *strings.Builder) { + customView.superView.htmlProperties(customView.superView, buffer) +} + +func (customView *CustomViewData) htmlDisabledProperties(self View, buffer *strings.Builder) { + customView.superView.htmlDisabledProperties(customView.superView, buffer) +} + +func (customView *CustomViewData) cssStyle(self View, builder cssBuilder) { + customView.superView.cssStyle(customView.superView, builder) +} + +func (customView *CustomViewData) addToCSSStyle(addCSS map[string]string) { + customView.superView.addToCSSStyle(addCSS) +} + +func (customView *CustomViewData) setNoResizeEvent() { + customView.superView.setNoResizeEvent() +} + +func (customView *CustomViewData) isNoResizeEvent() bool { + return customView.superView.isNoResizeEvent() +} + +// Views return a list of child views +func (customView *CustomViewData) Views() []View { + if customView.superView != nil { + if container, ok := customView.superView.(ViewsContainer); ok { + return container.Views() + } + } + return []View{} +} + +// Append appends a view to the end of the list of a view children +func (customView *CustomViewData) Append(view View) { + if customView.superView != nil { + if container, ok := customView.superView.(ViewsContainer); ok { + container.Append(view) + } + } +} + +// Insert inserts a view to the "index" position in the list of a view children +func (customView *CustomViewData) Insert(view View, index uint) { + if customView.superView != nil { + if container, ok := customView.superView.(ViewsContainer); ok { + container.Insert(view, index) + } + } +} + +// Remove removes a view from the list of a view children and return it +func (customView *CustomViewData) RemoveView(index uint) View { + if customView.superView != nil { + if container, ok := customView.superView.(ViewsContainer); ok { + return container.RemoveView(index) + } + } + + return nil +} + +func (customView *CustomViewData) String() string { + if customView.superView != nil { + writer := newRUIWriter() + customView.ruiString(writer) + return writer.finish() + } + return customView.tag + " { }" +} + +func (customView *CustomViewData) ruiString(writer ruiWriter) { + if customView.superView != nil { + ruiViewString(customView.superView, customView.tag, writer) + } +} + +func (customView *CustomViewData) setScroll(x, y, width, height float64) { + if customView.superView != nil { + customView.superView.setScroll(x, y, width, height) + } +} diff --git a/data.go b/data.go new file mode 100644 index 0000000..0f26698 --- /dev/null +++ b/data.go @@ -0,0 +1,631 @@ +package rui + +import ( + "strings" + "unicode" +) + +// DataValue interface of a data node value +type DataValue interface { + IsObject() bool + Object() DataObject + Value() string +} + +// DataObject interface of a data object +type DataObject interface { + DataValue + Tag() string + PropertyCount() int + Property(index int) DataNode + PropertyWithTag(tag string) DataNode + PropertyValue(tag string) (string, bool) + PropertyObject(tag string) DataObject + SetPropertyValue(tag, value string) + SetPropertyObject(tag string, object DataObject) +} + +const ( + // TextNode - node is the pair "tag - text value". Syntax: = + TextNode = 0 + // ObjectNode - node is the pair "tag - object". Syntax: = {...} + ObjectNode = 1 + // ArrayNode - node is the pair "tag - object". Syntax: = [...] + ArrayNode = 2 +) + +// DataNode interface of a data node +type DataNode interface { + Tag() string + Type() int + Text() string + Object() DataObject + ArraySize() int + ArrayElement(index int) DataValue + ArrayElements() []DataValue +} + +/******************************************************************************/ +type dataStringValue struct { + value string +} + +func (value *dataStringValue) Value() string { + return value.value +} + +func (value *dataStringValue) IsObject() bool { + return false +} + +func (value *dataStringValue) Object() DataObject { + return nil +} + +/******************************************************************************/ +type dataObject struct { + tag string + property []DataNode +} + +// NewDataObject create new DataObject with the tag and empty property list +func NewDataObject(tag string) DataObject { + obj := new(dataObject) + obj.tag = tag + obj.property = []DataNode{} + return obj +} + +func (object *dataObject) Value() string { + return "" +} + +func (object *dataObject) IsObject() bool { + return true +} + +func (object *dataObject) Object() DataObject { + return object +} + +func (object *dataObject) Tag() string { + return object.tag +} + +func (object *dataObject) PropertyCount() int { + if object.property != nil { + return len(object.property) + } + return 0 +} + +func (object *dataObject) Property(index int) DataNode { + if object.property == nil || index < 0 || index >= len(object.property) { + return nil + } + return object.property[index] +} + +func (object *dataObject) PropertyWithTag(tag string) DataNode { + if object.property != nil { + for _, node := range object.property { + if node.Tag() == tag { + return node + } + } + } + return nil +} + +func (object *dataObject) PropertyValue(tag string) (string, bool) { + if node := object.PropertyWithTag(tag); node != nil && node.Type() == TextNode { + return node.Text(), true + } + return "", false +} + +func (object *dataObject) PropertyObject(tag string) DataObject { + if node := object.PropertyWithTag(tag); node != nil && node.Type() == ObjectNode { + return node.Object() + } + return nil +} + +func (object *dataObject) setNode(node DataNode) { + if object.property == nil || len(object.property) == 0 { + object.property = []DataNode{node} + } else { + tag := node.Tag() + for i, p := range object.property { + if p.Tag() == tag { + object.property[i] = node + return + } + } + + object.property = append(object.property, node) + } +} + +// SetPropertyValue - set a string property with tag by value +func (object *dataObject) SetPropertyValue(tag, value string) { + val := new(dataStringValue) + val.value = value + node := new(dataNode) + node.tag = tag + node.value = val + object.setNode(node) +} + +// SetPropertyObject - set a property with tag by object +func (object *dataObject) SetPropertyObject(tag string, obj DataObject) { + node := new(dataNode) + node.tag = tag + node.value = obj + object.setNode(node) +} + +/******************************************************************************/ +type dataNode struct { + tag string + value DataValue + array []DataValue +} + +func (node *dataNode) Tag() string { + return node.tag +} + +func (node *dataNode) Type() int { + if node.array != nil { + return ArrayNode + } + if node.value.IsObject() { + return ObjectNode + } + return TextNode +} + +func (node *dataNode) Text() string { + if node.value != nil { + return node.value.Value() + } + return "" +} + +func (node *dataNode) Object() DataObject { + if node.value != nil { + return node.value.Object() + } + return nil +} + +func (node *dataNode) ArraySize() int { + if node.array != nil { + return len(node.array) + } + return 0 +} + +func (node *dataNode) ArrayElement(index int) DataValue { + if node.array != nil && index >= 0 && index < len(node.array) { + return node.array[index] + } + return nil +} + +func (node *dataNode) ArrayElements() []DataValue { + if node.array != nil { + return node.array + } + return []DataValue{} +} + +// ParseDataText - parse text and return DataNode +func ParseDataText(text string) DataObject { + + if strings.ContainsAny(text, "\r") { + text = strings.Replace(text, "\r\n", "\n", -1) + text = strings.Replace(text, "\r", "\n", -1) + } + data := append([]rune(text), rune(0)) + pos := 0 + size := len(data) - 1 + line := 1 + lineStart := 0 + + skipSpaces := func(skipNewLine bool) { + for pos < size { + switch data[pos] { + case '\n': + if !skipNewLine { + return + } + line++ + lineStart = pos + 1 + + case '/': + if pos+1 < size { + switch data[pos+1] { + case '/': + pos += 2 + for pos < size && data[pos] != '\n' { + pos++ + } + pos-- + + case '*': + pos += 3 + for { + if pos >= size { + ErrorLog("Unexpected end of file") + return + } + if data[pos-1] == '*' && data[pos] == '/' { + break + } + if data[pos-1] == '\n' { + line++ + lineStart = pos + } + pos++ + } + + default: + return + } + } + + case ' ', '\t': + // do nothing + + default: + if !unicode.IsSpace(data[pos]) { + return + } + } + pos++ + } + } + + parseTag := func() (string, bool) { + skipSpaces(true) + startPos := pos + if data[pos] == '`' { + pos++ + startPos++ + for data[pos] != '`' { + pos++ + if pos >= size { + ErrorLog("Unexpected end of text") + return string(data[startPos:size]), false + } + } + str := string(data[startPos:pos]) + pos++ + return str, true + + } else if data[pos] == '\'' || data[pos] == '"' { + + stopSymbol := data[pos] + pos++ + startPos++ + slash := false + for stopSymbol != data[pos] { + if data[pos] == '\\' { + pos += 2 + slash = true + } else { + pos++ + } + if pos >= size { + ErrorLog("Unexpected end of text") + return string(data[startPos:size]), false + } + } + + if !slash { + str := string(data[startPos:pos]) + pos++ + skipSpaces(false) + return str, true + } + + buffer := make([]rune, pos-startPos+1) + n1 := 0 + n2 := startPos + + invalidEscape := func() (string, bool) { + str := string(data[startPos:pos]) + pos++ + ErrorLogF("Invalid escape sequence in \"%s\" (position %d)", str, n2-2-startPos) + return str, false + } + + for n2 < pos { + if data[n2] != '\\' { + buffer[n1] = data[n2] + n2++ + } else { + n2 += 2 + switch data[n2-1] { + case 'n': + buffer[n1] = '\n' + + case 'r': + buffer[n1] = '\r' + + case 't': + buffer[n1] = '\t' + + case '"': + buffer[n1] = '"' + + case '\'': + buffer[n1] = '\'' + + case '\\': + buffer[n1] = '\\' + + case 'x', 'X': + if n2+2 > pos { + return invalidEscape() + } + x := 0 + for i := 0; i < 2; i++ { + ch := data[n2] + if ch >= '0' && ch <= '9' { + x = x*16 + int(ch-'0') + } else if ch >= 'a' && ch <= 'f' { + x = x*16 + int(ch-'a'+10) + } else if ch >= 'A' && ch <= 'F' { + x = x*16 + int(ch-'A'+10) + } else { + return invalidEscape() + } + n2++ + } + buffer[n1] = rune(x) + + case 'u', 'U': + if n2+4 > pos { + return invalidEscape() + } + x := 0 + for i := 0; i < 4; i++ { + ch := data[n2] + if ch >= '0' && ch <= '9' { + x = x*16 + int(ch-'0') + } else if ch >= 'a' && ch <= 'f' { + x = x*16 + int(ch-'a'+10) + } else if ch >= 'A' && ch <= 'F' { + x = x*16 + int(ch-'A'+10) + } else { + return invalidEscape() + } + n2++ + } + buffer[n1] = rune(x) + + default: + str := string(data[startPos:pos]) + ErrorLogF("Invalid escape sequence in \"%s\" (position %d)", str, n2-2-startPos) + return str, false + } + } + n1++ + } + + pos++ + skipSpaces(false) + return string(buffer[0:n1]), true + } + + stopSymbol := func(symbol rune) bool { + if unicode.IsSpace(symbol) { + return true + } + for _, sym := range []rune{'=', '{', '}', '[', ']', ',', ' ', '\t', '\n', '\'', '"', '`', '/'} { + if sym == symbol { + return true + } + } + return false + } + + for pos < size && !stopSymbol(data[pos]) { + pos++ + } + + endPos := pos + skipSpaces(false) + if startPos == endPos { + ErrorLog("empty tag") + return "", false + } + return string(data[startPos:endPos]), true + } + + var parseObject func(tag string) DataObject + var parseArray func() []DataValue + + parseNode := func() DataNode { + var tag string + var ok bool + + if tag, ok = parseTag(); !ok { + return nil + } + + skipSpaces(true) + if data[pos] != '=' { + ErrorLogF("expected '=' after a tag name (line: %d, position: %d)", line, pos-lineStart) + return nil + } + + pos++ + skipSpaces(true) + switch data[pos] { + case '[': + node := new(dataNode) + node.tag = tag + + if node.array = parseArray(); node.array == nil { + return nil + } + return node + + case '{': + node := new(dataNode) + node.tag = tag + if node.value = parseObject("_"); node.value == nil { + return nil + } + return node + + case '}', ']', '=': + ErrorLogF("Expected '[', '{' or a tag name after '=' (line: %d, position: %d)", line, pos-lineStart) + return nil + + default: + var str string + if str, ok = parseTag(); !ok { + return nil + } + + node := new(dataNode) + node.tag = tag + + if data[pos] == '{' { + if node.value = parseObject(str); node.value == nil { + return nil + } + } else { + val := new(dataStringValue) + val.value = str + node.value = val + } + + return node + } + } + + parseObject = func(tag string) DataObject { + if data[pos] != '{' { + ErrorLogF("Expected '{' (line: %d, position: %d)", line, pos-lineStart) + return nil + } + pos++ + + obj := new(dataObject) + obj.tag = tag + obj.property = []DataNode{} + + for pos < size { + var node DataNode + + skipSpaces(true) + if data[pos] == '}' { + pos++ + skipSpaces(false) + return obj + } + + if node = parseNode(); node == nil { + return nil + } + obj.property = append(obj.property, node) + if data[pos] == '}' { + pos++ + skipSpaces(true) + return obj + } else if data[pos] != ',' && data[pos] != '\n' { + ErrorLogF(`Expected '}', '\n' or ',' (line: %d, position: %d)`, line, pos-lineStart) + return nil + } + if data[pos] != '\n' { + pos++ + } + skipSpaces(true) + for data[pos] == ',' { + pos++ + skipSpaces(true) + } + } + + ErrorLog("Unexpected end of text") + return nil + } + + parseArray = func() []DataValue { + pos++ + skipSpaces(true) + + array := []DataValue{} + + for pos < size { + var tag string + var ok bool + + skipSpaces(true) + for data[pos] == ',' && pos < size { + pos++ + skipSpaces(true) + } + + if pos >= size { + break + } + + if data[pos] == ']' { + pos++ + skipSpaces(true) + return array + } + + if tag, ok = parseTag(); !ok { + return nil + } + + if data[pos] == '{' { + obj := parseObject(tag) + if obj == nil { + return nil + } + array = append(array, obj) + } else { + val := new(dataStringValue) + val.value = tag + array = append(array, val) + } + + switch data[pos] { + case ']', ',', '\n': + + default: + ErrorLogF("Expected ']' or ',' (line: %d, position: %d)", line, pos-lineStart) + return nil + } + + /* + if data[pos] == ']' { + pos++ + skipSpaces() + return array, nil + } else if data[pos] != ',' { + return nil, fmt.Errorf("Expected ']' or ',' (line: %d, position: %d)", line, pos-lineStart) + } + pos++ + skipSpaces() + */ + } + + ErrorLog("Unexpected end of text") + return nil + } + + if tag, ok := parseTag(); ok { + return parseObject(tag) + } + return nil +} diff --git a/dataWriter_test.go b/dataWriter_test.go new file mode 100644 index 0000000..1207f4a --- /dev/null +++ b/dataWriter_test.go @@ -0,0 +1,66 @@ +package rui + +/* +import ( + "testing" +) + +func TestDataWriter(t *testing.T) { + w := NewDataWriter() + w.StartObject("root") + w.WriteStringKey("key1", "text") + w.WriteStringKey("key2", "text 2") + w.WriteStringKey("key 3", "text4") + w.WriteStringsKey("key4", []string{"text4.1", "text4.2", "text4.3"}, '|') + w.WriteStringsKey("key5", []string{"text5.1", "text5.2", "text5.3"}, ',') + w.WriteColorKey("color", Color(0x7FD18243)) + w.WriteColorsKey("colors", []Color{Color(0x7FD18243), Color(0xFF817263)}, ',') + w.WriteIntKey("int", 43) + w.WriteIntsKey("ints", []int{111, 222, 333}, '|') + + w.StartObjectKey("obj", "xxx") + w.WriteSizeUnitKey("size", Px(16)) + w.WriteSizeUnitsKey("sizes", []SizeUnit{Px(8), Percent(100)}, ',') + w.StartArray("array") + w.WriteStringToArray("text") + w.WriteColorToArray(Color(0x23456789)) + w.WriteIntToArray(1) + w.WriteSizeUnitToArray(Inch(2)) + w.FinishArray() + w.WriteBoundsKey("bounds1", Bounds{Px(8), Px(8), Px(8), Px(8)}) + w.WriteBoundsKey("bounds2", Bounds{Px(8), Pt(12), Mm(4.5), Inch(1.2)}) + w.FinishObject() // xxx + + w.FinishObject() // root + + text := w.String() + expected := `root { + key1 = text, + key2 = "text 2", + "key 3" = text4, + key4 = text4.1|text4.2|text4.3, + key5 = "text5.1,text5.2,text5.3", + color = #7FD18243, + colors = "#7FD18243,#FF817263", + int = 43, + ints = 111|222|333, + obj = xxx { + size = 16px, + sizes = "8px,100%", + array = [ + text, + #23456789, + 1, + 2in + ], + bounds1 = 8px, + bounds2 = "8px,12pt,4.5mm,1.2in" + } +}` + + if text != expected { + t.Error("DataWriter test fail. Result:\n`" + text + "`\nExpected:\n`" + expected + "`") + } + +} +*/ diff --git a/data_test.go b/data_test.go new file mode 100644 index 0000000..806e318 --- /dev/null +++ b/data_test.go @@ -0,0 +1,211 @@ +package rui + +import ( + "testing" +) + +func TestParseDataText(t *testing.T) { + + SetErrorLog(func(text string) { + t.Error(text) + }) + + text := `obj1 { + key1 = val1, + key2=obj2{ + key2.1=[val2.1,obj2.2{}, obj2.3{}], + "key 2.2"='val 2.2' + // Comment + key2.3/* comment */ = { + } + /* + Multiline comment + */ + 'key2.4' = obj2.3{ text = " "}, + key2.5= [], + }, + key3 = "\n \t \\ \r \" ' \X4F\x4e \U01Ea",` + + "key4=`" + `\n \t \\ \r \" ' \x8F \UF80a` + "`\r}" + + obj := ParseDataText(text) + if obj != nil { + if obj.Tag() != "obj1" { + t.Error(`obj.Tag() != "obj1"`) + } + if !obj.IsObject() { + t.Error(`!obj.IsObject()`) + } + if obj.PropertyCount() != 4 { + t.Error(`obj.PropertyCount() != 4`) + } + + if obj.Property(-1) != nil { + t.Error(`obj.Property(-1) != nil`) + } + + if val, ok := obj.PropertyValue("key1"); !ok || val != "val1" { + t.Errorf(`obj.PropertyValue("key1") result: ("%s",%v)`, val, ok) + } + + if val, ok := obj.PropertyValue("key3"); !ok || val != "\n \t \\ \r \" ' \x4F\x4e \u01Ea" { + t.Errorf(`obj.PropertyValue("key3") result: ("%s",%v)`, val, ok) + } + + if val, ok := obj.PropertyValue("key4"); !ok || val != `\n \t \\ \r \" ' \x8F \UF80a` { + t.Errorf(`obj.PropertyValue("key4") result: ("%s",%v)`, val, ok) + } + + if o := obj.PropertyObject("key2"); o == nil { + t.Error(`obj.PropertyObject("key2") == nil`) + } + + if o := obj.PropertyObject("key1"); o != nil { + t.Error(`obj.PropertyObject("key1") != nil`) + } + + if o := obj.PropertyObject("key5"); o != nil { + t.Error(`obj.PropertyObject("key5") != nil`) + } + + if val, ok := obj.PropertyValue("key2"); ok { + t.Errorf(`obj.PropertyValue("key2") result: ("%s",%v)`, val, ok) + } + + if val, ok := obj.PropertyValue("key5"); ok { + t.Errorf(`obj.PropertyValue("key5") result: ("%s",%v)`, val, ok) + } + + testKey := func(obj DataObject, index int, tag string, nodeType int) DataNode { + key := obj.Property(index) + if key == nil { + t.Errorf(`%s.Property(%d) == nil`, obj.Tag(), index) + } else { + if key.Tag() != tag { + t.Errorf(`%s.Property(%d).Tag() != "%s"`, obj.Tag(), index, tag) + } + + if key.Type() != nodeType { + switch nodeType { + case TextNode: + t.Errorf(`%s.Property(%d) is not text`, obj.Tag(), index) + + case ObjectNode: + t.Errorf(`%s.Property(%d) is not object`, obj.Tag(), index) + + case ArrayNode: + t.Errorf(`%s.Property(%d) is not array`, obj.Tag(), index) + } + } + } + + return key + } + + if key := testKey(obj, 0, "key1", TextNode); key != nil { + if key.Text() != "val1" { + t.Error(`key1.Value() != "val1"`) + } + } + + if key := testKey(obj, 1, "key2", ObjectNode); key != nil { + o := key.Object() + if o == nil { + t.Error(`key2.Value().Object() == nil`) + } else { + if o.PropertyCount() != 5 { + t.Error(`key2.Value().Object().PropertyCount() != 4`) + } + + type testKeyData struct { + tag string + nodeType int + } + + data := []testKeyData{ + {tag: "key2.1", nodeType: ArrayNode}, + {tag: "key 2.2", nodeType: TextNode}, + {tag: "key2.3", nodeType: ObjectNode}, + {tag: "key2.4", nodeType: ObjectNode}, + {tag: "key2.5", nodeType: ArrayNode}, + } + + for i, d := range data { + testKey(o, i, d.tag, d.nodeType) + } + } + } + + node1 := obj.Property(1) + if node1 == nil { + t.Error("obj.Property(1) != nil") + } else if node1.Type() != ObjectNode { + t.Error("obj.Property(1).Type() != ObjectNode") + } else if obj := node1.Object(); obj != nil { + if key := obj.Property(0); key != nil { + if key.Type() != ArrayNode { + t.Error("obj.Property(1).Object().Property(0)..Type() != ArrayNode") + } else { + if key.ArraySize() != 3 { + t.Error("obj.Property(1).Object().Property(0).ArraySize() != 3") + } + + if e := key.ArrayElement(0); e == nil { + t.Error("obj.Property(1).Object().Property(0).ArrayElement(0) == nil") + } else if e.IsObject() { + t.Error("obj.Property(1).Object().Property(0).ArrayElement(0).IsObject() == true") + } + + if e := key.ArrayElement(2); e == nil { + t.Error("obj.Property(1).Object().Property(0).ArrayElement(2) == nil") + } else if !e.IsObject() { + t.Error("obj.Property(1).Object().Property(0).ArrayElement(2).IsObject() == false") + } else if e.Value() != "" { + t.Error(`obj.Property(1).Object().Property(0).ArrayElement(2).Value() != ""`) + } + + if e := key.ArrayElement(3); e != nil { + t.Error("obj.Property(1).Object().Property(0).ArrayElement(3) != nil") + } + } + } + } else { + t.Error("obj.Property(1).Object() == nil") + } + } + + SetErrorLog(func(text string) { + }) + + failText := []string{ + " ", + "obj[]", + "obj={}", + "obj{key}", + "obj{key=}", + "obj{key=val", + "obj{key=obj2{}", + "obj{key=obj2{key2}}", + "obj{key=\"val}", + "obj{key=val\"}", + "obj{key=\"val`}", + "obj{key=[}}", + "obj{key=[val", + "obj{key=[val,", + "obj{key=[obj2{]", + `obj{key="""}`, + `obj{key="\z"}`, + `obj{key="\xG6"}`, + `obj{key="\uG678"}`, + `obj{key="\x6"}`, + `obj{key="\u678"}`, + `obj{key1=val1 key2=val2}`, + `obj{key=//"\u678"}`, + `obj{key="\u678" /*}`, + } + + for _, txt := range failText { + if obj := ParseDataText(txt); obj != nil { + t.Errorf("result ParseDataText(\"%s\") must be fail", txt) + } + } +} diff --git a/datePicker.go b/datePicker.go new file mode 100644 index 0000000..a2310f9 --- /dev/null +++ b/datePicker.go @@ -0,0 +1,404 @@ +package rui + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +const ( + DateChangedEvent = "date-changed" + DatePickerMin = "date-picker-min" + DatePickerMax = "date-picker-max" + DatePickerStep = "date-picker-step" + DatePickerValue = "date-picker-value" + dateFormat = "2006-01-02" +) + +// DatePicker - DatePicker view +type DatePicker interface { + View +} + +type datePickerData struct { + viewData + dateChangedListeners []func(DatePicker, time.Time) +} + +// NewDatePicker create new DatePicker object and return it +func NewDatePicker(session Session, params Params) DatePicker { + view := new(datePickerData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newDatePicker(session Session) View { + return NewDatePicker(session, nil) +} + +func (picker *datePickerData) Init(session Session) { + picker.viewData.Init(session) + picker.tag = "DatePicker" + picker.dateChangedListeners = []func(DatePicker, time.Time){} +} + +func (picker *datePickerData) normalizeTag(tag string) string { + tag = strings.ToLower(tag) + switch tag { + case Type, Min, Max, Step, Value: + return "date-picker-" + tag + } + + return 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){} + } + + case DatePickerMin: + delete(picker.properties, DatePickerMin) + removeProperty(picker.htmlID(), Min, picker.session) + + case DatePickerMax: + delete(picker.properties, DatePickerMax) + removeProperty(picker.htmlID(), Max, picker.session) + + case DatePickerStep: + delete(picker.properties, DatePickerMax) + removeProperty(picker.htmlID(), Step, picker.session) + + case DatePickerValue: + delete(picker.properties, DatePickerValue) + updateProperty(picker.htmlID(), Value, time.Now().Format(dateFormat), picker.session) + + default: + picker.viewData.remove(tag) + picker.propertyChanged(tag) + } +} + +func (picker *datePickerData) Set(tag string, value interface{}) bool { + return picker.set(picker.normalizeTag(tag), value) +} + +func (picker *datePickerData) set(tag string, value interface{}) bool { + if value == nil { + picker.remove(tag) + return true + } + + setTimeValue := func(tag string) (time.Time, bool) { + //old, oldOK := getDateProperty(picker, tag, shortTag) + switch value := value.(type) { + case time.Time: + picker.properties[tag] = value + return value, true + + case string: + if text, ok := picker.Session().resolveConstants(value); ok { + if date, err := time.Parse(dateFormat, text); err == nil { + picker.properties[tag] = value + return date, true + } + } + } + + notCompatibleType(tag, value) + return time.Now(), false + } + + switch tag { + case DatePickerMin: + old, oldOK := getDateProperty(picker, DatePickerMin, Min) + if date, ok := setTimeValue(DatePickerMin); ok { + if !oldOK || date != old { + updateProperty(picker.htmlID(), Min, date.Format(dateFormat), picker.session) + } + return true + } + + case DatePickerMax: + old, oldOK := getDateProperty(picker, DatePickerMax, Max) + if date, ok := setTimeValue(DatePickerMax); ok { + if !oldOK || date != old { + updateProperty(picker.htmlID(), Max, date.Format(dateFormat), picker.session) + } + return true + } + + case DatePickerStep: + oldStep := GetDatePickerStep(picker, "") + if picker.setIntProperty(DatePickerStep, value) { + step := GetDatePickerStep(picker, "") + if oldStep != step { + if step > 0 { + updateProperty(picker.htmlID(), Step, strconv.Itoa(step), picker.session) + } else { + removeProperty(picker.htmlID(), Step, picker.session) + } + } + return true + } + + case DatePickerValue: + oldDate := GetDatePickerValue(picker, "") + if date, ok := setTimeValue(DatePickerMax); ok { + picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, picker.htmlID(), date.Format(dateFormat))) + if date != oldDate { + for _, listener := range picker.dateChangedListeners { + listener(picker, date) + } + } + return true + } + + case DateChangedEvent: + switch value := value.(type) { + case func(DatePicker, time.Time): + picker.dateChangedListeners = []func(DatePicker, time.Time){value} + + case func(time.Time): + fn := func(view DatePicker, date time.Time) { + value(date) + } + picker.dateChangedListeners = []func(DatePicker, time.Time){fn} + + case []func(DatePicker, time.Time): + picker.dateChangedListeners = value + + case []func(time.Time): + listeners := make([]func(DatePicker, time.Time), len(value)) + for i, val := range value { + if val == nil { + notCompatibleType(tag, val) + return false + } + + listeners[i] = func(view DatePicker, date time.Time) { + val(date) + } + } + picker.dateChangedListeners = listeners + + case []interface{}: + listeners := make([]func(DatePicker, time.Time), len(value)) + for i, val := range value { + if val == nil { + notCompatibleType(tag, val) + return false + } + + switch val := val.(type) { + case func(DatePicker, time.Time): + listeners[i] = val + + case func(time.Time): + listeners[i] = func(view DatePicker, date time.Time) { + val(date) + } + + default: + notCompatibleType(tag, val) + return false + } + } + picker.dateChangedListeners = listeners + } + return true + + default: + if picker.viewData.set(tag, value) { + picker.propertyChanged(tag) + return true + } + } + return false +} + +func (picker *datePickerData) Get(tag string) interface{} { + return picker.get(picker.normalizeTag(tag)) +} + +func (picker *datePickerData) get(tag string) interface{} { + switch tag { + case DateChangedEvent: + return picker.dateChangedListeners + + default: + return picker.viewData.get(tag) + } +} + +func (picker *datePickerData) htmlTag() string { + return "input" +} + +func (picker *datePickerData) htmlProperties(self View, buffer *strings.Builder) { + picker.viewData.htmlProperties(self, buffer) + + buffer.WriteString(` type="date"`) + + if min, ok := getDateProperty(picker, DatePickerMin, Min); ok { + buffer.WriteString(` min="`) + buffer.WriteString(min.Format(dateFormat)) + buffer.WriteByte('"') + } + + if max, ok := getDateProperty(picker, DatePickerMax, Max); ok { + buffer.WriteString(` max="`) + buffer.WriteString(max.Format(dateFormat)) + buffer.WriteByte('"') + } + + if step, ok := intProperty(picker, DatePickerStep, picker.Session(), 0); ok && step > 0 { + buffer.WriteString(` step="`) + buffer.WriteString(strconv.Itoa(step)) + buffer.WriteByte('"') + } + + buffer.WriteString(` value="`) + buffer.WriteString(GetDatePickerValue(picker, "").Format(dateFormat)) + buffer.WriteByte('"') + + buffer.WriteString(` oninput="editViewInputEvent(this)"`) +} + +func (picker *datePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) { + if IsDisabled(self) { + buffer.WriteString(` disabled`) + } + picker.viewData.htmlDisabledProperties(self, buffer) +} + +func (picker *datePickerData) handleCommand(self View, command string, data DataObject) bool { + switch command { + case "textChanged": + if text, ok := data.PropertyValue("text"); ok { + if value, err := time.Parse(dateFormat, text); err == nil { + oldValue := GetDatePickerValue(picker, "") + picker.properties[DatePickerValue] = value + if value != oldValue { + for _, listener := range picker.dateChangedListeners { + listener(picker, value) + } + } + } + } + return true + } + + return picker.viewData.handleCommand(self, command, data) +} + +func getDateProperty(view View, mainTag, shortTag string) (time.Time, bool) { + valueToTime := func(value interface{}) (time.Time, bool) { + if value != nil { + switch value := value.(type) { + case time.Time: + return value, true + + case string: + if text, ok := view.Session().resolveConstants(value); ok { + if result, err := time.Parse(dateFormat, text); err == nil { + return result, true + } + } + } + } + return time.Now(), false + } + + if view != nil { + if result, ok := valueToTime(view.getRaw(mainTag)); ok { + return result, true + } + + if value, ok := valueFromStyle(view, shortTag); ok { + if result, ok := valueToTime(value); ok { + return result, true + } + } + } + + return time.Now(), false +} + +// GetDatePickerMin returns the min date of DatePicker subview and "true" as the second value if the min date is set, +// "false" as the second value otherwise. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetDatePickerMin(view View, subviewID string) (time.Time, bool) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + return getDateProperty(view, DatePickerMin, Min) + } + return time.Now(), false +} + +// GetDatePickerMax returns the max date of DatePicker subview and "true" as the second value if the min date is set, +// "false" as the second value otherwise. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetDatePickerMax(view View, subviewID string) (time.Time, bool) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + return getDateProperty(view, DatePickerMax, Max) + } + return time.Now(), false +} + +// GetDatePickerStep returns the date changing step in days of DatePicker subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetDatePickerStep(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, _ := intStyledProperty(view, DatePickerStep, 0); result >= 0 { + return result + } + } + return 0 +} + +// GetDatePickerValue returns the date of DatePicker subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetDatePickerValue(view View, subviewID string) time.Time { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return time.Now() + } + date, _ := getDateProperty(view, DatePickerValue, Value) + return date +} + +// GetDateChangedListeners returns the DateChangedListener list of an DatePicker subview. +// If there are no listeners then the empty list is returned +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetDateChangedListeners(view View, subviewID string) []func(DatePicker, time.Time) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.Get(DateChangedEvent); value != nil { + if listeners, ok := value.([]func(DatePicker, time.Time)); ok { + return listeners + } + } + } + return []func(DatePicker, time.Time){} +} diff --git a/defaultTheme.rui b/defaultTheme.rui new file mode 100644 index 0000000..966a722 --- /dev/null +++ b/defaultTheme.rui @@ -0,0 +1,163 @@ +theme { + colors = _{ + ruiTextColor = #FF000000, + ruiDisabledTextColor = #FF202020, + ruiBackgroundColor = #FFFFFFFF, + ruiButtonColor = #FFE0E0E0, + ruiButtonActiveColor = #FFC0C0C0, + ruiButtonTextColor = #FF000000, + ruiButtonDisabledColor = #FFE0E0E0, + ruiButtonDisabledTextColor = #FF202020, + ruiHighlightColor = #FF1A74E8, + ruiHighlightTextColor = #FFFFFFFF, + ruiSelectedColor = #FFE0E0E0, + ruiSelectedTextColor = #FF000000, + ruiPopupBackgroundColor = #FFFFFFFF, + ruiPopupTextColor = #FF000000, + ruiPopupTitleColor = #FF0000FF, + ruiPopupTitleTextColor = #FFFFFFFF, + + ruiTabsBackgroundColor = #FFEEEEEE, + ruiInactiveTabColor = #FFD0D0D0, + ruiInactiveTabTextColor = #FF202020, + ruiActiveTabColor = #FFFFFFFF, + ruiActiveTabTextColor = #FF000000, + }, + colors:dark = _{ + ruiTextColor = #FFE0E0E0, + ruiDisabledTextColor = #FFA0A0A0, + ruiBackgroundColor = #FF080808, + ruiButtonColor = #FF404040, + ruiButtonTextColor = #FFE0E0E0, + ruiButtonDisabledColor = #FF404040, + ruiButtonDisabledTextColor = #FFA0A0A0, + ruiHighlightColor = #FF1A74E8, + ruiHighlightTextColor = #FFFFFFFF, + }, + constants = _{ + ruiButtonHorizontalPadding = 16px, + ruiButtonVerticalPadding = 8px, + ruiButtonMargin = 4px, + ruiButtonRadius = 4px, + ruiButtonHighlightDilation = 1.5px, + ruiButtonHighlightBlur = 2px, + ruiCheckboxGap = 12px, + ruiListItemHorizontalPadding = 12px, + ruiListItemVerticalPadding = 4px, + ruiPopupTitleHeight = 32px, + ruiPopupTitlePadding = 8px, + ruiPopupButtonGap = 4px, + ruiTabSpace = 2px, + ruiTabHeight = 32px, + ruiTabPadding = 2px, + }, + constants:touch = _{ + ruiButtonHorizontalPadding = 20px, + ruiButtonVerticalPadding = 16px + }, + styles = [ + ruiApp { + font-name = "Arial, Helvetica, sans-serif", + text-size = 12pt, + text-color = @ruiTextColor, + background-color = @ruiBackgroundColor, + }, + ruiButton { + align = center, + padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding", + margin = @ruiButtonMargin, + radius = @ruiButtonRadius, + background-color = @ruiButtonColor, + text-color = @ruiButtonTextColor, + border = _{width = 1px, style = solid, color = @ruiButtonTextColor} + }, + ruiDisabledButton { + align = center, + padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding", + margin = @ruiButtonMargin, + radius = @ruiButtonRadius, + background-color = @ruiButtonDisabledColor, + text-color = @ruiButtonDisabledTextColor, + border = _{width = 1px, style = solid, color = @ruiButtonDisabledTextColor} + }, + ruiButton:hover { + text-color = @ruiTextColor, + background-color = @ruiBackgroundColor, + }, + ruiButton:focus { + shadow = _{spread-radius = @ruiButtonHighlightDilation, blur = @ruiButtonHighlightBlur, color = @ruiHighlightColor }, + }, + ruiButton:active { + background-color = @ruiButtonActiveColor + }, + ruiCheckbox { + radius = 2px, + padding = 1px, + margin = 2px, + }, + ruiCheckbox:focus { + margin = 0, + border = _{style = solid, color = @ruiHighlightColor, width = 2px }, + }, + ruiListItem { + radius = 4px, + padding = "@ruiListItemVerticalPadding, @ruiListItemHorizontalPadding, @ruiListItemVerticalPadding, @ruiListItemHorizontalPadding", + }, + ruiListItemSelected { + background-color=@ruiSelectedColor, + text-color=@ruiSelectedTextColor, + }, + ruiListItemFocused { + background-color=@ruiHighlightColor, + text-color=@ruiHighlightTextColor, + }, + ruiActiveTab { + background-color = @ruiActiveTabColor, + text-color = @ruiActiveTabTextColor, + padding-left = 8px, + padding-right = 8px, + }, + ruiInactiveTab { + background-color = @ruiInactiveTabColor, + text-color = @ruiInactiveTabTextColor, + padding-left = 8px, + padding-right = 8px, + }, + ruiActiveVerticalTab { + background-color = @ruiActiveTabColor, + text-color = @ruiActiveTabTextColor, + padding-top = 8px, + padding-bottom = 8px, + }, + ruiInactiveVerticalTab { + background-color = @ruiInactiveTabColor, + text-color = @ruiInactiveTabTextColor, + padding-top = 8px, + padding-bottom = 8px, + }, + ruiPopup { + background-color = @ruiPopupBackgroundColor, + text-color = @ruiPopupTextColor, + radius = 4px, + shadow = _{spread-radius=4px, blur=16px, color=#80808080}, + } + ruiPopupTitle { + background-color = @ruiPopupTitleColor, + text-color = @ruiPopupTitleTextColor, + min-height = 24px, + } + ruiMessageText { + padding-left = 64px, + padding-right = 64px, + padding-top = 32px, + padding-bottom = 32px, + } + ruiPopupMenuItem { + padding-top = 4px, + padding-bottom = 4px, + padding-left = 8px, + padding-right = 8px, + } + ], +} + diff --git a/detailsView.go b/detailsView.go new file mode 100644 index 0000000..6614576 --- /dev/null +++ b/detailsView.go @@ -0,0 +1,177 @@ +package rui + +import "strings" + +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" +) + +// DetailsView - collapsible container of View +type DetailsView interface { + ViewsContainer +} + +type detailsViewData struct { + viewsContainerData +} + +// NewDetailsView create new DetailsView object and return it +func NewDetailsView(session Session, params Params) DetailsView { + view := new(detailsViewData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newDetailsView(session Session) View { + return NewDetailsView(session, nil) +} + +// Init initialize fields of DetailsView by default values +func (detailsView *detailsViewData) Init(session Session) { + detailsView.viewsContainerData.Init(session) + detailsView.tag = "DetailsView" + //detailsView.systemClass = "ruiDetailsView" +} + +func (detailsView *detailsViewData) Remove(tag string) { + detailsView.remove(strings.ToLower(tag)) +} + +func (detailsView *detailsViewData) remove(tag string) { + if _, ok := detailsView.properties[tag]; ok { + switch tag { + case Summary: + delete(detailsView.properties, tag) + updateInnerHTML(detailsView.htmlID(), detailsView.Session()) + + case Expanded: + delete(detailsView.properties, tag) + removeProperty(detailsView.htmlID(), "open", detailsView.Session()) + + default: + detailsView.viewsContainerData.remove(tag) + } + } +} + +func (detailsView *detailsViewData) Set(tag string, value interface{}) bool { + return detailsView.set(strings.ToLower(tag), value) +} + +func (detailsView *detailsViewData) set(tag string, value interface{}) bool { + switch tag { + case Summary: + switch value := value.(type) { + case string: + detailsView.properties[Summary] = value + + case View: + detailsView.properties[Summary] = value + + case DataObject: + if view := CreateViewFromObject(detailsView.Session(), value); view != nil { + detailsView.properties[Summary] = view + } else { + return false + } + + default: + notCompatibleType(tag, value) + return false + } + updateInnerHTML(detailsView.htmlID(), detailsView.Session()) + return true + + case Expanded: + if detailsView.setBoolProperty(tag, value) { + if IsDetailsExpanded(detailsView, "") { + updateProperty(detailsView.htmlID(), "open", "", detailsView.Session()) + } else { + removeProperty(detailsView.htmlID(), "open", detailsView.Session()) + } + return true + } + notCompatibleType(tag, value) + return false + } + + return detailsView.viewsContainerData.Set(tag, value) +} + +func (detailsView *detailsViewData) Get(tag string) interface{} { + return detailsView.get(strings.ToLower(tag)) +} + +func (detailsView *detailsViewData) get(tag string) interface{} { + return detailsView.viewsContainerData.get(tag) +} + +func (detailsView *detailsViewData) htmlTag() string { + return "details" +} + +func (detailsView *detailsViewData) htmlProperties(self View, buffer *strings.Builder) { + detailsView.viewsContainerData.htmlProperties(self, buffer) + if IsDetailsExpanded(detailsView, "") { + buffer.WriteString(` open`) + } +} + +func (detailsView *detailsViewData) htmlSubviews(self View, buffer *strings.Builder) { + if value, ok := detailsView.properties[Summary]; ok { + switch value := value.(type) { + case string: + buffer.WriteString("") + buffer.WriteString(value) + buffer.WriteString("") + + case View: + buffer.WriteString("") + viewHTML(value, buffer) + buffer.WriteString("") + } + } + + detailsView.viewsContainerData.htmlSubviews(self, buffer) +} + +// GetDetailsSummary returns a value of the Summary property of DetailsView. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetDetailsSummary(view View, subviewID string) View { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.Get(Summary); value != nil { + switch value := value.(type) { + case string: + return NewTextView(view.Session(), Params{Text: value}) + + case View: + return value + } + } + } + return nil +} + +// IsDetailsExpanded returns a value of the Expanded property of DetailsView. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func IsDetailsExpanded(view View, subviewID string) bool { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := boolStyledProperty(view, Expanded); ok { + return result + } + } + return false +} diff --git a/dropDownList.go b/dropDownList.go new file mode 100644 index 0000000..757d8a0 --- /dev/null +++ b/dropDownList.go @@ -0,0 +1,346 @@ +package rui + +import ( + "fmt" + "strconv" + "strings" +) + +const DropDownEvent = "drop-down-event" + +// DropDownList - the interface of a drop-down list view +type DropDownList interface { + View + getItems() []string +} + +type dropDownListData struct { + viewData + items []string + dropDownListener []func(DropDownList, int) +} + +// NewDropDownList create new DropDownList object and return it +func NewDropDownList(session Session, params Params) DropDownList { + view := new(dropDownListData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newDropDownList(session Session) View { + return NewDropDownList(session, nil) +} + +func (list *dropDownListData) Init(session Session) { + list.viewData.Init(session) + list.tag = "DropDownList" + list.items = []string{} + list.dropDownListener = []func(DropDownList, int){} +} + +func (list *dropDownListData) Remove(tag string) { + list.remove(strings.ToLower(tag)) +} + +func (list *dropDownListData) remove(tag string) { + switch tag { + case Items: + if len(list.items) > 0 { + list.items = []string{} + updateInnerHTML(list.htmlID(), list.session) + } + + case Current: + list.set(Current, 0) + + case DropDownEvent: + if len(list.dropDownListener) > 0 { + list.dropDownListener = []func(DropDownList, int){} + } + + default: + list.viewData.remove(tag) + } +} + +func (list *dropDownListData) Set(tag string, value interface{}) bool { + return list.set(strings.ToLower(tag), value) +} + +func (list *dropDownListData) set(tag string, value interface{}) bool { + switch tag { + case Items: + return list.setItems(value) + + case Current: + oldCurrent := GetDropDownCurrent(list, "") + if !list.setIntProperty(Current, value) { + return false + } + + if !list.session.ignoreViewUpdates() { + current := GetDropDownCurrent(list, "") + if oldCurrent != current { + list.session.runScript(fmt.Sprintf(`selectDropDownListItem('%s', %d)`, list.htmlID(), current)) + list.onSelectedItemChanged(current) + } + } + return true + + case DropDownEvent: + return list.setDropDownListener(value) + } + + return list.viewData.set(tag, value) +} + +func (list *dropDownListData) setItems(value interface{}) bool { + switch value := value.(type) { + case string: + list.items = []string{value} + + case []string: + list.items = value + + case []DataValue: + list.items = []string{} + for _, val := range value { + if !val.IsObject() { + list.items = append(list.items, val.Value()) + } + } + + case []fmt.Stringer: + list.items = make([]string, len(value)) + for i, str := range value { + list.items[i] = str.String() + } + + case []interface{}: + items := []string{} + 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 { + notCompatibleType(Items, value) + return false + } + } + } + + list.items = items + + default: + notCompatibleType(Items, value) + return false + } + + if !list.session.ignoreViewUpdates() { + updateInnerHTML(list.htmlID(), list.session) + } + return true +} + +func (list *dropDownListData) setDropDownListener(value interface{}) bool { + switch value := value.(type) { + case func(DropDownList, int): + list.dropDownListener = []func(DropDownList, int){value} + return true + + case func(int): + list.dropDownListener = []func(DropDownList, int){func(list DropDownList, index int) { + value(index) + }} + return true + + case []func(DropDownList, int): + list.dropDownListener = value + return true + + case []func(int): + listeners := make([]func(DropDownList, int), len(value)) + for i, val := range value { + if val == nil { + notCompatibleType(DropDownEvent, value) + return false + } + listeners[i] = func(list DropDownList, index int) { + val(index) + } + } + list.dropDownListener = listeners + return true + + case []interface{}: + listeners := make([]func(DropDownList, int), len(value)) + for i, val := range value { + if val == nil { + notCompatibleType(DropDownEvent, value) + return false + } + switch val := val.(type) { + case func(DropDownList, int): + listeners[i] = val + + case func(int): + listeners[i] = func(list DropDownList, index int) { + val(index) + } + + default: + notCompatibleType(DropDownEvent, value) + return false + } + list.dropDownListener = listeners + } + return true + } + + notCompatibleType(DropDownEvent, value) + return false +} + +func (list *dropDownListData) Get(tag string) interface{} { + return list.get(strings.ToLower(tag)) +} + +func (list *dropDownListData) get(tag string) interface{} { + switch tag { + case Items: + return list.items + + 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 +} + +func (list *dropDownListData) htmlTag() string { + return "select" +} + +func (list *dropDownListData) htmlSubviews(self View, buffer *strings.Builder) { + if list.items != nil { + current := GetDropDownCurrent(list, "") + notTranslate := GetNotTranslate(list, "") + for i, item := range list.items { + if i == current { + buffer.WriteString("") + } + } +} + +func (list *dropDownListData) htmlProperties(self View, buffer *strings.Builder) { + list.viewData.htmlProperties(self, buffer) + buffer.WriteString(` size="1" onchange="dropDownListEvent(this, event)"`) +} + +func (list *dropDownListData) htmlDisabledProperties(self View, buffer *strings.Builder) { + list.viewData.htmlDisabledProperties(self, buffer) + if IsDisabled(list) { + buffer.WriteString(`disabled`) + } +} + +func (list *dropDownListData) onSelectedItemChanged(number int) { + for _, listener := range list.dropDownListener { + listener(list, number) + } +} + +func (list *dropDownListData) handleCommand(self View, command string, data DataObject) bool { + switch command { + case "itemSelected": + if text, ok := data.PropertyValue("number"); ok { + if number, err := strconv.Atoi(text); err == nil { + if GetDropDownCurrent(list, "") != number && number >= 0 && number < len(list.items) { + list.properties[Current] = number + list.onSelectedItemChanged(number) + } + } else { + ErrorLog(err.Error()) + } + } + + default: + return list.viewData.handleCommand(self, command, data) + } + return true +} + +func GetDropDownListeners(view View) []func(DropDownList, int) { + if value := view.Get(DropDownEvent); value != nil { + if listeners, ok := value.([]func(DropDownList, int)); ok { + return listeners + } + } + return []func(DropDownList, int){} +} + +// func GetDropDownItems return the view items list +func GetDropDownItems(view View, subviewID string) []string { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if list, ok := view.(DropDownList); ok { + return list.getItems() + } + } + return []string{} +} + +// func GetDropDownCurrentItem return the number of the selected item +func GetDropDownCurrent(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + result, _ := intProperty(view, Current, view.Session(), 0) + return result + } + return 0 +} diff --git a/editView.go b/editView.go new file mode 100644 index 0000000..10c8f5b --- /dev/null +++ b/editView.go @@ -0,0 +1,632 @@ +package rui + +import ( + "fmt" + "strconv" + "strings" +) + +const ( + // EditTextChangedEvent is the constant for the "edit-text-changed" property tag. + EditTextChangedEvent = "edit-text-changed" + // EditViewType is the constant for the "edit-view-type" property tag. + EditViewType = "edit-view-type" + // EditViewPattern is the constant for the "edit-view-pattern" property tag. + EditViewPattern = "edit-view-pattern" + // Spellcheck is the constant for the "spellcheck" property tag. + Spellcheck = "spellcheck" +) + +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 separeted 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 +type EditView interface { + View + AppendText(text string) +} + +type editViewData struct { + viewData + textChangeListeners []func(EditView, string) +} + +// NewEditView create new EditView object and return it +func NewEditView(session Session, params Params) EditView { + view := new(editViewData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newEditView(session Session) View { + return NewEditView(session, nil) +} + +func (edit *editViewData) Init(session Session) { + edit.viewData.Init(session) + edit.textChangeListeners = []func(EditView, string){} + edit.tag = "EditView" +} + +func (edit *editViewData) normalizeTag(tag string) string { + tag = strings.ToLower(tag) + switch tag { + case Type, "edit-type": + return EditViewType + + case Pattern, "edit-pattern": + return EditViewPattern + + case "maxlength", "maxlen": + return MaxLength + } + + return tag +} + +func (edit *editViewData) Remove(tag string) { + edit.remove(edit.normalizeTag(tag)) +} + +func (edit *editViewData) remove(tag string) { + if _, ok := edit.properties[tag]; ok { + switch tag { + case Hint: + delete(edit.properties, Hint) + removeProperty(edit.htmlID(), "placeholder", edit.session) + + case MaxLength: + delete(edit.properties, MaxLength) + removeProperty(edit.htmlID(), "maxlength", edit.session) + + case ReadOnly, Spellcheck: + delete(edit.properties, tag) + updateBoolProperty(edit.htmlID(), tag, false, edit.session) + + case EditTextChangedEvent: + if len(edit.textChangeListeners) > 0 { + edit.textChangeListeners = []func(EditView, string){} + } + + case Text: + oldText := GetText(edit, "") + delete(edit.properties, tag) + if oldText != "" { + edit.textChanged("") + edit.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, edit.htmlID(), "")) + } + + case EditViewPattern: + oldText := GetEditViewPattern(edit, "") + delete(edit.properties, tag) + if oldText != "" { + removeProperty(edit.htmlID(), Pattern, edit.session) + } + + case EditViewType: + oldType := GetEditViewType(edit, "") + delete(edit.properties, tag) + if oldType != 0 { + updateInnerHTML(edit.parentHTMLID(), edit.session) + } + + case Wrap: + oldWrap := IsEditViewWrap(edit, "") + delete(edit.properties, tag) + if GetEditViewType(edit, "") == MultiLineText { + if wrap := IsEditViewWrap(edit, ""); wrap != oldWrap { + if wrap { + updateProperty(edit.htmlID(), "wrap", "soft", edit.session) + } else { + updateProperty(edit.htmlID(), "wrap", "off", edit.session) + } + } + } + + default: + edit.viewData.remove(tag) + } + } +} + +func (edit *editViewData) Set(tag string, value interface{}) bool { + return edit.set(edit.normalizeTag(tag), value) +} + +func (edit *editViewData) set(tag string, value interface{}) bool { + if value == nil { + edit.remove(tag) + return true + } + + switch tag { + case Text: + oldText := GetText(edit, "") + if text, ok := value.(string); ok { + edit.properties[Text] = text + if text = GetText(edit, ""); oldText != text { + edit.textChanged(text) + if GetEditViewType(edit, "") == MultiLineText { + updateInnerHTML(edit.htmlID(), edit.Session()) + } else { + text = strings.ReplaceAll(text, `"`, `\"`) + text = strings.ReplaceAll(text, `'`, `\'`) + text = strings.ReplaceAll(text, "\n", `\n`) + text = strings.ReplaceAll(text, "\r", `\r`) + edit.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, edit.htmlID(), text)) + } + } + return true + } + return false + + case Hint: + oldText := GetHint(edit, "") + if text, ok := value.(string); ok { + edit.properties[Hint] = text + if text = GetHint(edit, ""); oldText != text { + if text != "" { + updateProperty(edit.htmlID(), "placeholder", text, edit.session) + } else { + removeProperty(edit.htmlID(), "placeholder", edit.session) + } + } + return true + } + return false + + case MaxLength: + oldMaxLength := GetMaxLength(edit, "") + if edit.setIntProperty(MaxLength, value) { + if maxLength := GetMaxLength(edit, ""); maxLength != oldMaxLength { + if maxLength > 0 { + updateProperty(edit.htmlID(), "maxlength", strconv.Itoa(maxLength), edit.session) + } else { + removeProperty(edit.htmlID(), "maxlength", edit.session) + } + } + return true + } + return false + + case ReadOnly: + if edit.setBoolProperty(ReadOnly, value) { + if IsReadOnly(edit, "") { + updateProperty(edit.htmlID(), ReadOnly, "", edit.session) + } else { + removeProperty(edit.htmlID(), ReadOnly, edit.session) + } + return true + } + return false + + case Spellcheck: + if edit.setBoolProperty(Spellcheck, value) { + updateBoolProperty(edit.htmlID(), Spellcheck, IsSpellcheck(edit, ""), edit.session) + return true + } + return false + + case EditViewPattern: + oldText := GetEditViewPattern(edit, "") + if text, ok := value.(string); ok { + edit.properties[Pattern] = text + if text = GetEditViewPattern(edit, ""); oldText != text { + if text != "" { + updateProperty(edit.htmlID(), Pattern, text, edit.session) + } else { + removeProperty(edit.htmlID(), Pattern, edit.session) + } + } + return true + } + return false + + case EditViewType: + oldType := GetEditViewType(edit, "") + if edit.setEnumProperty(EditViewType, value, enumProperties[EditViewType].values) { + if GetEditViewType(edit, "") != oldType { + updateInnerHTML(edit.parentHTMLID(), edit.session) + } + return true + } + return false + + case Wrap: + oldWrap := IsEditViewWrap(edit, "") + if edit.setBoolProperty(Wrap, value) { + if GetEditViewType(edit, "") == MultiLineText { + if wrap := IsEditViewWrap(edit, ""); wrap != oldWrap { + if wrap { + updateProperty(edit.htmlID(), "wrap", "soft", edit.session) + } else { + updateProperty(edit.htmlID(), "wrap", "off", edit.session) + } + } + } + return true + } + return false + + case EditTextChangedEvent: + ok := edit.setChangeListeners(value) + if !ok { + notCompatibleType(tag, value) + } + return ok + } + + return edit.viewData.set(tag, value) +} + +func (edit *editViewData) setChangeListeners(value interface{}) bool { + switch value := value.(type) { + case func(EditView, string): + edit.textChangeListeners = []func(EditView, string){value} + + case func(string): + fn := func(view EditView, text string) { + value(text) + } + edit.textChangeListeners = []func(EditView, string){fn} + + case []func(EditView, string): + edit.textChangeListeners = value + + case []func(string): + listeners := make([]func(EditView, string), len(value)) + for i, v := range value { + if v == nil { + return false + } + listeners[i] = func(view EditView, text string) { + v(text) + } + } + edit.textChangeListeners = listeners + + case []interface{}: + listeners := make([]func(EditView, string), len(value)) + for i, v := range value { + if v == nil { + return false + } + switch v := v.(type) { + case func(EditView, string): + listeners[i] = v + + case func(string): + listeners[i] = func(view EditView, text string) { + v(text) + } + + default: + return false + } + } + edit.textChangeListeners = listeners + + default: + return false + } + return true +} + +func (edit *editViewData) Get(tag string) interface{} { + return edit.get(edit.normalizeTag(tag)) +} + +func (edit *editViewData) get(tag string) interface{} { + return edit.viewData.get(tag) +} + +func (edit *editViewData) AppendText(text string) { + if GetEditViewType(edit, "") == MultiLineText { + if value := edit.getRaw(Text); value != nil { + if textValue, ok := value.(string); ok { + textValue += text + edit.properties[Text] = textValue + + text := strings.ReplaceAll(text, `"`, `\"`) + text = strings.ReplaceAll(text, `'`, `\'`) + text = strings.ReplaceAll(text, "\n", `\n`) + text = strings.ReplaceAll(text, "\r", `\r`) + + edit.session.runScript(`appendToInnerHTML("` + edit.htmlID() + `", "` + text + `")`) + + edit.textChanged(textValue) + return + } + } + edit.set(Text, text) + } else { + edit.set(Text, GetText(edit, "")+text) + } +} + +func (edit *editViewData) textChanged(newText string) { + for _, listener := range edit.textChangeListeners { + listener(edit, newText) + } +} + +func (edit *editViewData) htmlTag() string { + if GetEditViewType(edit, "") == MultiLineText { + return "textarea" + } + return "input" +} + +func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) { + edit.viewData.htmlProperties(self, buffer) + + writeSpellcheck := func() { + if spellcheck := IsSpellcheck(edit, ""); spellcheck { + buffer.WriteString(` spellcheck="true"`) + } else { + buffer.WriteString(` spellcheck="false"`) + } + } + + editType := GetEditViewType(edit, "") + switch editType { + case SingleLineText: + buffer.WriteString(` type="text" inputmode="text"`) + writeSpellcheck() + + case PasswordText: + buffer.WriteString(` type="password" inputmode="text"`) + + case EmailText: + buffer.WriteString(` type="email" inputmode="email"`) + + case EmailsText: + buffer.WriteString(` type="email" inputmode="email" multiple`) + + case URLText: + buffer.WriteString(` type="url" inputmode="url"`) + + case PhoneText: + buffer.WriteString(` type="tel" inputmode="tel"`) + + case MultiLineText: + if IsEditViewWrap(edit, "") { + buffer.WriteString(` wrap="soft"`) + } else { + buffer.WriteString(` wrap="off"`) + } + writeSpellcheck() + } + + if IsReadOnly(edit, "") { + buffer.WriteString(` readonly`) + } + + if maxLength := GetMaxLength(edit, ""); maxLength > 0 { + buffer.WriteString(` maxlength="`) + buffer.WriteString(strconv.Itoa(maxLength)) + buffer.WriteByte('"') + } + + if hint := GetHint(edit, ""); hint != "" { + buffer.WriteString(` placeholder="`) + buffer.WriteString(hint) + buffer.WriteByte('"') + } + + buffer.WriteString(` oninput="editViewInputEvent(this)"`) + if pattern := GetEditViewPattern(edit, ""); pattern != "" { + buffer.WriteString(` pattern="`) + buffer.WriteString(pattern) + buffer.WriteByte('"') + } + + if editType != MultiLineText { + if text := GetText(edit, ""); text != "" { + buffer.WriteString(` value="`) + buffer.WriteString(text) + buffer.WriteByte('"') + } + } +} + +func (edit *editViewData) htmlDisabledProperties(self View, buffer *strings.Builder) { + if IsDisabled(self) { + buffer.WriteString(` disabled`) + } + edit.viewData.htmlDisabledProperties(self, buffer) +} + +func (edit *editViewData) htmlSubviews(self View, buffer *strings.Builder) { + if GetEditViewType(edit, "") == MultiLineText { + text := strings.ReplaceAll(GetText(edit, ""), `"`, `\"`) + text = strings.ReplaceAll(text, "\n", `\n`) + text = strings.ReplaceAll(text, "\r", `\r`) + buffer.WriteString(strings.ReplaceAll(text, `'`, `\'`)) + } +} + +func (edit *editViewData) handleCommand(self View, command string, 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.textChanged(text) + } + } + return true + } + + return edit.viewData.handleCommand(self, command, data) +} + +// GetText returns a text of the subview. +// If the second argument (subviewID) is "" then a text of the first argument (view) is returned. +func GetText(view View, subviewID string) string { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if text, ok := stringProperty(view, Text, view.Session()); ok { + return text + } + } + return "" +} + +// GetHint returns a hint text of the subview. +// If the second argument (subviewID) is "" then a text of the first argument (view) is returned. +func GetHint(view View, subviewID string) string { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if text, ok := stringProperty(view, Hint, view.Session()); ok { + return text + } + if text, ok := valueFromStyle(view, Hint); ok { + if text, ok = view.Session().resolveConstants(text); ok { + return text + } + } + } + return "" +} + +// GetMaxLength returns a maximal lenght of EditView. If a maximal lenght is not limited then 0 is returned +// If the second argument (subviewID) is "" then a value of the first argument (view) is returned. +func GetMaxLength(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := intStyledProperty(view, MaxLength, 0); ok { + return result + } + } + return 0 +} + +// IsReadOnly returns the true if a EditView works in read only mode. +// If the second argument (subviewID) is "" then a value of the first argument (view) is returned. +func IsReadOnly(view View, subviewID string) bool { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := boolStyledProperty(view, ReadOnly); ok { + return result + } + } + return false +} + +// IsSpellcheck returns a value of the Spellcheck property of EditView. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func IsSpellcheck(view View, subviewID string) bool { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if spellcheck, ok := boolStyledProperty(view, Spellcheck); ok { + return spellcheck + } + } + return false +} + +// GetTextChangedListeners returns the TextChangedListener list of an EditView or MultiLineEditView subview. +// If there are no listeners then the empty list is returned +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTextChangedListeners(view View, subviewID string) []func(EditView, string) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.Get(EditTextChangedEvent); value != nil { + if result, ok := value.([]func(EditView, string)); ok { + return result + } + } + } + return []func(EditView, string){} +} + +// GetEditViewType returns a value of the Type property of EditView. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetEditViewType(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return SingleLineText + } + t, _ := enumStyledProperty(view, EditViewType, SingleLineText) + return t +} + +// GetEditViewPattern returns a value of the Pattern property of EditView. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetEditViewPattern(view View, subviewID string) string { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if pattern, ok := stringProperty(view, EditViewPattern, view.Session()); ok { + return pattern + } + if pattern, ok := valueFromStyle(view, EditViewPattern); ok { + if pattern, ok = view.Session().resolveConstants(pattern); ok { + return pattern + } + } + } + return "" +} + +// IsEditViewWrap returns a value of the Wrap property of MultiLineEditView. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func IsEditViewWrap(view View, subviewID string) bool { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if wrap, ok := boolStyledProperty(view, Wrap); ok { + return wrap + } + } + return false + +} + +// AppendEditText appends the text to the EditView content. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func AppendEditText(view View, subviewID string, text string) { + if subviewID != "" { + if edit := EditViewByID(view, subviewID); edit != nil { + edit.AppendText(text) + return + } + } + + if edit, ok := view.(EditView); ok { + edit.AppendText(text) + } +} diff --git a/focusEvents.go b/focusEvents.go new file mode 100644 index 0000000..66657a9 --- /dev/null +++ b/focusEvents.go @@ -0,0 +1,158 @@ +package rui + +import "strings" + +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" + + // 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" +) + +func valueToFocusListeners(value interface{}) ([]func(View), bool) { + if value == nil { + return nil, true + } + + switch value := value.(type) { + case func(View): + return []func(View){value}, true + + case func(): + fn := func(View) { + value() + } + return []func(View){fn}, true + + case []func(View): + 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(View), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(View) { + v() + } + } + return listeners, true + + case []interface{}: + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View), count) + for i, v := range value { + if v == nil { + return nil, false + } + switch v := v.(type) { + case func(View): + listeners[i] = v + + case func(): + listeners[i] = func(View) { + 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 interface{}) bool { + listeners, ok := valueToFocusListeners(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 { + updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session()) + } + } else { + return false + } + return true +} + +func (view *viewData) removeFocusListener(tag string) { + delete(view.properties, tag) + if view.created { + if js, ok := focusEvents[tag]; ok { + updateProperty(view.htmlID(), js.jsEvent, "", view.Session()) + } + } +} + +func getFocusListeners(view View, subviewID string, tag string) []func(View) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + 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) { + for tag, js := range focusEvents { + if value := view.getRaw(tag); value != nil { + if listeners, ok := value.([]func(View)); ok && len(listeners) > 0 { + buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `) + } + } + } +} + +// GetFocusListeners returns a FocusListener list. If there are no listeners then the empty list is returned +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetFocusListeners(view View, subviewID string) []func(View) { + return getFocusListeners(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 "" then a value from the first argument (view) is returned. +func GetLostFocusListeners(view View, subviewID string) []func(View) { + return getFocusListeners(view, subviewID, LostFocusEvent) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..944bf12 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/anoshenko/rui + +go 1.17 + +require github.com/gorilla/websocket v1.4.2 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..85efffd --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= diff --git a/gridLayout.go b/gridLayout.go new file mode 100644 index 0000000..3758f80 --- /dev/null +++ b/gridLayout.go @@ -0,0 +1,391 @@ +package rui + +import ( + "fmt" + "strings" +) + +// GridLayout - grid-container of View +type GridLayout interface { + ViewsContainer +} + +type gridLayoutData struct { + viewsContainerData +} + +// NewGridLayout create new GridLayout object and return it +func NewGridLayout(session Session, params Params) GridLayout { + view := new(gridLayoutData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newGridLayout(session Session) View { + return NewGridLayout(session, nil) +} + +// Init initialize fields of GridLayout by default values +func (gridLayout *gridLayoutData) Init(session Session) { + gridLayout.viewsContainerData.Init(session) + gridLayout.tag = "GridLayout" + gridLayout.systemClass = "ruiGridLayout" +} + +func (style *viewStyle) setGridCellSize(tag string, value interface{}) bool { + setValues := func(values []string) bool { + count := len(values) + if count > 1 { + sizes := make([]interface{}, count) + for i, val := range values { + val = strings.Trim(val, " \t\n\r") + if isConstantName(val) { + sizes[i] = val + } else if size, ok := StringToSizeUnit(val); ok { + sizes[i] = size + } else { + invalidPropertyValue(tag, value) + return false + } + } + style.properties[tag] = sizes + } else if isConstantName(values[0]) { + style.properties[tag] = values[0] + } else if size, ok := StringToSizeUnit(values[0]); ok { + style.properties[tag] = size + } else { + invalidPropertyValue(tag, value) + return false + } + return true + } + + switch tag { + case CellWidth, CellHeight: + switch value := value.(type) { + case SizeUnit, []SizeUnit: + style.properties[tag] = value + + case string: + if !setValues(strings.Split(value, ",")) { + return false + } + + case []string: + if !setValues(value) { + return false + } + + case []DataValue: + count := len(value) + if count == 0 { + invalidPropertyValue(tag, value) + return false + } + values := make([]string, count) + for i, val := range value { + if val.IsObject() { + invalidPropertyValue(tag, value) + return false + } + values[i] = val.Value() + } + if !setValues(values) { + return false + } + + case []interface{}: + count := len(value) + if count == 0 { + invalidPropertyValue(tag, value) + return false + } + sizes := make([]interface{}, count) + for i, val := range value { + switch val := val.(type) { + case SizeUnit: + sizes[i] = val + + case string: + if isConstantName(val) { + sizes[i] = val + } else if size, ok := StringToSizeUnit(val); ok { + sizes[i] = size + } else { + invalidPropertyValue(tag, value) + return false + } + + default: + invalidPropertyValue(tag, value) + return false + } + } + style.properties[tag] = sizes + + default: + notCompatibleType(tag, value) + return false + } + + return true + } + + return false +} + +func (style *viewStyle) gridCellSizesCSS(tag string, session Session) string { + switch cellSize := gridCellSizes(style, tag, session); len(cellSize) { + case 0: + + case 1: + if cellSize[0].Type != Auto { + return `repeat(auto-fill, ` + cellSize[0].cssString(`auto`) + `)` + } + + default: + allAuto := true + allEqual := true + for i, size := range cellSize { + if size.Type != Auto { + allAuto = false + } + if i > 0 && !size.Equal(cellSize[0]) { + allEqual = false + } + } + if !allAuto { + if allEqual { + return fmt.Sprintf(`repeat(%d, %s)`, len(cellSize), cellSize[0].cssString(`auto`)) + } + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + for _, size := range cellSize { + buffer.WriteRune(' ') + buffer.WriteString(size.cssString(`auto`)) + } + return buffer.String() + } + } + + return "" +} + +func (gridLayout *gridLayoutData) normalizeTag(tag string) string { + tag = strings.ToLower(tag) + switch tag { + case VerticalAlign: + return CellVerticalAlign + + case HorizontalAlign: + return CellHorizontalAlign + + case "row-gap": + return GridRowGap + + case ColumnGap: + return GridColumnGap + } + return tag +} + +func (gridLayout *gridLayoutData) Get(tag string) interface{} { + return gridLayout.get(gridLayout.normalizeTag(tag)) +} + +func (gridLayout *gridLayoutData) get(tag string) interface{} { + if tag == 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) { + if tag == Gap { + gridLayout.remove(GridRowGap) + gridLayout.remove(GridColumnGap) + return + } + + gridLayout.viewsContainerData.remove(tag) + switch tag { + case CellWidth: + updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`, + gridLayout.gridCellSizesCSS(CellWidth, gridLayout.session), gridLayout.session) + + case CellHeight: + updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`, + gridLayout.gridCellSizesCSS(CellHeight, gridLayout.session), gridLayout.session) + + } +} + +func (gridLayout *gridLayoutData) Set(tag string, value interface{}) bool { + return gridLayout.set(gridLayout.normalizeTag(tag), value) +} + +func (gridLayout *gridLayoutData) set(tag string, value interface{}) bool { + if value == nil { + gridLayout.remove(tag) + return true + } + + if tag == Gap { + return gridLayout.set(GridRowGap, value) && gridLayout.set(GridColumnGap, value) + } + + if gridLayout.viewsContainerData.set(tag, value) { + switch tag { + case CellWidth: + updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`, + gridLayout.gridCellSizesCSS(CellWidth, gridLayout.session), gridLayout.session) + + case CellHeight: + updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`, + gridLayout.gridCellSizesCSS(CellHeight, gridLayout.session), gridLayout.session) + + } + return true + } + + return false +} + +func gridCellSizes(properties Properties, tag string, session Session) []SizeUnit { + if value := properties.Get(tag); value != nil { + switch value := value.(type) { + case []SizeUnit: + return value + + case SizeUnit: + return []SizeUnit{value} + + case []interface{}: + result := make([]SizeUnit, len(value)) + for i, val := range value { + result[i] = AutoSize() + switch val := val.(type) { + case SizeUnit: + result[i] = val + + case string: + if text, ok := session.resolveConstants(val); ok { + result[i], _ = StringToSizeUnit(text) + } + } + } + return result + + case string: + if text, ok := session.resolveConstants(value); ok { + values := strings.Split(text, ",") + result := make([]SizeUnit, len(values)) + for i, val := range values { + result[i], _ = StringToSizeUnit(val) + } + return result + } + } + } + + return []SizeUnit{} +} + +func (gridLayout *gridLayoutData) cssStyle(self View, builder cssBuilder) { + gridLayout.viewsContainerData.cssStyle(self, builder) + // TODO +} + +// GetCellVerticalAlign returns the vertical align of a GridLayout cell content: TopAlign (0), BottomAlign (1), CenterAlign (2), StretchAlign (3) +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetCellVerticalAlign(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if align, ok := enumProperty(view, CellVerticalAlign, view.Session(), StretchAlign); ok { + return align + } + } + return StretchAlign +} + +// GetCellHorizontalAlign returns the vertical align of a GridLayout cell content: LeftAlign (0), RightAlign (1), CenterAlign (2), StretchAlign (3) +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetCellHorizontalAlign(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if align, ok := enumProperty(view, CellHorizontalAlign, view.Session(), StretchAlign); ok { + return align + } + } + return StretchAlign +} + +// GetCellWidth returns the width of a GridLayout cell. If the result is an empty array, then the width is not set. +// If the result is a single value array, then the width of all cell is equal. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetCellWidth(view View, subviewID string) []SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + return gridCellSizes(view, CellWidth, view.Session()) + } + return []SizeUnit{} +} + +// GetCellHeight returns the height of a GridLayout cell. If the result is an empty array, then the height is not set. +// If the result is a single value array, then the height of all cell is equal. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetCellHeight(view View, subviewID string) []SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + return gridCellSizes(view, CellHeight, view.Session()) + } + return []SizeUnit{} +} + +// GetGridRowGap returns the gap between GridLayout rows. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetGridRowGap(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := sizeProperty(view, GridRowGap, view.Session()); ok { + return result + } + } + return AutoSize() +} + +// GetGridColumnGap returns the gap between GridLayout columns. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetGridColumnGap(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := sizeProperty(view, GridColumnGap, view.Session()); ok { + return result + } + } + return AutoSize() +} diff --git a/image.go b/image.go new file mode 100644 index 0000000..27592e1 --- /dev/null +++ b/image.go @@ -0,0 +1,132 @@ +package rui + +import "strconv" + +const ( + // ImageLoading is the image loading status: in the process of loading + ImageLoading = 0 + // ImageReady is the image loading status: the image is loaded successfully + ImageReady = 1 + // ImageLoadingError is the image loading status: an error occurred while loading + ImageLoadingError = 2 +) + +// Image defines the image that is used for drawing operations on the Canvas. +type Image interface { + // URL returns the url of the image + URL() string + // LoadingStatus returns the status of the image loading: ImageLoading (0), ImageReady (1), ImageLoadingError (2) + LoadingStatus() int + // LoadingError: if LoadingStatus() == ImageLoadingError then returns the error text, "" otherwise + LoadingError() string + setLoadingError(err string) + // Width returns the width of the image in pixels. While LoadingStatus() != ImageReady returns 0 + Width() float64 + // Height returns the height of the image in pixels. While LoadingStatus() != ImageReady returns 0 + Height() float64 +} + +type imageData struct { + url string + loadingStatus int + loadingError string + width, height float64 + listener func(Image) +} + +type imageManager struct { + images map[string]*imageData +} + +func (image *imageData) URL() string { + return image.url +} + +func (image *imageData) LoadingStatus() int { + return image.loadingStatus +} + +func (image *imageData) LoadingError() string { + return image.loadingError +} + +func (image *imageData) setLoadingError(err string) { + image.loadingError = err +} + +func (image *imageData) Width() float64 { + return image.width +} + +func (image *imageData) Height() float64 { + return image.height +} + +func (manager *imageManager) loadImage(url string, onLoaded func(Image), session Session) Image { + if manager.images == nil { + manager.images = make(map[string]*imageData) + } + + if image, ok := manager.images[url]; ok && image.loadingStatus == ImageReady { + return image + } + + image := new(imageData) + image.url = url + image.listener = onLoaded + image.loadingStatus = ImageLoading + manager.images[url] = image + session.runScript("loadImage('" + url + "');") + return image +} + +func (manager *imageManager) imageLoaded(obj DataObject, session Session) { + if manager.images == nil { + manager.images = make(map[string]*imageData) + return + } + + if url, ok := obj.PropertyValue("url"); ok { + if image, ok := manager.images[url]; ok { + image.loadingStatus = ImageReady + if width, ok := obj.PropertyValue("width"); ok { + if w, err := strconv.ParseFloat(width, 64); err == nil { + image.width = w + } + } + if height, ok := obj.PropertyValue("height"); ok { + if h, err := strconv.ParseFloat(height, 64); err == nil { + image.height = h + } + } + if image.listener != nil { + image.listener(image) + } + } + } +} + +func (manager *imageManager) imageLoadError(obj DataObject, session Session) { + if manager.images == nil { + manager.images = make(map[string]*imageData) + return + } + + if url, ok := obj.PropertyValue("url"); ok { + if image, ok := manager.images[url]; ok { + delete(manager.images, url) + + text, _ := obj.PropertyValue("message") + image.setLoadingError(text) + + if image.listener != nil { + image.listener(image) + } + } + } +} + +// LoadImage starts the async image loading by url +func LoadImage(url string, onLoaded func(Image), session Session) Image { + return session.imageManager().loadImage(url, onLoaded, session) +} diff --git a/imageView.go b/imageView.go new file mode 100644 index 0000000..15cd908 --- /dev/null +++ b/imageView.go @@ -0,0 +1,264 @@ +package rui + +import ( + "fmt" + "strings" +) + +const ( + // 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 element’s 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 element’s 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 element’s 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 +type ImageView interface { + View +} + +type imageViewData struct { + viewData +} + +// NewImageView create new ImageView object and return it +func NewImageView(session Session, params Params) ImageView { + view := new(imageViewData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newImageView(session Session) View { + return NewImageView(session, nil) +} + +// Init initialize fields of imageView by default values +func (imageView *imageViewData) Init(session Session) { + imageView.viewData.Init(session) + imageView.tag = "ImageView" + //imageView.systemClass = "ruiImageView" + +} + +func (imageView *imageViewData) normalizeTag(tag string) string { + tag = strings.ToLower(tag) + switch tag { + case "source": + tag = Source + + case VerticalAlign: + tag = ImageVerticalAlign + + case HorizontalAlign: + tag = ImageHorizontalAlign + + case altProperty: + tag = AltText + } + return tag +} + +func (imageView *imageViewData) Remove(tag string) { + imageView.remove(imageView.normalizeTag(tag)) +} + +func (imageView *imageViewData) remove(tag string) { + imageView.viewData.remove(tag) + switch tag { + case Source: + updateProperty(imageView.htmlID(), "src", "", imageView.session) + removeProperty(imageView.htmlID(), "srcset", imageView.session) + + case AltText: + updateInnerHTML(imageView.htmlID(), imageView.session) + + case ImageVerticalAlign, ImageHorizontalAlign: + updateCSSStyle(imageView.htmlID(), imageView.session) + } +} + +func (imageView *imageViewData) Set(tag string, value interface{}) bool { + return imageView.set(imageView.normalizeTag(tag), value) +} + +func (imageView *imageViewData) set(tag string, value interface{}) bool { + if value == nil { + imageView.remove(tag) + return true + } + + switch tag { + case Source: + if text, ok := value.(string); ok { + imageView.properties[Source] = text + updateProperty(imageView.htmlID(), "src", text, imageView.session) + if srcset := imageView.srcSet(text); srcset != "" { + updateProperty(imageView.htmlID(), "srcset", srcset, imageView.session) + } else { + removeProperty(imageView.htmlID(), "srcset", imageView.session) + } + return true + } + notCompatibleType(tag, value) + + case AltText: + if text, ok := value.(string); ok { + imageView.properties[AltText] = text + updateInnerHTML(imageView.htmlID(), imageView.session) + return true + } + notCompatibleType(tag, value) + + default: + if imageView.viewData.set(tag, value) { + switch tag { + case ImageVerticalAlign, ImageHorizontalAlign: + updateCSSStyle(imageView.htmlID(), imageView.session) + } + return true + } + } + + return false +} + +func (imageView *imageViewData) Get(tag string) interface{} { + return imageView.viewData.get(imageView.normalizeTag(tag)) +} + +func (imageView *imageViewData) srcSet(path string) string { + if srcset, ok := resources.imageSrcSets[path]; ok { + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + for i, src := range srcset { + if i > 0 { + buffer.WriteString(", ") + } + buffer.WriteString(src.path) + buffer.WriteString(fmt.Sprintf(" %gx", src.scale)) + } + return buffer.String() + } + return "" +} + +func (imageView *imageViewData) htmlTag() string { + return "img" +} + +/* +func (imageView *imageViewData) closeHTMLTag() bool { + return false +} +*/ + +func (imageView *imageViewData) htmlProperties(self View, buffer *strings.Builder) { + imageView.viewData.htmlProperties(self, buffer) + imageResource := GetImageViewSource(imageView, "") + if imageResource != "" { + buffer.WriteString(` src="`) + buffer.WriteString(imageResource) + buffer.WriteString(`"`) + if srcset := imageView.srcSet(imageResource); srcset != "" { + buffer.WriteString(` srcset="`) + buffer.WriteString(srcset) + buffer.WriteString(`"`) + } + } +} + +func (imageView *imageViewData) cssStyle(self View, builder cssBuilder) { + imageView.viewData.cssStyle(self, builder) + + if value, ok := enumProperty(imageView, Fit, imageView.session, 0); ok { + builder.add("object-fit", enumProperties[Fit].cssValues[value]) + } else { + builder.add("object-fit", "none") + } + + vAlign := GetImageViewVerticalAlign(imageView, "") + hAlign := GetImageViewHorizontalAlign(imageView, "") + if vAlign != CenterAlign || hAlign != CenterAlign { + var position string + switch hAlign { + case LeftAlign: + position = "left" + case RightAlign: + position = "right" + default: + position = "center" + } + + switch vAlign { + case TopAlign: + position += " top" + case BottomAlign: + position += " bottom" + default: + position += " center" + } + + builder.add("object-position", position) + } +} + +// GetImageViewSource returns the image URL of an ImageView subview. +// If the second argument (subviewID) is "" then a left position of the first argument (view) is returned +func GetImageViewSource(view View, subviewID string) string { + if image, ok := stringProperty(view, Source, view.Session()); ok { + return image + } + return "" +} + +// GetImageViewAltText returns an alternative text description of an ImageView subview. +// If the second argument (subviewID) is "" then a left position of the first argument (view) is returned +func GetImageViewAltText(view View, subviewID string) string { + if text, ok := stringProperty(view, AltText, view.Session()); ok { + return text + } + return "" +} + +// GetImageViewFit returns how the content of a replaced ImageView subview: +// NoneFit (0), ContainFit (1), CoverFit (2), FillFit (3), or ScaleDownFit (4). +// If the second argument (subviewID) is "" then a left position of the first argument (view) is returned +func GetImageViewFit(view View, subviewID string) int { + if value, ok := enumProperty(view, Fit, view.Session(), 0); ok { + return value + } + return 0 +} + +// GetImageViewVerticalAlign return the vertical align of an ImageView subview: TopAlign (0), BottomAlign (1), CenterAlign (2) +// If the second argument (subviewID) is "" then a left position of the first argument (view) is returned +func GetImageViewVerticalAlign(view View, subviewID string) int { + if align, ok := enumProperty(view, ImageVerticalAlign, view.Session(), LeftAlign); ok { + return align + } + return CenterAlign +} + +// GetImageViewHorizontalAlign return the vertical align of an ImageView subview: LeftAlign (0), RightAlign (1), CenterAlign (2) +// If the second argument (subviewID) is "" then a left position of the first argument (view) is returned +func GetImageViewHorizontalAlign(view View, subviewID string) int { + if align, ok := enumProperty(view, ImageHorizontalAlign, view.Session(), LeftAlign); ok { + return align + } + return CenterAlign +} diff --git a/init.go b/init.go new file mode 100644 index 0000000..e881d96 --- /dev/null +++ b/init.go @@ -0,0 +1,7 @@ +package rui + +func init() { + //resources.init() + defaultTheme.init() + defaultTheme.addText(defaultThemeText) +} diff --git a/keyEvents.go b/keyEvents.go new file mode 100644 index 0000000..1e9a9a9 --- /dev/null +++ b/keyEvents.go @@ -0,0 +1,271 @@ +package rui + +import "strings" + +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" + + // 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" +) + +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. + TimeStamp uint64 + + // Key is the key value of the key represented by the event. If the value has a printed representation, + // this attribute's value is the same as the char property. Otherwise, it's one of the key value strings + // specified in Key values. If the key can't be identified, its value is the string "Unidentified". + Key string + + // Code holds a string that identifies the physical key being pressed. The value is not affected + // by the current keyboard layout or modifier state, so a particular key will always return the same value. + Code string + + // Repeat == true if a key has been depressed long enough to trigger key repetition, otherwise false. + Repeat bool + + // CtrlKey == true if the control key was down when the event was fired. false otherwise. + CtrlKey bool + + // ShiftKey == true if the shift key was down when the event was fired. false otherwise. + ShiftKey bool + + // AltKey == true if the alt key was down when the event was fired. false otherwise. + AltKey bool + + // MetaKey == true if the meta key was down when the event was fired. false otherwise. + MetaKey bool +} + +func valueToKeyListeners(value interface{}) ([]func(View, KeyEvent), bool) { + if value == nil { + return nil, true + } + + switch value := value.(type) { + case func(View, KeyEvent): + return []func(View, KeyEvent){value}, true + + case func(KeyEvent): + fn := func(view View, event KeyEvent) { + value(event) + } + return []func(View, KeyEvent){fn}, true + + case func(View): + fn := func(view View, event KeyEvent) { + value(view) + } + return []func(View, KeyEvent){fn}, true + + case func(): + fn := func(view View, event KeyEvent) { + value() + } + return []func(View, KeyEvent){fn}, true + + case []func(View, KeyEvent): + if len(value) == 0 { + return nil, true + } + for _, fn := range value { + if fn == nil { + return nil, false + } + } + return value, true + + case []func(KeyEvent): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View, KeyEvent), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(view View, event KeyEvent) { + v(event) + } + } + return listeners, true + + case []func(View): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View, KeyEvent), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(view View, event KeyEvent) { + v(view) + } + } + return listeners, true + + case []func(): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View, KeyEvent), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(view View, event KeyEvent) { + v() + } + } + return listeners, true + + case []interface{}: + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View, KeyEvent), count) + for i, v := range value { + if v == nil { + return nil, false + } + switch v := v.(type) { + case func(View, KeyEvent): + listeners[i] = v + + case func(KeyEvent): + listeners[i] = func(view View, event KeyEvent) { + v(event) + } + + case func(View): + listeners[i] = func(view View, event KeyEvent) { + v(view) + } + + case func(): + listeners[i] = func(view View, event KeyEvent) { + v() + } + + default: + return nil, false + } + } + return listeners, true + } + + return nil, false +} + +var keyEvents = map[string]struct{ jsEvent, jsFunc string }{ + KeyDownEvent: {jsEvent: "onkeydown", jsFunc: "keyDownEvent"}, + KeyUpEvent: {jsEvent: "onkeyup", jsFunc: "keyUpEvent"}, +} + +func (view *viewData) setKeyListener(tag string, value interface{}) bool { + listeners, ok := valueToKeyListeners(value) + if !ok { + notCompatibleType(tag, value) + return false + } + + if listeners == nil { + view.removeKeyListener(tag) + } else if js, ok := keyEvents[tag]; ok { + view.properties[tag] = listeners + if view.created { + updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session()) + } + } else { + return false + } + return true +} + +func (view *viewData) removeKeyListener(tag string) { + delete(view.properties, tag) + if view.created { + if js, ok := keyEvents[tag]; ok { + updateProperty(view.htmlID(), js.jsEvent, "", view.Session()) + } + } +} + +func getKeyListeners(view View, subviewID string, tag string) []func(View, KeyEvent) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.Get(tag); value != nil { + if result, ok := value.([]func(View, KeyEvent)); ok { + return result + } + } + } + return []func(View, KeyEvent){} +} + +func keyEventsHtml(view View, buffer *strings.Builder) { + for tag, js := range keyEvents { + if listeners := getKeyListeners(view, "", tag); len(listeners) > 0 { + buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `) + } + } +} + +func handleKeyEvents(view View, tag string, data DataObject) { + listeners := getKeyListeners(view, "", tag) + if len(listeners) == 0 { + return + } + + getBool := func(tag string) bool { + if value, ok := data.PropertyValue(tag); ok && value == "1" { + return true + } + return false + } + + key, _ := data.PropertyValue("key") + code, _ := data.PropertyValue("code") + event := KeyEvent{ + TimeStamp: getTimeStamp(data), + Key: key, + Code: code, + Repeat: getBool("repeat"), + CtrlKey: getBool("ctrlKey"), + ShiftKey: getBool("shiftKey"), + AltKey: getBool("altKey"), + MetaKey: getBool("metaKey"), + } + + for _, listener := range listeners { + listener(view, event) + } +} + +// 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 "" then a value from the first argument (view) is returned. +func GetKeyDownListeners(view View, subviewID string) []func(View, KeyEvent) { + return getKeyListeners(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 "" then a value from the first argument (view) is returned. +func GetKeyUpListeners(view View, subviewID string) []func(View, KeyEvent) { + return getKeyListeners(view, subviewID, KeyUpEvent) +} diff --git a/listAdapter.go b/listAdapter.go new file mode 100644 index 0000000..efa3d69 --- /dev/null +++ b/listAdapter.go @@ -0,0 +1,83 @@ +package rui + +// ListAdapter - the list data source +type ListAdapter interface { + ListSize() int + ListItem(index int, session Session) View + IsListItemEnabled(index int) bool +} + +type textListAdapter struct { + items []string + views []View + params Params +} + +type viewListAdapter struct { + items []View +} + +// NewTextListAdapter create the new ListAdapter for a string list displaying. The second argument is parameters of a TextView item +func NewTextListAdapter(items []string, params Params) ListAdapter { + if items == nil { + return nil + } + adapter := new(textListAdapter) + adapter.items = items + if params != nil { + adapter.params = params + } else { + adapter.params = Params{} + } + adapter.views = make([]View, len(items)) + return adapter +} + +// NewTextListAdapter create the new ListAdapter for a view list displaying +func NewViewListAdapter(items []View) ListAdapter { + if items != nil { + adapter := new(viewListAdapter) + adapter.items = items + return adapter + } + return nil +} + +func (adapter *textListAdapter) ListSize() int { + return len(adapter.items) +} + +func (adapter *textListAdapter) ListItem(index int, session Session) View { + if index < 0 || index >= len(adapter.items) { + return nil + } + + if adapter.views[index] == nil { + adapter.params[Text] = adapter.items[index] + adapter.views[index] = NewTextView(session, adapter.params) + } + + return adapter.views[index] +} + +func (adapter *textListAdapter) IsListItemEnabled(index int) bool { + return true +} + +func (adapter *viewListAdapter) ListSize() int { + return len(adapter.items) +} + +func (adapter *viewListAdapter) ListItem(index int, session Session) View { + if index >= 0 && index < len(adapter.items) { + return adapter.items[index] + } + return nil +} + +func (adapter *viewListAdapter) IsListItemEnabled(index int) bool { + if index >= 0 && index < len(adapter.items) { + return !IsDisabled(adapter.items[index]) + } + return true +} diff --git a/listLayout.go b/listLayout.go new file mode 100644 index 0000000..dabc6eb --- /dev/null +++ b/listLayout.go @@ -0,0 +1,148 @@ +package rui + +import ( + "strings" +) + +const ( + // TopDownOrientation - subviews are arranged from top to bottom. Synonym of VerticalOrientation + TopDownOrientation = 0 + // StartToEndOrientation - subviews are arranged from left to right. Synonym of HorizontalOrientation + StartToEndOrientation = 1 + // BottomUpOrientation - subviews are arranged from bottom to top + BottomUpOrientation = 2 + // EndToStartOrientation - subviews are arranged from right to left + EndToStartOrientation = 3 + // WrapOff - subviews are scrolled and "true" if a new row/column starts + WrapOff = 0 + // WrapOn - the new row/column starts at bottom/right + WrapOn = 1 + // WrapReverse - the new row/column starts at top/left + WrapReverse = 2 +) + +// ListLayout - list-container of View +type ListLayout interface { + ViewsContainer +} + +type listLayoutData struct { + viewsContainerData +} + +// NewListLayout create new ListLayout object and return it +func NewListLayout(session Session, params Params) ListLayout { + view := new(listLayoutData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newListLayout(session Session) View { + return NewListLayout(session, nil) +} + +// Init initialize fields of ViewsAlignContainer by default values +func (listLayout *listLayoutData) Init(session Session) { + listLayout.viewsContainerData.Init(session) + listLayout.tag = "ListLayout" + listLayout.systemClass = "ruiListLayout" +} + +func (listLayout *listLayoutData) Remove(tag string) { + listLayout.remove(strings.ToLower(tag)) +} + +func (listLayout *listLayoutData) remove(tag string) { + listLayout.viewsContainerData.remove(tag) + switch tag { + case Orientation, Wrap, HorizontalAlign, VerticalAlign: + updateCSSStyle(listLayout.htmlID(), listLayout.session) + } +} + +func (listLayout *listLayoutData) Set(tag string, value interface{}) bool { + return listLayout.set(strings.ToLower(tag), value) +} + +func (listLayout *listLayoutData) set(tag string, value interface{}) bool { + if value == nil { + listLayout.remove(tag) + return true + } + + if listLayout.viewsContainerData.set(tag, value) { + switch tag { + case Orientation, Wrap, HorizontalAlign, VerticalAlign: + updateCSSStyle(listLayout.htmlID(), listLayout.session) + } + return true + } + 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) + } + } +} + +// GetListVerticalAlign returns the vertical align of a ListLayout or ListView sibview: +// TopAlign (0), BottomAlign (1), CenterAlign (2), or StretchAlign (3) +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListVerticalAlign(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return LeftAlign + } + result, _ := enumProperty(view, VerticalAlign, view.Session(), 0) + return result +} + +// GetListHorizontalAlign returns the vertical align of a ListLayout or ListView subview: +// LeftAlign (0), RightAlign (1), CenterAlign (2), or StretchAlign (3) +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListHorizontalAlign(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return TopAlign + } + result, _ := enumProperty(view, HorizontalAlign, view.Session(), 0) + return result +} + +// GetListOrientation returns the orientation of a ListLayout or ListView subview: +// TopDownOrientation (0), StartToEndOrientation (1), BottomUpOrientation (2), or EndToStartOrientation (3) +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListOrientation(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return 0 + } + orientation, _ := getOrientation(view, view.Session()) + return orientation +} + +// GetListWrap returns the wrap type of a ListLayout or ListView subview: +// WrapOff (0), WrapOn (1), or WrapReverse (2) +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListWrap(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := enumProperty(view, Wrap, view.Session(), 0); ok { + return result + } + } + return WrapOff +} diff --git a/listView.go b/listView.go new file mode 100644 index 0000000..449e69c --- /dev/null +++ b/listView.go @@ -0,0 +1,1278 @@ +package rui + +import ( + "fmt" + "strconv" + "strings" +) + +const ( + // ListItemClickedEvent is the constant for "list-item-clicked" property tag. + // The "list-item-clicked" event occurs when the user clicks on an item in the list. + // The main listener format: func(ListView, int), where the second argument is the item index. + ListItemClickedEvent = "list-item-clicked" + // ListItemSelectedEvent is the constant for "list-item-selected" property tag. + // The "list-item-selected" event occurs when a list item becomes selected. + // The main listener format: func(ListView, int), where the second argument is the item index. + ListItemSelectedEvent = "list-item-selected" + // ListItemCheckedEvent is the constant for "list-item-checked" property tag. + // The "list-item-checked" event occurs when a list item checkbox becomes checked/unchecked. + // The main listener format: func(ListView, []int), where the second argument is the array of checked item indexes. + ListItemCheckedEvent = "list-item-checked" + // ListItemStyle is the constant for "list-item-style" property tag + // The "list-item-style" string property defines the style of an unselected item + ListItemStyle = "list-item-style" + // CurrentStyle is the constant for "current-style" property tag + // The "current-style" string property defines the style of the selected item when the ListView is focused. + CurrentStyle = "current-style" + // CurrentInactiveStyle is the constant for "current-inactive-style" property tag + // The "current-inactive-style" string property defines the style of the selected item when the ListView is unfocused. + CurrentInactiveStyle = "current-inactive-style" +) + +const ( + // VerticalOrientation is the vertical ListView orientation + VerticalOrientation = 0 + // HorizontalOrientation is the horizontal ListView orientation + HorizontalOrientation = 1 + + // NoneCheckbox is value of "checkbox" property: no checkbox + NoneCheckbox = 0 + // SingleCheckbox is value of "checkbox" property: only one item can be checked + SingleCheckbox = 1 + // MultipleCheckbox is value of "checkbox" property: several items can be checked + MultipleCheckbox = 2 +) + +// ListView - the list view interface +type ListView interface { + View + ParanetView + // ReloadListViewData updates ListView content + ReloadListViewData() + + getCheckedItems() []int + getItemFrames() []Frame +} + +type listViewData struct { + viewData + adapter ListAdapter + clickedListeners []func(ListView, int) + selectedListeners []func(ListView, int) + checkedListeners []func(ListView, []int) + items []View + itemFrame []Frame + checkedItem []int +} + +// NewListView creates the new list view +func NewListView(session Session, params Params) ListView { + view := new(listViewData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newListView(session Session) View { + return NewListView(session, nil) +} + +// Init initialize fields of ViewsContainer by default values +func (listView *listViewData) Init(session Session) { + listView.viewData.Init(session) + listView.tag = "ListView" + listView.systemClass = "ruiListView" + listView.items = []View{} + listView.itemFrame = []Frame{} + listView.checkedItem = []int{} + listView.clickedListeners = []func(ListView, int){} + listView.selectedListeners = []func(ListView, int){} + listView.checkedListeners = []func(ListView, []int){} +} + +func (listView *listViewData) Views() []View { + return listView.items +} + +func (listView *listViewData) normalizeTag(tag string) string { + tag = strings.ToLower(tag) + switch tag { + case HorizontalAlign: + tag = ItemHorizontalAlign + + case VerticalAlign: + tag = ItemVerticalAlign + } + return tag +} + +func (listView *listViewData) Remove(tag string) { + listView.remove(listView.normalizeTag(tag)) +} + +func (listView *listViewData) remove(tag string) { + switch tag { + case Checked: + listView.checkedItem = []int{} + updateInnerHTML(listView.htmlID(), listView.session) + + case Items: + listView.adapter = nil + updateInnerHTML(listView.htmlID(), listView.session) + + case Orientation, Wrap: + delete(listView.properties, tag) + updateCSSStyle(listView.htmlID(), listView.session) + + case Current: + current := GetListViewCurrent(listView, "") + delete(listView.properties, tag) + updateInnerHTML(listView.htmlID(), listView.session) + if current != -1 { + for _, listener := range listView.selectedListeners { + listener(listView, -1) + } + } + + case ItemWidth, ItemHeight, ItemHorizontalAlign, ItemVerticalAlign, ItemCheckbox, + CheckboxHorizontalAlign, CheckboxVerticalAlign, ListItemStyle, CurrentStyle, CurrentInactiveStyle: + + delete(listView.properties, tag) + updateInnerHTML(listView.htmlID(), listView.session) + + case ListItemClickedEvent: + if len(listView.clickedListeners) > 0 { + listView.clickedListeners = []func(ListView, int){} + } + + case ListItemSelectedEvent: + if len(listView.selectedListeners) > 0 { + listView.selectedListeners = []func(ListView, int){} + } + + case ListItemCheckedEvent: + if len(listView.checkedListeners) > 0 { + listView.checkedListeners = []func(ListView, []int){} + } + + default: + listView.viewData.remove(tag) + } +} + +func (listView *listViewData) Set(tag string, value interface{}) bool { + return listView.set(listView.normalizeTag(tag), value) +} + +func (listView *listViewData) set(tag string, value interface{}) bool { + if value == nil { + listView.remove(tag) + return true + } + + result := false + + switch tag { + + case ListItemClickedEvent: + listeners := listView.valueToItemListeners(value) + if listeners == nil { + notCompatibleType(tag, value) + return false + } + listView.clickedListeners = listeners + return true + + case ListItemSelectedEvent: + listeners := listView.valueToItemListeners(value) + if listeners == nil { + notCompatibleType(tag, value) + return false + } + listView.selectedListeners = listeners + return true + + case ListItemCheckedEvent: + return listView.setItemCheckedEvent(value) + + case Checked: + return listView.setChecked(value) + + case Items: + result = listView.setItems(value) + + case Current: + oldCurrent := GetListViewCurrent(listView, "") + if listView.setIntProperty(Current, value) { + current := GetListViewCurrent(listView, "") + if oldCurrent != current { + updateInnerHTML(listView.htmlID(), listView.session) + for _, listener := range listView.selectedListeners { + listener(listView, current) + } + } + return true + } + + case Orientation, Wrap: + if listView.viewData.set(tag, value) { + updateCSSStyle(listView.htmlID(), listView.session) + return true + } + + case ItemWidth, ItemHeight: + result = listView.setSizeProperty(tag, value) + + case ItemHorizontalAlign, ItemVerticalAlign, ItemCheckbox, CheckboxHorizontalAlign, CheckboxVerticalAlign: + result = listView.setEnumProperty(tag, value, enumProperties[tag].values) + + case ListItemStyle, CurrentStyle, CurrentInactiveStyle: + switch value := value.(type) { + case string: + listView.properties[tag] = value + result = true + + default: + notCompatibleType(tag, value) + return false + } + + default: + return listView.viewData.set(tag, value) + } + + if result { + updateInnerHTML(listView.htmlID(), listView.session) + } + + return result +} + +func (listView *listViewData) setItemCheckedEvent(value interface{}) bool { + switch value := value.(type) { + case func(ListView, []int): + listView.checkedListeners = []func(ListView, []int){value} + + case func([]int): + fn := func(view ListView, date []int) { + value(date) + } + listView.checkedListeners = []func(ListView, []int){fn} + + case []func(ListView, []int): + listView.checkedListeners = value + + case []func([]int): + listeners := make([]func(ListView, []int), len(value)) + for i, val := range value { + if val == nil { + notCompatibleType(ListItemCheckedEvent, val) + return false + } + + listeners[i] = func(view ListView, date []int) { + val(date) + } + } + listView.checkedListeners = listeners + + case []interface{}: + listeners := make([]func(ListView, []int), len(value)) + for i, val := range value { + if val == nil { + notCompatibleType(ListItemCheckedEvent, val) + return false + } + + switch val := val.(type) { + case func(ListView, []int): + listeners[i] = val + + case func([]int): + listeners[i] = func(view ListView, checked []int) { + val(checked) + } + + default: + notCompatibleType(ListItemCheckedEvent, val) + return false + } + } + listView.checkedListeners = listeners + } + return true +} + +func (listView *listViewData) Get(tag string) interface{} { + return listView.get(listView.normalizeTag(tag)) +} + +func (listView *listViewData) get(tag string) interface{} { + switch tag { + case ListItemClickedEvent: + return listView.clickedListeners + + case ListItemSelectedEvent: + return listView.selectedListeners + + case ListItemCheckedEvent: + return listView.checkedListeners + + case Checked: + return listView.checkedItem + + case Items: + return listView.adapter + + case ListItemStyle: + return listView.listItemStyle() + + case CurrentStyle: + return listView.currentStyle() + + case CurrentInactiveStyle: + return listView.currentInactiveStyle() + } + return listView.viewData.get(tag) +} + +func (listView *listViewData) setItems(value interface{}) bool { + switch value := value.(type) { + case []string: + listView.adapter = NewTextListAdapter(value, nil) + + case []DataValue: + hasObject := false + for _, val := range value { + if val.IsObject() { + hasObject = true + break + } + } + + if hasObject { + items := make([]View, len(value)) + for i, val := range value { + if val.IsObject() { + if view := CreateViewFromObject(listView.session, val.Object()); view != nil { + items[i] = view + } else { + return false + } + } else { + items[i] = NewTextView(listView.session, Params{Text: val.Value()}) + } + } + listView.adapter = NewViewListAdapter(items) + } else { + items := make([]string, len(value)) + for i, val := range value { + items[i] = val.Value() + } + listView.adapter = NewTextListAdapter(items, nil) + } + + case []interface{}: + items := make([]View, len(value)) + for i, val := range value { + switch value := val.(type) { + case View: + items[i] = value + + case string: + items[i] = NewTextView(listView.session, Params{Text: value}) + + case fmt.Stringer: + items[i] = NewTextView(listView.session, Params{Text: value.String()}) + + case float32: + items[i] = NewTextView(listView.session, Params{Text: fmt.Sprintf("%g", float64(value))}) + + case float64: + items[i] = NewTextView(listView.session, Params{Text: fmt.Sprintf("%g", value)}) + + default: + if n, ok := isInt(val); ok { + items[i] = NewTextView(listView.session, Params{Text: strconv.Itoa(n)}) + } else { + notCompatibleType(Items, value) + return false + } + } + } + listView.adapter = NewViewListAdapter(items) + + case []View: + listView.adapter = NewViewListAdapter(value) + + case ListAdapter: + listView.adapter = value + + default: + notCompatibleType(Items, value) + return false + } + + size := listView.adapter.ListSize() + listView.items = make([]View, size) + listView.itemFrame = make([]Frame, size) + + return true +} + +func (listView *listViewData) valueToItemListeners(value interface{}) []func(ListView, int) { + if value == nil { + return []func(ListView, int){} + } + + switch value := value.(type) { + case func(ListView, int): + return []func(ListView, int){value} + + case func(int): + fn := func(view ListView, index int) { + value(index) + } + return []func(ListView, int){fn} + + case []func(ListView, int): + return value + + case []func(int): + listeners := make([]func(ListView, int), len(value)) + for i, val := range value { + if val == nil { + return nil + } + listeners[i] = func(view ListView, index int) { + val(index) + } + } + return listeners + + case []interface{}: + listeners := make([]func(ListView, int), len(value)) + for i, val := range value { + if val == nil { + return nil + } + switch val := val.(type) { + case func(ListView, int): + listeners[i] = val + + case func(int): + listeners[i] = func(view ListView, index int) { + val(index) + } + + default: + return nil + } + } + return listeners + } + + return nil +} + +func (listView *listViewData) setChecked(value interface{}) bool { + var checked []int + if value == nil { + checked = []int{} + } else { + switch value := value.(type) { + case int: + checked = []int{value} + + case []int: + checked = value + + default: + return false + } + } + + switch GetListViewCheckbox(listView, "") { + case SingleCheckbox: + count := len(checked) + if count > 1 { + return false + } + + if len(listView.checkedItem) > 0 && + (count == 0 || listView.checkedItem[0] != checked[0]) { + listView.updateCheckboxItem(listView.checkedItem[0], false) + } + + if count == 1 { + listView.updateCheckboxItem(checked[0], true) + } + + case MultipleCheckbox: + inSlice := func(n int, slice []int) bool { + for _, n2 := range slice { + if n2 == n { + return true + } + } + return false + } + + for _, n := range listView.checkedItem { + if !inSlice(n, checked) { + listView.updateCheckboxItem(n, false) + } + } + + for _, n := range checked { + if !inSlice(n, listView.checkedItem) { + listView.updateCheckboxItem(n, true) + } + } + + default: + return false + } + + listView.checkedItem = checked + for _, listener := range listView.checkedListeners { + listener(listView, listView.checkedItem) + } + return true +} + +func (listView *listViewData) Focusable() bool { + return true +} + +func (listView *listViewData) ReloadListViewData() { + itemCount := 0 + if listView.adapter != nil { + itemCount = listView.adapter.ListSize() + + if itemCount != len(listView.items) { + listView.items = make([]View, itemCount) + listView.itemFrame = make([]Frame, itemCount) + } + + for i := 0; i < itemCount; i++ { + listView.items[i] = listView.adapter.ListItem(i, listView.Session()) + } + } else if len(listView.items) > 0 { + listView.items = []View{} + listView.itemFrame = []Frame{} + } + + updateInnerHTML(listView.htmlID(), listView.session) +} + +func (listView *listViewData) getCheckedItems() []int { + return listView.checkedItem +} + +func (listView *listViewData) getItemFrames() []Frame { + return listView.itemFrame +} + +func (listView *listViewData) htmlProperties(self View, buffer *strings.Builder) { + buffer.WriteString(`onfocus="listViewFocusEvent(this, event)" onblur="listViewBlurEvent(this, event)"`) + buffer.WriteString(` onkeydown="listViewKeyDownEvent(this, event)" data-focusitemstyle="`) + buffer.WriteString(listView.currentStyle()) + buffer.WriteString(`" data-bluritemstyle="`) + buffer.WriteString(listView.currentInactiveStyle()) + buffer.WriteString(`"`) + current := GetListViewCurrent(listView, "") + if listView.adapter != nil && current >= 0 && current < listView.adapter.ListSize() { + buffer.WriteString(` data-current="`) + buffer.WriteString(listView.htmlID()) + buffer.WriteRune('-') + buffer.WriteString(strconv.Itoa(current)) + buffer.WriteRune('"') + } +} + +func (listView *listViewData) cssStyle(self View, builder cssBuilder) { + listView.viewData.cssStyle(self, builder) + + if GetListWrap(listView, "") != WrapOff { + switch GetListOrientation(listView, "") { + case TopDownOrientation, BottomUpOrientation: + builder.add(`max-height`, `100%`) + default: + builder.add(`max-width`, `100%`) + } + } +} + +func (listView *listViewData) itemAlign(self View, buffer *strings.Builder) { + values := enumProperties[ItemHorizontalAlign].cssValues + if hAlign := GetListItemHorizontalAlign(listView, ""); hAlign >= 0 && hAlign < len(values) { + buffer.WriteString(" justify-items: ") + buffer.WriteString(values[hAlign]) + buffer.WriteRune(';') + } + + values = enumProperties[ItemVerticalAlign].cssValues + if vAlign := GetListItemVerticalAlign(listView, ""); vAlign >= 0 && vAlign < len(values) { + buffer.WriteString(" align-items: ") + buffer.WriteString(values[vAlign]) + buffer.WriteRune(';') + } +} + +func (listView *listViewData) itemSize(self View, buffer *strings.Builder) { + if itemWidth := GetListItemWidth(listView, ""); itemWidth.Type != Auto { + buffer.WriteString(` min-width: `) + buffer.WriteString(itemWidth.cssString("")) + buffer.WriteRune(';') + } + + if itemHeight := GetListItemHeight(listView, ""); itemHeight.Type != Auto { + buffer.WriteString(` min-height: `) + buffer.WriteString(itemHeight.cssString("")) + buffer.WriteRune(';') + } +} + +func (listView *listViewData) getDivs(self View, checkbox, hCheckboxAlign, vCheckboxAlign int) (string, string, string) { + session := listView.Session() + + contentBuilder := allocStringBuilder() + defer freeStringBuilder(contentBuilder) + + contentBuilder.WriteString(`
`) + } else { + vCheckboxAlign = TopAlign + onDivBuilder.WriteString(`
`) + } + } else { + if hCheckboxAlign == RightAlign { + onDivBuilder.WriteString(`
`) + } else { + onDivBuilder.WriteString(`
`) + } + switch vCheckboxAlign { + case BottomAlign: + onDivBuilder.WriteString(` align-items: end;`) + + case CenterAlign: + onDivBuilder.WriteString(` align-items: center;`) + + default: + onDivBuilder.WriteString(` align-items: start;`) + } + } + + onDivBuilder.WriteString(`">`) + + offDivBuilder := allocStringBuilder() + defer freeStringBuilder(offDivBuilder) + + offDivBuilder.WriteString(onDivBuilder.String()) + + if checkbox == SingleCheckbox { + offDivBuilder.WriteString(session.radiobuttonOffImage()) + onDivBuilder.WriteString(session.radiobuttonOnImage()) + } else { + offDivBuilder.WriteString(session.checkboxOffImage()) + onDivBuilder.WriteString(session.checkboxOnImage()) + } + + onDivBuilder.WriteString("
") + offDivBuilder.WriteString("
") + + return onDivBuilder.String(), offDivBuilder.String(), contentBuilder.String() +} + +func (listView *listViewData) checkboxItemDiv(self View, checkbox, hCheckboxAlign, vCheckboxAlign int) string { + itemStyleBuilder := allocStringBuilder() + defer freeStringBuilder(itemStyleBuilder) + + itemStyleBuilder.WriteString(`
`) + return itemStyleBuilder.String() + +} + +func (listView *listViewData) getItemView(index int) View { + if listView.adapter == nil || index < 0 || index >= listView.adapter.ListSize() { + return nil + } + + size := listView.adapter.ListSize() + if size != len(listView.items) { + listView.items = make([]View, size) + } + + if listView.items[index] == nil { + listView.items[index] = listView.adapter.ListItem(index, listView.Session()) + } + + return listView.items[index] +} + +func (listView *listViewData) listItemStyle() string { + if value := listView.getRaw(ListItemStyle); value != nil { + if style, ok := value.(string); ok { + if style, ok = listView.session.resolveConstants(style); ok { + return style + } + } + } + return "ruiListItem" +} + +func (listView *listViewData) currentStyle() string { + if value := listView.getRaw(CurrentStyle); value != nil { + if style, ok := value.(string); ok { + if style, ok = listView.session.resolveConstants(style); ok { + return style + } + } + } + return "ruiListItemFocused" +} + +func (listView *listViewData) currentInactiveStyle() string { + if value := listView.getRaw(CurrentInactiveStyle); value != nil { + if style, ok := value.(string); ok { + if style, ok = listView.session.resolveConstants(style); ok { + return style + } + } + } + return "ruiListItemSelected" +} + +func (listView *listViewData) checkboxSubviews(self View, buffer *strings.Builder, checkbox int) { + count := listView.adapter.ListSize() + listViewID := listView.htmlID() + + hCheckboxAlign := GetListViewCheckboxHorizontalAlign(listView, "") + vCheckboxAlign := GetListViewCheckboxVerticalAlign(listView, "") + + itemDiv := listView.checkboxItemDiv(self, checkbox, hCheckboxAlign, vCheckboxAlign) + onDiv, offDiv, contentDiv := listView.getDivs(self, checkbox, hCheckboxAlign, vCheckboxAlign) + + current := GetListViewCurrent(listView, "") + checkedItems := GetListViewCheckedItems(listView, "") + for i := 0; i < count; i++ { + buffer.WriteString(`
`) + buffer.WriteString(itemDiv) + + checked := false + for _, index := range checkedItems { + if index == i { + buffer.WriteString(onDiv) + checked = true + break + } + } + if !checked { + buffer.WriteString(offDiv) + } + buffer.WriteString(contentDiv) + + if view := listView.getItemView(i); view != nil { + //view.setNoResizeEvent() + viewHTML(view, buffer) + } else { + buffer.WriteString("ERROR: invalid item view") + } + + buffer.WriteString(`
`) + } +} + +func (listView *listViewData) noneCheckboxSubviews(self View, buffer *strings.Builder) { + count := listView.adapter.ListSize() + listViewID := listView.htmlID() + + itemStyleBuilder := allocStringBuilder() + defer freeStringBuilder(itemStyleBuilder) + + itemStyleBuilder.WriteString(`data-left="0" data-top="0" data-width="0" data-height="0" style="max-width: 100%; max-height: 100%; display: grid;`) + + listView.itemAlign(self, itemStyleBuilder) + listView.itemSize(self, itemStyleBuilder) + + itemStyleBuilder.WriteString(`" onclick="listItemClickEvent(this, event)"`) + itemStyle := itemStyleBuilder.String() + + current := GetListViewCurrent(listView, "") + for i := 0; i < count; i++ { + buffer.WriteString(`
`) + + if view := listView.getItemView(i); view != nil { + //view.setNoResizeEvent() + viewHTML(view, buffer) + } else { + buffer.WriteString("ERROR: invalid item view") + } + + buffer.WriteString(`
`) + } +} + +func (listView *listViewData) updateCheckboxItem(index int, checked bool) { + + checkbox := GetListViewCheckbox(listView, "") + hCheckboxAlign := GetListViewCheckboxHorizontalAlign(listView, "") + vCheckboxAlign := GetListViewCheckboxVerticalAlign(listView, "") + onDiv, offDiv, contentDiv := listView.getDivs(listView, checkbox, hCheckboxAlign, vCheckboxAlign) + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + buffer.WriteString(`updateInnerHTML('`) + buffer.WriteString(listView.htmlID()) + buffer.WriteRune('-') + buffer.WriteString(strconv.Itoa(index)) + buffer.WriteString(`', '`) + + buffer.WriteString(listView.checkboxItemDiv(listView, checkbox, hCheckboxAlign, vCheckboxAlign)) + if checked { + buffer.WriteString(onDiv) + } else { + buffer.WriteString(offDiv) + } + buffer.WriteString(contentDiv) + + session := listView.Session() + if listView.adapter != nil { + if view := listView.getItemView(index); view != nil { + view.setNoResizeEvent() + viewHTML(view, buffer) + } else { + buffer.WriteString("ERROR: invalid item view") + } + } + buffer.WriteString(`
');`) + + session.runScript(buffer.String()) +} + +func (listView *listViewData) htmlSubviews(self View, buffer *strings.Builder) { + if listView.adapter == nil { + return + } + if listView.adapter.ListSize() == 0 { + return + } + + if !listView.session.ignoreViewUpdates() { + listView.session.setIgnoreViewUpdates(true) + defer listView.session.setIgnoreViewUpdates(false) + } + + checkbox := GetListViewCheckbox(listView, "") + if checkbox == NoneCheckbox { + listView.noneCheckboxSubviews(self, buffer) + } else { + listView.checkboxSubviews(self, buffer, checkbox) + } +} + +func (listView *listViewData) handleCommand(self View, command string, data DataObject) bool { + switch command { + case "itemSelected": + if text, ok := data.PropertyValue(`number`); ok { + if number, err := strconv.Atoi(text); err == nil { + listView.properties[Current] = number + for _, listener := range listView.selectedListeners { + listener(listView, number) + } + } + } + + case "itemUnselected": + delete(listView.properties, Current) + for _, listener := range listView.selectedListeners { + listener(listView, -1) + } + + case "itemClick": + listView.onItemClick() + + default: + return listView.viewData.handleCommand(self, command, data) + } + + return true +} + +func (listView *listViewData) onItemClick() { + current := GetListViewCurrent(listView, "") + if current >= 0 && !IsDisabled(listView) { + checkbox := GetListViewCheckbox(listView, "") + m: + switch checkbox { + case SingleCheckbox: + if len(listView.checkedItem) == 0 { + listView.checkedItem = []int{current} + listView.updateCheckboxItem(current, true) + } else if listView.checkedItem[0] != current { + listView.updateCheckboxItem(listView.checkedItem[0], false) + listView.checkedItem[0] = current + listView.updateCheckboxItem(current, true) + } + + case MultipleCheckbox: + for i, index := range listView.checkedItem { + if index == current { + listView.updateCheckboxItem(index, false) + count := len(listView.checkedItem) + if count == 1 { + listView.checkedItem = []int{} + } else if i == 0 { + listView.checkedItem = listView.checkedItem[1:] + } else if i == count-1 { + listView.checkedItem = listView.checkedItem[:i] + } else { + listView.checkedItem = append(listView.checkedItem[:i], listView.checkedItem[i+1:]...) + } + break m + } + } + + listView.updateCheckboxItem(current, true) + listView.checkedItem = append(listView.checkedItem, current) + } + + if checkbox != NoneCheckbox { + for _, listener := range listView.checkedListeners { + listener(listView, listView.checkedItem) + } + } + for _, listener := range listView.clickedListeners { + listener(listView, current) + } + } +} + +func (listView *listViewData) onItemResize(self View, index int, x, y, width, height float64) { + if index >= 0 && index < len(listView.itemFrame) { + listView.itemFrame[index] = Frame{Left: x, Top: y, Width: width, Height: height} + } +} + +// GetVerticalAlign return the vertical align of a list: TopAlign (0), BottomAlign (1), CenterAlign (2), StretchAlign (3) +func GetVerticalAlign(view View) int { + if align, ok := enumProperty(view, VerticalAlign, view.Session(), TopAlign); ok { + return align + } + return TopAlign +} + +// GetHorizontalAlign return the vertical align of a list: LeftAlign (0), RightAlign (1), CenterAlign (2), StretchAlign (3) +func GetHorizontalAlign(view View) int { + if align, ok := enumProperty(view, HorizontalAlign, view.Session(), LeftAlign); ok { + return align + } + return LeftAlign +} + +// GetListItemClickedListeners returns a ListItemClickedListener of the ListView. +// If there are no listeners then the empty list is returned +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListItemClickedListeners(view View, subviewID string) []func(ListView, int) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.Get(ListItemClickedEvent); value != nil { + if result, ok := value.([]func(ListView, int)); ok { + return result + } + } + } + return []func(ListView, int){} +} + +// GetListItemSelectedListeners returns a ListItemSelectedListener of the ListView. +// If there are no listeners then the empty list is returned +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListItemSelectedListeners(view View, subviewID string) []func(ListView, int) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.Get(ListItemSelectedEvent); value != nil { + if result, ok := value.([]func(ListView, int)); ok { + return result + } + } + } + return []func(ListView, int){} +} + +// GetListItemCheckedListeners returns a ListItemCheckedListener of the ListView. +// If there are no listeners then the empty list is returned +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListItemCheckedListeners(view View, subviewID string) []func(ListView, []int) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.Get(ListItemCheckedEvent); value != nil { + if result, ok := value.([]func(ListView, []int)); ok { + return result + } + } + } + return []func(ListView, []int){} +} + +// GetListViewCurrent returns the index of the ListView selected item or <0 if there is no a selected item. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListViewCurrent(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := intProperty(view, Current, view.Session(), -1); ok { + return result + } + } + return -1 +} + +// GetListItemWidth returns the width of a ListView item. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListItemWidth(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + result, _ := sizeProperty(view, ItemWidth, view.Session()) + return result + } + return AutoSize() +} + +// GetListItemHeight returns the height of a ListView item. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListItemHeight(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + result, _ := sizeProperty(view, ItemHeight, view.Session()) + return result + } + return AutoSize() +} + +// GetListViewCheckbox returns the ListView checkbox type: NoneCheckbox (0), SingleCheckbox (1), or MultipleCheckbox (2). +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListViewCheckbox(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + result, _ := enumProperty(view, ItemCheckbox, view.Session(), 0) + return result + } + return 0 +} + +// GetListViewCheckedItems returns the array of ListView checked items. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListViewCheckedItems(view View, subviewID string) []int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if listView, ok := view.(ListView); ok { + checkedItems := listView.getCheckedItems() + switch GetListViewCheckbox(view, "") { + case NoneCheckbox: + return []int{} + + case SingleCheckbox: + if len(checkedItems) > 1 { + return []int{checkedItems[0]} + } + } + + return checkedItems + } + } + return []int{} +} + +// IsListViewCheckedItem returns true if the ListView item with index is checked, false otherwise. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func IsListViewCheckedItem(view View, subviewID string, index int) bool { + for _, n := range GetListViewCheckedItems(view, subviewID) { + if n == index { + return true + } + } + return false +} + +// GetListViewCheckboxVerticalAlign returns the vertical align of the ListView checkbox: +// TopAlign (0), BottomAlign (1), CenterAlign (2) +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListViewCheckboxVerticalAlign(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if align, ok := enumProperty(view, CheckboxVerticalAlign, view.Session(), TopAlign); ok { + return align + } + } + return TopAlign +} + +// GetListViewCheckboxHorizontalAlign returns the horizontal align of the ListView checkbox: +// LeftAlign (0), RightAlign (1), CenterAlign (2) +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListViewCheckboxHorizontalAlign(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if align, ok := enumProperty(view, CheckboxHorizontalAlign, view.Session(), LeftAlign); ok { + return align + } + } + return LeftAlign +} + +// GetListItemVerticalAlign returns the vertical align of the ListView item content: +// TopAlign (0), BottomAlign (1), CenterAlign (2) +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListItemVerticalAlign(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if align, ok := enumProperty(view, ItemVerticalAlign, view.Session(), TopAlign); ok { + return align + } + } + return TopAlign +} + +// ItemHorizontalAlign returns the horizontal align of the ListView item content: +// LeftAlign (0), RightAlign (1), CenterAlign (2), StretchAlign (3) +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListItemHorizontalAlign(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if align, ok := enumProperty(view, ItemHorizontalAlign, view.Session(), LeftAlign); ok { + return align + } + } + return LeftAlign +} + +// GetListItemFrame - returns the location and size of the ListView item in pixels. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListItemFrame(view View, subviewID string, index int) Frame { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if listView, ok := view.(ListView); ok { + itemFrames := listView.getItemFrames() + if index >= 0 && index < len(itemFrames) { + return itemFrames[index] + } + } + } + return Frame{Left: 0, Top: 0, Width: 0, Height: 0} +} + +// GetListViewAdapter - returns the ListView adapter. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetListViewAdapter(view View, subviewID string) ListAdapter { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.Get(Items); value != nil { + if adapter, ok := value.(ListAdapter); ok { + return adapter + } + } + } + return nil +} + +// ReloadListViewData updates ListView content +// If the second argument (subviewID) is "" then content the first argument (view) is updated. +func ReloadListViewData(view View, subviewID string) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if listView, ok := view.(ListView); ok { + listView.ReloadListViewData() + } + } +} diff --git a/mediaPlayer.go b/mediaPlayer.go new file mode 100644 index 0000000..33138c1 --- /dev/null +++ b/mediaPlayer.go @@ -0,0 +1,1162 @@ +package rui + +import ( + "fmt" + "math" + "strconv" + "strings" +) + +const ( + // Controls is the constant for the "autoplay" controls tag. + // If the "controls" bool property is "true", the browser will offer controls to allow the user + // to control audio/video playback, including volume, seeking, and pause/resume playback. + // Its default value is false. + Controls = "controls" + // Loop is the constant for the "loop" property tag. + // If the "loop" bool property is "true", the audio/video player will automatically seek back + // to the start upon reaching the end of the audio/video. + // Its default value is false. + Loop = "loop" + // Muted is the constant for the "muted" property tag. + // The "muted" bool property indicates whether the audio/video will be initially silenced. + // Its default value is false. + Muted = "muted" + // Preload is the constant for the "preload" property tag. + // The "preload" int property is intended to provide a hint to the browser about what + // the author thinks will lead to the best user experience. It may have one of the following values: + // PreloadNone (0), PreloadMetadata (1), and PreloadAuto (2) + // The default value is different for each browser. + Preload = "preload" + + // AbortEvent is the constant for the "abort-event" property tag. + // The "abort-event" event fired when the resource was not fully loaded, but not as the result of an error. + AbortEvent = "abort-event" + // CanPlayEvent is the constant for the "can-play-event" property tag. + // The "can-play-event" event occurs when the browser can play the media, but estimates that not enough data has been + // loaded to play the media up to its end without having to stop for further buffering of content. + CanPlayEvent = "can-play-event" + // CanPlayThroughEvent is the constant for the "can-play-through-event" property tag. + // The "can-play-through-event" event occurs when the browser estimates it can play the media up + // to its end without stopping for content buffering. + CanPlayThroughEvent = "can-play-through-event" + // CompleteEvent is the constant for the "complete-event" property tag. + // The "complete-event" event occurs when the rendering of an OfflineAudioContext is terminated. + CompleteEvent = "complete-event" + // DurationChangedEvent is the constant for the "duration-changed-event" property tag. + // The "duration-changed-event" event occurs when the duration attribute has been updated. + DurationChangedEvent = "duration-changed-event" + // EmptiedEvent is the constant for the "emptied-event" property tag. + // The "emptied-event" event occurs when the media has become empty; for example, this event is sent if the media has already been loaded + // (or partially loaded), and the HTMLMediaElement.load method is called to reload it. + EmptiedEvent = "emptied-event" + // EndedEvent is the constant for the "ended-event" property tag. + // The "ended-event" event occurs when the playback has stopped because the end of the media was reached. + EndedEvent = "ended-event" + // LoadedDataEvent is the constant for the "loaded-data-event" property tag. + // The "loaded-data-event" event occurs when the first frame of the media has finished loading. + LoadedDataEvent = "loaded-data-event" + // LoadedMetadataEvent is the constant for the "loaded-metadata-event" property tag. + // The "loaded-metadata-event" event occurs when the metadata has been loaded. + LoadedMetadataEvent = "loaded-metadata-event" + // LoadStartEvent is the constant for the "load-start-event" property tag. + // The "load-start-event" event is fired when the browser has started to load a resource. + LoadStartEvent = "load-start-event" + // PauseEvent is the constant for the "pause-event" property tag. + // The "pause-event" event occurs when the playback has been paused. + PauseEvent = "pause-event" + // PlayEvent is the constant for the "play-event" property tag. + // The "play-event" event occurs when the playback has begun. + PlayEvent = "play-event" + // PlayingEvent is the constant for the "playing-event" property tag. + // The "playing-event" event occurs when the playback is ready to start after having been paused or delayed due to lack of data. + PlayingEvent = "playing-event" + // ProgressEvent is the constant for the "progress-event" property tag. + // The "progress-event" event is fired periodically as the browser loads a resource. + ProgressEvent = "progress-event" + // RateChangeEvent is the constant for the "rate-change-event" property tag. + // The "rate-change-event" event occurs when the playback rate has changed. + RateChangedEvent = "rate-changed-event" + // SeekedEvent is the constant for the "seeked-event" property tag. + // The "seeked-event" event occurs when a seek operation completed. + SeekedEvent = "seeked-event" + // SeekingEvent is the constant for the "seeking-event" property tag. + // The "seeking-event" event occurs when a seek operation began. + SeekingEvent = "seeking-event" + // StalledEvent is the constant for the "stalled-event" property tag. + // The "stalled-event" event occurs when the user agent is trying to fetch media data, but data is unexpectedly not forthcoming. + StalledEvent = "stalled-event" + // SuspendEvent is the constant for the "suspend-event" property tag. + // The "suspend-event" event occurs when the media data loading has been suspended. + SuspendEvent = "suspend-event" + // TimeUpdateEvent is the constant for the "time-update-event" property tag. + // The "time-update-event" event occurs when the time indicated by the currentTime attribute has been updated. + TimeUpdateEvent = "time-update-event" + // VolumeChangedEvent is the constant for the "volume-change-event" property tag. + // The "volume-change-event" event occurs when the volume has changed. + VolumeChangedEvent = "volume-changed-event" + // WaitingEvent is the constant for the "waiting-event" property tag. + // The "waiting-event" event occurs when the playback has stopped because of a temporary lack of data + WaitingEvent = "waiting-event" + // PlayerErrorEvent is the constant for the "player-error-event" property tag. + // The "player-error-event" event is fired when the resource could not be loaded due to an error + // (for example, a network connectivity problem). + PlayerErrorEvent = "player-error-event" + + // PreloadNone - value of the view "preload" property: indicates that the audio/video should not be preloaded. + PreloadNone = 0 + // PreloadMetadata - value of the view "preload" property: indicates that only audio/video metadata (e.g. length) is fetched. + PreloadMetadata = 1 + // PreloadAuto - value of the view "preload" property: indicates that the whole audio file can be downloaded, + // even if the user is not expected to use it. + PreloadAuto = 2 + + // PlayerErrorUnknown - MediaPlayer error code: An unknown error. + PlayerErrorUnknown = 0 + // PlayerErrorAborted - MediaPlayer error code: The fetching of the associated resource was aborted by the user's request. + PlayerErrorAborted = 1 + // PlayerErrorNetwork - MediaPlayer error code: Some kind of network error occurred which prevented the media + // from being successfully fetched, despite having previously been available. + PlayerErrorNetwork = 2 + // PlayerErrorDecode - MediaPlayer error code: Despite having previously been determined to be usable, + // an error occurred while trying to decode the media resource, resulting in an error. + PlayerErrorDecode = 3 + // PlayerErrorSourceNotSupported - MediaPlayer error code: The associated resource or media provider object has been found to be unsuitable. + PlayerErrorSourceNotSupported = 4 +) + +type MediaPlayer interface { + View + // Play attempts to begin playback of the media. + Play() + // Pause will pause playback of the media, if the media is already in a paused state this method will have no effect. + Pause() + // SetCurrentTime sets the current playback time in seconds. + SetCurrentTime(seconds float64) + // CurrentTime returns the current playback time in seconds. + CurrentTime() float64 + // Duration returns the value indicating the total duration of the media in seconds. + // If no media data is available, the returned value is NaN. + Duration() float64 + // SetPlaybackRate sets the rate at which the media is being played back. This is used to implement user controls + // for fast forward, slow motion, and so forth. The normal playback rate is multiplied by this value to obtain + // the current rate, so a value of 1.0 indicates normal speed. + SetPlaybackRate(rate float64) + // PlaybackRate returns the rate at which the media is being played back. + PlaybackRate() float64 + // SetVolume sets the audio volume, from 0.0 (silent) to 1.0 (loudest). + SetVolume(volume float64) + // Volume returns the audio volume, from 0.0 (silent) to 1.0 (loudest). + Volume() float64 + // IsEnded function tells whether the media element is ended. + IsEnded() bool + // IsPaused function tells whether the media element is paused. + IsPaused() bool +} + +type mediaPlayerData struct { + viewData +} + +type MediaSource struct { + Url string + MimeType string +} + +func (player *mediaPlayerData) Init(session Session) { + player.viewData.Init(session) + player.tag = "MediaPlayer" +} + +func (player *mediaPlayerData) Remove(tag string) { + player.remove(strings.ToLower(tag)) +} + +func (player *mediaPlayerData) remove(tag string) { + switch tag { + + case Controls, Loop, Muted, Preload, AbortEvent, LoadStartEvent, PlayerErrorEvent, + CanPlayEvent, CanPlayThroughEvent, CompleteEvent, DurationChangedEvent, + EmptiedEvent, EndedEvent, LoadedDataEvent, LoadedMetadataEvent, PauseEvent, PlayEvent, + PlayingEvent, RateChangedEvent, SeekedEvent, SeekingEvent, StalledEvent, SuspendEvent, + ProgressEvent, TimeUpdateEvent, VolumeChangedEvent, WaitingEvent: + + player.viewData.remove(tag) + player.propertyChanged(tag) + + default: + player.viewData.remove(tag) + } +} + +func (player *mediaPlayerData) Set(tag string, value interface{}) bool { + return player.set(strings.ToLower(tag), value) +} + +func (player *mediaPlayerData) set(tag string, value interface{}) bool { + if value == nil { + player.remove(tag) + return true + } + + switch tag { + case Controls, Loop, Muted, Preload: + if player.viewData.set(tag, value) { + player.propertyChanged(tag) + return true + } + + case AbortEvent, CanPlayEvent, CanPlayThroughEvent, CompleteEvent, EmptiedEvent, LoadStartEvent, + EndedEvent, LoadedDataEvent, LoadedMetadataEvent, PauseEvent, PlayEvent, PlayingEvent, + ProgressEvent, SeekedEvent, SeekingEvent, StalledEvent, SuspendEvent, WaitingEvent: + if listeners, ok := valueToPlayerListeners(value); ok { + if listeners == nil { + delete(player.properties, tag) + } else { + player.properties[tag] = listeners + } + player.propertyChanged(tag) + return true + } + notCompatibleType(tag, value) + + case DurationChangedEvent, RateChangedEvent, TimeUpdateEvent, VolumeChangedEvent: + if listeners, ok := valueToPlayerTimeListeners(value); ok { + if listeners == nil { + delete(player.properties, tag) + } else { + player.properties[tag] = listeners + } + player.propertyChanged(tag) + return true + } + notCompatibleType(tag, value) + + case PlayerErrorEvent: + if listeners, ok := valueToPlayerErrorListeners(value); ok { + if listeners == nil { + delete(player.properties, tag) + } else { + player.properties[tag] = listeners + } + player.propertyChanged(tag) + return true + } + notCompatibleType(tag, value) + + case Source: + if player.setSource(value) { + player.propertyChanged(tag) + return true + } + + default: + if player.viewData.set(tag, value) { + return true + } + } + return false +} + +func (player *mediaPlayerData) setSource(value interface{}) bool { + switch value := value.(type) { + case string: + src := MediaSource{Url: value, MimeType: ""} + player.properties[Source] = []MediaSource{src} + + case MediaSource: + player.properties[Source] = []MediaSource{value} + + case []MediaSource: + player.properties[Source] = value + + case DataObject: + url, ok := value.PropertyValue("src") + if !ok || url == "" { + invalidPropertyValue(Source, value) + return false + } + + mimeType, _ := value.PropertyValue("mime-type") + src := MediaSource{Url: url, MimeType: mimeType} + player.properties[Source] = []MediaSource{src} + + case []DataValue: + src := []MediaSource{} + for _, val := range value { + if val.IsObject() { + obj := val.Object() + if url, ok := obj.PropertyValue("src"); ok && url != "" { + mimeType, _ := obj.PropertyValue("mime-type") + src = append(src, MediaSource{Url: url, MimeType: mimeType}) + } else { + invalidPropertyValue(Source, value) + return false + } + } else { + src = append(src, MediaSource{Url: val.Value(), MimeType: ""}) + } + } + + if len(src) == 0 { + invalidPropertyValue(Source, value) + return false + } + player.properties[Source] = src + + default: + notCompatibleType(Source, value) + return false + } + + return true +} + +func valueToPlayerListeners(value interface{}) ([]func(MediaPlayer), bool) { + if value == nil { + return nil, true + } + + switch value := value.(type) { + case func(MediaPlayer): + return []func(MediaPlayer){value}, true + + case func(): + fn := func(MediaPlayer) { + value() + } + return []func(MediaPlayer){fn}, true + + case []func(MediaPlayer): + 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(MediaPlayer), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(MediaPlayer) { + v() + } + } + return listeners, true + + case []interface{}: + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(MediaPlayer), count) + for i, v := range value { + if v == nil { + return nil, false + } + switch v := v.(type) { + case func(MediaPlayer): + listeners[i] = v + + case func(): + listeners[i] = func(MediaPlayer) { + v() + } + + default: + return nil, false + } + } + return listeners, true + } + + return nil, false +} + +func valueToPlayerTimeListeners(value interface{}) ([]func(MediaPlayer, float64), bool) { + if value == nil { + return nil, true + } + + switch value := value.(type) { + case func(MediaPlayer, float64): + return []func(MediaPlayer, float64){value}, true + + case func(float64): + fn := func(player MediaPlayer, time float64) { + value(time) + } + return []func(MediaPlayer, float64){fn}, true + + case func(MediaPlayer): + fn := func(player MediaPlayer, time float64) { + value(player) + } + return []func(MediaPlayer, float64){fn}, true + + case func(): + fn := func(player MediaPlayer, time float64) { + value() + } + return []func(MediaPlayer, float64){fn}, true + + case []func(MediaPlayer, float64): + if len(value) == 0 { + return nil, true + } + for _, fn := range value { + if fn == nil { + return nil, false + } + } + return value, true + + case []func(float64): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(MediaPlayer, float64), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(player MediaPlayer, time float64) { + v(time) + } + } + return listeners, true + + case []func(MediaPlayer): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(MediaPlayer, float64), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(player MediaPlayer, time float64) { + v(player) + } + } + return listeners, true + + case []func(): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(MediaPlayer, float64), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(player MediaPlayer, time float64) { + v() + } + } + return listeners, true + + case []interface{}: + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(MediaPlayer, float64), count) + for i, v := range value { + if v == nil { + return nil, false + } + switch v := v.(type) { + case func(MediaPlayer, float64): + listeners[i] = v + + case func(float64): + listeners[i] = func(player MediaPlayer, time float64) { + v(time) + } + + case func(MediaPlayer): + listeners[i] = func(player MediaPlayer, time float64) { + v(player) + } + + case func(): + listeners[i] = func(player MediaPlayer, time float64) { + v() + } + + default: + return nil, false + } + } + return listeners, true + } + + return nil, false +} + +func valueToPlayerErrorListeners(value interface{}) ([]func(MediaPlayer, int, string), bool) { + if value == nil { + return nil, true + } + + switch value := value.(type) { + case func(MediaPlayer, int, string): + return []func(MediaPlayer, int, string){value}, true + + case func(int, string): + fn := func(player MediaPlayer, code int, message string) { + value(code, message) + } + return []func(MediaPlayer, int, string){fn}, true + + case func(MediaPlayer): + fn := func(player MediaPlayer, code int, message string) { + value(player) + } + return []func(MediaPlayer, int, string){fn}, true + + case func(): + fn := func(player MediaPlayer, code int, message string) { + value() + } + return []func(MediaPlayer, int, string){fn}, true + + case []func(MediaPlayer, int, string): + if len(value) == 0 { + return nil, true + } + for _, fn := range value { + if fn == nil { + return nil, false + } + } + return value, true + + case []func(int, string): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(MediaPlayer, int, string), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(player MediaPlayer, code int, message string) { + v(code, message) + } + } + return listeners, true + + case []func(MediaPlayer): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(MediaPlayer, int, string), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(player MediaPlayer, code int, message string) { + v(player) + } + } + return listeners, true + + case []func(): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(MediaPlayer, int, string), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(player MediaPlayer, code int, message string) { + v() + } + } + return listeners, true + + case []interface{}: + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(MediaPlayer, int, string), count) + for i, v := range value { + if v == nil { + return nil, false + } + switch v := v.(type) { + case func(MediaPlayer, int, string): + listeners[i] = v + + case func(int, string): + listeners[i] = func(player MediaPlayer, code int, message string) { + v(code, message) + } + + case func(MediaPlayer): + listeners[i] = func(player MediaPlayer, code int, message string) { + v(player) + } + + case func(): + listeners[i] = func(player MediaPlayer, code int, message string) { + v() + } + + default: + return nil, false + } + } + return listeners, true + } + + return nil, false +} + +func playerEvents() []struct{ tag, cssTag string } { + return []struct{ tag, cssTag string }{ + {AbortEvent, "onabort"}, + {CanPlayEvent, "oncanplay"}, + {CanPlayThroughEvent, "oncanplaythrough"}, + {CompleteEvent, "oncomplete"}, + {EmptiedEvent, "onemptied"}, + {EndedEvent, "ended"}, + {LoadedDataEvent, "onloadeddata"}, + {LoadedMetadataEvent, "onloadedmetadata"}, + {LoadStartEvent, "onloadstart"}, + {PauseEvent, "onpause"}, + {PlayEvent, "onplay"}, + {PlayingEvent, "onplaying"}, + {ProgressEvent, "onprogress"}, + {SeekedEvent, "onseeked"}, + {SeekingEvent, "onseeking"}, + {StalledEvent, "onstalled"}, + {SuspendEvent, "onsuspend"}, + {WaitingEvent, "onwaiting"}, + } +} + +func (player *mediaPlayerData) propertyChanged(tag string) { + switch tag { + case Controls, Loop: + value, _ := boolProperty(player, tag, player.Session()) + if value { + updateBoolProperty(player.htmlID(), tag, value, player.Session()) + } else { + removeProperty(player.htmlID(), tag, player.Session()) + } + + case Muted: + value, _ := boolProperty(player, tag, player.Session()) + if value { + player.Session().runScript("setMediaMuted('" + player.htmlID() + "', true)") + } else { + player.Session().runScript("setMediaMuted('" + player.htmlID() + "', false)") + } + + case Preload: + value, _ := enumProperty(player, tag, player.Session(), 0) + values := enumProperties[Preload].values + updateProperty(player.htmlID(), tag, values[value], player.Session()) + + case AbortEvent, CanPlayEvent, CanPlayThroughEvent, CompleteEvent, EmptiedEvent, + EndedEvent, LoadedDataEvent, LoadedMetadataEvent, PauseEvent, PlayEvent, PlayingEvent, ProgressEvent, + LoadStartEvent, SeekedEvent, SeekingEvent, StalledEvent, SuspendEvent, WaitingEvent: + + for _, event := range playerEvents() { + if event.tag == tag { + if value := player.getRaw(event.tag); value != nil { + switch value := value.(type) { + case []func(MediaPlayer): + if len(value) > 0 { + fn := fmt.Sprintf(`playerEvent(this, "%s")`, event.tag) + updateProperty(player.htmlID(), event.cssTag, fn, player.Session()) + return + } + } + } + updateProperty(player.htmlID(), tag, "", player.Session()) + break + } + + } + case TimeUpdateEvent: + if value := player.getRaw(tag); value != nil { + updateProperty(player.htmlID(), "ontimeupdate", "playerTimeUpdatedEvent(this)", player.Session()) + } else { + updateProperty(player.htmlID(), "ontimeupdate", "", player.Session()) + } + + case VolumeChangedEvent: + if value := player.getRaw(tag); value != nil { + updateProperty(player.htmlID(), "onvolumechange", "playerVolumeChangedEvent(this)", player.Session()) + } else { + updateProperty(player.htmlID(), "onvolumechange", "", player.Session()) + } + + case DurationChangedEvent: + if value := player.getRaw(tag); value != nil { + updateProperty(player.htmlID(), "ondurationchange", "playerDurationChangedEvent(this)", player.Session()) + } else { + updateProperty(player.htmlID(), "ondurationchange", "", player.Session()) + } + + case RateChangedEvent: + if value := player.getRaw(tag); value != nil { + updateProperty(player.htmlID(), "onratechange", "playerRateChangedEvent(this)", player.Session()) + } else { + updateProperty(player.htmlID(), "onratechange", "", player.Session()) + } + + case PlayerErrorEvent: + if value := player.getRaw(tag); value != nil { + updateProperty(player.htmlID(), "onerror", "playerErrorEvent(this)", player.Session()) + } else { + updateProperty(player.htmlID(), "onerror", "", player.Session()) + } + + case Source: + updateInnerHTML(player.htmlID(), player.Session()) + + default: + player.viewData.propertyChanged(tag) + } +} + +func (player *mediaPlayerData) htmlSubviews(self View, buffer *strings.Builder) { + if value := player.getRaw(Source); value != nil { + if sources, ok := value.([]MediaSource); ok && len(sources) > 0 { + session := player.Session() + for _, src := range sources { + if url, ok := session.resolveConstants(src.Url); ok && url != "" { + buffer.WriteString(`') + } + } + } + } +} + +func (player *mediaPlayerData) htmlProperties(self View, buffer *strings.Builder) { + player.viewData.htmlProperties(self, buffer) + for _, tag := range []string{Controls, Loop, Muted, Preload} { + if value, _ := boolProperty(player, tag, player.Session()); value { + buffer.WriteRune(' ') + buffer.WriteString(tag) + } + } + + if value, ok := enumProperty(player, Preload, player.Session(), 0); ok { + values := enumProperties[Preload].values + buffer.WriteString(` preload="`) + buffer.WriteString(values[value]) + buffer.WriteRune('"') + } + + for _, event := range playerEvents() { + if value := player.getRaw(event.tag); value != nil { + switch value := value.(type) { + case []func(MediaPlayer): + if len(value) > 0 { + buffer.WriteString(` `) + buffer.WriteString(event.cssTag) + buffer.WriteString(`="playerEvent(this, \'`) + buffer.WriteString(event.tag) + buffer.WriteString(`\')"`) + } + } + } + } + + if value := player.getRaw(TimeUpdateEvent); value != nil { + buffer.WriteString(` ontimeupdate="playerTimeUpdatedEvent(this)"`) + } + + if value := player.getRaw(VolumeChangedEvent); value != nil { + buffer.WriteString(` onvolumechange="playerVolumeChangedEvent(this)"`) + } + + if value := player.getRaw(DurationChangedEvent); value != nil { + buffer.WriteString(` ondurationchange="playerDurationChangedEvent(this)"`) + } + + if value := player.getRaw(RateChangedEvent); value != nil { + buffer.WriteString(` onratechange="playerRateChangedEvent(this)"`) + } + + if value := player.getRaw(PlayerErrorEvent); value != nil { + buffer.WriteString(` onerror="playerErrorEvent(this)"`) + } +} + +func (player *mediaPlayerData) handleCommand(self View, command string, data DataObject) bool { + switch command { + case AbortEvent, CanPlayEvent, CanPlayThroughEvent, CompleteEvent, LoadStartEvent, + EmptiedEvent, EndedEvent, LoadedDataEvent, LoadedMetadataEvent, PauseEvent, PlayEvent, + PlayingEvent, ProgressEvent, SeekedEvent, SeekingEvent, StalledEvent, SuspendEvent, + WaitingEvent: + + if value := player.getRaw(command); value != nil { + if listeners, ok := value.([]func(MediaPlayer)); ok { + for _, listener := range listeners { + listener(player) + } + } + } + + case TimeUpdateEvent, DurationChangedEvent, RateChangedEvent, VolumeChangedEvent: + if value := player.getRaw(command); value != nil { + if listeners, ok := value.([]func(MediaPlayer, float64)); ok { + time := dataFloatProperty(data, "value") + for _, listener := range listeners { + listener(player, time) + } + } + } + + case PlayerErrorEvent: + if value := player.getRaw(command); value != nil { + if listeners, ok := value.([]func(MediaPlayer, int, string)); ok { + code := dataIntProperty(data, "code") + message, _ := data.PropertyValue("message") + for _, listener := range listeners { + listener(player, code, message) + } + } + } + } + + return player.viewData.handleCommand(self, command, data) +} + +func (player *mediaPlayerData) Play() { + player.session.runScript(fmt.Sprintf(`mediaPlay('%v');`, player.htmlID())) +} + +func (player *mediaPlayerData) Pause() { + player.session.runScript(fmt.Sprintf(`mediaPause('%v');`, player.htmlID())) +} + +func (player *mediaPlayerData) SetCurrentTime(seconds float64) { + player.session.runScript(fmt.Sprintf(`mediaSetSetCurrentTime('%v', %v);`, player.htmlID(), seconds)) +} + +func (player *mediaPlayerData) getFloatPlayerProperty(tag string) (float64, bool) { + + script := allocStringBuilder() + defer freeStringBuilder(script) + + script.WriteString(`const element = document.getElementById('`) + script.WriteString(player.htmlID()) + script.WriteString(`'); +if (element && element.`) + script.WriteString(tag) + script.WriteString(`) { + sendMessage('answer{answerID=' + answerID + ',`) + script.WriteString(tag) + script.WriteString(`=' + element.`) + script.WriteString(tag) + script.WriteString(` + '}'); +} else { + sendMessage('answer{answerID=' + answerID + ',`) + script.WriteString(tag) + script.WriteString(`=0}'); +}`) + + result := player.Session().runGetterScript(script.String()) + switch result.Tag() { + case "answer": + if value, ok := result.PropertyValue(tag); ok { + w, err := strconv.ParseFloat(value, 32) + if err == nil { + return w, true + } + ErrorLog(err.Error()) + } + + case "error": + if text, ok := result.PropertyValue("errorText"); ok { + ErrorLog(text) + } else { + ErrorLog("error") + } + + default: + ErrorLog("Unknown answer: " + result.Tag()) + } + return 0, false +} + +func (player *mediaPlayerData) CurrentTime() float64 { + if result, ok := player.getFloatPlayerProperty("currentTime"); ok { + return result + } + return 0 +} + +func (player *mediaPlayerData) Duration() float64 { + if result, ok := player.getFloatPlayerProperty("duration"); ok { + return result + } + return 0 +} + +func (player *mediaPlayerData) SetPlaybackRate(rate float64) { + player.session.runScript(fmt.Sprintf(`mediaSetPlaybackRate('%v', %v);`, player.htmlID(), rate)) +} + +func (player *mediaPlayerData) PlaybackRate() float64 { + if result, ok := player.getFloatPlayerProperty("playbackRate"); ok { + return result + } + return 1 +} + +func (player *mediaPlayerData) SetVolume(volume float64) { + if volume >= 0 && volume <= 1 { + player.session.runScript(fmt.Sprintf(`mediaSetVolume('%v', %v);`, player.htmlID(), volume)) + } +} + +func (player *mediaPlayerData) Volume() float64 { + if result, ok := player.getFloatPlayerProperty("volume"); ok { + return result + } + return 1 +} + +func (player *mediaPlayerData) getBoolPlayerProperty(tag string) (bool, bool) { + + script := allocStringBuilder() + defer freeStringBuilder(script) + + script.WriteString(`const element = document.getElementById('`) + script.WriteString(player.htmlID()) + script.WriteString(`'); +if (element && element.`) + script.WriteString(tag) + script.WriteString(`) { + sendMessage('answer{answerID=' + answerID + ',`) + script.WriteString(tag) + script.WriteString(`=1}') +} else { + sendMessage('answer{answerID=' + answerID + ',`) + script.WriteString(tag) + script.WriteString(`=0}') +}`) + + result := player.Session().runGetterScript(script.String()) + switch result.Tag() { + case "answer": + if value, ok := result.PropertyValue(tag); ok { + if value == "1" { + return true, true + } + return false, true + } + + case "error": + if text, ok := result.PropertyValue("errorText"); ok { + ErrorLog(text) + } else { + ErrorLog("error") + } + + default: + ErrorLog("Unknown answer: " + result.Tag()) + } + return false, false +} + +func (player *mediaPlayerData) IsEnded() bool { + if result, ok := player.getBoolPlayerProperty("ended"); ok { + return result + } + return false +} + +func (player *mediaPlayerData) IsPaused() bool { + if result, ok := player.getBoolPlayerProperty("paused"); ok { + return result + } + return false +} + +// MediaPlayerPlay attempts to begin playback of the media. +func MediaPlayerPlay(view View, playerID string) { + if playerID != "" { + view = ViewByID(view, playerID) + } + + if player, ok := view.(MediaPlayer); ok { + player.Play() + } else { + ErrorLog(`The found View is not MediaPlayer`) + } +} + +// MediaPlayerPause will pause playback of the media, if the media is already in a paused state this method will have no effect. +func MediaPlayerPause(view View, playerID string) { + if playerID != "" { + view = ViewByID(view, playerID) + } + + if player, ok := view.(MediaPlayer); ok { + player.Pause() + } else { + ErrorLog(`The found View is not MediaPlayer`) + } +} + +// SetMediaPlayerCurrentTime sets the current playback time in seconds. +func SetMediaPlayerCurrentTime(view View, playerID string, seconds float64) { + if playerID != "" { + view = ViewByID(view, playerID) + } + + if player, ok := view.(MediaPlayer); ok { + player.SetCurrentTime(seconds) + } else { + ErrorLog(`The found View is not MediaPlayer`) + } +} + +// MediaPlayerCurrentTime returns the current playback time in seconds. +func MediaPlayerCurrentTime(view View, playerID string) float64 { + if playerID != "" { + view = ViewByID(view, playerID) + } + + if player, ok := view.(MediaPlayer); ok { + return player.CurrentTime() + } + + ErrorLog(`The found View is not MediaPlayer`) + return 0 +} + +// MediaPlayerDuration returns the value indicating the total duration of the media in seconds. +// If no media data is available, the returned value is NaN. +func MediaPlayerDuration(view View, playerID string) float64 { + if playerID != "" { + view = ViewByID(view, playerID) + } + + if player, ok := view.(MediaPlayer); ok { + return player.Duration() + } + + ErrorLog(`The found View is not MediaPlayer`) + return math.NaN() +} + +// SetVolume sets the audio volume, from 0.0 (silent) to 1.0 (loudest). +func SetMediaPlayerVolume(view View, playerID string, volume float64) { + if playerID != "" { + view = ViewByID(view, playerID) + } + + if player, ok := view.(MediaPlayer); ok { + player.SetVolume(volume) + } else { + ErrorLog(`The found View is not MediaPlayer`) + } +} + +// Volume returns the audio volume, from 0.0 (silent) to 1.0 (loudest). +func MediaPlayerVolume(view View, playerID string) float64 { + if playerID != "" { + view = ViewByID(view, playerID) + } + + if player, ok := view.(MediaPlayer); ok { + return player.Volume() + } + + ErrorLog(`The found View is not MediaPlayer`) + return 1 +} + +// SetMediaPlayerPlaybackRate sets the rate at which the media is being played back. This is used to implement user controls +// for fast forward, slow motion, and so forth. The normal playback rate is multiplied by this value to obtain +// the current rate, so a value of 1.0 indicates normal speed. +func SetMediaPlayerPlaybackRate(view View, playerID string, rate float64) { + if playerID != "" { + view = ViewByID(view, playerID) + } + + if player, ok := view.(MediaPlayer); ok { + player.SetPlaybackRate(rate) + } else { + ErrorLog(`The found View is not MediaPlayer`) + } +} + +// MediaPlayerPlaybackRate returns the rate at which the media is being played back. +func MediaPlayerPlaybackRate(view View, playerID string) float64 { + if playerID != "" { + view = ViewByID(view, playerID) + } + + if player, ok := view.(MediaPlayer); ok { + return player.PlaybackRate() + } + + ErrorLog(`The found View is not MediaPlayer`) + return 1 +} + +// IsMediaPlayerEnded function tells whether the media element is ended. +func IsMediaPlayerEnded(view View, playerID string) bool { + if playerID != "" { + view = ViewByID(view, playerID) + } + + if player, ok := view.(MediaPlayer); ok { + return player.IsEnded() + } + + ErrorLog(`The found View is not MediaPlayer`) + return false +} + +// IsMediaPlayerPaused function tells whether the media element is paused. +func IsMediaPlayerPaused(view View, playerID string) bool { + if playerID != "" { + view = ViewByID(view, playerID) + } + + if player, ok := view.(MediaPlayer); ok { + return player.IsPaused() + } + + ErrorLog(`The found View is not MediaPlayer`) + return false +} diff --git a/mouseEvents.go b/mouseEvents.go new file mode 100644 index 0000000..acc4092 --- /dev/null +++ b/mouseEvents.go @@ -0,0 +1,406 @@ +package rui + +import ( + "strconv" + "strings" +) + +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" + + // 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" + + // 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" + + // 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" + + // 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" + + // 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(). + MouseOut = "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). + MouseOver = "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). + ContextMenuEvent = "context-menu-event" + + // PrimaryMouseButton is a number of the main pressed button, usually the left button or the un-initialized state + PrimaryMouseButton = 0 + // AuxiliaryMouseButton is a number of the auxiliary pressed button, usually the wheel button + // or the middle button (if present) + AuxiliaryMouseButton = 1 + // SecondaryMouseButton is a number of the secondary pressed button, usually the right button + SecondaryMouseButton = 2 + // MouseButton4 is a number of the fourth button, typically the Browser Back button + MouseButton4 = 3 + // MouseButton5 is a number of the fifth button, typically the Browser Forward button + MouseButton5 = 4 + + // PrimaryMouseMask is the mask of the primary button (usually the left button) + PrimaryMouseMask = 1 + // SecondaryMouseMask is the mask of the secondary button (usually the right button) + SecondaryMouseMask = 2 + // AuxiliaryMouseMask is the mask of the auxiliary button (usually the mouse wheel button or middle button) + AuxiliaryMouseMask = 4 + // MouseMask4 is the mask of the 4th button (typically the "Browser Back" button) + MouseMask4 = 8 + //MouseMask5 is the mask of the 5th button (typically the "Browser Forward" button) + MouseMask5 = 16 +) + +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. + TimeStamp uint64 + + // Button indicates which button was pressed on the mouse to trigger the event: + // PrimaryMouseButton (0), AuxiliaryMouseButton (1), SecondaryMouseButton (2), + // MouseButton4 (3), and MouseButton5 (4) + Button int + + // Buttons indicates which buttons are pressed on the mouse (or other input device) + // when a mouse event is triggered. Each button that can be pressed is represented by a given mask: + // PrimaryMouseMask (1), SecondaryMouseMask (2), AuxiliaryMouseMask (4), MouseMask4 (8), and MouseMask5 (16) + Buttons int + + // 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 mouse pointer in global (screen) coordinates. + ScreenX float64 + // ScreenY provides the vertical coordinate (offset) of the mouse pointer in global (screen) coordinates. + ScreenY float64 + + // CtrlKey == true if the control key was down when the event was fired. false otherwise. + CtrlKey bool + // ShiftKey == true if the shift key was down when the event was fired. false otherwise. + ShiftKey bool + // AltKey == true if the alt key was down when the event was fired. false otherwise. + AltKey bool + // MetaKey == true if the meta key was down when the event was fired. false otherwise. + MetaKey bool +} + +func valueToMouseListeners(value interface{}) ([]func(View, MouseEvent), bool) { + if value == nil { + return nil, true + } + + switch value := value.(type) { + case func(View, MouseEvent): + return []func(View, MouseEvent){value}, true + + case func(MouseEvent): + fn := func(view View, event MouseEvent) { + value(event) + } + return []func(View, MouseEvent){fn}, true + + case func(View): + fn := func(view View, event MouseEvent) { + value(view) + } + return []func(View, MouseEvent){fn}, true + + case func(): + fn := func(view View, event MouseEvent) { + value() + } + return []func(View, MouseEvent){fn}, true + + case []func(View, MouseEvent): + if len(value) == 0 { + return nil, true + } + for _, fn := range value { + if fn == nil { + return nil, false + } + } + return value, true + + case []func(MouseEvent): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View, MouseEvent), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(view View, event MouseEvent) { + v(event) + } + } + return listeners, true + + case []func(View): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View, MouseEvent), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(view View, event MouseEvent) { + v(view) + } + } + return listeners, true + + case []func(): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View, MouseEvent), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(view View, event MouseEvent) { + v() + } + } + return listeners, true + + case []interface{}: + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View, MouseEvent), count) + for i, v := range value { + if v == nil { + return nil, false + } + switch v := v.(type) { + case func(View, MouseEvent): + listeners[i] = v + + case func(MouseEvent): + listeners[i] = func(view View, event MouseEvent) { + v(event) + } + + case func(View): + listeners[i] = func(view View, event MouseEvent) { + v(view) + } + + case func(): + listeners[i] = func(view View, event MouseEvent) { + v() + } + + default: + return nil, false + } + } + return listeners, true + } + + return nil, false +} + +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 interface{}) bool { + listeners, ok := valueToMouseListeners(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 { + updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session()) + } + } else { + return false + } + return true +} + +func (view *viewData) removeMouseListener(tag string) { + delete(view.properties, tag) + if view.created { + if js, ok := mouseEvents[tag]; ok { + updateProperty(view.htmlID(), js.jsEvent, "", view.Session()) + } + } +} + +func getMouseListeners(view View, subviewID string, tag string) []func(View, MouseEvent) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.Get(tag); value != nil { + if result, ok := value.([]func(View, MouseEvent)); ok { + return result + } + } + } + return []func(View, MouseEvent){} +} + +func mouseEventsHtml(view View, buffer *strings.Builder) { + 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 + `="` + js.jsFunc + `(this, event)" `) + } + } + } +} + +func getTimeStamp(data DataObject) uint64 { + if value, ok := data.PropertyValue("timeStamp"); ok { + if index := strings.Index(value, "."); index > 0 { + value = value[:index] + } + if n, err := strconv.ParseUint(value, 10, 64); err == nil { + return n + } + } + return 0 +} + +func (event *MouseEvent) init(data DataObject) { + + event.TimeStamp = getTimeStamp(data) + event.Button = dataIntProperty(data, "button") + event.Buttons = dataIntProperty(data, "buttons") + event.X = dataFloatProperty(data, "x") + event.Y = dataFloatProperty(data, "y") + event.ClientX = dataFloatProperty(data, "clientX") + event.ClientY = dataFloatProperty(data, "clientY") + event.ScreenX = dataFloatProperty(data, "screenX") + event.ScreenY = dataFloatProperty(data, "screenY") + event.CtrlKey = dataBoolProperty(data, "ctrlKey") + event.ShiftKey = dataBoolProperty(data, "shiftKey") + event.AltKey = dataBoolProperty(data, "altKey") + event.MetaKey = dataBoolProperty(data, "metaKey") +} + +func handleMouseEvents(view View, tag string, data DataObject) { + listeners := getMouseListeners(view, "", tag) + if len(listeners) == 0 { + return + } + + var event MouseEvent + event.init(data) + + for _, listener := range listeners { + listener(view, event) + } +} + +// GetClickListeners returns the "click-event" listener list. If there are no listeners then the empty list is returned. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetClickListeners(view View, subviewID string) []func(View, MouseEvent) { + return getMouseListeners(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 "" then a value from the first argument (view) is returned. +func GetDoubleClickListeners(view View, subviewID string) []func(View, MouseEvent) { + return getMouseListeners(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 "" then a value from the first argument (view) is returned. +func GetContextMenuListeners(view View, subviewID string) []func(View, MouseEvent) { + return getMouseListeners(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 "" then a value from the first argument (view) is returned. +func GetMouseDownListeners(view View, subviewID string) []func(View, MouseEvent) { + return getMouseListeners(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 "" then a value from the first argument (view) is returned. +func GetMouseUpListeners(view View, subviewID string) []func(View, MouseEvent) { + return getMouseListeners(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 "" then a value from the first argument (view) is returned. +func GetMouseMoveListeners(view View, subviewID string) []func(View, MouseEvent) { + return getMouseListeners(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 "" then a value from the first argument (view) is returned. +func GetMouseOverListeners(view View, subviewID string) []func(View, MouseEvent) { + return getMouseListeners(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 "" then a value from the first argument (view) is returned. +func GetMouseOutListeners(view View, subviewID string) []func(View, MouseEvent) { + return getMouseListeners(view, subviewID, MouseOut) +} diff --git a/numberPicker.go b/numberPicker.go new file mode 100644 index 0000000..08c9b57 --- /dev/null +++ b/numberPicker.go @@ -0,0 +1,371 @@ +package rui + +import ( + "fmt" + "strconv" + "strings" +) + +const ( + NumberChangedEvent = "number-changed" + NumberPickerType = "number-picker-type" + NumberPickerMin = "number-picker-min" + NumberPickerMax = "number-picker-max" + NumberPickerStep = "number-picker-step" + NumberPickerValue = "number-picker-value" +) + +const ( + // NumberEditor - type of NumberPicker. NumberPicker is presented by editor + NumberEditor = 0 + // NumberSlider - type of NumberPicker. NumberPicker is presented by slider + NumberSlider = 1 +) + +// NumberPicker - NumberPicker view +type NumberPicker interface { + View +} + +type numberPickerData struct { + viewData + numberChangedListeners []func(NumberPicker, float64) +} + +// NewNumberPicker create new NumberPicker object and return it +func NewNumberPicker(session Session, params Params) NumberPicker { + view := new(numberPickerData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newNumberPicker(session Session) View { + return NewNumberPicker(session, nil) +} + +func (picker *numberPickerData) Init(session Session) { + picker.viewData.Init(session) + picker.tag = "NumberPicker" + picker.numberChangedListeners = []func(NumberPicker, float64){} +} + +func (picker *numberPickerData) normalizeTag(tag string) string { + tag = strings.ToLower(tag) + switch tag { + case Type, Min, Max, Step, Value: + return "number-picker-" + tag + } + + return tag +} + +func (picker *numberPickerData) Remove(tag string) { + picker.remove(picker.normalizeTag(tag)) +} + +func (picker *numberPickerData) remove(tag string) { + switch tag { + case NumberChangedEvent: + if len(picker.numberChangedListeners) > 0 { + picker.numberChangedListeners = []func(NumberPicker, float64){} + } + + default: + picker.viewData.remove(tag) + picker.propertyChanged(tag) + } +} + +func (picker *numberPickerData) Set(tag string, value interface{}) bool { + return picker.set(picker.normalizeTag(tag), value) +} + +func (picker *numberPickerData) set(tag string, value interface{}) bool { + if value == nil { + picker.remove(tag) + return true + } + + switch tag { + case NumberChangedEvent: + switch value := value.(type) { + case func(NumberPicker, float64): + picker.numberChangedListeners = []func(NumberPicker, float64){value} + + case func(float64): + fn := func(view NumberPicker, newValue float64) { + value(newValue) + } + picker.numberChangedListeners = []func(NumberPicker, float64){fn} + + case []func(NumberPicker, float64): + picker.numberChangedListeners = value + + case []func(float64): + listeners := make([]func(NumberPicker, float64), len(value)) + for i, val := range value { + if val == nil { + notCompatibleType(tag, val) + return false + } + + listeners[i] = func(view NumberPicker, newValue float64) { + val(newValue) + } + } + picker.numberChangedListeners = listeners + + case []interface{}: + listeners := make([]func(NumberPicker, float64), len(value)) + for i, val := range value { + if val == nil { + notCompatibleType(tag, val) + return false + } + + switch val := val.(type) { + case func(NumberPicker, float64): + listeners[i] = val + + default: + notCompatibleType(tag, val) + return false + } + } + picker.numberChangedListeners = listeners + } + return true + + case NumberPickerValue: + oldValue := GetNumberPickerValue(picker, "") + min, max := GetNumberPickerMinMax(picker, "") + if picker.setFloatProperty(NumberPickerValue, value, min, max) { + newValue := GetNumberPickerValue(picker, "") + if oldValue != newValue { + picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%f')`, picker.htmlID(), newValue)) + for _, listener := range picker.numberChangedListeners { + listener(picker, newValue) + } + } + return true + } + + default: + if picker.viewData.set(tag, value) { + picker.propertyChanged(tag) + return true + } + } + return false +} + +func (picker *numberPickerData) propertyChanged(tag string) { + switch tag { + case NumberPickerType: + if GetNumberPickerType(picker, "") == NumberSlider { + updateProperty(picker.htmlID(), "type", "range", picker.session) + } else { + updateProperty(picker.htmlID(), "type", "number", picker.session) + } + + case NumberPickerMin: + min, _ := GetNumberPickerMinMax(picker, "") + updateProperty(picker.htmlID(), Min, strconv.FormatFloat(min, 'f', -1, 32), picker.session) + + case NumberPickerMax: + _, max := GetNumberPickerMinMax(picker, "") + updateProperty(picker.htmlID(), Max, strconv.FormatFloat(max, 'f', -1, 32), picker.session) + + case NumberPickerStep: + if step := GetNumberPickerStep(picker, ""); step > 0 { + updateProperty(picker.htmlID(), Step, strconv.FormatFloat(step, 'f', -1, 32), picker.session) + } else { + updateProperty(picker.htmlID(), Step, "any", picker.session) + } + + case NumberPickerValue: + value := GetNumberPickerValue(picker, "") + picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%f')`, picker.htmlID(), value)) + for _, listener := range picker.numberChangedListeners { + listener(picker, value) + } + } +} + +func (picker *numberPickerData) Get(tag string) interface{} { + return picker.get(picker.normalizeTag(tag)) +} + +func (picker *numberPickerData) get(tag string) interface{} { + switch tag { + case NumberChangedEvent: + return picker.numberChangedListeners + + default: + return picker.viewData.get(tag) + } +} + +func (picker *numberPickerData) htmlTag() string { + return "input" +} + +func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builder) { + picker.viewData.htmlProperties(self, buffer) + + if GetNumberPickerType(picker, "") == NumberSlider { + buffer.WriteString(` type="range"`) + } else { + buffer.WriteString(` type="number"`) + } + + min, max := GetNumberPickerMinMax(picker, "") + buffer.WriteString(` min="`) + buffer.WriteString(strconv.FormatFloat(min, 'f', -1, 64)) + buffer.WriteByte('"') + + buffer.WriteString(` max="`) + buffer.WriteString(strconv.FormatFloat(max, 'f', -1, 64)) + buffer.WriteByte('"') + + step := GetNumberPickerStep(picker, "") + if step != 0 { + buffer.WriteString(` step="`) + buffer.WriteString(strconv.FormatFloat(step, 'f', -1, 64)) + buffer.WriteByte('"') + } else { + buffer.WriteString(` step="any"`) + } + + buffer.WriteString(` value="`) + buffer.WriteString(strconv.FormatFloat(GetNumberPickerValue(picker, ""), 'f', -1, 64)) + buffer.WriteByte('"') + + buffer.WriteString(` oninput="editViewInputEvent(this)"`) +} + +func (picker *numberPickerData) htmlDisabledProperties(self View, buffer *strings.Builder) { + if IsDisabled(self) { + buffer.WriteString(` disabled`) + } + picker.viewData.htmlDisabledProperties(self, buffer) +} + +func (picker *numberPickerData) handleCommand(self View, command string, data DataObject) bool { + switch command { + case "textChanged": + if text, ok := data.PropertyValue("text"); ok { + if value, err := strconv.ParseFloat(text, 32); err == nil { + oldValue := GetNumberPickerValue(picker, "") + picker.properties[NumberPickerValue] = value + if value != oldValue { + for _, listener := range picker.numberChangedListeners { + listener(picker, value) + } + } + } + } + return true + } + + return picker.viewData.handleCommand(self, command, data) +} + +// GetNumberPickerType returns the type of NumberPicker subview. Valid values: +// NumberEditor (0) - NumberPicker is presented by editor (default type) +// NumberSlider (1) - NumberPicker is presented by slider +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetNumberPickerType(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return 0 + } + + t, _ := enumStyledProperty(view, NumberPickerType, NumberEditor) + return t +} + +// GetNumberPickerMinMax returns the min and max value of NumberPicker subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetNumberPickerMinMax(view View, subviewID string) (float64, float64) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + min, ok := floatStyledProperty(view, NumberPickerMin, 0) + if !ok { + min, _ = floatStyledProperty(view, Min, 0) + } + + max, ok := floatStyledProperty(view, NumberPickerMax, 1) + if !ok { + min, _ = floatStyledProperty(view, Max, 1) + } + + if min > max { + return max, min + } + return min, max + } + return 0, 1 +} + +// GetNumberPickerStep returns the value changing step of NumberPicker subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetNumberPickerStep(view View, subviewID string) float64 { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return 0 + } + + result, ok := floatStyledProperty(view, NumberPickerStep, 0) + if !ok { + result, _ = floatStyledProperty(view, Step, 0) + } + + _, max := GetNumberPickerMinMax(view, "") + if result > max { + return max + } + return result +} + +// GetNumberPickerValue returns the value of NumberPicker subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetNumberPickerValue(view View, subviewID string) float64 { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return 0 + } + + min, _ := GetNumberPickerMinMax(view, "") + result, ok := floatStyledProperty(view, NumberPickerValue, min) + if !ok { + result, _ = floatStyledProperty(view, Value, min) + } + return result +} + +// 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 "" then a value from the first argument (view) is returned. +func GetNumberChangedListeners(view View, subviewID string) []func(NumberPicker, float64) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.Get(NumberChangedEvent); value != nil { + if listeners, ok := value.([]func(NumberPicker, float64)); ok { + return listeners + } + } + } + return []func(NumberPicker, float64){} +} diff --git a/outline.go b/outline.go new file mode 100644 index 0000000..62d81c7 --- /dev/null +++ b/outline.go @@ -0,0 +1,153 @@ +package rui + +import ( + "fmt" + "strings" +) + +type OutlineProperty interface { + Properties + ruiStringer + fmt.Stringer + ViewOutline(session Session) ViewOutline +} + +type outlinePropertyData struct { + propertyList +} + +func NewOutlineProperty(params Params) OutlineProperty { + outline := new(outlinePropertyData) + outline.properties = map[string]interface{}{} + for tag, value := range params { + outline.Set(tag, value) + } + return outline +} + +func (outline *outlinePropertyData) ruiString(writer ruiWriter) { + writer.startObject("_") + + for _, tag := range []string{Style, Width, ColorProperty} { + if value, ok := outline.properties[tag]; ok { + writer.writeProperty(Style, value) + } + } + + writer.endObject() +} + +func (outline *outlinePropertyData) String() string { + writer := newRUIWriter() + outline.ruiString(writer) + return writer.finish() +} + +func (outline *outlinePropertyData) normalizeTag(tag string) string { + return strings.TrimPrefix(strings.ToLower(tag), "outline-") +} + +func (outline *outlinePropertyData) Remove(tag string) { + delete(outline.properties, outline.normalizeTag(tag)) +} + +func (outline *outlinePropertyData) Set(tag string, value interface{}) bool { + if value == nil { + outline.Remove(tag) + return true + } + + tag = outline.normalizeTag(tag) + switch tag { + case Style: + return outline.setEnumProperty(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 outline.setSizeProperty(Width, value) + + case ColorProperty: + return outline.setColorProperty(ColorProperty, value) + + default: + ErrorLogF(`"%s" property is not compatible with the OutlineProperty`, tag) + } + return false +} + +func (outline *outlinePropertyData) Get(tag string) interface{} { + return outline.propertyList.Get(outline.normalizeTag(tag)) +} + +func (outline *outlinePropertyData) ViewOutline(session Session) ViewOutline { + style, _ := valueToEnum(outline.getRaw(Style), BorderStyle, session, NoneLine) + width, _ := sizeProperty(outline, Width, session) + color, _ := colorProperty(outline, ColorProperty, session) + return ViewOutline{Style: style, Width: width, Color: color} +} + +// ViewOutline describes parameters of a view border +type ViewOutline struct { + Style int + Color Color + Width SizeUnit +} + +func (outline ViewOutline) cssValue(builder cssBuilder) { + values := enumProperties[BorderStyle].cssValues + if outline.Style > 0 && outline.Style < len(values) && outline.Color.Alpha() > 0 && + outline.Width.Type != Auto && outline.Width.Type != SizeInFraction && + outline.Width.Type != SizeInPercent && outline.Width.Value > 0 { + builder.addValues("outline", " ", outline.Width.cssString("0"), values[outline.Style], outline.Color.cssString()) + } +} + +func (outline ViewOutline) cssString() string { + var builder cssValueBuilder + outline.cssValue(&builder) + return builder.finish() +} + +func getOutline(properties Properties) OutlineProperty { + if value := properties.Get(Outline); value != nil { + if outline, ok := value.(OutlineProperty); ok { + return outline + } + } + + return nil +} + +func (style *viewStyle) setOutline(value interface{}) bool { + switch value := value.(type) { + case OutlineProperty: + style.properties[Outline] = value + + case ViewOutline: + style.properties[Outline] = NewOutlineProperty(Params{Style: value.Style, Width: value.Width, ColorProperty: value.Color}) + + case ViewBorder: + style.properties[Outline] = NewOutlineProperty(Params{Style: value.Style, Width: value.Width, ColorProperty: value.Color}) + + case DataObject: + outline := NewOutlineProperty(nil) + for _, tag := range []string{Style, Width, ColorProperty} { + if text, ok := value.PropertyValue(tag); ok && text != "" { + outline.Set(tag, text) + } + } + style.properties[Outline] = outline + + default: + notCompatibleType(Outline, value) + return false + } + + return true +} diff --git a/path.go b/path.go new file mode 100644 index 0000000..413e805 --- /dev/null +++ b/path.go @@ -0,0 +1,196 @@ +package rui + +import ( + "strconv" + "strings" +) + +// 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) + + // LineTo adds a straight line to the current sub-path by connecting + // the sub-path's last point to the specified (x, y) coordinates + LineTo(x, y float64) + + // 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. + 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-axis and expressed in radians. + // 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, + // 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. + 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. + 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 + // from the positive x-axis and expressed in radians; + // 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 + 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() + + scriptText() string +} + +type pathData struct { + script strings.Builder +} + +// NewPath creates a new empty Path +func NewPath() Path { + path := new(pathData) + path.script.Grow(4096) + path.script.WriteString("\nctx.beginPath();") + return path +} + +func (path *pathData) Reset() { + path.script.Reset() + path.script.WriteString("\nctx.beginPath();") +} + +func (path *pathData) MoveTo(x, y float64) { + path.script.WriteString("\nctx.moveTo(") + path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64)) + path.script.WriteString(");") +} + +func (path *pathData) LineTo(x, y float64) { + path.script.WriteString("\nctx.lineTo(") + path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64)) + path.script.WriteString(");") +} + +func (path *pathData) ArcTo(x0, y0, x1, y1, radius float64) { + if radius > 0 { + path.script.WriteString("\nctx.arcTo(") + path.script.WriteString(strconv.FormatFloat(x0, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(y0, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(x1, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(y1, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(radius, 'g', -1, 64)) + path.script.WriteString(");") + } +} + +func (path *pathData) Arc(x, y, radius, startAngle, endAngle float64, clockwise bool) { + if radius > 0 { + path.script.WriteString("\nctx.arc(") + path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(radius, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(startAngle, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(endAngle, 'g', -1, 64)) + if !clockwise { + path.script.WriteString(",true);") + } else { + path.script.WriteString(");") + } + } +} + +func (path *pathData) BezierCurveTo(cp0x, cp0y, cp1x, cp1y, x, y float64) { + path.script.WriteString("\nctx.bezierCurveTo(") + path.script.WriteString(strconv.FormatFloat(cp0x, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(cp0y, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(cp1x, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(cp1y, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64)) + path.script.WriteString(");") +} + +func (path *pathData) QuadraticCurveTo(cpx, cpy, x, y float64) { + path.script.WriteString("\nctx.quadraticCurveTo(") + path.script.WriteString(strconv.FormatFloat(cpx, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(cpy, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64)) + path.script.WriteString(");") +} + +func (path *pathData) Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, clockwise bool) { + if radiusX > 0 && radiusY > 0 { + path.script.WriteString("\nctx.ellipse(") + path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(radiusX, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(radiusY, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(rotation, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(startAngle, 'g', -1, 64)) + path.script.WriteRune(',') + path.script.WriteString(strconv.FormatFloat(endAngle, 'g', -1, 64)) + if !clockwise { + path.script.WriteString(",true);") + } else { + path.script.WriteString(");") + } + } +} + +func (path *pathData) Close() { + path.script.WriteString("\nctx.close();") +} + +func (path *pathData) scriptText() string { + return path.script.String() +} diff --git a/pointerEvents.go b/pointerEvents.go new file mode 100644 index 0000000..60a6020 --- /dev/null +++ b/pointerEvents.go @@ -0,0 +1,341 @@ +package rui + +import ( + "strings" +) + +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" + + // 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" + + // 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" + + // 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" + + // 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" + + // 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" +) + +type PointerEvent struct { + MouseEvent + + // PointerID is a unique identifier for the pointer causing the event. + PointerID int + + // Width is the width (magnitude on the X axis), in pixels, of the contact geometry of the pointer. + Width float64 + // Height is the height (magnitude on the Y axis), in pixels, of the contact geometry of the pointer. + Height float64 + + // Pressure is the normalized pressure of the pointer input in the range 0 to 1, where 0 and 1 represent + // the minimum and maximum pressure the hardware is capable of detecting, respectively. + Pressure float64 + + // TangentialPressure is the normalized tangential pressure of the pointer input (also known + // as barrel pressure or cylinder stress) in the range -1 to 1, where 0 is the neutral position of the control. + TangentialPressure float64 + + // TiltX is the plane angle (in degrees, in the range of -90 to 90) between the Y–Z plane + // and the plane containing both the pointer (e.g. pen stylus) axis and the Y axis. + TiltX float64 + + // TiltY is the plane angle (in degrees, in the range of -90 to 90) between the X–Z plane + // and the plane containing both the pointer (e.g. pen stylus) axis and the X axis. + TiltY float64 + + // Twist is the clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees, + // with a value in the range 0 to 359. + Twist float64 + + // PointerType indicates the device type that caused the event ("mouse", "pen", "touch", etc.) + PointerType string + + // IsPrimary indicates if the pointer represents the primary pointer of this pointer type. + IsPrimary bool +} + +func valueToPointerListeners(value interface{}) ([]func(View, PointerEvent), bool) { + if value == nil { + return nil, true + } + + switch value := value.(type) { + case func(View, PointerEvent): + return []func(View, PointerEvent){value}, true + + case func(PointerEvent): + fn := func(view View, event PointerEvent) { + value(event) + } + return []func(View, PointerEvent){fn}, true + + case func(View): + fn := func(view View, event PointerEvent) { + value(view) + } + return []func(View, PointerEvent){fn}, true + + case func(): + fn := func(view View, event PointerEvent) { + value() + } + return []func(View, PointerEvent){fn}, true + + case []func(View, PointerEvent): + if len(value) == 0 { + return nil, true + } + for _, fn := range value { + if fn == nil { + return nil, false + } + } + return value, true + + case []func(PointerEvent): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View, PointerEvent), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(view View, event PointerEvent) { + v(event) + } + } + return listeners, true + + case []func(View): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View, PointerEvent), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(view View, event PointerEvent) { + v(view) + } + } + return listeners, true + + case []func(): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View, PointerEvent), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(view View, event PointerEvent) { + v() + } + } + return listeners, true + + case []interface{}: + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View, PointerEvent), count) + for i, v := range value { + if v == nil { + return nil, false + } + switch v := v.(type) { + case func(View, PointerEvent): + listeners[i] = v + + case func(PointerEvent): + listeners[i] = func(view View, event PointerEvent) { + v(event) + } + + case func(View): + listeners[i] = func(view View, event PointerEvent) { + v(view) + } + + case func(): + listeners[i] = func(view View, event PointerEvent) { + v() + } + + default: + return nil, false + } + } + return listeners, true + } + + return nil, false +} + +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 interface{}) bool { + listeners, ok := valueToPointerListeners(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 { + updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session()) + } + } else { + return false + } + return true +} + +func (view *viewData) removePointerListener(tag string) { + delete(view.properties, tag) + if view.created { + if js, ok := pointerEvents[tag]; ok { + updateProperty(view.htmlID(), js.jsEvent, "", view.Session()) + } + } +} + +func getPointerListeners(view View, subviewID string, tag string) []func(View, PointerEvent) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.Get(tag); value != nil { + if result, ok := value.([]func(View, PointerEvent)); ok { + return result + } + } + } + return []func(View, PointerEvent){} +} + +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 + `="` + js.jsFunc + `(this, event)" `) + } + } + } +} + +func (event *PointerEvent) init(data DataObject) { + event.MouseEvent.init(data) + + event.PointerID = dataIntProperty(data, "pointerId") + event.Width = dataFloatProperty(data, "width") + event.Height = dataFloatProperty(data, "height") + event.Pressure = dataFloatProperty(data, "pressure") + event.TangentialPressure = dataFloatProperty(data, "tangentialPressure") + event.TiltX = dataFloatProperty(data, "tiltX") + event.TiltY = dataFloatProperty(data, "tiltY") + event.Twist = dataFloatProperty(data, "twist") + value, _ := data.PropertyValue("pointerType") + event.PointerType = value + event.IsPrimary = dataBoolProperty(data, "isPrimary") +} + +func handlePointerEvents(view View, tag string, data DataObject) { + listeners := getPointerListeners(view, "", tag) + if len(listeners) == 0 { + return + } + + var event PointerEvent + event.init(data) + + for _, listener := range listeners { + listener(view, event) + } +} + +// GetPointerDownListeners returns the "pointer-down" listener list. If there are no listeners then the empty list is returned. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetPointerDownListeners(view View, subviewID string) []func(View, PointerEvent) { + return getPointerListeners(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 "" then a value from the first argument (view) is returned. +func GetPointerUpListeners(view View, subviewID string) []func(View, PointerEvent) { + return getPointerListeners(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 "" then a value from the first argument (view) is returned. +func GetPointerMoveListeners(view View, subviewID string) []func(View, PointerEvent) { + return getPointerListeners(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 "" then a value from the first argument (view) is returned. +func GetPointerCancelListeners(view View, subviewID string) []func(View, PointerEvent) { + return getPointerListeners(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 "" then a value from the first argument (view) is returned. +func GetPointerOverListeners(view View, subviewID string) []func(View, PointerEvent) { + return getPointerListeners(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 "" then a value from the first argument (view) is returned. +func GetPointerOutListeners(view View, subviewID string) []func(View, PointerEvent) { + return getPointerListeners(view, subviewID, PointerOut) +} diff --git a/popup.go b/popup.go new file mode 100644 index 0000000..f7d04a2 --- /dev/null +++ b/popup.go @@ -0,0 +1,310 @@ +package rui + +import "strings" + +const ( + // Title is the Popup string property + Title = "title" + // TitleStyle is the Popup string property + TitleStyle = "title-style" + // CloseButton is the Popup bool property + CloseButton = "close-button" + // OutsideClose is the Popup bool property + OutsideClose = "outside-close" + Buttons = "buttons" + ButtonsAlign = "buttons-align" +) + +type PopupButton struct { + Title string + OnClick func(Popup) +} + +// Popup interface +type Popup interface { + //Properties + View() View + Session() Session + Show() + Dismiss() + html(buffer *strings.Builder) + viewByHTMLID(id string) View +} + +type popupData struct { + //propertyList + layerView View + view View +} + +type popupManager struct { + popups []Popup +} + +func (popup *popupData) init(view View, params Params) { + popup.view = view + + props := propertyList{properties: params} + session := view.Session() + + var title View = nil + titleStyle := "ruiPopupTitle" + closeButton, _ := boolProperty(&props, CloseButton, session) + outsideClose, _ := boolProperty(&props, OutsideClose, session) + vAlign, _ := enumProperty(&props, VerticalAlign, session, CenterAlign) + hAlign, _ := enumProperty(&props, HorizontalAlign, session, CenterAlign) + buttonsAlign, _ := enumProperty(&props, ButtonsAlign, session, RightAlign) + + buttons := []PopupButton{} + if value, ok := params[Buttons]; ok && value != nil { + switch value := value.(type) { + case PopupButton: + buttons = []PopupButton{value} + + case []PopupButton: + buttons = value + } + } + + popupView := NewGridLayout(view.Session(), Params{ + Style: "ruiPopup", + MaxWidth: Percent(100), + MaxHeight: Percent(100), + CellVerticalAlign: StretchAlign, + CellHorizontalAlign: StretchAlign, + ClickEvent: func(View) {}, + }) + + for tag, value := range params { + switch tag { + case Title: + switch value := value.(type) { + case string: + title = NewTextView(view.Session(), Params{Text: value}) + + case View: + title = value + + default: + notCompatibleType(Title, value) + } + + case TitleStyle: + switch value := value.(type) { + case string: + titleStyle = value + + default: + notCompatibleType(TitleStyle, value) + } + + case CloseButton, OutsideClose, VerticalAlign, HorizontalAlign: + // do nothing + + default: + popupView.Set(tag, value) + } + } + + var cellHeight []SizeUnit + viewRow := 0 + if title != nil || closeButton { + viewRow = 1 + titleHeight, _ := sizeConstant(popup.Session(), "popupTitleHeight") + titleView := NewGridLayout(session, Params{ + Row: 0, + Style: titleStyle, + CellWidth: []SizeUnit{Fr(1), titleHeight}, + CellVerticalAlign: CenterAlign, + PaddingLeft: Px(12), + }) + if title != nil { + titleView.Append(title) + } + if closeButton { + titleView.Append(NewGridLayout(session, Params{ + Column: 1, + Height: titleHeight, + Width: titleHeight, + CellHorizontalAlign: CenterAlign, + CellVerticalAlign: CenterAlign, + TextSize: Px(20), + Content: "✕", + ClickEvent: func(View) { + popup.Dismiss() + }, + })) + } + + popupView.Append(titleView) + cellHeight = []SizeUnit{AutoSize(), Fr(1)} + } else { + cellHeight = []SizeUnit{Fr(1)} + } + + view.Set(Row, viewRow) + popupView.Append(view) + + if buttonCount := len(buttons); buttonCount > 0 { + cellHeight = append(cellHeight, AutoSize()) + gap, _ := sizeConstant(session, "popupButtonGap") + cellWidth := []SizeUnit{} + for i := 0; i < buttonCount; i++ { + cellWidth = append(cellWidth, Fr(1)) + } + + buttonsPanel := NewGridLayout(session, Params{ + CellWidth: cellWidth, + }) + if gap.Type != Auto && gap.Value > 0 { + buttonsPanel.Set(Gap, gap) + buttonsPanel.Set(Margin, gap) + } + + createButton := func(n int, button PopupButton) Button { + return NewButton(session, Params{ + Column: n, + Content: button.Title, + ClickEvent: func() { + if button.OnClick != nil { + button.OnClick(popup) + } else { + popup.Dismiss() + } + }, + }) + } + for i, button := range buttons { + buttonsPanel.Append(createButton(i, button)) + } + + popupView.Append(NewGridLayout(session, Params{ + Row: viewRow + 1, + CellHorizontalAlign: buttonsAlign, + Content: buttonsPanel, + })) + } + popupView.Set(CellHeight, cellHeight) + + popup.layerView = NewGridLayout(session, Params{ + Style: "ruiPopupLayer", + CellVerticalAlign: vAlign, + CellHorizontalAlign: hAlign, + Content: popupView, + MaxWidth: Percent(100), + MaxHeight: Percent(100), + }) + + if outsideClose { + popup.layerView.Set(ClickEvent, func(View) { + popup.Dismiss() + }) + } +} + +func (popup popupData) View() View { + return popup.view +} + +func (popup *popupData) Session() Session { + return popup.view.Session() +} + +func (popup *popupData) Dismiss() { + popup.Session().popupManager().dismissPopup(popup) + // TODO +} + +func (popup *popupData) Show() { + popup.Session().popupManager().showPopup(popup) +} + +func (popup *popupData) html(buffer *strings.Builder) { + + viewHTML(popup.layerView, buffer) +} + +func (popup *popupData) viewByHTMLID(id string) View { + return viewByHTMLID(id, popup.layerView) +} + +// NewPopup creates a new Popup +func NewPopup(view View, param Params) Popup { + if view == nil { + return nil + } + + popup := new(popupData) + popup.init(view, param) + return popup +} + +func (manager *popupManager) updatePopupLayerInnerHTML(session Session) { + if manager.popups == nil { + manager.popups = []Popup{} + session.runScript(`updateInnerHTML('ruiPopupLayer', '');`) + return + } + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + buffer.WriteString(`updateInnerHTML('ruiPopupLayer', '`) + for _, p := range manager.popups { + p.html(buffer) + } + buffer.WriteString(`');`) + session.runScript(buffer.String()) +} + +func (manager *popupManager) showPopup(popup Popup) { + if popup == nil { + return + } + + session := popup.Session() + if manager.popups == nil || len(manager.popups) == 0 { + manager.popups = []Popup{popup} + } else { + manager.popups = append(manager.popups, popup) + } + manager.updatePopupLayerInnerHTML(session) + updateCSSProperty("ruiPopupLayer", "visibility", "visible", session) +} + +func (manager *popupManager) dismissPopup(popup Popup) { + if manager.popups == nil { + manager.popups = []Popup{} + return + } + + count := len(manager.popups) + if count <= 0 || popup == nil { + return + } + + session := popup.Session() + if manager.popups[count-1] == popup { + if count == 1 { + manager.popups = []Popup{} + updateCSSProperty("ruiPopupLayer", "visibility", "hidden", session) + session.runScript(`updateInnerHTML('ruiPopupLayer', '');`) + } else { + manager.popups = manager.popups[:count-1] + manager.updatePopupLayerInnerHTML(session) + } + return + } + + for n, p := range manager.popups { + if p == popup { + if n == 0 { + manager.popups = manager.popups[1:] + } else { + manager.popups = append(manager.popups[:n], manager.popups[n+1:]...) + } + manager.updatePopupLayerInnerHTML(session) + return + } + } +} diff --git a/popupUtils.go b/popupUtils.go new file mode 100644 index 0000000..274746d --- /dev/null +++ b/popupUtils.go @@ -0,0 +1,171 @@ +package rui + +// ShowMessage displays the popup with text message +func ShowMessage(title, text string, session Session) { + textView := NewTextView(session, Params{ + Text: text, + Style: "ruiMessageText", + }) + params := Params{ + CloseButton: true, + OutsideClose: true, + } + if title != "" { + params[Title] = title + } + NewPopup(textView, params).Show() +} + +func ShowQuestion(title, text string, session Session, onYes func(), onNo func()) { + textView := NewTextView(session, Params{ + Text: text, + Style: "ruiMessageText", + }) + params := Params{ + CloseButton: false, + OutsideClose: false, + Buttons: []PopupButton{ + { + Title: "No", + OnClick: func(popup Popup) { + popup.Dismiss() + if onNo != nil { + onNo() + } + }, + }, + { + Title: "Yes", + OnClick: func(popup Popup) { + popup.Dismiss() + if onYes != nil { + onYes() + } + }, + }, + }, + } + if title != "" { + params[Title] = title + } + NewPopup(textView, params).Show() +} + +func ShowCancellableQuestion(title, text string, session Session, onYes func(), onNo func(), onCancel func()) { + textView := NewTextView(session, Params{ + Text: text, + Style: "ruiMessageText", + }) + + params := Params{ + CloseButton: false, + OutsideClose: false, + Buttons: []PopupButton{ + { + Title: "Cancel", + OnClick: func(popup Popup) { + popup.Dismiss() + if onCancel != nil { + onCancel() + } + }, + }, + { + Title: "No", + OnClick: func(popup Popup) { + popup.Dismiss() + if onNo != nil { + onNo() + } + }, + }, + { + Title: "Yes", + OnClick: func(popup Popup) { + popup.Dismiss() + if onYes != nil { + onYes() + } + }, + }, + }, + } + if title != "" { + params[Title] = title + } + NewPopup(textView, params).Show() +} + +type popupMenuData struct { + items []string + session Session + popup Popup + result func(int) +} + +func (popup *popupMenuData) itemClick(list ListView, n int) { + popup.popup.Dismiss() + if popup.result != nil { + popup.result(n) + } +} + +func (popup *popupMenuData) ListSize() int { + return len(popup.items) +} + +func (popup *popupMenuData) ListItem(index int, session Session) View { + return NewTextView(popup.session, Params{ + Text: popup.items[index], + Style: "ruiPopupMenuItem", + }) +} + +func (popup *popupMenuData) IsListItemEnabled(index int) bool { + return true +} + +const PopupMenuResult = "popup-menu-result" + +// ShowMenu displays the popup with text message +func ShowMenu(session Session, params Params) bool { + value, ok := params[Items] + if !ok || value == nil { + ErrorLog("Unable to show empty menu") + return false + } + + var adapter ListAdapter + data := new(popupMenuData) + data.session = session + + switch value := value.(type) { + case []string: + data.items = value + adapter = data + + case ListAdapter: + adapter = value + + default: + notCompatibleType(Items, value) + return false + } + + value, ok = params[PopupMenuResult] + if ok && value != nil { + if result, ok := value.(func(int)); ok { + data.result = result + } + } + + listView := NewListView(session, Params{ + Items: adapter, + Orientation: TopDownOrientation, + ListItemClickedEvent: data.itemClick, + }) + data.popup = NewPopup(listView, params) + data.popup.Show() + FocusView(listView) + return true +} diff --git a/progressBar.go b/progressBar.go new file mode 100644 index 0000000..c1b0814 --- /dev/null +++ b/progressBar.go @@ -0,0 +1,134 @@ +package rui + +import ( + "strconv" + "strings" +) + +const ( + ProgressBarMax = "progress-max" + ProgressBarValue = "progress-value" +) + +// ProgressBar - ProgressBar view +type ProgressBar interface { + View +} + +type progressBarData struct { + viewData +} + +// NewProgressBar create new ProgressBar object and return it +func NewProgressBar(session Session, params Params) ProgressBar { + view := new(progressBarData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newProgressBar(session Session) View { + return NewProgressBar(session, nil) +} + +func (progress *progressBarData) Init(session Session) { + progress.viewData.Init(session) + progress.tag = "ProgressBar" +} + +func (progress *progressBarData) normalizeTag(tag string) string { + tag = strings.ToLower(tag) + switch tag { + case Max, "progress-bar-max", "progressbar-max": + return ProgressBarMax + + case Value, "progress-bar-value", "progressbar-value": + return ProgressBarValue + } + return tag +} + +func (progress *progressBarData) Remove(tag string) { + progress.remove(progress.normalizeTag(tag)) +} + +func (progress *progressBarData) remove(tag string) { + progress.viewData.remove(tag) + progress.propertyChanged(tag) +} + +func (progress *progressBarData) propertyChanged(tag string) { + switch tag { + case ProgressBarMax: + updateProperty(progress.htmlID(), Max, strconv.FormatFloat(GetProgressBarMax(progress, ""), 'f', -1, 32), progress.session) + + case ProgressBarValue: + updateProperty(progress.htmlID(), Value, strconv.FormatFloat(GetProgressBarValue(progress, ""), 'f', -1, 32), progress.session) + } +} + +func (progress *progressBarData) Set(tag string, value interface{}) bool { + return progress.set(progress.normalizeTag(tag), value) +} + +func (progress *progressBarData) set(tag string, value interface{}) bool { + if progress.viewData.set(tag, value) { + progress.propertyChanged(tag) + return true + } + return false +} + +func (progress *progressBarData) Get(tag string) interface{} { + return progress.get(progress.normalizeTag(tag)) +} + +func (progress *progressBarData) htmlTag() string { + return "progress" +} + +func (progress *progressBarData) htmlProperties(self View, buffer *strings.Builder) { + progress.viewData.htmlProperties(self, buffer) + + buffer.WriteString(` max="`) + buffer.WriteString(strconv.FormatFloat(GetProgressBarMax(progress, ""), 'f', -1, 64)) + buffer.WriteByte('"') + + buffer.WriteString(` value="`) + buffer.WriteString(strconv.FormatFloat(GetProgressBarValue(progress, ""), 'f', -1, 64)) + buffer.WriteByte('"') +} + +// GetProgressBarMax returns the max value of ProgressBar subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetProgressBarMax(view View, subviewID string) float64 { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return 0 + } + + result, ok := floatStyledProperty(view, ProgressBarMax, 1) + if !ok { + result, _ = floatStyledProperty(view, Max, 1) + } + return result +} + +// GetProgressBarValue returns the value of ProgressBar subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetProgressBarValue(view View, subviewID string) float64 { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return 0 + } + + result, ok := floatStyledProperty(view, ProgressBarValue, 0) + if !ok { + result, _ = floatStyledProperty(view, Value, 0) + } + return result +} diff --git a/properties.go b/properties.go new file mode 100644 index 0000000..1b27e80 --- /dev/null +++ b/properties.go @@ -0,0 +1,87 @@ +package rui + +import ( + "sort" + "strings" +) + +// Properties interface of properties map +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) interface{} + getRaw(tag string) interface{} + // 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 interface{}) bool + setRaw(tag string, value interface{}) + // Remove removes the property with name defined by the argument + Remove(tag string) + // Clear removes all properties + Clear() + // AllTags returns an array of the set properties + AllTags() []string +} + +type propertyList struct { + properties map[string]interface{} +} + +func (properties *propertyList) init() { + properties.properties = map[string]interface{}{} +} + +func (properties *propertyList) Get(tag string) interface{} { + return properties.getRaw(strings.ToLower(tag)) +} + +func (properties *propertyList) getRaw(tag string) interface{} { + if value, ok := properties.properties[tag]; ok { + return value + } + return nil +} + +func (properties *propertyList) setRaw(tag string, value interface{}) { + 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]interface{}{} +} + +func (properties *propertyList) AllTags() []string { + tags := make([]string, 0, len(properties.properties)) + for t := range properties.properties { + tags = append(tags, t) + } + sort.Strings(tags) + return tags +} + +func parseProperties(properties Properties, object DataObject) { + count := object.PropertyCount() + for i := 0; i < count; i++ { + if node := object.Property(i); node != nil { + switch node.Type() { + case TextNode: + properties.Set(node.Tag(), node.Text()) + + case ObjectNode: + properties.Set(node.Tag(), node.Object()) + + case ArrayNode: + properties.Set(node.Tag(), node.ArrayElements()) + } + } + } +} diff --git a/properties_test.go b/properties_test.go new file mode 100644 index 0000000..3de1304 --- /dev/null +++ b/properties_test.go @@ -0,0 +1,141 @@ +package rui + +/* +import ( + "testing" +) + +func TestProperties(t *testing.T) { + + createTestLog(t, true) + + list := new(propertyList) + list.init() + + if !list.Set("name", "abc") { + t.Error(`list.Set("name", "abc") fail`) + } + + if !list.Has("name") { + t.Error(`list.Has("name") fail`) + } + + v := list.Get("name") + if v == nil { + t.Error(`list.Get("name") fail`) + } + if text, ok := v.(string); ok { + if text != "abc" { + t.Error(`list.Get("name") != "abc"`) + } + } else { + t.Error(`list.Get("name") is not string`) + } + + sizeValues := []interface{}{"@small", "auto", "10px", Pt(20), AutoSize()} + for _, value := range sizeValues { + if !list.setSizeProperty("size", value) { + t.Errorf(`setSizeProperty("size", %v) fail`, value) + } + } + + failSizeValues := []interface{}{"@small,big", "abc", "10", Color(20), 100} + for _, value := range failSizeValues { + if list.setSizeProperty("size", value) { + t.Errorf(`setSizeProperty("size", %v) success`, value) + } + } + + angleValues := []interface{}{"@angle", "2pi", "π", "3deg", "60°", Rad(1.5), Deg(45), 1, 1.5} + for _, value := range angleValues { + if !list.setAngleProperty("angle", value) { + t.Errorf(`setAngleProperty("angle", %v) fail`, value) + } + } + + failAngleValues := []interface{}{"@angle,2", "pi32", "deg", "60°x", Color(0xFFFFFFFF)} + for _, value := range failAngleValues { + if list.setAngleProperty("angle", value) { + t.Errorf(`setAngleProperty("angle", %v) success`, value) + } + } + + colorValues := []interface{}{"@color", "#FF234567", "#234567", "rgba(30%, 128, 0.5, .25)", "rgb(30%, 128, 0.5)", Color(0xFFFFFFFF), 0xFFFFFFFF, White} + for _, color := range colorValues { + if !list.setColorProperty("color", color) { + t.Errorf(`list.setColorProperty("color", %v) fail`, color) + } + } + + failColorValues := []interface{}{"@color|2", "#FF234567FF", "#TT234567", "rgba(500%, 128, 10.5, .25)", 10.6} + for _, color := range failColorValues { + if list.setColorProperty("color", color) { + t.Errorf(`list.setColorProperty("color", %v) success`, color) + } + } + + enumValues := []interface{}{"@enum", "inherit", "on", Inherit, 2} + inheritOffOn := inheritOffOnValues() + for _, value := range enumValues { + if !list.setEnumProperty("enum", value, inheritOffOn) { + t.Errorf(`list.setEnumProperty("enum", %v, %v) fail`, value, inheritOffOn) + } + } + + failEnumValues := []interface{}{"@enum 13", "inherit2", "onn", -1, 10} + for _, value := range failEnumValues { + if list.setEnumProperty("enum", value, inheritOffOn) { + t.Errorf(`list.setEnumProperty("enum", %v, %v) success`, value, inheritOffOn) + } + } + + boolValues := []interface{}{"@bool", "true", "yes ", "on", " 1", "false", "no", "off", "0", 0, 1, false, true} + for _, value := range boolValues { + if !list.setBoolProperty("bool", value) { + t.Errorf(`list.setBoolProperty("bool", %v) fail`, value) + } + } + + failBoolValues := []interface{}{"@bool,2", "tr", "ys", "10", -1, 10, 0.8} + for _, value := range failBoolValues { + if list.setBoolProperty("bool", value) { + t.Errorf(`list.setBoolProperty("bool", %v) success`, value) + } + } + + intValues := []interface{}{"@int", " 100", "-10 ", 0, 250} + for _, value := range intValues { + if !list.setIntProperty("int", value) { + t.Errorf(`list.setIntProperty("int", %v) fail`, value) + } + } + + failIntValues := []interface{}{"@int|10", "100i", "-1.0 ", 0.0} + for _, value := range failIntValues { + if list.setIntProperty("int", value) { + t.Errorf(`list.setIntProperty("int", %v) success`, value) + } + } + + floatValues := []interface{}{"@float", " 100.25", "-1.5e12 ", uint(0), 250, float32(10.2), float64(0)} + for _, value := range floatValues { + if !list.setFloatProperty("float", value) { + t.Errorf(`list.setFloatProperty("float", %v) fail`, value) + } + } + + failFloatValues := []interface{}{"@float|2", " 100.25i", "-1.5ee12 ", "abc"} + for _, value := range failFloatValues { + if list.setFloatProperty("float", value) { + t.Errorf(`list.setFloatProperty("float", %v) success`, value) + } + } + + boundsValues := []interface{}{"@bounds", "10px,20pt,@bottom,0", Em(2), []interface{}{"@top", Px(10), AutoSize(), "14pt"}} + for _, value := range boundsValues { + if !list.setBoundsProperty("margin", value) { + t.Errorf(`list.setBoundsProperty("margin", %v) fail`, value) + } + } +} +*/ diff --git a/propertyGet.go b/propertyGet.go new file mode 100644 index 0000000..9f1ddda --- /dev/null +++ b/propertyGet.go @@ -0,0 +1,247 @@ +package rui + +import ( + "strconv" + "strings" +) + +func stringProperty(properties Properties, tag string, session Session) (string, bool) { + if value := properties.getRaw(tag); value != nil { + if text, ok := value.(string); ok { + return session.resolveConstants(text) + } + } + return "", false +} + +func valueToSizeUnit(value interface{}, session Session) (SizeUnit, bool) { + if value != nil { + switch value := value.(type) { + case SizeUnit: + return value, true + + case string: + if text, ok := session.resolveConstants(value); ok { + return StringToSizeUnit(text) + } + } + } + + return AutoSize(), false +} + +func sizeProperty(properties Properties, tag string, session Session) (SizeUnit, bool) { + return valueToSizeUnit(properties.getRaw(tag), session) +} + +func angleProperty(properties Properties, tag string, session Session) (AngleUnit, bool) { + if value := properties.getRaw(tag); value != nil { + switch value := value.(type) { + case AngleUnit: + return value, true + + case string: + if text, ok := session.resolveConstants(value); ok { + return StringToAngleUnit(text) + } + } + } + + return AngleUnit{Type: 0, Value: 0}, false +} + +func valueToColor(value interface{}, session Session) (Color, bool) { + if value != nil { + switch value := value.(type) { + case Color: + return value, true + + case string: + if len(value) > 1 && value[0] == '@' { + return session.Color(value[1:]) + } + return StringToColor(value) + } + } + + return Color(0), false +} + +func colorProperty(properties Properties, tag string, session Session) (Color, bool) { + return valueToColor(properties.getRaw(tag), session) +} + +func valueToEnum(value interface{}, tag string, session Session, defaultValue int) (int, bool) { + if value != nil { + values := enumProperties[tag].values + switch value := value.(type) { + case int: + if value >= 0 && value < len(values) { + return value, true + } + + case string: + if text, ok := session.resolveConstants(value); ok { + if tag == Orientation { + switch strings.ToLower(text) { + case "vertical": + value = "up-down" + + case "horizontal": + value = "left-to-right" + } + } + if result, ok := enumStringToInt(text, values, true); ok { + return result, true + } + } + } + } + + return defaultValue, false +} + +func enumStringToInt(value string, enumValues []string, logError bool) (int, bool) { + value = strings.Trim(value, " \t\n\r") + + for n, val := range enumValues { + if val == value { + return n, true + } + } + + if n, err := strconv.Atoi(value); err == nil { + if n >= 0 && n < len(enumValues) { + return n, true + } + + if logError { + ErrorLogF(`Out of bounds: value index = %d, valid values = [%v]`, n, enumValues) + } + return 0, false + } + + value = strings.ToLower(value) + for n, val := range enumValues { + if val == value { + return n, true + } + } + + if logError { + ErrorLogF(`Unknown "%s" value. Valid values = [%v]`, value, enumValues) + } + return 0, false +} + +func enumProperty(properties Properties, tag string, session Session, defaultValue int) (int, bool) { + return valueToEnum(properties.getRaw(tag), tag, session, defaultValue) +} + +func valueToBool(value interface{}, session Session) (bool, bool) { + if value != nil { + switch value := value.(type) { + case bool: + return value, true + + case string: + if text, ok := session.resolveConstants(value); ok { + switch strings.ToLower(text) { + case "true", "yes", "on", "1": + return true, true + + case "false", "no", "off", "0": + return false, true + + default: + ErrorLog(`The error of converting of "` + text + `" to bool`) + } + } + } + } + + return false, false +} + +func boolProperty(properties Properties, tag string, session Session) (bool, bool) { + return valueToBool(properties.getRaw(tag), session) +} + +func valueToInt(value interface{}, session Session, defaultValue int) (int, bool) { + if value != nil { + switch value := value.(type) { + case string: + if text, ok := session.resolveConstants(value); ok { + n, err := strconv.Atoi(strings.Trim(text, " \t")) + if err == nil { + return n, true + } + ErrorLog(err.Error()) + } else { + n, err := strconv.Atoi(strings.Trim(value, " \t")) + if err == nil { + return n, true + } + ErrorLog(err.Error()) + } + + default: + return isInt(value) + } + } + + return defaultValue, false +} + +func intProperty(properties Properties, tag string, session Session, defaultValue int) (int, bool) { + return valueToInt(properties.getRaw(tag), session, defaultValue) +} + +func valueToFloat(value interface{}, session Session, defaultValue float64) (float64, bool) { + if value != nil { + switch value := value.(type) { + case float64: + return value, true + + case string: + if text, ok := session.resolveConstants(value); ok { + f, err := strconv.ParseFloat(text, 64) + if err == nil { + return f, true + } + ErrorLog(err.Error()) + } + } + } + + return defaultValue, false +} + +func floatProperty(properties Properties, tag string, session Session, defaultValue float64) (float64, bool) { + return valueToFloat(properties.getRaw(tag), session, defaultValue) +} + +func valueToRange(value interface{}, session Session) (Range, bool) { + if value != nil { + switch value := value.(type) { + case Range: + return value, true + + case int: + return Range{First: value, Last: value}, true + + case string: + if text, ok := session.resolveConstants(value); ok { + var result Range + if result.setValue(text) { + return result, true + } + } + } + } + return Range{}, false +} + +func rangeProperty(properties Properties, tag string, session Session) (Range, bool) { + return valueToRange(properties.getRaw(tag), session) +} diff --git a/propertyNames.go b/propertyNames.go new file mode 100644 index 0000000..d460f8e --- /dev/null +++ b/propertyNames.go @@ -0,0 +1,449 @@ +package rui + +const ( + // ID is the constant for the "id" property tag. + ID = "id" + // Style is the constant for the "style" property tag. + Style = "style" + // StyleDisabled is the constant for the "style-disabled" property tag. + StyleDisabled = "style-disabled" + // Disabled is the constant for the "disabled" property tag. + Disabled = "disabled" + // Semantics is the constant for the "semantics" property tag. + Semantics = "semantics" + // Visibility is the constant for the "visibility" property tag. + Visibility = "visibility" + // ZIndex is the constant for the "z-index" property tag. + // The int "z-index" property sets the z-order of a positioned view. + // Overlapping views with a larger z-index cover those with a smaller one. + ZIndex = "z-index" + // Opacity is the constant for the "opacity" property tag. + // The float "opacity" property in [1..0] range sets the opacity of an element. + // Opacity is the degree to which content behind an element is hidden, and is the opposite of transparency. + Opacity = "opacity" + // Row is the constant for the "row" property tag. + Row = "row" + // Column is the constant for the "column" property tag. + Column = "column" + // Left is the constant for the "left" property tag. + // The "left" SizeUnit property participates in specifying the left border position of a positioned view. + // Used only for views placed in an AbsoluteLayout. + Left = "left" + // Right is the constant for the "right" property tag. + // The "right" SizeUnit property participates in specifying the right border position of a positioned view. + // Used only for views placed in an AbsoluteLayout. + Right = "right" + // Top is the constant for the "top" property tag. + // The "top" SizeUnit property participates in specifying the top border position of a positioned view. + // Used only for views placed in an AbsoluteLayout. + Top = "top" + // Bottom is the constant for the "bottom" property tag. + // The "bottom" SizeUnit property participates in specifying the bottom border position of a positioned view. + // Used only for views placed in an AbsoluteLayout. + Bottom = "bottom" + // Width is the constant for the "width" property tag. + // The "width" SizeUnit property sets an view's width. + Width = "width" + // Height is the constant for the "height" property tag. + // The "height" SizeUnit property sets an view's height. + Height = "height" + // MinWidth is the constant for the "min-width" property tag. + // The "width" SizeUnit property sets an view's minimal width. + MinWidth = "min-width" + // MinHeight is the constant for the "min-height" property tag. + // The "height" SizeUnit property sets an view's minimal height. + MinHeight = "min-height" + // MaxWidth is the constant for the "max-width" property tag. + // The "width" SizeUnit property sets an view's maximal width. + MaxWidth = "max-width" + // MaxHeight is the constant for the "max-height" property tag. + // The "height" SizeUnit property sets an view's maximal height. + MaxHeight = "max-height" + // Margin is the constant for the "margin" property tag. + // The "margin" property sets the margin area on all four sides of an element. + // ... + Margin = "margin" + // MarginLeft is the constant for the "margin-left" property tag. + // The "margin-left" SizeUnit property sets the margin area on the left of a view. + // A positive value places it farther from its neighbors, while a negative value places it closer. + MarginLeft = "margin-left" + // MarginRight is the constant for the "margin-right" property tag. + // The "margin-right" SizeUnit property sets the margin area on the right of a view. + // A positive value places it farther from its neighbors, while a negative value places it closer. + MarginRight = "margin-right" + // MarginTop is the constant for the "margin-top" property tag. + // The "margin-top" SizeUnit property sets the margin area on the top of a view. + // A positive value places it farther from its neighbors, while a negative value places it closer. + MarginTop = "margin-top" + // MarginBottom is the constant for the "margin-bottom" property tag. + // The "margin-bottom" SizeUnit property sets the margin area on the bottom of a view. + // A positive value places it farther from its neighbors, while a negative value places it closer. + MarginBottom = "margin-bottom" + // Padding is the constant for the "padding" property tag. + // The "padding" Bounds property sets the padding area on all four sides of a view at once. + // An element's padding area is the space between its content and its border. + Padding = "padding" + // PaddingLeft is the constant for the "padding-left" property tag. + // The "padding-left" SizeUnit property sets the width of the padding area to the left of a view. + PaddingLeft = "padding-left" + // PaddingRight is the constant for the "padding-right" property tag. + // The "padding-right" SizeUnit property sets the width of the padding area to the right of a view. + PaddingRight = "padding-right" + // PaddingTop is the constant for the "padding-top" property tag. + // The "padding-top" SizeUnit property sets the height of the padding area to the top of a view. + PaddingTop = "padding-top" + // PaddingBottom is the constant for the "padding-bottom" property tag. + // The "padding-bottom" SizeUnit property sets the height of the padding area to the bottom of a view. + PaddingBottom = "padding-bottom" + // BackgroundColor is the constant for the "background-color" property tag. + // The "background-color" property sets the background color of a view. + BackgroundColor = "background-color" + // Background is the constant for the "background" property tag. + // The "background" property sets one or more background images and/or gradients on a view. + // ... + Background = "background" + // Cursor is the constant for the "cursor" property tag. + // The "cursor" int property sets the type of mouse cursor, if any, to show when the mouse pointer is over a view + // Valid values are "auto" (0), "default" (1), "none" (2), "context-menu" (3), "help" (4), "pointer" (5), + // "progress" (6), "wait" (7), "cell" (8), "crosshair" (9), "text" (10), "vertical-text" (11), "alias" (12), + // "copy" (13), "move" (14), "no-drop" (15), "not-allowed" (16), "e-resize" (17), "n-resize" (18), + // "ne-resize" (19), "nw-resize" (20), "s-resize" (21), "se-resize" (22), "sw-resize" (23), "w-resize" (24), + // "ew-resize" (25), "ns-resize" (26), "nesw-resize" (27), "nwse-resize" (28), "col-resize" (29), + // "row-resize" (30), "all-scroll" (31), "zoom-in" (32), "zoom-out" (33), "grab" (34), "grabbing" (35). + Cursor = "cursor" + // Border is the constant for the "border" property tag. + // The "border" property sets a view's border. It sets the values of a border width, style, and color. + // This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation. + Border = "border" + // BorderLeft is the constant for the "border-left" property tag. + // The "border-left" property sets a view's left border. It sets the values of a border width, style, and color. + // This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation. + BorderLeft = "border-left" + // BorderRight is the constant for the "border-right" property tag. + // The "border-right" property sets a view's right border. It sets the values of a border width, style, and color. + // This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation. + BorderRight = "border-right" + // BorderTop is the constant for the "border-top" property tag. + // The "border-top" property sets a view's top border. It sets the values of a border width, style, and color. + // This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation. + BorderTop = "border-top" + // BorderBottom is the constant for the "border-bottom" property tag. + // The "border-bottom" property sets a view's bottom border. It sets the values of a border width, style, and color. + // This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation. + BorderBottom = "border-bottom" + // BorderStyle is the constant for the "border-style" property tag. + // The "border-style" property sets the line style for all four sides of a view's border. + // Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4). + BorderStyle = "border-style" + // BorderLeftStyle is the constant for the "border-left-style" property tag. + // The "border-left-style" int property sets the line style of a view's left border. + // Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4). + BorderLeftStyle = "border-left-style" + // BorderRightStyle is the constant for the "border-right-style" property tag. + // The "border-right-style" int property sets the line style of a view's right border. + // Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4). + BorderRightStyle = "border-right-style" + // BorderTopStyle is the constant for the "border-top-style" property tag. + // The "border-top-style" int property sets the line style of a view's top border. + // Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4). + BorderTopStyle = "border-top-style" + // BorderBottomStyle is the constant for the "border-bottom-style" property tag. + // The "border-bottom-style" int property sets the line style of a view's bottom border. + // Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4). + BorderBottomStyle = "border-bottom-style" + // BorderWidth is the constant for the "border-width" property tag. + // The "border-width" property sets the line width for all four sides of a view's border. + BorderWidth = "border-width" + // BorderLeftWidth is the constant for the "border-left-width" property tag. + // The "border-left-width" SizeUnit property sets the line width of a view's left border. + BorderLeftWidth = "border-left-width" + // BorderRightWidth is the constant for the "border-right-width" property tag. + // The "border-right-width" SizeUnit property sets the line width of a view's right border. + BorderRightWidth = "border-right-width" + // BorderTopWidth is the constant for the "border-top-width" property tag. + // The "border-top-width" SizeUnit property sets the line width of a view's top border. + BorderTopWidth = "border-top-width" + // BorderBottomWidth is the constant for the "border-bottom-width" property tag. + // The "border-bottom-width" SizeUnit property sets the line width of a view's bottom border. + BorderBottomWidth = "border-bottom-width" + // BorderColor is the constant for the "border-color" property tag. + // The "border-color" property sets the line color for all four sides of a view's border. + BorderColor = "border-color" + // BorderLeftColor is the constant for the "border-left-color" property tag. + // The "border-left-color" property sets the line color of a view's left border. + BorderLeftColor = "border-left-color" + // BorderRightColor is the constant for the "border-right-color" property tag. + // The "border-right-color" property sets the line color of a view's right border. + BorderRightColor = "border-right-color" + // BorderTopColor is the constant for the "border-top-color" property tag. + // The "border-top-color" property sets the line color of a view's top border. + BorderTopColor = "border-top-color" + // BorderBottomColor is the constant for the "border-bottom-color" property tag. + // The "border-bottom-color" property sets the line color of a view's bottom border. + BorderBottomColor = "border-bottom-color" + // Outline is the constant for the "outline" property tag. + // The "border" property sets a view's outline. It sets the values of an outline width, style, and color. + Outline = "outline" + // OutlineStyle is the constant for the "outline-style" property tag. + // The "outline-style" int property sets the style of an view's outline. + // Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4). + OutlineStyle = "outline-style" + // OutlineColor is the constant for the "outline-color" property tag. + // The "outline-color" property sets the color of an view's outline. + OutlineColor = "outline-color" + // OutlineWidth is the constant for the "outline-width" property tag. + // The "outline-width" SizeUnit property sets the width of an view's outline. + OutlineWidth = "outline-width" + // Shadow is the constant for the "shadow" property tag. + // The "shadow" property adds shadow effects around a view's frame. A shadow is described + // by X and Y offsets relative to the element, blur and spread radius, and color. + // ... + Shadow = "shadow" + // FontName is the constant for the "font-name" property tag. + // The "font-name" string property specifies a prioritized list of one or more font family names and/or + // generic family names for the selected view. Values are separated by commas to indicate that they are alternatives. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + FontName = "font-name" + // TextColor is the constant for the "text-color" property tag. + // The "color" property sets the foreground color value of a view's text and text decorations. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + TextColor = "text-color" + // TextSize is the constant for the "text-size" property tag. + // The "text-size" SizeUnit property sets the size of the font. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + TextSize = "text-size" + // Italic is the constant for the "italic" property tag. + // The "italic" is the bool property. If it is "true" then a text is displayed in italics. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + Italic = "italic" + // SmallCaps is the constant for the "small-caps" property tag. + // The "small-caps" is the bool property. If it is "true" then a text is displayed in small caps. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + SmallCaps = "small-caps" + // Strikethrough is the constant for the "strikethrough" property tag. + // The "strikethrough" is the bool property. If it is "true" then a text is displayed strikethrough. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + Strikethrough = "strikethrough" + // Overline is the constant for the "overline" property tag. + // The "overline" is the bool property. If it is "true" then a text is displayed overlined. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + Overline = "overline" + // Underline is the constant for the "underline" property tag. + // The "underline" is the bool property. If it is "true" then a text is displayed underlined. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + Underline = "underline" + // TextLineThickness is the constant for the "text-decoration-thickness" property tag. + // The "text-decoration-thickness" SizeUnit property sets the stroke thickness of the decoration line that + // is used on text in an element, such as a line-through, underline, or overline. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + TextLineThickness = "text-line-thickness" + // TextLineStyle is the constant for the "text-decoration-style" property tag. + // The "text-decoration-style" int property sets the style of the lines specified by "text-decoration" property. + // The style applies to all lines that are set with "text-decoration" property. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + TextLineStyle = "text-line-style" + // TextLineColor is the constant for the "text-decoration-color" property tag. + // The "text-decoration-color" Color property sets the color of the lines specified by "text-decoration" property. + // The color applies to all lines that are set with "text-decoration" property. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + TextLineColor = "text-line-color" + // TextWeight is the constant for the "text-weight" property tag. + // Valid values are SolidLine (1), DashedLine (2), DottedLine (3), DoubleLine (4) and WavyLine (5). + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + TextWeight = "text-weight" + // TextAlign is the constant for the "text-align" property tag. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + TextAlign = "text-align" + // TextIndent is the constant for the "text-indent" property tag. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + TextIndent = "text-indent" + // TextShadow is the constant for the "text-shadow" property tag. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + TextShadow = "text-shadow" + // LetterSpacing is the constant for the "letter-spacing" property tag. + // The "letter-spacing" SizeUnit property sets the horizontal spacing behavior between text characters. + // This value is added to the natural spacing between characters while rendering the text. + // Positive values of letter-spacing causes characters to spread farther apart, + // while negative values of letter-spacing bring characters closer together. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + LetterSpacing = "letter-spacing" + // WordSpacing is the constant for the "word-spacing" property tag. + // The "word-spacing" SizeUnit property sets the length of space between words and between tags. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + WordSpacing = "word-spacing" + // LineHeight is the constant for the "line-height" property tag. + // The "line-height" SizeUnit property sets the height of a line box. + // It's commonly used to set the distance between lines of text. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + LineHeight = "line-height" + // WhiteSpace is the constant for the "white-space" property tag. + // The "white-space" int property sets how white space inside an element is handled. + // Valid values are WhiteSpaceNormal (0), WhiteSpaceNowrap (1), WhiteSpacePre (2), + // WhiteSpacePreWrap (3), WhiteSpacePreLine (4), WhiteSpaceBreakSpaces (5) + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + WhiteSpace = "white-space" + // WordBreak is the constant for the "word-break" property tag. + // The "word-break" int property sets whether line breaks appear wherever the text would otherwise overflow its content box. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + WordBreak = "word-break" + // TextTransform is the constant for the "text-transform" property tag. + // The "text-transform" int property specifies how to capitalize an element's text. + // It can be used to make text appear in all-uppercase or all-lowercase, or with each word capitalized. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + TextTransform = "text-transform" + // TextDirection is the constant for the "text-direction" property tag. + // The "text-direction" int property sets the direction of text, table columns, and horizontal overflow. + // Use 1 (LeftToRightDirection) for languages written from right to left (like Hebrew or Arabic), + // and 2 (RightToLeftDirection) for those written from left to right (like English and most other languages). + // The default value of the property is 0 (SystemTextDirection): use the system text direction. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + TextDirection = "text-direction" + // WritingMode is the constant for the "writing-mode" property tag. + // The "writing-mode" int property sets whether lines of text are laid out horizontally or vertically, + // as well as the direction in which blocks progress + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + WritingMode = "writing-mode" + // VerticalTextOrientation is the constant for the "vertical-text-orientation" property tag. + // The "vertical-text-orientation" int property sets the orientation of the text characters in a line. + // It only affects text in vertical mode ("writing-mode" property). + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + VerticalTextOrientation = "vertical-text-orientation" + // TextTverflow is the constant for the "text-overflow" property tag. + // The "text-overflow" int property sets how hidden overflow content is signaled to users. + // It can be clipped or display an ellipsis ('…'). Valid values are + TextOverflow = "text-overflow" + // Hint is the constant for the "hint" property tag. + // The "hint" string property sets a hint to the user of what can be entered in the control. + Hint = "hint" + // MaxLength is the constant for the "max-length" property tag. + // The "max-length" int property sets the maximum number of characters that the user can enter + MaxLength = "max-length" + // ReadOnly is the constant for the "readonly" property tag. + // This bool property indicates that the user cannot modify the value of the EditView. + ReadOnly = "readonly" + // Content is the constant for the "content" property tag. + Content = "content" + // Items is the constant for the "items" property tag. + Items = "items" + // Current is the constant for the "current" property tag. + Current = "current" + // Type is the constant for the "type" property tag. + Type = "type" + // Pattern is the constant for the "pattern" property tag. + Pattern = "pattern" + // CellWidth is the constant for the "cell-width" property tag. + CellWidth = "cell-width" + // CellHeight is the constant for the "cell-height" property tag. + CellHeight = "cell-height" + // RowGap is the constant for the "row-gap" property tag. + GridRowGap = "grid-row-gap" + // ColumnGap is the constant for the "column-gap" property tag. + GridColumnGap = "grid-column-gap" + // Source is the constant for the "src" property tag. + Source = "src" + // Fit is the constant for the "fit" property tag. + Fit = "fit" + backgroundFit = "background-fit" + // Repeat is the constant for the "repeat" property tag. + Repeat = "repeat" + // Attachment is the constant for the "attachment" property tag. + Attachment = "attachment" + // Clip is the constant for the "clip" property tag. + BackgroundClip = "background-clip" + // Gradient is the constant for the "gradient" property tag. + Gradient = "gradient" + // Direction is the constant for the "direction" property tag. + Direction = "direction" + // Repeating is the constant for the "repeating" property tag. + Repeating = "repeating" + // RadialGradientRadius is the constant for the "radial-gradient-radius" property tag. + RadialGradientRadius = "radial-gradient-radius" + // RadialGradientShape is the constant for the "radial-gradient-shape" property tag. + RadialGradientShape = "radial-gradient-shape" + // Shape is the constant for the "shape" property tag. It's a short form of "radial-gradient-shape" + Shape = "shape" + // CenterX is the constant for the "center-x" property tag. + CenterX = "center-x" + // CenterY is the constant for the "center-x" property tag. + CenterY = "center-y" + // AltText is the constant for the "alt-text" property tag. + AltText = "alt-text" + altProperty = "alt" + // AvoidBreak is the constant for the "avoid-break" property tag. + // The "avoid-break" bool property sets how region breaks should behave inside a generated box. + // If the property value is "true" then fvoids any break from being inserted within the principal box. + // If the property value is "false" then allows, but does not force, any break to be inserted within + // the principal box. + AvoidBreak = "avoid-break" + // ItemWidth is the constant for the "item-width" property tag. + ItemWidth = "item-width" + // ItemHeight is the constant for the "item-height" property tag. + ItemHeight = "item-height" + // Wrap is the constant for the "wrap" property tag. + Wrap = "wrap" + // Min is the constant for the "min" property tag. + Min = "min" + // Max is the constant for the "max" property tag. + Max = "max" + // Step is the constant for the "step" property tag. + Step = "step" + // Value is the constant for the "value" property tag. + Value = "value" + // Orientation is the constant for the "orientation" property tag. + Orientation = "orientation" + // Anchor is the constant for the "anchor" property tag. + Anchor = "anchor" + // Gap is the constant for the "gap" property tag. + Gap = "gap" + // Tabs is the constant for the "tabs" property tag. + Tabs = "tabs" + // TabStyle is the constant for the "tab-style" property tag. + TabStyle = "tab-style" + // CurrentTabStyle is the constant for the "current-tab-style" property tag. + CurrentTabStyle = "current-tab-style" + // Text is the constant for the "text" property tag. + Text = "text" + // VerticalAlign is the constant for the "vertical-align" property tag. + VerticalAlign = "vertical-align" + // HorizontalAlign is the constant for the "horizontal-align" property tag. + // The "horizontal-align" int property sets the horizontal alignment of the content inside a block element + HorizontalAlign = "horizontal-align" + // ImageVerticalAlign is the constant for the "image-vertical-align" property tag. + ImageVerticalAlign = "image-vertical-align" + // ImageHorizontalAlign is the constant for the "image-horizontal-align" property tag. + ImageHorizontalAlign = "image-horizontal-align" + // Checked is the constant for the "checked" property tag. + Checked = "checked" + // ItemVerticalAlign is the constant for the "item-vertical-align" property tag. + ItemVerticalAlign = "item-vertical-align" + // ItemHorizontalAlign is the constant for the "item-horizontal-align" property tag. + ItemHorizontalAlign = "item-horizontal-align" + // ItemCheckbox is the constant for the "checkbox" property tag. + ItemCheckbox = "checkbox" + // CheckboxHorizontalAlign is the constant for the "checkbox-horizontal-align" property tag. + CheckboxHorizontalAlign = "checkbox-horizontal-align" + // CheckboxVerticalAlign is the constant for the "checkbox-vertical-align" property tag. + CheckboxVerticalAlign = "checkbox-vertical-align" + // NotTranslate is the constant for the "not-translate" property tag. + // This bool property indicates that no need to translate the text. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + NotTranslate = "not-translate" + // Filter is the constant for the "filter" property tag. + // The "filter" property applies graphical effects like blur or color shift to a View. + Filter = "filter" + // Clip is the constant for the "clip" property tag. + // The "clip" property creates a clipping region that sets what part of a View should be shown. + Clip = "clip" + // Points is the constant for the "points" property tag. + Points = "points" + // ShapeOutside is the constant for the "shape-outside" property tag. + // The "shape-outside" property defines a shape (which may be non-rectangular) around which adjacent + // inline content should wrap. By default, inline content wraps around its margin box; + // "shape-outside" provides a way to customize this wrapping, making it possible to wrap text around + // complex objects rather than simple boxes. + ShapeOutside = "shape-outside" + // Float is the constant for the "float" property tag. + // The "float" property places a View on the left or right side of its container, + // allowing text and inline Views to wrap around it. + Float = "float" +) diff --git a/propertySet.go b/propertySet.go new file mode 100644 index 0000000..240f038 --- /dev/null +++ b/propertySet.go @@ -0,0 +1,764 @@ +package rui + +import ( + "math" + "strconv" + "strings" +) + +var colorProperties = []string{ + ColorProperty, + BackgroundColor, + TextColor, + BorderColor, + BorderLeftColor, + BorderRightColor, + BorderTopColor, + BorderBottomColor, + OutlineColor, + TextLineColor, + ColorPickerValue, +} + +func isPropertyInList(tag string, list []string) bool { + for _, prop := range list { + if prop == tag { + return true + } + } + return false +} + +var angleProperties = []string{ + Rotate, + SkewX, + SkewY, +} + +var boolProperties = []string{ + Disabled, + Inset, + BackfaceVisible, + ReadOnly, + Spellcheck, + CloseButton, + OutsideClose, + Italic, + SmallCaps, + Strikethrough, + Overline, + Underline, + Expanded, + AvoidBreak, + NotTranslate, + Controls, + Loop, + Muted, +} + +var intProperties = []string{ + ZIndex, + HeadHeight, + FootHeight, + RowSpan, + ColumnSpan, +} + +var floatProperties = map[string]struct{ min, max float64 }{ + 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}, + NumberPickerValue: {min: -math.MaxFloat64, max: math.MaxFloat64}, + ProgressBarMax: {min: 0, max: math.MaxFloat64}, + ProgressBarValue: {min: 0, max: math.MaxFloat64}, + VideoWidth: {min: 0, max: 10000}, + VideoHeight: {min: 0, max: 10000}, +} + +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, + TextSize: "font-size", + TextIndent: TextIndent, + LetterSpacing: LetterSpacing, + WordSpacing: WordSpacing, + LineHeight: LineHeight, + TextLineThickness: "text-decoration-thickness", + 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, + 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, +} + +var enumProperties = map[string]struct { + values []string + cssTag string + cssValues []string +}{ + Semantics: { + []string{"default", "article", "section", "aside", "header", "main", "footer", "navigation", "figure", "figure-caption", "button", "p", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "code"}, + "", + []string{"div", "article", "section", "aside", "header", "main", "footer", "nav", "figure", "figcaption", "button", "p", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "code"}, + }, + Visibility: { + []string{"visible", "invisible", "gone"}, + Visibility, + []string{"visible", "invisible", "gone"}, + }, + TextAlign: { + []string{"left", "right", "center", "justify"}, + TextAlign, + []string{"left", "right", "center", "justify"}, + }, + TextTransform: { + []string{"none", "capitalize", "lowercase", "uppercase"}, + TextTransform, + []string{"none", "capitalize", "lowercase", "uppercase"}, + }, + TextWeight: { + []string{"inherit", "thin", "extra-light", "light", "normal", "medium", "semi-bold", "bold", "extra-bold", "black"}, + "font-weight", + []string{"inherit", "100", "200", "300", "normal", "500", "600", "bold", "800", "900"}, + }, + WhiteSpace: { + []string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"}, + WhiteSpace, + []string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"}, + }, + WordBreak: { + []string{"normal", "break-all", "keep-all", "break-word"}, + WordBreak, + []string{"normal", "break-all", "keep-all", "break-word"}, + }, + TextOverflow: { + []string{"clip", "ellipsis"}, + TextOverflow, + []string{"clip", "ellipsis"}, + }, + WritingMode: { + []string{"horizontal-top-to-bottom", "horizontal-bottom-to-top", "vertical-right-to-left", "vertical-left-to-right"}, + WritingMode, + []string{"horizontal-tb", "horizontal-bt", "vertical-rl", "vertical-lr"}, + }, + TextDirection: { + []string{"system", "left-to-right", "right-to-left"}, + "direction", + []string{"", "ltr", "rtl"}, + }, + VerticalTextOrientation: { + []string{"mixed", "upright"}, + "text-orientation", + []string{"mixed", "upright"}, + }, + TextLineStyle: { + []string{"inherit", "solid", "dashed", "dotted", "double", "wavy"}, + "text-decoration-style", + []string{"inherit", "solid", "dashed", "dotted", "double", "wavy"}, + }, + BorderStyle: { + []string{"none", "solid", "dashed", "dotted", "double"}, + BorderStyle, + []string{"none", "solid", "dashed", "dotted", "double"}, + }, + TopStyle: { + []string{"none", "solid", "dashed", "dotted", "double"}, + "", + []string{"none", "solid", "dashed", "dotted", "double"}, + }, + RightStyle: { + []string{"none", "solid", "dashed", "dotted", "double"}, + "", + []string{"none", "solid", "dashed", "dotted", "double"}, + }, + BottomStyle: { + []string{"none", "solid", "dashed", "dotted", "double"}, + "", + []string{"none", "solid", "dashed", "dotted", "double"}, + }, + LeftStyle: { + []string{"none", "solid", "dashed", "dotted", "double"}, + "", + []string{"none", "solid", "dashed", "dotted", "double"}, + }, + OutlineStyle: { + []string{"none", "solid", "dashed", "dotted", "double"}, + OutlineStyle, + []string{"none", "solid", "dashed", "dotted", "double"}, + }, + Tabs: { + []string{"hidden", "top", "bottom", "left", "right", "left-list", "right-list"}, + "", + []string{"hidden", "top", "bottom", "left", "right", "left-list", "right-list"}, + }, + NumberPickerType: { + []string{"editor", "slider"}, + "", + []string{"editor", "slider"}, + }, + EditViewType: { + []string{"text", "password", "email", "emails", "url", "phone", "multiline"}, + "", + []string{"text", "password", "email", "emails", "url", "phone", "multiline"}, + }, + Orientation: { + []string{"up-down", "start-to-end", "bottom-up", "end-to-start"}, + "", + []string{"column", "row", "column-reverse", "row-reverse"}, + }, + Wrap: { + []string{"off", "on", "reverse"}, + "", + []string{"nowrap", "wrap", "wrap-reverse"}, + }, + "list-orientation": { + []string{"vertical", "horizontal"}, + "", + []string{"vertical", "horizontal"}, + }, + VerticalAlign: { + []string{"top", "bottom", "center", "stretch"}, + "", + []string{"top", "bottom", "center", "stretch"}, + }, + HorizontalAlign: { + []string{"left", "right", "center", "stretch"}, + "", + []string{"left", "right", "center", "stretch"}, + }, + ButtonsAlign: { + []string{"left", "right", "center", "stretch"}, + "", + []string{"left", "right", "center", "stretch"}, + }, + CellVerticalAlign: { + []string{"top", "bottom", "center", "stretch"}, + "align-items", + []string{"start", "end", "center", "stretch"}, + }, + CellHorizontalAlign: { + []string{"left", "right", "center", "stretch"}, + "justify-items", + []string{"start", "end", "center", "stretch"}, + }, + ImageVerticalAlign: { + []string{"top", "bottom", "center"}, + "", + []string{"top", "bottom", "center"}, + }, + ImageHorizontalAlign: { + []string{"left", "right", "center"}, + "", + []string{"left", "right", "center"}, + }, + ItemVerticalAlign: { + []string{"top", "bottom", "center", "stretch"}, + "", + []string{"start", "end", "center", "stretch"}, + }, + ItemHorizontalAlign: { + []string{"left", "right", "center", "stretch"}, + "", + []string{"start", "end", "center", "stretch"}, + }, + CheckboxVerticalAlign: { + []string{"top", "bottom", "center"}, + "", + []string{"start", "end", "center"}, + }, + CheckboxHorizontalAlign: { + []string{"left", "right", "center"}, + "", + []string{"start", "end", "center"}, + }, + TableVerticalAlign: { + []string{"top", "bottom", "center", "stretch", "baseline"}, + "vertical-align", + []string{"top", "bottom", "middle", "baseline", "baseline"}, + }, + Anchor: { + []string{"top", "bottom"}, + "", + []string{"top", "bottom"}, + }, + 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{"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: { + []string{"none", "contain", "cover", "fill", "scale-down"}, + "object-fit", + []string{"none", "contain", "cover", "fill", "scale-down"}, + }, + backgroundFit: { + []string{"none", "contain", "cover"}, + "", + []string{"none", "contain", "cover"}, + }, + Repeat: { + []string{"no-repeat", "repeat", "repeat-x", "repeat-y", "round", "space"}, + "", + []string{"no-repeat", "repeat", "repeat-x", "repeat-y", "round", "space"}, + }, + Attachment: { + []string{"scroll", "fixed", "local"}, + "", + []string{"scroll", "fixed", "local"}, + }, + BackgroundClip: { + []string{"border-box", "padding-box", "content-box"}, // "text"}, + "background-clip", + []string{"border-box", "padding-box", "content-box"}, // "text"}, + }, + Direction: { + []string{"to-top", "to-right-top", "to-right", "to-right-bottom", "to-bottom", "to-left-bottom", "to-left", "to-left-top"}, + "", + []string{"to top", "to right top", "to right", "to right bottom", "to bottom", "to left bottom", "to left", "to left top"}, + }, + RadialGradientShape: { + []string{"ellipse", "circle"}, + "", + []string{"ellipse", "circle"}, + }, + RadialGradientRadius: { + []string{"closest-side", "closest-corner", "farthest-side", "farthest-corner"}, + "", + []string{"closest-side", "closest-corner", "farthest-side", "farthest-corner"}, + }, + ItemCheckbox: { + []string{"none", "single", "multiple"}, + "", + []string{"none", "single", "multiple"}, + }, + Float: { + []string{"none", "left", "right"}, + "float", + []string{"none", "left", "right"}, + }, + Preload: { + []string{"none", "metadata", "auto"}, + "", + []string{"none", "metadata", "auto"}, + }, +} + +func notCompatibleType(tag string, value interface{}) { + ErrorLogF(`"%T" type not compatible with "%s" property`, value, tag) +} + +func invalidPropertyValue(tag string, value interface{}) { + ErrorLogF(`Invalid value "%v" of "%s" property`, value, tag) +} + +func isConstantName(text string) bool { + len := len(text) + if len <= 1 || text[0] != '@' { + return false + } + + if len > 2 { + last := len - 1 + if (text[1] == '`' && text[last] == '`') || + (text[1] == '"' && text[last] == '"') || + (text[1] == '\'' && text[last] == '\'') { + return true + } + } + + return !strings.ContainsAny(text, ",;|\"'`+(){}[]<>/\\*&%! \t\n\r") +} + +func isInt(value interface{}) (int, bool) { + var n int + switch value := value.(type) { + case int: + n = value + + case int8: + n = int(value) + + case int16: + n = int(value) + + case int32: + n = int(value) + + case int64: + n = int(value) + + case uint: + n = int(value) + + case uint8: + n = int(value) + + case uint16: + n = int(value) + + case uint32: + n = int(value) + + case uint64: + n = int(value) + + default: + return 0, false + } + + return n, true +} + +func (properties *propertyList) setSimpleProperty(tag string, value interface{}) bool { + if value == nil { + delete(properties.properties, tag) + return true + } else if text, ok := value.(string); ok { + text = strings.Trim(text, " \t\n\r") + if text == "" { + delete(properties.properties, tag) + return true + } + if isConstantName(text) { + properties.properties[tag] = text + return true + } + } + return false +} + +func (properties *propertyList) setSizeProperty(tag string, value interface{}) bool { + if !properties.setSimpleProperty(tag, value) { + var size SizeUnit + switch value := value.(type) { + case string: + var ok bool + if size, ok = StringToSizeUnit(value); !ok { + invalidPropertyValue(tag, value) + return false + } + case SizeUnit: + size = value + + case float32: + size.Type = SizeInPixel + size.Value = float64(value) + + case float64: + size.Type = SizeInPixel + size.Value = value + + default: + if n, ok := isInt(value); ok { + size.Type = SizeInPixel + size.Value = float64(n) + } else { + notCompatibleType(tag, value) + return false + } + } + + if size.Type == Auto { + delete(properties.properties, tag) + } else { + properties.properties[tag] = size + } + } + + return true +} + +func (properties *propertyList) setAngleProperty(tag string, value interface{}) bool { + if !properties.setSimpleProperty(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 + } + case AngleUnit: + angle = value + + case float32: + angle = Rad(float64(value)) + + case float64: + angle = Rad(value) + + default: + if n, ok := isInt(value); ok { + angle = Rad(float64(n)) + } else { + notCompatibleType(tag, value) + return false + } + } + properties.properties[tag] = angle + } + + return true +} + +func (properties *propertyList) setColorProperty(tag string, value interface{}) bool { + if !properties.setSimpleProperty(tag, value) { + var result Color + switch value := value.(type) { + case string: + var ok bool + if result, ok = StringToColor(value); !ok { + invalidPropertyValue(tag, value) + return false + } + case Color: + result = value + + default: + if color, ok := isInt(value); ok { + result = Color(color) + } else { + notCompatibleType(tag, value) + return false + } + } + + if result == 0 { + delete(properties.properties, tag) + } else { + properties.properties[tag] = result + } + } + + return true +} + +func (properties *propertyList) setEnumProperty(tag string, value interface{}, values []string) bool { + if !properties.setSimpleProperty(tag, value) { + var n int + if text, ok := value.(string); ok { + if n, ok = enumStringToInt(text, values, false); !ok { + invalidPropertyValue(tag, value) + return false + } + } else if i, ok := isInt(value); ok { + if i < 0 || i >= len(values) { + invalidPropertyValue(tag, value) + return false + } + n = i + } else { + notCompatibleType(tag, value) + return false + } + + properties.properties[tag] = n + } + + return true +} + +func (properties *propertyList) setBoolProperty(tag string, value interface{}) bool { + if !properties.setSimpleProperty(tag, value) { + if text, ok := value.(string); ok { + switch strings.ToLower(strings.Trim(text, " \t")) { + case "true", "yes", "on", "1": + properties.properties[tag] = true + + case "false", "no", "off", "0": + properties.properties[tag] = false + + default: + invalidPropertyValue(tag, value) + return false + } + } else if n, ok := isInt(value); ok { + switch n { + case 1: + properties.properties[tag] = true + + case 0: + properties.properties[tag] = false + + default: + invalidPropertyValue(tag, value) + return false + } + } else if b, ok := value.(bool); ok { + properties.properties[tag] = b + } else { + notCompatibleType(tag, value) + return false + } + } + + return true +} + +func (properties *propertyList) setIntProperty(tag string, value interface{}) bool { + if !properties.setSimpleProperty(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 + } + properties.properties[tag] = n + } else if n, ok := isInt(value); ok { + properties.properties[tag] = n + } else { + notCompatibleType(tag, value) + return false + } + } + + return true +} + +func (properties *propertyList) setFloatProperty(tag string, value interface{}, min, max float64) bool { + if !properties.setSimpleProperty(tag, value) { + f := float64(0) + switch value := value.(type) { + case string: + var err error + if f, err = strconv.ParseFloat(strings.Trim(value, " \t"), 64); err != nil { + invalidPropertyValue(tag, value) + ErrorLog(err.Error()) + return false + } + + case float32: + f = float64(value) + + case float64: + f = value + + default: + if n, ok := isInt(value); ok { + f = float64(n) + } else { + notCompatibleType(tag, value) + return false + } + } + + if f >= min && f <= max { + properties.properties[tag] = f + } else { + ErrorLogF(`"%T" out of range of "%s" property`, value, tag) + return false + } + } + + return true +} + +func (properties *propertyList) Set(tag string, value interface{}) bool { + return properties.set(strings.ToLower(tag), value) +} + +func (properties *propertyList) set(tag string, value interface{}) bool { + if value == nil { + delete(properties.properties, tag) + return true + } + + if _, ok := sizeProperties[tag]; ok { + return properties.setSizeProperty(tag, value) + } + + if valuesData, ok := enumProperties[tag]; ok { + return properties.setEnumProperty(tag, value, valuesData.values) + } + + if limits, ok := floatProperties[tag]; ok { + return properties.setFloatProperty(tag, value, limits.min, limits.max) + } + + if isPropertyInList(tag, colorProperties) { + return properties.setColorProperty(tag, value) + } + + if isPropertyInList(tag, angleProperties) { + return properties.setAngleProperty(tag, value) + } + + if isPropertyInList(tag, boolProperties) { + return properties.setBoolProperty(tag, value) + } + + if isPropertyInList(tag, intProperties) { + return properties.setIntProperty(tag, value) + } + + if text, ok := value.(string); ok { + properties.properties[tag] = text + return true + } + + notCompatibleType(tag, value) + return false +} diff --git a/propertyValues.go b/propertyValues.go new file mode 100644 index 0000000..7f087fa --- /dev/null +++ b/propertyValues.go @@ -0,0 +1,201 @@ +package rui + +const ( + // Visible - default value of the view Visibility property: View is visible + Visible = 0 + // Invisible - value of the view Visibility property: View is invisible but takes place + Invisible = 1 + // Gone - value of the view Visibility property: View is invisible and does not take place + Gone = 2 + + // NoneTextTransform - not transform text + NoneTextTransform = 0 + // CapitalizeTextTransform - capitalize text + CapitalizeTextTransform = 1 + // LowerCaseTextTransform - transform text to lower case + LowerCaseTextTransform = 2 + // UpperCaseTextTransform - transform text to upper case + UpperCaseTextTransform = 3 + + // HorizontalTopToBottom - content flows horizontally from left to right, vertically from top to bottom. + // The next horizontal line is positioned below the previous line. + HorizontalTopToBottom = 0 + // HorizontalBottomToTop - content flows horizontally from left to right, vertically from bottom to top. + // The next horizontal line is positioned above the previous line. + HorizontalBottomToTop = 1 + // VerticalRightToLeft - content flows vertically from top to bottom, horizontally from right to left. + // The next vertical line is positioned to the left of the previous line. + VerticalRightToLeft = 2 + // VerticalLeftToRight - content flows vertically from top to bottom, horizontally from left to right. + // The next vertical line is positioned to the right of the previous line. + VerticalLeftToRight = 3 + + // MixedTextOrientation - rotates the characters of horizontal scripts 90° clockwise. + // Lays out the characters of vertical scripts naturally. Default value. + MixedTextOrientation = 0 + // UprightTextOrientation - lays out the characters of horizontal scripts naturally (upright), + // as well as the glyphs for vertical scripts. Note that this keyword causes all characters + // to be considered as left-to-right: the used value of "text-direction" is forced to be "left-to-right". + UprightTextOrientation = 1 + + // SystemTextDirection - direction of a text and other elements defined by system. This is the default value. + SystemTextDirection = 0 + // LeftToRightDirection - text and other elements go from left to right. + LeftToRightDirection = 1 + //RightToLeftDirection - text and other elements go from right to left. + RightToLeftDirection = 2 + + // ThinFont - the value of "text-weight" property: the thin (hairline) text weight + ThinFont = 1 + // ExtraLightFont - the value of "text-weight" property: the extra light (ultra light) text weight + ExtraLightFont = 2 + // LightFont - the value of "text-weight" property: the light text weight + LightFont = 3 + // NormalFont - the value of "text-weight" property (default value): the normal text weight + NormalFont = 4 + // MediumFont - the value of "text-weight" property: the medium text weight + MediumFont = 5 + // SemiBoldFont - the value of "text-weight" property: the semi bold (demi bold) text weight + SemiBoldFont = 6 + // BoldFont - the value of "text-weight" property: the bold text weight + BoldFont = 7 + // ExtraBoldFont - the value of "text-weight" property: the extra bold (ultra bold) text weight + ExtraBoldFont = 8 + // BlackFont - the value of "text-weight" property: the black (heavy) text weight + BlackFont = 9 + + // TopAlign - top vertical-align for the "vertical-align" property + TopAlign = 0 + // BottomAlign - bottom vertical-align for the "vertical-align" property + BottomAlign = 1 + // LeftAlign - the left horizontal-align for the "horizontal-align" property + LeftAlign = 0 + // RightAlign - the right horizontal-align for the "horizontal-align" property + RightAlign = 1 + // CenterAlign - the center horizontal/vertical-align for the "horizontal-align"/"vertical-align" property + CenterAlign = 2 + // StretchAlign - the stretch horizontal/vertical-align for the "horizontal-align"/"vertical-align" property + StretchAlign = 3 + // JustifyAlign - the justify text align for "text-align" property + JustifyAlign = 3 + // BaselineAlign - the baseline cell-vertical-align for the "cell-vertical-align" property + BaselineAlign = 4 + + // WhiteSpaceNormal - sequences of white space are collapsed. Newline characters in the source + // are handled the same as other white space. Lines are broken as necessary to fill line boxes. + WhiteSpaceNormal = 0 + // WhiteSpaceNowrap - collapses white space as for normal, but suppresses line breaks (text wrapping) + // within the source. + WhiteSpaceNowrap = 1 + // WhiteSpacePre - sequences of white space are preserved. Lines are only broken at newline + // characters in the source and at
elements. + WhiteSpacePre = 2 + // WhiteSpacePreWrap - Sequences of white space are preserved. Lines are broken at newline + // characters, at
, and as necessary to fill line boxes. + WhiteSpacePreWrap = 3 + // WhiteSpacePreLine - sequences of white space are collapsed. Lines are broken at newline characters, + // at
, and as necessary to fill line boxes. + 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 box’s intrinsic sizes + // (min-content size and max-content size). + WhiteSpaceBreakSpaces = 5 + + // WordBreakNormal - use the default line break rule. + WordBreakNormal = 0 + // WordBreakAll - to prevent overflow, word breaks should be inserted between any two characters + // (excluding Chinese/Japanese/Korean text). + WordBreakAll = 1 + // WordBreakKeepAll - word breaks should not be used for Chinese/Japanese/Korean (CJK) text. + // Non-CJK text behavior is the same as for normal. + WordBreakKeepAll = 2 + // WordBreakWord - when the block boundaries are exceeded, the remaining whole words can be split + // in an arbitrary place, unless a more suitable place for the line break is found. + WordBreakWord = 3 + + // TextOverflowClip - truncate the text at the limit of the content area, therefore the truncation + // can happen in the middle of a character. + TextOverflowClip = 0 + // TextOverflowEllipsis - display an ellipsis ('…', U+2026 HORIZONTAL ELLIPSIS) to represent clipped text. + // The ellipsis is displayed inside the content area, decreasing the amount of text displayed. + // If there is not enough space to display the ellipsis, it is clipped. + TextOverflowEllipsis = 1 + + // DefaultSemantics - default value of the view Semantic property + DefaultSemantics = 0 + // ArticleSemantics - value of the view Semantic property: view represents a self-contained + // composition in a document, page, application, or site, which is intended to be + // independently distributable or reusable (e.g., in syndication) + ArticleSemantics = 1 + // SectionSemantics - value of the view Semantic property: view represents + // a generic standalone section of a document, which doesn't have a more specific + // semantic element to represent it. + SectionSemantics = 2 + // AsideSemantics - value of the view Semantic property: view represents a portion + // of a document whose content is only indirectly related to the document's main content. + // Asides are frequently presented as sidebars or call-out boxes. + AsideSemantics = 3 + // HeaderSemantics - value of the view Semantic property: view represents introductory + // content, typically a group of introductory or navigational aids. It may contain + // some heading elements but also a logo, a search form, an author name, and other elements. + HeaderSemantics = 4 + // MainSemantics - value of the view Semantic property: view represents the dominant content + // of the application. The main content area consists of content that is directly related + // to or expands upon the central topic of a document, or the central functionality of an application. + MainSemantics = 5 + // FooterSemantics - value of the view Semantic property: view represents a footer for its + // nearest sectioning content or sectioning root element. A footer view typically contains + // information about the author of the section, copyright data or links to related documents. + FooterSemantics = 6 + // NavigationSemantics - value of the view Semantic property: view represents a section of + // a page whose purpose is to provide navigation links, either within the current document + // or to other documents. Common examples of navigation sections are menus, tables of contents, + // and indexes. + NavigationSemantics = 7 + // FigureSemantics - value of the view Semantic property: view represents self-contained content, + // potentially with an optional caption, which is specified using the FigureCaptionSemantics view. + FigureSemantics = 8 + // FigureCaptionSemantics - value of the view Semantic property: view represents a caption or + // legend describing the rest of the contents of its parent FigureSemantics view. + FigureCaptionSemantics = 9 + // ButtonSemantics - value of the view Semantic property: view a clickable button + ButtonSemantics = 10 + // ParagraphSemantics - value of the view Semantic property: view represents a paragraph. + // Paragraphs are usually represented in visual media as blocks of text separated + // from adjacent blocks by blank lines and/or first-line indentation + ParagraphSemantics = 11 + // H1Semantics - value of the view Semantic property: view represent of first level section headings. + // H1Semantics is the highest section level and H6Semantics is the lowest. + H1Semantics = 12 + // H2Semantics - value of the view Semantic property: view represent of second level section headings. + // H1Semantics is the highest section level and H6Semantics is the lowest. + H2Semantics = 13 + // H3Semantics - value of the view Semantic property: view represent of third level section headings. + // H1Semantics is the highest section level and H6Semantics is the lowest. + H3Semantics = 14 + // H4Semantics - value of the view Semantic property: view represent of fourth level section headings. + // H1Semantics is the highest section level and H6Semantics is the lowest. + H4Semantics = 15 + // H5Semantics - value of the view Semantic property: view represent of fifth level section headings. + // H1Semantics is the highest section level and H6Semantics is the lowest. + H5Semantics = 16 + // H6Semantics - value of the view Semantic property: view represent of sixth level section headings. + // H1Semantics is the highest section level and H6Semantics is the lowest. + H6Semantics = 17 + // BlockquoteSemantics - value of the view Semantic property: view indicates that + // the enclosed text is an extended quotation. + BlockquoteSemantics = 18 + // CodeSemantics - value of the view Semantic property: view displays its contents styled + // in a fashion intended to indicate that the text is a short fragment of computer code + CodeSemantics = 19 + + // NoneFloat - value of the view "float" property: the View must not float. + NoneFloat = 0 + // LeftFloat - value of the view "float" property: the View must float on the left side of its containing block. + LeftFloat = 1 + // RightFloat - value of the view "float" property: the View must float on the right side of its containing block. + RightFloat = 2 +) diff --git a/radius.go b/radius.go new file mode 100644 index 0000000..bf3862c --- /dev/null +++ b/radius.go @@ -0,0 +1,770 @@ +package rui + +import ( + "fmt" + "strings" +) + +const ( + // Radius is the SizeUnit view property that determines the corners rounding radius + // of an element's outer border edge. + Radius = "radius" + // RadiusX is the SizeUnit view property that determines the x-axis corners elliptic rounding + // radius of an element's outer border edge. + RadiusX = "radius-x" + // RadiusY is the SizeUnit view property that determines the y-axis corners elliptic rounding + // radius of an element's outer border edge. + RadiusY = "radius-y" + // RadiusTopLeft is the SizeUnit view property that determines the top-left corner rounding radius + // of an element's outer border edge. + RadiusTopLeft = "radius-top-left" + // RadiusTopLeftX is the SizeUnit view property that determines the x-axis top-left corner elliptic + // rounding radius of an element's outer border edge. + RadiusTopLeftX = "radius-top-left-x" + // RadiusTopLeftY is the SizeUnit view property that determines the y-axis top-left corner elliptic + // rounding radius of an element's outer border edge. + RadiusTopLeftY = "radius-top-left-y" + // RadiusTopRight is the SizeUnit view property that determines the top-right corner rounding radius + // of an element's outer border edge. + RadiusTopRight = "radius-top-right" + // RadiusTopRightX is the SizeUnit view property that determines the x-axis top-right corner elliptic + // rounding radius of an element's outer border edge. + RadiusTopRightX = "radius-top-right-x" + // RadiusTopRightY is the SizeUnit view property that determines the y-axis top-right corner elliptic + // rounding radius of an element's outer border edge. + RadiusTopRightY = "radius-top-right-y" + // RadiusBottomLeft is the SizeUnit view property that determines the bottom-left corner rounding radius + // of an element's outer border edge. + RadiusBottomLeft = "radius-bottom-left" + // RadiusBottomLeftX is the SizeUnit view property that determines the x-axis bottom-left corner elliptic + // rounding radius of an element's outer border edge. + RadiusBottomLeftX = "radius-bottom-left-x" + // RadiusBottomLeftY is the SizeUnit view property that determines the y-axis bottom-left corner elliptic + // rounding radius of an element's outer border edge. + RadiusBottomLeftY = "radius-bottom-left-y" + // RadiusBottomRight is the SizeUnit view property that determines the bottom-right corner rounding radius + // of an element's outer border edge. + RadiusBottomRight = "radius-bottom-right" + // RadiusBottomRightX is the SizeUnit view property that determines the x-axis bottom-right corner elliptic + // rounding radius of an element's outer border edge. + RadiusBottomRightX = "radius-bottom-right-x" + // RadiusBottomRightY is the SizeUnit view property that determines the y-axis bottom-right corner elliptic + // rounding radius of an element's outer border edge. + RadiusBottomRightY = "radius-bottom-right-y" + // X is the SizeUnit property of the ShadowProperty that determines the x-axis corners elliptic rounding + // radius of an element's outer border edge. + X = "x" + // Y is the SizeUnit property of the ShadowProperty that determines the y-axis corners elliptic rounding + // radius of an element's outer border edge. + Y = "y" + // TopLeft is the SizeUnit property of the ShadowProperty that determines the top-left corner rounding radius + // of an element's outer border edge. + TopLeft = "top-left" + // TopLeftX is the SizeUnit property of the ShadowProperty that determines the x-axis top-left corner elliptic + // rounding radius of an element's outer border edge. + TopLeftX = "top-left-x" + // TopLeftY is the SizeUnit property of the ShadowProperty that determines the y-axis top-left corner elliptic + // rounding radius of an element's outer border edge. + TopLeftY = "top-left-y" + // TopRight is the SizeUnit property of the ShadowProperty that determines the top-right corner rounding radius + // of an element's outer border edge. + TopRight = "top-right" + // TopRightX is the SizeUnit property of the ShadowProperty that determines the x-axis top-right corner elliptic + // rounding radius of an element's outer border edge. + TopRightX = "top-right-x" + // TopRightY is the SizeUnit property of the ShadowProperty that determines the y-axis top-right corner elliptic + // rounding radius of an element's outer border edge. + TopRightY = "top-right-y" + // BottomLeft is the SizeUnit property of the ShadowProperty that determines the bottom-left corner rounding radius + // of an element's outer border edge. + BottomLeft = "bottom-left" + // BottomLeftX is the SizeUnit property of the ShadowProperty that determines the x-axis bottom-left corner elliptic + // rounding radius of an element's outer border edge. + BottomLeftX = "bottom-left-x" + // BottomLeftY is the SizeUnit property of the ShadowProperty that determines the y-axis bottom-left corner elliptic + // rounding radius of an element's outer border edge. + BottomLeftY = "bottom-left-y" + // BottomRight is the SizeUnit property of the ShadowProperty that determines the bottom-right corner rounding radius + // of an element's outer border edge. + BottomRight = "bottom-right" + // BottomRightX is the SizeUnit property of the ShadowProperty that determines the x-axis bottom-right corner elliptic + // rounding radius of an element's outer border edge. + BottomRightX = "bottom-right-x" + // BottomRightY is the SizeUnit property of the ShadowProperty that determines the y-axis bottom-right corner elliptic + // rounding radius of an element's outer border edge. + BottomRightY = "bottom-right-y" +) + +type RadiusProperty interface { + Properties + ruiStringer + fmt.Stringer + BoxRadius(session Session) BoxRadius +} + +type radiusPropertyData struct { + propertyList +} + +// NewRadiusProperty creates the new RadiusProperty +func NewRadiusProperty(params Params) RadiusProperty { + result := new(radiusPropertyData) + result.properties = map[string]interface{}{} + if params != nil { + for _, tag := range []string{X, Y, TopLeft, TopRight, BottomLeft, BottomRight, TopLeftX, TopLeftY, + TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY} { + if value, ok := params[tag]; ok { + result.Set(tag, value) + } + } + } + return result +} + +func (radius *radiusPropertyData) normalizeTag(tag string) string { + return strings.TrimPrefix(strings.ToLower(tag), "radius-") +} + +func (radius *radiusPropertyData) ruiString(writer ruiWriter) { + writer.startObject("_") + + for _, tag := range []string{X, Y, TopLeft, TopLeftX, TopLeftY, TopRight, TopRightX, TopRightY, + BottomLeft, BottomLeftX, BottomLeftY, BottomRight, BottomRightX, BottomRightY} { + if value, ok := radius.properties[tag]; ok { + writer.writeProperty(Style, value) + } + } + + writer.endObject() +} + +func (radius *radiusPropertyData) String() string { + writer := newRUIWriter() + radius.ruiString(writer) + return writer.finish() +} + +func (radius *radiusPropertyData) delete(tags []string) { + for _, tag := range tags { + delete(radius.properties, tag) + } +} + +func (radius *radiusPropertyData) deleteUnusedTags() { + for _, tag := range []string{X, Y} { + if _, ok := radius.properties[tag]; ok { + unused := true + for _, t := range []string{TopLeft, TopRight, BottomLeft, BottomRight} { + if _, ok := radius.properties[t+"-"+tag]; !ok { + if _, ok := radius.properties[t]; !ok { + unused = false + break + } + } + } + if unused { + delete(radius.properties, tag) + } + } + } + + equalValue := func(value1, value2 interface{}) bool { + switch value1 := value1.(type) { + case string: + switch value2 := value2.(type) { + case string: + return value1 == value2 + } + + case SizeUnit: + switch value2 := value2.(type) { + case SizeUnit: + return value1.Equal(value2) + } + } + return false + } + + for _, tag := range []string{TopLeft, TopRight, BottomLeft, BottomRight} { + tagX := tag + "-x" + tagY := tag + "-y" + valueX, okX := radius.properties[tagX] + valueY, okY := radius.properties[tagY] + + if value, ok := radius.properties[tag]; ok { + if okX && okY { + delete(radius.properties, tag) + } else if okX && !okY { + if equalValue(value, valueX) { + delete(radius.properties, tagX) + } else { + radius.properties[tagY] = value + delete(radius.properties, tag) + } + } else if !okX && okY { + if equalValue(value, valueY) { + delete(radius.properties, tagY) + } else { + radius.properties[tagX] = value + delete(radius.properties, tag) + } + } + } else if okX && okY && equalValue(valueX, valueY) { + radius.properties[tag] = valueX + delete(radius.properties, tagX) + delete(radius.properties, tagY) + } + } +} + +func (radius *radiusPropertyData) Remove(tag string) { + tag = radius.normalizeTag(tag) + + switch tag { + case X, Y: + if _, ok := radius.properties[tag]; ok { + radius.Set(tag, AutoSize()) + delete(radius.properties, tag) + } + + case TopLeftX, TopLeftY, TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY: + delete(radius.properties, tag) + + case TopLeft, TopRight, BottomLeft, BottomRight: + radius.delete([]string{tag, tag + "-x", tag + "-y"}) + + default: + ErrorLogF(`"%s" property is not compatible with the RadiusProperty`, tag) + } + +} + +func (radius *radiusPropertyData) Set(tag string, value interface{}) bool { + if value == nil { + radius.Remove(tag) + return true + } + + tag = radius.normalizeTag(tag) + switch tag { + case X: + if radius.setSizeProperty(tag, value) { + radius.delete([]string{TopLeftX, TopRightX, BottomLeftX, BottomRightX}) + for _, t := range []string{TopLeft, TopRight, BottomLeft, BottomRight} { + if val, ok := radius.properties[t]; ok { + if _, ok := radius.properties[t+"-y"]; !ok { + radius.properties[t+"-y"] = val + } + delete(radius.properties, t) + } + } + return true + } + + case Y: + if radius.setSizeProperty(tag, value) { + radius.delete([]string{TopLeftY, TopRightY, BottomLeftY, BottomRightY}) + for _, t := range []string{TopLeft, TopRight, BottomLeft, BottomRight} { + if val, ok := radius.properties[t]; ok { + if _, ok := radius.properties[t+"-x"]; !ok { + radius.properties[t+"-x"] = val + } + delete(radius.properties, t) + } + } + return true + } + + case TopLeftX, TopLeftY, TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY: + if radius.setSizeProperty(tag, value) { + radius.deleteUnusedTags() + return true + } + + case TopLeft, TopRight, BottomLeft, BottomRight: + switch value := value.(type) { + case SizeUnit: + radius.properties[tag] = value + radius.delete([]string{tag + "-x", tag + "-y"}) + radius.deleteUnusedTags() + return true + + case string: + if strings.Contains(value, "/") { + if values := strings.Split(value, "/"); len(values) == 2 { + xOK := radius.Set(tag+"-x", value[0]) + yOK := radius.Set(tag+"-y", value[1]) + return xOK && yOK + } else { + notCompatibleType(tag, value) + } + } else { + if radius.setSizeProperty(tag, value) { + radius.delete([]string{tag + "-x", tag + "-y"}) + radius.deleteUnusedTags() + return true + } + } + } + + default: + ErrorLogF(`"%s" property is not compatible with the RadiusProperty`, tag) + } + + return false +} + +func (radius *radiusPropertyData) Get(tag string) interface{} { + tag = radius.normalizeTag(tag) + if value, ok := radius.properties[tag]; ok { + return value + } + + switch tag { + case TopLeftX, TopLeftY, TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY: + tagLen := len(tag) + if value, ok := radius.properties[tag[:tagLen-2]]; ok { + return value + } + if value, ok := radius.properties[tag[tagLen-1:]]; ok { + return value + } + } + + return nil +} + +func (radius *radiusPropertyData) BoxRadius(session Session) BoxRadius { + x, _ := sizeProperty(radius, X, session) + y, _ := sizeProperty(radius, Y, session) + + getRadius := func(tag string) (SizeUnit, SizeUnit) { + rx := x + ry := y + if r, ok := sizeProperty(radius, tag, session); ok { + rx = r + ry = r + } + if r, ok := sizeProperty(radius, tag+"-x", session); ok { + rx = r + } + if r, ok := sizeProperty(radius, tag+"-y", session); ok { + ry = r + } + + return rx, ry + } + + var result BoxRadius + + result.TopLeftX, result.TopLeftY = getRadius(TopLeft) + result.TopRightX, result.TopRightY = getRadius(TopRight) + result.BottomLeftX, result.BottomLeftY = getRadius(BottomLeft) + result.BottomRightX, result.BottomRightY = getRadius(BottomRight) + + return result +} + +// BoxRadius defines radii of rounds the corners of an element's outer border edge +type BoxRadius struct { + TopLeftX, TopLeftY, TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY SizeUnit +} + +// AllAnglesIsEqual returns 'true' if all angles is equal, 'false' otherwise +func (radius BoxRadius) AllAnglesIsEqual() bool { + return radius.TopLeftX.Equal(radius.TopRightX) && + radius.TopLeftY.Equal(radius.TopRightY) && + radius.TopLeftX.Equal(radius.BottomLeftX) && + radius.TopLeftY.Equal(radius.BottomLeftY) && + radius.TopLeftX.Equal(radius.BottomRightX) && + radius.TopLeftY.Equal(radius.BottomRightY) +} + +// String returns a string representation of a BoxRadius struct +func (radius BoxRadius) String() string { + + if radius.AllAnglesIsEqual() { + if radius.TopLeftX.Equal(radius.TopLeftY) { + return radius.TopLeftX.String() + } else { + return fmt.Sprintf("_{ x = %s, y = %s }", radius.TopLeftX.String(), radius.TopLeftY.String()) + } + } + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + buffer.WriteString("_{ ") + + if radius.TopLeftX.Equal(radius.TopLeftY) { + buffer.WriteString("top-left = ") + buffer.WriteString(radius.TopLeftX.String()) + } else { + buffer.WriteString("top-left-x = ") + buffer.WriteString(radius.TopLeftX.String()) + buffer.WriteString("top-left-y = ") + buffer.WriteString(radius.TopLeftY.String()) + } + + if radius.TopRightX.Equal(radius.TopRightY) { + buffer.WriteString(", top-right = ") + buffer.WriteString(radius.TopRightX.String()) + } else { + buffer.WriteString(", top-right-x = ") + buffer.WriteString(radius.TopRightX.String()) + buffer.WriteString(", top-right-y = ") + buffer.WriteString(radius.TopRightY.String()) + } + + if radius.BottomLeftX.Equal(radius.BottomLeftY) { + buffer.WriteString(", bottom-left = ") + buffer.WriteString(radius.BottomLeftX.String()) + } else { + buffer.WriteString(", bottom-left-x = ") + buffer.WriteString(radius.BottomLeftX.String()) + buffer.WriteString(", bottom-left-y = ") + buffer.WriteString(radius.BottomLeftY.String()) + } + + if radius.BottomRightX.Equal(radius.BottomRightY) { + buffer.WriteString(", bottom-right = ") + buffer.WriteString(radius.BottomRightX.String()) + } else { + buffer.WriteString(", bottom-right-x = ") + buffer.WriteString(radius.BottomRightX.String()) + buffer.WriteString(", bottom-right-y = ") + buffer.WriteString(radius.BottomRightY.String()) + } + + buffer.WriteString(" }") + return buffer.String() +} + +func (radius BoxRadius) cssValue(builder cssBuilder) { + + if (radius.TopLeftX.Type == Auto || radius.TopLeftX.Value == 0) && + (radius.TopLeftY.Type == Auto || radius.TopLeftY.Value == 0) && + (radius.TopRightX.Type == Auto || radius.TopRightX.Value == 0) && + (radius.TopRightY.Type == Auto || radius.TopRightY.Value == 0) && + (radius.BottomRightX.Type == Auto || radius.BottomRightX.Value == 0) && + (radius.BottomRightY.Type == Auto || radius.BottomRightY.Value == 0) && + (radius.BottomLeftX.Type == Auto || radius.BottomLeftX.Value == 0) && + (radius.BottomLeftY.Type == Auto || radius.BottomLeftY.Value == 0) { + return + } + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + buffer.WriteString(radius.TopLeftX.cssString("0")) + + if radius.AllAnglesIsEqual() { + + if !radius.TopLeftX.Equal(radius.TopLeftY) { + buffer.WriteString(" / ") + buffer.WriteString(radius.TopLeftY.cssString("0")) + } + + } else { + + buffer.WriteRune(' ') + buffer.WriteString(radius.TopRightX.cssString("0")) + buffer.WriteRune(' ') + buffer.WriteString(radius.BottomRightX.cssString("0")) + buffer.WriteRune(' ') + buffer.WriteString(radius.BottomLeftX.cssString("0")) + + if !radius.TopLeftX.Equal(radius.TopLeftY) || + !radius.TopRightX.Equal(radius.TopRightY) || + !radius.BottomLeftX.Equal(radius.BottomLeftY) || + !radius.BottomRightX.Equal(radius.BottomRightY) { + + buffer.WriteString(" / ") + buffer.WriteString(radius.TopLeftY.cssString("0")) + buffer.WriteRune(' ') + buffer.WriteString(radius.TopRightY.cssString("0")) + buffer.WriteRune(' ') + buffer.WriteString(radius.BottomRightY.cssString("0")) + buffer.WriteRune(' ') + buffer.WriteString(radius.BottomLeftY.cssString("0")) + } + } + + builder.add("border-radius", buffer.String()) +} + +func (radius BoxRadius) cssString() string { + var builder cssValueBuilder + radius.cssValue(&builder) + return builder.finish() +} + +func getRadiusProperty(style Properties) RadiusProperty { + if value := style.Get(Radius); value != nil { + switch value := value.(type) { + case RadiusProperty: + return value + + case BoxRadius: + result := NewRadiusProperty(nil) + if value.AllAnglesIsEqual() { + result.Set(X, value.TopLeftX) + result.Set(Y, value.TopLeftY) + } else { + if value.TopLeftX.Equal(value.TopLeftY) { + result.Set(TopLeft, value.TopLeftX) + } else { + result.Set(TopLeftX, value.TopLeftX) + result.Set(TopLeftY, value.TopLeftY) + } + if value.TopRightX.Equal(value.TopRightY) { + result.Set(TopRight, value.TopRightX) + } else { + result.Set(TopRightX, value.TopRightX) + result.Set(TopRightY, value.TopRightY) + } + if value.BottomLeftX.Equal(value.BottomLeftY) { + result.Set(BottomLeft, value.BottomLeftX) + } else { + result.Set(BottomLeftX, value.BottomLeftX) + result.Set(BottomLeftY, value.BottomLeftY) + } + if value.BottomRightX.Equal(value.BottomRightY) { + result.Set(BottomRight, value.BottomRightX) + } else { + result.Set(BottomRightX, value.BottomRightX) + result.Set(BottomRightY, value.BottomRightY) + } + } + return result + + case SizeUnit: + return NewRadiusProperty(Params{ + X: value, + Y: value, + }) + + case string: + return NewRadiusProperty(Params{ + X: value, + Y: value, + }) + } + } + + return NewRadiusProperty(nil) +} + +func (properties *propertyList) setRadius(value interface{}) bool { + + if value == nil { + delete(properties.properties, Radius) + return true + } + + switch value := value.(type) { + case RadiusProperty: + properties.properties[Radius] = value + return true + + case SizeUnit: + properties.properties[Radius] = value + return true + + case BoxRadius: + radius := NewRadiusProperty(nil) + if value.AllAnglesIsEqual() { + radius.Set(X, value.TopLeftX) + radius.Set(Y, value.TopLeftY) + } else { + if value.TopLeftX.Equal(value.TopLeftY) { + radius.Set(TopLeft, value.TopLeftX) + } else { + radius.Set(TopLeftX, value.TopLeftX) + radius.Set(TopLeftY, value.TopLeftY) + } + if value.TopRightX.Equal(value.TopRightY) { + radius.Set(TopRight, value.TopRightX) + } else { + radius.Set(TopRightX, value.TopRightX) + radius.Set(TopRightY, value.TopRightY) + } + if value.BottomLeftX.Equal(value.BottomLeftY) { + radius.Set(BottomLeft, value.BottomLeftX) + } else { + radius.Set(BottomLeftX, value.BottomLeftX) + radius.Set(BottomLeftY, value.BottomLeftY) + } + if value.BottomRightX.Equal(value.BottomRightY) { + radius.Set(BottomRight, value.BottomRightX) + } else { + radius.Set(BottomRightX, value.BottomRightX) + radius.Set(BottomRightY, value.BottomRightY) + } + } + properties.properties[Radius] = radius + return true + + case string: + if strings.Contains(value, "/") { + values := strings.Split(value, "/") + if len(values) == 2 { + okX := properties.setRadiusElement(RadiusX, values[0]) + okY := properties.setRadiusElement(RadiusY, values[1]) + return okX && okY + } else { + notCompatibleType(Radius, value) + } + } else { + return properties.setSizeProperty(Radius, value) + } + + case DataObject: + radius := NewRadiusProperty(nil) + for _, tag := range []string{X, Y, TopLeft, TopRight, BottomLeft, BottomRight, TopLeftX, TopLeftY, + TopRightX, TopRightY, BottomLeftX, BottomLeftY, BottomRightX, BottomRightY} { + if value, ok := value.PropertyValue(tag); ok { + radius.Set(tag, value) + } + } + properties.properties[Radius] = radius + return true + + default: + notCompatibleType(Radius, value) + } + + return false +} + +func (properties *propertyList) removeRadiusElement(tag string) { + if value, ok := properties.properties[Radius]; ok && value != nil { + radius := getRadiusProperty(properties) + radius.Remove(tag) + if len(radius.AllTags()) == 0 { + delete(properties.properties, Radius) + } else { + properties.properties[Radius] = radius + } + } +} + +func (properties *propertyList) setRadiusElement(tag string, value interface{}) bool { + if value == nil { + properties.removeRadiusElement(tag) + return true + } + + radius := getRadiusProperty(properties) + if radius.Set(tag, value) { + properties.properties[Radius] = radius + return true + } + + return false +} + +func getRadiusElement(style Properties, tag string) interface{} { + value := style.Get(Radius) + if value != nil { + switch value := value.(type) { + case string: + return value + + case SizeUnit: + return value + + case RadiusProperty: + return value.Get(tag) + + case BoxRadius: + switch tag { + case RadiusX: + if value.TopLeftX.Equal(value.TopRightX) && + value.TopLeftX.Equal(value.BottomLeftX) && + value.TopLeftX.Equal(value.BottomRightX) { + return value.TopLeftX + } + + case RadiusY: + if value.TopLeftY.Equal(value.TopRightY) && + value.TopLeftY.Equal(value.BottomLeftY) && + value.TopLeftY.Equal(value.BottomRightY) { + return value.TopLeftY + } + + case RadiusTopLeft: + if value.TopLeftX.Equal(value.TopLeftY) { + return value.TopLeftY + } + + case RadiusTopRight: + if value.TopRightX.Equal(value.TopRightY) { + return value.TopRightY + } + + case RadiusBottomLeft: + if value.BottomLeftX.Equal(value.BottomLeftY) { + return value.BottomLeftY + } + + case RadiusBottomRight: + if value.BottomRightX.Equal(value.BottomRightY) { + return value.BottomRightY + } + + case RadiusTopLeftX: + return value.TopLeftX + + case RadiusTopLeftY: + return value.TopLeftY + + case RadiusTopRightX: + return value.TopRightX + + case RadiusTopRightY: + return value.TopRightY + + case RadiusBottomLeftX: + return value.BottomLeftX + + case RadiusBottomLeftY: + return value.BottomLeftY + + case RadiusBottomRightX: + return value.BottomRightX + + case RadiusBottomRightY: + return value.BottomRightY + } + } + } + + return nil +} + +func getRadius(properties Properties, session Session) BoxRadius { + if value := properties.Get(Radius); value != nil { + switch value := value.(type) { + case BoxRadius: + return value + + case RadiusProperty: + return value.BoxRadius(session) + + case SizeUnit: + return BoxRadius{TopLeftX: value, TopLeftY: value, TopRightX: value, TopRightY: value, + BottomLeftX: value, BottomLeftY: value, BottomRightX: value, BottomRightY: value} + + case string: + if text, ok := session.resolveConstants(value); ok { + if size, ok := StringToSizeUnit(text); ok { + return BoxRadius{TopLeftX: size, TopLeftY: size, TopRightX: size, TopRightY: size, + BottomLeftX: size, BottomLeftY: size, BottomRightX: size, BottomRightY: size} + } + } + } + } + + return BoxRadius{} +} diff --git a/resizable.go b/resizable.go new file mode 100644 index 0000000..52c28f9 --- /dev/null +++ b/resizable.go @@ -0,0 +1,449 @@ +package rui + +import ( + "fmt" + "strconv" + "strings" +) + +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" + // 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" + // CellVerticalAlign is the constant for the "cell-vertical-align" property tag. + CellVerticalAlign = "cell-vertical-align" + // CellHorizontalAlign is the constant for the "cell-horizontal-align" property tag. + CellHorizontalAlign = "cell-horizontal-align" + + // TopSide is value of the "side" property: the top side is used to resize + TopSide = 1 + // RightSide is value of the "side" property: the right side is used to resize + RightSide = 2 + // BottomSide is value of the "side" property: the bottom side is used to resize + BottomSide = 4 + // LeftSide is value of the "side" property: the left side is used to resize + LeftSide = 8 + // AllSides is value of the "side" property: all sides is used to resize + AllSides = TopSide | RightSide | BottomSide | LeftSide +) + +// Resizable - grid-container of View +type Resizable interface { + View + ParanetView +} + +type resizableData struct { + viewData + content []View +} + +// NewResizable create new Resizable object and return it +func NewResizable(session Session, params Params) Resizable { + view := new(resizableData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newResizable(session Session) View { + return NewResizable(session, nil) +} + +func (resizable *resizableData) Init(session Session) { + resizable.viewData.Init(session) + resizable.tag = "Resizable" + resizable.systemClass = "ruiGridLayout" + resizable.content = []View{} +} + +func (resizable *resizableData) Views() []View { + return resizable.content +} + +func (resizable *resizableData) Remove(tag string) { + resizable.remove(strings.ToLower(tag)) +} + +func (resizable *resizableData) remove(tag string) { + switch tag { + case Side: + oldSide := resizable.getSide() + delete(resizable.properties, Side) + if oldSide != resizable.getSide() { + updateInnerHTML(resizable.htmlID(), resizable.Session()) + resizable.updateResizeBorderWidth() + } + + case ResizeBorderWidth: + w := resizable.resizeBorderWidth() + delete(resizable.properties, ResizeBorderWidth) + if !w.Equal(resizable.resizeBorderWidth()) { + resizable.updateResizeBorderWidth() + } + + case Content: + if len(resizable.content) > 0 { + resizable.content = []View{} + updateInnerHTML(resizable.htmlID(), resizable.Session()) + } + + default: + resizable.viewData.remove(tag) + } +} + +func (resizable *resizableData) Set(tag string, value interface{}) bool { + return resizable.set(strings.ToLower(tag), value) +} + +func (resizable *resizableData) set(tag string, value interface{}) bool { + if value == nil { + resizable.remove(tag) + return true + } + + switch tag { + case Side: + oldSide := resizable.getSide() + ok := resizable.setSide(value) + if ok && oldSide != resizable.getSide() { + updateInnerHTML(resizable.htmlID(), resizable.Session()) + resizable.updateResizeBorderWidth() + } else { + notCompatibleType(tag, value) + } + return ok + + case ResizeBorderWidth: + w := resizable.resizeBorderWidth() + ok := resizable.setSizeProperty(tag, value) + if ok && !w.Equal(resizable.resizeBorderWidth()) { + resizable.updateResizeBorderWidth() + } + return ok + + case Content: + var newContent View = nil + switch value := value.(type) { + case string: + newContent = NewTextView(resizable.Session(), Params{Text: value}) + + case View: + newContent = value + + case DataObject: + if view := CreateViewFromObject(resizable.Session(), value); view != nil { + newContent = view + } + } + + if newContent != nil { + if len(resizable.content) == 0 { + resizable.content = []View{newContent} + } else { + resizable.content[0] = newContent + } + updateInnerHTML(resizable.htmlID(), resizable.Session()) + return true + } + + case CellWidth, CellHeight, GridRowGap, GridColumnGap, CellVerticalAlign, CellHorizontalAlign: + ErrorLogF(`Not supported "%s" property`, tag) + return false + } + + return resizable.viewData.set(tag, value) +} + +func (resizable *resizableData) Get(tag string) interface{} { + return resizable.get(strings.ToLower(tag)) +} + +func (resizable *resizableData) getSide() int { + if value := resizable.getRaw(Side); value != nil { + switch value := value.(type) { + case string: + if value, ok := resizable.session.resolveConstants(value); ok { + validValues := map[string]int{ + "top": TopSide, + "right": RightSide, + "bottom": BottomSide, + "left": LeftSide, + "all": AllSides, + } + + if strings.Contains(value, "|") { + values := strings.Split(value, "|") + sides := 0 + for _, val := range values { + if n, err := strconv.Atoi(val); err == nil { + if n < 1 || n > AllSides { + return AllSides + } + sides |= n + } else if n, ok := validValues[val]; ok { + sides |= n + } else { + return AllSides + } + } + return sides + + } else if n, err := strconv.Atoi(value); err == nil { + if n >= 1 || n <= AllSides { + return n + } + } else if n, ok := validValues[value]; ok { + return n + } + } + + case int: + if value >= 1 && value <= AllSides { + return value + } + } + } + return AllSides +} + +func (resizable *resizableData) setSide(value interface{}) bool { + 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 + } + return false + } + validValues := map[string]int{ + "top": TopSide, + "right": RightSide, + "bottom": BottomSide, + "left": LeftSide, + "all": AllSides, + } + if strings.Contains(value, "|") { + values := strings.Split(value, "|") + sides := 0 + hasConst := false + for i, val := range values { + val := strings.Trim(val, " \t\r\n") + values[i] = val + + if val[0] == '@' { + hasConst = true + } else if n, err := strconv.Atoi(val); err == nil { + if n < 1 || n > AllSides { + return false + } + sides |= n + } else if n, ok := validValues[val]; ok { + sides |= n + } else { + return false + } + } + + if hasConst { + value = values[0] + for i := 1; i < len(values); i++ { + value += "|" + values[i] + } + resizable.properties[Side] = value + return true + } + + if sides >= 1 && sides <= AllSides { + resizable.properties[Side] = sides + return true + } + + } else if value[0] == '@' { + resizable.properties[Side] = value + return true + } else if n, ok := validValues[value]; ok { + resizable.properties[Side] = n + return true + } + + case int: + if value >= 1 && value <= AllSides { + resizable.properties[Side] = value + return true + } else { + ErrorLogF(`Invalid value %d of "side" property`, value) + return false + } + + default: + if n, ok := isInt(value); ok { + if n >= 1 && n <= AllSides { + resizable.properties[Side] = n + return true + } else { + ErrorLogF(`Invalid value %d of "side" property`, n) + return false + } + } + } + + return false +} + +func (resizable *resizableData) resizeBorderWidth() SizeUnit { + result, _ := sizeProperty(resizable, ResizeBorderWidth, resizable.Session()) + if result.Type == Auto || result.Value == 0 { + return Px(4) + } + return result +} + +func (resizable *resizableData) updateResizeBorderWidth() { + htmlID := resizable.htmlID() + session := resizable.Session() + column, row := resizable.cellSizeCSS() + + updateCSSProperty(htmlID, "grid-template-columns", column, session) + updateCSSProperty(htmlID, "grid-template-rows", row, session) +} + +func (resizable *resizableData) cellSizeCSS() (string, string) { + w := resizable.resizeBorderWidth().cssString("4px") + side := resizable.getSide() + column := "1fr" + row := "1fr" + + if side&LeftSide != 0 { + if (side & RightSide) != 0 { + column = w + " 1fr " + w + } else { + column = w + " 1fr" + } + } else if (side & RightSide) != 0 { + column = "1fr " + w + } + + if side&TopSide != 0 { + if (side & BottomSide) != 0 { + row = w + " 1fr " + w + } else { + row = w + " 1fr" + } + } else if (side & BottomSide) != 0 { + row = "1fr " + w + } + + return column, row +} + +func (resizable *resizableData) cssStyle(self View, builder cssBuilder) { + column, row := resizable.cellSizeCSS() + + builder.add("grid-template-columns", column) + builder.add("grid-template-rows", row) + + resizable.viewData.cssStyle(self, builder) +} + +func (resizable *resizableData) htmlSubviews(self View, buffer *strings.Builder) { + + side := resizable.getSide() + left := 1 + top := 1 + leftSide := (side & LeftSide) != 0 + rightSide := (side & RightSide) != 0 + w := resizable.resizeBorderWidth().cssString("4px") + + if leftSide { + left = 2 + } + + writePos := func(x1, x2, y1, y2 int) { + buffer.WriteString(fmt.Sprintf(` grid-column-start: %d; grid-column-end: %d; grid-row-start: %d; grid-row-end: %d;">`, x1, x2, y1, y2)) + } + //htmlID := resizable.htmlID() + + if (side & TopSide) != 0 { + top = 2 + + if leftSide { + buffer.WriteString(`
0 { + view.properties[tag] = value + } else { + delete(view.properties, tag) + return true + } + + case func(Frame): + fn := func(view View, frame Frame) { + value(frame) + } + view.properties[tag] = []func(View, Frame){fn} + + case []func(Frame): + count := len(value) + if count == 0 { + delete(view.properties, tag) + return true + } + + listeners := make([]func(View, Frame), count) + for i, val := range value { + if val == nil { + notCompatibleType(tag, val) + return false + } + + listeners[i] = func(view View, frame Frame) { + val(frame) + } + } + view.properties[tag] = listeners + + case func(View): + fn := func(view View, frame Frame) { + value(view) + } + view.properties[tag] = []func(View, Frame){fn} + + case []func(View): + count := len(value) + if count == 0 { + delete(view.properties, tag) + return true + } + + listeners := make([]func(View, Frame), count) + for i, val := range value { + if val == nil { + notCompatibleType(tag, val) + return false + } + + listeners[i] = func(view View, frame Frame) { + val(view) + } + } + view.properties[tag] = listeners + + case func(): + fn := func(view View, frame Frame) { + value() + } + view.properties[tag] = []func(View, Frame){fn} + + case []func(): + count := len(value) + if count == 0 { + delete(view.properties, tag) + return true + } + + listeners := make([]func(View, Frame), count) + for i, val := range value { + if val == nil { + notCompatibleType(tag, val) + return false + } + + listeners[i] = func(view View, frame Frame) { + val() + } + } + view.properties[tag] = listeners + + case []interface{}: + count := len(value) + if count == 0 { + delete(view.properties, tag) + return true + } + + listeners := make([]func(View, Frame), count) + for i, val := range value { + if val == nil { + notCompatibleType(tag, val) + return false + } + + switch val := val.(type) { + case func(View, Frame): + listeners[i] = val + + case func(Frame): + listeners[i] = func(view View, frame Frame) { + val(frame) + } + + case func(View): + listeners[i] = func(view View, frame Frame) { + val(view) + } + + case func(): + listeners[i] = func(view View, frame Frame) { + val() + } + + default: + notCompatibleType(tag, val) + return false + } + } + view.properties[tag] = listeners + + default: + notCompatibleType(tag, value) + return false + } + + return true +} + +func (view *viewData) setNoResizeEvent() { + view.noResizeEvent = true +} + +func (view *viewData) isNoResizeEvent() bool { + return view.noResizeEvent +} + +func (container *viewsContainerData) isNoResizeEvent() bool { + if container.noResizeEvent { + return true + } + + if parent := container.Parent(); parent != nil { + return parent.isNoResizeEvent() + } + + return false +} + +func (view *viewData) Frame() Frame { + return view.frame +} + +// GetViewFrame returns the size and location of view's viewport. +// If the second argument (subviewID) is "" then the value of the first argument (view) is returned +func GetViewFrame(view View, subviewID string) Frame { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return Frame{} + } + return view.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 "" then the listeners list of the first argument (view) is returned +func GetResizeListeners(view View, subviewID string) []func(View, Frame) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.Get(ResizeEvent); value != nil { + if result, ok := value.([]func(View, Frame)); ok { + return result + } + } + } + return []func(View, Frame){} +} diff --git a/resources.go b/resources.go new file mode 100644 index 0000000..e4f3ef2 --- /dev/null +++ b/resources.go @@ -0,0 +1,418 @@ +package rui + +import ( + "embed" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strconv" + "strings" +) + +const ( + imageDir = "images" + themeDir = "themes" + viewDir = "views" + rawDir = "raw" + stringsDir = "strings" +) + +type scaledImage struct { + path string + scale float64 +} + +type imagePath struct { + path string + fs *embed.FS +} + +type resourceManager struct { + embedFS []*embed.FS + themes map[string]*theme + images map[string]imagePath + imageSrcSets map[string][]scaledImage + path string +} + +var resources = resourceManager{ + embedFS: []*embed.FS{}, + themes: map[string]*theme{}, + images: map[string]imagePath{}, + imageSrcSets: map[string][]scaledImage{}, +} + +func AddEmbedResources(fs *embed.FS) { + resources.embedFS = append(resources.embedFS, fs) + rootDirs := embedRootDirs(fs) + for _, dir := range rootDirs { + switch dir { + case imageDir: + scanEmbedImagesDir(fs, dir, "") + + case themeDir: + scanEmbedThemesDir(fs, dir) + + case stringsDir: + scanEmbedStringsDir(fs, dir) + + case viewDir, rawDir: + // do nothing + + default: + if files, err := fs.ReadDir(dir); err == nil { + for _, file := range files { + if file.IsDir() { + switch file.Name() { + case imageDir: + scanEmbedImagesDir(fs, dir+"/"+imageDir, "") + + case themeDir: + scanEmbedThemesDir(fs, dir+"/"+themeDir) + + case stringsDir: + scanEmbedStringsDir(fs, dir+"/"+stringsDir) + + case viewDir, rawDir: + // do nothing + } + } + } + } + } + } +} + +func embedRootDirs(fs *embed.FS) []string { + result := []string{} + if files, err := fs.ReadDir("."); err == nil { + for _, file := range files { + if file.IsDir() { + result = append(result, file.Name()) + } + } + } + return result +} + +func scanEmbedThemesDir(fs *embed.FS, dir string) { + if files, err := fs.ReadDir(dir); err == nil { + for _, file := range files { + name := file.Name() + path := dir + "/" + name + if file.IsDir() { + scanEmbedThemesDir(fs, path) + } else if strings.ToLower(filepath.Ext(name)) == ".rui" { + if data, err := fs.ReadFile(path); err == nil { + RegisterThemeText(string(data)) + } + } + } + } +} + +func scanEmbedImagesDir(fs *embed.FS, dir, prefix string) { + if files, err := fs.ReadDir(dir); err == nil { + for _, file := range files { + name := file.Name() + path := dir + "/" + name + if file.IsDir() { + scanEmbedImagesDir(fs, path, prefix+name+"/") + } else { + ext := strings.ToLower(filepath.Ext(name)) + switch ext { + case ".png", ".jpg", ".jpeg", ".svg": + registerImage(fs, path, prefix+name) + } + } + } + } +} + +func invalidImageFileFormat(filename string) { + ErrorLog(`Invalid image file name parameters: "` + filename + + `". Image file name format: name[@x-param].ext (examples: icon.png, icon@1.5x.png)`) +} + +func registerImage(fs *embed.FS, path, filename string) { + resources.images[filename] = imagePath{fs: fs, path: path} + + start := strings.LastIndex(filename, "@") + if start < 0 { + return + } + + ext := strings.LastIndex(filename, ".") + if start > ext || filename[ext-1] != 'x' { + invalidImageFileFormat(path) + return + } + + if scale, err := strconv.ParseFloat(filename[start+1:ext-1], 32); err == nil { + key := filename[:start] + filename[ext:] + images, ok := resources.imageSrcSets[key] + if ok { + for _, image := range images { + if image.scale == scale { + return + } + } + } else { + images = []scaledImage{} + } + resources.imageSrcSets[key] = append(images, scaledImage{path: filename, scale: scale}) + } else { + invalidImageFileFormat(path) + return + } +} + +func scanImagesDirectory(path, filePrefix string) { + if files, err := ioutil.ReadDir(path); err == nil { + for _, file := range files { + filename := file.Name() + if filename[0] != '.' { + newPath := path + `/` + filename + if !file.IsDir() { + registerImage(nil, newPath, filePrefix+filename) + } else { + scanImagesDirectory(newPath, filePrefix+filename+"/") + } + } + } + } else { + ErrorLog(err.Error()) + } +} + +func scanThemesDir(path string) { + if files, err := ioutil.ReadDir(path); err == nil { + for _, file := range files { + filename := file.Name() + if filename[0] != '.' { + newPath := path + `/` + filename + if file.IsDir() { + scanThemesDir(newPath) + } else if strings.ToLower(filepath.Ext(newPath)) == ".rui" { + if data, err := ioutil.ReadFile(newPath); err == nil { + RegisterThemeText(string(data)) + } else { + ErrorLog(err.Error()) + } + } + } + } + } else { + ErrorLog(err.Error()) + } +} + +// SetResourcePath set path of the resource directory +func SetResourcePath(path string) { + resources.path = path + pathLen := len(path) + if pathLen > 0 && path[pathLen-1] != '/' { + resources.path += "/" + } + + scanImagesDirectory(resources.path+imageDir, "") + scanThemesDir(resources.path + themeDir) + scanStringsDir(resources.path + stringsDir) +} + +// RegisterThemeText parse text and add result to the theme list +func RegisterThemeText(text string) bool { + data := ParseDataText(text) + if data == nil { + return false + } + + if !data.IsObject() { + ErrorLog(`Root element is not object`) + return false + } + if data.Tag() != "theme" { + ErrorLog(`Invalid the root object tag. Must be "theme"`) + return false + } + + if name, ok := data.PropertyValue("name"); ok && name != "" { + t := resources.themes[name] + if t == nil { + t = new(theme) + t.init() + resources.themes[name] = t + } + t.addData(data) + } else { + defaultTheme.addData(data) + } + + return true +} + +func serveResourceFile(filename string, w http.ResponseWriter, r *http.Request) bool { + serveEmbed := func(fs *embed.FS, path string) bool { + if file, err := fs.Open(path); err == nil { + if stat, err := file.Stat(); err == nil { + http.ServeContent(w, r, filename, stat.ModTime(), file.(io.ReadSeeker)) + return true + } + } + return false + } + + if image, ok := resources.images[filename]; ok { + if image.fs != nil { + if serveEmbed(image.fs, image.path) { + return true + } + } else { + if _, err := os.Stat(image.path); err == nil { + http.ServeFile(w, r, image.path) + return true + } + } + } + + for _, fs := range resources.embedFS { + if serveEmbed(fs, filename) { + return true + } + for _, dir := range embedRootDirs(fs) { + if serveEmbed(fs, dir+"/"+filename) { + return true + } + if subdirs, err := fs.ReadDir(dir); err == nil { + for _, subdir := range subdirs { + if subdir.IsDir() { + if serveEmbed(fs, dir+"/"+subdir.Name()+"/"+filename) { + return true + } + } + } + } + } + } + + serve := func(path, filename string) bool { + filepath := path + filename + if _, err := os.Stat(filepath); err == nil { + http.ServeFile(w, r, filepath) + return true + } + + filepath = path + imageDir + "/" + filename + if _, err := os.Stat(filepath); err == nil { + http.ServeFile(w, r, filepath) + return true + } + + return false + } + + if resources.path != "" && serve(resources.path, filename) { + return true + } + + if exe, err := os.Executable(); err == nil { + path := filepath.Dir(exe) + "/resources/" + if serve(path, filename) { + return true + } + } + + return false +} + +func ReadRawResource(filename string) []byte { + for _, fs := range resources.embedFS { + rootDirs := embedRootDirs(fs) + for _, dir := range rootDirs { + switch dir { + case imageDir, themeDir, viewDir: + // do nothing + + case rawDir: + if data, err := fs.ReadFile(dir + "/" + filename); err == nil { + return data + } + + default: + if data, err := fs.ReadFile(dir + "/" + rawDir + "/" + filename); err == nil { + return data + } + } + } + } + + readFile := func(path string) []byte { + if data, err := os.ReadFile(resources.path + rawDir + "/" + filename); err == nil { + return data + } + return nil + } + + if resources.path != "" { + if data := readFile(resources.path + rawDir + "/" + filename); data != nil { + return data + } + } + + if exe, err := os.Executable(); err == nil { + if data := readFile(filepath.Dir(exe) + "/resources/" + rawDir + "/" + filename); data != nil { + return data + } + } + + ErrorLogF(`The raw file "%s" don't found`, filename) + return nil +} + +func AllRawResources() []string { + result := []string{} + + for _, fs := range resources.embedFS { + rootDirs := embedRootDirs(fs) + for _, dir := range rootDirs { + switch dir { + case imageDir, themeDir, viewDir: + // do nothing + + case rawDir: + if files, err := fs.ReadDir(rawDir); err == nil { + for _, file := range files { + result = append(result, file.Name()) + } + } + + default: + if files, err := fs.ReadDir(dir + "/" + rawDir); err == nil { + for _, file := range files { + result = append(result, file.Name()) + } + } + } + } + } + + if resources.path != "" { + if files, err := ioutil.ReadDir(resources.path + rawDir); err == nil { + for _, file := range files { + result = append(result, file.Name()) + } + } + } + + if exe, err := os.Executable(); err == nil { + if files, err := ioutil.ReadDir(filepath.Dir(exe) + "/resources/" + rawDir); err == nil { + for _, file := range files { + result = append(result, file.Name()) + } + } + } + + return result +} diff --git a/rui.code-workspace b/rui.code-workspace new file mode 100644 index 0000000..876a149 --- /dev/null +++ b/rui.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/ruiWriter.go b/ruiWriter.go new file mode 100644 index 0000000..e6862fb --- /dev/null +++ b/ruiWriter.go @@ -0,0 +1,203 @@ +package rui + +import ( + "fmt" + "strconv" + "strings" +) + +type ruiWriter interface { + startObject(tag string) + startObjectProperty(tag, objectTag string) + endObject() + writeProperty(tag string, value interface{}) + finish() string +} + +type ruiStringer interface { + ruiString(writer ruiWriter) +} + +type ruiWriterData struct { + buffer *strings.Builder + indent string +} + +func newRUIWriter() ruiWriter { + writer := new(ruiWriterData) + return writer +} + +func (writer *ruiWriterData) writeIndent() { + if writer.buffer == nil { + writer.buffer = allocStringBuilder() + writer.indent = "" + return + } + + if writer.indent != "" { + writer.buffer.WriteString(writer.indent) + } +} + +func (writer *ruiWriterData) writeString(str string) { + esc := map[string]string{"\t": `\t`, "\r": `\r`, "\n": `\n`, "\"": `"`} + hasEsc := false + for s := range esc { + if strings.Contains(str, s) { + hasEsc = true + break + } + } + if hasEsc || strings.Contains(str, " ") || strings.Contains(str, ",") { + if !strings.Contains(str, "`") && (hasEsc || strings.Contains(str, `\`)) { + writer.buffer.WriteRune('`') + writer.buffer.WriteString(str) + writer.buffer.WriteRune('`') + } else { + str = strings.Replace(str, `\`, `\\`, -1) + for oldStr, newStr := range esc { + str = strings.Replace(str, oldStr, newStr, -1) + } + writer.buffer.WriteRune('"') + writer.buffer.WriteString(str) + writer.buffer.WriteRune('"') + } + } else { + writer.buffer.WriteString(str) + } +} + +func (writer *ruiWriterData) startObject(tag string) { + writer.writeIndent() + writer.indent += "\t" + writer.writeString(tag) + writer.buffer.WriteString(" {\n") +} + +func (writer *ruiWriterData) startObjectProperty(tag, objectTag string) { + writer.writeIndent() + writer.indent += "\t" + writer.writeString(tag) + writer.writeString(" = ") + writer.writeString(objectTag) + writer.buffer.WriteString(" {\n") +} + +func (writer *ruiWriterData) endObject() { + if len(writer.indent) > 0 { + writer.indent = writer.indent[1:] + } + writer.writeIndent() + writer.buffer.WriteRune('}') +} + +func (writer *ruiWriterData) writeValue(value interface{}) { + + switch value := value.(type) { + case string: + writer.writeString(value) + + case ruiStringer: + value.ruiString(writer) + // TODO + + case fmt.Stringer: + writer.writeString(value.String()) + + case float32: + writer.writeString(fmt.Sprintf("%g", float64(value))) + + case float64: + writer.writeString(fmt.Sprintf("%g", value)) + + case []string: + switch len(value) { + case 0: + writer.buffer.WriteString("[]\n") + + case 1: + writer.writeString(value[0]) + + default: + writer.buffer.WriteString("[\n") + writer.indent += "\t" + for _, v := range value { + writer.buffer.WriteString(writer.indent) + writer.writeString(v) + writer.buffer.WriteString(",\n") + } + + writer.indent = writer.indent[1:] + writer.buffer.WriteString(writer.indent) + writer.buffer.WriteRune(']') + } + + case []View: + switch len(value) { + case 0: + writer.buffer.WriteString("[]\n") + + case 1: + writer.writeValue(value[0]) + + default: + writer.buffer.WriteString("[\n") + writer.indent += "\t" + for _, v := range value { + writer.buffer.WriteString(writer.indent) + v.ruiString(writer) + writer.buffer.WriteString(",\n") + } + + writer.indent = writer.indent[1:] + writer.buffer.WriteString(writer.indent) + writer.buffer.WriteRune(']') + } + + case []interface{}: + switch len(value) { + case 0: + writer.buffer.WriteString("[]\n") + + case 1: + writer.writeValue(value[0]) + + default: + writer.buffer.WriteString("[\n") + writer.indent += "\t" + for _, v := range value { + writer.buffer.WriteString(writer.indent) + writer.writeValue(v) + writer.buffer.WriteString(",\n") + } + + writer.indent = writer.indent[1:] + writer.buffer.WriteString(writer.indent) + writer.buffer.WriteRune(']') + } + + default: + if n, ok := isInt(value); ok { + writer.buffer.WriteString(strconv.Itoa(n)) + } + } + writer.buffer.WriteString(",\n") +} + +func (writer *ruiWriterData) writeProperty(tag string, value interface{}) { + writer.writeIndent() + writer.writeString(tag) + writer.buffer.WriteString(" = ") + writer.writeValue(value) +} + +func (writer *ruiWriterData) finish() string { + result := "" + if writer.buffer != nil { + result = writer.buffer.String() + freeStringBuilder(writer.buffer) + writer.buffer = nil + } + return result +} diff --git a/scrollEvent.go b/scrollEvent.go new file mode 100644 index 0000000..a3c312d --- /dev/null +++ b/scrollEvent.go @@ -0,0 +1,91 @@ +package rui + +import "fmt" + +// ScrollEvent is the constant for "scroll-event" property tag +// The "resize-event" is fired when the content of the view is scrolled. +// The main listener format: func(View, Frame). +// The additional listener formats: func(Frame), func(View), and func(). +const ScrollEvent = "scroll-event" + +func (view *viewData) onScroll(self View, x, y, width, height float64) { + view.scroll.Left = x + view.scroll.Top = y + view.scroll.Width = width + view.scroll.Height = height + for _, listener := range GetScrollListeners(view, "") { + listener(self, view.scroll) + } +} + +func (view *viewData) Scroll() Frame { + return view.scroll +} + +func (view *viewData) setScroll(x, y, width, height float64) { + view.scroll.Left = x + view.scroll.Top = y + view.scroll.Width = width + view.scroll.Height = height +} + +// GetViewScroll returns ... +// If the second argument (subviewID) is "" then a value of the first argument (view) is returned +func GetViewScroll(view View, subviewID string) Frame { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return Frame{} + } + return view.Scroll() +} + +// 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 "" then the listeners list of the first argument (view) is returned +func GetScrollListeners(view View, subviewID string) []func(View, Frame) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.Get(ScrollEvent); value != nil { + if result, ok := value.([]func(View, Frame)); ok { + return result + } + } + } + return []func(View, Frame){} +} + +// ScrollTo scrolls the view's content to the given position. +// If the second argument (subviewID) is "" then the first argument (view) is used +func ScrollViewTo(view View, subviewID string, x, y float64) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + view.Session().runScript(fmt.Sprintf(`scrollTo("%s", %g, %g)`, view.htmlID(), x, y)) + } +} + +// ScrollViewToEnd scrolls the view's content to the start of view. +// If the second argument (subviewID) is "" then the first argument (view) is used +func ScrollViewToStart(view View, subviewID string) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + view.Session().runScript(`scrollToStart("` + view.htmlID() + `")`) + } +} + +// ScrollViewToEnd scrolls the view's content to the end of view. +// If the second argument (subviewID) is "" then the first argument (view) is used +func ScrollViewToEnd(view View, subviewID string) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + view.Session().runScript(`scrollToEnd("` + view.htmlID() + `")`) + } +} diff --git a/session.go b/session.go new file mode 100644 index 0000000..c6ec066 --- /dev/null +++ b/session.go @@ -0,0 +1,401 @@ +package rui + +import ( + "fmt" + "strconv" + "strings" +) + +// SessionContent is the interface of a session content +type SessionContent interface { + CreateRootView(session Session) View +} + +// Session provide interface to session parameters assess +type Session interface { + // App return the current application interface + App() Application + // ID return the id of the session + ID() int + + // DarkTheme returns "true" if the dark theme is used + DarkTheme() bool + // Mobile returns "true" if current session is displayed on a touch screen device + TouchScreen() bool + // PixelRatio returns the ratio of the resolution in physical pixels to the resolution + // in logical pixels for the current display device. + PixelRatio() float64 + // TextDirection returns the default text direction (LeftToRightDirection (1) or RightToLeftDirection (2)) + TextDirection() int + // Constant returns the constant with "tag" name or "" if it is not exists + Constant(tag string) (string, bool) + // Color returns the color with "tag" name or 0 if it is not exists + Color(tag string) (Color, bool) + // SetCustomTheme set the custom theme + SetCustomTheme(name string) bool + // Language returns the current session language + Language() string + // SetLanguage set the current session language + SetLanguage(lang string) + // GetString returns the text for the current language + GetString(tag string) (string, bool) + + Content() SessionContent + setContent(content SessionContent, self Session) bool + + // RootView returns the root view of the session + 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) interface{} + // 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 interface{}) bool + + resolveConstants(value string) (string, bool) + checkboxOffImage() string + checkboxOnImage() string + radiobuttonOffImage() string + radiobuttonOnImage() string + + viewByHTMLID(id string) View + nextViewID() string + styleProperty(styleTag, property string) (string, bool) + stylePropertyNode(styleTag, propertyTag string) DataNode + + setBrige(events chan DataObject, brige WebBrige) + writeInitScript(writer *strings.Builder) + runScript(script string) + runGetterScript(script string) DataObject //, answer chan DataObject) + handleAnswer(data DataObject) + handleResize(data DataObject) + handleViewEvent(command string, data DataObject) + close() + + onStart() + onFinish() + onPause() + onResume() + onDisconnect() + onReconnect() + + ignoreViewUpdates() bool + setIgnoreViewUpdates(ignore bool) + + popupManager() *popupManager + imageManager() *imageManager +} + +type sessionData struct { + customTheme *theme + darkTheme bool + touchScreen bool + textDirection int + pixelRatio float64 + language string + languages []string + checkboxOff string + checkboxOn string + radiobuttonOff string + radiobuttonOn string + app Application + sessionID int + viewCounter int + content SessionContent + rootView View + ignoreUpdates bool + popups *popupManager + images *imageManager + brige WebBrige + events chan DataObject +} + +func newSession(app Application, id int, customTheme string, params DataObject) Session { + session := new(sessionData) + session.app = app + session.sessionID = id + session.darkTheme = false + session.touchScreen = false + session.pixelRatio = 1 + session.textDirection = LeftToRightDirection + session.languages = []string{} + session.viewCounter = 0 + session.ignoreUpdates = false + + if customTheme != "" { + if theme, ok := newTheme(customTheme); ok { + session.customTheme = theme + } + } + + if value, ok := params.PropertyValue("touch"); ok { + session.touchScreen = (value == "1" || value == "true") + } + + if value, ok := params.PropertyValue("direction"); ok { + if value == "rtl" { + session.textDirection = RightToLeftDirection + } + } + + if value, ok := params.PropertyValue("languages"); ok { + session.languages = strings.Split(value, ",") + } + + if value, ok := params.PropertyValue("dark"); ok { + session.darkTheme = (value == "1" || value == "true") + } + + if value, ok := params.PropertyValue("pixel-ratio"); ok { + if f, err := strconv.ParseFloat(value, 64); err != nil { + ErrorLog(err.Error()) + } else { + session.pixelRatio = f + } + } + + return session +} + +func (session *sessionData) App() Application { + return session.app +} + +func (session *sessionData) ID() int { + return session.sessionID +} + +func (session *sessionData) setBrige(events chan DataObject, brige WebBrige) { + session.events = events + session.brige = brige +} + +func (session *sessionData) close() { + if session.events != nil { + session.events <- ParseDataText(`session-close{session="` + strconv.Itoa(session.sessionID) + `"}`) + } +} + +func (session *sessionData) styleProperty(styleTag, propertyTag string) (string, bool) { + var style DataObject + ok := false + if session.customTheme != nil { + style, ok = session.customTheme.styles[styleTag] + } + if !ok { + style, ok = defaultTheme.styles[styleTag] + } + + if ok { + if node := style.PropertyWithTag(propertyTag); node != nil && node.Type() == TextNode { + return session.resolveConstants(node.Text()) + } + } + + //errorLogF(`property "%v" not found`, propertyTag) + return "", false +} + +func (session *sessionData) stylePropertyNode(styleTag, propertyTag string) DataNode { + var style DataObject + ok := false + if session.customTheme != nil { + style, ok = session.customTheme.styles[styleTag] + } + if !ok { + style, ok = defaultTheme.styles[styleTag] + } + + if ok { + return style.PropertyWithTag(propertyTag) + } + + return nil +} + +func (session *sessionData) nextViewID() string { + session.viewCounter++ + return fmt.Sprintf("id%06d", session.viewCounter) +} + +func (session *sessionData) viewByHTMLID(id string) View { + if session.rootView == nil { + return nil + } + popupManager := session.popupManager() + for _, popup := range popupManager.popups { + if view := popup.viewByHTMLID(id); view != nil { + return view + } + } + return viewByHTMLID(id, session.rootView) +} + +func (session *sessionData) Content() SessionContent { + return session.content +} + +func (session *sessionData) setContent(content SessionContent, self Session) bool { + if content != nil { + session.content = content + session.rootView = content.CreateRootView(self) + if session.rootView != nil { + return true + } + } + return false +} + +func (session *sessionData) RootView() View { + return session.rootView +} + +func (session *sessionData) writeInitScript(writer *strings.Builder) { + var workTheme *theme + if session.customTheme == nil { + workTheme = defaultTheme + } else { + workTheme = new(theme) + workTheme.init() + workTheme.concat(defaultTheme) + workTheme.concat(session.customTheme) + } + + if css := workTheme.cssText(session); css != "" { + writer.WriteString(`document.querySelector('style').textContent += "`) + writer.WriteString(css) + writer.WriteString("\";\n") + } + + if session.rootView != nil { + writer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`) + viewHTML(session.rootView, writer) + writer.WriteString("';\nscanElementsSize();") + } +} + +func (session *sessionData) reload() { + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + session.writeInitScript(buffer) + session.runScript(buffer.String()) +} + +func (session *sessionData) ignoreViewUpdates() bool { + return session.brige == nil || session.ignoreUpdates +} + +func (session *sessionData) setIgnoreViewUpdates(ignore bool) { + session.ignoreUpdates = ignore +} + +func (session *sessionData) Get(viewID, tag string) interface{} { + if view := ViewByID(session.RootView(), viewID); view != nil { + return view.Get(tag) + } + return nil +} + +func (session *sessionData) Set(viewID, tag string, value interface{}) bool { + if view := ViewByID(session.RootView(), viewID); view != nil { + return view.Set(tag, value) + } + return false +} + +func (session *sessionData) popupManager() *popupManager { + if session.popups == nil { + session.popups = new(popupManager) + session.popups.popups = []Popup{} + } + return session.popups +} + +func (session *sessionData) imageManager() *imageManager { + if session.images == nil { + session.images = new(imageManager) + session.images.images = make(map[string]*imageData) + } + return session.images +} + +func (session *sessionData) runScript(script string) { + if session.brige != nil { + session.brige.WriteMessage(script) + } else { + ErrorLog("No connection") + } +} + +func (session *sessionData) runGetterScript(script string) DataObject { //}, answer chan DataObject) { + if session.brige != nil { + return session.brige.RunGetterScript(script) + } + + ErrorLog("No connection") + result := NewDataObject("error") + result.SetPropertyValue("text", "No connection") + return result +} + +func (session *sessionData) handleAnswer(data DataObject) { + session.brige.AnswerReceived(data) +} + +func (session *sessionData) handleResize(data DataObject) { + if node := data.PropertyWithTag("views"); node != nil && node.Type() == ArrayNode { + for _, el := range node.ArrayElements() { + if el.IsObject() { + obj := el.Object() + getFloat := func(tag string) float64 { + if value, ok := obj.PropertyValue(tag); ok { + f, err := strconv.ParseFloat(value, 64) + if err == nil { + return f + } + ErrorLog(`Resize event error: ` + err.Error()) + } else { + ErrorLogF(`Resize event error: the property "%s" not found`, tag) + } + return 0 + } + if viewID, ok := obj.PropertyValue("id"); ok { + if n := strings.IndexRune(viewID, '-'); n > 0 { + if index, err := strconv.Atoi(viewID[n+1:]); err == nil { + if view := session.viewByHTMLID(viewID[:n]); view != nil { + view.onItemResize(view, index, getFloat("x"), getFloat("y"), getFloat("width"), getFloat("height")) + } else { + ErrorLogF(`View with id == %s not found`, viewID[:n]) + } + } else { + ErrorLogF(`Invalid view id == %s not found`, viewID) + } + } else if view := session.viewByHTMLID(viewID); view != nil { + view.onResize(view, getFloat("x"), getFloat("y"), getFloat("width"), getFloat("height")) + view.setScroll(getFloat("scroll-x"), getFloat("scroll-y"), getFloat("scroll-width"), getFloat("scroll-height")) + } else { + ErrorLogF(`View with id == %s not found`, viewID) + } + } else { + ErrorLog(`"id" property not found`) + } + } else { + ErrorLog(`Resize event error: views element is not object`) + } + } + } else { + ErrorLog(`Resize event error: invalid "views" property`) + } +} + +func (session *sessionData) handleViewEvent(command string, data DataObject) { + if viewID, ok := data.PropertyValue("id"); ok { + if view := session.viewByHTMLID(viewID); view != nil { + view.handleCommand(view, command, data) + } + } else { + ErrorLog(`"id" property not found. Event: ` + command) + } +} diff --git a/sessionEvents.go b/sessionEvents.go new file mode 100644 index 0000000..9c66e7b --- /dev/null +++ b/sessionEvents.go @@ -0,0 +1,81 @@ +package rui + +// SessionStartListener is the listener interface of a session start event +type SessionStartListener interface { + OnStart(session Session) +} + +// SessionFinishListener is the listener interface of a session start event +type SessionFinishListener interface { + OnFinish(session Session) +} + +// SessionResumeListener is the listener interface of a session resume event +type SessionResumeListener interface { + OnResume(session Session) +} + +// SessionPauseListener is the listener interface of a session pause event +type SessionPauseListener interface { + OnPause(session Session) +} + +// SessionPauseListener is the listener interface of a session disconnect event +type SessionDisconnectListener interface { + OnDisconnect(session Session) +} + +// SessionPauseListener is the listener interface of a session reconnect event +type SessionReconnectListener interface { + OnReconnect(session Session) +} + +func (session *sessionData) onStart() { + if session.content != nil { + if listener, ok := session.content.(SessionStartListener); ok { + listener.OnStart(session) + } + session.onResume() + } +} + +func (session *sessionData) onFinish() { + if session.content != nil { + session.onPause() + if listener, ok := session.content.(SessionFinishListener); ok { + listener.OnFinish(session) + } + } +} + +func (session *sessionData) onPause() { + if session.content != nil { + if listener, ok := session.content.(SessionPauseListener); ok { + listener.OnPause(session) + } + } +} + +func (session *sessionData) onResume() { + if session.content != nil { + if listener, ok := session.content.(SessionResumeListener); ok { + listener.OnResume(session) + } + } +} + +func (session *sessionData) onDisconnect() { + if session.content != nil { + if listener, ok := session.content.(SessionDisconnectListener); ok { + listener.OnDisconnect(session) + } + } +} + +func (session *sessionData) onReconnect() { + if session.content != nil { + if listener, ok := session.content.(SessionReconnectListener); ok { + listener.OnReconnect(session) + } + } +} diff --git a/sessionTheme.go b/sessionTheme.go new file mode 100644 index 0000000..56a439f --- /dev/null +++ b/sessionTheme.go @@ -0,0 +1,359 @@ +package rui + +import ( + "fmt" + "strings" +) + +/* +type Session struct { + customTheme *theme + darkTheme bool + touchScreen bool + textDirection int + pixelRatio float64 + language string + languages []string + checkboxOff string + checkboxOn string + radiobuttonOff string + radiobuttonOn string +} +*/ +func (session *sessionData) DarkTheme() bool { + return session.darkTheme +} + +func (session *sessionData) TouchScreen() bool { + return session.touchScreen +} + +func (session *sessionData) PixelRatio() float64 { + return session.pixelRatio +} + +func (session *sessionData) TextDirection() int { + return session.textDirection +} + +func (session *sessionData) constant(tag string, prevTags []string) (string, bool) { + tags := append(prevTags, tag) + result := "" + themes := session.themes() + for { + ok := false + if session.touchScreen { + for _, theme := range themes { + if theme.touchConstants != nil { + if result, ok = theme.touchConstants[tag]; ok { + break + } + } + } + } + + if !ok { + for _, theme := range themes { + if theme.constants != nil { + if result, ok = theme.constants[tag]; ok { + break + } + } + } + } + + if !ok { + ErrorLogF(`"%v" constant not found`, tag) + return "", false + } + + if len(result) < 2 || !strings.ContainsRune(result, '@') { + return result, true + } + + for _, separator := range []string{",", " ", ":", ";", "|", "/"} { + if strings.Contains(result, separator) { + result, ok = session.resolveConstantsNext(result, tags) + return result, ok + } + } + + if result[0] != '@' { + return result, true + } + + tag = result[1:] + for _, t := range tags { + if t == tag { + ErrorLogF(`"%v" constant is cyclic`, tag) + return "", false + } + } + tags = append(tags, tag) + } +} + +func (session *sessionData) resolveConstants(value string) (string, bool) { + return session.resolveConstantsNext(value, []string{}) +} + +func (session *sessionData) resolveConstantsNext(value string, prevTags []string) (string, bool) { + if !strings.Contains(value, "@") { + return value, true + } + + separators := []rune{',', ' ', ':', ';', '|', '/'} + sep := rune(0) + index := -1 + for _, s := range separators { + if i := strings.IndexRune(value, s); i >= 0 { + if i < index || index < 0 { + sep = s + index = i + } + } + } + + ok := true + if index >= 0 { + v1 := strings.Trim(value[:index], " \t\n\r") + v2 := strings.Trim(value[index+1:], " \t\n\r") + if len(v1) > 1 && v1[0] == '@' { + if v1, ok = session.constant(v1[1:], prevTags); !ok { + return value, false + } + if v, ok := session.resolveConstantsNext(v1, prevTags); ok { + v1 = v + } else { + return v1 + string(sep) + v2, false + } + } + + if v, ok := session.resolveConstantsNext(v2, prevTags); ok { + v2 = v + } + + return v1 + string(sep) + v2, ok + + } else if value[0] == '@' { + + if value, ok = session.constant(value[1:], prevTags); ok { + return session.resolveConstantsNext(value, prevTags) + } + } + + return value, false +} + +func (session *sessionData) Constant(tag string) (string, bool) { + return session.constant(tag, []string{}) +} + +func (session *sessionData) themes() []*theme { + if session.customTheme != nil { + return []*theme{session.customTheme, defaultTheme} + } + + return []*theme{defaultTheme} +} + +// Color return the color with "tag" name or 0 if it is not exists +func (session *sessionData) Color(tag string) (Color, bool) { + tags := []string{tag} + result := "" + themes := session.themes() + for { + ok := false + if session.darkTheme { + for _, theme := range themes { + if theme.darkColors != nil { + if result, ok = theme.darkColors[tag]; ok { + break + } + } + } + } + + if !ok { + for _, theme := range themes { + if theme.colors != nil { + if result, ok = theme.colors[tag]; ok { + break + } + } + } + } + + if !ok { + ErrorLogF(`"%v" color not found`, tag) + return 0, false + } + + if len(result) == 0 || result[0] != '@' { + color, ok := StringToColor(result) + if !ok { + ErrorLogF(`invalid value "%v" of "%v" color constant`, result, tag) + return 0, false + } + return color, true + } + + tag = result[1:] + for _, t := range tags { + if t == tag { + ErrorLogF(`"%v" color is cyclic`, tag) + return 0, false + } + } + + tags = append(tags, tag) + } +} + +func (session *sessionData) SetCustomTheme(name string) bool { + if name == "" { + if session.customTheme == nil { + return true + } + } else if theme, ok := resources.themes[name]; ok { + session.customTheme = theme + } else { + return false + } + + session.reload() + return true +} + +const checkImage = `` + +func (session *sessionData) checkboxImage(checked bool) string { + + var borderColor, backgroundColor Color + var ok bool + + if borderColor, ok = session.Color("ruiDisabledTextColor"); !ok { + if session.darkTheme { + borderColor = 0xFFA0A0A0 + } else { + borderColor = 0xFF202020 + } + } + + if checked { + if backgroundColor, ok = session.Color("ruiHighlightColor"); !ok { + backgroundColor = 0xFF1A74E8 + } + } else if backgroundColor, ok = session.Color("backgroundColor"); !ok { + if session.darkTheme { + backgroundColor = 0xFFA0A0A0 + } else { + backgroundColor = 0xFF202020 + } + } + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + buffer.WriteString(`
`) + if checked { + buffer.WriteString(checkImage) + } + buffer.WriteString(`
`) + + return buffer.String() +} + +func (session *sessionData) checkboxOffImage() string { + if session.checkboxOff == "" { + session.checkboxOff = session.checkboxImage(false) + } + return session.checkboxOff +} + +func (session *sessionData) checkboxOnImage() string { + if session.checkboxOn == "" { + session.checkboxOn = session.checkboxImage(true) + } + return session.checkboxOn +} + +func (session *sessionData) radiobuttonOffImage() string { + if session.radiobuttonOff == "" { + var borderColor, backgroundColor Color + var ok bool + + if borderColor, ok = session.Color("ruiDisabledTextColor"); !ok { + if session.darkTheme { + borderColor = 0xFFA0A0A0 + } else { + borderColor = 0xFF202020 + } + } + + if backgroundColor, ok = session.Color("backgroundColor"); !ok { + if session.darkTheme { + backgroundColor = 0xFFA0A0A0 + } else { + backgroundColor = 0xFF202020 + } + } + + session.radiobuttonOff = fmt.Sprintf(`
`, + backgroundColor.cssString(), borderColor.cssString()) + } + return session.radiobuttonOff +} + +func (session *sessionData) radiobuttonOnImage() string { + if session.radiobuttonOn == "" { + var borderColor, backgroundColor Color + var ok bool + + if borderColor, ok = session.Color("ruiHighlightColor"); !ok { + borderColor = 0xFF1A74E8 + } + + if backgroundColor, ok = session.Color("ruiHighlightTextColor"); !ok { + backgroundColor = 0xFFFFFFFF + } + + session.radiobuttonOn = fmt.Sprintf(`
`, + backgroundColor.cssString(), borderColor.cssString(), borderColor.cssString()) + } + return session.radiobuttonOn +} + +func (session *sessionData) Language() string { + if session.language != "" { + return session.language + } + + if session.languages != nil && len(session.languages) > 0 { + return session.languages[0] + } + + return "en" +} + +func (session *sessionData) SetLanguage(lang string) { + lang = strings.Trim(lang, " \t\n\r") + if lang != session.language { + session.language = lang + + if session.rootView != nil { + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + buffer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`) + viewHTML(session.rootView, buffer) + buffer.WriteString("';\nscanElementsSize();") + + session.runScript(buffer.String()) + } + } +} diff --git a/sessionUtils.go b/sessionUtils.go new file mode 100644 index 0000000..990db24 --- /dev/null +++ b/sessionUtils.go @@ -0,0 +1,109 @@ +package rui + +import ( + "fmt" +) + +func sizeConstant(session Session, tag string) (SizeUnit, bool) { + if text, ok := session.Constant(tag); ok { + return StringToSizeUnit(text) + } + return AutoSize(), false +} + +func updateCSSStyle(htmlID string, session Session) { + if !session.ignoreViewUpdates() { + if view := session.viewByHTMLID(htmlID); view != nil { + var builder viewCSSBuilder + + builder.buffer = allocStringBuilder() + builder.buffer.WriteString(`updateCSSStyle('`) + builder.buffer.WriteString(view.htmlID()) + builder.buffer.WriteString(`', '`) + view.cssStyle(view, &builder) + builder.buffer.WriteString(`');`) + view.Session().runScript(builder.finish()) + } + } +} + +func updateInnerHTML(htmlID string, session Session) { + if !session.ignoreViewUpdates() { + if view := session.viewByHTMLID(htmlID); view != nil { + script := allocStringBuilder() + defer freeStringBuilder(script) + + script.Grow(32 * 1024) + view.htmlSubviews(view, script) + view.Session().runScript(fmt.Sprintf(`updateInnerHTML('%v', '%v');`, view.htmlID(), script.String())) + //view.updateEventHandlers() + } + } +} + +func appendToInnerHTML(htmlID, content string, session Session) { + if !session.ignoreViewUpdates() { + if view := session.viewByHTMLID(htmlID); view != nil { + view.Session().runScript(fmt.Sprintf(`appendToInnerHTML('%v', '%v');`, view.htmlID(), content)) + //view.updateEventHandlers() + } + } +} + +func updateProperty(htmlID, property, value string, session Session) { + if !session.ignoreViewUpdates() { + session.runScript(fmt.Sprintf(`updateProperty('%v', '%v', '%v');`, htmlID, property, value)) + } +} + +func updateCSSProperty(htmlID, property, value string, session Session) { + if !session.ignoreViewUpdates() { + session.runScript(fmt.Sprintf(`updateCSSProperty('%v', '%v', '%v');`, htmlID, property, value)) + } +} + +func updateBoolProperty(htmlID, property string, value bool, session Session) { + if !session.ignoreViewUpdates() { + if value { + session.runScript(fmt.Sprintf(`updateProperty('%v', '%v', true);`, htmlID, property)) + } else { + session.runScript(fmt.Sprintf(`updateProperty('%v', '%v', false);`, htmlID, property)) + } + } +} + +func removeProperty(htmlID, property string, session Session) { + if !session.ignoreViewUpdates() { + session.runScript(fmt.Sprintf(`removeProperty('%v', '%v');`, htmlID, property)) + } +} + +/* +func setDisabled(htmlID string, disabled bool, session Session) { + if !session.ignoreViewUpdates() { + if disabled { + session.runScript(fmt.Sprintf(`setDisabled('%v', true);`, htmlID)) + } else { + session.runScript(fmt.Sprintf(`setDisabled('%v', false);`, htmlID)) + } + } +} +*/ + +func viewByHTMLID(id string, startView View) View { + if startView != nil { + if startView.htmlID() == id { + return startView + } + if container, ok := startView.(ParanetView); ok { + for _, view := range container.Views() { + if view != nil { + if v := viewByHTMLID(id, view); v != nil { + return v + } + } + } + } + } + return nil +} diff --git a/session_test.go b/session_test.go new file mode 100644 index 0000000..d79469c --- /dev/null +++ b/session_test.go @@ -0,0 +1,122 @@ +package rui + +import ( + "testing" +) + +var stopTestLogFlag = false +var testLogDone chan int +var ignoreTestLog = false + +func createTestLog(t *testing.T, ignore bool) { + ignoreTestLog = ignore + SetErrorLog(func(text string) { + if ignoreTestLog { + t.Log(text) + } else { + t.Error(text) + } + }) + SetDebugLog(func(text string) { + t.Log(text) + }) +} + +/* +func createTestSession(t *testing.T) *sessionData { + session := new(sessionData) + createTestLog(t, false) + return session +} + +func TestSessionConstants(t *testing.T) { + session := createTestSession(t) + + customTheme := ` + theme { + colors = _{ + textColor = #FF080808, + myColor = #81234567 + }, + colors:dark = _{ + textColor = #FFF0F0F0, + myColor = #87654321 + }, + constants = _{ + defaultPadding = 10px, + myConstant = 100% + const1 = "@const2, 10px; @const3" + const2 = "20mm / @const4" + const3 = "@const5 : 30pt" + const4 = "40%" + const5 = "50px" + }, + constants:touch = _{ + defaultPadding = 20px, + myConstant = 80%, + }, + } + ` + + SetErrorLog(func(text string) { + t.Error(text) + }) + + theme, ok := newTheme(customTheme) + if !ok { + return + } + + session.SetCustomTheme(theme) + + type constPair struct { + tag, value string + } + + testConstants := func(constants []constPair) { + for _, constant := range constants { + if value, ok := session.Constant(constant.tag); ok { + if value != constant.value { + t.Error(constant.tag + " = " + value + ". Need: " + constant.value) + } + } + } + } + + testConstants([]constPair{ + {tag: "defaultPadding", value: "10px"}, + {tag: "myConstant", value: "100%"}, + {tag: "buttonMargin", value: "4px"}, + }) + + session.SetConstant("myConstant", "25px") + + testConstants([]constPair{ + {tag: "defaultPadding", value: "10px"}, + {tag: "myConstant", value: "25px"}, + {tag: "buttonMargin", value: "4px"}, + }) + + session.touchScreen = true + + testConstants([]constPair{ + {tag: "defaultPadding", value: "20px"}, + {tag: "myConstant", value: "80%"}, + {tag: "buttonMargin", value: "4px"}, + }) + + session.SetTouchConstant("myConstant", "30pt") + + testConstants([]constPair{ + {tag: "defaultPadding", value: "20px"}, + {tag: "myConstant", value: "30pt"}, + {tag: "buttonMargin", value: "4px"}, + }) + + if value, ok := session.Constant("const1"); ok { + if value != "20mm/40%,10px;50px:30pt" { + t.Error("const1 = " + value + ". Need: 20mm/40%,10px;50px:30pt") + } + } +} +*/ diff --git a/shadow.go b/shadow.go new file mode 100644 index 0000000..1ce0a87 --- /dev/null +++ b/shadow.go @@ -0,0 +1,312 @@ +package rui + +import ( + "fmt" + "strings" +) + +const ( + // ColorProperty is the name of the color property of the shadow. + ColorProperty = "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" +) + +// ViewShadow contains attributes of the view shadow +type ViewShadow interface { + Properties + fmt.Stringer + ruiStringer + cssStyle(buffer *strings.Builder, session Session, lead string) bool + cssTextStyle(buffer *strings.Builder, session Session, lead string) bool + visible(session Session) bool +} + +type viewShadowData struct { + propertyList +} + +// 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, + BlurRadius: blurRadius, + SpreadRadius: spreadRadius, + ColorProperty: 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, + BlurRadius: blurRadius, + SpreadRadius: spreadRadius, + ColorProperty: color, + Inset: true, + }) +} + +// 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, + BlurRadius: blurRadius, + ColorProperty: color, + }) +} + +// NewShadowWithParams create the new shadow for a view. +func NewShadowWithParams(params Params) ViewShadow { + shadow := new(viewShadowData) + shadow.propertyList.init() + if params != nil { + for _, tag := range []string{ColorProperty, Inset, XOffset, YOffset, BlurRadius, SpreadRadius} { + if value, ok := params[tag]; ok && value != nil { + shadow.Set(tag, value) + } + } + } + return shadow +} + +// parseViewShadow parse DataObject and create ViewShadow object +func parseViewShadow(object DataObject) ViewShadow { + shadow := new(viewShadowData) + shadow.propertyList.init() + parseProperties(shadow, object) + return shadow +} + +func (shadow *viewShadowData) Remove(tag string) { + delete(shadow.properties, strings.ToLower(tag)) +} + +func (shadow *viewShadowData) Set(tag string, value interface{}) bool { + if value == nil { + shadow.Remove(tag) + return true + } + + tag = strings.ToLower(tag) + switch tag { + case ColorProperty, 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) interface{} { + return shadow.propertyList.Get(strings.ToLower(tag)) +} + +func (shadow *viewShadowData) cssStyle(buffer *strings.Builder, session Session, lead string) bool { + color, _ := colorProperty(shadow, ColorProperty, session) + offsetX, _ := sizeProperty(shadow, XOffset, session) + offsetY, _ := sizeProperty(shadow, YOffset, session) + blurRadius, _ := sizeProperty(shadow, BlurRadius, session) + spreadRadius, _ := sizeProperty(shadow, SpreadRadius, session) + + if color.Alpha() == 0 || + ((offsetX.Type == Auto || offsetX.Value == 0) && + (offsetY.Type == Auto || offsetY.Value == 0) && + (blurRadius.Type == Auto || blurRadius.Value == 0) && + (spreadRadius.Type == Auto || spreadRadius.Value == 0)) { + return false + } + + buffer.WriteString(lead) + if inset, _ := boolProperty(shadow, Inset, session); inset { + buffer.WriteString("inset ") + } + + buffer.WriteString(offsetX.cssString("0")) + buffer.WriteByte(' ') + buffer.WriteString(offsetY.cssString("0")) + buffer.WriteByte(' ') + buffer.WriteString(blurRadius.cssString("0")) + buffer.WriteByte(' ') + buffer.WriteString(spreadRadius.cssString("0")) + buffer.WriteByte(' ') + buffer.WriteString(color.cssString()) + return true +} + +func (shadow *viewShadowData) cssTextStyle(buffer *strings.Builder, session Session, lead string) bool { + color, _ := colorProperty(shadow, ColorProperty, session) + offsetX, _ := sizeProperty(shadow, XOffset, session) + offsetY, _ := sizeProperty(shadow, YOffset, session) + blurRadius, _ := sizeProperty(shadow, BlurRadius, session) + + if color.Alpha() == 0 || + ((offsetX.Type == Auto || offsetX.Value == 0) && + (offsetY.Type == Auto || offsetY.Value == 0) && + (blurRadius.Type == Auto || blurRadius.Value == 0)) { + return false + } + + buffer.WriteString(lead) + buffer.WriteString(offsetX.cssString("0")) + buffer.WriteByte(' ') + buffer.WriteString(offsetY.cssString("0")) + buffer.WriteByte(' ') + buffer.WriteString(blurRadius.cssString("0")) + buffer.WriteByte(' ') + buffer.WriteString(color.cssString()) + return true +} + +func (shadow *viewShadowData) visible(session Session) bool { + color, _ := colorProperty(shadow, ColorProperty, session) + offsetX, _ := sizeProperty(shadow, XOffset, session) + offsetY, _ := sizeProperty(shadow, YOffset, session) + blurRadius, _ := sizeProperty(shadow, BlurRadius, session) + spreadRadius, _ := sizeProperty(shadow, SpreadRadius, session) + + if color.Alpha() == 0 || + ((offsetX.Type == Auto || offsetX.Value == 0) && + (offsetY.Type == Auto || offsetY.Value == 0) && + (blurRadius.Type == Auto || blurRadius.Value == 0) && + (spreadRadius.Type == Auto || spreadRadius.Value == 0)) { + return false + } + return true +} + +func (shadow *viewShadowData) String() string { + writer := newRUIWriter() + shadow.ruiString(writer) + return writer.finish() +} + +func (shadow *viewShadowData) ruiString(writer ruiWriter) { + writer.startObject("_") + for _, tag := range shadow.AllTags() { + if value := shadow.Get(tag); value != nil { + writer.writeProperty(tag, value) + } + } + writer.endObject() +} + +func (properties *propertyList) setShadow(tag string, value interface{}) bool { + + if value == nil { + delete(properties.properties, tag) + return true + } + + switch value := value.(type) { + case ViewShadow: + properties.properties[tag] = []ViewShadow{value} + + case []ViewShadow: + if len(value) == 0 { + delete(properties.properties, tag) + } else { + properties.properties[tag] = value + } + + case DataValue: + if !value.IsObject() { + return false + } + properties.properties[tag] = []ViewShadow{parseViewShadow(value.Object())} + + case []DataValue: + shadows := []ViewShadow{} + for _, data := range value { + if data.IsObject() { + shadows = append(shadows, parseViewShadow(data.Object())) + } + } + if len(shadows) == 0 { + return false + } + properties.properties[tag] = shadows + + case string: + obj := NewDataObject(value) + if obj == nil { + notCompatibleType(tag, value) + return false + } + properties.properties[tag] = []ViewShadow{parseViewShadow(obj)} + + default: + notCompatibleType(tag, value) + return false + } + + return true +} + +func getShadows(properties Properties, tag string) []ViewShadow { + if value := properties.Get(tag); value != nil { + switch value := value.(type) { + case []ViewShadow: + return value + + case ViewShadow: + return []ViewShadow{value} + } + } + return []ViewShadow{} +} + +func shadowCSS(properties Properties, tag string, session Session) string { + shadows := getShadows(properties, tag) + if len(shadows) == 0 { + return "" + } + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + lead := "" + if tag == Shadow { + for _, shadow := range shadows { + if shadow.cssStyle(buffer, session, lead) { + lead = ", " + } + } + } else { + for _, shadow := range shadows { + if shadow.cssTextStyle(buffer, session, lead) { + lead = ", " + } + } + } + return buffer.String() +} diff --git a/sizeUnit.go b/sizeUnit.go new file mode 100644 index 0000000..159201e --- /dev/null +++ b/sizeUnit.go @@ -0,0 +1,177 @@ +package rui + +import ( + "fmt" + "strconv" + "strings" +) + +// SizeUnitType : type of enumerated constants for define a type of SizeUnit value. +// +// Can take the following values: Auto, SizeInPixel, SizeInPercent, +// SizeInDIP, SizeInPt, SizeInInch, SizeInMM, SizeInFraction +type SizeUnitType uint8 + +const ( + // Auto - default value. + Auto SizeUnitType = 0 + // SizeInPixel - size in pixels. + SizeInPixel SizeUnitType = 1 + // SizeInEM - size in em. + SizeInEM SizeUnitType = 2 + // SizeInEX - size in em. + SizeInEX SizeUnitType = 3 + // SizeInPercent - size in percents of a parant size. + SizeInPercent SizeUnitType = 4 + // SizeInPt - size in pt (1/72 inch). + SizeInPt SizeUnitType = 5 + // SizeInPc - size in pc (1pc = 12pt). + SizeInPc SizeUnitType = 6 + // SizeInInch - size in inches. + SizeInInch SizeUnitType = 7 + // SizeInMM - size in millimeters. + SizeInMM SizeUnitType = 8 + // SizeInCM - size in centimeters. + SizeInCM SizeUnitType = 9 + // SizeInFraction - size in fraction. Used only for "cell-width" and "cell-height" property + SizeInFraction SizeUnitType = 10 +) + +// SizeUnit describe a size (Value field) and size unit (Type field). +type SizeUnit struct { + Type SizeUnitType + Value float64 +} + +// AutoSize creates SizeUnit with Auto type +func AutoSize() SizeUnit { + return SizeUnit{Auto, 0} +} + +// Px creates SizeUnit with SizeInPixel type +func Px(value float64) SizeUnit { + return SizeUnit{SizeInPixel, value} +} + +// Em creates SizeUnit with SizeInEM type +func Em(value float64) SizeUnit { + return SizeUnit{SizeInEM, value} +} + +// Ex creates SizeUnit with SizeInEX type +func Ex(value float64) SizeUnit { + return SizeUnit{SizeInEX, value} +} + +// Percent creates SizeUnit with SizeInDIP type +func Percent(value float64) SizeUnit { + return SizeUnit{SizeInPercent, value} +} + +// Pt creates SizeUnit with SizeInPt type +func Pt(value float64) SizeUnit { + return SizeUnit{SizeInPt, value} +} + +// Pc creates SizeUnit with SizeInPc type +func Pc(value float64) SizeUnit { + return SizeUnit{SizeInPc, value} +} + +// Mm creates SizeUnit with SizeInMM type +func Mm(value float64) SizeUnit { + return SizeUnit{SizeInMM, value} +} + +// Cm creates SizeUnit with SizeInCM type +func Cm(value float64) SizeUnit { + return SizeUnit{SizeInCM, value} +} + +// Inch creates SizeUnit with SizeInInch type +func Inch(value float64) SizeUnit { + return SizeUnit{SizeInInch, value} +} + +// Fr creates SizeUnit with SizeInFraction type +func Fr(value float64) SizeUnit { + return SizeUnit{SizeInFraction, value} +} + +// Equal compare two SizeUnit. Return true if SizeUnit are equal +func (size SizeUnit) Equal(size2 SizeUnit) bool { + return size.Type == size2.Type && (size.Type == Auto || size.Value == size2.Value) +} + +func sizeUnitSuffixes() map[SizeUnitType]string { + return map[SizeUnitType]string{ + SizeInPixel: "px", + SizeInPercent: "%", + SizeInEM: "em", + SizeInEX: "ex", + SizeInPt: "pt", + SizeInPc: "pc", + SizeInInch: "in", + SizeInMM: "mm", + SizeInCM: "cm", + SizeInFraction: "fr", + } +} + +// StringToSizeUnit converts the string argument to SizeUnit +func StringToSizeUnit(value string) (SizeUnit, bool) { + + value = strings.Trim(value, " \t\n\r") + + switch value { + case "auto", "none", "": + return SizeUnit{Type: Auto, Value: 0}, true + + case "0": + return SizeUnit{Type: SizeInPixel, Value: 0}, true + } + + suffixes := sizeUnitSuffixes() + for unitType, suffix := range suffixes { + if strings.HasSuffix(value, suffix) { + var err error + var val float64 + if val, err = strconv.ParseFloat(value[:len(value)-len(suffix)], 64); err != nil { + ErrorLog(err.Error()) + return SizeUnit{Type: Auto, Value: 0}, false + } + return SizeUnit{Type: unitType, Value: val}, true + } + } + + ErrorLog(`Invalid SizeUnit value: "` + value + `"`) + return SizeUnit{Type: Auto, Value: 0}, false +} + +// String - convert SizeUnit to string +func (size SizeUnit) String() string { + if size.Type == Auto { + return "auto" + } + if suffix, ok := sizeUnitSuffixes()[size.Type]; ok { + return fmt.Sprintf("%g%s", size.Value, suffix) + } + return strconv.FormatFloat(size.Value, 'g', -1, 64) +} + +// cssString - convert SizeUnit to string +func (size SizeUnit) cssString(textForAuto string) string { + switch size.Type { + case Auto: + return textForAuto + + case SizeInEM: + return fmt.Sprintf("%grem", size.Value) + } + + if size.Value == 0 { + return "0" + } + + return size.String() +} diff --git a/sizeUnit_test.go b/sizeUnit_test.go new file mode 100644 index 0000000..ff63079 --- /dev/null +++ b/sizeUnit_test.go @@ -0,0 +1,124 @@ +package rui + +/* +import ( + "testing" +) + +func TestSizeUnitNew(t *testing.T) { + _ = createTestSession(t) + size := SizeUnit{SizeInPixel, 10} + if Px(10) != size { + t.Error("Px(10) error") + } + + size = SizeUnit{SizeInPercent, 10} + if Percent(10) != size { + t.Error("Percent(10) error") + } + + size = SizeUnit{SizeInPt, 10} + if Pt(10) != size { + t.Error("Pt(10) error") + } + + size = SizeUnit{SizeInCM, 10} + if Cm(10) != size { + t.Error("Dip(10) error") + } + + size = SizeUnit{SizeInMM, 10} + if Mm(10) != size { + t.Error("Mm(10) error") + } + + size = SizeUnit{SizeInInch, 10} + if Inch(10) != size { + t.Error("Inch(10) error") + } +} + +func TestSizeUnitSet(t *testing.T) { + _ = createTestSession(t) + + obj := new(dataObject) + obj.SetPropertyValue("x", "20") + obj.SetPropertyValue("size", "10mm") + + size := SizeUnit{Auto, 0} + if size.setProperty(obj, "size", new(sessionData), nil) && (size.Type != SizeInMM || size.Value != 10) { + t.Errorf("result: Type = %d, Value = %g", size.Type, size.Value) + } +} + +func TestSizeUnitSetValue(t *testing.T) { + _ = createTestSession(t) + + type testData struct { + text string + size SizeUnit + } + + testValues := []testData{ + testData{"auto", SizeUnit{Auto, 0}}, + testData{"1.5em", SizeUnit{SizeInEM, 1.5}}, + testData{"2ex", SizeUnit{SizeInEX, 2}}, + testData{"20px", SizeUnit{SizeInPixel, 20}}, + testData{"100%", SizeUnit{SizeInPercent, 100}}, + testData{"14pt", SizeUnit{SizeInPt, 14}}, + testData{"10pc", SizeUnit{SizeInPc, 10}}, + testData{"0.1in", SizeUnit{SizeInInch, 0.1}}, + testData{"10mm", SizeUnit{SizeInMM, 10}}, + testData{"90.5cm", SizeUnit{SizeInCM, 90.5}}, + } + + var size SizeUnit + for _, data := range testValues { + if size.SetValue(data.text) && size != data.size { + t.Errorf("set \"%s\" result: Type = %d, Value = %g", data.text, size.Type, size.Value) + } + } + + failValues := []string{ + "xxx", + "10.10.10px", + "1000", + "5km", + } + + for _, text := range failValues { + size.SetValue(text) + } +} + +func TestSizeUnitWriteData(t *testing.T) { + _ = createTestSession(t) + type testData struct { + text string + size SizeUnit + } + + testValues := []testData{ + testData{"auto", SizeUnit{Auto, 0}}, + testData{"1.5em", SizeUnit{SizeInEM, 1.5}}, + testData{"2ex", SizeUnit{SizeInEX, 2}}, + testData{"20px", SizeUnit{SizeInPixel, 20}}, + testData{"100%", SizeUnit{SizeInPercent, 100}}, + testData{"14pt", SizeUnit{SizeInPt, 14}}, + testData{"10pc", SizeUnit{SizeInPc, 10}}, + testData{"0.1in", SizeUnit{SizeInInch, 0.1}}, + testData{"10mm", SizeUnit{SizeInMM, 10}}, + testData{"90.5cm", SizeUnit{SizeInCM, 90.5}}, + } + + buffer := new(bytes.Buffer) + for _, data := range testValues { + buffer.Reset() + buffer.WriteString(data.size.String()) + str := buffer.String() + if str != data.text { + t.Errorf("result: \"%s\", expected: \"%s\"", str, data.text) + } + } +} +*/ diff --git a/stackLayout.go b/stackLayout.go new file mode 100644 index 0000000..b9320e4 --- /dev/null +++ b/stackLayout.go @@ -0,0 +1,290 @@ +package rui + +import ( + "fmt" + "strconv" + "strings" +) + +const ( + // DefaultAnimation - default animation of StackLayout push + DefaultAnimation = 0 + // StartToEndAnimation - start to end animation of StackLayout push + StartToEndAnimation = 1 + // EndToStartAnimation - end to start animation of StackLayout push + EndToStartAnimation = 2 + // TopDownAnimation - top down animation of StackLayout push + TopDownAnimation = 3 + // BottomUpAnimation - bottom up animation of StackLayout push + BottomUpAnimation = 4 +) + +// StackLayout - list-container of View +type StackLayout interface { + ViewsContainer + Peek() View + MoveToFront(view View) bool + MoveToFrontByID(viewID string) bool + Push(view View, animation int, onPushFinished func()) + Pop(animation int, onPopFinished func(View)) bool +} + +type stackLayoutData struct { + viewsContainerData + peek uint + pushView, popView View + animationType int + onPushFinished func() + onPopFinished func(View) +} + +// NewStackLayout create new StackLayout object and return it +func NewStackLayout(session Session, params Params) StackLayout { + view := new(stackLayoutData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newStackLayout(session Session) View { + return NewStackLayout(session, nil) +} + +// Init initialize fields of ViewsContainer by default values +func (layout *stackLayoutData) Init(session Session) { + layout.viewsContainerData.Init(session) + layout.tag = "StackLayout" + layout.systemClass = "ruiStackLayout" +} + +func (layout *stackLayoutData) OnAnimationFinished(view View, tag string) { + switch tag { + case "ruiPush": + if layout.pushView != nil { + layout.pushView = nil + count := len(layout.views) + if count > 0 { + layout.peek = uint(count - 1) + } else { + layout.peek = 0 + } + updateInnerHTML(layout.htmlID(), layout.session) + } + if layout.onPushFinished != nil { + onPushFinished := layout.onPushFinished + layout.onPushFinished = nil + onPushFinished() + } + + case "ruiPop": + popView := layout.popView + layout.popView = nil + updateInnerHTML(layout.htmlID(), layout.session) + if layout.onPopFinished != nil { + onPopFinished := layout.onPopFinished + layout.onPopFinished = nil + onPopFinished(popView) + } + } +} + +func (layout *stackLayoutData) Peek() View { + if int(layout.peek) < len(layout.views) { + return layout.views[layout.peek] + } + return nil +} + +func (layout *stackLayoutData) MoveToFront(view View) bool { + peek := int(layout.peek) + htmlID := view.htmlID() + for i, view2 := range layout.views { + if view2.htmlID() == htmlID { + if i != peek { + if peek < len(layout.views) { + updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(peek), "visibility", "hidden", layout.Session()) + } + + layout.peek = uint(i) + updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(i), "visibility", "visible", layout.Session()) + } + return true + } + } + + ErrorLog(`MoveToFront() fail. Subview not found."`) + return false +} + +func (layout *stackLayoutData) MoveToFrontByID(viewID string) bool { + peek := int(layout.peek) + for i, view := range layout.views { + if view.ID() == viewID { + if i != peek { + if peek < len(layout.views) { + updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(peek), "visibility", "hidden", layout.Session()) + } + + layout.peek = uint(i) + updateCSSProperty(layout.htmlID()+"page"+strconv.Itoa(i), "visibility", "visible", layout.Session()) + } + return true + } + } + + ErrorLogF(`MoveToFront("%s") fail. Subview with "%s" not found."`, viewID, viewID) + return false +} + +func (layout *stackLayoutData) Append(view View) { + if view != nil { + layout.peek = uint(len(layout.views)) + layout.viewsContainerData.Append(view) + } else { + ErrorLog("StackLayout.Append(nil, ....) is forbidden") + } +} + +func (layout *stackLayoutData) Insert(view View, index uint) { + if view != nil { + count := uint(len(layout.views)) + if index < count { + layout.peek = index + } else { + layout.peek = count + } + layout.viewsContainerData.Insert(view, index) + } else { + ErrorLog("StackLayout.Insert(nil, ....) is forbidden") + } +} + +func (layout *stackLayoutData) RemoveView(index uint) View { + if layout.peek > 0 { + layout.peek-- + } + return layout.viewsContainerData.RemoveView(index) +} + +func (layout *stackLayoutData) Push(view View, animation int, onPushFinished func()) { + if view == nil { + ErrorLog("StackLayout.Push(nil, ....) is forbidden") + return + } + + layout.pushView = view + layout.animationType = animation + layout.animation["ruiPush"] = Animation{FinishListener: layout} + layout.onPushFinished = onPushFinished + + htmlID := layout.htmlID() + session := layout.Session() + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + buffer.WriteString(`
`) + + viewHTML(layout.pushView, buffer) + buffer.WriteString(`
`) + + appendToInnerHTML(htmlID, buffer.String(), session) + updateCSSProperty(htmlID+"push", "transform", "translate(0px, 0px)", layout.session) + + layout.views = append(layout.views, view) + view.setParentID(htmlID) +} + +func (layout *stackLayoutData) Pop(animation int, onPopFinished func(View)) bool { + count := uint(len(layout.views)) + if count == 0 || layout.peek >= count { + ErrorLog("StackLayout is empty") + return false + } + + layout.popView = layout.views[layout.peek] + layout.RemoveView(layout.peek) + + layout.animationType = animation + layout.animation["ruiPop"] = Animation{FinishListener: layout} + layout.onPopFinished = onPopFinished + + htmlID := layout.htmlID() + session := layout.Session() + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + buffer.WriteString(`
`) + viewHTML(layout.popView, buffer) + buffer.WriteString(`
`) + + appendToInnerHTML(htmlID, buffer.String(), session) + + var value string + switch layout.animationType { + case TopDownAnimation: + value = fmt.Sprintf("translate(0px, -%gpx)", layout.frame.Height) + + case BottomUpAnimation: + value = fmt.Sprintf("translate(0px, %gpx)", layout.frame.Height) + + case StartToEndAnimation: + value = fmt.Sprintf("translate(-%gpx, 0px)", layout.frame.Width) + + default: + value = fmt.Sprintf("translate(%gpx, 0px)", layout.frame.Width) + } + + updateCSSProperty(htmlID+"pop", "transform", value, layout.session) + return true +} + +func (layout *stackLayoutData) htmlSubviews(self View, buffer *strings.Builder) { + count := len(layout.views) + if count > 0 { + htmlID := layout.htmlID() + peek := int(layout.peek) + if peek >= count { + peek = count - 1 + } + + for i, view := range layout.views { + buffer.WriteString(``) + } + } +} diff --git a/strings.go b/strings.go new file mode 100644 index 0000000..f516902 --- /dev/null +++ b/strings.go @@ -0,0 +1,128 @@ +package rui + +import ( + "embed" + "io/ioutil" + "path/filepath" + "strings" +) + +var stringResources = map[string]map[string]string{} + +func scanEmbedStringsDir(fs *embed.FS, dir string) { + if files, err := fs.ReadDir(dir); err == nil { + for _, file := range files { + name := file.Name() + path := dir + "/" + name + if file.IsDir() { + scanEmbedStringsDir(fs, path) + } else if strings.ToLower(filepath.Ext(name)) == ".rui" { + if data, err := fs.ReadFile(path); err == nil { + loadStringResources(string(data)) + } else { + ErrorLog(err.Error()) + } + } + } + } +} + +func scanStringsDir(path string) { + if files, err := ioutil.ReadDir(path); err == nil { + for _, file := range files { + filename := file.Name() + if filename[0] != '.' { + newPath := path + `/` + filename + if file.IsDir() { + scanStringsDir(newPath) + } else if strings.ToLower(filepath.Ext(newPath)) == ".rui" { + if data, err := ioutil.ReadFile(newPath); err == nil { + loadStringResources(string(data)) + } else { + ErrorLog(err.Error()) + } + } + } + } + } else { + ErrorLog(err.Error()) + } +} + +func loadStringResources(text string) { + data := ParseDataText(text) + if data == nil { + return + } + + parseStrings := func(obj DataObject, lang string) { + table, ok := stringResources[lang] + if !ok { + table = map[string]string{} + } + + for i := 0; i < obj.PropertyCount(); i++ { + if prop := obj.Property(i); prop != nil && prop.Type() == TextNode { + table[prop.Tag()] = prop.Text() + } + } + + stringResources[lang] = table + } + + tag := data.Tag() + if tag == "strings" { + for i := 0; i < data.PropertyCount(); i++ { + if prop := data.Property(i); prop != nil && prop.Type() == ObjectNode { + parseStrings(prop.Object(), prop.Tag()) + } + } + + } else if strings.HasPrefix(tag, "strings:") { + if lang := tag[8:]; lang != "" { + parseStrings(data, lang) + } + } +} + +// GetString returns the text for the language which is defined by "lang" parameter +func GetString(tag, lang string) (string, bool) { + if table, ok := stringResources[lang]; ok { + if text, ok := table[tag]; ok { + return text, true + } + DebugLogF(`There is no "%s" string resource`, tag) + } + DebugLogF(`There are no "%s" language resources`, lang) + return tag, false +} + +func (session *sessionData) GetString(tag string) (string, bool) { + getString := func(tag, lang string) (string, bool) { + if table, ok := stringResources[lang]; ok { + if text, ok := table[tag]; ok { + return text, true + } + DebugLogF(`There is no "%s" string in "%s" resources`, tag, lang) + } + return tag, false + } + + if session.language != "" { + if text, ok := getString(tag, session.language); ok { + return text, true + } + } + + if session.languages != nil { + for _, lang := range session.languages { + if lang != session.language { + if text, ok := getString(tag, lang); ok { + return text, true + } + } + } + } + + return tag, false +} diff --git a/tableAdapter.go b/tableAdapter.go new file mode 100644 index 0000000..a32c36a --- /dev/null +++ b/tableAdapter.go @@ -0,0 +1,331 @@ +package rui + +type TableAdapter interface { + RowCount() int + ColumnCount() int + Cell(row, column int) interface{} +} + +type TableColumnStyle interface { + ColumnStyle(column int) Params +} + +type TableRowStyle interface { + RowStyle(row int) Params +} + +type TableCellStyle interface { + CellStyle(row, column int) Params +} + +type SimpleTableAdapter interface { + TableAdapter + TableCellStyle +} + +type simpleTableAdapter struct { + content [][]interface{} + columnCount int +} + +type TextTableAdapter interface { + TableAdapter +} + +type textTableAdapter struct { + content [][]string + columnCount int +} + +type VerticalTableJoin struct { +} + +type HorizontalTableJoin struct { +} + +func NewSimpleTableAdapter(content [][]interface{}) SimpleTableAdapter { + if content == nil { + return nil + } + + adapter := new(simpleTableAdapter) + adapter.content = content + adapter.columnCount = 0 + for _, row := range content { + if row != nil { + columnCount := len(row) + if adapter.columnCount < columnCount { + adapter.columnCount = columnCount + } + } + } + + return adapter +} + +func (adapter *simpleTableAdapter) RowCount() int { + if adapter.content != nil { + return len(adapter.content) + } + return 0 +} + +func (adapter *simpleTableAdapter) ColumnCount() int { + return adapter.columnCount +} + +func (adapter *simpleTableAdapter) Cell(row, column int) interface{} { + if adapter.content != nil && row >= 0 && row < len(adapter.content) && + adapter.content[row] != nil && column >= 0 && column < len(adapter.content[row]) { + return adapter.content[row][column] + } + return nil +} + +func (adapter *simpleTableAdapter) CellStyle(row, column int) Params { + if adapter.content == nil { + return nil + } + + getColumnSpan := func() int { + count := 0 + for i := column + 1; i < adapter.columnCount; i++ { + next := adapter.Cell(row, i) + switch next.(type) { + case HorizontalTableJoin: + count++ + + default: + return count + } + } + return count + } + + getRowSpan := func() int { + rowCount := len(adapter.content) + count := 0 + for i := row + 1; i < rowCount; i++ { + next := adapter.Cell(i, column) + switch next.(type) { + case VerticalTableJoin: + count++ + + default: + return count + } + } + return count + } + + columnSpan := getColumnSpan() + rowSpan := getRowSpan() + + var params Params = nil + if rowSpan > 0 { + params = Params{RowSpan: rowSpan + 1} + } + + if columnSpan > 0 { + if params == nil { + params = Params{ColumnSpan: columnSpan + 1} + } else { + params[ColumnSpan] = columnSpan + } + } + + return params +} + +func NewTextTableAdapter(content [][]string) TextTableAdapter { + if content == nil { + return nil + } + + adapter := new(textTableAdapter) + adapter.content = content + adapter.columnCount = 0 + for _, row := range content { + if row != nil { + columnCount := len(row) + if adapter.columnCount < columnCount { + adapter.columnCount = columnCount + } + } + } + + return adapter +} + +func (adapter *textTableAdapter) RowCount() int { + if adapter.content != nil { + return len(adapter.content) + } + return 0 +} + +func (adapter *textTableAdapter) ColumnCount() int { + return adapter.columnCount +} + +func (adapter *textTableAdapter) Cell(row, column int) interface{} { + if adapter.content != nil && row >= 0 && row < len(adapter.content) && + adapter.content[row] != nil && column >= 0 && column < len(adapter.content[row]) { + return adapter.content[row][column] + } + return nil +} + +type simpleTableRowStyle struct { + params []Params +} + +func (style *simpleTableRowStyle) RowStyle(row int) Params { + if row < len(style.params) { + params := style.params[row] + if len(params) > 0 { + return params + } + } + return nil +} + +func (table *tableViewData) setRowStyle(value interface{}) bool { + newSimpleTableRowStyle := func(params []Params) TableRowStyle { + if len(params) == 0 { + return nil + } + result := new(simpleTableRowStyle) + result.params = params + return result + } + + switch value := value.(type) { + case TableRowStyle: + table.properties[RowStyle] = value + + case []Params: + if style := newSimpleTableRowStyle(value); style != nil { + table.properties[RowStyle] = style + } else { + delete(table.properties, RowStyle) + } + + case DataNode: + if value.Type() == ArrayNode { + params := make([]Params, value.ArraySize()) + for i, element := range value.ArrayElements() { + params[i] = Params{} + if element.IsObject() { + obj := element.Object() + for k := 0; k < obj.PropertyCount(); k++ { + if prop := obj.Property(k); prop != nil && prop.Type() == TextNode { + params[i][prop.Tag()] = prop.Text() + } + } + } else { + params[i][Style] = element.Value() + } + } + if style := newSimpleTableRowStyle(params); style != nil { + table.properties[RowStyle] = style + } else { + delete(table.properties, RowStyle) + } + } else { + return false + } + + default: + return false + } + return true +} + +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 +} + +type simpleTableColumnStyle struct { + params []Params +} + +func (style *simpleTableColumnStyle) ColumnStyle(row int) Params { + if row < len(style.params) { + params := style.params[row] + if len(params) > 0 { + return params + } + } + return nil +} + +func (table *tableViewData) setColumnStyle(value interface{}) bool { + newSimpleTableColumnStyle := func(params []Params) TableColumnStyle { + if len(params) == 0 { + return nil + } + result := new(simpleTableColumnStyle) + result.params = params + return result + } + + switch value := value.(type) { + case TableColumnStyle: + table.properties[ColumnStyle] = value + + case []Params: + if style := newSimpleTableColumnStyle(value); style != nil { + table.properties[ColumnStyle] = style + } else { + delete(table.properties, ColumnStyle) + } + + case DataNode: + if value.Type() == ArrayNode { + params := make([]Params, value.ArraySize()) + for i, element := range value.ArrayElements() { + params[i] = Params{} + if element.IsObject() { + obj := element.Object() + for k := 0; k < obj.PropertyCount(); k++ { + if prop := obj.Property(k); prop != nil && prop.Type() == TextNode { + params[i][prop.Tag()] = prop.Text() + } + } + } else { + params[i][Style] = element.Value() + } + } + if style := newSimpleTableColumnStyle(params); style != nil { + table.properties[ColumnStyle] = style + } else { + delete(table.properties, ColumnStyle) + } + } else { + return false + } + + default: + return false + } + return true +} + +func (table *tableViewData) getColumnStyle() TableColumnStyle { + for _, tag := range []string{ColumnStyle, Content} { + if value := table.getRaw(tag); value != nil { + if style, ok := value.(TableColumnStyle); ok { + return style + } + } + } + return nil +} diff --git a/tableView.go b/tableView.go new file mode 100644 index 0000000..e531afa --- /dev/null +++ b/tableView.go @@ -0,0 +1,842 @@ +package rui + +import ( + "fmt" + "strconv" + "strings" +) + +const ( + // TableVerticalAlign is the constant for the "table-vertical-align" property tag. + // The "table-vertical-align" int property sets the vertical alignment of the content inside a table cell. + // Valid values are LeftAlign (0), RightAlign (1), CenterAlign (2), and BaselineAlign (3, 4) + TableVerticalAlign = "table-vertical-align" + // HeadHeight is the constant for the "head-height" property tag. + // The "head-height" int property sets the number of rows in the table header. + // The default value is 0 (no header) + HeadHeight = "head-height" + // HeadStyle is the constant for the "head-style" property tag. + // The "head-style" string property sets the header style name + HeadStyle = "head-style" + // FootHeight is the constant for the "foot-height" property tag. + // The "foot-height" int property sets the number of rows in the table footer. + // The default value is 0 (no footer) + FootHeight = "foot-height" + // FootStyle is the constant for the "foot-style" property tag. + // The "foot-style" string property sets the footer style name + FootStyle = "foot-style" + // RowSpan is the constant for the "row-span" property tag. + // The "row-span" int property sets the number of table row to span. + // Used only when specifying cell parameters in the implementation of TableCellStyle + RowSpan = "row-span" + // ColumnSpan is the constant for the "column-span" property tag. + // The "column-span" int property sets the number of table column to span. + // Used only when specifying cell parameters in the implementation of TableCellStyle + ColumnSpan = "column-span" + // RowStyle is the constant for the "row-style" property tag. + // The "row-style" property sets the adapter which specifies styles of each table row. + // This property can be assigned or by an implementation of TableRowStyle interface, or by an array of Params. + RowStyle = "row-style" + // ColumnStyle is the constant for the "column-style" property tag. + // The "column-style" property sets the adapter which specifies styles of each table column. + // This property can be assigned or by an implementation of TableColumnStyle interface, or by an array of Params. + ColumnStyle = "column-style" + // CellStyle is the constant for the "cell-style" property tag. + // The "cell-style" property sets the adapter which specifies styles of each table cell. + // This property can be assigned only by an implementation of TableCellStyle interface. + CellStyle = "cell-style" + // CellPadding is the constant for the "cell-padding" property tag. + // The "cell-padding" Bounds property sets the padding area on all four sides of a table call at once. + // An element's padding area is the space between its content and its border. + CellPadding = "cell-padding" + // CellPaddingLeft is the constant for the "cell-padding-left" property tag. + // The "cell-padding-left" SizeUnit property sets the width of the padding area to the left of a cell content. + // An element's padding area is the space between its content and its border. + CellPaddingLeft = "cell-padding-left" + // CellPaddingRight is the constant for the "cell-padding-right" property tag. + // The "cell-padding-right" SizeUnit property sets the width of the padding area to the left of a cell content. + // An element's padding area is the space between its content and its border. + CellPaddingRight = "cell-padding-right" + // CellPaddingTop is the constant for the "cell-padding-top" property tag. + // The "cell-padding-top" SizeUnit property sets the height of the padding area to the top of a cell content. + // An element's padding area is the space between its content and its border. + CellPaddingTop = "cell-padding-top" + // CellPaddingBottom is the constant for the "cell-padding-bottom" property tag. + // The "cell-padding-bottom" SizeUnit property sets the height of the padding area to the bottom of a cell content. + CellPaddingBottom = "cell-padding-bottom" + // CellBorder is the constant for the "cell-border" property tag. + // The "cell-border" property sets a table cell's border. It sets the values of a border width, style, and color. + // This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation. + CellBorder = "cell-border" + // CellBorderLeft is the constant for the "cell-border-left" property tag. + // The "cell-border-left" property sets a view's left border. It sets the values of a border width, style, and color. + // This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation. + CellBorderLeft = "cell-border-left" + // CellBorderRight is the constant for the "cell-border-right" property tag. + // The "cell-border-right" property sets a view's right border. It sets the values of a border width, style, and color. + // This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation. + CellBorderRight = "cell-border-right" + // CellBorderTop is the constant for the "cell-border-top" property tag. + // The "cell-border-top" property sets a view's top border. It sets the values of a border width, style, and color. + // This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation. + CellBorderTop = "cell-border-top" + // CellBorderBottom is the constant for the "cell-border-bottom" property tag. + // The "cell-border-bottom" property sets a view's bottom border. It sets the values of a border width, style, and color. + // This property can be assigned a value of BorderProperty type, or ViewBorder type, or BorderProperty text representation. + CellBorderBottom = "cell-border-bottom" + // CellBorderStyle is the constant for the "cell-border-style" property tag. + // The "cell-border-style" int property sets the line style for all four sides of a table cell's border. + // Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4). + CellBorderStyle = "cell-border-style" + // CellBorderLeftStyle is the constant for the "cell-border-left-style" property tag. + // The "cell-border-left-style" int property sets the line style of a table cell's left border. + // Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4). + CellBorderLeftStyle = "cell-border-left-style" + // CellBorderRightStyle is the constant for the "cell-border-right-style" property tag. + // The "cell-border-right-style" int property sets the line style of a table cell's right border. + // Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4). + CellBorderRightStyle = "cell-border-right-style" + // CellBorderTopStyle is the constant for the "cell-border-top-style" property tag. + // The "cell-border-top-style" int property sets the line style of a table cell's top border. + // Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4). + CellBorderTopStyle = "cell-border-top-style" + // CellBorderBottomStyle is the constant for the "cell-border-bottom-style" property tag. + // The "cell-border-bottom-style" int property sets the line style of a table cell's bottom border. + // Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4). + CellBorderBottomStyle = "cell-border-bottom-style" + // CellBorderWidth is the constant for the "cell-border-width" property tag. + // The "cell-border-width" property sets the line width for all four sides of a table cell's border. + CellBorderWidth = "cell-border-width" + // CellBorderLeftWidth is the constant for the "cell-border-left-width" property tag. + // The "cell-border-left-width" SizeUnit property sets the line width of a table cell's left border. + CellBorderLeftWidth = "cell-border-left-width" + // CellBorderRightWidth is the constant for the "cell-border-right-width" property tag. + // The "cell-border-right-width" SizeUnit property sets the line width of a table cell's right border. + CellBorderRightWidth = "cell-border-right-width" + // CellBorderTopWidth is the constant for the "cell-border-top-width" property tag. + // The "cell-border-top-width" SizeUnit property sets the line width of a table cell's top border. + CellBorderTopWidth = "cell-border-top-width" + // CellBorderBottomWidth is the constant for the "cell-border-bottom-width" property tag. + // The "cell-border-bottom-width" SizeUnit property sets the line width of a table cell's bottom border. + CellBorderBottomWidth = "cell-border-bottom-width" + // CellBorderColor is the constant for the "cell-border-color" property tag. + // The "cell-border-color" property sets the line color for all four sides of a table cell's border. + CellBorderColor = "cell-border-color" + // CellBorderLeftColor is the constant for the "cell-border-left-color" property tag. + // The "cell-border-left-color" property sets the line color of a table cell's left border. + CellBorderLeftColor = "cell-border-left-color" + // CellBorderRightColor is the constant for the "cell-border-right-color" property tag. + // The "cell-border-right-color" property sets the line color of a table cell's right border. + CellBorderRightColor = "cell-border-right-color" + // CellBorderTopColor is the constant for the "cell-border-top-color" property tag. + // The "cell-border-top-color" property sets the line color of a table cell's top border. + CellBorderTopColor = "cell-border-top-color" + // CellBorderBottomColor is the constant for the "cell-border-bottom-color" property tag. + // The "cell-border-bottom-color" property sets the line color of a table cell's bottom border. + CellBorderBottomColor = "cell-border-bottom-color" +) + +// TableView - text View +type TableView interface { + View + ReloadTableData() +} + +type tableViewData struct { + viewData +} + +type tableCellView struct { + viewData +} + +// NewTableView create new TableView object and return it +func NewTableView(session Session, params Params) TableView { + view := new(tableViewData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newTableView(session Session) View { + return NewTableView(session, nil) +} + +// Init initialize fields of TableView by default values +func (table *tableViewData) Init(session Session) { + table.viewData.Init(session) + table.tag = "TableView" +} + +func (table *tableViewData) Get(tag string) interface{} { + return table.get(strings.ToLower(tag)) +} + +func (table *tableViewData) Remove(tag string) { + table.remove(strings.ToLower(tag)) +} + +func (table *tableViewData) remove(tag string) { + switch tag { + + case CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft, + "top-cell-padding", "right-cell-padding", "bottom-cell-padding", "left-cell-padding": + table.removeBoundsSide(CellPadding, tag) + + case Gap, CellBorder, CellPadding, RowStyle, ColumnStyle, CellStyle, + HeadHeight, HeadStyle, FootHeight, FootStyle: + delete(table.properties, tag) + + default: + table.viewData.remove(tag) + return + } + + table.propertyChanged(tag) +} + +func (table *tableViewData) Set(tag string, value interface{}) bool { + return table.set(strings.ToLower(tag), value) +} + +func (table *tableViewData) set(tag string, value interface{}) bool { + if value == nil { + table.remove(tag) + return true + } + + switch tag { + case Content: + switch val := value.(type) { + case TableAdapter: + table.properties[Content] = value + + case [][]interface{}: + table.properties[Content] = NewSimpleTableAdapter(val) + + case [][]string: + table.properties[Content] = NewTextTableAdapter(val) + + default: + notCompatibleType(tag, value) + return false + } + + case CellStyle: + if style, ok := value.(TableCellStyle); ok { + table.properties[tag] = style + } else { + notCompatibleType(tag, value) + return false + } + + case RowStyle: + if !table.setRowStyle(value) { + notCompatibleType(tag, value) + return false + } + + case ColumnStyle: + if !table.setColumnStyle(value) { + notCompatibleType(tag, value) + return false + } + + case HeadHeight, FootHeight: + switch value := value.(type) { + case string: + if isConstantName(value) { + table.properties[tag] = value + } else if n, err := strconv.Atoi(value); err == nil { + table.properties[tag] = n + } else { + ErrorLog(err.Error()) + notCompatibleType(tag, value) + return false + } + + default: + if n, ok := isInt(value); ok { + table.properties[tag] = n + } + } + + case HeadStyle, FootStyle: + switch value := value.(type) { + case string: + table.properties[tag] = value + + case Params: + if len(value) > 0 { + table.properties[tag] = value + } else { + delete(table.properties, tag) + } + + case DataNode: + switch value.Type() { + case ObjectNode: + obj := value.Object() + params := Params{} + for k := 0; k < obj.PropertyCount(); k++ { + if prop := obj.Property(k); prop != nil && prop.Type() == TextNode { + params[prop.Tag()] = prop.Text() + } + } + if len(params) > 0 { + table.properties[tag] = params + } else { + delete(table.properties, tag) + } + + case TextNode: + table.properties[tag] = value.Text() + + default: + notCompatibleType(tag, value) + return false + } + + default: + notCompatibleType(tag, value) + return false + } + + case CellPadding: + if !table.setBounds(tag, value) { + return false + } + + case CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft, + "top-cell-padding", "right-cell-padding", "bottom-cell-padding", "left-cell-padding": + if !table.setBoundsSide(CellPadding, tag, value) { + return false + } + + case Gap: + if !table.setSizeProperty(Gap, value) { + return false + } + + case CellBorder, CellBorderStyle, CellBorderColor, CellBorderWidth, + CellBorderLeft, CellBorderLeftStyle, CellBorderLeftColor, CellBorderLeftWidth, + CellBorderRight, CellBorderRightStyle, CellBorderRightColor, CellBorderRightWidth, + CellBorderTop, CellBorderTopStyle, CellBorderTopColor, CellBorderTopWidth, + CellBorderBottom, CellBorderBottomStyle, CellBorderBottomColor, CellBorderBottomWidth: + if !table.viewData.set(tag, value) { + return false + } + + default: + return table.viewData.set(tag, value) + } + + table.propertyChanged(tag) + return true +} + +func (table *tableViewData) propertyChanged(tag string) { + switch tag { + case Content, RowStyle, ColumnStyle, CellStyle, CellPadding, CellBorder, + HeadHeight, HeadStyle, FootHeight, FootStyle, + CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft, + "top-cell-padding", "right-cell-padding", "bottom-cell-padding", "left-cell-padding": + table.ReloadTableData() + + case Gap: + htmlID := table.htmlID() + session := table.Session() + gap, ok := sizeProperty(table, Gap, session) + if !ok || gap.Type == Auto || gap.Value <= 0 { + updateCSSProperty(htmlID, "border-spacing", "0", session) + updateCSSProperty(htmlID, "border-collapse", "collapse", session) + } else { + updateCSSProperty(htmlID, "border-spacing", gap.cssString("0"), session) + updateCSSProperty(htmlID, "border-collapse", "separate", session) + } + + } +} + +func (table *tableViewData) htmlTag() string { + return "table" +} + +func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) { + content := table.getRaw(Content) + if content == nil { + return + } + + adapter, ok := content.(TableAdapter) + if !ok { + return + } + + rowCount := adapter.RowCount() + columnCount := adapter.ColumnCount() + if rowCount == 0 || columnCount == 0 { + return + } + + rowStyle := table.getRowStyle() + + var cellStyle1 TableCellStyle = nil + if style, ok := content.(TableCellStyle); ok { + cellStyle1 = style + } + + var cellStyle2 TableCellStyle = nil + if value := table.getRaw(CellStyle); value != nil { + if style, ok := value.(TableCellStyle); ok { + cellStyle2 = style + } + } + + session := table.Session() + + if !session.ignoreViewUpdates() { + session.setIgnoreViewUpdates(true) + defer session.setIgnoreViewUpdates(false) + } + + var cssBuilder viewCSSBuilder + cssBuilder.buffer = allocStringBuilder() + defer freeStringBuilder(cssBuilder.buffer) + + var view tableCellView + view.Init(session) + + ignorCells := []struct{ row, column int }{} + + tableCSS := func(startRow, endRow int, cellTag string, cellBorder BorderProperty, cellPadding BoundsProperty) { + for row := startRow; row < endRow; row++ { + + cssBuilder.buffer.Reset() + if rowStyle != nil { + if styles := rowStyle.RowStyle(row); styles != nil { + view.Clear() + for tag, value := range styles { + view.Set(tag, value) + } + view.cssStyle(&view, &cssBuilder) + } + } + + if cssBuilder.buffer.Len() > 0 { + buffer.WriteString(``) + } else { + buffer.WriteString("") + } + + for column := 0; column < columnCount; column++ { + ignore := false + for _, cell := range ignorCells { + if cell.row == row && cell.column == column { + ignore = true + break + } + } + + if !ignore { + rowSpan := 0 + columnSpan := 0 + + cssBuilder.buffer.Reset() + view.Clear() + + if cellBorder != nil { + view.set(Border, cellBorder) + } + + if cellPadding != nil { + view.set(Padding, cellPadding) + } + + appendFrom := func(cellStyle TableCellStyle) { + if cellStyle != nil { + if styles := cellStyle.CellStyle(row, column); styles != nil { + for tag, value := range styles { + valueToInt := func() int { + switch value := value.(type) { + case int: + return value + + case string: + if value, ok = session.resolveConstants(value); ok { + if n, err := strconv.Atoi(value); err == nil { + return n + } + } + } + return 0 + } + + switch tag = strings.ToLower(tag); tag { + case RowSpan: + rowSpan = valueToInt() + + case ColumnSpan: + columnSpan = valueToInt() + + default: + view.set(tag, value) + } + } + } + } + } + appendFrom(cellStyle1) + appendFrom(cellStyle2) + + if len(view.properties) > 0 { + view.cssStyle(&view, &cssBuilder) + } + + buffer.WriteRune('<') + buffer.WriteString(cellTag) + + if columnSpan > 1 { + buffer.WriteString(` colspan="`) + buffer.WriteString(strconv.Itoa(columnSpan)) + buffer.WriteRune('"') + for c := column + 1; c < column+columnSpan; c++ { + ignorCells = append(ignorCells, struct { + row int + column int + }{row: row, column: c}) + } + } + + if rowSpan > 1 { + buffer.WriteString(` rowspan="`) + buffer.WriteString(strconv.Itoa(rowSpan)) + buffer.WriteRune('"') + if columnSpan < 1 { + columnSpan = 1 + } + for r := row + 1; r < row+rowSpan; r++ { + for c := column; c < column+columnSpan; c++ { + ignorCells = append(ignorCells, struct { + row int + column int + }{row: r, column: c}) + } + } + } + + if cssBuilder.buffer.Len() > 0 { + buffer.WriteString(` style="`) + buffer.WriteString(cssBuilder.buffer.String()) + buffer.WriteRune('"') + } + buffer.WriteRune('>') + + switch value := adapter.Cell(row, column).(type) { + case string: + buffer.WriteString(value) + + case View: + viewHTML(value, buffer) + + case Color: + buffer.WriteString(`
    
`) + buffer.WriteString(value.String()) + + case fmt.Stringer: + buffer.WriteString(value.String()) + + case rune: + buffer.WriteRune(value) + + case float32: + buffer.WriteString(fmt.Sprintf("%g", float64(value))) + + case float64: + buffer.WriteString(fmt.Sprintf("%g", value)) + + case bool: + if value { + buffer.WriteString(session.checkboxOnImage()) + } else { + buffer.WriteString(session.checkboxOffImage()) + } + + default: + if n, ok := isInt(value); ok { + buffer.WriteString(fmt.Sprintf("%d", n)) + } else { + buffer.WriteString("") + } + } + + buffer.WriteString(`') + } + } + + buffer.WriteString("") + } + } + + if columnStyle := table.getColumnStyle(); columnStyle != nil { + buffer.WriteString("") + for column := 0; column < columnCount; column++ { + cssBuilder.buffer.Reset() + if styles := columnStyle.ColumnStyle(column); styles != nil { + view.Clear() + for tag, value := range styles { + view.Set(tag, value) + } + view.cssStyle(&view, &cssBuilder) + } + + if cssBuilder.buffer.Len() > 0 { + buffer.WriteString(``) + } else { + buffer.WriteString("") + } + } + buffer.WriteString("") + } + + headHeight, _ := intProperty(table, HeadHeight, table.Session(), 0) + footHeight, _ := intProperty(table, FootHeight, table.Session(), 0) + cellBorder := table.getCellBorder() + cellPadding := table.boundsProperty(CellPadding) + if cellPadding == nil { + if style, ok := stringProperty(table, Style, table.Session()); ok { + if style, ok := table.Session().resolveConstants(style); ok { + cellPadding = table.cellPaddingFromStyle(style) + } + } + } + + headFootStart := func(htmlTag, styleTag string) (BorderProperty, BoundsProperty) { + buffer.WriteRune('<') + buffer.WriteString(htmlTag) + if value := table.getRaw(styleTag); value != nil { + switch value := value.(type) { + case string: + if style, ok := session.resolveConstants(value); ok { + buffer.WriteString(` class="`) + buffer.WriteString(style) + buffer.WriteString(`">`) + return table.cellBorderFromStyle(style), table.cellPaddingFromStyle(style) + } + + case Params: + cssBuilder.buffer.Reset() + view.Clear() + for tag, val := range value { + view.Set(tag, val) + } + + var border BorderProperty = nil + if value := view.Get(CellBorder); value != nil { + border = value.(BorderProperty) + } + var padding BoundsProperty = nil + if value := view.Get(CellPadding); value != nil { + switch value := value.(type) { + case SizeUnit: + padding = NewBoundsProperty(Params{ + Top: value, + Right: value, + Bottom: value, + Left: value, + }) + + case BoundsProperty: + padding = value + } + } + + view.cssStyle(&view, &cssBuilder) + if cssBuilder.buffer.Len() > 0 { + buffer.WriteString(` style="`) + buffer.WriteString(cssBuilder.buffer.String()) + buffer.WriteString(`"`) + } + buffer.WriteRune('>') + return border, padding + } + } + buffer.WriteRune('>') + return nil, nil + } + + if headHeight > 0 { + headCellBorder := cellBorder + headCellPadding := cellPadding + + if headHeight > rowCount { + headHeight = rowCount + } + + border, padding := headFootStart("thead", HeadStyle) + if border != nil { + headCellBorder = border + } + if padding != nil { + headCellPadding = padding + } + tableCSS(0, headHeight, "th", headCellBorder, headCellPadding) + buffer.WriteString("") + } + + if footHeight > rowCount-headHeight { + footHeight = rowCount - headHeight + } + + if rowCount > footHeight+headHeight { + buffer.WriteString("") + tableCSS(headHeight, rowCount-footHeight, "td", cellBorder, cellPadding) + buffer.WriteString("") + } + + if footHeight > 0 { + footCellBorder := cellBorder + footCellPadding := cellPadding + + border, padding := headFootStart("tfoot", FootStyle) + if border != nil { + footCellBorder = border + } + if padding != nil { + footCellPadding = padding + } + tableCSS(rowCount-footHeight, rowCount, "td", footCellBorder, footCellPadding) + buffer.WriteString("") + } +} + +func (table *tableViewData) cellPaddingFromStyle(style string) BoundsProperty { + session := table.Session() + var result BoundsProperty = nil + + if node := session.stylePropertyNode(style, CellPadding); node != nil && node.Type() == ObjectNode { + for _, tag := range []string{Left, Right, Top, Bottom} { + if node := node.Object().PropertyWithTag(tag); node != nil && node.Type() == TextNode { + if result == nil { + result = NewBoundsProperty(nil) + } + result.Set(tag, node.Text()) + } + } + } + + for _, tag := range []string{CellPaddingLeft, CellPaddingRight, CellPaddingTop, CellPaddingBottom} { + if value, ok := session.styleProperty(style, CellPadding); ok { + if result == nil { + result = NewBoundsProperty(nil) + } + result.Set(tag, value) + } + } + + return result +} + +func (table *tableViewData) cellBorderFromStyle(style string) BorderProperty { + + border := new(borderProperty) + border.properties = map[string]interface{}{} + + session := table.Session() + if node := session.stylePropertyNode(style, CellBorder); node != nil && node.Type() == ObjectNode { + border.setBorderObject(node.Object()) + } + + for _, tag := range []string{ + CellBorderLeft, + CellBorderRight, + CellBorderTop, + CellBorderBottom, + CellBorderStyle, + CellBorderLeftStyle, + CellBorderRightStyle, + CellBorderTopStyle, + CellBorderBottomStyle, + CellBorderWidth, + CellBorderLeftWidth, + CellBorderRightWidth, + CellBorderTopWidth, + CellBorderBottomWidth, + CellBorderColor, + CellBorderLeftColor, + CellBorderRightColor, + CellBorderTopColor, + CellBorderBottomColor, + } { + if value, ok := session.styleProperty(style, tag); ok { + border.Set(tag, value) + } + } + + if len(border.properties) == 0 { + return nil + } + return border +} + +func (table *tableViewData) getCellBorder() BorderProperty { + if value := table.getRaw(CellBorder); value != nil { + if border, ok := value.(BorderProperty); ok { + return border + } + } + + if style, ok := stringProperty(table, Style, table.Session()); ok { + if style, ok := table.Session().resolveConstants(style); ok { + return table.cellBorderFromStyle(style) + } + } + + return nil +} + +func (table *tableViewData) cssStyle(self View, builder cssBuilder) { + table.viewData.cssViewStyle(builder, table.Session(), self) + + gap, ok := sizeProperty(table, Gap, table.Session()) + if !ok || gap.Type == Auto || gap.Value <= 0 { + builder.add("border-spacing", "0") + builder.add("border-collapse", "collapse") + } else { + builder.add("border-spacing", gap.cssString("0")) + builder.add("border-collapse", "separate") + } +} + +func (table *tableViewData) ReloadTableData() { + updateInnerHTML(table.htmlID(), table.Session()) +} + +func (cell *tableCellView) Set(tag string, value interface{}) bool { + return cell.set(strings.ToLower(tag), value) +} + +func (cell *tableCellView) set(tag string, value interface{}) bool { + switch tag { + case VerticalAlign: + tag = TableVerticalAlign + } + return cell.viewData.set(tag, value) +} + +func (cell *tableCellView) cssStyle(self View, builder cssBuilder) { + session := cell.Session() + cell.viewData.cssViewStyle(builder, session, self) + + if value, ok := enumProperty(cell, TableVerticalAlign, session, 0); ok { + builder.add("vertical-align", enumProperties[TableVerticalAlign].values[value]) + } +} diff --git a/tabsLayout.go b/tabsLayout.go new file mode 100644 index 0000000..2b229e0 --- /dev/null +++ b/tabsLayout.go @@ -0,0 +1,490 @@ +package rui + +import ( + "fmt" + "strconv" + "strings" +) + +const ( + // HiddenTabs - tabs of TabsLayout are hidden + HiddenTabs = 0 + // TopTabs - tabs of TabsLayout are on the top + TopTabs = 1 + // BottomTabs - tabs of TabsLayout are on the bottom + BottomTabs = 2 + // LeftTabs - tabs of TabsLayout are on the left + LeftTabs = 3 + // RightTabs - tabs of TabsLayout are on the right + RightTabs = 4 + // LeftListTabs - tabs of TabsLayout are on the left + LeftListTabs = 5 + // RightListTabs - tabs of TabsLayout are on the right + RightListTabs = 6 +) + +// TabsLayoutCurrentChangedListener - listener of the current tab changing +type TabsLayoutCurrentChangedListener interface { + OnTabsLayoutCurrentChanged(tabsLayout TabsLayout, newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View) +} + +type tabsLayoutCurrentChangedListenerFunc struct { + listenerFunc func(tabsLayout TabsLayout, newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View) +} + +func (listener *tabsLayoutCurrentChangedListenerFunc) OnTabsLayoutCurrentChanged(tabsLayout TabsLayout, + newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View) { + if listener.listenerFunc != nil { + listener.listenerFunc(tabsLayout, newCurrent, newCurrentView, oldCurrent, oldCurrentView) + } +} + +// TabsLayout - multi-tab container of View +type TabsLayout interface { + ViewsContainer + /* + // Current return the index of active tab + currentItem() int + // SetCurrent set the index of active tab + SetCurrent(current int) + // TabsLocation return the location of tabs. It returns one of the following values: HiddenTabs (0), + // TopTabs (1), BottomTabs (2), LeftTabs (3), RightTabs (4), LeftListTabs (5), RightListTabs (6) + tabsLocation() int + // TabsLocation set the location of tabs. Valid values: HiddenTabs (0), TopTabs (1), + // BottomTabs (2), LeftTabs (3), RightTabs (4), LeftListTabs (5), RightListTabs (6) + SetTabsLocation(location int) + // TabStyle() return styles of tab in the passive and the active state + TabStyle() (string, string) + SetTabStyle(tabStyle string, activeTabStyle string) + */ + // SetCurrentTabChangedListener add the listener of the current tab changing + SetCurrentTabChangedListener(listener TabsLayoutCurrentChangedListener) + // SetCurrentTabChangedListener add the listener function of the current tab changing + SetCurrentTabChangedListenerFunc(listenerFunc func(tabsLayout TabsLayout, + newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View)) +} + +type tabsLayoutData struct { + viewsContainerData + //currentTab, tabsLocation int + //tabStyle, activeTabStyle string + tabListener TabsLayoutCurrentChangedListener +} + +// NewTabsLayout create new TabsLayout object and return it +func NewTabsLayout(session Session) TabsLayout { + view := new(tabsLayoutData) + view.Init(session) + return view +} + +func newTabsLayout(session Session) View { + return NewTabsLayout(session) +} + +// Init initialize fields of ViewsContainer by default values +func (tabsLayout *tabsLayoutData) Init(session Session) { + tabsLayout.viewsContainerData.Init(session) + tabsLayout.tag = "TabsLayout" + tabsLayout.systemClass = "ruiTabsLayout" + tabsLayout.tabListener = nil +} + +func (tabsLayout *tabsLayoutData) currentItem() int { + result, _ := intProperty(tabsLayout, Current, tabsLayout.session, 0) + return result +} + +func (tabsLayout *tabsLayoutData) Set(tag string, value interface{}) bool { + switch tag { + case Current: + oldCurrent := tabsLayout.currentItem() + if !tabsLayout.setIntProperty(Current, value) { + return false + } + + if !tabsLayout.session.ignoreViewUpdates() { + current := tabsLayout.currentItem() + if oldCurrent != current { + tabsLayout.session.runScript(fmt.Sprintf("activateTab(%v, %d);", tabsLayout.htmlID(), current)) + if tabsLayout.tabListener != nil { + oldView := tabsLayout.views[oldCurrent] + view := tabsLayout.views[current] + tabsLayout.tabListener.OnTabsLayoutCurrentChanged(tabsLayout, current, view, oldCurrent, oldView) + } + } + } + + case Tabs: + if !tabsLayout.setEnumProperty(Tabs, value, enumProperties[Tabs].values) { + return false + } + if !tabsLayout.session.ignoreViewUpdates() { + htmlID := tabsLayout.htmlID() + updateCSSStyle(htmlID, tabsLayout.session) + updateInnerHTML(htmlID, tabsLayout.session) + } + + case TabStyle, CurrentTabStyle: + if value == nil { + delete(tabsLayout.properties, tag) + } else 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.session.ignoreViewUpdates() { + htmlID := tabsLayout.htmlID() + updateProperty(htmlID, "data-tabStyle", tabsLayout.inactiveTabStyle(), tabsLayout.session) + updateProperty(htmlID, "data-activeTabStyle", tabsLayout.activeTabStyle(), tabsLayout.session) + updateInnerHTML(htmlID, tabsLayout.session) + } + + default: + return tabsLayout.viewsContainerData.Set(tag, value) + } + + return true +} + +func (tabsLayout *tabsLayoutData) tabsLocation() int { + tabs, _ := enumProperty(tabsLayout, Tabs, tabsLayout.session, 0) + return tabs +} + +func (tabsLayout *tabsLayoutData) inactiveTabStyle() string { + if style, ok := stringProperty(tabsLayout, TabStyle, tabsLayout.session); ok { + return style + } + switch tabsLayout.tabsLocation() { + case LeftTabs, RightTabs: + return "ruiInactiveVerticalTab" + } + return "ruiInactiveTab" +} + +func (tabsLayout *tabsLayoutData) activeTabStyle() string { + if style, ok := stringProperty(tabsLayout, CurrentTabStyle, tabsLayout.session); ok { + return style + } + switch tabsLayout.tabsLocation() { + case LeftTabs, RightTabs: + return "ruiActiveVerticalTab" + } + return "ruiActiveTab" +} + +func (tabsLayout *tabsLayoutData) TabStyle() (string, string) { + return tabsLayout.inactiveTabStyle(), tabsLayout.activeTabStyle() +} + +func (tabsLayout *tabsLayoutData) SetCurrentTabChangedListener(listener TabsLayoutCurrentChangedListener) { + tabsLayout.tabListener = listener +} + +/* +// SetCurrentTabChangedListener add the listener of the current tab changing +func (tabsLayout *tabsLayoutData) SetCurrentTabChangedListener(listener TabsLayoutCurrentChangedListener) { + tabsLayout.tabListener = listener +} + +// SetCurrentTabChangedListener add the listener function of the current tab changing +func (tabsLayout *tabsLayoutData) SetCurrentTabChangedListenerFunc(listenerFunc func(tabsLayout TabsLayout, + newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View)) { + } +*/ + +func (tabsLayout *tabsLayoutData) SetCurrentTabChangedListenerFunc(listenerFunc func(tabsLayout TabsLayout, + newCurrent int, newCurrentView View, oldCurrent int, oldCurrentView View)) { + listener := new(tabsLayoutCurrentChangedListenerFunc) + listener.listenerFunc = listenerFunc + tabsLayout.SetCurrentTabChangedListener(listener) +} + +// Append appends view to the end of view list +func (tabsLayout *tabsLayoutData) Append(view View) { + if tabsLayout.views == nil { + tabsLayout.views = []View{} + } + tabsLayout.viewsContainerData.Append(view) + if len(tabsLayout.views) == 1 { + tabsLayout.setIntProperty(Current, 0) + if tabsLayout.tabListener != nil { + tabsLayout.tabListener.OnTabsLayoutCurrentChanged(tabsLayout, 0, tabsLayout.views[0], -1, nil) + } + } + updateInnerHTML(tabsLayout.htmlID(), tabsLayout.session) +} + +// Insert inserts view to the "index" position in view list +func (tabsLayout *tabsLayoutData) Insert(view View, index uint) { + if tabsLayout.views == nil { + tabsLayout.views = []View{} + } + tabsLayout.viewsContainerData.Insert(view, index) + current := tabsLayout.currentItem() + if current >= int(index) { + tabsLayout.Set(Current, current+1) + } +} + +// Remove removes view from list and return it +func (tabsLayout *tabsLayoutData) RemoveView(index uint) View { + if tabsLayout.views == nil { + tabsLayout.views = []View{} + return nil + } + i := int(index) + count := len(tabsLayout.views) + if i >= count { + return nil + } + + if count == 1 { + view := tabsLayout.views[0] + tabsLayout.views = []View{} + if tabsLayout.tabListener != nil { + tabsLayout.tabListener.OnTabsLayoutCurrentChanged(tabsLayout, 0, nil, 0, view) + } + return view + } + + current := tabsLayout.currentItem() + removeCurrent := (i == current) + if i < current || (removeCurrent && i == count-1) { + tabsLayout.properties[Current] = current - 1 + if tabsLayout.tabListener != nil { + currentView := tabsLayout.views[current-1] + oldCurrentView := tabsLayout.views[current] + tabsLayout.tabListener.OnTabsLayoutCurrentChanged(tabsLayout, current-1, currentView, current, oldCurrentView) + } + } + + return tabsLayout.viewsContainerData.RemoveView(index) +} + +func (tabsLayout *tabsLayoutData) htmlProperties(self View, buffer *strings.Builder) { + tabsLayout.viewsContainerData.htmlProperties(self, buffer) + buffer.WriteString(` data-inactiveTabStyle="`) + buffer.WriteString(tabsLayout.inactiveTabStyle()) + buffer.WriteString(`" data-activeTabStyle="`) + buffer.WriteString(tabsLayout.activeTabStyle()) + buffer.WriteString(`" data-current="`) + buffer.WriteString(tabsLayout.htmlID()) + buffer.WriteRune('-') + buffer.WriteString(strconv.Itoa(tabsLayout.currentItem())) + buffer.WriteRune('"') +} + +func (tabsLayout *tabsLayoutData) cssStyle(self View, builder cssBuilder) { + tabsLayout.viewsContainerData.cssStyle(self, builder) + switch tabsLayout.tabsLocation() { + case TopTabs: + builder.add(`grid-template-rows`, `auto 1fr`) + + case BottomTabs: + builder.add(`grid-template-rows`, `1fr auto`) + + case LeftTabs, LeftListTabs: + builder.add(`grid-template-columns`, `auto 1fr`) + + case RightTabs, RightListTabs: + builder.add(`grid-template-columns`, `1fr auto`) + } +} + +func (tabsLayout *tabsLayoutData) htmlSubviews(self View, buffer *strings.Builder) { + if tabsLayout.views == nil { + return + } + + //viewCount := len(tabsLayout.views) + current := tabsLayout.currentItem() + location := tabsLayout.tabsLocation() + tabsLayoutID := tabsLayout.htmlID() + + if location != HiddenTabs { + tabsHeight, _ := sizeConstant(tabsLayout.session, "ruiTabHeight") + tabsSpace, _ := sizeConstant(tabsLayout.session, "ruiTabSpace") + rowLayout := false + buffer.WriteString(`
`) + + inactiveStyle := tabsLayout.inactiveTabStyle() + activeStyle := tabsLayout.activeTabStyle() + + notTranslate := GetNotTranslate(tabsLayout, "") + last := len(tabsLayout.views) - 1 + for n, view := range tabsLayout.views { + title, _ := stringProperty(view, "title", tabsLayout.session) + if !notTranslate { + title, _ = tabsLayout.Session().GetString(title) + } + + buffer.WriteString(`
`) + + case RightTabs: + buffer.WriteString(` style="writing-mode: vertical-lr;">`) + + default: + buffer.WriteByte('>') + } + buffer.WriteString(title) + buffer.WriteString(`
`) + } + + buffer.WriteString(`
`) + } + + for n, view := range tabsLayout.views { + buffer.WriteString(`
`) + + view.addToCSSStyle(map[string]string{`position`: `absolute`, `left`: `0`, `right`: `0`, `top`: `0`, `bottom`: `0`}) + viewHTML(tabsLayout.views[n], buffer) + buffer.WriteString(`
`) + } +} + +func (tabsLayout *tabsLayoutData) handleCommand(self View, command string, 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() + if current != number { + tabsLayout.properties[Current] = number + if tabsLayout.tabListener != nil { + oldView := tabsLayout.views[current] + view := tabsLayout.views[number] + tabsLayout.tabListener.OnTabsLayoutCurrentChanged(tabsLayout, number, view, current, oldView) + } + } + } + } + return true + } + return tabsLayout.viewsContainerData.handleCommand(self, command, data) +} diff --git a/textView.go b/textView.go new file mode 100644 index 0000000..bf74308 --- /dev/null +++ b/textView.go @@ -0,0 +1,142 @@ +package rui + +import ( + "fmt" + "strings" +) + +// TextView - text View +type TextView interface { + View +} + +type textViewData struct { + viewData + // TODO textShadow +} + +// NewTextView create new TextView object and return it +func NewTextView(session Session, params Params) TextView { + view := new(textViewData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newTextView(session Session) View { + return NewTextView(session, nil) +} + +// Init initialize fields of TextView by default values +func (textView *textViewData) Init(session Session) { + textView.viewData.Init(session) + textView.tag = "TextView" +} + +func (textView *textViewData) Get(tag string) interface{} { + 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) + switch tag { + case Text: + updateInnerHTML(textView.htmlID(), textView.session) + + case TextOverflow: + textView.textOverflowUpdated() + } +} + +func (textView *textViewData) Set(tag string, value interface{}) bool { + return textView.set(strings.ToLower(tag), value) +} + +func (textView *textViewData) set(tag string, value interface{}) bool { + switch tag { + case Text: + switch value := value.(type) { + case string: + textView.properties[Text] = value + + case fmt.Stringer: + textView.properties[Text] = value.String() + + case float32: + textView.properties[Text] = fmt.Sprintf("%g", float64(value)) + + case float64: + textView.properties[Text] = fmt.Sprintf("%g", value) + + case []rune: + textView.properties[Text] = string(value) + + case bool: + if value { + textView.properties[Text] = "true" + } else { + textView.properties[Text] = "false" + } + + default: + if n, ok := isInt(value); ok { + textView.properties[Text] = fmt.Sprintf("%d", n) + } else { + notCompatibleType(tag, value) + return false + } + } + updateInnerHTML(textView.htmlID(), textView.session) + return true + + case TextOverflow: + if textView.viewData.set(tag, value) { + textView.textOverflowUpdated() + } + } + + return textView.viewData.set(tag, value) +} + +func (textView *textViewData) textOverflowUpdated() { + session := textView.Session() + if n, ok := enumProperty(textView, TextOverflow, textView.session, 0); ok { + values := enumProperties[TextOverflow].cssValues + if n >= 0 && n < len(values) { + updateCSSProperty(textView.htmlID(), TextOverflow, values[n], session) + return + } + } + updateCSSProperty(textView.htmlID(), TextOverflow, "", session) +} + +func (textView *textViewData) htmlSubviews(self View, buffer *strings.Builder) { + if value, ok := stringProperty(textView, Text, textView.Session()); ok { + if !GetNotTranslate(textView, "") { + value, _ = textView.session.GetString(value) + } + + text := strings.ReplaceAll(value, `"`, `\"`) + text = strings.ReplaceAll(text, "\n", `\n`) + text = strings.ReplaceAll(text, "\r", `\r`) + buffer.WriteString(strings.ReplaceAll(text, `'`, `\'`)) + } +} + +// GetTextOverflow returns a value of the "text-overflow" property: +// TextOverflowClip (0) or TextOverflowEllipsis (1). +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTextOverflow(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return SingleLineText + } + t, _ := enumStyledProperty(view, TextOverflow, SingleLineText) + return t +} diff --git a/theme.go b/theme.go new file mode 100644 index 0000000..fb0934a --- /dev/null +++ b/theme.go @@ -0,0 +1,329 @@ +package rui + +import ( + "sort" + "strconv" + "strings" +) + +const ( + defaultMedia = 0 + portraitMedia = 1 + landscapeMedia = 2 +) + +type mediaStyle struct { + orientation int + width int + height int + styles map[string]DataObject +} + +func (rule mediaStyle) cssText() string { + builder := allocStringBuilder() + defer freeStringBuilder(builder) + + switch rule.orientation { + case portraitMedia: + builder.WriteString(" and (orientation: portrait)") + + case landscapeMedia: + builder.WriteString(" and (orientation: landscape)") + } + + if rule.width > 0 { + builder.WriteString(" and (max-width: ") + builder.WriteString(strconv.Itoa(rule.width)) + builder.WriteString("px)") + } + + if rule.height > 0 { + builder.WriteString(" and (max-height: ") + builder.WriteString(strconv.Itoa(rule.height)) + builder.WriteString("px)") + } + + return builder.String() +} + +func parseMediaRule(text string) (mediaStyle, bool) { + rule := mediaStyle{orientation: defaultMedia, width: 0, height: 0, styles: map[string]DataObject{}} + elements := strings.Split(text, ":") + for i := 1; i < len(elements); i++ { + switch element := elements[i]; element { + case "portrait": + if rule.orientation != defaultMedia { + ErrorLog(`Duplicate orientation tag in the style section "` + text + `"`) + return rule, false + } + rule.orientation = portraitMedia + + case "landscape": + if rule.orientation != defaultMedia { + ErrorLog(`Duplicate orientation tag in the style section "` + text + `"`) + return rule, false + } + rule.orientation = landscapeMedia + + default: + elementSize := func(name string) (int, bool) { + if strings.HasPrefix(element, name) { + size, err := strconv.Atoi(element[len(name):]) + if err == nil && size > 0 { + return size, true + } + ErrorLogF(`Invalid style section name "%s": %s`, text, err.Error()) + return 0, false + } + return 0, true + } + + if size, ok := elementSize("width"); !ok || size > 0 { + if !ok { + return rule, false + } + if rule.width != 0 { + ErrorLog(`Duplicate "width" tag in the style section "` + text + `"`) + return rule, false + } + rule.width = size + } else if size, ok := elementSize("height"); !ok || size > 0 { + if !ok { + return rule, false + } + if rule.height != 0 { + ErrorLog(`Duplicate "height" tag in the style section "` + text + `"`) + return rule, false + } + rule.height = size + } else { + ErrorLogF(`Unknown elemnet "%s" in the style section name "%s"`, element, text) + return rule, false + } + } + } + return rule, true +} + +type theme struct { + name string + constants map[string]string + touchConstants map[string]string + colors map[string]string + darkColors map[string]string + styles map[string]DataObject + mediaStyles []mediaStyle +} + +var defaultTheme = new(theme) + +func newTheme(text string) (*theme, bool) { + result := new(theme) + result.init() + ok := result.addText(text) + return result, ok +} + +func (theme *theme) init() { + theme.constants = map[string]string{} + theme.touchConstants = map[string]string{} + theme.colors = map[string]string{} + theme.darkColors = map[string]string{} + theme.styles = map[string]DataObject{} + theme.mediaStyles = []mediaStyle{} +} + +func (theme *theme) concat(anotherTheme *theme) { + if theme.constants == nil { + theme.init() + } + + for tag, constant := range anotherTheme.constants { + theme.constants[tag] = constant + } + + for tag, constant := range anotherTheme.touchConstants { + theme.touchConstants[tag] = constant + } + + for tag, color := range anotherTheme.colors { + theme.colors[tag] = color + } + + for tag, color := range anotherTheme.darkColors { + theme.darkColors[tag] = color + } + + for tag, style := range anotherTheme.styles { + theme.styles[tag] = style + } + + for _, anotherMedia := range anotherTheme.mediaStyles { + exists := false + for _, media := range theme.mediaStyles { + if anotherMedia.height == media.height && + anotherMedia.width == media.width && + anotherMedia.orientation == media.orientation { + for tag, style := range anotherMedia.styles { + media.styles[tag] = style + } + exists = true + break + } + } + if !exists { + theme.mediaStyles = append(theme.mediaStyles, anotherMedia) + } + } +} + +func (theme *theme) cssText(session Session) string { + if theme.styles == nil { + theme.init() + return "" + } + + var builder cssStyleBuilder + builder.init() + + for tag, obj := range theme.styles { + var style viewStyle + style.init() + parseProperties(&style, obj) + builder.startStyle(tag) + style.cssViewStyle(&builder, session, nil) + builder.endStyle() + } + + for _, media := range theme.mediaStyles { + builder.startMedia(media.cssText()) + for tag, obj := range media.styles { + var style viewStyle + style.init() + parseProperties(&style, obj) + builder.startStyle(tag) + style.cssViewStyle(&builder, session, nil) + builder.endStyle() + } + builder.endMedia() + } + + return builder.finish() +} + +func (theme *theme) addText(themeText string) bool { + data := ParseDataText(themeText) + if data == nil { + return false + } + + theme.addData(data) + return true +} + +func (theme *theme) addData(data DataObject) { + if theme.constants == nil { + theme.init() + } + + if data.IsObject() && data.Tag() == "theme" { + theme.parseThemeData(data) + } +} + +func (theme *theme) parseThemeData(data DataObject) { + count := data.PropertyCount() + + for i := 0; i < count; i++ { + if d := data.Property(i); d != nil { + switch tag := d.Tag(); tag { + case "constants": + if d.Type() == ObjectNode { + if obj := d.Object(); obj != nil { + objCount := obj.PropertyCount() + for k := 0; k < objCount; k++ { + if prop := obj.Property(k); prop != nil && prop.Type() == TextNode { + theme.constants[prop.Tag()] = prop.Text() + } + } + } + } + + case "constants:touch": + if d.Type() == ObjectNode { + if obj := d.Object(); obj != nil { + objCount := obj.PropertyCount() + for k := 0; k < objCount; k++ { + if prop := obj.Property(k); prop != nil && prop.Type() == TextNode { + theme.touchConstants[prop.Tag()] = prop.Text() + } + } + } + } + + case "colors": + if d.Type() == ObjectNode { + if obj := d.Object(); obj != nil { + objCount := obj.PropertyCount() + for k := 0; k < objCount; k++ { + if prop := obj.Property(k); prop != nil && prop.Type() == TextNode { + theme.colors[prop.Tag()] = prop.Text() + } + } + } + } + + case "colors:dark": + if d.Type() == ObjectNode { + if obj := d.Object(); obj != nil { + objCount := obj.PropertyCount() + for k := 0; k < objCount; k++ { + if prop := obj.Property(k); prop != nil && prop.Type() == TextNode { + theme.darkColors[prop.Tag()] = prop.Text() + } + } + } + } + + case "styles": + if d.Type() == ArrayNode { + arraySize := d.ArraySize() + for k := 0; k < arraySize; k++ { + if element := d.ArrayElement(k); element != nil && element.IsObject() { + if obj := element.Object(); obj != nil { + theme.styles[obj.Tag()] = obj + } + } + } + } + + default: + if d.Type() == ArrayNode && strings.HasPrefix(tag, "styles:") { + if rule, ok := parseMediaRule(tag); ok { + arraySize := d.ArraySize() + for k := 0; k < arraySize; k++ { + if element := d.ArrayElement(k); element != nil && element.IsObject() { + if obj := element.Object(); obj != nil { + rule.styles[obj.Tag()] = obj + } + } + } + theme.mediaStyles = append(theme.mediaStyles, rule) + } + } + } + } + } + + if len(theme.mediaStyles) > 0 { + sort.SliceStable(theme.mediaStyles, func(i, j int) bool { + if theme.mediaStyles[i].orientation != theme.mediaStyles[j].orientation { + return theme.mediaStyles[i].orientation < theme.mediaStyles[j].orientation + } + if theme.mediaStyles[i].width != theme.mediaStyles[j].width { + return theme.mediaStyles[i].width < theme.mediaStyles[j].width + } + return theme.mediaStyles[i].height < theme.mediaStyles[j].height + }) + } +} diff --git a/timePicker.go b/timePicker.go new file mode 100644 index 0000000..996a34c --- /dev/null +++ b/timePicker.go @@ -0,0 +1,410 @@ +package rui + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +const ( + TimeChangedEvent = "time-changed" + TimePickerMin = "time-picker-min" + TimePickerMax = "time-picker-max" + TimePickerStep = "time-picker-step" + TimePickerValue = "time-picker-value" + timeFormat = "15:04" +) + +// TimePicker - TimePicker view +type TimePicker interface { + View +} + +type timePickerData struct { + viewData + timeChangedListeners []func(TimePicker, time.Time) +} + +// NewTimePicker create new TimePicker object and return it +func NewTimePicker(session Session, params Params) TimePicker { + view := new(timePickerData) + view.Init(session) + setInitParams(view, params) + return view +} + +func newTimePicker(session Session) View { + return NewTimePicker(session, nil) +} + +func (picker *timePickerData) Init(session Session) { + picker.viewData.Init(session) + picker.tag = "TimePicker" + picker.timeChangedListeners = []func(TimePicker, time.Time){} +} + +func (picker *timePickerData) normalizeTag(tag string) string { + tag = strings.ToLower(tag) + switch tag { + case Type, Min, Max, Step, Value: + return "time-picker-" + tag + } + + return tag +} + +func (picker *timePickerData) Remove(tag string) { + picker.remove(picker.normalizeTag(tag)) +} + +func (picker *timePickerData) remove(tag string) { + switch tag { + case TimeChangedEvent: + if len(picker.timeChangedListeners) > 0 { + picker.timeChangedListeners = []func(TimePicker, time.Time){} + } + + case TimePickerMin: + delete(picker.properties, TimePickerMin) + removeProperty(picker.htmlID(), Min, picker.session) + + case TimePickerMax: + delete(picker.properties, TimePickerMax) + removeProperty(picker.htmlID(), Max, picker.session) + + case TimePickerStep: + delete(picker.properties, TimePickerMax) + removeProperty(picker.htmlID(), Step, picker.session) + + case TimePickerValue: + delete(picker.properties, TimePickerValue) + updateProperty(picker.htmlID(), Value, time.Now().Format(timeFormat), picker.session) + + default: + picker.viewData.remove(tag) + picker.propertyChanged(tag) + } +} + +func (picker *timePickerData) Set(tag string, value interface{}) bool { + return picker.set(picker.normalizeTag(tag), value) +} + +func (picker *timePickerData) set(tag string, value interface{}) bool { + if value == nil { + picker.remove(tag) + return true + } + + setTimeValue := func(tag string) (time.Time, bool) { + switch value := value.(type) { + case time.Time: + picker.properties[tag] = value + return value, true + + case string: + if text, ok := picker.Session().resolveConstants(value); ok { + if time, err := time.Parse(timeFormat, text); err == nil { + picker.properties[tag] = value + return time, true + } + } + } + + notCompatibleType(tag, value) + return time.Now(), false + } + + switch tag { + case TimePickerMin: + old, oldOK := getTimeProperty(picker, TimePickerMin, Min) + if time, ok := setTimeValue(TimePickerMin); ok { + if !oldOK || time != old { + updateProperty(picker.htmlID(), Min, time.Format(timeFormat), picker.session) + } + return true + } + + case TimePickerMax: + old, oldOK := getTimeProperty(picker, TimePickerMax, Max) + if time, ok := setTimeValue(TimePickerMax); ok { + if !oldOK || time != old { + updateProperty(picker.htmlID(), Max, time.Format(timeFormat), picker.session) + } + return true + } + + case TimePickerStep: + oldStep := GetTimePickerStep(picker, "") + if picker.setIntProperty(TimePickerStep, value) { + step := GetTimePickerStep(picker, "") + if oldStep != step { + if step > 0 { + updateProperty(picker.htmlID(), Step, strconv.Itoa(step), picker.session) + } else { + removeProperty(picker.htmlID(), Step, picker.session) + } + } + return true + } + + case TimePickerValue: + oldTime := GetTimePickerValue(picker, "") + if time, ok := setTimeValue(TimePickerMax); ok { + picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, picker.htmlID(), time.Format(timeFormat))) + if time != oldTime { + for _, listener := range picker.timeChangedListeners { + listener(picker, time) + } + } + return true + } + + case TimeChangedEvent: + switch value := value.(type) { + case func(TimePicker, time.Time): + picker.timeChangedListeners = []func(TimePicker, time.Time){value} + + case func(time.Time): + fn := func(view TimePicker, date time.Time) { + value(date) + } + picker.timeChangedListeners = []func(TimePicker, time.Time){fn} + + case []func(TimePicker, time.Time): + picker.timeChangedListeners = value + + case []func(time.Time): + listeners := make([]func(TimePicker, time.Time), len(value)) + for i, val := range value { + if val == nil { + notCompatibleType(tag, val) + return false + } + + listeners[i] = func(view TimePicker, date time.Time) { + val(date) + } + } + picker.timeChangedListeners = listeners + + case []interface{}: + listeners := make([]func(TimePicker, time.Time), len(value)) + for i, val := range value { + if val == nil { + notCompatibleType(tag, val) + return false + } + + switch val := val.(type) { + case func(TimePicker, time.Time): + listeners[i] = val + + case func(time.Time): + listeners[i] = func(view TimePicker, date time.Time) { + val(date) + } + + default: + notCompatibleType(tag, val) + return false + } + } + picker.timeChangedListeners = listeners + } + return true + + default: + if picker.viewData.set(tag, value) { + picker.propertyChanged(tag) + return true + } + } + return false +} + +func (picker *timePickerData) Get(tag string) interface{} { + return picker.get(picker.normalizeTag(tag)) +} + +func (picker *timePickerData) get(tag string) interface{} { + switch tag { + case TimeChangedEvent: + return picker.timeChangedListeners + + default: + return picker.viewData.get(tag) + } +} + +func (picker *timePickerData) htmlTag() string { + return "input" +} + +func (picker *timePickerData) htmlProperties(self View, buffer *strings.Builder) { + picker.viewData.htmlProperties(self, buffer) + + buffer.WriteString(` type="time"`) + + if min, ok := getTimeProperty(picker, TimePickerMin, Min); ok { + buffer.WriteString(` min="`) + buffer.WriteString(min.Format(timeFormat)) + buffer.WriteByte('"') + } + + if max, ok := getTimeProperty(picker, TimePickerMax, Max); ok { + buffer.WriteString(` max="`) + buffer.WriteString(max.Format(timeFormat)) + buffer.WriteByte('"') + } + + if step, ok := intProperty(picker, TimePickerStep, picker.Session(), 0); ok && step > 0 { + buffer.WriteString(` step="`) + buffer.WriteString(strconv.Itoa(step)) + buffer.WriteByte('"') + } + + buffer.WriteString(` value="`) + buffer.WriteString(GetTimePickerValue(picker, "").Format(timeFormat)) + buffer.WriteByte('"') + + buffer.WriteString(` oninput="editViewInputEvent(this)"`) +} + +func (picker *timePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) { + if IsDisabled(self) { + buffer.WriteString(` disabled`) + } + picker.viewData.htmlDisabledProperties(self, buffer) +} + +func (picker *timePickerData) handleCommand(self View, command string, data DataObject) bool { + switch command { + case "textChanged": + if text, ok := data.PropertyValue("text"); ok { + if value, err := time.Parse(timeFormat, text); err == nil { + oldValue := GetTimePickerValue(picker, "") + picker.properties[TimePickerValue] = value + if value != oldValue { + for _, listener := range picker.timeChangedListeners { + listener(picker, value) + } + } + } + } + return true + } + + return picker.viewData.handleCommand(self, command, data) +} + +func getTimeProperty(view View, mainTag, shortTag string) (time.Time, bool) { + valueToTime := func(value interface{}) (time.Time, bool) { + if value != nil { + switch value := value.(type) { + case time.Time: + return value, true + + case string: + if text, ok := view.Session().resolveConstants(value); ok { + if result, err := time.Parse(timeFormat, text); err == nil { + return result, true + } + } + } + } + return time.Now(), false + } + + if view != nil { + if result, ok := valueToTime(view.getRaw(mainTag)); ok { + return result, true + } + + if value, ok := valueFromStyle(view, shortTag); ok { + if result, ok := valueToTime(value); ok { + return result, true + } + } + } + + return time.Now(), false +} + +// GetTimePickerMin returns the min time of TimePicker subview and "true" as the second value if the min time is set, +// "false" as the second value otherwise. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTimePickerMin(view View, subviewID string) (time.Time, bool) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + return getTimeProperty(view, TimePickerMin, Min) + } + return time.Now(), false +} + +// GetTimePickerMax returns the max time of TimePicker subview and "true" as the second value if the min time is set, +// "false" as the second value otherwise. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTimePickerMax(view View, subviewID string) (time.Time, bool) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + return getTimeProperty(view, TimePickerMax, Max) + } + return time.Now(), false +} + +// GetTimePickerStep returns the time changing step in seconds of TimePicker subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTimePickerStep(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return 60 + } + + result, ok := intStyledProperty(view, TimePickerStep, 60) + if !ok { + result, _ = intStyledProperty(view, Step, 60) + } + + if result < 0 { + return 60 + } + return result +} + +// GetTimePickerValue returns the time of TimePicker subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTimePickerValue(view View, subviewID string) time.Time { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return time.Now() + } + time, _ := getTimeProperty(view, TimePickerValue, Value) + return time +} + +// GetTimeChangedListeners returns the TimeChangedListener list of an TimePicker subview. +// If there are no listeners then the empty list is returned +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTimeChangedListeners(view View, subviewID string) []func(TimePicker, time.Time) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.Get(TimeChangedEvent); value != nil { + if listeners, ok := value.([]func(TimePicker, time.Time)); ok { + return listeners + } + } + } + return []func(TimePicker, time.Time){} +} diff --git a/touchEvents.go b/touchEvents.go new file mode 100644 index 0000000..63f7a28 --- /dev/null +++ b/touchEvents.go @@ -0,0 +1,347 @@ +package rui + +import ( + "strconv" + "strings" +) + +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" + + // 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" + + // 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" + + // 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" +) + +// Touch contains parameters of a single touch of a touch event +type Touch struct { + // Identifier is a unique identifier for this Touch object. A given touch point (say, by a finger) + // will have the same identifier for the duration of its movement around the surface. + // This lets you ensure that you're tracking the same touch all the time. + Identifier int + + // 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 + + // RotationAngle is the angle (in degrees) that the ellipse described by radiusX and radiusY must be rotated, + // clockwise, to most accurately cover the area of contact between the user and the surface. + RotationAngle float64 + + // Force is the amount of pressure being applied to the surface by the user, as a float + // between 0.0 (no pressure) and 1.0 (maximum pressure). + Force float64 +} + +// TouchEvent contains parameters of a touch event +type TouchEvent 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. + TimeStamp uint64 + + // Touches is the array of all the Touch objects representing all current points + // of contact with the surface, regardless of target or changed status. + Touches []Touch + + // CtrlKey == true if the control key was down when the event was fired. false otherwise. + CtrlKey bool + // ShiftKey == true if the shift key was down when the event was fired. false otherwise. + ShiftKey bool + // AltKey == true if the alt key was down when the event was fired. false otherwise. + AltKey bool + // MetaKey == true if the meta key was down when the event was fired. false otherwise. + MetaKey bool +} + +func valueToTouchListeners(value interface{}) ([]func(View, TouchEvent), bool) { + if value == nil { + return nil, true + } + + switch value := value.(type) { + case func(View, TouchEvent): + return []func(View, TouchEvent){value}, true + + case func(TouchEvent): + fn := func(view View, event TouchEvent) { + value(event) + } + return []func(View, TouchEvent){fn}, true + + case func(View): + fn := func(view View, event TouchEvent) { + value(view) + } + return []func(View, TouchEvent){fn}, true + + case func(): + fn := func(view View, event TouchEvent) { + value() + } + return []func(View, TouchEvent){fn}, true + + case []func(View, TouchEvent): + if len(value) == 0 { + return nil, true + } + for _, fn := range value { + if fn == nil { + return nil, false + } + } + return value, true + + case []func(TouchEvent): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View, TouchEvent), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(view View, event TouchEvent) { + v(event) + } + } + return listeners, true + + case []func(View): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View, TouchEvent), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(view View, event TouchEvent) { + v(view) + } + } + return listeners, true + + case []func(): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View, TouchEvent), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(view View, event TouchEvent) { + v() + } + } + return listeners, true + + case []interface{}: + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(View, TouchEvent), count) + for i, v := range value { + if v == nil { + return nil, false + } + switch v := v.(type) { + case func(View, TouchEvent): + listeners[i] = v + + case func(TouchEvent): + listeners[i] = func(view View, event TouchEvent) { + v(event) + } + + case func(View): + listeners[i] = func(view View, event TouchEvent) { + v(view) + } + + case func(): + listeners[i] = func(view View, event TouchEvent) { + v() + } + + default: + return nil, false + } + } + return listeners, true + } + + return nil, false +} + +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 interface{}) bool { + listeners, ok := valueToTouchListeners(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 { + updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session()) + } + } else { + return false + } + return true +} + +func (view *viewData) removeTouchListener(tag string) { + delete(view.properties, tag) + if view.created { + if js, ok := touchEvents[tag]; ok { + updateProperty(view.htmlID(), js.jsEvent, "", view.Session()) + } + } +} + +func getTouchListeners(view View, subviewID string, tag string) []func(View, TouchEvent) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.Get(tag); value != nil { + if result, ok := value.([]func(View, TouchEvent)); ok { + return result + } + } + } + return []func(View, TouchEvent){} +} + +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 + `="` + js.jsFunc + `(this, event)" `) + } + } + } +} + +func (event *TouchEvent) init(data DataObject) { + + event.Touches = []Touch{} + event.TimeStamp = getTimeStamp(data) + if node := data.PropertyWithTag("touches"); node != nil && node.Type() == ArrayNode { + for i := 0; i < node.ArraySize(); i++ { + if element := node.ArrayElement(i); element != nil && element.IsObject() { + if obj := element.Object(); obj != nil { + var touch Touch + if value, ok := obj.PropertyValue("identifier"); ok { + touch.Identifier, _ = strconv.Atoi(value) + } + touch.X = dataFloatProperty(obj, "x") + touch.Y = dataFloatProperty(obj, "y") + touch.ClientX = dataFloatProperty(obj, "clientX") + touch.ClientY = dataFloatProperty(obj, "clientY") + touch.ScreenX = dataFloatProperty(obj, "screenX") + touch.ScreenY = dataFloatProperty(obj, "screenY") + touch.RadiusX = dataFloatProperty(obj, "radiusX") + touch.RadiusY = dataFloatProperty(obj, "radiusY") + touch.RotationAngle = dataFloatProperty(obj, "rotationAngle") + touch.Force = dataFloatProperty(obj, "force") + event.Touches = append(event.Touches, touch) + } + } + } + } + event.CtrlKey = dataBoolProperty(data, "ctrlKey") + event.ShiftKey = dataBoolProperty(data, "shiftKey") + event.AltKey = dataBoolProperty(data, "altKey") + event.MetaKey = dataBoolProperty(data, "metaKey") +} + +func handleTouchEvents(view View, tag string, data DataObject) { + listeners := getTouchListeners(view, "", tag) + if len(listeners) == 0 { + return + } + + var event TouchEvent + event.init(data) + + for _, listener := range listeners { + listener(view, event) + } +} + +// GetTouchStartListeners returns the "touch-start" listener list. If there are no listeners then the empty list is returned. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTouchStartListeners(view View, subviewID string) []func(View, TouchEvent) { + return getTouchListeners(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 "" then a value from the first argument (view) is returned. +func GetTouchEndListeners(view View, subviewID string) []func(View, TouchEvent) { + return getTouchListeners(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 "" then a value from the first argument (view) is returned. +func GetTouchMoveListeners(view View, subviewID string) []func(View, TouchEvent) { + return getTouchListeners(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 "" then a value from the first argument (view) is returned. +func GetTouchCancelListeners(view View, subviewID string) []func(View, TouchEvent) { + return getTouchListeners(view, subviewID, TouchCancel) +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..4a10ec3 --- /dev/null +++ b/utils.go @@ -0,0 +1,78 @@ +package rui + +import ( + "net" + "strconv" + "strings" +) + +var stringBuilders []*strings.Builder = make([]*strings.Builder, 4096) +var stringBuilderCount = 0 + +func allocStringBuilder() *strings.Builder { + for stringBuilderCount > 0 { + stringBuilderCount-- + result := stringBuilders[stringBuilderCount] + if result != nil { + stringBuilders[stringBuilderCount] = nil + result.Reset() + return result + } + } + + result := new(strings.Builder) + result.Grow(4096) + return result +} + +func freeStringBuilder(builder *strings.Builder) { + if builder != nil { + if stringBuilderCount == len(stringBuilders) { + stringBuilders = append(stringBuilders, builder) + } else { + stringBuilders[stringBuilderCount] = builder + } + stringBuilderCount++ + } +} + +func GetLocalIP() string { + addrs, err := net.InterfaceAddrs() + if err != nil { + return "" + } + for _, address := range addrs { + // check the address type and if it is not a loopback the display it + if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + return ipnet.IP.String() + } + } + } + return "localhost" +} + +func dataIntProperty(data DataObject, tag string) int { + if value, ok := data.PropertyValue(tag); ok { + if n, err := strconv.Atoi(value); err == nil { + return n + } + } + return 0 +} + +func dataBoolProperty(data DataObject, tag string) bool { + if value, ok := data.PropertyValue(tag); ok && value == "1" { + return true + } + return false +} + +func dataFloatProperty(data DataObject, tag string) float64 { + if value, ok := data.PropertyValue(tag); ok { + if n, err := strconv.ParseFloat(value, 64); err == nil { + return n + } + } + return 0 +} diff --git a/videoPlayer.go b/videoPlayer.go new file mode 100644 index 0000000..17158fe --- /dev/null +++ b/videoPlayer.go @@ -0,0 +1,134 @@ +package rui + +import ( + "fmt" + "strings" +) + +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" + // 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" + // 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" +) + +type VideoPlayer interface { + MediaPlayer +} + +type videoPlayerData struct { + mediaPlayerData +} + +// NewVideoPlayer create new MediaPlayer object and return it +func NewVideoPlayer(session Session, params Params) MediaPlayer { + view := new(videoPlayerData) + view.Init(session) + view.tag = "VideoPlayer" + setInitParams(view, params) + return view +} + +func newVideoPlayer(session Session) View { + return NewVideoPlayer(session, nil) +} + +func (player *videoPlayerData) Init(session Session) { + player.mediaPlayerData.Init(session) + player.tag = "VideoPlayer" +} + +func (player *videoPlayerData) htmlTag() string { + return "video" +} + +func (player *videoPlayerData) Remove(tag string) { + player.remove(strings.ToLower(tag)) +} + +func (player *videoPlayerData) remove(tag string) { + switch tag { + + case VideoWidth: + delete(player.properties, tag) + removeProperty(player.htmlID(), "width", player.Session()) + + case VideoHeight: + delete(player.properties, tag) + removeProperty(player.htmlID(), "height", player.Session()) + + case Poster: + delete(player.properties, tag) + removeProperty(player.htmlID(), Poster, player.Session()) + + default: + player.mediaPlayerData.remove(tag) + } +} + +func (player *videoPlayerData) Set(tag string, value interface{}) bool { + return player.set(strings.ToLower(tag), value) +} + +func (player *videoPlayerData) set(tag string, value interface{}) 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 := floatProperty(player, tag, session, 0); ok { + if size > 0 { + updateProperty(player.htmlID(), cssTag, fmt.Sprintf("%g", size), session) + } else { + removeProperty(player.htmlID(), cssTag, session) + } + } + } + + switch tag { + case VideoWidth: + updateSize("width") + + case VideoHeight: + updateSize("height") + + case Poster: + if url, ok := stringProperty(player, Poster, session); ok { + updateProperty(player.htmlID(), Poster, url, session) + } + } + return true + } + + return false +} + +func (player *videoPlayerData) htmlProperties(self View, buffer *strings.Builder) { + player.mediaPlayerData.htmlProperties(self, buffer) + + session := player.Session() + + if size, ok := floatProperty(player, VideoWidth, session, 0); ok && size > 0 { + buffer.WriteString(fmt.Sprintf(` width="%g"`, size)) + } + + if size, ok := floatProperty(player, VideoHeight, session, 0); ok && size > 0 { + buffer.WriteString(fmt.Sprintf(` height="%g"`, size)) + } + + if url, ok := stringProperty(player, Poster, session); ok && url != "" { + buffer.WriteString(` poster="`) + buffer.WriteString(url) + buffer.WriteString(`"`) + } +} diff --git a/view.go b/view.go new file mode 100644 index 0000000..f34a01e --- /dev/null +++ b/view.go @@ -0,0 +1,760 @@ +package rui + +import ( + "fmt" + "sort" + "strconv" + "strings" +) + +// Frame - the location and size of a rectangle area +type Frame struct { + // Left - the left border + Left float64 + // Top - the top border + Top float64 + // Width - the width of a rectangle area + Width float64 + // Height - the height of a rectangle area + Height float64 +} + +// Right returns the right border +func (frame Frame) Right() float64 { + return frame.Left + frame.Width +} + +// Bottom returns the bottom border +func (frame Frame) Bottom() float64 { + return frame.Top + frame.Height +} + +// Params defines a type of a parameters list +type Params map[string]interface{} + +func (params Params) AllTags() []string { + tags := make([]string, 0, len(params)) + for t := range params { + tags = append(tags, t) + } + sort.Strings(tags) + return tags +} + +// View - base view interface +type View interface { + Properties + fmt.Stringer + ruiStringer + + // Init initializes fields of View by default values + Init(session Session) + // Session returns the current Session interface + Session() Session + // Parent returns the parent view + Parent() View + parentHTMLID() string + setParentID(parentID string) + // Tag returns the tag of View interface + Tag() string + // ID returns the id of the view + ID() string + // Focusable returns true if the view receives the focus + Focusable() bool + // Frame returns the location and size of the view in pixels + Frame() Frame + // Scroll returns the location size of the scrolable view in pixels + Scroll() Frame + // 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 + SetAnimated(tag string, value interface{}, animation Animation) bool + + handleCommand(self View, command string, data DataObject) bool + //updateEventHandlers() + htmlClass(disabled bool) string + htmlTag() string + closeHTMLTag() bool + htmlID() string + htmlSubviews(self View, buffer *strings.Builder) + htmlProperties(self View, buffer *strings.Builder) + htmlDisabledProperties(self View, buffer *strings.Builder) + cssStyle(self View, builder cssBuilder) + addToCSSStyle(addCSS map[string]string) + + onResize(self View, x, y, width, height float64) + onItemResize(self View, index int, x, y, width, height float64) + setNoResizeEvent() + isNoResizeEvent() bool + setScroll(x, y, width, height float64) +} + +// viewData - base implementation of View interface +type viewData struct { + viewStyle + session Session + tag string + viewID string + _htmlID string + parentID string + systemClass string + animation map[string]Animation + addCSS map[string]string + frame Frame + scroll Frame + noResizeEvent bool + created bool + //animation map[string]AnimationEndListener +} + +func newView(session Session) View { + view := new(viewData) + view.Init(session) + return view +} + +func setInitParams(view View, params Params) { + if params != nil { + session := view.Session() + if !session.ignoreViewUpdates() { + session.setIgnoreViewUpdates(true) + defer session.setIgnoreViewUpdates(false) + } + for _, tag := range params.AllTags() { + if value, ok := params[tag]; ok { + view.Set(tag, value) + } + } + } +} + +// NewView create new View object and return it +func NewView(session Session, params Params) View { + view := new(viewData) + view.Init(session) + setInitParams(view, params) + return view +} + +func (view *viewData) Init(session Session) { + view.viewStyle.init() + view.tag = "View" + view.session = session + view.addCSS = map[string]string{} + //view.animation = map[string]AnimationEndListener{} + view.animation = map[string]Animation{} + view.noResizeEvent = false + view.created = false +} + +func (view *viewData) Session() Session { + return view.session +} + +func (view *viewData) Parent() View { + return view.session.viewByHTMLID(view.parentID) +} + +func (view *viewData) parentHTMLID() string { + return view.parentID +} + +func (view *viewData) setParentID(parentID string) { + view.parentID = parentID +} + +func (view *viewData) Tag() string { + return view.tag +} + +func (view *viewData) ID() string { + return view.viewID +} + +func (view *viewData) ViewByID(id string) View { + if id == view.ID() { + if v := view.session.viewByHTMLID(view.htmlID()); v != nil { + return v + } + return view + } + return nil +} + +func (view *viewData) Focusable() bool { + return false +} + +func (view *viewData) Remove(tag string) { + view.remove(strings.ToLower(tag)) +} + +func (view *viewData) remove(tag string) { + switch tag { + case ID: + view.viewID = "" + + case Style, StyleDisabled: + if _, ok := view.properties[tag]; ok { + delete(view.properties, tag) + updateProperty(view.htmlID(), "class", view.htmlClass(IsDisabled(view)), view.session) + } + + case FocusEvent, LostFocusEvent: + view.removeFocusListener(tag) + + case KeyDownEvent, KeyUpEvent: + view.removeKeyListener(tag) + + case ClickEvent, DoubleClickEvent, MouseDown, MouseUp, MouseMove, MouseOut, MouseOver, ContextMenuEvent: + view.removeMouseListener(tag) + + case PointerDown, PointerUp, PointerMove, PointerOut, PointerOver, PointerCancel: + view.removePointerListener(tag) + + case TouchStart, TouchEnd, TouchMove, TouchCancel: + view.removeTouchListener(tag) + + case ResizeEvent, ScrollEvent: + delete(view.properties, tag) + + case Content: + if _, ok := view.properties[Content]; ok { + delete(view.properties, Content) + updateInnerHTML(view.htmlID(), view.session) + } + + default: + view.viewStyle.remove(tag) + view.propertyChanged(tag) + } +} + +func (view *viewData) Set(tag string, value interface{}) bool { + return view.set(strings.ToLower(tag), value) +} + +func (view *viewData) set(tag string, value interface{}) bool { + if value == nil { + view.remove(tag) + return true + } + + switch tag { + case ID: + if text, ok := value.(string); ok { + view.viewID = text + return true + } + notCompatibleType(ID, value) + return false + + case Style, StyleDisabled: + if text, ok := value.(string); ok { + view.properties[tag] = text + //updateInnerHTML(view.parentID, view.session) + if view.created { + updateProperty(view.htmlID(), "class", view.htmlClass(IsDisabled(view)), view.session) + } + return true + } + notCompatibleType(ID, value) + return false + + case FocusEvent, LostFocusEvent: + return view.setFocusListener(tag, value) + + case KeyDownEvent, KeyUpEvent: + return view.setKeyListener(tag, value) + + case ClickEvent, DoubleClickEvent, MouseDown, MouseUp, MouseMove, MouseOut, MouseOver, ContextMenuEvent: + return view.setMouseListener(tag, value) + + case PointerDown, PointerUp, PointerMove, PointerOut, PointerOver, PointerCancel: + return view.setPointerListener(tag, value) + + case TouchStart, TouchEnd, TouchMove, TouchCancel: + return view.setTouchListener(tag, value) + + case ResizeEvent, ScrollEvent: + return view.setFrameListener(tag, value) + } + + if view.viewStyle.set(tag, value) { + if view.created { + view.propertyChanged(tag) + } + return true + } + + return false +} + +func (view *viewData) propertyChanged(tag string) { + + if view.updateTransformProperty(tag) { + return + } + + htmlID := view.htmlID() + session := view.session + + switch tag { + case Disabled: + updateInnerHTML(view.parentHTMLID(), session) + + case Background: + updateCSSProperty(htmlID, Background, view.backgroundCSS(view), session) + return + + case Border: + if getBorder(view, Border) == nil { + updateCSSProperty(htmlID, BorderWidth, "", session) + updateCSSProperty(htmlID, BorderColor, "", session) + updateCSSProperty(htmlID, BorderStyle, "none", session) + return + } + fallthrough + + case BorderLeft, BorderRight, BorderTop, BorderBottom: + if border := getBorder(view, Border); border != nil { + updateCSSProperty(htmlID, BorderWidth, border.cssWidthValue(session), session) + updateCSSProperty(htmlID, BorderColor, border.cssColorValue(session), session) + updateCSSProperty(htmlID, BorderStyle, border.cssStyleValue(session), session) + } + return + + case BorderStyle, BorderLeftStyle, BorderRightStyle, BorderTopStyle, BorderBottomStyle: + if border := getBorder(view, Border); border != nil { + updateCSSProperty(htmlID, BorderStyle, border.cssStyleValue(session), session) + } + return + + case BorderColor, BorderLeftColor, BorderRightColor, BorderTopColor, BorderBottomColor: + if border := getBorder(view, Border); border != nil { + updateCSSProperty(htmlID, BorderColor, border.cssColorValue(session), session) + } + return + + case BorderWidth, BorderLeftWidth, BorderRightWidth, BorderTopWidth, BorderBottomWidth: + if border := getBorder(view, Border); border != nil { + updateCSSProperty(htmlID, BorderWidth, border.cssWidthValue(session), session) + } + return + + case Outline, OutlineColor, OutlineStyle, OutlineWidth: + updateCSSProperty(htmlID, Outline, GetOutline(view, "").cssString(), session) + return + + case Shadow: + updateCSSProperty(htmlID, "box-shadow", shadowCSS(view, Shadow, session), session) + return + + case TextShadow: + updateCSSProperty(htmlID, "text-shadow", shadowCSS(view, TextShadow, session), session) + return + + case Radius, RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY, + RadiusTopRight, RadiusTopRightX, RadiusTopRightY, + RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY, + RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY: + radius := GetRadius(view, "") + updateCSSProperty(htmlID, "border-radius", radius.cssString(), session) + return + + case Margin, MarginTop, MarginRight, MarginBottom, MarginLeft, + "top-margin", "right-margin", "bottom-margin", "left-margin": + margin := GetMargin(view, "") + updateCSSProperty(htmlID, Margin, margin.cssString(), session) + return + + case Padding, PaddingTop, PaddingRight, PaddingBottom, PaddingLeft, + "top-padding", "right-padding", "bottom-padding", "left-padding": + padding := GetPadding(view, "") + updateCSSProperty(htmlID, Padding, padding.cssString(), session) + return + + case AvoidBreak: + if avoid, ok := boolProperty(view, AvoidBreak, session); ok { + if avoid { + updateCSSProperty(htmlID, "break-inside", "avoid", session) + } else { + updateCSSProperty(htmlID, "break-inside", "auto", session) + } + } + return + + case Clip: + if clip := getClipShape(view, Clip, session); clip != nil && clip.valid(session) { + updateCSSProperty(htmlID, `clip-path`, clip.cssStyle(session), session) + } else { + updateCSSProperty(htmlID, `clip-path`, "none", session) + } + return + + case ShapeOutside: + if clip := getClipShape(view, ShapeOutside, session); clip != nil && clip.valid(session) { + updateCSSProperty(htmlID, ShapeOutside, clip.cssStyle(session), session) + } else { + updateCSSProperty(htmlID, ShapeOutside, "none", session) + } + return + + case Filter: + text := "" + if value := view.getRaw(Filter); value != nil { + if filter, ok := value.(ViewFilter); ok { + text = filter.cssStyle(session) + } + } + updateCSSProperty(htmlID, Filter, text, session) + return + + case FontName: + if font, ok := stringProperty(view, FontName, session); ok { + updateCSSProperty(htmlID, "font-family", font, session) + } else { + updateCSSProperty(htmlID, "font-family", "", session) + } + return + + case Italic: + if state, ok := boolProperty(view, tag, session); ok { + if state { + updateCSSProperty(htmlID, "font-style", "italic", session) + } else { + updateCSSProperty(htmlID, "font-style", "normal", session) + } + } else { + updateCSSProperty(htmlID, "font-style", "", session) + } + + case SmallCaps: + if state, ok := boolProperty(view, tag, session); ok { + if state { + updateCSSProperty(htmlID, "font-variant", "small-caps", session) + } else { + updateCSSProperty(htmlID, "font-variant", "normal", session) + } + } else { + updateCSSProperty(htmlID, "font-variant", "", session) + } + + case Strikethrough, Overline, Underline: + updateCSSProperty(htmlID, "text-decoration", view.cssTextDecoration(session), session) + for _, tag2 := range []string{TextLineColor, TextLineStyle, TextLineThickness} { + view.propertyChanged(tag2) + } + + } + + if cssTag, ok := sizeProperties[tag]; ok { + size, _ := sizeProperty(view, tag, session) + updateCSSProperty(htmlID, cssTag, size.cssString(""), session) + return + } + + colorTags := map[string]string{ + BackgroundColor: BackgroundColor, + TextColor: "color", + TextLineColor: "text-decoration-color", + } + if cssTag, ok := colorTags[tag]; ok { + if color, ok := colorProperty(view, tag, session); ok { + updateCSSProperty(htmlID, cssTag, color.cssString(), session) + } else { + updateCSSProperty(htmlID, cssTag, "", session) + } + return + } + + if valuesData, ok := enumProperties[tag]; ok && valuesData.cssTag != "" { + n, _ := enumProperty(view, tag, session, 0) + updateCSSProperty(htmlID, valuesData.cssTag, valuesData.cssValues[n], session) + return + } + + for _, floatTag := range []string{ScaleX, ScaleY, ScaleZ, RotateX, RotateY, RotateZ} { + if tag == floatTag { + if f, ok := floatProperty(view, floatTag, session, 0); ok { + updateCSSProperty(htmlID, floatTag, strconv.FormatFloat(f, 'g', -1, 64), session) + } + return + } + } +} + +func (view *viewData) Get(tag string) interface{} { + return view.get(strings.ToLower(tag)) +} + +func (view *viewData) get(tag string) interface{} { + return view.viewStyle.get(tag) +} + +func (view *viewData) htmlTag() string { + if semantics := GetSemantics(view, ""); semantics > DefaultSemantics { + values := enumProperties[Semantics].cssValues + if semantics < len(values) { + return values[semantics] + } + } + return "div" +} + +func (view *viewData) closeHTMLTag() bool { + return true +} + +func (view *viewData) htmlID() string { + if view._htmlID == "" { + view._htmlID = view.session.nextViewID() + } + return view._htmlID +} + +func (view *viewData) htmlSubviews(self View, buffer *strings.Builder) { +} + +func (view *viewData) addToCSSStyle(addCSS map[string]string) { + view.addCSS = addCSS +} + +func (view *viewData) cssStyle(self View, builder cssBuilder) { + view.viewStyle.cssViewStyle(builder, view.session, self) + switch GetVisibility(view, "") { + case Invisible: + builder.add(`visibility`, `hidden`) + + case Gone: + builder.add(`display`, `none`) + } + + if view.addCSS != nil { + for tag, value := range view.addCSS { + builder.add(tag, value) + } + } +} + +func (view *viewData) htmlProperties(self View, buffer *strings.Builder) { + view.created = true + if view.frame.Left != 0 || view.frame.Top != 0 || view.frame.Width != 0 || view.frame.Height != 0 { + buffer.WriteString(fmt.Sprintf(` data-left="%g" data-top="%g" data-width="%g" data-height="%g"`, + view.frame.Left, view.frame.Top, view.frame.Width, view.frame.Height)) + } +} + +func (view *viewData) htmlDisabledProperties(self View, buffer *strings.Builder) { + if IsDisabled(self) { + buffer.WriteString(` data-disabled="1"`) + } else { + buffer.WriteString(` data-disabled="0"`) + } +} + +func viewHTML(view View, buffer *strings.Builder) { + viewHTMLTag := view.htmlTag() + buffer.WriteRune('<') + buffer.WriteString(viewHTMLTag) + buffer.WriteString(` id="`) + buffer.WriteString(view.htmlID()) + buffer.WriteRune('"') + + disabled := IsDisabled(view) + + if cls := view.htmlClass(disabled); cls != "" { + buffer.WriteString(` class="`) + buffer.WriteString(cls) + buffer.WriteRune('"') + } + + var cssBuilder viewCSSBuilder + view.cssStyle(view, &cssBuilder) + + if style := cssBuilder.finish(); style != "" { + buffer.WriteString(` style="`) + buffer.WriteString(style) + buffer.WriteRune('"') + } + + buffer.WriteRune(' ') + view.htmlProperties(view, buffer) + buffer.WriteRune(' ') + view.htmlDisabledProperties(view, buffer) + + if view.isNoResizeEvent() { + buffer.WriteString(` data-noresize="1" `) + } else { + buffer.WriteRune(' ') + } + + if view.Focusable() && !disabled { + buffer.WriteString(`tabindex="0" `) + } + + buffer.WriteString(`onscroll="scrollEvent(this, event)" `) + + keyEventsHtml(view, buffer) + mouseEventsHtml(view, buffer) + pointerEventsHtml(view, buffer) + touchEventsHtml(view, buffer) + focusEventsHtml(view, buffer) + + buffer.WriteRune('>') + view.htmlSubviews(view, buffer) + if view.closeHTMLTag() { + buffer.WriteString(`') + } +} + +func (view *viewData) htmlClass(disabled bool) string { + cls := "ruiView" + disabledStyle := false + if disabled { + if value, ok := stringProperty(view, StyleDisabled, view.Session()); ok && value != "" { + cls += " " + value + disabledStyle = true + } + } + if !disabledStyle { + if value, ok := stringProperty(view, Style, view.Session()); ok { + cls += " " + value + } + } + + if view.systemClass != "" { + cls = view.systemClass + " " + cls + } + + return cls +} + +func (view *viewData) handleCommand(self View, command string, data DataObject) bool { + switch command { + + case KeyDownEvent, KeyUpEvent: + if !IsDisabled(self) { + handleKeyEvents(self, command, data) + } + + case ClickEvent, DoubleClickEvent, MouseDown, MouseUp, MouseMove, MouseOut, MouseOver, ContextMenuEvent: + handleMouseEvents(self, command, data) + + case PointerDown, PointerUp, PointerMove, PointerOut, PointerOver, PointerCancel: + handlePointerEvents(self, command, data) + + case TouchStart, TouchEnd, TouchMove, TouchCancel: + handleTouchEvents(self, command, data) + + case FocusEvent, LostFocusEvent: + for _, listener := range getFocusListeners(view, "", command) { + listener(self) + } + + case "scroll": + view.onScroll(view, dataFloatProperty(data, "x"), dataFloatProperty(data, "y"), dataFloatProperty(data, "width"), dataFloatProperty(data, "height")) + + case "widthChanged": + if value, ok := data.PropertyValue("width"); ok { + if width, ok := StringToSizeUnit(value); ok { + self.setRaw(Width, width) + } + } + + case "heightChanged": + if value, ok := data.PropertyValue("height"); ok { + if height, ok := StringToSizeUnit(value); ok { + self.setRaw(Height, height) + } + } + + case "transitionEnd": + if property, ok := data.PropertyValue("property"); ok { + if animation, ok := view.animation[property]; ok { + delete(view.animation, property) + view.updateTransitionCSS() + if animation.FinishListener != nil { + animation.FinishListener.OnAnimationFinished(self, property) + } + } + return true + } + /* + case "resize": + floatProperty := func(tag string) float64 { + if value, ok := data.PropertyValue(tag); ok { + if result, err := strconv.ParseFloat(value, 64); err == nil { + return result + } + } + return 0 + } + + self.onResize(self, floatProperty("x"), floatProperty("y"), floatProperty("width"), floatProperty("height")) + return true + */ + default: + return false + } + return true + +} + +func ruiViewString(view View, viewTag string, writer ruiWriter) { + writer.startObject(viewTag) + + tags := view.AllTags() + count := len(tags) + if count > 0 { + if count > 1 { + tagToStart := func(tag string) { + for i, t := range tags { + if t == tag { + if i > 0 { + for n := i; n > 0; n-- { + tags[n] = tags[n-1] + } + tags[0] = tag + } + return + } + } + } + tagToStart(StyleDisabled) + tagToStart(Style) + tagToStart(ID) + } + + for _, tag := range tags { + if value := view.Get(tag); value != nil { + writer.writeProperty(tag, value) + } + } + } + + writer.endObject() +} + +func (view *viewData) ruiString(writer ruiWriter) { + ruiViewString(view, view.Tag(), writer) +} + +func (view *viewData) String() string { + writer := newRUIWriter() + view.ruiString(writer) + return writer.finish() +} + +// IsDisabled returns "true" if the view is disabled +func IsDisabled(view View) bool { + if disabled, _ := boolProperty(view, Disabled, view.Session()); disabled { + return true + } + if parent := view.Parent(); parent != nil { + return IsDisabled(parent) + } + return false +} diff --git a/viewAnimation.go b/viewAnimation.go new file mode 100644 index 0000000..cfa46f8 --- /dev/null +++ b/viewAnimation.go @@ -0,0 +1,170 @@ +package rui + +import ( + "fmt" + "strconv" + "strings" +) + +const ( + // EaseTiming - a timing function which increases in velocity towards the middle of the transition, slowing back down at the end + EaseTiming = "ease" + // EaseInTiming - a timing function which starts off slowly, with the transition speed increasing until complete + EaseInTiming = "ease-in" + // EaseOutTiming - a timing function which starts transitioning quickly, slowing down the transition continues. + EaseOutTiming = "ease-out" + // EaseInOutTiming - a timing function which starts transitioning slowly, speeds up, and then slows down again. + EaseInOutTiming = "ease-in-out" + // LinearTiming - a timing function at an even speed + LinearTiming = "linear" +) + +// StepsTiming return a timing function along stepCount stops along the transition, diplaying each stop for equal lengths of time +func StepsTiming(stepCount int) string { + return "steps(" + strconv.Itoa(stepCount) + ")" +} + +// CubicBezierTiming return a cubic-Bezier curve timing function. x1 and x2 must be in the range [0, 1]. +func CubicBezierTiming(x1, y1, x2, y2 float64) string { + if x1 < 0 { + x1 = 0 + } else if x1 > 1 { + x1 = 1 + } + if x2 < 0 { + x2 = 0 + } else if x2 > 1 { + x2 = 1 + } + return fmt.Sprintf("cubic-bezier(%g, %g, %g, %g)", x1, y1, x2, y2) +} + +// AnimationFinishedListener describes the end of an animation event handler +type AnimationFinishedListener interface { + // OnAnimationFinished is called when a property animation is finished + OnAnimationFinished(view View, property string) +} + +type Animation struct { + // Duration defines the time in seconds an animation should take to complete + Duration float64 + // TimingFunction defines how intermediate values are calculated for a property being affected + // by an animation effect. If the value is "" then the "ease" function is used + TimingFunction string + // Delay defines the duration in seconds to wait before starting a property's animation. + Delay float64 + // FinishListener defines the end of an animation event handler + FinishListener AnimationFinishedListener +} + +type animationFinishedFunc struct { + finishFunc func(View, string) +} + +func (listener animationFinishedFunc) OnAnimationFinished(view View, property string) { + if listener.finishFunc != nil { + listener.finishFunc(view, property) + } +} + +func AnimationFinishedFunc(finishFunc func(View, string)) AnimationFinishedListener { + listener := new(animationFinishedFunc) + listener.finishFunc = finishFunc + return listener +} + +func validateTimingFunction(timingFunction string) bool { + switch timingFunction { + case "", EaseTiming, EaseInTiming, EaseOutTiming, EaseInOutTiming, LinearTiming: + return true + } + + size := len(timingFunction) + if size > 0 && timingFunction[size-1] == ')' { + if index := strings.IndexRune(timingFunction, '('); index > 0 { + args := timingFunction[index+1 : size-1] + switch timingFunction[:index] { + case "steps": + if _, err := strconv.Atoi(strings.Trim(args, " \t\n")); err == nil { + return true + } + + case "cubic-bezier": + if params := strings.Split(args, ","); len(params) == 4 { + for _, param := range params { + if _, err := strconv.ParseFloat(strings.Trim(param, " \t\n"), 64); err != nil { + return false + } + } + return true + } + } + } + } + + return false +} + +func (view *viewData) SetAnimated(tag string, value interface{}, animation Animation) bool { + timingFunction, ok := view.session.resolveConstants(animation.TimingFunction) + if !ok || animation.Duration <= 0 || !validateTimingFunction(timingFunction) { + if view.Set(tag, value) { + if animation.FinishListener != nil { + animation.FinishListener.OnAnimationFinished(view, tag) + } + return true + } + return false + } + + updateProperty(view.htmlID(), "ontransitionend", "transitionEndEvent(this, event)", view.session) + updateProperty(view.htmlID(), "ontransitioncancel", "transitionCancelEvent(this, event)", view.session) + animation.TimingFunction = timingFunction + view.animation[tag] = animation + view.updateTransitionCSS() + + result := view.Set(tag, value) + if !result { + delete(view.animation, tag) + view.updateTransitionCSS() + } + + return result +} + +func (view *viewData) transitionCSS() string { + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + for tag, animation := range view.animation { + if buffer.Len() > 0 { + buffer.WriteString(", ") + } + buffer.WriteString(tag) + buffer.WriteString(fmt.Sprintf(" %gs", animation.Duration)) + if animation.TimingFunction != "" { + buffer.WriteRune(' ') + buffer.WriteString(animation.TimingFunction) + } + if animation.Delay > 0 { + if animation.TimingFunction == "" { + buffer.WriteString(" ease") + } + buffer.WriteString(fmt.Sprintf(" %gs", animation.Delay)) + } + } + return buffer.String() +} + +func (view *viewData) updateTransitionCSS() { + updateCSSProperty(view.htmlID(), "transition", view.transitionCSS(), view.Session()) +} + +// SetAnimated sets the property with name "tag" of the "rootView" subview with "viewID" id by value. Result: +// true - success, +// false - error (incompatible type or invalid format of a string value, see AppLog). +func SetAnimated(rootView View, viewID, tag string, value interface{}, animation Animation) bool { + if view := ViewByID(rootView, viewID); view != nil { + return view.SetAnimated(tag, value, animation) + } + return false +} diff --git a/viewByID.go b/viewByID.go new file mode 100644 index 0000000..074bf1e --- /dev/null +++ b/viewByID.go @@ -0,0 +1,266 @@ +package rui + +// 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 { + if rootView == nil { + ErrorLog(`ViewByID(nil, "` + id + `"): rootView is nil`) + return nil + } + if rootView.ID() == id { + return rootView + } + if container, ok := rootView.(ParanetView); ok { + if view := viewByID(container, id); view != nil { + return view + } + } + ErrorLog(`ViewByID(_, "` + id + `"): View not found`) + return nil +} + +func viewByID(rootView ParanetView, id string) View { + for _, view := range rootView.Views() { + if view != nil { + if view.ID() == id { + return view + } + if container, ok := view.(ParanetView); ok { + if v := viewByID(container, id); v != nil { + return v + } + } + } + } + + 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 { + if list, ok := view.(ViewsContainer); ok { + return list + } + ErrorLog(`ViewsContainerByID(_, "` + id + `"): The found View is not 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 { + if list, ok := view.(ListLayout); ok { + return list + } + ErrorLog(`ListLayoutByID(_, "` + id + `"): The found View is not 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 { + if list, ok := view.(StackLayout); ok { + return list + } + ErrorLog(`StackLayoutByID(_, "` + id + `"): The found View is not 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 { + if list, ok := view.(GridLayout); ok { + return list + } + ErrorLog(`GridLayoutByID(_, "` + id + `"): The found View is not 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 { + if list, ok := view.(ColumnLayout); ok { + return list + } + ErrorLog(`ColumnLayoutByID(_, "` + id + `"): The found View is not 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 { + if details, ok := view.(DetailsView); ok { + return details + } + ErrorLog(`DetailsViewByID(_, "` + id + `"): The found View is not 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 { + if list, ok := view.(DropDownList); ok { + return list + } + ErrorLog(`DropDownListByID(_, "` + id + `"): The found View is not 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 { + if list, ok := view.(TabsLayout); ok { + return list + } + ErrorLog(`TabsLayoutByID(_, "` + id + `"): The found View is not 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 { + if list, ok := view.(ListView); ok { + return list + } + ErrorLog(`ListViewByID(_, "` + id + `"): The found View is not 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 { + if text, ok := view.(TextView); ok { + return text + } + ErrorLog(`TextViewByID(_, "` + id + `"): The found View is not 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 { + if button, ok := view.(Button); ok { + return button + } + ErrorLog(`ButtonByID(_, "` + id + `"): The found View is not 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 { + if checkbox, ok := view.(Checkbox); ok { + return checkbox + } + ErrorLog(`CheckboxByID(_, "` + id + `"): The found View is not 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 { + if buttons, ok := view.(EditView); ok { + return buttons + } + ErrorLog(`EditViewByID(_, "` + id + `"): The found View is not 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 { + if buttons, ok := view.(ProgressBar); ok { + return buttons + } + ErrorLog(`ProgressBarByID(_, "` + id + `"): The found View is not ProgressBar`) + } + 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 { + if input, ok := view.(NumberPicker); ok { + return input + } + ErrorLog(`NumberPickerByID(_, "` + id + `"): The found View is not NumberPicker`) + } + 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 { + if canvas, ok := view.(CanvasView); ok { + return canvas + } + ErrorLog(`CanvasViewByID(_, "` + id + `"): The found View is not 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 { + if canvas, ok := view.(AudioPlayer); ok { + return canvas + } + ErrorLog(`AudioPlayerByID(_, "` + id + `"): The found View is not 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 { + if canvas, ok := view.(VideoPlayer); ok { + return canvas + } + ErrorLog(`VideoPlayerByID(_, "` + id + `"): The found View is not VideoPlayer`) + } + return nil +} diff --git a/viewClip.go b/viewClip.go new file mode 100644 index 0000000..bfedf35 --- /dev/null +++ b/viewClip.go @@ -0,0 +1,594 @@ +package rui + +import ( + "fmt" + "strings" +) + +// ClipShape defines a View clipping area +type ClipShape interface { + Properties + fmt.Stringer + ruiStringer + cssStyle(session Session) string + valid(session Session) bool +} + +type insetClip struct { + propertyList +} + +type ellipseClip struct { + propertyList +} + +type polygonClip struct { + points []interface{} +} + +// 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(ellipseClip) + 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 []interface{}) ClipShape { + clip := new(polygonClip) + clip.points = []interface{}{} + 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 = []interface{}{} + if clip.Set(Points, points) { + return clip + } + return nil +} + +func (clip *insetClip) Set(tag string, value interface{}) 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 { + writer := newRUIWriter() + clip.ruiString(writer) + return writer.finish() +} + +func (clip *insetClip) ruiString(writer ruiWriter) { + writer.startObject("inset") + for _, tag := range []string{Top, Right, Bottom, Left} { + if value, ok := clip.properties[tag]; ok { + switch value := value.(type) { + case string: + writer.writeProperty(tag, value) + + case fmt.Stringer: + writer.writeProperty(tag, value.String()) + } + } + } + + if value := clip.Get(Radius); value != nil { + switch value := value.(type) { + case RadiusProperty: + writer.writeProperty(Radius, value.String()) + + case SizeUnit: + writer.writeProperty(Radius, value.String()) + + case string: + writer.writeProperty(Radius, value) + } + } + + writer.endObject() +} + +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")) + leadText = " " + } + + if radius := getRadiusProperty(clip); radius != nil { + buffer.WriteString(" round ") + buffer.WriteString(radius.BoxRadius(session).cssString()) + } + + 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 *ellipseClip) Set(tag string, value interface{}) bool { + if value == nil { + clip.Remove(tag) + } + + switch strings.ToLower(tag) { + case X, Y: + return clip.setSizeProperty(tag, value) + + case Radius: + result := clip.setSizeProperty(tag, value) + if result { + delete(clip.properties, RadiusX) + delete(clip.properties, RadiusY) + } + return result + + case RadiusX: + result := clip.setSizeProperty(tag, value) + if result { + if r, ok := clip.properties[Radius]; ok { + clip.properties[RadiusY] = r + delete(clip.properties, Radius) + } + } + return result + + case RadiusY: + result := clip.setSizeProperty(tag, value) + if result { + if r, ok := clip.properties[Radius]; ok { + clip.properties[RadiusX] = r + delete(clip.properties, Radius) + } + } + return result + } + + ErrorLogF(`"%s" property is not supported by the inset clip shape`, tag) + return false +} + +func (clip *ellipseClip) String() string { + writer := newRUIWriter() + clip.ruiString(writer) + return writer.finish() +} + +func (clip *ellipseClip) ruiString(writer ruiWriter) { + writeProperty := func(tag string, value interface{}) { + switch value := value.(type) { + case string: + writer.writeProperty(tag, value) + + case fmt.Stringer: + writer.writeProperty(tag, value.String()) + } + } + + if r, ok := clip.properties[Radius]; ok { + writer.startObject("circle") + writeProperty(Radius, r) + } else { + writer.startObject("ellipse") + for _, tag := range []string{RadiusX, RadiusY} { + if value, ok := clip.properties[tag]; ok { + writeProperty(tag, value) + } + } + } + + for _, tag := range []string{X, Y} { + if value, ok := clip.properties[tag]; ok { + writeProperty(tag, value) + } + } + writer.endObject() +} + +func (clip *ellipseClip) cssStyle(session Session) string { + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + if r, ok := sizeProperty(clip, Radius, session); ok { + buffer.WriteString("circle(") + buffer.WriteString(r.cssString("0")) + } else { + rx, _ := sizeProperty(clip, RadiusX, session) + ry, _ := sizeProperty(clip, RadiusX, session) + buffer.WriteString("ellipse(") + buffer.WriteString(rx.cssString("0")) + buffer.WriteRune(' ') + buffer.WriteString(ry.cssString("0")) + } + + buffer.WriteString(" at ") + x, _ := sizeProperty(clip, X, session) + buffer.WriteString(x.cssString("0")) + buffer.WriteRune(' ') + + y, _ := sizeProperty(clip, Y, session) + buffer.WriteString(y.cssString("0")) + buffer.WriteRune(')') + + return buffer.String() +} + +func (clip *ellipseClip) valid(session Session) bool { + if value, ok := sizeProperty(clip, Radius, session); ok && value.Type != Auto && value.Value != 0 { + return true + } + + rx, okX := sizeProperty(clip, RadiusX, session) + ry, okY := sizeProperty(clip, RadiusY, session) + return okX && okY && rx.Type != Auto && rx.Value != 0 && ry.Type != Auto && ry.Value != 0 +} + +func (clip *polygonClip) Get(tag string) interface{} { + if Points == strings.ToLower(tag) { + return clip.points + } + return nil +} + +func (clip *polygonClip) getRaw(tag string) interface{} { + return clip.Get(tag) +} + +func (clip *polygonClip) Set(tag string, value interface{}) bool { + if Points == strings.ToLower(tag) { + switch value := value.(type) { + case []interface{}: + result := true + clip.points = make([]interface{}, 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([]interface{}, len(value)) + for i, point := range value { + clip.points[i] = point + } + return true + + case string: + result := true + values := strings.Split(value, ",") + clip.points = make([]interface{}, 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 interface{}) { + clip.Set(tag, value) +} + +func (clip *polygonClip) Remove(tag string) { + if Points == strings.ToLower(tag) { + clip.points = []interface{}{} + } +} + +func (clip *polygonClip) Clear() { + clip.points = []interface{}{} +} + +func (clip *polygonClip) AllTags() []string { + return []string{Points} +} + +func (clip *polygonClip) String() string { + writer := newRUIWriter() + clip.ruiString(writer) + return writer.finish() +} + +func (clip *polygonClip) ruiString(writer ruiWriter) { + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + writer.startObject("polygon") + + if clip.points != nil { + for i, value := range clip.points { + if i > 0 { + buffer.WriteString(", ") + } + switch value := value.(type) { + case string: + buffer.WriteString(value) + + case fmt.Stringer: + buffer.WriteString(value.String()) + + default: + buffer.WriteString("0px") + } + } + + writer.writeProperty(Points, buffer.String()) + } + + writer.endObject() +} + +func (clip *polygonClip) cssStyle(session Session) string { + + if clip.points == nil { + return "" + } + + count := len(clip.points) + if count < 2 { + return "" + } + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + writePoint := func(value interface{}) { + switch value := value.(type) { + case string: + if val, ok := session.resolveConstants(value); ok { + if size, ok := StringToSizeUnit(val); ok { + buffer.WriteString(size.cssString("0px")) + return + } + } + + case SizeUnit: + buffer.WriteString(value.cssString("0px")) + 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 interface{}) 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 "" then a top position of the first argument (view) is returned +func GetClip(view View, subviewID string) ClipShape { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + 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 "" then a top position of the first argument (view) is returned +func GetShapeOutside(view View, subviewID string) ClipShape { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + return getClipShape(view, ShapeOutside, view.Session()) + } + + return nil +} diff --git a/viewFactory.go b/viewFactory.go new file mode 100644 index 0000000..f30f63e --- /dev/null +++ b/viewFactory.go @@ -0,0 +1,138 @@ +package rui + +import ( + "path/filepath" + "strings" +) + +var viewCreators = map[string]func(Session) View{ + "View": newView, + "ColumnLayout": newColumnLayout, + "ListLayout": newListLayout, + "GridLayout": newGridLayout, + "StackLayout": newStackLayout, + "TabsLayout": newTabsLayout, + "AbsoluteLayout": newAbsoluteLayout, + "Resizable": newResizable, + "DetailsView": newDetailsView, + "TextView": newTextView, + "Button": newButton, + "Checkbox": newCheckbox, + "DropDownList": newDropDownList, + "ProgressBar": newProgressBar, + "NumberPicker": newNumberPicker, + "ColorPicker": newColorPicker, + "DatePicker": newDatePicker, + "TimePicker": newTimePicker, + "EditView": newEditView, + "ListView": newListView, + "CanvasView": newCanvasView, + "ImageView": newImageView, + "TableView": newTableView, + "AudioPlayer": newAudioPlayer, + "VideoPlayer": newVideoPlayer, +} + +// RegisterViewCreator register function of creating view +func RegisterViewCreator(tag string, creator func(Session) View) bool { + builtinViews := []string{ + "View", + "ViewsContainer", + "ColumnLayout", + "ListLayout", + "GridLayout", + "StackLayout", + "TabsLayout", + "AbsoluteLayout", + "Resizable", + "DetailsView", + "TextView", + "Button", + "Checkbox", + "DropDownList", + "ProgressBar", + "NumberPicker", + "ColorPicker", + "DatePicker", + "TimePicker", + "EditView", + "ListView", + "CanvasView", + "ImageView", + "TableView", + } + + for _, name := range builtinViews { + if name == tag { + return false + } + } + + viewCreators[tag] = creator + return true +} + +// CreateViewFromObject create new View and initialize it by Node data +func CreateViewFromObject(session Session, object DataObject) View { + tag := object.Tag() + + if creator, ok := viewCreators[tag]; ok { + if !session.ignoreViewUpdates() { + session.setIgnoreViewUpdates(true) + defer session.setIgnoreViewUpdates(false) + } + view := creator(session) + if customView, ok := view.(CustomView); ok { + if !InitCustomView(customView, tag, session, nil) { + return nil + } + } + parseProperties(view, object) + return view + } + + ErrorLog(`Unknown view type "` + object.Tag() + `"`) + return nil +} + +// CreateViewFromText create new View and initialize it by content of text +func CreateViewFromText(session Session, text string) View { + if data := ParseDataText(text); data != nil { + return CreateViewFromObject(session, data) + } + return nil +} + +// CreateViewFromResources create new View and initialize it by the content of +// the resource file from "views" directory +func CreateViewFromResources(session Session, name string) View { + if strings.ToLower(filepath.Ext(name)) != ".rui" { + name += ".rui" + } + + for _, fs := range resources.embedFS { + rootDirs := embedRootDirs(fs) + for _, dir := range rootDirs { + switch dir { + case imageDir, themeDir, rawDir: + // do nothing + + case viewDir: + if data, err := fs.ReadFile(dir + "/" + name); err == nil { + if data := ParseDataText(string(data)); data != nil { + return CreateViewFromObject(session, data) + } + } + + default: + if data, err := fs.ReadFile(dir + "/" + viewDir + "/" + name); err == nil { + if data := ParseDataText(string(data)); data != nil { + return CreateViewFromObject(session, data) + } + } + } + } + } + + return nil +} diff --git a/viewFilter.go b/viewFilter.go new file mode 100644 index 0000000..eaca1c6 --- /dev/null +++ b/viewFilter.go @@ -0,0 +1,264 @@ +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 + ruiStringer + cssStyle(session Session) string +} + +type viewFilter struct { + propertyList +} + +// NewViewFilter creates the new ViewFilter +func NewViewFilter(params Params) ViewFilter { + 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 interface{}) 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 { + writer := newRUIWriter() + filter.ruiString(writer) + return writer.finish() +} + +func (filter *viewFilter) ruiString(writer ruiWriter) { + writer.startObject("filter") + for tag, value := range filter.properties { + writer.writeProperty(tag, value) + } + writer.endObject() +} + +func (filter *viewFilter) cssStyle(session Session) string { + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + if value, ok := floatProperty(filter, Blur, session, 0); ok { + size := SizeUnit{Type: SizeInPixel, Value: value} + buffer.WriteString(Blur) + buffer.WriteRune('(') + buffer.WriteString(size.cssString("0px")) + buffer.WriteRune(')') + } + + for _, tag := range []string{Brightness, Contrast, Saturate, Grayscale, Invert, Opacity, Sepia} { + if value, ok := floatProperty(filter, tag, session, 0); ok { + if buffer.Len() > 0 { + buffer.WriteRune(' ') + } + buffer.WriteString(fmt.Sprintf("%s(%g%%)", tag, value)) + } + } + + 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(value interface{}) bool { + switch value := value.(type) { + case ViewFilter: + style.properties[Filter] = value + return true + + case string: + if obj := NewDataObject(value); obj == nil { + if filter := newViewFilter(obj); filter != nil { + style.properties[Filter] = filter + return true + } + } + case DataObject: + if filter := newViewFilter(value); filter != nil { + style.properties[Filter] = filter + return true + } + + case DataValue: + if value.IsObject() { + if filter := newViewFilter(value.Object()); filter != nil { + style.properties[Filter] = filter + return true + } + } + } + + notCompatibleType(Filter, value) + return false +} + +// GetFilter returns a View graphical effects like blur or color shift. +// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned +func GetFilter(view View, subviewID string) ViewFilter { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if value := view.getRaw(Filter); value != nil { + if filter, ok := value.(ViewFilter); ok { + return filter + } + } + } + + return nil +} diff --git a/viewStyle.go b/viewStyle.go new file mode 100644 index 0000000..f76e9a8 --- /dev/null +++ b/viewStyle.go @@ -0,0 +1,422 @@ +package rui + +import ( + "fmt" + "strconv" + "strings" +) + +// ViewStyle interface of the style of view +type ViewStyle interface { + Properties + cssViewStyle(buffer cssBuilder, session Session, view View) +} + +type viewStyle struct { + propertyList + //transitions map[string]ViewTransition +} + +// 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 (style *viewStyle) init() { + style.propertyList.init() + //style.shadows = []ViewShadow{} + //style.transitions = map[string]ViewTransition{} +} + +// NewViewStyle create new ViewStyle object +func NewViewStyle(params Params) ViewStyle { + style := new(viewStyle) + style.init() + for tag, value := range params { + style.Set(tag, value) + } + return style +} + +func (style *viewStyle) cssTextDecoration(session Session) string { + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + noDecoration := false + if strikethrough, ok := boolProperty(style, Strikethrough, session); ok { + if strikethrough { + buffer.WriteString("line-through") + } + noDecoration = true + } + + if overline, ok := boolProperty(style, Overline, session); ok { + if overline { + if buffer.Len() > 0 { + buffer.WriteRune(' ') + } + buffer.WriteString("overline") + } + noDecoration = true + } + + if underline, ok := boolProperty(style, Underline, session); ok { + if underline { + if buffer.Len() > 0 { + buffer.WriteRune(' ') + } + buffer.WriteString("underline") + } + noDecoration = true + } + + if buffer.Len() == 0 && noDecoration { + return "none" + } + + return buffer.String() +} + +func split4Values(text string) []string { + values := strings.Split(text, ",") + count := len(values) + switch count { + case 1, 4: + return values + + case 2: + if strings.Trim(values[1], " \t\r\n") == "" { + return values[:1] + } + + case 5: + if strings.Trim(values[4], " \t\r\n") != "" { + return values[:4] + } + } + return []string{} +} + +func (style *viewStyle) backgroundCSS(view View) 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(view); 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, view View) { + + if margin, ok := boundsProperty(style, Margin, session); ok { + margin.cssValue(Margin, builder) + } + + if padding, ok := boundsProperty(style, Padding, session); ok { + padding.cssValue(Padding, builder) + } + + if border := getBorder(style, Border); border != nil { + border.cssStyle(builder, session) + border.cssWidth(builder, session) + border.cssColor(builder, session) + } + + radius := getRadius(style, session) + radius.cssValue(builder) + + if outline := getOutline(style); outline != nil { + outline.ViewOutline(session).cssValue(builder) + } + + if z, ok := intProperty(style, ZIndex, session, 0); ok { + builder.add(ZIndex, strconv.Itoa(z)) + } + + if opacity, ok := floatProperty(style, Opacity, session, 1.0); ok && opacity >= 0 && opacity <= 1 { + builder.add(Opacity, strconv.FormatFloat(opacity, 'f', 3, 32)) + } + + if n, ok := intProperty(style, ColumnCount, session, 0); ok && n > 0 { + builder.add(ColumnCount, strconv.Itoa(n)) + } + + for _, tag := range []string{ + Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight, Left, Right, Top, Bottom, + TextSize, TextIndent, LetterSpacing, WordSpacing, LineHeight, TextLineThickness, + GridRowGap, GridColumnGap, ColumnGap, ColumnWidth} { + + if size, ok := sizeProperty(style, tag, session); ok && size.Type != Auto { + cssTag, ok := sizeProperties[tag] + if !ok { + cssTag = tag + } + builder.add(cssTag, size.cssString("")) + } + } + + colorProperties := []struct{ property, cssTag string }{ + {BackgroundColor, BackgroundColor}, + {TextColor, "color"}, + {TextLineColor, "text-decoration-color"}, + } + for _, p := range colorProperties { + if color, ok := colorProperty(style, p.property, session); ok && color != 0 { + builder.add(p.cssTag, color.cssString()) + } + } + + if value, ok := enumProperty(style, BackgroundClip, session, 0); ok { + builder.add(BackgroundClip, enumProperties[BackgroundClip].values[value]) + } + + if background := style.backgroundCSS(view); background != "" { + builder.add("background", background) + } + + if font, ok := stringProperty(style, FontName, session); ok && font != "" { + builder.add(`font-family`, font) + } + + writingMode := 0 + for _, tag := range []string{ + TextAlign, TextTransform, TextWeight, TextLineStyle, WritingMode, TextDirection, + VerticalTextOrientation, CellVerticalAlign, CellHorizontalAlign, Cursor, WhiteSpace, + WordBreak, TextOverflow, Float, TableVerticalAlign} { + + if data, ok := enumProperties[tag]; ok { + if tag != VerticalTextOrientation || (writingMode != VerticalLeftToRight && writingMode != VerticalRightToLeft) { + if value, ok := enumProperty(style, tag, session, 0); ok { + cssValue := data.values[value] + if cssValue != "" { + builder.add(data.cssTag, cssValue) + } + + if tag == WritingMode { + writingMode = value + } + } + } + } + } + + for _, prop := range []struct{ tag, cssTag, off, on string }{ + {tag: Italic, cssTag: "font-style", off: "normal", on: "italic"}, + {tag: SmallCaps, cssTag: "font-variant", off: "normal", on: "small-caps"}, + } { + if flag, ok := boolProperty(style, prop.tag, session); ok { + if flag { + builder.add(prop.cssTag, prop.on) + } else { + builder.add(prop.cssTag, prop.off) + } + } + } + + if text := style.cssTextDecoration(session); text != "" { + builder.add("text-decoration", text) + } + + if css := shadowCSS(style, Shadow, session); css != "" { + builder.add("box-shadow", css) + } + + if css := shadowCSS(style, TextShadow, session); css != "" { + builder.add("text-shadow", css) + } + + if value, ok := style.properties[ColumnSeparator]; ok { + if separator, ok := value.(ColumnSeparatorProperty); ok { + if css := separator.cssValue(session); css != "" { + builder.add("column-rule", css) + } + } + } + + if avoid, ok := boolProperty(style, AvoidBreak, session); ok { + if avoid { + builder.add("break-inside", "avoid") + } else { + builder.add("break-inside", "auto") + } + } + + wrap, _ := enumProperty(style, Wrap, session, 0) + orientation, ok := getOrientation(style, session) + if ok || wrap > 0 { + cssText := enumProperties[Orientation].cssValues[orientation] + switch wrap { + case WrapOn: + cssText += " wrap" + + case WrapReverse: + cssText += " wrap-reverse" + } + builder.add(`flex-flow`, cssText) + } + + rows := (orientation == StartToEndOrientation || orientation == EndToStartOrientation) + + var hAlignTag, vAlignTag string + if rows { + hAlignTag = `justify-content` + vAlignTag = `align-items` + } else { + hAlignTag = `align-items` + vAlignTag = `justify-content` + } + + if align, ok := enumProperty(style, HorizontalAlign, session, LeftAlign); ok { + switch align { + case LeftAlign: + if (!rows && wrap == WrapReverse) || orientation == EndToStartOrientation { + builder.add(hAlignTag, `flex-end`) + } else { + builder.add(hAlignTag, `flex-start`) + } + case RightAlign: + if (!rows && wrap == WrapReverse) || orientation == EndToStartOrientation { + builder.add(hAlignTag, `flex-start`) + } else { + builder.add(hAlignTag, `flex-end`) + } + case CenterAlign: + builder.add(hAlignTag, `center`) + + case StretchAlign: + if rows { + builder.add(hAlignTag, `space-between`) + } else { + builder.add(hAlignTag, `stretch`) + } + } + } + + if align, ok := enumProperty(style, VerticalAlign, session, LeftAlign); ok { + switch align { + case TopAlign: + if (rows && wrap == WrapReverse) || orientation == BottomUpOrientation { + builder.add(vAlignTag, `flex-end`) + } else { + builder.add(vAlignTag, `flex-start`) + } + case BottomAlign: + if (rows && wrap == WrapReverse) || orientation == BottomUpOrientation { + builder.add(vAlignTag, `flex-start`) + } else { + builder.add(vAlignTag, `flex-end`) + } + case CenterAlign: + builder.add(vAlignTag, `center`) + + case StretchAlign: + if rows { + builder.add(hAlignTag, `stretch`) + } else { + builder.add(hAlignTag, `space-between`) + } + } + } + + if r, ok := rangeProperty(style, Row, session); ok { + builder.add("grid-row-start", strconv.Itoa(r.First+1)) + builder.add("grid-row-end", strconv.Itoa(r.Last+2)) + } + if r, ok := rangeProperty(style, Column, session); ok { + builder.add("grid-column-start", strconv.Itoa(r.First+1)) + builder.add("grid-column-end", strconv.Itoa(r.Last+2)) + } + if text := style.gridCellSizesCSS(CellWidth, session); text != "" { + builder.add(`grid-template-columns`, text) + } + if text := style.gridCellSizesCSS(CellHeight, session); text != "" { + builder.add(`grid-template-rows`, text) + } + + style.writeViewTransformCSS(builder, session) + + if clip := getClipShape(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) { + builder.add(`shape-outside`, clip.cssStyle(session)) + } + + if value := style.getRaw(Filter); value != nil { + if filter, ok := value.(ViewFilter); ok { + if text := filter.cssStyle(session); text != "" { + builder.add(`filter`, text) + } + } + } + /* + if len(style.transitions) > 0 { + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + for property, transition := range style.transitions { + if buffer.Len() > 0 { + buffer.WriteString(`, `) + } + buffer.WriteString(property) + transition.cssWrite(buffer, session) + } + + if buffer.Len() > 0 { + builder.add(`transition`, buffer.String()) + } + } + */ + + // TODO text-shadow +} diff --git a/viewStyleGet.go b/viewStyleGet.go new file mode 100644 index 0000000..1dbaa51 --- /dev/null +++ b/viewStyleGet.go @@ -0,0 +1,85 @@ +package rui + +import ( + "strings" +) + +func getOrientation(style Properties, session Session) (int, bool) { + if value := style.Get(Orientation); value != nil { + switch value := value.(type) { + case int: + return value, true + + case string: + text, ok := session.resolveConstants(value) + if !ok { + return 0, false + } + + text = strings.ToLower(strings.Trim(text, " \t\n\r")) + switch text { + case "vertical": + return TopDownOrientation, true + + case "horizontal": + return StartToEndOrientation, true + } + + if result, ok := enumStringToInt(text, enumProperties[Orientation].values, true); ok { + return result, true + } + } + } + + return 0, false +} + +func (style *viewStyle) Get(tag string) interface{} { + return style.get(strings.ToLower(tag)) +} + +func (style *viewStyle) get(tag string) interface{} { + 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 { + return border.Get(tag) + } + return nil + + case CellBorderLeft, CellBorderRight, CellBorderTop, CellBorderBottom, + CellBorderStyle, CellBorderLeftStyle, CellBorderRightStyle, CellBorderTopStyle, CellBorderBottomStyle, + CellBorderColor, CellBorderLeftColor, CellBorderRightColor, CellBorderTopColor, CellBorderBottomColor, + CellBorderWidth, CellBorderLeftWidth, CellBorderRightWidth, CellBorderTopWidth, CellBorderBottomWidth: + if border := getBorder(style, CellBorder); border != nil { + return border.Get(tag) + } + return nil + + case RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY, + RadiusTopRight, RadiusTopRightX, RadiusTopRightY, + RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY, + 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 { + separator := val.(ColumnSeparatorProperty) + return separator.Get(tag) + } + return nil + } + + return style.propertyList.getRaw(tag) +} diff --git a/viewStyleSet.go b/viewStyleSet.go new file mode 100644 index 0000000..5b1b737 --- /dev/null +++ b/viewStyleSet.go @@ -0,0 +1,273 @@ +package rui + +import ( + "strings" +) + +func (style *viewStyle) setRange(tag string, value interface{}) bool { + switch value := value.(type) { + case string: + if strings.Contains(value, "@") { + style.properties[tag] = value + return true + } + var r Range + if !r.setValue(value) { + invalidPropertyValue(tag, value) + return false + } + style.properties[tag] = r + + case int: + style.properties[tag] = Range{First: value, Last: value} + + case Range: + style.properties[tag] = value + + default: + notCompatibleType(tag, value) + return false + } + return true +} + +func (style *viewStyle) setBackground(value interface{}) bool { + switch value := value.(type) { + case BackgroundElement: + style.properties[Background] = []BackgroundElement{value} + return true + + case []BackgroundElement: + style.properties[Background] = value + return true + + case DataObject: + if element := createBackground(value); element != nil { + style.properties[Background] = []BackgroundElement{element} + return true + } + + case []DataObject: + for _, obj := range value { + background := []BackgroundElement{} + if element := createBackground(obj); element != nil { + background = append(background, element) + } + if len(background) > 0 { + style.properties[Background] = background + return true + } + } + + case string: + if obj := ParseDataText(value); obj != nil { + if element := createBackground(obj); element != nil { + style.properties[Background] = []BackgroundElement{element} + return true + } + } + } + + return false +} + +func (style *viewStyle) Remove(tag string) { + style.remove(strings.ToLower(tag)) +} + +func (style *viewStyle) remove(tag string) { + switch tag { + case BorderStyle, BorderColor, BorderWidth, + BorderLeft, BorderLeftStyle, BorderLeftColor, BorderLeftWidth, + BorderRight, BorderRightStyle, BorderRightColor, BorderRightWidth, + BorderTop, BorderTopStyle, BorderTopColor, BorderTopWidth, + BorderBottom, BorderBottomStyle, BorderBottomColor, BorderBottomWidth: + if border := getBorder(style, Border); border != nil { + border.delete(tag) + } + + case CellBorderStyle, CellBorderColor, CellBorderWidth, + CellBorderLeft, CellBorderLeftStyle, CellBorderLeftColor, CellBorderLeftWidth, + CellBorderRight, CellBorderRightStyle, CellBorderRightColor, CellBorderRightWidth, + CellBorderTop, CellBorderTopStyle, CellBorderTopColor, CellBorderTopWidth, + CellBorderBottom, CellBorderBottomStyle, CellBorderBottomColor, CellBorderBottomWidth: + if border := getBorder(style, CellBorder); border != nil { + border.delete(tag) + } + + case MarginTop, MarginRight, MarginBottom, MarginLeft, + "top-margin", "right-margin", "bottom-margin", "left-margin": + style.removeBoundsSide(Margin, tag) + + case PaddingTop, PaddingRight, PaddingBottom, PaddingLeft, + "top-padding", "right-padding", "bottom-padding", "left-padding": + style.removeBoundsSide(Padding, tag) + + case CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft: + style.removeBoundsSide(CellPadding, tag) + + case RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY, + RadiusTopRight, RadiusTopRightX, RadiusTopRightY, + RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY, + RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY: + style.removeRadiusElement(tag) + + case OutlineStyle, OutlineWidth, OutlineColor: + if outline := getOutline(style); outline != nil { + outline.Remove(tag) + } + + default: + style.propertyList.remove(tag) + } +} + +func (style *viewStyle) Set(tag string, value interface{}) bool { + return style.set(strings.ToLower(tag), value) +} + +func (style *viewStyle) set(tag string, value interface{}) bool { + if value == nil { + style.remove(tag) + return true + } + + switch tag { + case Shadow, TextShadow: + return style.setShadow(tag, value) + + case Background: + return style.setBackground(value) + + case Border, CellBorder: + if border := newBorderProperty(value); border != nil { + style.properties[tag] = border + return true + } + + case BorderStyle, BorderColor, BorderWidth, + BorderLeft, BorderLeftStyle, BorderLeftColor, BorderLeftWidth, + BorderRight, BorderRightStyle, BorderRightColor, BorderRightWidth, + BorderTop, BorderTopStyle, BorderTopColor, BorderTopWidth, + BorderBottom, BorderBottomStyle, BorderBottomColor, BorderBottomWidth: + + border := getBorder(style, Border) + if border == nil { + border = NewBorder(nil) + if border.Set(tag, value) { + style.properties[Border] = border + return true + } + return false + } + return border.Set(tag, value) + + case CellBorderStyle, CellBorderColor, CellBorderWidth, + CellBorderLeft, CellBorderLeftStyle, CellBorderLeftColor, CellBorderLeftWidth, + CellBorderRight, CellBorderRightStyle, CellBorderRightColor, CellBorderRightWidth, + CellBorderTop, CellBorderTopStyle, CellBorderTopColor, CellBorderTopWidth, + CellBorderBottom, CellBorderBottomStyle, CellBorderBottomColor, CellBorderBottomWidth: + + border := getBorder(style, CellBorder) + if border == nil { + border = NewBorder(nil) + if border.Set(tag, value) { + style.properties[CellBorder] = border + return true + } + return false + } + return border.Set(tag, value) + + case Radius: + return style.setRadius(value) + + case RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY, + RadiusTopRight, RadiusTopRightX, RadiusTopRightY, + RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY, + RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY: + return style.setRadiusElement(tag, value) + + case Margin, Padding, CellPadding: + return style.setBounds(tag, value) + + case MarginTop, MarginRight, MarginBottom, MarginLeft, + "top-margin", "right-margin", "bottom-margin", "left-margin": + return style.setBoundsSide(Margin, tag, value) + + case PaddingTop, PaddingRight, PaddingBottom, PaddingLeft, + "top-padding", "right-padding", "bottom-padding", "left-padding": + return style.setBoundsSide(Padding, tag, value) + + case CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft: + return style.setBoundsSide(CellPadding, tag, value) + + case Outline: + return style.setOutline(value) + + case OutlineStyle, OutlineWidth, OutlineColor: + if outline := getOutline(style); outline != nil { + return outline.Set(tag, value) + } + style.properties[Outline] = NewOutlineProperty(Params{tag: value}) + return true + + case Orientation: + if text, ok := value.(string); ok { + switch strings.ToLower(text) { + case "vertical": + style.properties[Orientation] = TopDownOrientation + return true + + case "horizontal": + style.properties[Orientation] = StartToEndOrientation + return true + } + } + + case TextWeight: + if n, ok := value.(int); ok && n >= 100 && n%100 == 0 { + n /= 100 + if n > 0 && n <= 9 { + style.properties[TextWeight] = StartToEndOrientation + return true + } + } + + case Row, Column: + return style.setRange(tag, value) + + case CellWidth, CellHeight: + return style.setGridCellSize(tag, value) + + case ColumnSeparator: + if separator := newColumnSeparatorProperty(value); separator != nil { + style.properties[ColumnSeparator] = separator + return true + } + return false + + case ColumnSeparatorStyle, ColumnSeparatorWidth, ColumnSeparatorColor: + var separator ColumnSeparatorProperty = nil + if val, ok := style.properties[ColumnSeparator]; ok { + separator = val.(ColumnSeparatorProperty) + } + if separator == nil { + separator = newColumnSeparatorProperty(nil) + } + + if separator.Set(tag, value) { + style.properties[ColumnSeparator] = separator + return true + } + return false + + case Clip, ShapeOutside: + return style.setClipShape(tag, value) + + case Filter: + return style.setFilter(value) + } + + return style.propertyList.set(tag, value) +} diff --git a/viewStyle_test.go b/viewStyle_test.go new file mode 100644 index 0000000..d117585 --- /dev/null +++ b/viewStyle_test.go @@ -0,0 +1,131 @@ +package rui + +/* +import ( + "strings" + "testing" +) + +func TestViewStyleCreate(t *testing.T) { + + app := new(application) + app.init("") + session := newSession(app, 1, "", false, false) + + var style viewStyle + style.init() + + data := []struct{ property, value string }{ + {Width, "100%"}, + {Height, "400px"}, + {Margin, "4px"}, + {Margin + "-bottom", "auto"}, + {Padding, "1em"}, + {Font, "Arial"}, + {BackgroundColor, "#FF008000"}, + {TextColor, "#FF000000"}, + {TextSize, "1.25em"}, + {TextWeight, "bold"}, + {TextAlign, "center"}, + {TextTransform, "uppercase"}, + {TextIndent, "0.25em"}, + {LetterSpacing, "1.5em"}, + {WordSpacing, "8px"}, + {LineHeight, "2em"}, + {Italic, "on"}, + {TextDecoration, "strikethrough | overline | underline"}, + {SmallCaps, "on"}, + } + + for _, prop := range data { + style.Set(prop.property, prop.value) + } + + style.AddShadow(NewViewShadow(SizeUnit{Auto, 0}, SizeUnit{Auto, 0}, Px(4), Px(6), 0xFF808080)) + + expected := `width: 100%; height: 400px; font-size: 1.25rem; text-indent: 0.25rem; letter-spacing: 1.5rem; word-spacing: 8px; ` + + `line-height: 2rem; padding: 1rem; margin-left: 4px; margin-top: 4px; margin-right: 4px; box-shadow: 0 0 4px 6px rgb(128,128,128); ` + + `background-color: rgb(0,128,0); color: rgb(0,0,0); font-family: Arial; font-weight: bold; font-style: italic; font-variant: small-caps; ` + + `text-align: center; text-decoration: line-through overline underline; text-transform: uppercase;` + + buffer := strings.Builder{} + style.cssViewStyle(&buffer, session) + if text := strings.Trim(buffer.String(), " "); text != expected { + t.Error("\nresult : " + text + "\nexpected: " + expected) + } + + w := newCompactDataWriter() + w.StartObject("_") + style.writeStyle(w) + w.FinishObject() + expected2 := `_{width=100%,height=400px,margin="4px,4px,auto,4px",padding=1em,background-color=#FF008000,shadow=_{color=#FF808080,blur=4px,spread-radius=6px},font=Arial,text-color=#FF000000,text-size=1.25em,text-weight=bold,italic=on,small-caps=on,text-decoration=strikethrough|overline|underline,text-align=center,text-indent=0.25em,letter-spacing=1.5em,word-spacing=8px,line-height=2em,text-transform=uppercase}` + + if text := w.String(); text != expected2 { + t.Error("\n result: " + text + "\nexpected: " + expected2) + } + + var style1 viewStyle + style1.init() + if obj, err := ParseDataText(expected2); err == nil { + style1.parseStyle(obj, new(sessionData)) + buffer.Reset() + style.cssStyle(&buffer) + if text := buffer.String(); text != expected { + t.Error("\n result: " + text + "\nexpected: " + expected) + } + } else { + t.Error(err) + } + + var style2 viewStyle + style2.init() + + style2.textWeight = 4 + style2.textAlign = RightAlign + style2.textTransform = LowerCaseTextTransform + style2.textDecoration = NoneDecoration + style2.italic = Off + style2.smallCaps = Off + + expected = `font-weight: normal; font-style: normal; font-variant: normal; text-align: right; text-decoration: none; text-transform: lowercase; ` + buffer.Reset() + style2.cssStyle(&buffer) + if text := buffer.String(); text != expected { + t.Error("\n result: " + text + "\nexpected: " + expected) + } + + w.Reset() + w.StartObject("_") + style2.writeStyle(w) + w.FinishObject() + expected = `_{text-weight=normal,italic=off,small-caps=off,text-decoration=none,text-align=right,text-transform=lowercase}` + + if text := w.String(); text != expected { + t.Error("\n result: " + text + "\nexpected: " + expected) + } + + style2.textWeight = 5 + style2.textAlign = JustifyTextAlign + style2.textTransform = CapitalizeTextTransform + style2.textDecoration = Inherit + style2.italic = Inherit + style2.smallCaps = Inherit + + expected = `font-weight: 500; text-align: justify; text-transform: capitalize; ` + buffer.Reset() + style2.cssStyle(&buffer) + if text := buffer.String(); text != expected { + t.Error("\n result: " + text + "\nexpected: " + expected) + } + + w.Reset() + w.StartObject("_") + style2.writeStyle(w) + w.FinishObject() + expected = `_{text-weight=5,text-align=justify,text-transform=capitalize}` + + if text := w.String(); text != expected { + t.Error("\n result: " + text + "\nexpected: " + expected) + } +} +*/ diff --git a/viewTransform.go b/viewTransform.go new file mode 100644 index 0000000..198bb58 --- /dev/null +++ b/viewTransform.go @@ -0,0 +1,299 @@ +package rui + +import ( + "strconv" +) + +const ( + // Perspective is the name of the SizeUnit property that determines the 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). + Perspective = "perspective" + // PerspectiveOriginX is the name of the SizeUnit property that determines the 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%. + PerspectiveOriginX = "perspective-origin-x" + // PerspectiveOriginY is the name of the SizeUnit property that determines the 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%. + PerspectiveOriginY = "perspective-origin-y" + // BackfaceVisible is the name of the 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. + BackfaceVisible = "backface-visibility" + // OriginX is the name of the SizeUnit property that determines the x-coordinate of the point around which + // a view transformation is applied. + // The default value is 50%. + OriginX = "origin-x" + // OriginY is the name of the SizeUnit property that determines the y-coordinate of the point around which + // a view transformation is applied. + // The default value is 50%. + OriginY = "origin-y" + // OriginZ is the name of the SizeUnit property that determines the z-coordinate of the point around which + // a view transformation is applied. + // The default value is 50%. + OriginZ = "origin-z" + // TranslateX is the name of the SizeUnit property that specify the x-axis translation value + // of a 2D/3D translation + TranslateX = "translate-x" + // TranslateY is the name of the SizeUnit property that specify the y-axis translation value + // of a 2D/3D translation + TranslateY = "translate-y" + // TranslateZ is the name of the SizeUnit property that specify the z-axis translation value + // of a 3D translation + TranslateZ = "translate-z" + // ScaleX is the name of the float property that specify the x-axis scaling value of a 2D/3D scale + // The default value is 1. + ScaleX = "scale-x" + // ScaleY is the name of the float property that specify the y-axis scaling value of a 2D/3D scale + // The default value is 1. + ScaleY = "scale-y" + // ScaleZ is the name of the float property that specify the z-axis scaling value of a 3D scale + // The default value is 1. + ScaleZ = "scale-z" + // Rotate is the name of the AngleUnit property that determines the angle of the view rotation. + // A positive angle denotes a clockwise rotation, a negative angle a counter-clockwise one. + Rotate = "rotate" + // RotateX is the name of the float property that determines the x-coordinate of the vector denoting + // the axis of rotation which could between 0 and 1. + RotateX = "rotate-x" + // RotateY is the name of the float property that determines the y-coordinate of the vector denoting + // the axis of rotation which could between 0 and 1. + RotateY = "rotate-y" + // RotateZ is the name of the float property that determines the z-coordinate of the vector denoting + // the axis of rotation which could between 0 and 1. + RotateZ = "rotate-z" + // SkewX is the name of the AngleUnit property that representing the angle to use to distort + // the element along the abscissa. The default value is 0. + SkewX = "skew-x" + // SkewY is the name of the AngleUnit property that representing the angle to use to distort + // the element along the ordinate. The default value is 0. + SkewY = "skew-y" +) + +func getTransform3D(style Properties, session Session) bool { + perspective, ok := sizeProperty(style, Perspective, session) + return ok && perspective.Type != Auto && perspective.Value != 0 +} + +func getPerspectiveOrigin(style Properties, session Session) (SizeUnit, SizeUnit) { + x, _ := sizeProperty(style, PerspectiveOriginX, session) + y, _ := sizeProperty(style, PerspectiveOriginY, session) + return x, y +} + +func getOrigin(style Properties, session Session) (SizeUnit, SizeUnit, SizeUnit) { + x, _ := sizeProperty(style, OriginX, session) + y, _ := sizeProperty(style, OriginY, session) + z, _ := sizeProperty(style, OriginZ, session) + return x, y, z +} + +func getSkew(style Properties, session Session) (AngleUnit, AngleUnit) { + skewX, _ := angleProperty(style, SkewX, session) + skewY, _ := angleProperty(style, SkewY, session) + return skewX, skewY +} + +func getTranslate(style Properties, session Session) (SizeUnit, SizeUnit, SizeUnit) { + x, _ := sizeProperty(style, TranslateX, session) + y, _ := sizeProperty(style, TranslateY, session) + z, _ := sizeProperty(style, TranslateZ, session) + return x, y, z +} + +func getScale(style Properties, session Session) (float64, float64, float64) { + scaleX, _ := floatProperty(style, ScaleX, session, 1) + scaleY, _ := floatProperty(style, ScaleY, session, 1) + scaleZ, _ := floatProperty(style, ScaleZ, session, 1) + return scaleX, scaleY, scaleZ +} + +func getRotate(style Properties, session Session) (float64, float64, float64, AngleUnit) { + rotateX, _ := floatProperty(style, RotateX, session, 1) + rotateY, _ := floatProperty(style, RotateY, session, 1) + rotateZ, _ := floatProperty(style, RotateZ, session, 1) + angle, _ := angleProperty(style, Rotate, session) + return rotateX, rotateY, rotateZ, angle +} + +func (style *viewStyle) transform(session Session) string { + + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) + + skewX, skewY := getSkew(style, session) + if skewX.Value != 0 || skewY.Value != 0 { + buffer.WriteString(`skew(`) + buffer.WriteString(skewX.cssString()) + buffer.WriteRune(',') + buffer.WriteString(skewY.cssString()) + buffer.WriteRune(')') + } + + x, y, z := getTranslate(style, session) + scaleX, scaleY, scaleZ := getScale(style, session) + if getTransform3D(style, session) { + if (x.Type != Auto && x.Value != 0) || (y.Type != Auto && y.Value != 0) || (z.Type != Auto && z.Value != 0) { + if buffer.Len() > 0 { + buffer.WriteRune(' ') + } + buffer.WriteString(`translate3d(`) + buffer.WriteString(x.cssString("0")) + buffer.WriteRune(',') + buffer.WriteString(y.cssString("0")) + buffer.WriteRune(',') + buffer.WriteString(z.cssString("0")) + buffer.WriteRune(')') + } + + if scaleX != 1 || scaleY != 1 || scaleZ != 1 { + if buffer.Len() > 0 { + buffer.WriteRune(' ') + } + buffer.WriteString(`scale3d(`) + buffer.WriteString(strconv.FormatFloat(scaleX, 'g', -1, 64)) + buffer.WriteRune(',') + buffer.WriteString(strconv.FormatFloat(scaleY, 'g', -1, 64)) + buffer.WriteRune(',') + buffer.WriteString(strconv.FormatFloat(scaleZ, 'g', -1, 64)) + buffer.WriteRune(')') + } + + rotateX, rotateY, rotateZ, angle := getRotate(style, session) + if angle.Value != 0 && (rotateX != 0 || rotateY != 0 || rotateZ != 0) { + if buffer.Len() > 0 { + buffer.WriteRune(' ') + } + buffer.WriteString(`rotate3d(`) + buffer.WriteString(strconv.FormatFloat(rotateX, 'g', -1, 64)) + buffer.WriteRune(',') + buffer.WriteString(strconv.FormatFloat(rotateY, 'g', -1, 64)) + buffer.WriteRune(',') + buffer.WriteString(strconv.FormatFloat(rotateZ, 'g', -1, 64)) + buffer.WriteRune(',') + buffer.WriteString(angle.cssString()) + buffer.WriteRune(')') + } + } else { + if (x.Type != Auto && x.Value != 0) || (y.Type != Auto && y.Value != 0) { + if buffer.Len() > 0 { + buffer.WriteRune(' ') + } + buffer.WriteString(`translate(`) + buffer.WriteString(x.cssString("0")) + buffer.WriteRune(',') + buffer.WriteString(y.cssString("0")) + buffer.WriteRune(')') + } + + if scaleX != 1 || scaleY != 1 { + if buffer.Len() > 0 { + buffer.WriteRune(' ') + } + buffer.WriteString(`scale(`) + buffer.WriteString(strconv.FormatFloat(scaleX, 'g', -1, 64)) + buffer.WriteRune(',') + buffer.WriteString(strconv.FormatFloat(scaleY, 'g', -1, 64)) + buffer.WriteRune(')') + } + + angle, _ := angleProperty(style, Rotate, session) + if angle.Value != 0 { + if buffer.Len() > 0 { + buffer.WriteRune(' ') + } + buffer.WriteString(`rotate(`) + buffer.WriteString(angle.cssString()) + buffer.WriteRune(')') + } + } + + return buffer.String() +} + +func (style *viewStyle) writeViewTransformCSS(builder cssBuilder, session Session) { + if getTransform3D(style, session) { + if perspective, ok := sizeProperty(style, Perspective, session); ok && perspective.Type != Auto && perspective.Value != 0 { + builder.add(`perspective`, perspective.cssString("0")) + } + + x, y := getPerspectiveOrigin(style, session) + if x.Type != Auto || y.Type != Auto { + builder.addValues(`perspective-origin`, ` `, x.cssString("50%"), y.cssString("50%")) + } + + if backfaceVisible, ok := boolProperty(style, BackfaceVisible, session); ok { + if backfaceVisible { + builder.add(`backface-visibility`, `visible`) + } else { + builder.add(`backface-visibility`, `hidden`) + } + } + + x, y, z := getOrigin(style, session) + if x.Type != Auto || y.Type != Auto || z.Type != Auto { + builder.addValues(`transform-origin`, ` `, x.cssString("50%"), y.cssString("50%"), z.cssString("0")) + } + } else { + x, y, _ := getOrigin(style, session) + if x.Type != Auto || y.Type != Auto { + builder.addValues(`transform-origin`, ` `, x.cssString("50%"), y.cssString("50%")) + } + } + + builder.add(`transform`, style.transform(session)) +} + +func (view *viewData) updateTransformProperty(tag string) bool { + htmlID := view.htmlID() + session := view.session + + switch tag { + case Perspective: + updateCSSStyle(htmlID, session) + + case PerspectiveOriginX, PerspectiveOriginY: + if getTransform3D(view, session) { + x, y := GetPerspectiveOrigin(view, "") + value := "" + if x.Type != Auto || y.Type != Auto { + value = x.cssString("50%") + " " + y.cssString("50%") + } + updateCSSProperty(htmlID, "perspective-origin", value, session) + } + + case BackfaceVisible: + if getTransform3D(view, session) { + if GetBackfaceVisible(view, "") { + updateCSSProperty(htmlID, BackfaceVisible, "visible", session) + } else { + updateCSSProperty(htmlID, BackfaceVisible, "hidden", session) + } + } + + case OriginX, OriginY, OriginZ: + x, y, z := getOrigin(view, session) + value := "" + if getTransform3D(view, session) { + if x.Type != Auto || y.Type != Auto || z.Type != Auto { + value = x.cssString("50%") + " " + y.cssString("50%") + " " + z.cssString("50%") + } + } else { + if x.Type != Auto || y.Type != Auto { + value = x.cssString("50%") + " " + y.cssString("50%") + } + } + updateCSSProperty(htmlID, "transform-origin", value, session) + + case SkewX, SkewY, TranslateX, TranslateY, TranslateZ, ScaleX, ScaleY, ScaleZ, Rotate, RotateX, RotateY, RotateZ: + updateCSSProperty(htmlID, "transform", view.transform(session), session) + + default: + return false + } + + return true +} diff --git a/viewUtils.go b/viewUtils.go new file mode 100644 index 0000000..e1ca150 --- /dev/null +++ b/viewUtils.go @@ -0,0 +1,1003 @@ +package rui + +// Get returns a value of the property with name "tag" of the "rootView" subview with "viewID" id value. +// The type of return value depends on the property. +// If the subview don't exists or the property is not set then nil is returned. +func Get(rootView View, viewID, tag string) interface{} { + if view := ViewByID(rootView, viewID); view != nil { + return view.Get(tag) + } + return nil +} + +// Set sets the property with name "tag" of the "rootView" subview with "viewID" id by value. Result: +// true - success, +// false - error (incompatible type or invalid format of a string value, see AppLog). +func Set(rootView View, viewID, tag string, value interface{}) bool { + if view := ViewByID(rootView, viewID); view != nil { + return view.Set(tag, value) + } + return false +} + +// SetParams sets properties with name "tag" of the "rootView" subview. Result: +// true - all properties were set successful, +// false - error (incompatible type or invalid format of a string value, see AppLog). +func SetParams(rootView View, viewID string, params Params) bool { + view := ViewByID(rootView, viewID) + if view == nil { + return false + } + + result := true + for tag, value := range params { + result = view.Set(tag, value) && result + } + return result +} + +// GetSemantics returns the subview semantics. Valid semantics values are +// DefaultSemantics (0), ArticleSemantics (1), SectionSemantics (2), AsideSemantics (3), +// HeaderSemantics (4), MainSemantics (5), FooterSemantics (6), NavigationSemantics (7), +// FigureSemantics (8), FigureCaptionSemantics (9), ButtonSemantics (10), ParagraphSemantics (11), +// H1Semantics (12) - H6Semantics (17), BlockquoteSemantics (18), and CodeSemantics (19). +// If the second argument (subviewID) is "" then a semantics of the first argument (view) is returned +func GetSemantics(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if semantics, ok := enumStyledProperty(view, Semantics, DefaultSemantics); ok { + return semantics + } + } + + return DefaultSemantics +} + +// GetOpacity returns the subview opacity. +// If the second argument (subviewID) is "" then an opacity of the first argument (view) is returned +func GetOpacity(view View, subviewID string) float64 { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if style, ok := floatStyledProperty(view, Opacity, 1); ok { + return style + } + } + return 1 +} + +// GetStyle returns the subview style id. +// If the second argument (subviewID) is "" then a style of the first argument (view) is returned +func GetStyle(view View, subviewID string) string { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if style, ok := stringProperty(view, Style, view.Session()); ok { + return style + } + } + return "" +} + +// GetDisabledStyle returns the disabled subview style id. +// If the second argument (subviewID) is "" then a style of the first argument (view) is returned +func GetDisabledStyle(view View, subviewID string) string { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if style, ok := stringProperty(view, StyleDisabled, view.Session()); ok { + return style + } + } + return "" +} + +// GetVisibility returns the subview visibility. One of the following values is returned: +// Visible (0), Invisible (1), or Gone (2) +// If the second argument (subviewID) is "" then a visibility of the first argument (view) is returned +func GetVisibility(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return Visible + } + result, _ := enumStyledProperty(view, Visibility, Visible) + return result +} + +// GetZIndex returns the subview z-order. +// If the second argument (subviewID) is "" then a z-order of the first argument (view) is returned +func GetZIndex(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return 0 + } + result, _ := intStyledProperty(view, Visibility, 0) + return result +} + +// GetWidth returns the subview width. +// If the second argument (subviewID) is "" then a width of the first argument (view) is returned +func GetWidth(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return AutoSize() + } + result, _ := sizeStyledProperty(view, Width) + return result +} + +// GetHeight returns the subview height. +// If the second argument (subviewID) is "" then a height of the first argument (view) is returned +func GetHeight(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return AutoSize() + } + result, _ := sizeStyledProperty(view, Height) + return result +} + +// GetMinWidth returns a minimal subview width. +// If the second argument (subviewID) is "" then a minimal width of the first argument (view) is returned +func GetMinWidth(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return AutoSize() + } + result, _ := sizeStyledProperty(view, MinWidth) + return result +} + +// GetMinHeight returns a minimal subview height. +// If the second argument (subviewID) is "" then a minimal height of the first argument (view) is returned +func GetMinHeight(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return AutoSize() + } + result, _ := sizeStyledProperty(view, MinHeight) + return result +} + +// GetMaxWidth returns a maximal subview width. +// If the second argument (subviewID) is "" then a maximal width of the first argument (view) is returned +func GetMaxWidth(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return AutoSize() + } + result, _ := sizeStyledProperty(view, MaxWidth) + return result +} + +// GetMaxHeight returns a maximal subview height. +// If the second argument (subviewID) is "" then a maximal height of the first argument (view) is returned +func GetMaxHeight(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return AutoSize() + } + result, _ := sizeStyledProperty(view, MaxHeight) + return result +} + +// GetLeft returns a left position of the subview in an AbsoluteLayout container. +// If a parent view is not an AbsoluteLayout container then this value is ignored. +// If the second argument (subviewID) is "" then a left position of the first argument (view) is returned +func GetLeft(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return AutoSize() + } + result, _ := sizeStyledProperty(view, Left) + return result +} + +// GetRight returns a right position of the subview in an AbsoluteLayout container. +// If a parent view is not an AbsoluteLayout container then this value is ignored. +// If the second argument (subviewID) is "" then a right position of the first argument (view) is returned +func GetRight(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return AutoSize() + } + result, _ := sizeStyledProperty(view, Right) + return result +} + +// GetTop returns a top position of the subview in an AbsoluteLayout container. +// If a parent view is not an AbsoluteLayout container then this value is ignored. +// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned +func GetTop(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return AutoSize() + } + result, _ := sizeStyledProperty(view, Top) + return result +} + +// GetBottom returns a top position of the subview in an AbsoluteLayout container. +// If a parent view is not an AbsoluteLayout container then this value is ignored. +// If the second argument (subviewID) is "" then a bottom position of the first argument (view) is returned +func GetBottom(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return AutoSize() + } + result, _ := sizeStyledProperty(view, Bottom) + return result +} + +// Margin returns the subview margin. +// If the second argument (subviewID) is "" then a margin of the first argument (view) is returned +func GetMargin(view View, subviewID string) Bounds { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + var bounds Bounds + if view != nil { + bounds.setFromProperties(Margin, MarginTop, MarginRight, MarginBottom, MarginLeft, view, view.Session()) + } + return bounds +} + +// GetPadding returns the subview padding. +// If the second argument (subviewID) is "" then a padding of the first argument (view) is returned +func GetPadding(view View, subviewID string) Bounds { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + var bounds Bounds + if view != nil { + bounds.setFromProperties(Padding, PaddingTop, PaddingRight, PaddingBottom, PaddingLeft, view, view.Session()) + } + return bounds +} + +// GetBorder returns ViewBorders of the subview. +// If the second argument (subviewID) is "" then a ViewBorders of the first argument (view) is returned. +func GetBorder(view View, subviewID string) ViewBorders { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if border := getBorder(view, Border); border != nil { + return border.ViewBorders(view.Session()) + } + } + return ViewBorders{} +} + +// Radius returns the BoxRadius structure of the subview. +// If the second argument (subviewID) is "" then a BoxRadius of the first argument (view) is returned. +func GetRadius(view View, subviewID string) BoxRadius { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return BoxRadius{} + } + return getRadius(view, view.Session()) +} + +// GetOutline returns ViewOutline of the subview. +// If the second argument (subviewID) is "" then a ViewOutline of the first argument (view) is returned. +func GetOutline(view View, subviewID string) ViewOutline { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if outline := getOutline(view); outline != nil { + return outline.ViewOutline(view.Session()) + } + } + return ViewOutline{Style: NoneLine, Width: AutoSize(), Color: 0} +} + +// GetViewShadows returns shadows of the subview. +// If the second argument (subviewID) is "" then shadows of the first argument (view) is returned. +func GetViewShadows(view View, subviewID string) []ViewShadow { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return []ViewShadow{} + } + return getShadows(view, Shadow) +} + +// GetTextShadows returns text shadows of the subview. +// If the second argument (subviewID) is "" then shadows of the first argument (view) is returned. +func GetTextShadows(view View, subviewID string) []ViewShadow { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return []ViewShadow{} + } + return getShadows(view, TextShadow) +} + +// GetBackgroundColor returns a background color of the subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetBackgroundColor(view View, subviewID string) Color { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return 0 + } + color, _ := colorStyledProperty(view, BackgroundColor) + return color +} + +// GetFontName returns the subview font. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetFontName(view View, subviewID string) string { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if font, ok := stringProperty(view, FontName, view.Session()); ok { + return font + } + if font, ok := valueFromStyle(view, FontName); ok { + return font + } + if parent := view.Parent(); parent != nil { + return GetFontName(parent, "") + } + } + return "" +} + +// GetTextColor returns a text color of the subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTextColor(view View, subviewID string) Color { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if color, ok := colorStyledProperty(view, TextColor); ok { + return color + } + if parent := view.Parent(); parent != nil { + return GetTextColor(parent, "") + } + } + return 0 +} + +// GetTextSize returns a text size of the subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTextSize(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := sizeStyledProperty(view, TextSize); ok { + return result + } + if parent := view.Parent(); parent != nil { + return GetTextSize(parent, "") + } + } + return AutoSize() +} + +// GetTextWeight returns a text weight of the subview. Returns one of next values: +// 1, 2, 3, 4 (normal text), 5, 6, 7 (bold text), 8 and 9 +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTextWeight(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if weight, ok := enumStyledProperty(view, TextWeight, NormalFont); ok { + return weight + } + if parent := view.Parent(); parent != nil { + return GetTextWeight(parent, "") + } + } + return NormalFont +} + +// GetTextAlign returns a text align of the subview. Returns one of next values: +// LeftAlign = 0, RightAlign = 1, CenterAlign = 2, JustifyAlign = 3 +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTextAlign(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := enumStyledProperty(view, TextAlign, LeftAlign); ok { + return result + } + if parent := view.Parent(); parent != nil { + return GetTextAlign(parent, "") + } + } + return LeftAlign +} + +// GetTextIndent returns a text indent of the subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTextIndent(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := sizeStyledProperty(view, TextIndent); ok { + return result + } + if parent := view.Parent(); parent != nil { + return GetTextIndent(parent, "") + } + } + return AutoSize() +} + +// GetLetterSpacing returns a letter spacing of the subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetLetterSpacing(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := sizeStyledProperty(view, LetterSpacing); ok { + return result + } + if parent := view.Parent(); parent != nil { + return GetLetterSpacing(parent, "") + } + } + return AutoSize() +} + +// GetWordSpacing returns a word spacing of the subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetWordSpacing(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := sizeStyledProperty(view, WordSpacing); ok { + return result + } + if parent := view.Parent(); parent != nil { + return GetWordSpacing(parent, "") + } + } + return AutoSize() +} + +// GetLineHeight returns a height of a text line of the subview. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetLineHeight(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := sizeStyledProperty(view, LineHeight); ok { + return result + } + if parent := view.Parent(); parent != nil { + return GetLineHeight(parent, "") + } + } + return AutoSize() +} + +// IsItalic returns "true" if a text font of the subview is displayed in italics, "false" otherwise. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func IsItalic(view View, subviewID string) bool { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := boolStyledProperty(view, Italic); ok { + return result + } + if parent := view.Parent(); parent != nil { + return IsItalic(parent, "") + } + } + return false +} + +// IsSmallCaps returns "true" if a text font of the subview is displayed in small caps, "false" otherwise. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func IsSmallCaps(view View, subviewID string) bool { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := boolStyledProperty(view, SmallCaps); ok { + return result + } + if parent := view.Parent(); parent != nil { + return IsSmallCaps(parent, "") + } + } + return false +} + +// IsStrikethrough returns "true" if a text font of the subview is displayed strikethrough, "false" otherwise. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func IsStrikethrough(view View, subviewID string) bool { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := boolStyledProperty(view, Strikethrough); ok { + return result + } + if parent := view.Parent(); parent != nil { + return IsStrikethrough(parent, "") + } + } + return false +} + +// IsOverline returns "true" if a text font of the subview is displayed overlined, "false" otherwise. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func IsOverline(view View, subviewID string) bool { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := boolStyledProperty(view, Overline); ok { + return result + } + if parent := view.Parent(); parent != nil { + return IsOverline(parent, "") + } + } + return false +} + +// IsUnderline returns "true" if a text font of the subview is displayed underlined, "false" otherwise. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func IsUnderline(view View, subviewID string) bool { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := boolStyledProperty(view, Underline); ok { + return result + } + if parent := view.Parent(); parent != nil { + return IsUnderline(parent, "") + } + } + return false +} + +// GetTextLineThickness returns the stroke thickness of the decoration line that +// is used on text in an element, such as a line-through, underline, or overline. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTextLineThickness(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := sizeStyledProperty(view, TextLineThickness); ok { + return result + } + if parent := view.Parent(); parent != nil { + return GetTextLineThickness(parent, "") + } + } + return AutoSize() +} + +// GetTextLineStyle returns the stroke style of the decoration line that +// is used on text in an element, such as a line-through, underline, or overline. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTextLineStyle(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := enumStyledProperty(view, TextLineStyle, SolidLine); ok { + return result + } + if parent := view.Parent(); parent != nil { + return GetTextLineStyle(parent, "") + } + } + return SolidLine +} + +// GetTextLineColor returns the stroke color of the decoration line that +// is used on text in an element, such as a line-through, underline, or overline. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTextLineColor(view View, subviewID string) Color { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if color, ok := colorStyledProperty(view, TextLineColor); ok { + return color + } + if parent := view.Parent(); parent != nil { + return GetTextLineColor(parent, "") + } + } + return 0 +} + +// GetTextTransform returns a text transform of the subview. Return one of next values: +// NoneTextTransform (0), CapitalizeTextTransform (1), LowerCaseTextTransform (2) or UpperCaseTextTransform (3) +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTextTransform(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := enumStyledProperty(view, TextTransform, NoneTextTransform); ok { + return result + } + if parent := view.Parent(); parent != nil { + return GetTextTransform(parent, "") + } + } + return NoneTextTransform +} + +// GetWritingMode returns whether lines of text are laid out horizontally or vertically, as well as +// the direction in which blocks progress. Valid values are HorizontalTopToBottom (0), +// HorizontalBottomToTop (1), VerticalRightToLeft (2) and VerticalLeftToRight (3) +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetWritingMode(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := enumStyledProperty(view, WritingMode, HorizontalTopToBottom); ok { + return result + } + if parent := view.Parent(); parent != nil { + return GetWritingMode(parent, "") + } + } + return HorizontalTopToBottom +} + +// GetTextDirection - returns a direction of text, table columns, and horizontal overflow. +// Valid values are Inherit (0), LeftToRightDirection (1), and RightToLeftDirection (2). +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTextDirection(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := enumStyledProperty(view, TextDirection, SystemTextDirection); ok { + return result + } + if parent := view.Parent(); parent != nil { + return GetTextDirection(parent, "") + } + } + return SystemTextDirection + // TODO return system text direction +} + +// GetVerticalTextOrientation returns a orientation of the text characters in a line. It only affects text +// in vertical mode (when "writing-mode" is "vertical-right-to-left" or "vertical-left-to-right"). +// Valid values are MixedTextOrientation (0), UprightTextOrientation (1), and SidewaysTextOrientation (2). +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetVerticalTextOrientation(view View, subviewID string) int { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := enumStyledProperty(view, VerticalTextOrientation, MixedTextOrientation); ok { + return result + } + if parent := view.Parent(); parent != nil { + return GetVerticalTextOrientation(parent, "") + } + } + return MixedTextOrientation +} + +// GetRow returns the range of row numbers of a GridLayout in which the subview is placed. +// If the second argument (subviewID) is "" then a values from the first argument (view) is returned. +func GetRow(view View, subviewID string) Range { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + session := view.Session() + if result, ok := rangeProperty(view, Row, session); ok { + return result + } + if value, ok := valueFromStyle(view, Row); ok { + if result, ok := valueToRange(value, session); ok { + return result + } + } + } + return Range{} +} + +// GetColumn returns the range of column numbers of a GridLayout in which the subview is placed. +// If the second argument (subviewID) is "" then a values from the first argument (view) is returned. +func GetColumn(view View, subviewID string) Range { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + session := view.Session() + if result, ok := rangeProperty(view, Column, session); ok { + return result + } + if value, ok := valueFromStyle(view, Column); ok { + if result, ok := valueToRange(value, session); ok { + return result + } + } + } + return Range{} +} + +// 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 "" then a value from the first argument (view) is returned. +func GetPerspective(view View, subviewID string) SizeUnit { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return AutoSize() + } + result, _ := sizeStyledProperty(view, Perspective) + return result +} + +// 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 "" then a value from the first argument (view) is returned. +func GetPerspectiveOrigin(view View, subviewID string) (SizeUnit, SizeUnit) { + if subviewID != "" { + view = ViewByID(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 "" then a value from the first argument (view) is returned. +func GetBackfaceVisible(view View, subviewID string) bool { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := boolStyledProperty(view, BackfaceVisible); ok { + return result + } + } + return true +} + +// GetOrigin 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 "" then a value from the first argument (view) is returned. +func GetOrigin(view View, subviewID string) (SizeUnit, SizeUnit, SizeUnit) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return AutoSize(), AutoSize(), AutoSize() + } + return getOrigin(view, view.Session()) +} + +// GetTranslate returns a x-, y-, and z-axis translation value of a 2D/3D translation +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func GetTranslate(view View, subviewID string) (SizeUnit, SizeUnit, SizeUnit) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return AutoSize(), AutoSize(), AutoSize() + } + return getTranslate(view, view.Session()) +} + +// 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 "" then a value from the first argument (view) is returned. +func GetSkew(view View, subviewID string) (AngleUnit, AngleUnit) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return AngleUnit{Value: 0, Type: Radian}, AngleUnit{Value: 0, Type: Radian} + } + return getSkew(view, view.Session()) +} + +// 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 "" then a value from the first argument (view) is returned. +func GetScale(view View, subviewID string) (float64, float64, float64) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return 1, 1, 1 + } + return getScale(view, view.Session()) +} + +// 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 "" then a value from the first argument (view) is returned. +func GetRotate(view View, subviewID string) (float64, float64, float64, AngleUnit) { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return 0, 0, 0, AngleUnit{Value: 0, Type: Radian} + } + return getRotate(view, view.Session()) +} + +// GetAvoidBreak returns "true" if avoids any break from being inserted within the principal box, +// and "false" if allows, but does not force, any break to be inserted within the principal box. +// If the second argument (subviewID) is "" then a top position of the first argument (view) is returned +func GetAvoidBreak(view View, subviewID string) bool { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view == nil { + return false + } + result, _ := boolStyledProperty(view, AvoidBreak) + return result +} + +func GetNotTranslate(view View, subviewID string) bool { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + if view != nil { + if result, ok := boolStyledProperty(view, NotTranslate); ok { + return result + } + if parent := view.Parent(); parent != nil { + return GetNotTranslate(parent, "") + } + } + return false +} + +func valueFromStyle(view View, tag string) (string, bool) { + session := view.Session() + getValue := func(styleTag string) (string, bool) { + if style, ok := stringProperty(view, styleTag, session); ok { + if style, ok := session.resolveConstants(style); ok { + return session.styleProperty(style, tag) + } + } + return "", false + } + + if IsDisabled(view) { + if value, ok := getValue(StyleDisabled); ok { + return value, true + } + } + return getValue(Style) +} + +func sizeStyledProperty(view View, tag string) (SizeUnit, bool) { + if value, ok := sizeProperty(view, tag, view.Session()); ok { + return value, true + } + if value, ok := valueFromStyle(view, tag); ok { + return valueToSizeUnit(value, view.Session()) + } + return AutoSize(), false +} + +func enumStyledProperty(view View, tag string, defaultValue int) (int, bool) { + if value, ok := enumProperty(view, tag, view.Session(), defaultValue); ok { + return value, true + } + if value, ok := valueFromStyle(view, tag); ok { + return valueToEnum(value, tag, view.Session(), defaultValue) + } + return defaultValue, false +} + +func boolStyledProperty(view View, tag string) (bool, bool) { + if value, ok := boolProperty(view, tag, view.Session()); ok { + return value, true + } + if value, ok := valueFromStyle(view, tag); ok { + return valueToBool(value, view.Session()) + } + return false, false +} + +func intStyledProperty(view View, tag string, defaultValue int) (int, bool) { + if value, ok := intProperty(view, tag, view.Session(), defaultValue); ok { + return value, true + } + if value, ok := valueFromStyle(view, tag); ok { + return valueToInt(value, view.Session(), defaultValue) + } + return defaultValue, false +} + +func floatStyledProperty(view View, tag string, defaultValue float64) (float64, bool) { + if value, ok := floatProperty(view, tag, view.Session(), defaultValue); ok { + return value, true + } + if value, ok := valueFromStyle(view, tag); ok { + return valueToFloat(value, view.Session(), defaultValue) + } + + return defaultValue, false +} + +func colorStyledProperty(view View, tag string) (Color, bool) { + if value, ok := colorProperty(view, tag, view.Session()); ok { + return value, true + } + if value, ok := valueFromStyle(view, tag); ok { + return valueToColor(value, view.Session()) + } + return Color(0), false +} + +func FocusView(view View) { + if view != nil { + view.Session().runScript("focus('" + view.htmlID() + "')") + } +} + +func FocusViewByID(viewID string, session Session) { + if viewID != "" { + session.runScript("focus('" + viewID + "')") + } +} diff --git a/view_test.go b/view_test.go new file mode 100644 index 0000000..433c1e8 --- /dev/null +++ b/view_test.go @@ -0,0 +1,56 @@ +package rui + +/* +import ( + "testing" +) + +func testViewCreate(t *testing.T, session Session, viewText string) View { + if obj, err := ParseDataText(viewText); err == nil { + if view := CreateView(session, obj); view != nil { + writer := newCompactDataWriter() + WriteViewData(writer, view) + if str := writer.String(); str != viewText { + t.Errorf("\n result: \"%s\"\nexpected: \"%s\"", str, viewText) + } + return view + } + t.Errorf("CreateView(`%s`) == nil", viewText) + + } else { + t.Error(err) + } + return nil +} + +func TestViewCreate(t *testing.T) { + + testView1 := `View{id=View1,width=100%,height=20cm,margin="0px,0.8in,0px,16mm",padding="10px,8px,12px,16px",visibility=invisible,p=x}` + session := newSession(nil, 0, "", false, false) + + if obj, err := ParseDataText(testView1); err == nil { + if view := CreateView(session, obj); view != nil { + //view.ParseProperties(obj) + if view.ID() != "View1" { + t.Errorf(`view.ID() != "%s"`, view.ID()) + } + if view.Tag() != "View" { + t.Errorf(`view.Tag() != "%s"`, view.Tag()) + } + if view.Width() != Percent(100) { + t.Errorf(`view.Width() == "%s"`, view.Width().String()) + } + if view.Height() != Cm(20) { + t.Errorf(`view.Height() == "%s"`, view.Height().String()) + } + if view.Visibility() != Invisible { + t.Error(`view.Visibility() != Invisible`) + } + } + } else { + t.Error(err) + } + + testViewCreate(t, session, testView1) +} +*/ diff --git a/viewsContainer.go b/viewsContainer.go new file mode 100644 index 0000000..6b8094a --- /dev/null +++ b/viewsContainer.go @@ -0,0 +1,319 @@ +package rui + +import "strings" + +type ParanetView interface { + // Views return a list of child views + Views() []View +} + +// ViewsContainer - mutable list-container of Views +type ViewsContainer interface { + View + ParanetView + // Append appends a view to the end of the list of a view children + Append(view View) + // Insert inserts a view to the "index" position in the list of a view children + Insert(view View, index uint) + // Remove removes a view from the list of a view children and return it + RemoveView(index uint) View +} + +type viewsContainerData struct { + viewData + views []View +} + +// Init initialize fields of ViewsContainer by default values +func (container *viewsContainerData) Init(session Session) { + container.viewData.Init(session) + container.tag = "ViewsContainer" + container.views = []View{} +} + +func (container *viewsContainerData) setParentID(parentID string) { + container.viewData.setParentID(parentID) + htmlID := container.htmlID() + for _, view := range container.views { + view.setParentID(htmlID) + } +} + +// SetDisabled set the View disabled state +func (container *viewsContainerData) SetDisabled(disabled bool) { + container.viewData.Set(Disabled, disabled) + if container.views != nil { + for _, view := range container.views { + view.Set(Disabled, disabled) + } + } +} + +// Views return a list of child views +func (container *viewsContainerData) Views() []View { + if container.views == nil { + container.views = []View{} + } + return container.views +} + +// Append appends a view to the end of the list of a view children +func (container *viewsContainerData) Append(view View) { + if view != nil { + htmlID := container.htmlID() + view.setParentID(htmlID) + if container.views == nil || len(container.views) == 0 { + container.views = []View{view} + } else { + container.views = append(container.views, view) + } + updateInnerHTML(container.htmlID(), container.session) + } +} + +// Insert inserts a view to the "index" position in the list of a view children +func (container *viewsContainerData) Insert(view View, index uint) { + if view != nil { + htmlID := container.htmlID() + if container.views == nil || index >= uint(len(container.views)) { + container.Append(view) + } else if index > 0 { + view.setParentID(htmlID) + container.views = append(container.views[:index], append([]View{view}, container.views[index:]...)...) + updateInnerHTML(container.htmlID(), container.session) + } else { + view.setParentID(htmlID) + container.views = append([]View{view}, container.views...) + updateInnerHTML(container.htmlID(), container.session) + } + } +} + +// Remove removes view from list and return it +func (container *viewsContainerData) RemoveView(index uint) View { + if container.views == nil { + container.views = []View{} + return nil + } + + viewsLen := uint(len(container.views)) + if index >= viewsLen { + return nil + } + + view := container.views[index] + if index == 0 { + container.views = container.views[1:] + } else if index == viewsLen-1 { + container.views = container.views[:index] + } else { + container.views = append(container.views[:index], container.views[index+1:]...) + } + + view.setParentID("") + updateInnerHTML(container.htmlID(), container.session) + return view +} + +func (container *viewsContainerData) cssStyle(self View, builder cssBuilder) { + container.viewData.cssStyle(self, builder) + builder.add(`overflow`, `auto`) +} + +func (container *viewsContainerData) htmlSubviews(self View, buffer *strings.Builder) { + if container.views != nil { + for _, view := range container.views { + viewHTML(view, buffer) + } + } +} + +func viewFromTextValue(text string, session Session) View { + if strings.Contains(text, "{") && strings.Contains(text, "}") { + data := ParseDataText(text) + if data != nil { + if view := CreateViewFromObject(session, data); view != nil { + return view + } + } + } + return NewTextView(session, Params{Text: text}) +} + +func (container *viewsContainerData) Remove(tag string) { + container.remove(strings.ToLower(tag)) +} + +func (container *viewsContainerData) remove(tag string) { + switch tag { + case Content: + if container.views == nil || len(container.views) > 0 { + container.views = []View{} + updateInnerHTML(container.htmlID(), container.Session()) + } + + default: + container.viewData.remove(tag) + } +} + +func (container *viewsContainerData) Set(tag string, value interface{}) bool { + return container.set(strings.ToLower(tag), value) +} + +func (container *viewsContainerData) set(tag string, value interface{}) bool { + if value == nil { + container.remove(tag) + return true + } + + if tag != Content { + return container.viewData.set(tag, value) + } + + session := container.Session() + switch value := value.(type) { + case View: + container.views = []View{value} + + case []View: + container.views = value + + case string: + container.views = []View{viewFromTextValue(value, session)} + + case []string: + views := []View{} + for _, text := range value { + views = append(views, viewFromTextValue(text, session)) + } + container.views = views + + case []interface{}: + views := []View{} + for _, v := range value { + switch v := v.(type) { + case View: + views = append(views, v) + + case string: + views = append(views, viewFromTextValue(v, session)) + + default: + notCompatibleType(tag, value) + return false + } + } + container.views = views + + case DataObject: + if view := CreateViewFromObject(session, value); view != nil { + container.views = []View{view} + } else { + return false + } + + case []DataValue: + views := []View{} + for _, data := range value { + if data.IsObject() { + if view := CreateViewFromObject(session, data.Object()); view != nil { + views = append(views, view) + } + } else { + views = append(views, viewFromTextValue(data.Value(), session)) + } + } + container.views = views + + default: + notCompatibleType(tag, value) + return false + } + + htmlID := container.htmlID() + for _, view := range container.views { + view.setParentID(htmlID) + } + + if container.created { + updateInnerHTML(htmlID, container.session) + } + return true +} + +func (container *viewsContainerData) Get(tag string) interface{} { + return container.get(strings.ToLower(tag)) +} + +func (container *viewsContainerData) get(tag string) interface{} { + switch tag { + case Content: + return container.views + + default: + return container.viewData.get(tag) + } +} + +// AppendView appends a view to the end of the list of a view children +func AppendView(rootView View, containerID string, view View) bool { + var container ViewsContainer = nil + if containerID != "" { + container = ViewsContainerByID(rootView, containerID) + } else { + if cont, ok := rootView.(ViewsContainer); ok { + container = cont + } else { + ErrorLogF(`Unable to add a view to "%s"`, rootView.Tag()) + } + } + + if container != nil { + container.Append(view) + return true + } + + return false +} + +// Insert inserts a view to the "index" position in the list of a view children +func InsertView(rootView View, containerID string, view View, index uint) bool { + var container ViewsContainer = nil + if containerID != "" { + container = ViewsContainerByID(rootView, containerID) + } else { + if cont, ok := rootView.(ViewsContainer); ok { + container = cont + } else { + ErrorLogF(`Unable to add a view to "%s"`, rootView.Tag()) + } + } + + if container != nil { + container.Insert(view, index) + return true + } + + return false +} + +// Remove removes a view from the list of a view children and return it +func RemoveView(rootView View, containerID string, index uint) View { + var container ViewsContainer = nil + if containerID != "" { + container = ViewsContainerByID(rootView, containerID) + } else { + if cont, ok := rootView.(ViewsContainer); ok { + container = cont + } else { + ErrorLogF(`Unable to add a view to "%s"`, rootView.Tag()) + } + } + + if container != nil { + return container.RemoveView(index) + } + + return nil +} diff --git a/webBrige.go b/webBrige.go new file mode 100644 index 0000000..0c45aed --- /dev/null +++ b/webBrige.go @@ -0,0 +1,124 @@ +package rui + +import ( + "net/http" + "strconv" + "sync" + + "github.com/gorilla/websocket" +) + +type WebBrige interface { + ReadMessage() (string, bool) + WriteMessage(text string) bool + RunGetterScript(script string) DataObject + AnswerReceived(answer DataObject) + Close() +} + +type wsBrige struct { + conn *websocket.Conn + answer map[int]chan DataObject + answerID int + answerMutex sync.Mutex + closed bool +} + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 8096, +} + +func CreateSocketBrige(w http.ResponseWriter, req *http.Request) WebBrige { + conn, err := upgrader.Upgrade(w, req, nil) + if err != nil { + ErrorLog(err.Error()) + return nil + } + + brige := new(wsBrige) + brige.answerID = 1 + brige.answer = make(map[int]chan DataObject) + brige.conn = conn + brige.closed = false + return brige +} + +func (brige *wsBrige) Close() { + brige.closed = true + brige.conn.Close() +} + +func (brige *wsBrige) ReadMessage() (string, bool) { + //messageType, p, err := brige.conn.ReadMessage() + _, p, err := brige.conn.ReadMessage() + if err != nil { + if !brige.closed { + ErrorLog(err.Error()) + } + return "", false + } + + return string(p), true +} + +func (brige *wsBrige) WriteMessage(script string) bool { + if ProtocolInDebugLog { + DebugLog("Run script:") + DebugLog(script) + } + if err := brige.conn.WriteMessage(websocket.TextMessage, []byte(script)); err != nil { + ErrorLog(err.Error()) + return false + } + return true +} + +func (brige *wsBrige) RunGetterScript(script string) DataObject { + brige.answerMutex.Lock() + answerID := brige.answerID + brige.answerID++ + brige.answerMutex.Unlock() + + answer := make(chan DataObject) + brige.answer[answerID] = answer + errorText := "" + if brige.conn != nil { + script = "var answerID = " + strconv.Itoa(answerID) + ";\n" + script + if ProtocolInDebugLog { + DebugLog("\n" + script) + } + err := brige.conn.WriteMessage(websocket.TextMessage, []byte(script)) + if err == nil { + return <-answer + } + errorText = err.Error() + } else { + if ProtocolInDebugLog { + DebugLog("\n" + script) + } + errorText = "No connection" + } + + result := NewDataObject("error") + result.SetPropertyValue("text", errorText) + delete(brige.answer, answerID) + return result +} + +func (brige *wsBrige) AnswerReceived(answer DataObject) { + if text, ok := answer.PropertyValue("answerID"); ok { + if id, err := strconv.Atoi(text); err == nil { + if chanel, ok := brige.answer[id]; ok { + chanel <- answer + delete(brige.answer, id) + } else { + ErrorLog("Bad answerID = " + text + " (chan not found)") + } + } else { + ErrorLog("Invalid answerID = " + text) + } + } else { + ErrorLog("answerID not found") + } +}