forked from mbk-lab/rui_orig
2
0
Fork 0

Merge branch 'Work'

This commit is contained in:
Alexei Anoshenko 2022-03-19 19:39:23 +03:00
commit b0e51e0830
31 changed files with 1937 additions and 248 deletions

View File

@ -1,3 +1,12 @@
# v0.5.0
* NewApplication function and Start function of the Application interface were replaced by StartApp function
* Added HasFocus function to the View interface
* Added the UserAgent function to the Session interface
* Added the following properties to TableView: "selection-mode", "allow-selection", "current", "current-style", "current-inactive-style"
* Added the following events to TableView: "table-cell-selected", "table-cell-clicked", "table-row-selected", "table-row-clicked"
* Bug fixing
# v0.4.0
* Added SetTitle and SetTitleColor function to the Session interface

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 Alexei Anoshenko
Copyright (c) 2021-2022 Alexei Anoshenko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -21,28 +21,33 @@
}
func main() {
app := rui.NewApplication("Hello world", "icon.svg", createHelloWorldSession)
app.Start("localhost:8000")
rui.StartApp("localhost:8000", createHelloWorldSession, rui.AppParams{
Title: "Hello world",
Icon: "icon.svg",
})
}
В функции main создается rui приложение и запускается основной цикл.
При создании приложения задаются 3 параметра: имя приложения, имя иконки и функция createHelloWorldSession.
Функция createHelloWorldSession создает структуру реализующую интерфейс SessionContent:
В функции main вызывается функция StartApp. Она создает rui приложение и запускает его основной цикл.
Функция StartApp имеет 3 параметра:
1) IP адрес по которому будет доступно приложение (в нашем примере это "localhost:8000")
2) Фуекция создает структуру реализующую интерфейс SessionContent
3) Дополнительные опциональные параметры (в нашем примере это заголовок и имя файла иконки)
Интерфейс SessionContent объявлен как:
type SessionContent interface {
CreateRootView(session rui.Session) rui.View
}
Для каждой новой сессии создается свой экземпляр структуры.
Функция CreateRootView интерфейса SessionContent создает корневой элемент.
Когда пользователь обращается к приложению набрав в браузере адрес "localhost:8000", то создается новая сессия,
для нее создается новый экземпляр структуры helloWorldSession и в конце вызывается функция CreateRootView.
Функция createRootView возвращает представление строки текста, создаваемое с помощью функции NewTextView.
Если вы хотите чтобы приложение было видно вне вашего компьютера, то поменяйте адрес в функции Start:
app.Start(rui.GetLocalIP() + ":80")
rui.StartApp(rui.GetLocalIP() + ":80", ...
## Используемые типы данных
@ -3260,6 +3265,7 @@ Cell(row, column int) возвращает содержимое ячейки т
* rui.Color
* rui.View
* fmt.Stringer
* rui.VerticalTableJoin, rui.HorizontalTableJoin
Свойству "content" можно также присваивать следующие типы данных
@ -3314,11 +3320,11 @@ Cell(row, column int) возвращает содержимое ячейки т
В этом случае таблица будет иметь следующий вид
|------|----------------|
| | |
| |-------|--------|
| | | |
|------|-------|--------|
|------|----------------|
| | |
| |-------|--------|
| | | |
|------|-------|--------|
Если в качестве значения свойства "content" используется [][]interface{}, то для объединения
ячеек используются пустые структуры
@ -3421,7 +3427,110 @@ TableColumnStyle объявлена как
| 2 | CenterAlign | "center" | Выравнивание по центру |
| 3, 4 | BaselineAlign | "baseline" | Выравнивание по базовой линии |
Для горизонтального выравнивания используется свойство "text-align"
Для горизонтального выравнивания используется свойство "text-align".
Получить значение данного свойства можно с помощью функции
func GetTableVerticalAlign(view View, subviewID string) int
### Свойство "selection-mode"
Свойство "selection-mode" (константа SelectionMode) типа int определяет режим
выделения (подсвечивания) элементов таблицы. Доступные режимы:
* NoneSelection (0). Режим по умолчанию. В данном режиме нельзя выделять элементы таблицы. Таблица не может
получить фокус ввода.
* CellSelection (1). В данном режиме может выделяться (подсвечиваться) одна ячейка таблицы.
Ячейка выделяется интерактивно с помощью мыши или клавиатуры (с использованием клавиш управления курсором).
В данном режиме таблица может получить фокус ввода. В данном режиме таблица генерирует два вида
событий: "table-cell-selected" и "table-cell-clicked" (о них ниже).
* RowSelection (2). В данном режиме может выделяться (подсвечиваться) только строка таблицы целиком.
В данном режиме таблица похожа на ListView. Строка выделяется интерактивно с помощью мыши или клавиатуры
(с использованием клавиш управления курсором). В данном режиме таблица может получить фокус ввода.
В данном режиме таблица генерирует два вида событий: "table-row-selected" и "table-row-clicked" (о них ниже).
Получить значение данного свойства можно с помощью функции
func GetSelectionMode(view View, subviewID string) int
### Свойство "current"
Свойство "current" (константа Current) задает координаты выбранной ячейки/строки
в виде структуры
type CellIndex struct {
Row, Column int
}
Если ячейка не выбрана, то значения полей Row и Column будут меньше 0.
В режиме RowSelection значение поля Column игнорируется. Также в данном режиме
свойству "current" можно присваивать значение типа int (индекс строки).
Получить значение данного свойства можно с помощью функции
func GetTableCurrent(view View, subviewID string) CellIndex
### Свойство "allow-selection"
По умолчанию вы можете выделить любую ячейку/строку таблицы. Однако часто необходимо запретить
выбор определенных элементов. Свойство "selection-mode" (константа SelectionMode) позволяет
задать такое правило.
В режиме CellSelection данному свойству присваивается реализация интерфейса
type TableAllowCellSelection interface {
AllowCellSelection(row, column int) bool
}
а в режиме RowSelection - реализация интерфейса
type TableAllowRowSelection interface {
AllowRowSelection(row int) bool
}
Функция AllowCellSelection/AllowRowSelection должна возвращать "true" если ячейка/строка
может быть выделена и "false" если ячейку/строку запрещено выделять.
### События "table-cell-selected" и "table-cell-clicked"
Событие "table-cell-selected" генерируется в режиме CellSelection когда пользователь выделил
ячейку таблицы с помощью мыши или клавиатуры.
Событие "table-cell-clicked" возникает если пользователь кликает мышью по ячейке таблицы
(при этом если она не выделена, то сначала возникает событие "table-cell-selected") или
нажимает клавишу Enter или пробел
Основной слушатель данных событий имеет следующий формат:
func(TableView, int, int)
где второй аргумент это индекс строки ячейки, третий - индекс столбца
Можно также использовать слушателя следующего формата:
func(int, int)
### События "table-row-selected" и "table-row-clicked"
Событие "table-row-selected" генерируется в режиме RowSelection когда пользователь выделил
строку таблицы с помощью мыши или клавиатуры.
Событие "table-row-clicked" возникает если пользователь кликает мышью по строке таблицы
(при этом если она не выделена, то сначала возникает событие "table-row-selected") или
нажимает клавишу Enter или пробел
Основной слушатель данных событий имеет следующий формат:
func(TableView, int)
где второй аргумент это индекс строки.
Можно также использовать слушателя следующего формата:
func(int)
## Пользовательский View

163
README.md
View File

@ -1,6 +1,6 @@
# RUI library
The RUI (Remoute User Interface) library is designed to create web applications in the go language.
The RUI (Remote 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.
@ -21,13 +21,19 @@ and the browser is used as a thin client. WebSocket is used for client-server co
}
func main() {
app := rui.NewApplication("Hello world", "icon.svg", createHelloWorldSession)
app.Start("localhost:8000")
rui.StartApp("localhost:8000", createHelloWorldSession, rui.AppParams{
Title: "Hello world",
Icon: "icon.svg",
})
}
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:
In the main function, the StartApp function is called. It creates a rui app and runs its main loop.
The StartApp function has 3 parameters:
1) IP address where the application will be available (in our example it is "localhost:8000")
2) The function creates a structure that implements the SessionContent interface
3) Additional optional parameters (in our example, this is the title and the icon file name)
The SessionContent interface is declared as:
type SessionContent interface {
CreateRootView(session rui.Session) rui.View
@ -42,7 +48,7 @@ The createRootView function returns a representation of a text that is created u
If you want the application to be visible outside your computer, then change the address in the Start function:
app.Start(rui.GetLocalIP() + ":80")
rui.StartApp(rui.GetLocalIP() + ":80", ...
## Used data types
@ -768,12 +774,12 @@ For this, the following properties of the SizeUnit type are used:
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 |
| 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
@ -1301,7 +1307,7 @@ Lines are split on newlines, on "br" elements, and optionally to fill inline box
* 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.
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 |
|-----------------------|-----------|-----------------|---------------|--------------------|------------------------------------|
@ -1448,7 +1454,8 @@ You can get the value of this property using the function
#### "text-indent" property
The "text-indent" (TextIndent constant) SizeUnit property determines the size of the indent (empty space) before the first line of text.
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
@ -1514,7 +1521,7 @@ You can get the value of this property using the function
#### "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:
Possible values are:
| Value | Constant | Description |
|:-----:|-----------------------|------------------------------------------------------------------|
@ -1871,8 +1878,8 @@ The Touch structure describes a single touch and has the following fields
| 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. |
| 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. |
@ -2038,8 +2045,8 @@ relative to each other. The property can take the following values:
| 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.
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
@ -2610,7 +2617,7 @@ For a multi-line editor, auto-wrap mode can be enabled. The bool property "wrap"
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:
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
@ -2665,7 +2672,7 @@ The value of the "date-picker-value" property can also be read using the functio
func GetNumberPickerValue(view View, subviewID string) float64
The entered values may be subject to restrictions. For this, the following properties are used:
The entered values may be subject to restrictions. For this, the following properties are used:
| Property | Constant | Restriction |
|--------------------|------------------|-------------------|
@ -2678,7 +2685,7 @@ Assignments to these properties can be the same value types as "date-picker-valu
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:
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
@ -3027,8 +3034,8 @@ will be positioned relative to each other. The property can take the following v
| 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.
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
@ -3224,6 +3231,7 @@ Cell(row, column int) returns the contents of a table cell. The Cell() function
* rui.Color
* rui.View
* fmt.Stringer
* rui.VerticalTableJoin, rui.HorizontalTableJoin
The "content" property can also be assigned the following data types
@ -3277,11 +3285,11 @@ The "row-span" property specifies how many cells to merge vertically, and the "c
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
@ -3385,6 +3393,101 @@ the vertical alignment of data within a table cell. Valid values:
For horizontal alignment, use the "text-align" property
You can get the value of this property using the function
func GetTableVerticalAlign(view View, subviewID string) int
### "selection-mode" property
The "selection-mode" property (SelectionMode constant) of the int type determines the mode of selection (highlighting) of table elements. Available modes:
* NoneSelection (0). Default mode. In this mode, you cannot select table elements. The table cannot receive input focus.
* CellSelection (1). In this mode, one table cell can be selected (highlighted).
The cell is selected interactively using the mouse or keyboard (using the cursor keys).
In this mode, the table can receive input focus. In this mode, the table generates two types of events: "table-cell-selected" and "table-cell-clicked" (see below).
* RowSelection (2). In this mode, only the entire table row can be selected (highlighted).
In this mode, the table is similar to a ListView. The row is selected interactively
with the mouse or keyboard (using the cursor keys). In this mode, the table can receive input focus.
In this mode, the table generates two types of events: "table-row-selected" and "table-row-clicked" (see below).
You can get the value of this property using the function
func GetSelectionMode(view View, subviewID string) int
### "current" property
The "current" property (Current constant) sets the coordinates of the selected cell/row as a structure
type CellIndex struct {
Row, Column int
}
If the cell is not selected, then the values of the Row and Column fields will be less than 0.
In RowSelection mode, the value of the Column field is ignored. Also in this mode,
the "current" property can be assigned a value of type int (row index).
You can get the value of this property using the function
func GetTableCurrent(view View, subviewID string) CellIndex
### "allow-selection" property
By default, you can select any cell/row of the table. However, it is often necessary to disable the selection of certain elements. The "selection-mode" property (SelectionMode constant) allows you to set such a rule.
In CellSelection mode, this property is assigned the implementation of the interface
type TableAllowCellSelection interface {
AllowCellSelection(row, column int) bool
}
and in RowSelection mode this property is assigned the implementation of the interface
type TableAllowRowSelection interface {
AllowRowSelection(row int) bool
}
The AllowCellSelection/AllowRowSelection function must return "true"
if the cell/row can be selected and "false" if the cell/row cannot be selected.
### "table-cell-selected" and "table-cell-clicked" events
The "table-cell-selected" event is fired in CellSelection mode when the user has selected
a table cell with the mouse or keyboard.
The "table-cell-clicked" event occurs if the user clicks on a table cell (and if it is not selected,
the "table-cell-selected" event occurs first) or presses the Enter or Space key.
The main listener for these events has the following format:
func(TableView, int, int)
where the second argument is the cell row index, the third argument is the column index
You can also use a listener in the following format:
func(int, int)
### "table-row-selected" and "table-row-clicked" events
The "table-row-selected" event is fired in RowSelection mode when the user has selected
a table row with the mouse or keyboard.
The "table-row-clicked" event occurs if the user clicks on a table row (if it is not selected,
the "table-row-selected" event fires first) or presses the Enter or Space key.
The main listener for these events has the following format:
func(TableView, int)
where the second argument is the row index.
You can also use a listener in the following format:
func(int)
## Custom View
A custom View must implement the CustomView interface, which extends the ViewsContainer and View interfaces.

View File

@ -42,9 +42,19 @@ function socketOpen() {
}
}
const lang = window.navigator.languages;
const lang = window.navigator.language;
if (lang) {
message += ",languages=\"" + lang + "\"";
message += ",language=\"" + lang + "\"";
}
const langs = window.navigator.languages;
if (langs) {
message += ",languages=\"" + langs + "\"";
}
const userAgent = window.navigator.userAgent
if (userAgent) {
message += ",user-agent=\"" + userAgent + "\"";
}
const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
@ -615,12 +625,13 @@ function selectDropDownListItem(elementId, number) {
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;
}
const focusStyle = getListFocusedItemStyle(element);
const blurStyle = getListSelectedItemStyle(element);
selected = (element.classList.contains(focusStyle) || element.classList.contains(blurStyle));
}
var list = element.parentNode.parentNode
if (list) {
@ -640,26 +651,33 @@ function getListItemNumber(itemId) {
}
}
function getStyleAttribute(element, attr, defValue) {
var result = element.getAttribute(attr);
if (result) {
return result;
}
return defValue;
}
function getListFocusedItemStyle(element) {
return getStyleAttribute(element, "data-focusitemstyle", "ruiListItemFocused");
}
function getListSelectedItemStyle(element) {
return getStyleAttribute(element, "data-bluritemstyle", "ruiListItemSelected");
}
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"
}
const focusStyle = getListFocusedItemStyle(element);
const blurStyle = getListSelectedItemStyle(element);
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 + "}";
@ -671,14 +689,10 @@ function selectListItem(element, item, needSendMessage) {
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
}
}
@ -690,6 +704,12 @@ function selectListItem(element, item, needSendMessage) {
}
}
if (item.scrollIntoViewIfNeeded) {
item.scrollIntoViewIfNeeded()
} else {
item.scrollIntoView({block: "nearest", inline: "nearest"});
}
/*
var left = item.offsetLeft - element.offsetLeft;
if (left < element.scrollLeft) {
element.scrollLeft = left;
@ -708,7 +728,7 @@ function selectListItem(element, item, needSendMessage) {
var bottom = top + item.offsetHeight
if (bottom > element.scrollTop + element.clientHeight) {
element.scrollTop = bottom - element.clientHeight;
}
}*/
}
if (needSendMessage && message != undefined) {
@ -801,24 +821,29 @@ function findBottomListItem(list, x, y) {
return result
}
function listViewKeyDownEvent(element, event) {
var key;
function getKey(event) {
if (event.key) {
key = event.key;
} else if (event.keyCode) {
return event.key;
}
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;
case 13: return "Enter";
case 32: return " ";
case 33: return "PageUp";
case 34: return "PageDown";
case 35: return "End";
case 36: return "Home";
case 37: return "ArrowLeft";
case 38: return "ArrowUp";
case 39: return "ArrowRight";
case 40: return "ArrowDown";
}
}
}
function listViewKeyDownEvent(element, event) {
const key = getKey(event);
if (key) {
var currentId = element.getAttribute("data-current");
var current
@ -885,21 +910,10 @@ function listViewFocusEvent(element, event) {
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;
}
current.classList.remove(getListSelectedItemStyle(element));
current.classList.add(getListFocusedItemStyle(element));
}
}
}
}
@ -909,20 +923,9 @@ function listViewBlurEvent(element, event) {
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;
current.classList.remove(getListFocusedItemStyle(element));
current.classList.add(getListSelectedItemStyle(element));
}
}
}
@ -1372,4 +1375,397 @@ function setTitleColor(color) {
function detailsEvent(element) {
sendMessage("details-open{session=" + sessionID + ",id=" + element.id + ",open=" + (element.open ? "1}" : "0}"));
}
function getTableFocusedItemStyle(element) {
return getStyleAttribute(element, "data-focusitemstyle", "ruiCurrentTableCellFocused");
}
function getTableSelectedItemStyle(element) {
return getStyleAttribute(element, "data-bluritemstyle", "ruiCurrentTableCell");
}
function tableViewFocusEvent(element, event) {
var currentId = element.getAttribute("data-current");
if (currentId) {
var current = document.getElementById(currentId);
if (current) {
if (current.classList) {
current.classList.remove(getTableSelectedItemStyle(element));
current.classList.add(getTableFocusedItemStyle(element));
}
}
}
}
function tableViewBlurEvent(element, event) {
var currentId = element.getAttribute("data-current");
if (currentId) {
var current = document.getElementById(currentId);
if (current && current.classList) {
current.classList.remove(getTableFocusedItemStyle(element));
current.classList.add(getTableSelectedItemStyle(element));
}
}
}
function setTableCellCursor(element, row, column) {
const cellID = element.id + "-" + row + "-" + column;
var cell = document.getElementById(cellID);
if (!cell || cell.getAttribute("data-disabled")) {
return false;
}
const focusStyle = getTableFocusedItemStyle(element);
const oldCellID = element.getAttribute("data-current");
if (oldCellID) {
const oldCell = document.getElementById(oldCellID);
if (oldCell && oldCell.classList) {
oldCell.classList.remove(focusStyle);
oldCell.classList.remove(getTableSelectedItemStyle(element));
}
}
cell.classList.add(focusStyle);
element.setAttribute("data-current", cellID);
if (cell.scrollIntoViewIfNeeded) {
cell.scrollIntoViewIfNeeded()
} else {
cell.scrollIntoView({block: "nearest", inline: "nearest"});
}
sendMessage("currentCell{session=" + sessionID + ",id=" + element.id +
",row=" + row + ",column=" + column + "}");
return true;
}
function moveTableCellCursor(element, row, column, dr, dc) {
const rows = element.getAttribute("data-rows");
if (!rows) {
return;
}
const columns = element.getAttribute("data-columns");
if (!columns) {
return;
}
const rowCount = parseInt(rows);
const columnCount = parseInt(columns);
row += dr;
column += dc;
while (row >= 0 && row < rowCount && column >= 0 && column < columnCount) {
if (setTableCellCursor(element, row, column)) {
return;
} else if (dr == 0) {
var r2 = row - 1;
while (r2 >= 0) {
if (setTableCellCursor(element, r2, column)) {
return;
}
r2--;
}
} else if (dc == 0) {
var c2 = column - 1;
while (c2 >= 0) {
if (setTableCellCursor(element, row, c2)) {
return;
}
c2--;
}
}
row += dr;
column += dc;
}
}
function tableViewCellKeyDownEvent(element, event) {
const key = getKey(event);
if (!key) {
return;
}
const currentId = element.getAttribute("data-current");
if (!currentId || currentId == "") {
switch (key) {
case "ArrowLeft":
case "ArrowRight":
case "ArrowDown":
case "ArrowUp":
case "Home":
case "End":
case "PageUp":
case "PageDown":
const rows = element.getAttribute("data-rows");
const columns = element.getAttribute("data-columns");
if (rows && columns) {
const rowCount = parseInt(rows);
const columnCount = parseInt(rows);
row = 0;
while (row < rowCount) {
column = 0;
while (columns < columnCount) {
if (setTableCellCursor(element, row, column)) {
event.stopPropagation();
event.preventDefault();
return;
}
column++;
}
row++;
}
}
event.stopPropagation();
event.preventDefault();
break;
}
return;
}
const elements = currentId.split("-");
if (elements.length >= 3) {
const row = parseInt(elements[1], 10)
const column = parseInt(elements[2], 10)
switch (key) {
case " ":
case "Enter":
sendMessage("cellClick{session=" + sessionID + ",id=" + element.id +
",row=" + row + ",column=" + column + "}");
break;
case "ArrowLeft":
moveTableCellCursor(element, row, column, 0, -1)
break;
case "ArrowRight":
moveTableCellCursor(element, row, column, 0, 1)
break;
case "ArrowDown":
moveTableCellCursor(element, row, column, 1, 0)
break;
case "ArrowUp":
moveTableCellCursor(element, row, column, -1, 0)
break;
case "Home":
// TODO
break;
case "End":
/*var newRow = rowCount-1;
while (newRow > row) {
if (setTableRowCursor(element, newRow)) {
break;
}
newRow--;
}*/
// TODO
break;
case "PageUp":
// TODO
break;
case "PageDown":
// TODO
break;
default:
return;
}
event.stopPropagation();
event.preventDefault();
} else {
element.setAttribute("data-current", "");
}
}
function setTableRowCursor(element, row) {
const tableRowID = element.id + "-" + row;
var tableRow = document.getElementById(tableRowID);
if (!tableRow || tableRow.getAttribute("data-disabled")) {
return false;
}
const focusStyle = getTableFocusedItemStyle(element);
const oldRowID = element.getAttribute("data-current");
if (oldRowID) {
const oldRow = document.getElementById(oldRowID);
if (oldRow && oldRow.classList) {
oldRow.classList.remove(focusStyle);
oldRow.classList.remove(getTableSelectedItemStyle(element));
}
}
tableRow.classList.add(focusStyle);
element.setAttribute("data-current", tableRowID);
if (tableRow.scrollIntoViewIfNeeded) {
tableRow.scrollIntoViewIfNeeded()
} else {
tableRow.scrollIntoView({block: "nearest", inline: "nearest"});
}
sendMessage("currentRow{session=" + sessionID + ",id=" + element.id + ",row=" + row + "}");
return true;
}
function moveTableRowCursor(element, row, dr) {
const rows = element.getAttribute("data-rows");
if (!rows) {
return;
}
const rowCount = parseInt(rows);
row += dr;
while (row >= 0 && row < rowCount) {
if (setTableRowCursor(element, row)) {
return;
}
row += dr;
}
}
function tableViewRowKeyDownEvent(element, event) {
const key = getKey(event);
if (key) {
const currentId = element.getAttribute("data-current");
if (currentId) {
const elements = currentId.split("-");
if (elements.length >= 2) {
const row = parseInt(elements[1], 10);
switch (key) {
case " ":
case "Enter":
sendMessage("rowClick{session=" + sessionID + ",id=" + element.id + ",row=" + row + "}");
break;
case "ArrowDown":
moveTableRowCursor(element, row, 1)
break;
case "ArrowUp":
moveTableRowCursor(element, row, -1)
break;
case "Home":
var newRow = 0;
while (newRow < row) {
if (setTableRowCursor(element, newRow)) {
break;
}
newRow++;
}
break;
case "End":
var newRow = rowCount-1;
while (newRow > row) {
if (setTableRowCursor(element, newRow)) {
break;
}
newRow--;
}
break;
case "PageUp":
// TODO
break;
case "PageDown":
// TODO
break;
default:
return;
}
event.stopPropagation();
event.preventDefault();
return;
}
}
switch (key) {
case "ArrowLeft":
case "ArrowRight":
case "ArrowDown":
case "ArrowUp":
case "Home":
case "End":
case "PageUp":
case "PageDown":
const rows = element.getAttribute("data-rows");
if (rows) {
const rowCount = parseInt(rows);
row = 0;
while (row < rowCount) {
if (setTableRowCursor(element, row)) {
break;
}
row++;
}
}
break;
default:
return;
}
}
event.stopPropagation();
event.preventDefault();
}
function tableCellClickEvent(element, event) {
event.preventDefault();
const elements = element.id.split("-");
if (elements.length < 3) {
return
}
const tableID = elements[0];
const row = parseInt(elements[1], 10);
const column = parseInt(elements[2], 10);
const table = document.getElementById(tableID);
if (table) {
const selection = table.getAttribute("data-selection");
if (selection == "cell") {
const currentID = table.getAttribute("data-current");
if (!currentID || currentID != element.ID) {
setTableCellCursor(table, row, column)
}
}
}
sendMessage("cellClick{session=" + sessionID + ",id=" + tableID +
",row=" + row + ",column=" + column + "}");
}
function tableRowClickEvent(element, event) {
event.preventDefault();
const elements = element.id.split("-");
if (elements.length < 2) {
return
}
const tableID = elements[0];
const row = parseInt(elements[1], 10);
const table = document.getElementById(tableID);
if (table) {
const selection = table.getAttribute("data-selection");
if (selection == "cell") {
const currentID = table.getAttribute("data-current");
if (!currentID || currentID != element.ID) {
setTableRowCursor(table, row)
}
}
}
sendMessage("rowClick{session=" + sessionID + ",id=" + tableID + ",row=" + row + "}");
}

View File

@ -27,19 +27,24 @@ 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
params AppParams
createContentFunc func(Session) SessionContent
sessions map[int]Session
}
// AppParams defines parameters of the app
type AppParams struct {
Title string
TitleColor Color
Icon string
}
func (app *application) getStartPage() string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
@ -49,12 +54,19 @@ func (app *application) getStartPage() string {
<head>
<meta charset="utf-8">
<title>`)
buffer.WriteString(app.name)
buffer.WriteString(app.params.Title)
buffer.WriteString("</title>")
if app.icon != "" {
if app.params.Icon != "" {
buffer.WriteString(`
<link rel="icon" href="`)
buffer.WriteString(app.icon)
buffer.WriteString(app.params.Icon)
buffer.WriteString(`">`)
}
if app.params.TitleColor != 0 {
buffer.WriteString(`
<meta name="theme-color" content="`)
buffer.WriteString(app.params.TitleColor.cssString())
buffer.WriteString(`">`)
}
@ -78,9 +90,8 @@ func (app *application) getStartPage() string {
return buffer.String()
}
func (app *application) init(name, icon string) {
app.name = name
app.icon = icon
func (app *application) init(params AppParams) {
app.params = params
app.sessions = map[int]Session{}
}
@ -277,12 +288,14 @@ func (app *application) startSession(params DataObject, events chan DataObject,
return session, answerText
}
// NewApplication - create the new application of the single view type.
func NewApplication(name, icon string, createContentFunc func(Session) SessionContent) Application {
// NewApplication - create the new application and start it
func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) {
app := new(application)
app.init(name, icon)
app.init(params)
app.createContentFunc = createContentFunc
return app
http.Handle("/", app)
log.Fatal(http.ListenAndServe(addr, nil))
}
func OpenBrowser(url string) bool {

View File

@ -148,11 +148,15 @@ func (customView *CustomViewData) Scroll() Frame {
return customView.superView.Scroll()
}
func (customView *CustomViewData) HasFocus() bool {
return customView.superView.HasFocus()
}
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) {
func (customView *CustomViewData) onItemResize(self View, index string, x, y, width, height float64) {
customView.superView.onItemResize(customView.superView, index, x, y, width, height)
}

View File

@ -44,6 +44,10 @@ func (picker *datePickerData) Init(session Session) {
picker.dateChangedListeners = []func(DatePicker, time.Time){}
}
func (picker *datePickerData) Focusable() bool {
return true
}
func (picker *datePickerData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag {

View File

@ -213,24 +213,32 @@ theme {
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,
}
},
ruiCurrentTableCell {
background-color=@ruiSelectedColor,
text-color=@ruiSelectedTextColor,
},
ruiCurrentTableCellFocused {
background-color=@ruiHighlightColor,
text-color=@ruiHighlightTextColor,
},
],
}

View File

@ -74,7 +74,13 @@ func textCanvasDemo(canvas rui.Canvas) {
canvas.SetTextAlign(rui.LeftAlign)
canvas.SetTextBaseline(rui.TopBaseline)
canvas.SetSolidColorFillStyle(0xFF000000)
if canvas.View().Session().DarkTheme() {
canvas.SetSolidColorFillStyle(0xFFFFFFFF)
canvas.SetSolidColorStrokeStyle(0xFFFFFFFF)
} else {
canvas.SetSolidColorFillStyle(0xFF000000)
canvas.SetSolidColorStrokeStyle(0xFF000000)
}
canvas.FillText(10, 10, "Default font")
canvas.StrokeText(300, 10, "Default font")
@ -136,7 +142,11 @@ func textCanvasDemo(canvas rui.Canvas) {
func textAlignCanvasDemo(canvas rui.Canvas) {
canvas.Save()
canvas.SetFont("sans-serif", rui.Pt(10))
canvas.SetSolidColorFillStyle(0xFF000000)
if canvas.View().Session().DarkTheme() {
canvas.SetSolidColorFillStyle(0xFFFFFFFF)
} else {
canvas.SetSolidColorFillStyle(0xFF000000)
}
canvas.SetSolidColorStrokeStyle(0xFF00FFFF)
baseline := []string{"Alphabetic", "Top", "Middle", "Bottom", "Hanging", "Ideographic"}
@ -176,18 +186,28 @@ func lineStyleCanvasDemo(canvas rui.Canvas) {
canvas.SetLineWidth(1)
y := float64(40 + 20*i)
canvas.DrawLine(10, y, 180, y)
canvas.SetSolidColorStrokeStyle(0xFF000000)
if canvas.View().Session().DarkTheme() {
canvas.SetSolidColorStrokeStyle(0xFFFFFFFF)
} else {
canvas.SetSolidColorStrokeStyle(0xFF000000)
}
canvas.SetLineWidth(10)
canvas.SetLineCap(i)
canvas.DrawLine(20, y, 170, y)
canvas.FillText(200, y, cap)
}
canvas.SetSolidColorStrokeStyle(0xFF0000FF)
if canvas.View().Session().DarkTheme() {
canvas.SetSolidColorStrokeStyle(0xFFFFFFFF)
canvas.SetSolidColorFillStyle(0xFF00FFFF)
} else {
canvas.SetSolidColorStrokeStyle(0xFF000000)
canvas.SetSolidColorFillStyle(0xFF0000FF)
}
canvas.SetFont("courier", rui.Pt(12))
canvas.FillText(80, 115, "SetLineJoin(...)")
canvas.SetSolidColorStrokeStyle(0xFF000000)
canvas.SetLineWidth(10)
canvas.SetLineCap(rui.ButtCap)
@ -206,14 +226,11 @@ func lineStyleCanvasDemo(canvas rui.Canvas) {
canvas.StrokePath(path)
canvas.FillText(210, y+20, join)
}
canvas.SetSolidColorStrokeStyle(0xFF0000FF)
canvas.SetFont("courier", rui.Pt(12))
canvas.FillText(20, 300, "SetLineDash([]float64{16, 8, 4, 8}, ...)")
canvas.SetFont("courier", rui.Pt(10))
canvas.SetLineDash([]float64{16, 8, 4, 8}, 0)
canvas.SetSolidColorStrokeStyle(0xFF000000)
canvas.SetLineWidth(4)
canvas.SetLineCap(rui.ButtCap)
@ -246,8 +263,13 @@ func transformCanvasDemo(canvas rui.Canvas) {
y1 := y0 + float64((ny-1)*20)
canvas.SetFont("serif", rui.Pt(10))
canvas.SetSolidColorFillStyle(rui.Black)
if canvas.View().Session().DarkTheme() {
canvas.SetSolidColorStrokeStyle(0xFFFFFFFF)
canvas.SetSolidColorFillStyle(0xFFFFFFFF)
} else {
canvas.SetSolidColorStrokeStyle(0xFF000000)
canvas.SetSolidColorFillStyle(0xFF000000)
}
canvas.SetTextAlign(rui.CenterAlign)
canvas.SetTextBaseline(rui.BottomBaseline)
for i := 0; i < nx; i++ {
@ -266,7 +288,11 @@ func transformCanvasDemo(canvas rui.Canvas) {
}
canvas.SetFont("courier", rui.Pt(14))
canvas.SetSolidColorFillStyle(rui.Black)
if canvas.View().Session().DarkTheme() {
canvas.SetSolidColorFillStyle(0xFFFFFFFF)
} else {
canvas.SetSolidColorFillStyle(0xFF000000)
}
canvas.SetTextAlign(rui.CenterAlign)
canvas.SetTextBaseline(rui.TopBaseline)

View File

@ -113,10 +113,6 @@ func (demo *demoSession) CreateRootView(session rui.Session) rui.View {
rui.Set(demo.rootView, "rootTitleButton", rui.ClickEvent, demo.clickMenuButton)
demo.showPage(0)
if color, ok := rui.StringToColor("#ffc0ded9"); ok {
session.SetTitleColor(color)
}
return demo.rootView
}
@ -158,11 +154,14 @@ func (demo *demoSession) showPage(index int) {
func main() {
rui.ProtocolInDebugLog = true
rui.AddEmbedResources(&resources)
app := rui.NewApplication("RUI demo", "icon.svg", createDemo)
//addr := rui.GetLocalIP() + ":8080"
addr := "localhost:8000"
fmt.Print(addr)
rui.OpenBrowser("http://" + addr)
app.Start(addr)
rui.StartApp(addr, createDemo, rui.AppParams{
Title: "RUI demo",
Icon: "icon.svg",
TitleColor: rui.Color(0xffc0ded9),
})
}

View File

@ -17,7 +17,7 @@ GridLayout {
content = [
GridLayout {
id = mouseEventsTest, cell-horizontal-align = center, cell-vertical-align = center,
height = 150%,
height = 100%,
border = _{ style = solid, width = 1px, color = gray},
content = [
TextView {

View File

@ -32,6 +32,12 @@ GridLayout {
DropDownList { row = 5, column = 1, id = tableFootStyle, current = 0, items = ["none", "tableFoot1", "rui.Params"]},
Checkbox { row = 6, column = 0:1, id = tableRowStyle, content = "Row style" },
Checkbox { row = 7, column = 0:1, id = tableColumnStyle, content = "Column style" },
TextView { row = 8, text = "Selection mode" },
DropDownList { row = 8, column = 1, id = tableSelectionMode, current = 0, items = ["none", "cell", "row"]},
Checkbox { row = 9, column = 0:1, id = tableDisableHead, content = "Disable head selection" },
Checkbox { row = 10, column = 0:1, id = tableDisableFoot, content = "Disable foot selection" },
TextView { row = 11, text = "Vertical align in cells" },
DropDownList { row = 11, column = 1, id = tableVerticalAlign, current = 0, items = ["top", "bottom", "center", "baseline"]},
]
}
]
@ -40,6 +46,31 @@ GridLayout {
}
`
type demoTableAllowSelection struct {
index []int
}
func (allow *demoTableAllowSelection) AllowCellSelection(row, column int) bool {
return allow.AllowRowSelection(row)
}
func (allow *demoTableAllowSelection) AllowRowSelection(row int) bool {
if allow.index != nil {
for _, index := range allow.index {
if index == row {
return false
}
}
}
return true
}
func newDemoTableAllowSelection(index []int) *demoTableAllowSelection {
result := new(demoTableAllowSelection)
result.index = index
return result
}
func createTableViewDemo(session rui.Session) rui.View {
view := rui.CreateViewFromText(session, tableViewDemoText)
if view == nil {
@ -93,6 +124,49 @@ func createTableViewDemo(session rui.Session) rui.View {
}
}
rui.Set(view, "tableSelectionMode", rui.DropDownEvent, func(list rui.DropDownList, number int) {
rui.Set(view, "demoTableView1", rui.SelectionMode, number)
switch rui.GetCurrent(view, "tableSelectionMode") {
case rui.CellSelection:
// TODO
case rui.RowSelection:
// TODO
}
})
rui.Set(view, "tableDisableHead", rui.CheckboxChangedEvent, func(checked bool) {
if checked {
if rui.IsCheckboxChecked(view, "tableDisableFoot") {
rui.Set(view, "demoTableView1", rui.AllowSelection, newDemoTableAllowSelection([]int{0, 1, 11}))
} else {
rui.Set(view, "demoTableView1", rui.AllowSelection, newDemoTableAllowSelection([]int{0, 1}))
}
} else {
if rui.IsCheckboxChecked(view, "tableDisableFoot") {
rui.Set(view, "demoTableView1", rui.AllowSelection, newDemoTableAllowSelection([]int{11}))
} else {
rui.Set(view, "demoTableView1", rui.AllowSelection, nil)
}
}
})
rui.Set(view, "tableDisableFoot", rui.CheckboxChangedEvent, func(checked bool) {
if checked {
if rui.IsCheckboxChecked(view, "tableDisableHead") {
rui.Set(view, "demoTableView1", rui.AllowSelection, newDemoTableAllowSelection([]int{0, 1, 11}))
} else {
rui.Set(view, "demoTableView1", rui.AllowSelection, newDemoTableAllowSelection([]int{11}))
}
} else {
if rui.IsCheckboxChecked(view, "tableDisableHead") {
rui.Set(view, "demoTableView1", rui.AllowSelection, newDemoTableAllowSelection([]int{0, 1}))
} else {
rui.Set(view, "demoTableView1", rui.AllowSelection, nil)
}
}
})
rui.Set(view, "tableCellGap", rui.DropDownEvent, func(list rui.DropDownList, number int) {
if number == 0 {
rui.Set(view, "demoTableView1", rui.Gap, rui.Px(0))
@ -202,5 +276,9 @@ func createTableViewDemo(session rui.Session) rui.View {
}
})
rui.Set(view, "tableVerticalAlign", rui.DropDownEvent, func(list rui.DropDownList, number int) {
rui.Set(view, "demoTableView1", rui.TableVerticalAlign, number)
})
return view
}

View File

@ -17,7 +17,7 @@ GridLayout {
content = [
GridLayout {
id = touchEventsTest, cell-horizontal-align = center, cell-vertical-align = center,
height = 150%,
height = 100%,
border = _{ style = solid, width = 1px, color = gray},
content = [
TextView {

View File

@ -39,6 +39,10 @@ func (list *dropDownListData) Init(session Session) {
list.dropDownListener = []func(DropDownList, int){}
}
func (list *dropDownListData) Focusable() bool {
return true
}
func (list *dropDownListData) Remove(tag string) {
list.remove(strings.ToLower(tag))
}

View File

@ -63,6 +63,10 @@ func (edit *editViewData) Init(session Session) {
edit.tag = "EditView"
}
func (edit *editViewData) Focusable() bool {
return true
}
func (edit *editViewData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag {

View File

@ -89,6 +89,10 @@ func (picker *filePickerData) Init(session Session) {
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){}
}
func (picker *filePickerData) Focusable() bool {
return true
}
func (picker *filePickerData) Files() []FileInfo {
return picker.files
}

View File

@ -140,11 +140,9 @@ func getFocusListeners(view View, subviewID string, tag string) []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)" `)
}
if view.Focusable() {
for _, js := range focusEvents {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
}
}
}

View File

@ -929,6 +929,8 @@ func (listView *listViewData) htmlProperties(self View, buffer *strings.Builder)
buffer.WriteString(strconv.Itoa(current))
buffer.WriteRune('"')
}
listView.viewData.htmlProperties(self, buffer)
}
/*
@ -1084,14 +1086,12 @@ func (listView *listViewData) htmlSubviews(self View, buffer *strings.Builder) {
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)
}
listView.propertyChangedEvent(Current)
if number, ok := dataIntProperty(data, `number`); ok {
listView.properties[Current] = number
for _, listener := range listView.selectedListeners {
listener(listView, number)
}
listView.propertyChangedEvent(Current)
}
case "itemUnselected":
@ -1163,9 +1163,14 @@ func (listView *listViewData) onItemClick() {
}
}
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}
func (listView *listViewData) onItemResize(self View, index string, x, y, width, height float64) {
n, err := strconv.Atoi(index)
if err != nil {
ErrorLog(err.Error())
} else if n >= 0 && n < len(listView.itemFrame) {
listView.itemFrame[n] = Frame{Left: x, Top: y, Width: width, Height: height}
} else {
ErrorLogF(`Invalid ListView item index: %d`, n)
}
}

View File

@ -168,6 +168,10 @@ func (player *mediaPlayerData) Init(session Session) {
player.tag = "MediaPlayer"
}
func (player *mediaPlayerData) Focusable() bool {
return true
}
func (player *mediaPlayerData) Remove(tag string) {
player.remove(strings.ToLower(tag))
}

View File

@ -50,6 +50,10 @@ func (picker *numberPickerData) Init(session Session) {
picker.numberChangedListeners = []func(NumberPicker, float64){}
}
func (picker *numberPickerData) Focusable() bool {
return true
}
func (picker *numberPickerData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag {

View File

@ -412,6 +412,11 @@ var enumProperties = map[string]struct {
"",
[]string{"none", "metadata", "auto"},
},
SelectionMode: {
[]string{"none", "cell", "row"},
"",
[]string{"none", "cell", "row"},
},
}
func notCompatibleType(tag string, value interface{}) {

View File

@ -18,7 +18,7 @@ func (view *viewData) onResize(self View, x, y, width, height float64) {
}
}
func (view *viewData) onItemResize(self View, index int, x, y, width, height float64) {
func (view *viewData) onItemResize(self View, index string, x, y, width, height float64) {
}
func (view *viewData) setFrameListener(tag string, value interface{}) bool {

View File

@ -33,6 +33,10 @@ type Session interface {
Color(tag string) (Color, bool)
// SetCustomTheme set the custom theme
SetCustomTheme(name string) bool
// UserAgent returns the "user-agent" text of the client browser
UserAgent() string
// RemoteAddr returns the client address.
RemoteAddr() string
// Language returns the current session language
Language() string
// SetLanguage set the current session language
@ -106,6 +110,7 @@ type sessionData struct {
touchScreen bool
textDirection int
pixelRatio float64
userAgent string
language string
languages []string
checkboxOff string
@ -148,12 +153,20 @@ func newSession(app Application, id int, customTheme string, params DataObject)
session.touchScreen = (value == "1" || value == "true")
}
if value, ok := params.PropertyValue("user-agent"); ok {
session.userAgent = value
}
if value, ok := params.PropertyValue("direction"); ok {
if value == "rtl" {
session.textDirection = RightToLeftDirection
}
}
if value, ok := params.PropertyValue("language"); ok {
session.language = value
}
if value, ok := params.PropertyValue("languages"); ok {
session.languages = strings.Split(value, ",")
}
@ -378,14 +391,10 @@ func (session *sessionData) handleResize(data DataObject) {
}
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])
}
if view := session.viewByHTMLID(viewID[:n]); view != nil {
view.onItemResize(view, viewID[n+1:], getFloat("x"), getFloat("y"), getFloat("width"), getFloat("height"))
} else {
ErrorLogF(`Invalid view id == %s not found`, viewID)
ErrorLogF(`View with id == %s not found`, viewID[:n])
}
} else if view := session.viewByHTMLID(viewID); view != nil {
view.onResize(view, getFloat("x"), getFloat("y"), getFloat("width"), getFloat("height"))
@ -423,3 +432,7 @@ func (session *sessionData) SetTitle(title string) {
func (session *sessionData) SetTitleColor(color Color) {
session.runScript(`setTitleColor("` + color.cssString() + `");`)
}
func (session *sessionData) RemoteAddr() string {
return session.brige.remoteAddr()
}

View File

@ -328,6 +328,10 @@ func (session *sessionData) radiobuttonOnImage() string {
return session.radiobuttonOn
}
func (session *sessionData) UserAgent() string {
return session.userAgent
}
func (session *sessionData) Language() string {
if session.language != "" {
return session.language

View File

@ -1,23 +1,66 @@
package rui
// TableAdapter describes the TableView content
type TableAdapter interface {
// RowCount returns number of rows in the table
RowCount() int
// ColumnCount returns number of columns in the table
ColumnCount() int
// Cell returns the contents of a table cell. The function can return elements of the following types:
// * string
// * rune
// * float32, float64
// * integer values: int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64
// * bool
// * rui.Color
// * rui.View
// * fmt.Stringer
// * rui.VerticalTableJoin, rui.HorizontalTableJoin
Cell(row, column int) interface{}
}
// TableColumnStyle describes the style of TableView columns.
// To set column styles, you must either implement the TableColumnStyle interface in the table adapter
// or assign its separate implementation to the "column-style" property.
type TableColumnStyle interface {
ColumnStyle(column int) Params
}
// TableRowStyle describes the style of TableView rows.
// To set row styles, you must either implement the TableRowStyle interface in the table adapter
// or assign its separate implementation to the "row-style" property.
type TableRowStyle interface {
RowStyle(row int) Params
}
// TableCellStyle describes the style of TableView cells.
// To set row cells, you must either implement the TableCellStyle interface in the table adapter
// or assign its separate implementation to the "cell-style" property.
type TableCellStyle interface {
CellStyle(row, column int) Params
}
// TableAllowCellSelection determines whether TableView cell selection is allowed.
// It is only used if the "selection-mode" property is set to CellSelection (1).
// To set cell selection allowing, you must either implement the TableAllowCellSelection interface
// in the table adapter or assign its separate implementation to the "allow-selection" property.
type TableAllowCellSelection interface {
AllowCellSelection(row, column int) bool
}
// TableAllowRowSelection determines whether TableView row selection is allowed.
// It is only used if the "selection-mode" property is set to RowSelection (2).
// To set row selection allowing, you must either implement the TableAllowRowSelection interface
// in the table adapter or assign its separate implementation to the "allow-selection" property.
type TableAllowRowSelection interface {
AllowRowSelection(row int) bool
}
// SimpleTableAdapter is implementation of TableAdapter where the content
// defines as [][]interface{}.
// When you assign [][]interface{} value to the "content" property, it is converted to SimpleTableAdapter
type SimpleTableAdapter interface {
TableAdapter
TableCellStyle
@ -28,6 +71,9 @@ type simpleTableAdapter struct {
columnCount int
}
// TextTableAdapter is implementation of TableAdapter where the content
// defines as [][]string.
// When you assign [][]string value to the "content" property, it is converted to TextTableAdapter
type TextTableAdapter interface {
TableAdapter
}
@ -37,12 +83,17 @@ type textTableAdapter struct {
columnCount int
}
// NewTextTableAdapter is an auxiliary structure. It used as cell content and
// specifies that the cell should be merged with the one above it
type VerticalTableJoin struct {
}
// HorizontalTableJoin is an auxiliary structure. It used as cell content and
// specifies that the cell should be merged with the one before it
type HorizontalTableJoin struct {
}
// NewSimpleTableAdapter creates the new SimpleTableAdapter
func NewSimpleTableAdapter(content [][]interface{}) SimpleTableAdapter {
if content == nil {
return nil
@ -137,6 +188,7 @@ func (adapter *simpleTableAdapter) CellStyle(row, column int) Params {
return params
}
// NewTextTableAdapter creates the new TextTableAdapter
func NewTextTableAdapter(content [][]string) TextTableAdapter {
if content == nil {
return nil
@ -329,3 +381,14 @@ func (table *tableViewData) getColumnStyle() TableColumnStyle {
}
return nil
}
func (table *tableViewData) getCellStyle() TableCellStyle {
for _, tag := range []string{CellStyle, Content} {
if value := table.getRaw(tag); value != nil {
if style, ok := value.(TableCellStyle); ok {
return style
}
}
}
return nil
}

File diff suppressed because it is too large Load Diff

232
tableViewUtils.go Normal file
View File

@ -0,0 +1,232 @@
package rui
import "strings"
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)
if value, ok := enumProperty(cell, TableVerticalAlign, session, 0); ok {
builder.add("vertical-align", enumProperties[TableVerticalAlign].values[value])
}
}
// GetTableContent returns a TableAdapter which defines the TableView content.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTableContent(view View, subviewID string) TableAdapter {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if tableView, ok := view.(TableView); ok {
return tableView.content()
}
}
return nil
}
// GetTableRowStyle returns a TableRowStyle which defines styles of TableView rows.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTableRowStyle(view View, subviewID string) TableRowStyle {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if tableView, ok := view.(TableView); ok {
return tableView.getRowStyle()
}
}
return nil
}
// GetTableColumnStyle returns a TableColumnStyle which defines styles of TableView columns.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTableColumnStyle(view View, subviewID string) TableColumnStyle {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if tableView, ok := view.(TableView); ok {
return tableView.getColumnStyle()
}
}
return nil
}
// GetTableCellStyle returns a TableCellStyle which defines styles of TableView cells.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTableCellStyle(view View, subviewID string) TableCellStyle {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if tableView, ok := view.(TableView); ok {
return tableView.getCellStyle()
}
}
return nil
}
// GetTableSelectionMode returns the mode of the TableView elements selection.
// Valid values are NoneSelection (0), CellSelection (1), and RowSelection (2).
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTableSelectionMode(view View, subviewID string) int {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if result, ok := enumStyledProperty(view, SelectionMode, NoneSelection); ok {
return result
}
}
return NoneSelection
}
// GetTableVerticalAlign returns a vertical align in a TavleView cell. Returns one of next values:
// TopAlign (0), BottomAlign (1), CenterAlign (2), and BaselineAlign (3)
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTableVerticalAlign(view View, subviewID string) int {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if result, ok := enumStyledProperty(view, TableVerticalAlign, TopAlign); ok {
return result
}
}
return TopAlign
}
// GetTableHeadHeight returns the number of rows in the table header.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTableHeadHeight(view View, subviewID string) int {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
headHeight, _ := intStyledProperty(view, HeadHeight, 0)
return headHeight
}
return 0
}
// GetTableFootHeight returns the number of rows in the table footer.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTableFootHeight(view View, subviewID string) int {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
headHeight, _ := intStyledProperty(view, FootHeight, 0)
return headHeight
}
return 0
}
// GetTableCurrent returns the row and column index of the TableView selected cell/row.
// If there is no selected cell/row or the selection mode is NoneSelection (0),
// then a value of the row and column index less than 0 is returned.
// If the selection mode is RowSelection (2) then the returned column index is less than 0.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTableCurrent(view View, subviewID string) CellIndex {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if selectionMode := GetTableSelectionMode(view, ""); selectionMode != NoneSelection {
if tableView, ok := view.(TableView); ok {
return tableView.getCurrent()
}
}
}
return CellIndex{Row: -1, Column: -1}
}
// GetTableCellClickedListeners returns listeners of event which occurs when the user clicks on a table cell.
// 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 GetTableCellClickedListeners(view View, subviewID string) []func(TableView, int, int) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.Get(TableCellClickedEvent); value != nil {
if result, ok := value.([]func(TableView, int, int)); ok {
return result
}
}
}
return []func(TableView, int, int){}
}
// GetTableCellSelectedListeners returns listeners of event which occurs when a table cell becomes selected.
// If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTableCellSelectedListeners(view View, subviewID string) []func(TableView, int, int) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.Get(TableCellSelectedEvent); value != nil {
if result, ok := value.([]func(TableView, int, int)); ok {
return result
}
}
}
return []func(TableView, int, int){}
}
// GetTableRowClickedListeners returns listeners of event which occurs when the user clicks on a table row.
// If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTableRowClickedListeners(view View, subviewID string) []func(TableView, int) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.Get(TableRowClickedEvent); value != nil {
if result, ok := value.([]func(TableView, int)); ok {
return result
}
}
}
return []func(TableView, int){}
}
// GetTableRowSelectedListeners returns listeners of event which occurs when a table row becomes selected.
// If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is "" then a value from the first argument (view) is returned.
func GetTableRowSelectedListeners(view View, subviewID string) []func(TableView, int) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
if view != nil {
if value := view.Get(TableRowSelectedEvent); value != nil {
if result, ok := value.([]func(TableView, int)); ok {
return result
}
}
}
return []func(TableView, int){}
}

View File

@ -44,6 +44,10 @@ func (picker *timePickerData) Init(session Session) {
picker.timeChangedListeners = []func(TimePicker, time.Time){}
}
func (picker *timePickerData) Focusable() bool {
return true
}
func (picker *timePickerData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag {

18
view.go
View File

@ -58,6 +58,8 @@ type View interface {
SetAnimated(tag string, value interface{}, animation Animation) bool
// SetChangeListener set the function to track the change of the View property
SetChangeListener(tag string, listener func(View, string))
// HasFocus returns 'true' if the view has focus
HasFocus() bool
handleCommand(self View, command string, data DataObject) bool
htmlClass(disabled bool) string
@ -73,7 +75,7 @@ type View interface {
getTransitions() Params
onResize(self View, x, y, width, height float64)
onItemResize(self View, index int, x, y, width, height float64)
onItemResize(self View, index string, x, y, width, height float64)
setNoResizeEvent()
isNoResizeEvent() bool
setScroll(x, y, width, height float64)
@ -95,6 +97,7 @@ type viewData struct {
scroll Frame
noResizeEvent bool
created bool
hasFocus bool
//animation map[string]AnimationEndListener
}
@ -737,7 +740,14 @@ func (view *viewData) handleCommand(self View, command string, data DataObject)
case TouchStart, TouchEnd, TouchMove, TouchCancel:
handleTouchEvents(self, command, data)
case FocusEvent, LostFocusEvent:
case FocusEvent:
view.hasFocus = true
for _, listener := range getFocusListeners(view, "", command) {
listener(self)
}
case LostFocusEvent:
view.hasFocus = false
for _, listener := range getFocusListeners(view, "", command) {
listener(self)
}
@ -838,3 +848,7 @@ func (view *viewData) SetChangeListener(tag string, listener func(View, string))
view.changeListener[tag] = listener
}
}
func (view *viewData) HasFocus() bool {
return view.hasFocus
}

View File

@ -14,6 +14,7 @@ type WebBrige interface {
RunGetterScript(script string) DataObject
AnswerReceived(answer DataObject)
Close()
remoteAddr() string
}
type wsBrige struct {
@ -122,3 +123,7 @@ func (brige *wsBrige) AnswerReceived(answer DataObject) {
ErrorLog("answerID not found")
}
}
func (brige *wsBrige) remoteAddr() string {
return brige.conn.RemoteAddr().String()
}