diff --git a/CHANGELOG.md b/CHANGELOG.md index a24be61..b2078be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# v0.8.0 + +* Added "loaded-event" and "error-event" events to ImageView +* Added NaturalSize and CurrentSource methods to ImageView +* Added "user-select" property and IsUserSelect function +* Renamed "LightGoldenrodYellow" color constant to "LightGoldenRodYellow" + # v0.7.0 * Added "resize", "grid-auto-flow", "caret-color", and "backdrop-filter" properties diff --git a/README-ru.md b/README-ru.md index c8a2a3d..2968b42 100644 --- a/README-ru.md +++ b/README-ru.md @@ -1630,7 +1630,7 @@ radius необходимо передать nil #### Свойство "vertical-text-orientation" -Свойство "vertical-text-orientation" (константа VerticalTextOrientation) - свойство типа int используется, только +Свойство "vertical-text-orientation" (константа VerticalTextOrientation) типа int используется, только если "writing-mode" установлено в VerticalRightToLeft (2) или VerticalLeftToRight (3) и определяет положение символов вертикальной строки. Возможны следующие значения: @@ -1643,6 +1643,22 @@ radius необходимо передать nil func GetVerticalTextOrientation(view View, subviewID string) int +#### Свойство "user-select" + +Свойство "user-select" (константа UserSelect) типа bool определяет может ли пользователь выделять текст. +Соответственно если свойство установлено в true, то пользователь может выделять текст. Если в false, то не может. + +Значение по умолчанию зависит, от значения свойства "semantics". Если "semantics" установлено в "p", "h1"..."h6", +"blockquote" или "code", то значение по умолчанию равно "true", в остальных случаях значение по умолчанию равно "false". +Исключением является TableView. Для него значение по умолчанию равно "true". + +Как и все свойства текста свойство "user-select" наследуемое, т.е. если вы установите его для контейнера, +то оно также примениться ко всем дочерним элементам + +Получить значение данного свойства можно с помощью функции + + func IsUserSelect(view View, subviewID string) bool + ### Свойства трансформации Данные свойства используются для трансформации (наклон, масштабирование и т.п.) содержимого View. @@ -2693,6 +2709,84 @@ TextView наследует от View все свойства параметро | 0 | TextOverflowClip | "clip" | Текст обрезается по границе (по умолчанию) | | 1 | TextOverflowEllipsis | "ellipsis" | В конце видимой части текста выводится '…' | +## ImageView + +Элемент ImageView расширяющий интерфейс View предназначен для вывода изображений. + +Для создания ImageView используется функция: + + func NewImageView(session Session, params Params) ImageView + +Выводимое изображение задается string свойством "src" (константа Source). +В качестве значения данному свойству присваивается либо имя изображения в папке images ресурсов, +либо url изобрадения. + +ImageView позволяет выдодить разные изображения в зависимости от плотности экрана +(см. раздел "Изображения для экранов с разной плотностью пикселей"). +В связи с этим интерфейс ImageView имеет два дополнительных метода, которые позволяют узнать какое именно изображение отображается: + + CurrentSource() string + NaturalSize() (float64, float64) + +Метод CurrentSource возвращает url выводимого изображения. Пока изображение не загрузилось данный метод возвращает пустую строку. + +NaturalSize() возвращает исходную ширину и высоту выводимого изображения в экранных пикселях. +Т.е. если исходное изображение имеет размер 100x200, а плотность экрана равна 2, то метод NaturalSize вернет значение (50, 100). +Пока изображение не загрузилось данный метод возвращает значение (0, 0). + +Для отслеживания загрузки изображения используются два события: + +* "loaded-event" (константа LoadedEvent). Данное событие возникает сразу после загрузки изображения. + +* "error-event" (константа ErrorEvent). Данное событие возникает если при загрузке изображения возникла ошибка. + +Основной слушатель этих событий имеет следующий формат: + + func(ImageView) + +Свойство "alt-text" (константа AltText) типа string позволяет задать описание изображения. +Данный текст отображается если браузер не смог загрузить изображение. +Также данный текст используется в системе озвучивания для незрячих. + +Свойство "fit" (константа Fit) типа int определяет параметры масштабирования изображения. +Допустимые значения: + +| Значение | Константа | Имя | Значение | +|:--------:|--------------|--------------|-----------------------------------| +| 0 | NoneFit | "none" | Размер изображения не изменяется | +| 1 | ContainFit | "contain" | Изображение масштабируется так чтобы сохранить соотношение сторон и вписаться в размеры ImageView | +| 2 | CoverFit | "cover" | Изображение масштабируется так чтобы полность заполнить область ImageView. При этом сохраняется соотношение сторон изображения. Если после масштабтрования изображение выходит за границы по высоте или ширине, то оно обрезается | +| 3 | FillFit | "fill" | Изображение масштабируется так чтобы полность заполнить область ImageView. При этом соотношение сторон изображения может не сохраняться | +| 4 | ScaleDownFit | "scale-down" | Изображение масштабируется так как если бы были указаны NoneFit или ContainFit, в зависимости от того, что приведет к меньшему размеру изображения. Т.е. масштабирование может выполняться только в сторону уменьшения изображения | + +Свойство "image-horizontal-align" (константа ImageHorizontalAlign) типа int устанавливет +горизонтальное выравнивание изображения относительно границ ImageView. Допустимые значения: + +| Значение | Константа | Имя | Значение | +|:--------:|--------------|-----------|------------------------------| +| 0 | LeftAlign | "left" | Выравнивание по левому краю | +| 1 | RightAlign | "right" | Выравнивание по правому краю | +| 2 | CenterAlign | "center" | Выравнивание по центру | +| 3 | StretchAlign | "stretch" | Выравнивание по ширине | + +Свойство "image-vertical-align" (константа ImageVerticalAlign) типа int устанавливает вертикальное +выравнивание изображения относительно границ ImageView. Допустимые значения: + +| Значение | Константа | Имя | Значение | +|:--------:|--------------|-----------|-------------------------------| +| 0 | TopAlign | "top" | Выравнивание по верхнему краю | +| 1 | BottomAlign | "bottom" | Выравнивание по нижнему краю | +| 2 | CenterAlign | "center" | Выравнивание по центру | +| 3 | StretchAlign | "stretch" | Выравнивание по высоте | + +Для получения значений свойств ImageView могут использоваться следующие функции: + + func GetImageViewSource(view View, subviewID string) string + func GetImageViewAltText(view View, subviewID string) string + func GetImageViewFit(view View, subviewID string) int + func GetImageViewVerticalAlign(view View, subviewID string) int + func GetImageViewHorizontalAlign(view View, subviewID string) int + ## EditView Элемент EditView является редактором теста и расширяет интерфейс View. @@ -4211,6 +4305,172 @@ MediaPlayer имеет ряд методов для управления пар где view - корневой View, playerID - id of AudioPlayer or VideoPlayer +## Popup + +Popup это интерфейс позволяющий отобразить произвольный View в виде всплывающего окна. +Для создания интерфейса Popup используется функция + + NewPopup(view View, param Params) Popup + +где view - View содержимого всплывающего окна (не может быть nil); +params - параметры всплавающего окна (может быть nil). В качестве параметров всплавающего окна, могут +использоваться как любые свойства View, так и ряд дополнительных свойств (они будут описаны ниже) + +После создания Popup его необходимо отобразить. Для этого используется метод Show() интерфейса Popup. +Для упрощения кода можно использовать функцию ShowPopup, которая опредена как + + func ShowPopup(view View, param Params) Popup { + popup := NewPopup(view, param) + if popup != nil { + popup.Show() + } + return popup + } + +Для закрытия всплавающего окна используется метод Dismiss() интерфейса Popup. + +Помимо медодов Show() и Dismiss() интерфейс Popup имеет следующие методы: + +* Session() Session - возвращает текущую сессию; +* View() View - возвращает содержимое всплывающего окна. + +### Заголовок Popup + +Всплывающее окно может иметь заголовок. Для того чтобы добавить заголовок необходимо добавить текст заголовка. +Для этого используется свойство "title" (константа Title) которое может принимать два типа значений: + +* string +* View + +Для установления стиля заголовка используется свойство "title-style" (константа TitleStyle) типа string. +Стиль заголовка по умолчанию "ruiPopupTitle". Если вы хотите чтобы все ваши всплывающие окна имели одинаковый стиль, +для этого лучше не использовать свойство "title-style", а переопределить стиль "ruiPopupTitle". + +Заголовок также может иметь кнопку закрытия окна. Для ее добавления к заголовку используется свойство "close-button" типа bool. +Установка этого свойства в "true" добавляет к заголовку кнопку закрытия окна (значение по умолчанию равно "false"). + +### Закрытие Popup + +Как было сказано выше, для закрытия всплавающего окна используется метод Dismiss() интерфейса Popup. + +Если к заголовку окна добавлена кнопка закрытия, то нажатие на нее автоматически вызывает метод Dismiss(). +Переопределить поведение кнопки закрытия окна нельзя. +Если все же необходимо переопределить поведение этой кнопки, то это можно сделать создав кастомный заголовок и +создав в нем свою кнопку закрытия. + +Существует еще один способ автоматического вызова метода Dismiss(). Это свойство "outside-close" (константа OutsideClose) типа bool. +Если это свойство установлено в "true", то клик мышью вне пределов всплывающего окна автоматически вызывает метод Dismiss(). + +Для отслеживания закрытия всплывающего окна используются событие "dismiss-event" (константа DismissEvent). +Оно возникает после того как Popup исчезнет с экрана. +Основной слушатель этого событя имеет следующий формат: + + func(Popup) + +### Область кнопок + +Часто во всплывающее окно необходимо добавить кнопки, такие как "OK", "Cancel" и т.п. +С помощью свойства "buttons" (константа Buttons) вы можете добавлять кнопки, которые будут располагаться внизу окна. +Свойству "buttons" можно присваивать следующие типы данных: + +* PopupButton +* []PopupButton + +Где структура PopupButton объявлена как + + type PopupButton struct { + Title string + OnClick func(Popup) + } + +где Title - текст кнопки, OnClick - функция вызываемая при нажатии на кнопку + +По умолчанию кнопки выравниваются по правому краю окна. Однако это поведение можно переопределить. +Для этого используется свойство "buttons-align" (константа ButtonsAlign) типа int, которое может принимать следующие значения: + +| Значение | Константа | Имя | Выравнивание | +|:--------:|--------------|-----------|------------------------------| +| 0 | LeftAlign | "left" | Выравнивание по левому краю | +| 1 | RightAlign | "right" | Выравнивание по правому краю | +| 2 | CenterAlign | "center" | Выравнивание по центру | +| 3 | StretchAlign | "stretch" | Выравнивание по ширине | + +Расстояние между кнопками задается с помощью константы "ruiPopupButtonGap" типа SizeUnit. Вы можете переопределить ее в своей теме. + +### Выравнивание Popup + +По умолчанию всплывающее окно располагается по центру окна браузера. Изменить это поведение можно с помощью свойств +"vertical-align" (константа VerticalAlign) и "horizontal-align" (константа HorizontalAlign) типа int. + +Свойство "vertical-align" может принимать следующие значения: + +| Значение | Константа | Имя | Значение | +|:--------:|--------------|-----------|-------------------------------| +| 0 | TopAlign | "top" | Выравнивание по верхнему краю | +| 1 | BottomAlign | "bottom" | Выравнивание по нижнему краю | +| 2 | CenterAlign | "center" | Выравнивание по центру | +| 3 | StretchAlign | "stretch" | Выравнивание по высоте | + +Свойство "horizontal-align" может принимать следующие значения: + +| Значение | Константа | Имя | Значение | +|:--------:|--------------|-----------|------------------------------| +| 0 | LeftAlign | "left" | Выравнивание по левому краю | +| 1 | RightAlign | "right" | Выравнивание по правому краю | +| 2 | CenterAlign | "center" | Выравнивание по центру | +| 3 | StretchAlign | "stretch" | Выравнивание по ширине | + +Для сдвига окна может использоваться свойство "margin". + +Например, организовать выподающее окно привязанное к кнопке можно так + + rui.ShowPopup(myPopupView, rui.Params{ + rui.HorizontalAlign: rui.LeftAlign, + rui.VerticalAlign: rui.TopAlign, + rui.MarginLeft: rui.Px(myButton.Frame().Left), + rui.MarginTop: rui.Px(myButton.Frame().Bottom()), + }) + +### Стандартные Popup + +В библеотеке rui уже реализованы некотороые стандартные всплывающие окна. +Для их отображения используются следующие функции + + func ShowMessage(title, text string, session Session) + +Данная функция выводит на экран сообщение с заголовком заданным в аргументе title и текстом сообщения заданном в аргументе text. + + func ShowQuestion(title, text string, session Session, onYes func(), onNo func()) + +Данная функция выводит на экран сообщение с заданным заголовком и текстом и двумя кнопками "Yes" и "No". +При нажатии кнопки "Yes" сообщение закрывается и вызывается функция onYes (если она не nil). +При нажатии кнопки "No" сообщение закрывается и вызывается функция onNo (если она не nil). + + func ShowCancellableQuestion(title, text string, session Session, onYes func(), onNo func(), onCancel func()) + +Данная функция выводит на экран сообщение с заданным заголовком и текстом и тремя кнопками "Yes", "No" и "Cancel". +При нажатии кнопки "Yes", "No" или "Cancel" сообщение закрывается и вызывается, соотвественно, функция onYes, +onNo или onCancel (если она не nil). + + func ShowMenu(session Session, params Params) Popup + +Данная функция выводит на экран меню. Пункты меню задаются с помощью свойства Items. +Свойство идентично Items идентично одноименному свойству ListView. +С помощью свойства "popup-menu-result" (константа PopupMenuResult) задается функция вызываемая при выборе пункта меню. +Ее формат + + func(int) + +Пример меню + + rui.ShowMenu(session, rui.Params{ + rui.OutsideClose: true, + rui.Items: []string{"Item 1", "Item 2", "Item 3"}, + rui.PopupMenuResult: func(index int) { + // ... + }, + }) + ## Анимация Библиотека поддерживает два вида анимации: diff --git a/README.md b/README.md index caf00a6..dc0d816 100644 --- a/README.md +++ b/README.md @@ -1622,6 +1622,22 @@ You can get the value of this property using the function func GetVerticalTextOrientation(view View, subviewID string) int +#### "user-select" property + +The "user-select" property (UserSelect constant) of type bool determines whether the user can select text. +Accordingly, if the property is set to true, then the user can select text. If it's false, then it can't. + +The default value depends on the value of the "semantics" property. If "semantics" is set to "p", "h1"..."h6", +"blockquote" or "code", then the default value is "true", otherwise the default value is "false". +The exception is TableView. Its default value is "true". + +Like all text properties, the "user-select" property is inherited, i.e. if you set it for a container, +it will also apply to all child elements + +You can get the value of this property using the function + + func IsUserSelect(view View, subviewID string) bool + ### Transformation properties These properties are used to transform (skew, scale, etc.) the content of the View. @@ -2667,6 +2683,81 @@ This property of type int can take the following values | 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 | +## ImageView + +The ImageView element extending the View interface is designed to display images. + +To create an ImageView function is used: + + func NewImageView(session Session, params Params) ImageView + +The displayed image is specified by the string property "src" (Source constant). +As a value, this property is assigned either the name of the image in the "images" folder of the resources, or the url of the image. + +ImageView allows you to display different images depending on screen density +(See section "Images for screens with different pixel densities"). +In this regard, the ImageView interface has two additional methods that allow you to find out which image is displayed: + + CurrentSource() string + NaturalSize() (float64, float64) + +CurrentSource returns the url of the rendered image. Until the image is loaded, this method returns an empty string. + +NaturalSize() returns the original width and height of the rendered image, in screen pixels. +Those if the original image is 100x200 and the screen density is 2, then the NaturalSize method will return (50, 100). +Until the image is loaded, this method returns the value (0, 0). + +Two events are used to track the loading of an image: + +* "loaded-event" (LoadedEvent constant). This event fires right after the image is loaded. + +* "error-event" (ErrorEvent constant). This event occurs when an error occurs while loading an image. + +The main listener for these events has the following format: + + func(ImageView) + +The "alt-text" property (AltText constant) of the string type allows you to set a description of the image. +This text is displayed if the browser was unable to load the image. +Also, this text is used in the sound system for the blind. + +The "fit" property (Fit constant) of type int defines the image scaling parameters. +Valid values: + +| Value | Constant | Name | Resizing | +|:-----:|--------------|--------------|------------------------------| +| 0 | NoneFit | "none" | The image is not resized | +| 1 | ContainFit | "contain" | The image 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 | +| 2 | CoverFit | "cover" | The image 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 | +| 3 | FillFit | "fill" | The image 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 | +| 4 | ScaleDownFit | "scale-down" | The image is sized as if NoneFit or ContainFit were specified, whichever would result in a smaller concrete object size | + +The "image-vertical-align" int property (ImageVerticalAlign constant) sets the vertical alignment of the image +relative to the bounds of the ImageView. Valid values: + +| Value | Constant | Name | Alignment | +|:-----:|--------------|-----------|------------------| +| 0 | TopAlign | "top" | Top alignment | +| 1 | BottomAlign | "bottom" | Bottom alignment | +| 2 | CenterAlign | "center" | Center alignment | + +The "image-horizontal-align" int property (ImageHorizontalAlign constant) sets the horizontal alignment of the image +relative to the bounds of the ImageView. Valid values: + +| Value | Constant | Name | Alignment | +|:-----:|--------------|-----------|------------------| +| 0 | LeftAlign | "left" | Left alignment | +| 1 | RightAlign | "right" | Right alignment | +| 2 | CenterAlign | "center" | Center alignment | + +The following functions can be used to retrieve ImageView property values: + + func GetImageViewSource(view View, subviewID string) string + func GetImageViewAltText(view View, subviewID string) string + func GetImageViewFit(view View, subviewID string) int + func GetImageViewVerticalAlign(view View, subviewID string) int + func GetImageViewHorizontalAlign(view View, subviewID string) int + ## EditView The EditView element is a test editor and extends the View interface. @@ -4177,6 +4268,171 @@ For quick access to these methods, there are global functions: where view is the root View, playerID is the id of AudioPlayer or VideoPlayer +## Popup + +Popup is an interface that allows you to display an arbitrary View as a popup window. +To create the Popup interface, use the function + + NewPopup(view View, param Params) Popup + +where view - View of popup content (cannot be nil); +params - parameters of the popup window (may be nil). As parameters of the pop-up window, either any View properties +or a number of additional properties (they will be described below) can be used. + +Once a Popup has been created, it needs to be displayed. To do this, use the Show() method of the Popup interface. +To simplify the code, you can use the ShowPopup function, which is defined as + + func ShowPopup(view View, param Params) Popup { + popup := NewPopup(view, param) + if popup != nil { + popup.Show() + } + return popup + } + +To close a popup window, use the Dismiss() method of the Popup interface. + +In addition to the Show() and Dismiss() methods, the Popup interface has the following methods: + +* Session() Session - returns the current session; +* View() View - returns the contents of the popup window. + +### Popup header + +The popup window can have a title. In order to add a title, you need to add title text. +For this, the "title" property (Title constant) is used, which can take two types of values: + +* string +* view + +The "title-style" string property (TitleStyle constant) is used to set the title style. +The default title style is "ruiPopupTitle". If you want all your popups to have the same style, it's better +not to use the "title-style" property, but to override the "ruiPopupTitle" style. + +The header can also have a window close button. To add it to the header, use the "close-button" bool property. +Setting this property to "true" adds a window close button to the title bar (the default value is "false"). + +### Close Popup + +As it was said above, the Dismiss() method of the Popup interface is used to close the popup window. + +If a close button is added to the window title, clicking on it automatically calls the Dismiss() method. +You cannot override the behavior of the window's close button. +If you still need to redefine the behavior of this button, then this can be done by creating a custom header and creating your own close button in it. + +There is another way to automatically call the Dismiss() method. This is the "outside-close" bool property (OutsideClose constant). +If this property is set to "true", then clicking outside the popup window automatically calls the Dismiss() method. + +The "dismiss-event" event (DismissEvent constant) is used to track the closing of the popup. +It occurs after the Popup disappears from the screen. +The main listener for this event has the following format: + + func(Popup) + +### Button area + +It is often necessary to add buttons such as "OK", "Cancel", etc. to the popup window. +Using the "buttons" property (Buttons constant) you can add buttons that will be placed at the bottom of the window. +The "buttons" property can be assigned the following data types: + +* PopupButton +* []PopupButton + +Where the PopupButton structure is declared as + + type PopupButton struct { + Title string + OnClick func(Popup) + } + +where Title is the text of the button, OnClick is the function called when the button is clicked. + +By default, buttons are aligned to the right edge of the popup window. However, this behavior can be overridden. +For this, the "buttons-align" int property (ButtonsAlign constant) is used, which can take the following 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 | + +The distance between the buttons is set using the "ruiPopupButtonGap" constant of the SizeUnit type. You can override it in your theme. + +### Popup alignment + +By default, the popup is positioned in the center of the browser window. You can change this behavior using +the "vertical-align" (VerticalAlign constant) and "horizontal-align" (HorizontalAlign constant) int properties. + +The "vertical-align" property can take the following 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 | + +The "horizontal-align" property can take the following 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 | + +The "margin" property can be used to move the window. + +For example, you can organize a popup window attached to a button like this + + rui.ShowPopup(myPopupView, rui.Params{ + rui.HorizontalAlign: rui.LeftAlign, + rui.VerticalAlign: rui.TopAlign, + rui.MarginLeft: rui.Px(myButton.Frame().Left), + rui.MarginTop: rui.Px(myButton.Frame().Bottom()), + }) + +### Standard Popup + +The rui library already implements some standard popups. +The following functions are used to display them. + + func ShowMessage(title, text string, session Session) + +This function displays a message with the title given in the "title" argument and the message text given in the "text" argument. + + func ShowQuestion(title, text string, session Session, onYes func(), onNo func()) + +This function displays a message with the given title and text and two buttons "Yes" and "No". +When the "Yes" button is clicked, the message is closed and the onYes function is called (if it is not nil). +When the "No" button is pressed, the message is closed and the onNo function is called (if it is not nil). + + func ShowCancellableQuestion(title, text string, session Session, onYes func(), onNo func(), onCancel func()) + +This function displays a message with the given title and text and three buttons "Yes", "No" and "Cancel". +When the "Yes", "No" or "Cancel" button is pressed, the message is closed and the onYes, onNo or onCancel function +(if it is not nil) is called, respectively. + + func ShowMenu(session Session, params Params) Popup + +This function displays the menu. Menu items are set using the Items property. +The property is identical to Items and is identical to the ListView property of the same name. +The "popup-menu-result" property (PopupMenuResult constant) sets the function to be called when a menu item is selected. +Its format: + + func(int) + +Menu example: + + rui.ShowMenu(session, rui.Params{ + rui.OutsideClose: true, + rui.Items: []string{"Item 1", "Item 2", "Item 3"}, + rui.PopupMenuResult: func(index int) { + // ... + }, + }) + ## Animation The library supports two types of animation: diff --git a/app_scripts.js b/app_scripts.js index 09241dc..bec958d 100644 --- a/app_scripts.js +++ b/app_scripts.js @@ -1787,6 +1787,15 @@ function tableRowClickEvent(element, event) { sendMessage("rowClick{session=" + sessionID + ",id=" + tableID + ",row=" + row + "}"); } -function stopEventPropagation(element, event) { - event.stopPropagation() -} \ No newline at end of file +function imageLoaded(element, event) { + var message = "imageViewLoaded{session=" + sessionID + ",id=" + element.id + + ",natural-width=" + element.naturalWidth + + ",natural-height=" + element.naturalHeight + + ",current-src=\"" + element.currentSrc + "\"}"; + sendMessage(message); +} + +function imageError(element, event) { + var message = "imageViewError{session=" + sessionID + ",id=" + element.id + "}"; + sendMessage(message); +} diff --git a/app_styles.css b/app_styles.css index 57b8ab0..b85cfb8 100644 --- a/app_styles.css +++ b/app_styles.css @@ -8,27 +8,31 @@ text-overflow: ellipsis; } -div { +body { -webkit-touch-callout: none; -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; user-select: none; + margin: 0 auto; + width: 100%; + height: 100vh; } -p, h1, h2, h3, h4, h5, h6, blockquote, code { - -webkit-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; +div { + cursor: default; } +p, h1, h2, h3, h4, h5, h6, blockquote, code, table { + cursor: text; + -webkit-user-select: auto; + user-select: auto; +} /* div:focus { outline: none; } */ + input { margin: 2px; padding: 1px; @@ -55,11 +59,6 @@ ul:focus { outline: none; } -body { - margin: 0 auto; - width: 100%; - height: 100vh; -} .ruiRoot { position: absolute; diff --git a/colorConstants.go b/colorConstants.go index 5d194e9..4505a1f 100644 --- a/colorConstants.go +++ b/colorConstants.go @@ -166,7 +166,7 @@ const ( // LightCyan color constant LightCyan Color = 0xffe0ffff // LightGoldenrodYellow color constant - LightGoldenrodYellow Color = 0xfffafad2 + LightGoldenRodYellow Color = 0xfffafad2 // LightGray color constant LightGray Color = 0xffd3d3d3 // LightGreen color constant diff --git a/demo/imageViewDemo.go b/demo/imageViewDemo.go index 4771b13..bc97b9e 100644 --- a/demo/imageViewDemo.go +++ b/demo/imageViewDemo.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "github.com/anoshenko/rui" ) @@ -8,9 +10,17 @@ const imageViewDemoText = ` GridLayout { style = demoPage, content = [ - ImageView { - id = imageView1, width = 100%, height = 100%, src = "cat.jpg", - border = _{ style = solid, width = 1px, color = #FF008800 } + GridLayout { + cell-height = "auto, 1fr", + content = [ + TextView { + id = imageViewInfo, + }, + ImageView { + id = imageView1, row = 1, width = 100%, height = 100%, src = "cat.jpg", + border = _{ style = solid, width = 1px, color = #FF008800 } + }, + ], }, ListLayout { style = optionsPanel, @@ -21,7 +31,7 @@ GridLayout { TextView { row = 0, text = "Image" }, DropDownList { row = 0, column = 1, id = imageViewImage, current = 0, items = ["cat.jpg", "winds.png", "gifsInEmail.gif", "mountain.svg"]}, TextView { row = 1, text = "Fit" }, - DropDownList { row = 1, column = 1, id = imageViewFit, current = 0, items = ["none", "fill", "contain", "cover", "scale-down"]}, + DropDownList { row = 1, column = 1, id = imageViewFit, current = 0, items = ["none", "contain", "cover", "fill", "scale-down"]}, TextView { row = 2, text = "Horizontal align" }, DropDownList { row = 2, column = 1, id = imageViewHAlign, current = 2, items = ["left", "right", "center"]}, TextView { row = 3, text = "Vertical align" }, @@ -40,6 +50,15 @@ func createImageViewDemo(session rui.Session) rui.View { return nil } + rui.Set(view, "imageView1", rui.LoadedEvent, func(imageView rui.ImageView) { + w, h := imageView.NaturalSize() + rui.Set(view, "imageViewInfo", rui.Text, fmt.Sprintf("Natural size: (%g, %g). Current URL: %s", w, h, imageView.CurrentSource())) + }) + + rui.Set(view, "imageView1", rui.ErrorEvent, func(imageView rui.ImageView) { + rui.Set(view, "imageViewInfo", rui.Text, "Image loading error") + }) + rui.Set(view, "imageViewImage", rui.DropDownEvent, func(list rui.DropDownList, number int) { images := []string{"cat.jpg", "winds.png", "gifsInEmail.gif", "mountain.svg"} if number < len(images) { diff --git a/imageView.go b/imageView.go index e078c98..096bc3d 100644 --- a/imageView.go +++ b/imageView.go @@ -6,6 +6,13 @@ import ( ) const ( + // LoadedEvent is the constant for the "loaded-event" property tag. + // The "loaded-event" event occurs event occurs when the image has been loaded. + LoadedEvent = "loaded-event" + // ErrorEvent is the constant for the "error-event" property tag. + // The "error-event" event occurs event occurs when the image loading failed. + ErrorEvent = "error-event" + // 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 @@ -29,10 +36,19 @@ const ( // ImageView - image View type ImageView interface { View + // NaturalSize returns the intrinsic, density-corrected size (width, height) of the image in pixels. + // If the image hasn't been loaded yet or an load error has occurred, then (0, 0) is returned. + NaturalSize() (float64, float64) + // CurrentSource() return the full URL of the image currently visible in the ImageView. + // If the image hasn't been loaded yet or an load error has occurred, then "" is returned. + CurrentSource() string } type imageViewData struct { viewData + naturalWidth float64 + naturalHeight float64 + currentSrc string } // NewImageView create new ImageView object and return it @@ -102,6 +118,77 @@ func (imageView *imageViewData) Set(tag string, value interface{}) bool { return imageView.set(imageView.normalizeTag(tag), value) } +func valueToImageListeners(value interface{}) ([]func(ImageView), bool) { + if value == nil { + return nil, true + } + + switch value := value.(type) { + case func(ImageView): + return []func(ImageView){value}, true + + case func(): + fn := func(ImageView) { + value() + } + return []func(ImageView){fn}, true + + case []func(ImageView): + 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(ImageView), count) + for i, v := range value { + if v == nil { + return nil, false + } + listeners[i] = func(ImageView) { + v() + } + } + return listeners, true + + case []interface{}: + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(ImageView), count) + for i, v := range value { + if v == nil { + return nil, false + } + switch v := v.(type) { + case func(ImageView): + listeners[i] = v + + case func(): + listeners[i] = func(ImageView) { + v() + } + + default: + return nil, false + } + } + return listeners, true + } + + return nil, false +} + func (imageView *imageViewData) set(tag string, value interface{}) bool { if value == nil { imageView.remove(tag) @@ -140,6 +227,12 @@ func (imageView *imageViewData) set(tag string, value interface{}) bool { } notCompatibleType(tag, value) + case LoadedEvent, ErrorEvent: + if listeners, ok := valueToImageListeners(value); ok { + imageView.properties[tag] = listeners + return true + } + default: if imageView.viewData.set(tag, value) { if imageView.created { @@ -159,6 +252,15 @@ func (imageView *imageViewData) Get(tag string) interface{} { return imageView.viewData.get(imageView.normalizeTag(tag)) } +func (imageView *imageViewData) imageListeners(tag string) []func(ImageView) { + if value := imageView.getRaw(tag); value != nil { + if listeners, ok := value.([]func(ImageView)); ok { + return listeners + } + } + return []func(ImageView){} +} + func (imageView *imageViewData) srcSet(path string) string { if srcset, ok := resources.imageSrcSets[path]; ok { buffer := allocStringBuilder() @@ -214,6 +316,12 @@ func (imageView *imageViewData) htmlProperties(self View, buffer *strings.Builde buffer.WriteString(textToJS(text)) buffer.WriteString(`"`) } + + buffer.WriteString(` onload="imageLoaded(this, event)"`) + + if len(imageView.imageListeners(ErrorEvent)) > 0 { + buffer.WriteString(` onerror="imageError(this, event)"`) + } } func (imageView *imageViewData) cssStyle(self View, builder cssBuilder) { @@ -251,6 +359,36 @@ func (imageView *imageViewData) cssStyle(self View, builder cssBuilder) { } } +func (imageView *imageViewData) handleCommand(self View, command string, data DataObject) bool { + switch command { + case "imageViewError": + for _, listener := range imageView.imageListeners(ErrorEvent) { + listener(imageView) + } + + case "imageViewLoaded": + imageView.naturalWidth = dataFloatProperty(data, "natural-width") + imageView.naturalHeight = dataFloatProperty(data, "natural-height") + imageView.currentSrc, _ = data.PropertyValue("current-src") + + for _, listener := range imageView.imageListeners(LoadedEvent) { + listener(imageView) + } + + default: + return imageView.viewData.handleCommand(self, command, data) + } + return true +} + +func (imageView *imageViewData) NaturalSize() (float64, float64) { + return imageView.naturalWidth, imageView.naturalHeight +} + +func (imageView *imageViewData) CurrentSource() string { + return imageView.currentSrc +} + // 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 { diff --git a/popup.go b/popup.go index d2db464..74d966d 100644 --- a/popup.go +++ b/popup.go @@ -3,19 +3,42 @@ package rui import "strings" const ( - // Title is the Popup string property + // Title is the constant for the "title" property tag. + // The "title" property is defined the Popup/Tabs title Title = "title" - // TitleStyle is the Popup string property + + // TitleStyle is the constant for the "title-style" property tag. + // The "title-style" string property is used to set the title style of the Popup. TitleStyle = "title-style" - // CloseButton is the Popup bool property + + // CloseButton is the constant for the "close-button" property tag. + // The "close-button" bool property allow to add the close button to the Popup. + // Setting this property to "true" adds a window close button to the title bar (the default value is "false"). CloseButton = "close-button" - // OutsideClose is the Popup bool property + + // OutsideClose is the constant for the "outside-close" property tag. + // The "outside-close" is a bool property. If it is set to "true", + // then clicking outside the popup window automatically calls the Dismiss() method. OutsideClose = "outside-close" - Buttons = "buttons" + + // Buttons is the constant for the "buttons" property tag. + // Using the "buttons" property you can add buttons that will be placed at the bottom of the Popup. + // The "buttons" property can be assigned the following data types: PopupButton and []PopupButton + Buttons = "buttons" + + // ButtonsAlign is the constant for the "buttons-align" property tag. + // The "buttons-align" int property is used for set the horizontal alignment of Popup buttons. + // Valid values: LeftAlign (0), RightAlign (1), CenterAlign (2), and StretchAlign (3) ButtonsAlign = "buttons-align" + + // DismissEvent is the constant for the "dismiss-event" property tag. + // The "dismiss-event" event is used to track the closing of the Popup. + // It occurs after the Popup disappears from the screen. + // The main listener for this event has the following format: func(Popup) DismissEvent = "dismiss-event" ) +// PopupButton describes a button that will be placed at the bottom of the window. type PopupButton struct { Title string OnClick func(Popup) @@ -23,11 +46,11 @@ type PopupButton struct { // Popup interface type Popup interface { - //Properties View() View Session() Session Show() Dismiss() + onDismiss() html(buffer *strings.Builder) viewByHTMLID(id string) View } @@ -46,6 +69,10 @@ func (popup *popupData) init(view View, params Params) { popup.view = view session := view.Session() + if params == nil { + params = Params{} + } + popup.dismissListener = []func(Popup){} if value, ok := params[DismissEvent]; ok && value != nil { switch value := value.(type) { @@ -98,7 +125,6 @@ func (popup *popupData) init(view View, params Params) { outsideClose, _ := boolProperty(params, OutsideClose, session) vAlign, _ := enumProperty(params, VerticalAlign, session, CenterAlign) hAlign, _ := enumProperty(params, HorizontalAlign, session, CenterAlign) - buttonsAlign, _ := enumProperty(params, ButtonsAlign, session, RightAlign) buttons := []PopupButton{} if value, ok := params[Buttons]; ok && value != nil { @@ -191,6 +217,7 @@ func (popup *popupData) init(view View, params Params) { popupView.Append(view) if buttonCount := len(buttons); buttonCount > 0 { + buttonsAlign, _ := enumProperty(params, ButtonsAlign, session, RightAlign) cellHeight = append(cellHeight, AutoSize()) gap, _ := sizeConstant(session, "ruiPopupButtonGap") cellWidth := []SizeUnit{} @@ -276,6 +303,12 @@ func (popup *popupData) viewByHTMLID(id string) View { return viewByHTMLID(id, popup.layerView) } +func (popup *popupData) onDismiss() { + for _, listener := range popup.dismissListener { + listener(popup) + } +} + // NewPopup creates a new Popup func NewPopup(view View, param Params) Popup { if view == nil { @@ -287,6 +320,15 @@ func NewPopup(view View, param Params) Popup { return popup } +// ShowPopup creates a new Popup and shows it +func ShowPopup(view View, param Params) Popup { + popup := NewPopup(view, param) + if popup != nil { + popup.Show() + } + return popup +} + func (manager *popupManager) updatePopupLayerInnerHTML(session Session) { if manager.popups == nil { manager.popups = []Popup{} @@ -343,6 +385,7 @@ func (manager *popupManager) dismissPopup(popup Popup) { manager.popups = manager.popups[:count-1] manager.updatePopupLayerInnerHTML(session) } + popup.onDismiss() return } @@ -354,6 +397,7 @@ func (manager *popupManager) dismissPopup(popup Popup) { manager.popups = append(manager.popups[:n], manager.popups[n+1:]...) } manager.updatePopupLayerInnerHTML(session) + popup.onDismiss() return } } diff --git a/popupUtils.go b/popupUtils.go index 8448daa..bcc696d 100644 --- a/popupUtils.go +++ b/popupUtils.go @@ -1,6 +1,6 @@ package rui -// ShowMessage displays the popup with text message +// ShowMessage displays the popup with the title given in the "title" argument and the message text given in the "text" argument. func ShowMessage(title, text string, session Session) { textView := NewTextView(session, Params{ Text: text, @@ -16,6 +16,9 @@ func ShowMessage(title, text string, session Session) { NewPopup(textView, params).Show() } +// ShowQuestion displays a message with the given title and text and two buttons "Yes" and "No". +// When the "Yes" button is clicked, the message is closed and the onYes function is called (if it is not nil). +// When the "No" button is pressed, the message is closed and the onNo function is called (if it is not nil). func ShowQuestion(title, text string, session Session, onYes func(), onNo func()) { textView := NewTextView(session, Params{ Text: text, @@ -51,6 +54,9 @@ func ShowQuestion(title, text string, session Session, onYes func(), onNo func() NewPopup(textView, params).Show() } +// ShowCancellableQuestion displays a message with the given title and text and three buttons "Yes", "No" and "Cancel". +// When the "Yes", "No" or "Cancel" button is pressed, the message is closed and the onYes, onNo or onCancel function +// (if it is not nil) is called, respectively. func ShowCancellableQuestion(title, text string, session Session, onYes func(), onNo func(), onCancel func()) { textView := NewTextView(session, Params{ Text: text, @@ -128,9 +134,13 @@ func (popup *popupMenuData) IsListItemEnabled(index int) bool { return true } +// PopupMenuResult is the constant for the "popup-menu-result" property tag. +// The "popup-menu-result" property sets the function (format: func(int)) to be called when +// a menu item of popup menu is selected. const PopupMenuResult = "popup-menu-result" -// ShowMenu displays the popup with text message +// ShowMenu displays the menu. Menu items are set using the Items property. +// The "popup-menu-result" property sets the function (format: func(int)) to be called when a menu item is selected. func ShowMenu(session Session, params Params) Popup { value, ok := params[Items] if !ok || value == nil { diff --git a/propertyNames.go b/propertyNames.go index c5256c0..00e32bd 100644 --- a/propertyNames.go +++ b/propertyNames.go @@ -466,13 +466,7 @@ const ( // The "grid-column-gap" SizeUnit properties allow to set the distance between the columns of the GridLayout container. // The default is 0px. GridColumnGap = "grid-column-gap" - /* - // GridAutoRows is the constant for the "grid-auto-rows" property tag. - GridAutoRows = "grid-auto-rows" - // GridAutoColumns is the constant for the "grid-auto-columns" property tag. - GridAutoColumns = "grid-auto-columns" - */ // Source is the constant for the "src" property tag. Source = "src" @@ -638,4 +632,9 @@ const ( // The "resize" int property sets whether an element is resizable, and if so, in which directions. // Valid values are "none" (0), "both" (1), horizontal (2), and "vertical" (3) Resize = "resize" + + // UserSelect is the constant for the "user-select" property tag. + // The "user-select" bool property controls whether the user can select text. + // This is an inherited property, i.e. if it is not defined, then the value of the parent view is used. + UserSelect = "user-select" ) diff --git a/propertySet.go b/propertySet.go index 5b05778..ac78adc 100644 --- a/propertySet.go +++ b/propertySet.go @@ -62,6 +62,7 @@ var boolProperties = []string{ Multiple, TabCloseButton, Repeating, + UserSelect, } var intProperties = []string{ diff --git a/view.go b/view.go index ec75be1..59122c7 100644 --- a/view.go +++ b/view.go @@ -583,6 +583,21 @@ func viewPropertyChanged(view *viewData, tag string) { updateInnerHTML(parent, session) } return + + case UserSelect: + if userSelect, ok := boolProperty(view, UserSelect, session); ok { + if userSelect { + updateCSSProperty(htmlID, "-webkit-user-select", "auto", session) + updateCSSProperty(htmlID, "user-select", "auto", session) + } else { + updateCSSProperty(htmlID, "-webkit-user-select", "none", session) + updateCSSProperty(htmlID, "user-select", "none", session) + } + } else { + updateCSSProperty(htmlID, "-webkit-user-select", "", session) + updateCSSProperty(htmlID, "user-select", "", session) + } + return } if cssTag, ok := sizeProperties[tag]; ok { diff --git a/viewStyle.go b/viewStyle.go index bd88246..9052fe3 100644 --- a/viewStyle.go +++ b/viewStyle.go @@ -272,6 +272,16 @@ func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session) { builder.add("text-decoration", text) } + if userSelect, ok := boolProperty(style, UserSelect, session); ok { + if userSelect { + builder.add("-webkit-user-select", "auto") + builder.add("user-select", "auto") + } else { + builder.add("-webkit-user-select", "none") + builder.add("user-select", "none") + } + } + if css := shadowCSS(style, Shadow, session); css != "" { builder.add("box-shadow", css) } diff --git a/viewUtils.go b/viewUtils.go index b6d6dff..00aa590 100644 --- a/viewUtils.go +++ b/viewUtils.go @@ -1103,3 +1103,46 @@ func GetCurrent(view View, subviewID string) int { } return defaultValue } + +// IsUserSelect returns "true" if the user can select text, "false" otherwise. +// If the second argument (subviewID) is "" then a value from the first argument (view) is returned. +func IsUserSelect(view View, subviewID string) bool { + if subviewID != "" { + view = ViewByID(view, subviewID) + } + + if view != nil { + value, _ := isUserSelect(view) + return value + } + + return false +} + +func isUserSelect(view View) (bool, bool) { + result, ok := boolStyledProperty(view, UserSelect) + if ok { + return result, true + } + + if parent := view.Parent(); parent != nil { + result, ok = isUserSelect(parent) + if ok { + return result, true + } + } + + if !result { + switch GetSemantics(view, "") { + case ParagraphSemantics, H1Semantics, H2Semantics, H3Semantics, H4Semantics, H5Semantics, + H6Semantics, BlockquoteSemantics, CodeSemantics: + return true, false + } + + if _, ok := view.(TableView); ok { + return true, false + } + } + + return result, false +}