Compare commits

..

No commits in common. "main" and "v0.9.0" have entirely different histories.
main ... v0.9.0

120 changed files with 15160 additions and 30379 deletions

View File

@ -1,153 +1,3 @@
# v0.20.0
* Added support of binding
* Added "binding" argument to CreateViewFromResources, CreateViewFromText, and CreateViewFromObject functions
* Added CreatePopupFromResources, CreatePopupFromText, and CreatePopupFromObject functions
* Added All() iterator and IsEmpty() method to Properties interface
* Added implementation of Properties interface to Popup
* Changed ParseDataText function return values
* Added `Properties() iter.Seq[DataNode]` iterator to DataObject interface
* Renamed `ArrayElements() []DataValue` method of DataNode interface to `Array() []DataValue`
* Added `ArrayElements() iter.Seq[DataValue]` iterator to DataNode interface
# v0.19.0
* Added support of drag-and-drop
* Added LoadFile method to View interface
# v0.18.2
* fixed typo: GetShadowProperties -> GetShadowProperty
# v0.18.0
* Property name type changed from string to PropertyName.
* Renamed:
Transform interface -> TransformProperty
NewTransform function -> NewTransformProperty
TransformTag constant -> Transform.
"origin-x" property -> "transform-origin-x"
"origin-y" property -> "transform-origin-y"
"origin-z" property -> "transform-origin-z"
GetOrigin function -> GetTransformOrigin.
BorderBoxClip constant -> BorderBox
PaddingBoxClip constant -> PaddingBox
ContentBoxClip constant -> ContentBox.
ViewShadow interface -> ShadowProperty
NewViewShadow function -> NewShadow
NewInsetViewShadow function -> NewInsetShadow
NewShadowWithParams function -> NewShadowProperty
NewColumnSeparator function -> NewColumnSeparatorProperty
ClipShape interface -> ClipShapeProperty
InsetClip function -> NewInsetClip
CircleClip function -> NewCircleClip
EllipseClip function -> NewEllipseClip
PolygonClip function -> NewPolygonClip
PolygonPointsClip function -> NewPolygonPointsClip
ViewFilter interface -> FilterProperty
NewViewFilter function -> NewFilterProperty
Animation interface -> AnimationProperty
AnimationTag constant -> Animation
NewAnimation function -> NewAnimationProperty
* Added functions: NewBounds, NewEllipticRadius, NewRadii, NewLinearGradient, NewCircleRadialGradient,
NewEllipseRadialGradient, GetPushTransform, GetPushDuration, GetPushTiming, IsMoveToFrontAnimation,
GetBackground, GetMask, GetBackgroundClip,GetBackgroundOrigin, GetMaskClip, GetMaskOrigin, NewColumnSeparator,
NewClipShapeProperty, NewTransitionAnimation, NewAnimation, IsSummaryMarkerHidden.
* Changed ViewByID functions
* Added SetConicGradientFillStyle and SetConicGradientStrokeStyle methods to Canvas interface.
* Changed Push, Pop, MoveToFront, and MoveToFrontByID methods of StackLayout interface.
* Removed DefaultAnimation, StartToEndAnimation, EndToStartAnimation, TopDownAnimation, and BottomUpAnimation constants.
* Added StackLayout properties: "push-transform", "push-duration", "push-timing", "move-to-front-animation", "push-perspective",
"push-rotate-x", "push-rotate-y", "push-rotate-z", "push-rotate", "push-skew-x", "push-skew-y",
"push-scale-x", "push-scale-y", "push-scale-z", "push-translate-x", "push-translate-y", "push-translate-z".
* Added "show-opacity", "show-transform", "show-duration", and "show-timing" Popup properties.
* Added "mask", "mask-clip", "mask-origin", and "background-origin" properties.
* Added "hide-summary-marker" DetailsView property.
* Added LineJoin type. Type of constants MiterJoin, RoundJoin, and BevelJoin changed to LineJoin. Type of Canvas.SetLineJoin function argument changed to LineJoin.
* Added LineCap type. Type of constants ButtCap, RoundCap, and SquareCap changed to LineCap. Type of Canvas.SetLineCap function argument changed to LineCap.
# v0.17.3
Added SetParams method to View interface
# v0.17.0
* Added "mod", "rem", "round", "round-up", "round-down", and "round-to-zero" SizeFunc functions
* Added ModSize, RemSize, RoundSize, RoundUpSize, RoundDownSize, and RoundToZeroSize functions
* Added Start, Stop, Pause, and Resume methods to Animation interface
* Added "transform" property and Transform interface
* Added OpenRawResource, GetCheckboxChangedListeners functions
* Added RemoveClientItem method to Session interface
* Added "item-separators" property to DropDownList and GetDropDownItemSeparators function
* Added NewPath and NewPathFromSvg methods to Canvas interface
* Removed NewPath function
* Removed Reset methods from Path interface
# v0.16.0
* Can use ListAdapter as "content" property value of ListLayout
* The IsListItemEnabled method of the ListAdapter interface has been made optional
* Can use GridAdapter as "content" property value of GridLayout
* Added "text-wrap" property and GetGetTextWrap function
* Bug fixing
# v0.15.0
* Added "data-list" property
* Bug fixing
# v0.14.0
* Added the ability to work without creating a WebSocket. Added NoSocket property to AppParams.
* Added SocketAutoClose property to AppParams.
* Added the ability to run a timer on the client side. Added StartTimer and StopTimer methods to Session interface.
* Added "cell-vertical-self-align", and "cell-horizontal-self-align" properties
* Bug fixing
# v0.13.x
* Added NewHandler function
* Bug fixing
# v0.13.0
* Added SetHotKey function to Session interface
* Added ViewIndex function to ViewsContainer interface
* Added ReloadCell function to TableView interface
* Added ReloadTableViewCell function
* Added "tooltip" property and GetTooltip function
* Added "outline-offset" property and GetOutlineOffset function
* Changed the main event listener format for "drop-down-event", "edit-text-changed",
"color-changed", "number-changed", "date-changed", and "time-changed" events.
Old format is "<listener>(<view>, <new value>)", new format is "<listener>(<view>, <new value>, <old value>)"
* Changed FocusView function
* Added support for height and width range in media styles.
Changed MediaStyle, SetMediaStyle, and MediaStyles functions of Theme interface
* Bug fixing
# v0.12.0
* Added SvgImageView
* Added InlineImageFromResource function
# v0.11.0
* Added "tabindex", "order", "column-fill", "column-span-all", "background-blend-mode", and "mix-blend-mode" properties
* Added GetTabIndex, GetOrder, GetColumnFill, IsColumnSpanAll, GetBackgroundBlendMode, and GetMixBlendMode functions
* ClientItem, SetClientItem, and RemoveAllClientItems method added to Session interface
* PropertyWithTag method of DataObject interface renamed to PropertyByTag
# v0.10.0
* The Canvas.TextWidth method replaced by Canvas.TextMetrics
* Added support of WebAssembly
# v0.9.0 # v0.9.0
* Requires go 1.18 or higher * Requires go 1.18 or higher
@ -218,7 +68,7 @@ Changed MediaStyle, SetMediaStyle, and MediaStyles functions of Theme interface
# v0.2.0 # v0.2.0
* Added "animation" and "transition" properties, Animation interface, animation events * Added "animation" and "transition" properties, Animation interface, animation events
* Renamed ColorProperty constant to ColorTag * Renamed ColorPropery constant to ColorTag
* Updated readme * Updated readme
* Added the Animation example to the demo * Added the Animation example to the demo
* Bug fixing * Bug fixing

File diff suppressed because it is too large Load Diff

989
README.md

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,7 +1,6 @@
package rui package rui
import ( import (
"errors"
"fmt" "fmt"
"math" "math"
"strconv" "strconv"
@ -12,51 +11,43 @@ import (
// Can take the following values: Radian, Degree, Gradian, and Turn // Can take the following values: Radian, Degree, Gradian, and Turn
type AngleUnitType uint8 type AngleUnitType uint8
// Constants which represent values or the [AngleUnitType]
const ( const (
// Radian - angle in radians // Radian - angle in radians
Radian AngleUnitType = 0 Radian AngleUnitType = 0
// Radian - angle in radians * π // Radian - angle in radians * π
PiRadian AngleUnitType = 1 PiRadian AngleUnitType = 1
// Degree - angle in degrees // Degree - angle in degrees
Degree AngleUnitType = 2 Degree AngleUnitType = 2
// Gradian - angle in gradian (1400 of a full circle) // Gradian - angle in gradian (1400 of a full circle)
Gradian AngleUnitType = 3 Gradian AngleUnitType = 3
// Turn - angle in turns (1 turn = 360 degree) // Turn - angle in turns (1 turn = 360 degree)
Turn AngleUnitType = 4 Turn AngleUnitType = 4
) )
// AngleUnit used to represent an angular values // AngleUnit describe a size (Value field) and size unit (Type field).
type AngleUnit struct { type AngleUnit struct {
// Type of the angle value
Type AngleUnitType Type AngleUnitType
// Value of the angle in Type units
Value float64 Value float64
} }
// Deg creates AngleUnit with Degree type // Deg creates AngleUnit with Degree type
func Deg[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) AngleUnit { func Deg(value float64) AngleUnit {
return AngleUnit{Type: Degree, Value: float64(value)} return AngleUnit{Type: Degree, Value: value}
} }
// Rad create AngleUnit with Radian type // Rad create AngleUnit with Radian type
func Rad[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) AngleUnit { func Rad(value float64) AngleUnit {
return AngleUnit{Type: Radian, Value: float64(value)} return AngleUnit{Type: Radian, Value: value}
} }
// PiRad create AngleUnit with PiRadian type // PiRad create AngleUnit with PiRadian type
func PiRad[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) AngleUnit { func PiRad(value float64) AngleUnit {
return AngleUnit{Type: PiRadian, Value: float64(value)} return AngleUnit{Type: PiRadian, Value: value}
} }
// Grad create AngleUnit with Gradian type // Grad create AngleUnit with Gradian type
func Grad[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) AngleUnit { func Grad(value float64) AngleUnit {
return AngleUnit{Type: Gradian, Value: float64(value)} return AngleUnit{Type: Gradian, Value: value}
} }
// Equal compare two AngleUnit. Return true if AngleUnit are equal // Equal compare two AngleUnit. Return true if AngleUnit are equal
@ -87,10 +78,6 @@ func StringToAngleUnit(value string) (AngleUnit, bool) {
func stringToAngleUnit(value string) (AngleUnit, error) { func stringToAngleUnit(value string) (AngleUnit, error) {
value = strings.ToLower(strings.Trim(value, " \t\n\r")) value = strings.ToLower(strings.Trim(value, " \t\n\r"))
if value == "" {
return AngleUnit{}, errors.New(`invalid AngleUnit value: ""`)
}
setValue := func(suffix string, unitType AngleUnitType) (AngleUnit, error) { setValue := func(suffix string, unitType AngleUnitType) (AngleUnit, error) {
val, err := strconv.ParseFloat(value[:len(value)-len(suffix)], 64) val, err := strconv.ParseFloat(value[:len(value)-len(suffix)], 64)
if err != nil { if err != nil {

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package rui
import ( import (
"fmt" "fmt"
"log"
"runtime" "runtime"
) )
@ -9,8 +10,14 @@ import (
// clients and the server is displayed in the debug log // clients and the server is displayed in the debug log
var ProtocolInDebugLog = false var ProtocolInDebugLog = false
var debugLogFunc func(string) = debugLog var debugLogFunc func(string) = func(text string) {
var errorLogFunc func(string) = errorLog log.Println("\033[34m" + text)
}
var errorLogFunc = func(text string) {
log.Println("\033[31m" + text)
//println(text)
}
// SetDebugLog sets a function for outputting debug info. // SetDebugLog sets a function for outputting debug info.
// The default value is nil (debug info is ignored) // The default value is nil (debug info is ignored)

View File

@ -1,459 +0,0 @@
//go:build !wasm
package rui
import (
"context"
_ "embed"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os/exec"
"runtime"
"strconv"
"strings"
"time"
"golang.org/x/crypto/acme/autocert"
)
//go:embed app_socket.js
var socketScripts string
//go:embed app_post.js
var httpPostScripts string
func debugLog(text string) {
log.Println("\033[34m" + text)
}
func errorLog(text string) {
log.Println("\033[31m" + text)
}
type sessionInfo struct {
session Session
response chan string
}
type application struct {
server *http.Server
params AppParams
createContentFunc func(Session) SessionContent
sessions map[int]sessionInfo
}
func (app *application) getStartPage() string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString("<!DOCTYPE html>\n<html>\n")
getStartPage(buffer, app.params)
buffer.WriteString("\n</html>")
return buffer.String()
}
func (app *application) Params() AppParams {
params := app.params
if params.NoSocket {
params.SocketAutoClose = 0
}
return params
}
func (app *application) Finish() {
for _, session := range app.sessions {
session.session.close()
if session.response != nil {
close(session.response)
session.response = nil
}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := app.server.Shutdown(ctx); err != nil {
log.Println(err.Error())
}
}
func (app *application) nextSessionID() int {
n := rand.Intn(0x7FFFFFFE) + 1
_, ok := app.sessions[n]
for ok {
n = rand.Intn(0x7FFFFFFE) + 1
_, ok = app.sessions[n]
}
return n
}
func (app *application) removeSession(id int) {
if info, ok := app.sessions[id]; ok {
if info.response != nil {
close(info.response)
}
delete(app.sessions, id)
}
}
func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if ProtocolInDebugLog {
DebugLogF("%s %s", req.Method, req.URL.Path)
}
switch req.Method {
case http.MethodPost:
if req.URL.Path == "/" {
app.postHandler(w, req)
}
case http.MethodGet:
switch req.URL.Path {
case "/":
w.WriteHeader(http.StatusOK)
io.WriteString(w, app.getStartPage())
case "/ws":
if bridge := createSocketBridge(w, req); bridge != nil {
go app.socketReader(bridge)
}
case "/script.js":
w.WriteHeader(http.StatusOK)
if app.params.NoSocket {
io.WriteString(w, httpPostScripts)
} else {
io.WriteString(w, socketScripts)
}
io.WriteString(w, "\n")
io.WriteString(w, defaultScripts)
default:
filename := req.URL.Path[1:]
if size := len(filename); size > 0 && filename[size-1] == '/' {
filename = filename[:size-1]
}
if !serveResourceFile(filename, w, req) &&
!serveDownloadFile(filename, w, req) {
w.WriteHeader(http.StatusNotFound)
}
}
}
}
func setSessionIDCookie(w http.ResponseWriter, sessionID int) {
cookie := http.Cookie{
Name: "session",
Value: strconv.Itoa(sessionID),
HttpOnly: true,
}
http.SetCookie(w, &cookie)
}
func (app *application) postHandler(w http.ResponseWriter, req *http.Request) {
if reqBody, err := io.ReadAll(req.Body); err == nil {
message := string(reqBody)
if ProtocolInDebugLog {
DebugLog(message)
}
obj, err := ParseDataText(message)
if err != nil {
ErrorLog(err.Error())
return
}
var session Session = nil
var response chan string = nil
if cookie, err := req.Cookie("session"); err == nil {
sessionID, err := strconv.Atoi(cookie.Value)
if err != nil {
ErrorLog(err.Error())
} else if info, ok := app.sessions[sessionID]; ok && info.response != nil {
response = info.response
session = info.session
}
}
command := obj.Tag()
startSession := false
if session == nil || command == "startSession" {
events := make(chan DataObject, 1024)
bridge := createHttpBridge(req)
response = bridge.response
answer := ""
session, answer = app.startSession(obj, events, bridge, response)
bridge.writeMessage(answer)
session.onStart()
if command == "session-resume" {
session.onResume()
}
bridge.sendResponse()
setSessionIDCookie(w, session.ID())
startSession = true
go sessionEventHandler(session, events, bridge)
}
if !startSession {
switch command {
case "nop":
session.sendResponse()
case "session-close":
session.onFinish()
session.App().removeSession(session.ID())
return
default:
if !session.handleAnswer(command, obj) {
session.addToEventsQueue(obj)
}
}
}
io.WriteString(w, <-response)
for len(response) > 0 {
io.WriteString(w, <-response)
}
}
}
func (app *application) socketReader(bridge *wsBridge) {
var session Session
events := make(chan DataObject, 1024)
for {
message, ok := bridge.readMessage()
if !ok {
events <- NewDataObject("disconnect")
return
}
if ProtocolInDebugLog {
DebugLog("🖥️ -> " + message)
}
obj, err := ParseDataText(message)
if err != nil {
ErrorLog(err.Error())
return
}
switch command := obj.Tag(); command {
case "startSession":
answer := ""
if session, answer = app.startSession(obj, events, bridge, nil); session != nil {
if !bridge.writeMessage(answer) {
return
}
session.onStart()
go sessionEventHandler(session, events, bridge)
}
case "reconnect":
session = nil
if sessionText, ok := obj.PropertyValue("session"); ok {
if sessionID, err := strconv.Atoi(sessionText); err == nil {
if info, ok := app.sessions[sessionID]; ok {
session = info.session
session.setBridge(events, bridge)
go sessionEventHandler(session, events, bridge)
session.onReconnect()
} else {
DebugLogF("Session #%d not exists", sessionID)
}
} else {
ErrorLog(`strconv.Atoi(sessionText) error: ` + err.Error())
}
} else {
ErrorLog(`"session" key not found`)
}
if session == nil {
/* answer := ""
if session, answer = app.startSession(obj, events, bridge, nil); session != nil {
if !bridge.writeMessage(answer) {
return
}
session.onStart()
go sessionEventHandler(session, events, bridge)
bridge.writeMessage("restartSession();")
}
*/
bridge.writeMessage("reloadPage();")
return
}
default:
if !session.handleAnswer(command, obj) {
events <- obj
}
}
}
}
func sessionEventHandler(session Session, events chan DataObject, bridge bridge) {
for {
data := <-events
switch command := data.Tag(); command {
case "disconnect":
session.onDisconnect()
return
case "session-close":
session.onFinish()
session.App().removeSession(session.ID())
bridge.close()
default:
session.handleEvent(command, data)
}
}
}
func (app *application) startSession(params DataObject, events chan DataObject,
bridge bridge, response chan string) (Session, string) {
if app.createContentFunc == nil {
return nil, ""
}
session := newSession(app, app.nextSessionID(), "", params)
session.setBridge(events, bridge)
if !session.setContent(app.createContentFunc(session)) {
return nil, ""
}
app.sessions[session.ID()] = sessionInfo{
session: session,
response: response,
}
answer := allocStringBuilder()
defer freeStringBuilder(answer)
answer.WriteString("sessionID = '")
answer.WriteString(strconv.Itoa(session.ID()))
answer.WriteString("';\n")
session.writeInitScript(answer)
answerText := answer.String()
if ProtocolInDebugLog {
DebugLog("Start session:")
DebugLog(answerText)
}
return session, answerText
}
var apps = []*application{}
// StartApp - create the new application and start it
func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) {
resources.scanDefaultResourcePath()
app := new(application)
app.params = params
app.sessions = map[int]sessionInfo{}
app.createContentFunc = createContentFunc
apps = append(apps, app)
redirectAddr := ""
https := params.AutoCertDomain != "" || (params.CertFile != "" && params.KeyFile != "")
if index := strings.IndexRune(addr, ':'); index >= 0 {
redirectAddr = addr[:index] + ":80"
} else {
redirectAddr = addr + ":80"
if https {
addr += ":443"
} else {
addr += ":80"
}
}
serverRun := func(err error) {
if err != nil {
if err == http.ErrServerClosed {
log.Println(err)
} else {
log.Fatal(err)
}
}
}
if https {
if params.Redirect80 {
redirectTLS := func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://"+addr+r.RequestURI, http.StatusMovedPermanently)
}
go func() {
serverRun(http.ListenAndServe(redirectAddr, http.HandlerFunc(redirectTLS)))
}()
}
if params.AutoCertDomain != "" {
mux := http.NewServeMux()
mux.Handle("/", app)
serverRun(http.Serve(autocert.NewListener(params.AutoCertDomain), mux))
} else {
app.server = &http.Server{Addr: addr}
http.Handle("/", app)
serverRun(app.server.ListenAndServeTLS(params.CertFile, params.KeyFile))
}
} else {
app.server = &http.Server{Addr: addr}
http.Handle("/", app)
serverRun(app.server.ListenAndServe())
}
}
// FinishApp finishes application
func FinishApp() {
for _, app := range apps {
app.Finish()
}
apps = []*application{}
}
// OpenBrowser open browser with specific URL locally. Useful for applications which run on local machine
// or for debug purposes.
func OpenBrowser(url string) bool {
var err error
switch runtime.GOOS {
case "linux":
for _, provider := range []string{"xdg-open", "x-www-browser", "www-browser"} {
if _, err = exec.LookPath(provider); err == nil {
if err = exec.Command(provider, url).Start(); err == nil {
return true
}
}
}
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
return err != nil
}

View File

@ -1,186 +0,0 @@
//go:build wasm
package rui
import (
_ "embed"
"encoding/base64"
"path/filepath"
"strings"
"syscall/js"
)
//go:embed app_wasm.js
var wasmScripts string
type wasmApp struct {
params AppParams
createContentFunc func(Session) SessionContent
session Session
bridge bridge
close chan DataObject
}
func (app *wasmApp) Finish() {
app.session.close()
}
func (app *wasmApp) Params() AppParams {
params := app.params
params.SocketAutoClose = 0
return params
}
func debugLog(text string) {
js.Global().Get("console").Call("log", text)
}
func errorLog(text string) {
js.Global().Get("console").Call("log", "%c"+text, "color: #F00;")
}
func (app *wasmApp) handleMessage(this js.Value, args []js.Value) any {
if len(args) > 0 {
text := args[0].String()
if ProtocolInDebugLog {
DebugLog(text)
}
if obj := ParseDataText(text); obj != nil {
switch command := obj.Tag(); command {
case "session-close":
app.close <- obj
default:
if !app.session.handleAnswer(command, obj) {
app.session.handleEvent(command, obj)
}
}
}
}
return nil
}
func (app *wasmApp) removeSession(id int) {
}
func (app *wasmApp) createSession() Session {
session := newSession(app, 0, "", ParseDataText(js.Global().Call("sessionInfo", "").String()))
session.setBridge(app.close, app.bridge)
session.setContent(app.createContentFunc(session))
return session
}
func (app *wasmApp) init(params AppParams) {
app.params = params
document := js.Global().Get("document")
body := document.Call("querySelector", "body")
head := document.Call("querySelector", "head")
meta := document.Call("createElement", "meta")
meta.Set("name", "viewport")
meta.Set("content", "width=device-width")
head.Call("appendChild", meta)
meta = document.Call("createElement", "base")
meta.Set("target", "_blank")
meta.Set("rel", "noopener")
head.Call("appendChild", meta)
if params.Icon != "" {
url := params.Icon
if image, ok := resources.images[params.Icon]; ok && image.fs != nil {
dataType := map[string]string{
".svg": "data:image/svg+xml",
".png": "data:image/png",
".jpg": "data:image/jpg",
".jpeg": "data:image/jpg",
".gif": "data:image/gif",
}
ext := strings.ToLower(filepath.Ext(params.Icon))
if prefix, ok := dataType[ext]; ok {
if data, err := image.fs.ReadFile(image.path); err == nil {
url = prefix + ";base64," + base64.StdEncoding.EncodeToString(data)
} else {
DebugLog(err.Error())
}
}
}
meta = document.Call("createElement", "link")
meta.Set("rel", "icon")
meta.Set("href", url)
head.Call("appendChild", meta)
}
script := document.Call("createElement", "script")
script.Set("type", "text/javascript")
script.Set("textContent", defaultScripts+wasmScripts)
body.Call("appendChild", script)
js.Global().Set("sendMessage", js.FuncOf(app.handleMessage))
app.close = make(chan DataObject)
app.session = app.createSession()
style := document.Call("createElement", "style")
css := appStyles + app.session.getCurrentTheme().cssText(app.session)
css = strings.ReplaceAll(css, `\n`, "\n")
css = strings.ReplaceAll(css, `\t`, "\t")
style.Set("textContent", css)
document.Call("querySelector", "head").Call("appendChild", style)
style = document.Call("createElement", "style")
style.Set("id", "ruiAnimations")
document.Call("querySelector", "head").Call("appendChild", style)
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
div := document.Call("createElement", "div")
div.Set("className", "ruiRoot")
div.Set("id", "ruiRootView")
viewHTML(app.session.RootView(), buffer, "")
div.Set("innerHTML", buffer.String())
body.Call("appendChild", div)
div = document.Call("createElement", "div")
div.Set("className", "ruiPopupLayer")
div.Set("id", "ruiPopupLayer")
div.Set("style", "visibility: hidden;")
body.Call("appendChild", div)
div = document.Call("createElement", "a")
div.Set("id", "ruiDownloader")
div.Set("download", "")
div.Set("style", "display: none;")
body.Call("appendChild", div)
if params.TitleColor != 0 {
app.bridge.callFunc("setTitleColor", params.TitleColor.cssString())
}
}
// StartApp - create the new wasmApp and start it
func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) {
if createContentFunc == nil {
return
}
app := new(wasmApp)
app.createContentFunc = createContentFunc
app.close = make(chan DataObject)
app.bridge = createWasmBridge(app.close)
app.init(params)
<-app.close
}
func FinishApp() {
}
func OpenBrowser(url string) bool {
return false
}

View File

@ -1,25 +0,0 @@
async function sendMessage(message) {
const response = await fetch('/', {
method : 'POST',
body : message,
"Content-Type" : "text/plain",
});
const text = await response.text();
if (text != "") {
window.eval(text)
}
}
window.onload = function() {
sendMessage( sessionInfo() );
}
window.onfocus = function() {
windowFocus = true
sendMessage( "session-resume{}" );
}
function closeSocket() {
}

File diff suppressed because it is too large Load Diff

View File

@ -1,78 +0,0 @@
let socket
function sendMessage(message) {
if (!socket) {
createSocket(function() {
sendMessage( "reconnect{session=" + sessionID + "}" );
if (!windowFocus) {
windowFocus = true;
sendMessage( "session-resume{session=" + sessionID +"}" );
}
socket.send(message);
});
} else {
socket.send(message);
}
}
function createSocket(onopen) {
let socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://"
socketUrl += document.location.hostname
const port = document.location.port
if (port) {
socketUrl += ":" + port
}
socketUrl += window.location.pathname + "ws"
socket = new WebSocket(socketUrl);
socket.onopen = onopen;
socket.onclose = onSocketClose;
socket.onerror = onSocketError;
socket.onmessage = function(event) {
window.execScript ? window.execScript(event.data) : window.eval(event.data);
};
}
function closeSocket() {
if (socket) {
socket.close()
}
}
window.onload = createSocket(function() {
sendMessage( sessionInfo() );
});
window.onfocus = function() {
windowFocus = true
if (!socket) {
createSocket(function() {
sendMessage( "reconnect{session=" + sessionID + "}" );
sendMessage( "session-resume{session=" + sessionID +"}" );
});
} else {
sendMessage( "session-resume{session=" + sessionID +"}" );
}
}
function onSocketReopen() {
sendMessage( "reconnect{session=" + sessionID + "}" );
}
function socketReconnect() {
if (!socket) {
createSocket(onSocketReopen);
}
}
function onSocketClose(event) {
console.log("socket closed")
socket = null;
if (!event.wasClean && windowFocus) {
window.setTimeout(socketReconnect, 10000);
}
}
function onSocketError(error) {
console.log(error);
}

View File

@ -8,13 +8,6 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
:root {
--tooltip-arrow-size: 6px;
--tooltip-background: white;
--tooltip-text-color: black;
--tooltip-shadow-color: gray;
}
body { body {
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;
@ -22,7 +15,6 @@ body {
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
height: 100vh; height: 100vh;
font-family: system-ui;
} }
div { div {
@ -42,14 +34,12 @@ div:focus {
*/ */
input { input {
box-sizing: border-box;
margin: 2px; margin: 2px;
padding: 1px; padding: 1px;
font-size: inherit; font-size: inherit;
} }
select { select {
box-sizing: border-box;
margin: 2px; margin: 2px;
font-size: inherit; font-size: inherit;
} }
@ -59,12 +49,10 @@ button {
} }
textarea { textarea {
box-sizing: border-box;
margin: 2px; margin: 2px;
padding: 4px; padding: 1px;
overflow: auto; overflow: auto;
font-size: inherit; font-size: inherit;
resize: none;
} }
ul:focus { ul:focus {
@ -80,7 +68,7 @@ ul:focus {
} }
.ruiPopupLayer { .ruiPopupLayer {
/*background-color: rgba(128,128,128,0.1);*/ background-color: rgba(128,128,128,0.1);
position: absolute; position: absolute;
top: 0px; top: 0px;
bottom: 0px; bottom: 0px;
@ -88,54 +76,7 @@ ul:focus {
left: 0px; left: 0px;
} }
.ruiTooltipLayer {
display: grid;
grid-template-rows: 1fr auto 1fr;
justify-items: center;
align-items: center;
position: absolute;
top: 0px;
bottom: 0px;
right: 0px;
left: 0px;
transition: opacity 0.5s ease-out;
filter: drop-shadow(0px 0px 2px var(--tooltip-shadow-color));
}
.ruiTooltipTopArrow {
grid-row-start: 1;
grid-row-end: 2;
border-width: var(--tooltip-arrow-size);
border-style: solid;
border-color: transparent transparent var(--tooltip-background) transparent;
margin-left: 12px;
margin-right: 12px;
}
.ruiTooltipBottomArrow {
grid-row-start: 3;
grid-row-end: 4;
border-width: var(--tooltip-arrow-size);
border-style: solid;
border-color: var(--tooltip-background) transparent transparent transparent;
margin-left: 12px;
margin-right: 12px;
}
.ruiTooltipText {
grid-row-start: 2;
grid-row-end: 3;
padding: 4px 8px 4px 8px;
margin-left: 8px;
margin-right: 8px;
background-color: var(--tooltip-background);
color: var(--tooltip-text-color);
/*box-shadow: 0px 0px 4px 2px #8888;*/
border-radius: 4px;
}
.ruiView { .ruiView {
box-sizing: border-box;
} }
.ruiAbsoluteLayout { .ruiAbsoluteLayout {
@ -148,19 +89,6 @@ ul:focus {
.ruiListLayout { .ruiListLayout {
display: flex; display: flex;
overflow: auto;
}
.ruiButton {
display: flex;
overflow: auto;
justify-content: center;
align-items: center;
flex-flow: row;
}
.ruiColumnLayout {
overflow: auto;
} }
.ruiStackLayout { .ruiStackLayout {
@ -184,25 +112,16 @@ ul:focus {
} }
.ruiImageView { .ruiImageView {
display: block;
}
.ruiSvgImageView {
display: grid; display: grid;
} }
.ruiListView { .ruiListView {
overflow: auto; overflow: auto;
/*
display: flex;
align-content: stretch;
*/
} }
.hiddenMarker {
list-style: none;
}
.hiddenMarker::-webkit-details-marker {
display: none;
}
/* /*
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
body { body {

View File

@ -1,8 +0,0 @@
window.onfocus = function() {
windowFocus = true
sendMessage( "session-resume{session=" + sessionID +"}" );
}
function closeSocket() {
}

View File

@ -1,8 +1,21 @@
package rui package rui
import ( import (
"bytes"
"context"
_ "embed" _ "embed"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings" "strings"
"time"
) )
//go:embed app_scripts.js //go:embed app_scripts.js
@ -14,70 +27,62 @@ var appStyles string
//go:embed defaultTheme.rui //go:embed defaultTheme.rui
var defaultThemeText string var defaultThemeText string
// Application represent generic application interface, see also [Session] // Application - app interface
type Application interface { type Application interface {
// Finish finishes the application
Finish() Finish()
nextSessionID() int
// Params returns application parameters set by StartApp function
Params() AppParams
removeSession(id int) removeSession(id int)
} }
type application struct {
server *http.Server
params AppParams
createContentFunc func(Session) SessionContent
sessions map[int]Session
}
// AppParams defines parameters of the app // AppParams defines parameters of the app
type AppParams struct { type AppParams struct {
// Title - title of the app window/tab // Title - title of the app window/tab
Title string Title string
// TitleColor - background color of the app window/tab (applied only for Safari and Chrome for Android) // TitleColor - background color of the app window/tab (applied only for Safari and Chrome for Android)
TitleColor Color TitleColor Color
// Icon - the icon file name // Icon - the icon file name
Icon string Icon string
// CertFile - path of a certificate for the server must be provided // CertFile - path of a certificate for the server must be provided
// if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated. // if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated.
// If the certificate is signed by a certificate authority, the certFile should be the concatenation // If the certificate is signed by a certificate authority, the certFile should be the concatenation
// of the server's certificate, any intermediates, and the CA's certificate. // of the server's certificate, any intermediates, and the CA's certificate.
CertFile string CertFile string
AutoCertDomain string
// KeyFile - path of a private key for the server must be provided // KeyFile - path of a private key for the server must be provided
// if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated. // if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated.
KeyFile string KeyFile string
// Redirect80 - if true then the function of redirect from port 80 to 443 is created // Redirect80 - if true then the function of redirect from port 80 to 443 is created
Redirect80 bool Redirect80 bool
// NoSocket - if true then WebSockets will not be used and information exchange
// between the client and the server will be carried out only via http.
NoSocket bool
// SocketAutoClose - time in seconds after which the socket is automatically closed for an inactive session.
// The countdown begins after the OnPause event arrives.
// If the value of this property is less than or equal to 0 then the socket is not closed.
SocketAutoClose int
} }
func getStartPage(buffer *strings.Builder, params AppParams) { func (app *application) getStartPage() string {
buffer.WriteString(`<head> buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(`<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>`) <title>`)
buffer.WriteString(params.Title) buffer.WriteString(app.params.Title)
buffer.WriteString("</title>") buffer.WriteString("</title>")
if params.Icon != "" { if app.params.Icon != "" {
buffer.WriteString(` buffer.WriteString(`
<link rel="icon" href="`) <link rel="icon" href="`)
buffer.WriteString(params.Icon) buffer.WriteString(app.params.Icon)
buffer.WriteString(`">`) buffer.WriteString(`">`)
} }
if params.TitleColor != 0 { if app.params.TitleColor != 0 {
buffer.WriteString(` buffer.WriteString(`
<meta name="theme-color" content="`) <meta name="theme-color" content="`)
buffer.WriteString(params.TitleColor.cssString()) buffer.WriteString(app.params.TitleColor.cssString())
buffer.WriteString(`">`) buffer.WriteString(`">`)
} }
@ -87,18 +92,356 @@ func getStartPage(buffer *strings.Builder, params AppParams) {
<style>`) <style>`)
buffer.WriteString(appStyles) buffer.WriteString(appStyles)
buffer.WriteString(`</style> buffer.WriteString(`</style>
<style id="ruiAnimations"></style> <script>`)
<script src="/script.js"></script> buffer.WriteString(defaultScripts)
buffer.WriteString(`</script>
</head> </head>
<body id="body" onkeydown="keyDownEvent(this, event)"> <body>
<div class="ruiRoot" id="ruiRootView"></div> <div class="ruiRoot" id="ruiRootView"></div>
<div class="ruiPopupLayer" id="ruiPopupLayer" style="visibility: hidden; isolation: isolate;"></div> <div class="ruiPopupLayer" id="ruiPopupLayer" style="visibility: hidden;" onclick="clickOutsidePopup(event)"></div>
<div class="ruiTooltipLayer" id="ruiTooltipLayer" style="visibility: hidden; opacity: 0;">
<div id="ruiTooltipText" class="ruiTooltipText"></div>
<div id="ruiTooltipTopArrow" class="ruiTooltipTopArrow"></div>
<div id="ruiTooltipBottomArrow" class="ruiTooltipBottomArrow"></div>
</div>
<a id="ruiDownloader" download style="display: none;"></a> <a id="ruiDownloader" download style="display: none;"></a>
</body>`) </body>
</html>`)
return buffer.String()
}
func (app *application) Finish() {
for _, session := range app.sessions {
session.close()
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := app.server.Shutdown(ctx); err != nil {
log.Println(err.Error())
}
}
func (app *application) nextSessionID() int {
n := rand.Intn(0x7FFFFFFE) + 1
_, ok := app.sessions[n]
for ok {
n = rand.Intn(0x7FFFFFFE) + 1
_, ok = app.sessions[n]
}
return n
}
func (app *application) removeSession(id int) {
delete(app.sessions, id)
}
func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if ProtocolInDebugLog {
DebugLogF("%s %s", req.Method, req.URL.Path)
}
switch req.Method {
case "GET":
switch req.URL.Path {
case "/":
w.WriteHeader(http.StatusOK)
io.WriteString(w, app.getStartPage())
case "/ws":
if brige := CreateSocketBrige(w, req); brige != nil {
go app.socketReader(brige)
}
default:
filename := req.URL.Path[1:]
if size := len(filename); size > 0 && filename[size-1] == '/' {
filename = filename[:size-1]
}
if !serveResourceFile(filename, w, req) &&
!serveDownloadFile(filename, w, req) {
w.WriteHeader(http.StatusNotFound)
}
}
}
}
func (app *application) socketReader(brige WebBrige) {
var session Session
events := make(chan DataObject, 1024)
for {
message, ok := brige.ReadMessage()
if !ok {
events <- NewDataObject("disconnect")
return
}
if ProtocolInDebugLog {
DebugLog(message)
}
if obj := ParseDataText(message); obj != nil {
command := obj.Tag()
switch command {
case "startSession":
answer := ""
if session, answer = app.startSession(obj, events, brige); session != nil {
if !brige.WriteMessage(answer) {
return
}
session.onStart()
go sessionEventHandler(session, events, brige)
}
case "reconnect":
if sessionText, ok := obj.PropertyValue("session"); ok {
if sessionID, err := strconv.Atoi(sessionText); err == nil {
if session = app.sessions[sessionID]; session != nil {
session.setBrige(events, brige)
answer := allocStringBuilder()
defer freeStringBuilder(answer)
session.writeInitScript(answer)
if !brige.WriteMessage(answer.String()) {
return
}
session.onReconnect()
go sessionEventHandler(session, events, brige)
return
}
DebugLogF("Session #%d not exists", sessionID)
} else {
ErrorLog(`strconv.Atoi(sessionText) error: ` + err.Error())
}
} else {
ErrorLog(`"session" key not found`)
}
answer := ""
if session, answer = app.startSession(obj, events, brige); session != nil {
if !brige.WriteMessage(answer) {
return
}
session.onStart()
go sessionEventHandler(session, events, brige)
}
case "answer":
session.handleAnswer(obj)
case "imageLoaded":
session.imageManager().imageLoaded(obj, session)
case "imageError":
session.imageManager().imageLoadError(obj, session)
default:
events <- obj
}
}
}
}
func sessionEventHandler(session Session, events chan DataObject, brige WebBrige) {
for {
data := <-events
switch command := data.Tag(); command {
case "disconnect":
session.onDisconnect()
return
case "session-close":
session.onFinish()
session.App().removeSession(session.ID())
brige.Close()
case "session-pause":
session.onPause()
case "session-resume":
session.onResume()
case "root-size":
session.handleRootSize(data)
case "resize":
session.handleResize(data)
default:
session.handleViewEvent(command, data)
}
}
}
func (app *application) startSession(params DataObject, events chan DataObject, brige WebBrige) (Session, string) {
if app.createContentFunc == nil {
return nil, ""
}
session := newSession(app, app.nextSessionID(), "", params)
session.setBrige(events, brige)
if !session.setContent(app.createContentFunc(session), session) {
return nil, ""
}
app.sessions[session.ID()] = session
answer := allocStringBuilder()
defer freeStringBuilder(answer)
answer.WriteString("sessionID = '")
answer.WriteString(strconv.Itoa(session.ID()))
answer.WriteString("';\n")
session.writeInitScript(answer)
answerText := answer.String()
if ProtocolInDebugLog {
DebugLog("Start session:")
DebugLog(answerText)
}
return session, answerText
}
var apps = []*application{}
// StartApp - create the new application and start it
func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) {
app := new(application)
app.params = params
app.sessions = map[int]Session{}
app.createContentFunc = createContentFunc
apps = append(apps, app)
redirectAddr := ""
if index := strings.IndexRune(addr, ':'); index >= 0 {
redirectAddr = addr[:index] + ":80"
} else {
redirectAddr = addr + ":80"
if params.CertFile != "" && params.KeyFile != "" {
addr += ":443"
} else {
addr += ":80"
}
}
app.server = &http.Server{Addr: addr}
http.Handle("/", app)
serverRun := func(err error) {
if err != nil {
if err == http.ErrServerClosed {
log.Println(err)
} else {
log.Fatal(err)
}
}
}
if params.CertFile != "" && params.KeyFile != "" {
if params.Redirect80 {
redirectTLS := func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://"+addr+r.RequestURI, http.StatusMovedPermanently)
}
go func() {
serverRun(http.ListenAndServe(redirectAddr, http.HandlerFunc(redirectTLS)))
}()
}
serverRun(app.server.ListenAndServeTLS(params.CertFile, params.KeyFile))
} else {
serverRun(app.server.ListenAndServe())
}
}
func FinishApp() {
for _, app := range apps {
app.Finish()
}
apps = []*application{}
}
func OpenBrowser(url string) bool {
var err error
switch runtime.GOOS {
case "linux":
for _, provider := range []string{"xdg-open", "x-www-browser", "www-browser"} {
if _, err = exec.LookPath(provider); err == nil {
if exec.Command(provider, url).Start(); err == nil {
return true
}
}
}
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
return err != nil
}
type downloadFile struct {
filename string
path string
data []byte
}
var currentDownloadId = int(rand.Int31())
var downloadFiles = map[string]downloadFile{}
func (session *sessionData) startDownload(file downloadFile) {
currentDownloadId++
id := strconv.Itoa(currentDownloadId)
downloadFiles[id] = file
session.runScript(fmt.Sprintf(`startDowndload("%s", "%s")`, id, file.filename))
}
func serveDownloadFile(id string, w http.ResponseWriter, r *http.Request) bool {
if file, ok := downloadFiles[id]; ok {
delete(downloadFiles, id)
if file.data != nil {
http.ServeContent(w, r, file.filename, time.Now(), bytes.NewReader(file.data))
return true
} else if _, err := os.Stat(file.path); err == nil {
http.ServeFile(w, r, file.path)
return true
}
}
return false
}
// DownloadFile starts downloading the file on the client side.
func (session *sessionData) DownloadFile(path string) {
if _, err := os.Stat(path); err != nil {
ErrorLog(err.Error())
return
}
_, filename := filepath.Split(path)
session.startDownload(downloadFile{
filename: filename,
path: path,
data: nil,
})
}
// DownloadFileData starts downloading the file on the client side. Arguments specify the name of the downloaded file and its contents
func (session *sessionData) DownloadFileData(filename string, data []byte) {
if data == nil {
ErrorLog("Invalid download data. Must be not nil.")
return
}
session.startDownload(downloadFile{
filename: filename,
path: "",
data: data,
})
} }

View File

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

View File

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

View File

@ -19,23 +19,15 @@ type BackgroundGradientAngle struct {
} }
// NewBackgroundConicGradient creates the new background conic gradient // NewBackgroundConicGradient creates the new background conic gradient
//
// The following properties can be used:
// - "gradient" [Gradient] - Describes gradient stop points. This is a mandatory property while describing background gradients.
// - "center-x" [CenterX] - center X point of the gradient.
// - "center-y" [CenterY] - center Y point of the gradient.
// - "from" [From] - start angle position of the gradient.
// - "repeating" [Repeating] - Defines whether stop points needs to be repeated after the last one.
func NewBackgroundConicGradient(params Params) BackgroundElement { func NewBackgroundConicGradient(params Params) BackgroundElement {
result := new(backgroundConicGradient) result := new(backgroundConicGradient)
result.init() result.properties = map[string]any{}
for tag, value := range params { for tag, value := range params {
result.Set(tag, value) result.Set(tag, value)
} }
return result return result
} }
// String convert internal representation of [BackgroundGradientAngle] into a string.
func (point *BackgroundGradientAngle) String() string { func (point *BackgroundGradientAngle) String() string {
result := "black" result := "black"
if point.Color != nil { if point.Color != nil {
@ -55,6 +47,7 @@ func (point *BackgroundGradientAngle) String() string {
case AngleUnit: case AngleUnit:
result += " " + value.String() result += " " + value.String()
} }
} }
@ -65,21 +58,20 @@ func (point *BackgroundGradientAngle) color(session Session) (Color, bool) {
if point.Color != nil { if point.Color != nil {
switch color := point.Color.(type) { switch color := point.Color.(type) {
case string: case string:
if ok, constName := isConstantName(color); ok { if color != "" {
if clr, ok := session.Color(constName); ok { if color[0] == '@' {
if clr, ok := session.Color(color[1:]); ok {
return clr, true return clr, true
} }
} else if clr, ok := StringToColor(color); ok { } else {
if clr, ok := StringToColor(color); ok {
return clr, true return clr, true
} }
}
}
case Color: case Color:
return color, true return color, true
default:
if n, ok := isInt(color); ok {
return Color(n), true
}
} }
} }
return 0, false return 0, false
@ -100,8 +92,11 @@ func (point *BackgroundGradientAngle) cssString(session Session, buffer *strings
if point.Angle != nil { if point.Angle != nil {
switch value := point.Angle.(type) { switch value := point.Angle.(type) {
case string: case string:
if ok, constName := isConstantName(value); ok { if value != "" {
if value, ok = session.Constant(constName); !ok { if value[0] == '@' {
if val, ok := session.Constant(value[1:]); ok {
value = val
} else {
return return
} }
} }
@ -110,6 +105,7 @@ func (point *BackgroundGradientAngle) cssString(session Session, buffer *strings
buffer.WriteRune(' ') buffer.WriteRune(' ')
buffer.WriteString(angle.cssString()) buffer.WriteString(angle.cssString())
} }
}
case AngleUnit: case AngleUnit:
buffer.WriteRune(' ') buffer.WriteRune(' ')
@ -118,15 +114,6 @@ func (point *BackgroundGradientAngle) cssString(session Session, buffer *strings
} }
} }
func (gradient *backgroundConicGradient) init() {
gradient.backgroundElement.init()
gradient.normalize = normalizeConicGradientTag
gradient.set = backgroundConicGradientSet
gradient.supportedProperties = []PropertyName{
CenterX, CenterY, Repeating, From, Gradient,
}
}
func (gradient *backgroundConicGradient) Tag() string { func (gradient *backgroundConicGradient) Tag() string {
return "conic-gradient" return "conic-gradient"
} }
@ -139,8 +126,8 @@ func (image *backgroundConicGradient) Clone() BackgroundElement {
return result return result
} }
func normalizeConicGradientTag(tag PropertyName) PropertyName { func (gradient *backgroundConicGradient) normalizeTag(tag string) string {
tag = defaultNormalize(tag) tag = strings.ToLower(tag)
switch tag { switch tag {
case "x-center": case "x-center":
tag = CenterX tag = CenterX
@ -152,50 +139,27 @@ func normalizeConicGradientTag(tag PropertyName) PropertyName {
return tag return tag
} }
func backgroundConicGradientSet(properties Properties, tag PropertyName, value any) []PropertyName { func (gradient *backgroundConicGradient) Set(tag string, value any) bool {
tag = gradient.normalizeTag(tag)
switch tag { switch tag {
case CenterX, CenterY, Repeating, From:
return gradient.propertyList.Set(tag, value)
case Gradient: case Gradient:
switch value := value.(type) { return gradient.setGradient(value)
case string:
if value == "" {
return propertiesRemove(properties, tag)
} }
if strings.ContainsAny(value, ", ") { ErrorLogF(`"%s" property is not supported by BackgroundConicGradient`, tag)
if vector := parseGradientText(value); vector != nil { return false
properties.setRaw(Gradient, vector) }
return []PropertyName{tag}
}
} else if ok, _ := isConstantName(value); ok {
properties.setRaw(Gradient, value)
return []PropertyName{tag}
}
ErrorLogF(`Invalid conic gradient: "%s"`, value) func (gradient *backgroundConicGradient) stringToAngle(text string) (any, bool) {
if text == "" {
case []BackgroundGradientAngle: return nil, false
count := len(value) } else if text[0] == '@' {
if count < 2 { return text, true
ErrorLog("The gradient must contain at least 2 points")
return nil
} }
return StringToAngleUnit(text)
for i, point := range value {
if point.Color == nil {
ErrorLogF("Invalid %d element of the conic gradient: Color is nil", i)
return nil
}
}
properties.setRaw(Gradient, value)
return []PropertyName{tag}
default:
notCompatibleType(tag, value)
}
return nil
}
return propertiesSet(properties, tag, value)
} }
func (gradient *backgroundConicGradient) stringToGradientPoint(text string) (BackgroundGradientAngle, bool) { func (gradient *backgroundConicGradient) stringToGradientPoint(text string) (BackgroundGradientAngle, bool) {
@ -214,7 +178,7 @@ func (gradient *backgroundConicGradient) stringToGradientPoint(text string) (Bac
return result, false return result, false
} }
if ok, _ := isConstantName(colorText); ok { if colorText[0] == '@' {
result.Color = colorText result.Color = colorText
} else if color, ok := StringToColor(colorText); ok { } else if color, ok := StringToColor(colorText); ok {
result.Color = color result.Color = color
@ -223,9 +187,7 @@ func (gradient *backgroundConicGradient) stringToGradientPoint(text string) (Bac
} }
if pointText != "" { if pointText != "" {
if ok, _ := isConstantName(pointText); ok { if angle, ok := gradient.stringToAngle(pointText); ok {
result.Angle = pointText
} else if angle, ok := StringToAngleUnit(text); ok {
result.Angle = angle result.Angle = angle
} else { } else {
return result, false return result, false
@ -247,12 +209,63 @@ func (gradient *backgroundConicGradient) parseGradientText(value string) []Backg
for i, element := range elements { for i, element := range elements {
var ok bool var ok bool
if vector[i], ok = gradient.stringToGradientPoint(strings.Trim(element, " ")); !ok { if vector[i], ok = gradient.stringToGradientPoint(strings.Trim(element, " ")); !ok {
ErrorLogF(`Invalid %d element of the conic gradient: "%s"`, i, element) ErrorLogF(`Ivalid %d element of the conic gradient: "%s"`, i, element)
return nil return nil
} }
} }
return vector return vector
} }
func (gradient *backgroundConicGradient) setGradient(value any) bool {
if value == nil {
delete(gradient.properties, Gradient)
return true
}
switch value := value.(type) {
case string:
if value == "" {
delete(gradient.properties, Gradient)
return true
}
if strings.Contains(value, ",") || strings.Contains(value, " ") {
if vector := gradient.parseGradientText(value); vector != nil {
gradient.properties[Gradient] = vector
return true
}
return false
} else if value[0] == '@' {
gradient.properties[Gradient] = value
return true
}
ErrorLogF(`Ivalid conic gradient: "%s"`, value)
return false
case []BackgroundGradientAngle:
count := len(value)
if count < 2 {
ErrorLog("The gradient must contain at least 2 points")
return false
}
for i, point := range value {
if point.Color == nil {
ErrorLogF("Ivalid %d element of the conic gradient: Color is nil", i)
return false
}
}
gradient.properties[Gradient] = value
return true
}
return false
}
func (gradient *backgroundConicGradient) Get(tag string) any {
return gradient.backgroundElement.Get(gradient.normalizeTag(tag))
}
func (gradient *backgroundConicGradient) cssStyle(session Session) string { func (gradient *backgroundConicGradient) cssStyle(session Session) string {
points := []BackgroundGradientAngle{} points := []BackgroundGradientAngle{}
@ -323,16 +336,3 @@ func (gradient *backgroundConicGradient) cssStyle(session Session) string {
return buffer.String() return buffer.String()
} }
func (gradient *backgroundConicGradient) writeString(buffer *strings.Builder, indent string) {
gradient.writeToBuffer(buffer, indent, gradient.Tag(), []PropertyName{
Gradient,
CenterX,
CenterY,
Repeating,
})
}
func (gradient *backgroundConicGradient) String() string {
return runStringWriter(gradient)
}

612
backgroundGradient.go Normal file
View File

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

View File

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

View File

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

View File

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

600
border.go
View File

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

245
bounds.go
View File

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

View File

@ -1,86 +1,36 @@
package rui package rui
import "strings" // Button - button view
// Button represent a Button view
type Button interface { type Button interface {
ListLayout CustomView
} }
type buttonData struct { type buttonData struct {
listLayoutData CustomViewData
} }
// NewButton create new Button object and return it // NewButton create new Button object and return it
func NewButton(session Session, params Params) Button { func NewButton(session Session, params Params) Button {
button := new(buttonData) button := new(buttonData)
button.init(session) InitCustomView(button, "Button", session, params)
setInitParams(button, params)
return button return button
} }
func newButton(session Session) View { func newButton(session Session) View {
return new(buttonData) return NewButton(session, nil)
} }
func (button *buttonData) init(session Session) { func (button *buttonData) CreateSuperView(session Session) View {
button.listLayoutData.init(session) return NewListLayout(session, Params{
button.tag = "Button" Semantics: ButtonSemantics,
button.systemClass = "ruiButton" Style: "ruiButton",
button.setRaw(Style, "ruiEnabledButton") StyleDisabled: "ruiDisabledButton",
button.setRaw(StyleDisabled, "ruiDisabledButton") HorizontalAlign: CenterAlign,
button.setRaw(Semantics, ButtonSemantics) VerticalAlign: CenterAlign,
button.setRaw(TabIndex, 0) Orientation: StartToEndOrientation,
})
} }
func (button *buttonData) Focusable() bool { func (button *buttonData) Focusable() bool {
return true return true
} }
func (button *buttonData) htmlSubviews(self View, buffer *strings.Builder) {
if button.views != nil {
for _, view := range button.views {
view.addToCSSStyle(map[string]string{`flex`: `0 0 auto`})
viewHTML(view, buffer, "")
}
}
}
// GetButtonVerticalAlign returns the vertical align of a Button subview:
// TopAlign (0), BottomAlign (1), CenterAlign (2), or StretchAlign (3)
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetButtonVerticalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, VerticalAlign, CenterAlign, false)
}
// GetButtonHorizontalAlign returns the vertical align of a Button subview:
// LeftAlign (0), RightAlign (1), CenterAlign (2), or StretchAlign (3)
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetButtonHorizontalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, HorizontalAlign, CenterAlign, false)
}
// GetButtonOrientation returns the orientation of a Button subview:
// TopDownOrientation (0), StartToEndOrientation (1), BottomUpOrientation (2), or EndToStartOrientation (3)
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetButtonOrientation(view View, subviewID ...string) int {
if view = getSubview(view, subviewID); view != nil {
if orientation, ok := valueToOrientation(view.Get(Orientation), view.Session()); ok {
return orientation
}
if value := valueFromStyle(view, Orientation); value != nil {
if orientation, ok := valueToOrientation(value, view.Session()); ok {
return orientation
}
}
}
return StartToEndOrientation
}

821
canvas.go

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +1,21 @@
package rui package rui
import "reflect" import "strings"
// DrawFunction is the constant for "draw-function" property tag. // DrawFunction is the constant for the "draw-function" property tag.
// // The "draw-function" property sets the draw function of CanvasView.
// Used by `CanvasView`. // The function should have the following format: func(Canvas)
// Property sets the draw function of `CanvasView`. const DrawFunction = "draw-function"
//
// Supported types: `func(Canvas)`.
const DrawFunction PropertyName = "draw-function"
// CanvasView interface of a custom draw view // CanvasView interface of a custom draw view
type CanvasView interface { type CanvasView interface {
View View
// Redraw forces CanvasView to redraw its content
Redraw() Redraw()
} }
type canvasViewData struct { type canvasViewData struct {
viewData viewData
drawer func(Canvas)
} }
// NewCanvasView creates the new custom draw view // NewCanvasView creates the new custom draw view
@ -31,21 +27,21 @@ func NewCanvasView(session Session, params Params) CanvasView {
} }
func newCanvasView(session Session) View { func newCanvasView(session Session) View {
return new(canvasViewData) return NewCanvasView(session, nil)
} }
// Init initialize fields of ViewsContainer by default values // Init initialize fields of ViewsContainer by default values
func (canvasView *canvasViewData) init(session Session) { func (canvasView *canvasViewData) init(session Session) {
canvasView.viewData.init(session) canvasView.viewData.init(session)
canvasView.tag = "CanvasView" canvasView.tag = "CanvasView"
canvasView.normalize = normalizeCanvasViewTag
canvasView.set = canvasView.setFunc
canvasView.remove = canvasView.removeFunc
canvasView.changed = canvasView.propertyChanged
} }
func normalizeCanvasViewTag(tag PropertyName) PropertyName { func (canvasView *canvasViewData) String() string {
tag = defaultNormalize(tag) return getViewString(canvasView)
}
func (canvasView *canvasViewData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag { switch tag {
case "draw-func": case "draw-func":
tag = DrawFunction tag = DrawFunction
@ -53,81 +49,66 @@ func normalizeCanvasViewTag(tag PropertyName) PropertyName {
return tag return tag
} }
func (canvasView *canvasViewData) removeFunc(tag PropertyName) []PropertyName { func (canvasView *canvasViewData) Remove(tag string) {
if tag == DrawFunction { canvasView.remove(canvasView.normalizeTag(tag))
if canvasView.getRaw(DrawFunction) != nil {
canvasView.setRaw(DrawFunction, nil)
//canvasView.Redraw()
return []PropertyName{DrawFunction}
}
return []PropertyName{}
}
return canvasView.viewData.removeFunc(tag)
} }
func (canvasView *canvasViewData) setFunc(tag PropertyName, value any) []PropertyName { func (canvasView *canvasViewData) remove(tag string) {
if tag == DrawFunction {
switch value := value.(type) {
case func(Canvas):
canvasView.setRaw(DrawFunction, value)
case string:
canvasView.setRaw(DrawFunction, value)
default:
notCompatibleType(tag, value)
return nil
}
return []PropertyName{DrawFunction}
}
return canvasView.viewData.setFunc(tag, value)
}
func (canvasView *canvasViewData) propertyChanged(tag PropertyName) {
if tag == DrawFunction { if tag == DrawFunction {
canvasView.drawer = nil
canvasView.Redraw() canvasView.Redraw()
canvasView.propertyChangedEvent(tag)
} else { } else {
canvasView.viewData.propertyChanged(tag) canvasView.viewData.remove(tag)
} }
} }
func (canvasView *canvasViewData) Set(tag string, value any) bool {
return canvasView.set(canvasView.normalizeTag(tag), value)
}
func (canvasView *canvasViewData) set(tag string, value any) bool {
if tag == DrawFunction {
if value == nil {
canvasView.drawer = nil
} else if fn, ok := value.(func(Canvas)); ok {
canvasView.drawer = fn
} else {
notCompatibleType(tag, value)
return false
}
canvasView.Redraw()
canvasView.propertyChangedEvent(tag)
return true
}
return canvasView.viewData.set(tag, value)
}
func (canvasView *canvasViewData) Get(tag string) any {
return canvasView.get(canvasView.normalizeTag(tag))
}
func (canvasView *canvasViewData) get(tag string) any {
if tag == DrawFunction {
return canvasView.drawer
}
return canvasView.viewData.get(tag)
}
func (canvasView *canvasViewData) htmlTag() string { func (canvasView *canvasViewData) htmlTag() string {
return "canvas" return "canvas"
} }
func (canvasView *canvasViewData) Redraw() { func (canvasView *canvasViewData) Redraw() {
if canvasView.drawer != nil {
canvas := newCanvas(canvasView) canvas := newCanvas(canvasView)
canvas.ClearRect(0, 0, canvasView.frame.Width, canvasView.frame.Height) canvas.ClearRect(0, 0, canvasView.frame.Width, canvasView.frame.Height)
if value := canvasView.getRaw(DrawFunction); value != nil { if canvasView.drawer != nil {
switch drawer := value.(type) { canvasView.drawer(canvas)
case func(Canvas):
drawer(canvas)
case string:
bind := canvasView.binding()
if bind == nil {
ErrorLogF(`There is no a binding object for call "%s"`, drawer)
break
} }
canvasView.session.runScript(canvas.finishDraw())
val := reflect.ValueOf(bind)
method := val.MethodByName(drawer)
if !method.IsValid() {
ErrorLogF(`The "%s" method is not valid`, drawer)
break
} }
methodType := method.Type()
if methodType.NumIn() == 1 && equalType(methodType.In(0), reflect.TypeOf(canvas)) {
method.Call([]reflect.Value{reflect.ValueOf(canvas)})
} else {
ErrorLogF(`Unsupported prototype of "%s" method`, drawer)
}
}
}
canvas.finishDraw()
} }
func (canvasView *canvasViewData) onResize(self View, x, y, width, height float64) { func (canvasView *canvasViewData) onResize(self View, x, y, width, height float64) {

View File

@ -1,163 +1,199 @@
package rui package rui
import ( import (
"fmt"
"strings" "strings"
) )
// CheckboxChangedEvent is the constant for "checkbox-event" property tag. // CheckboxChangedEvent is the constant for "checkbox-event" property tag.
// // The "checkbox-event" event occurs when the checkbox becomes checked/unchecked.
// Used by `Checkbox`. // The main listener format: func(Checkbox, bool), where the second argument is the checkbox state.
// Event occurs when the checkbox becomes checked/unchecked. const CheckboxChangedEvent = "checkbox-event"
//
// General listener format:
//
// func(checkbox rui.Checkbox, checked bool)
//
// where:
// - checkbox - Interface of a checkbox which generated this event,
// - checked - Checkbox state.
//
// Allowed listener formats:
//
// func(checkbox rui.Checkbox)
// func(checked bool)
// func()
const CheckboxChangedEvent PropertyName = "checkbox-event"
// Checkbox represent a Checkbox view // Checkbox - checkbox view
type Checkbox interface { type Checkbox interface {
ViewsContainer ViewsContainer
} }
type checkboxData struct { type checkboxData struct {
viewsContainerData viewsContainerData
checkedListeners []func(Checkbox, bool)
} }
// NewCheckbox create new Checkbox object and return it // NewCheckbox create new Checkbox object and return it
func NewCheckbox(session Session, params Params) Checkbox { func NewCheckbox(session Session, params Params) Checkbox {
view := new(checkboxData) view := new(checkboxData)
view.init(session) view.init(session)
setInitParams(view, Params{
ClickEvent: checkboxClickListener,
KeyDownEvent: checkboxKeyListener,
})
setInitParams(view, params) setInitParams(view, params)
return view return view
} }
func newCheckbox(session Session) View { func newCheckbox(session Session) View {
return new(checkboxData) return NewCheckbox(session, nil)
} }
func (button *checkboxData) init(session Session) { func (button *checkboxData) init(session Session) {
button.viewsContainerData.init(session) button.viewsContainerData.init(session)
button.tag = "Checkbox" button.tag = "Checkbox"
button.systemClass = "ruiGridLayout ruiCheckbox" button.systemClass = "ruiGridLayout ruiCheckbox"
button.get = button.getFunc button.checkedListeners = []func(Checkbox, bool){}
button.set = button.setFunc }
button.remove = button.removeFunc
button.changed = button.propertyChanged
button.setRaw(ClickEvent, []oneArgListener[View, MouseEvent]{newOneArgListenerVE(checkboxClickListener)}) func (button *checkboxData) String() string {
button.setRaw(KeyDownEvent, []oneArgListener[View, KeyEvent]{newOneArgListenerVE(checkboxKeyListener)}) return getViewString(button)
} }
func (button *checkboxData) Focusable() bool { func (button *checkboxData) Focusable() bool {
return true return true
} }
func (button *checkboxData) propertyChanged(tag PropertyName) { func (button *checkboxData) Get(tag string) any {
switch strings.ToLower(tag) {
case CheckboxChangedEvent:
return button.checkedListeners
}
return button.viewsContainerData.Get(tag)
}
func (button *checkboxData) Set(tag string, value any) bool {
return button.set(tag, value)
}
func (button *checkboxData) set(tag string, value any) bool {
switch tag { switch tag {
case CheckboxChangedEvent:
if !button.setChangedListener(value) {
notCompatibleType(tag, value)
return false
}
case Checked: case Checked:
session := button.Session() oldChecked := button.checked()
checked := IsCheckboxChecked(button) if !button.setBoolProperty(Checked, value) {
if listeners := getOneArgEventListeners[Checkbox, bool](button, nil, CheckboxChangedEvent); len(listeners) > 0 { return false
for _, listener := range listeners {
listener.Run(button, checked)
} }
if button.created {
checked := button.checked()
if checked != oldChecked {
button.changedCheckboxState(checked)
}
}
case CheckboxHorizontalAlign, CheckboxVerticalAlign:
if !button.setEnumProperty(tag, value, enumProperties[tag].values) {
return false
}
if button.created {
htmlID := button.htmlID()
updateCSSStyle(htmlID, button.session)
updateInnerHTML(htmlID, button.session)
}
case VerticalAlign:
if !button.setEnumProperty(tag, value, enumProperties[tag].values) {
return false
}
if button.created {
updateCSSProperty(button.htmlID()+"content", "align-items", button.cssVerticalAlign(), button.session)
}
case HorizontalAlign:
if !button.setEnumProperty(tag, value, enumProperties[tag].values) {
return false
}
if button.created {
updateCSSProperty(button.htmlID()+"content", "justify-items", button.cssHorizontalAlign(), button.session)
}
case CellVerticalAlign, CellHorizontalAlign, CellWidth, CellHeight:
return false
default:
return button.viewsContainerData.set(tag, value)
}
button.propertyChangedEvent(tag)
return true
}
func (button *checkboxData) Remove(tag string) {
button.remove(strings.ToLower(tag))
}
func (button *checkboxData) remove(tag string) {
switch tag {
case ClickEvent:
if !button.viewsContainerData.set(ClickEvent, checkboxClickListener) {
delete(button.properties, tag)
}
case KeyDownEvent:
if !button.viewsContainerData.set(KeyDownEvent, checkboxKeyListener) {
delete(button.properties, tag)
}
case CheckboxChangedEvent:
if len(button.checkedListeners) > 0 {
button.checkedListeners = []func(Checkbox, bool){}
}
case Checked:
oldChecked := button.checked()
delete(button.properties, tag)
if button.created && oldChecked {
button.changedCheckboxState(false)
}
case CheckboxHorizontalAlign, CheckboxVerticalAlign:
delete(button.properties, tag)
if button.created {
htmlID := button.htmlID()
updateCSSStyle(htmlID, button.session)
updateInnerHTML(htmlID, button.session)
}
case VerticalAlign:
delete(button.properties, tag)
if button.created {
updateCSSProperty(button.htmlID()+"content", "align-items", button.cssVerticalAlign(), button.session)
}
case HorizontalAlign:
delete(button.properties, tag)
if button.created {
updateCSSProperty(button.htmlID()+"content", "justify-items", button.cssHorizontalAlign(), button.session)
}
default:
button.viewsContainerData.remove(tag)
return
}
button.propertyChangedEvent(tag)
}
func (button *checkboxData) checked() bool {
checked, _ := boolProperty(button, Checked, button.Session())
return checked
}
func (button *checkboxData) changedCheckboxState(state bool) {
for _, listener := range button.checkedListeners {
listener(button, state)
} }
buffer := allocStringBuilder() buffer := allocStringBuilder()
defer freeStringBuilder(buffer) defer freeStringBuilder(buffer)
checkboxHtml(button, buffer, checked) button.htmlCheckbox(buffer, state)
session.updateInnerHTML(button.htmlID()+"checkbox", buffer.String()) button.Session().runScript(fmt.Sprintf(`updateInnerHTML('%v', '%v');`, button.htmlID()+"checkbox", buffer.String()))
case CheckboxHorizontalAlign, CheckboxVerticalAlign:
htmlID := button.htmlID()
session := button.Session()
updateCSSStyle(htmlID, session)
updateInnerHTML(htmlID, session)
case VerticalAlign:
button.Session().updateCSSProperty(button.htmlID()+"content", "align-items", checkboxVerticalAlignCSS(button))
case HorizontalAlign:
button.Session().updateCSSProperty(button.htmlID()+"content", "justify-items", checkboxHorizontalAlignCSS(button))
case AccentColor:
updateInnerHTML(button.htmlID(), button.Session())
default:
button.viewsContainerData.propertyChanged(tag)
}
} }
func (button *checkboxData) getFunc(tag PropertyName) any { func checkboxClickListener(view View) {
switch tag {
case CheckboxChangedEvent:
if listeners := getOneArgEventRawListeners[Checkbox, bool](button, nil, tag); len(listeners) > 0 {
return listeners
}
return nil
}
return button.viewData.getFunc(tag)
}
func (button *checkboxData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case ClickEvent:
if listeners, ok := valueToOneArgEventListeners[View, MouseEvent](value); ok && listeners != nil {
listeners = append(listeners, newOneArgListenerVE(checkboxClickListener))
button.setRaw(tag, listeners)
return []PropertyName{tag}
}
return nil
case KeyDownEvent:
if listeners, ok := valueToOneArgEventListeners[View, KeyEvent](value); ok && listeners != nil {
listeners = append(listeners, newOneArgListenerVE(checkboxKeyListener))
button.setRaw(tag, listeners)
return []PropertyName{tag}
}
return nil
case CheckboxChangedEvent:
return setOneArgEventListener[Checkbox, bool](button, tag, value)
case Checked:
return setBoolProperty(button, Checked, value)
case CellVerticalAlign, CellHorizontalAlign, CellWidth, CellHeight:
ErrorLogF(`"%s" property is not compatible with the BoundsProperty`, string(tag))
return nil
}
return button.viewsContainerData.setFunc(tag, value)
}
func (button *checkboxData) removeFunc(tag PropertyName) []PropertyName {
switch tag {
case ClickEvent:
button.setRaw(ClickEvent, []oneArgListener[View, MouseEvent]{newOneArgListenerVE(checkboxClickListener)})
return []PropertyName{ClickEvent}
case KeyDownEvent:
button.setRaw(KeyDownEvent, []oneArgListener[View, KeyEvent]{newOneArgListenerVE(checkboxKeyListener)})
return []PropertyName{ClickEvent}
}
return button.viewsContainerData.removeFunc(tag)
}
func checkboxClickListener(view View, _ MouseEvent) {
view.Set(Checked, !IsCheckboxChecked(view)) view.Set(Checked, !IsCheckboxChecked(view))
BlurView(view) BlurView(view)
} }
@ -169,6 +205,17 @@ func checkboxKeyListener(view View, event KeyEvent) {
} }
} }
func (button *checkboxData) setChangedListener(value any) bool {
listeners, ok := valueToEventListeners[Checkbox, bool](value)
if !ok {
return false
} else if listeners == nil {
listeners = []func(Checkbox, bool){}
}
button.checkedListeners = listeners
return true
}
func (button *checkboxData) cssStyle(self View, builder cssBuilder) { func (button *checkboxData) cssStyle(self View, builder cssBuilder) {
session := button.Session() session := button.Session()
vAlign := GetCheckboxVerticalAlign(button) vAlign := GetCheckboxVerticalAlign(button)
@ -195,11 +242,10 @@ func (button *checkboxData) cssStyle(self View, builder cssBuilder) {
builder.add("align-items", "stretch") builder.add("align-items", "stretch")
builder.add("justify-items", "stretch") builder.add("justify-items", "stretch")
button.viewData.cssStyle(self, builder) button.viewsContainerData.cssStyle(self, builder)
} }
func checkboxHtml(button View, buffer *strings.Builder, checked bool) (int, int) { func (button *checkboxData) htmlCheckbox(buffer *strings.Builder, checked bool) (int, int) {
//func (button *checkboxData) htmlCheckbox(buffer *strings.Builder, checked bool) (int, int) {
vAlign := GetCheckboxVerticalAlign(button) vAlign := GetCheckboxVerticalAlign(button)
hAlign := GetCheckboxHorizontalAlign(button) hAlign := GetCheckboxHorizontalAlign(button)
@ -233,16 +279,10 @@ func checkboxHtml(button View, buffer *strings.Builder, checked bool) (int, int)
} }
buffer.WriteString(`">`) buffer.WriteString(`">`)
accentColor := Color(0)
if color := GetAccentColor(button, ""); color != 0 {
accentColor = color
}
if checked { if checked {
buffer.WriteString(button.Session().checkboxOnImage(accentColor)) buffer.WriteString(button.Session().checkboxOnImage())
} else { } else {
buffer.WriteString(button.Session().checkboxOffImage(accentColor)) buffer.WriteString(button.Session().checkboxOffImage())
} }
buffer.WriteString(`</div>`) buffer.WriteString(`</div>`)
@ -251,7 +291,7 @@ func checkboxHtml(button View, buffer *strings.Builder, checked bool) (int, int)
func (button *checkboxData) htmlSubviews(self View, buffer *strings.Builder) { func (button *checkboxData) htmlSubviews(self View, buffer *strings.Builder) {
vCheckboxAlign, hCheckboxAlign := checkboxHtml(button, buffer, IsCheckboxChecked(button)) vCheckboxAlign, hCheckboxAlign := button.htmlCheckbox(buffer, IsCheckboxChecked(button))
buffer.WriteString(`<div id="`) buffer.WriteString(`<div id="`)
buffer.WriteString(button.htmlID()) buffer.WriteString(button.htmlID())
@ -269,11 +309,11 @@ func (button *checkboxData) htmlSubviews(self View, buffer *strings.Builder) {
} }
buffer.WriteString(" align-items: ") buffer.WriteString(" align-items: ")
buffer.WriteString(checkboxVerticalAlignCSS(button)) buffer.WriteString(button.cssVerticalAlign())
buffer.WriteRune(';') buffer.WriteRune(';')
buffer.WriteString(" justify-items: ") buffer.WriteString(" justify-items: ")
buffer.WriteString(checkboxHorizontalAlignCSS(button)) buffer.WriteString(button.cssHorizontalAlign())
buffer.WriteRune(';') buffer.WriteRune(';')
buffer.WriteString(`">`) buffer.WriteString(`">`)
@ -281,8 +321,8 @@ func (button *checkboxData) htmlSubviews(self View, buffer *strings.Builder) {
buffer.WriteString(`</div>`) buffer.WriteString(`</div>`)
} }
func checkboxHorizontalAlignCSS(view View) string { func (button *checkboxData) cssHorizontalAlign() string {
align := GetHorizontalAlign(view) align := GetHorizontalAlign(button)
values := enumProperties[CellHorizontalAlign].cssValues values := enumProperties[CellHorizontalAlign].cssValues
if align >= 0 && align < len(values) { if align >= 0 && align < len(values) {
return values[align] return values[align]
@ -290,8 +330,8 @@ func checkboxHorizontalAlignCSS(view View) string {
return values[0] return values[0]
} }
func checkboxVerticalAlignCSS(view View) string { func (button *checkboxData) cssVerticalAlign() string {
align := GetVerticalAlign(view) align := GetVerticalAlign(button)
values := enumProperties[CellVerticalAlign].cssValues values := enumProperties[CellVerticalAlign].cssValues
if align >= 0 && align < len(values) { if align >= 0 && align < len(values) {
return values[align] return values[align]
@ -300,41 +340,19 @@ func checkboxVerticalAlignCSS(view View) string {
} }
// IsCheckboxChecked returns true if the Checkbox is checked, false otherwise. // IsCheckboxChecked returns true if the Checkbox is checked, false otherwise.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func IsCheckboxChecked(view View, subviewID ...string) bool { func IsCheckboxChecked(view View, subviewID ...string) bool {
return boolStyledProperty(view, subviewID, Checked, false) return boolStyledProperty(view, subviewID, Checked, false)
} }
// GetCheckboxVerticalAlign return the vertical align of a Checkbox subview: TopAlign (0), BottomAlign (1), CenterAlign (2) // GetCheckboxVerticalAlign return the vertical align of a Checkbox subview: TopAlign (0), BottomAlign (1), CenterAlign (2)
// // If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetCheckboxVerticalAlign(view View, subviewID ...string) int { func GetCheckboxVerticalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, CheckboxVerticalAlign, LeftAlign, false) return enumStyledProperty(view, subviewID, CheckboxVerticalAlign, LeftAlign, false)
} }
// GetCheckboxHorizontalAlign return the vertical align of a Checkbox subview: LeftAlign (0), RightAlign (1), CenterAlign (2) // GetCheckboxHorizontalAlign return the vertical align of a Checkbox subview: LeftAlign (0), RightAlign (1), CenterAlign (2)
// // If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetCheckboxHorizontalAlign(view View, subviewID ...string) int { func GetCheckboxHorizontalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, CheckboxHorizontalAlign, TopAlign, false) return enumStyledProperty(view, subviewID, CheckboxHorizontalAlign, TopAlign, false)
} }
// GetCheckboxChangedListeners returns the CheckboxChangedListener list of an Checkbox subview.
// If there are no listeners then the empty list is returned.
//
// Result elements can be of the following types:
// - func(Checkbox, bool),
// - func(Checkbox),
// - func(bool),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetCheckboxChangedListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[Checkbox, bool](view, subviewID, CheckboxChangedEvent)
}

View File

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

View File

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

View File

@ -2,7 +2,6 @@ package rui
import "sort" import "sort"
// A set of predefined colors used in the library
const ( const (
// Black color constant // Black color constant
Black Color = 0xff000000 Black Color = 0xff000000
@ -56,8 +55,8 @@ const (
BlueViolet Color = 0xff8a2be2 BlueViolet Color = 0xff8a2be2
// Brown color constant // Brown color constant
Brown Color = 0xffa52a2a Brown Color = 0xffa52a2a
// BurlyWood color constant // Burlywood color constant
BurlyWood Color = 0xffdeb887 Burlywood Color = 0xffdeb887
// CadetBlue color constant // CadetBlue color constant
CadetBlue Color = 0xff5f9ea0 CadetBlue Color = 0xff5f9ea0
// Chartreuse color constant // Chartreuse color constant
@ -68,8 +67,8 @@ const (
Coral Color = 0xffff7f50 Coral Color = 0xffff7f50
// CornflowerBlue color constant // CornflowerBlue color constant
CornflowerBlue Color = 0xff6495ed CornflowerBlue Color = 0xff6495ed
// CornSilk color constant // Cornsilk color constant
CornSilk Color = 0xfffff8dc Cornsilk Color = 0xfffff8dc
// Crimson color constant // Crimson color constant
Crimson Color = 0xffdc143c Crimson Color = 0xffdc143c
// Cyan color constant // Cyan color constant
@ -106,8 +105,8 @@ const (
DarkSlateBlue Color = 0xff483d8b DarkSlateBlue Color = 0xff483d8b
// DarkSlateGray color constant // DarkSlateGray color constant
DarkSlateGray Color = 0xff2f4f4f DarkSlateGray Color = 0xff2f4f4f
// DarkSlateGrey color constant // Darkslategrey color constant
DarkSlateGrey Color = 0xff2f4f4f Darkslategrey Color = 0xff2f4f4f
// DarkTurquoise color constant // DarkTurquoise color constant
DarkTurquoise Color = 0xff00ced1 DarkTurquoise Color = 0xff00ced1
// DarkViolet color constant // DarkViolet color constant
@ -136,8 +135,8 @@ const (
Gold Color = 0xffffd700 Gold Color = 0xffffd700
// GoldenRod color constant // GoldenRod color constant
GoldenRod Color = 0xffdaa520 GoldenRod Color = 0xffdaa520
// GreenYellow color constant // GreenyEllow color constant
GreenYellow Color = 0xffadff2f GreenyEllow Color = 0xffadff2f
// Grey color constant // Grey color constant
Grey Color = 0xff808080 Grey Color = 0xff808080
// Honeydew color constant // Honeydew color constant
@ -294,8 +293,8 @@ const (
Violet Color = 0xffee82ee Violet Color = 0xffee82ee
// Wheat color constant // Wheat color constant
Wheat Color = 0xfff5deb3 Wheat Color = 0xfff5deb3
// WhiteSmoke color constant // Whitesmoke color constant
WhiteSmoke Color = 0xfff5f5f5 Whitesmoke Color = 0xfff5f5f5
// YellowGreen color constant // YellowGreen color constant
YellowGreen Color = 0xff9acd32 YellowGreen Color = 0xff9acd32
) )
@ -450,12 +449,8 @@ var colorConstants = map[string]Color{
"yellowgreen": 0xff9acd32, "yellowgreen": 0xff9acd32,
} }
// NamedColor make a relation between color and its name
type NamedColor struct { type NamedColor struct {
// Name of a color
Name string Name string
// Color value
Color Color Color Color
} }

View File

@ -1,51 +1,23 @@
package rui package rui
import ( import (
"fmt"
"strings" "strings"
) )
// Constants for [ColorPicker] specific properties and events.
const ( const (
// ColorChangedEvent is the constant for "color-changed" property tag. ColorChangedEvent = "color-changed"
// ColorPickerValue = "color-picker-value"
// Used by `ColorPicker`.
// Event generated when color picker value has been changed.
//
// General listener format:
// func(picker rui.ColorPicker, newColor, oldColor rui.Color)
//
// where:
// - picker - Interface of a color picker which generated this event,
// - newColor - New color value,
// - oldColor - Old color value.
//
// Allowed listener formats:
// func(picker rui.ColorPicker, newColor rui.Color)
// func(newColor, oldColor rui.Color)
// func(newColor rui.Color)
// func(picker rui.ColorPicker)
// func()
ColorChangedEvent PropertyName = "color-changed"
// ColorPickerValue is the constant for "color-picker-value" property tag.
//
// Used by `ColorPicker`.
// Define current color picker value.
//
// Supported types: `Color`, `string`.
//
// Internal type is `Color`, other types converted to it during assignment.
// See `Color` description for more details.
ColorPickerValue PropertyName = "color-picker-value"
) )
// ColorPicker represent a ColorPicker view // ColorPicker - ColorPicker view
type ColorPicker interface { type ColorPicker interface {
View View
} }
type colorPickerData struct { type colorPickerData struct {
viewData viewData
colorChangedListeners []func(ColorPicker, Color)
} }
// NewColorPicker create new ColorPicker object and return it // NewColorPicker create new ColorPicker object and return it
@ -57,94 +29,118 @@ func NewColorPicker(session Session, params Params) ColorPicker {
} }
func newColorPicker(session Session) View { func newColorPicker(session Session) View {
return new(colorPickerData) return NewColorPicker(session, nil)
} }
func (picker *colorPickerData) init(session Session) { func (picker *colorPickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "ColorPicker" picker.tag = "ColorPicker"
picker.hasHtmlDisabled = true picker.colorChangedListeners = []func(ColorPicker, Color){}
picker.properties[Padding] = Px(0) picker.properties[Padding] = Px(0)
picker.normalize = normalizeColorPickerTag
picker.get = picker.getFunc
picker.set = picker.setFunc
picker.changed = picker.propertyChanged
} }
func normalizeColorPickerTag(tag PropertyName) PropertyName { func (picker *colorPickerData) String() string {
tag = defaultNormalize(tag) return getViewString(picker)
}
func (picker *colorPickerData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag { switch tag {
case Value, ColorTag: case Value, ColorTag:
return ColorPickerValue return ColorPickerValue
} }
return normalizeDataListTag(tag) return tag
} }
func (picker *colorPickerData) getFunc(tag PropertyName) any { func (picker *colorPickerData) Remove(tag string) {
switch tag { picker.remove(picker.normalizeTag(tag))
case ColorChangedEvent:
if listeners := getTwoArgEventRawListeners[ColorPicker, Color](picker, nil, tag); len(listeners) > 0 {
return listeners
}
return nil
}
return picker.viewData.getFunc(tag)
} }
func (picker *colorPickerData) setFunc(tag PropertyName, value any) []PropertyName { func (picker *colorPickerData) remove(tag string) {
switch tag { switch tag {
case ColorChangedEvent: case ColorChangedEvent:
return setTwoArgEventListener[ColorPicker, Color](picker, tag, value) if len(picker.colorChangedListeners) > 0 {
picker.colorChangedListeners = []func(ColorPicker, Color){}
picker.propertyChangedEvent(tag)
}
case ColorPickerValue: case ColorPickerValue:
oldColor := GetColorPickerValue(picker) oldColor := GetColorPickerValue(picker)
result := setColorProperty(picker, ColorPickerValue, value) delete(picker.properties, ColorPickerValue)
if result != nil { picker.colorChanged(oldColor)
picker.setRaw("old-color", oldColor)
}
return result
case DataList: default:
return setDataList(picker, value, "") picker.viewData.remove(tag)
} }
return picker.viewData.setFunc(tag, value)
} }
func (picker *colorPickerData) propertyChanged(tag PropertyName) { func (picker *colorPickerData) Set(tag string, value any) bool {
switch tag { return picker.set(picker.normalizeTag(tag), value)
case ColorPickerValue: }
color := GetColorPickerValue(picker)
picker.Session().callFunc("setInputValue", picker.htmlID(), color.rgbString())
if listeners := getTwoArgEventListeners[ColorPicker, Color](picker, nil, ColorChangedEvent); len(listeners) > 0 { func (picker *colorPickerData) set(tag string, value any) bool {
oldColor := Color(0) if value == nil {
if value := picker.getRaw("old-color"); value != nil { picker.remove(tag)
oldColor = value.(Color) return true
} }
for _, listener := range listeners {
listener.Run(picker, color, oldColor) switch tag {
case ColorChangedEvent:
listeners, ok := valueToEventListeners[ColorPicker, Color](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(ColorPicker, Color){}
} }
picker.colorChangedListeners = listeners
picker.propertyChangedEvent(tag)
return true
case ColorPickerValue:
oldColor := GetColorPickerValue(picker)
if picker.setColorProperty(ColorPickerValue, value) {
picker.colorChanged(oldColor)
return true
} }
default: default:
picker.viewData.propertyChanged(tag) return picker.viewData.set(tag, value)
} }
return false
}
func (picker *colorPickerData) colorChanged(oldColor Color) {
if newColor := GetColorPickerValue(picker); oldColor != newColor {
if picker.created {
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, picker.htmlID(), newColor.rgbString()))
}
for _, listener := range picker.colorChangedListeners {
listener(picker, newColor)
}
picker.propertyChangedEvent(ColorTag)
}
}
func (picker *colorPickerData) Get(tag string) any {
return picker.get(picker.normalizeTag(tag))
}
func (picker *colorPickerData) get(tag string) any {
switch tag {
case ColorChangedEvent:
return picker.colorChangedListeners
default:
return picker.viewData.get(tag)
}
} }
func (picker *colorPickerData) htmlTag() string { func (picker *colorPickerData) htmlTag() string {
return "input" return "input"
} }
func (picker *colorPickerData) htmlSubviews(self View, buffer *strings.Builder) {
dataListHtmlSubviews(self, buffer, func(text string, session Session) string {
text, _ = session.resolveConstants(text)
return text
})
}
func (picker *colorPickerData) htmlProperties(self View, buffer *strings.Builder) { func (picker *colorPickerData) htmlProperties(self View, buffer *strings.Builder) {
picker.viewData.htmlProperties(self, buffer) picker.viewData.htmlProperties(self, buffer)
@ -156,22 +152,26 @@ func (picker *colorPickerData) htmlProperties(self View, buffer *strings.Builder
if picker.getRaw(ClickEvent) == nil { if picker.getRaw(ClickEvent) == nil {
buffer.WriteString(` onclick="stopEventPropagation(this, event)"`) buffer.WriteString(` onclick="stopEventPropagation(this, event)"`)
} }
dataListHtmlProperties(picker, buffer)
} }
func (picker *colorPickerData) handleCommand(self View, command PropertyName, data DataObject) bool { func (picker *colorPickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
if IsDisabled(self) {
buffer.WriteString(` disabled`)
}
picker.viewData.htmlDisabledProperties(self, buffer)
}
func (picker *colorPickerData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "textChanged": case "textChanged":
if text, ok := data.PropertyValue("text"); ok { if text, ok := data.PropertyValue("text"); ok {
if color, ok := StringToColor(text); ok {
oldColor := GetColorPickerValue(picker) oldColor := GetColorPickerValue(picker)
if color, ok := StringToColor(text); ok {
picker.properties[ColorPickerValue] = color picker.properties[ColorPickerValue] = color
if color != oldColor { if color != oldColor {
for _, listener := range getTwoArgEventListeners[ColorPicker, Color](picker, nil, ColorChangedEvent) { for _, listener := range picker.colorChangedListeners {
listener.Run(picker, color, oldColor) listener(picker, color)
} }
picker.runChangeListener(ColorPickerValue)
} }
} }
} }
@ -182,15 +182,16 @@ func (picker *colorPickerData) handleCommand(self View, command PropertyName, da
} }
// GetColorPickerValue returns the value of ColorPicker subview. // GetColorPickerValue returns the value of ColorPicker subview.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetColorPickerValue(view View, subviewID ...string) Color { func GetColorPickerValue(view View, subviewID ...string) Color {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value, ok := colorProperty(view, ColorPickerValue, view.Session()); ok { if value, ok := colorProperty(view, ColorPickerValue, view.Session()); ok {
return value return value
} }
for _, tag := range []PropertyName{ColorPickerValue, Value, ColorTag} { for _, tag := range []string{ColorPickerValue, Value, ColorTag} {
if value := valueFromStyle(view, tag); value != nil { if value := valueFromStyle(view, tag); value != nil {
if result, ok := valueToColor(value, view.Session()); ok { if result, ok := valueToColor(value, view.Session()); ok {
return result return result
@ -203,18 +204,7 @@ func GetColorPickerValue(view View, subviewID ...string) Color {
// GetColorChangedListeners returns the ColorChangedListener list of an ColorPicker subview. // GetColorChangedListeners returns the ColorChangedListener list of an ColorPicker subview.
// If there are no listeners then the empty list is returned // If there are no listeners then the empty list is returned
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetColorChangedListeners(view View, subviewID ...string) []func(ColorPicker, Color) {
// - func(rui.ColorPicker, rui.Color, rui.Color), return getEventListeners[ColorPicker, Color](view, subviewID, ColorChangedEvent)
// - func(rui.ColorPicker, rui.Color),
// - func(rui.ColorPicker),
// - func(rui.Color, rui.Color),
// - func(rui.Color),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetColorChangedListeners(view View, subviewID ...string) []any {
return getTwoArgEventRawListeners[ColorPicker, Color](view, subviewID, ColorChangedEvent)
} }

View File

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

View File

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

View File

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

View File

@ -1,20 +1,12 @@
package rui package rui
import ( import "strings"
"iter"
"strings"
)
// CustomView defines a custom view interface // CustomView defines a custom view interface
type CustomView interface { type CustomView interface {
ViewsContainer ViewsContainer
// CreateSuperView must be implemented to create a base view from which custom control has been built
CreateSuperView(session Session) View CreateSuperView(session Session) View
// SuperView must be implemented to return a base view from which custom control has been built
SuperView() View SuperView() View
setSuperView(view View) setSuperView(view View)
setTag(tag string) setTag(tag string)
} }
@ -23,7 +15,6 @@ type CustomView interface {
type CustomViewData struct { type CustomViewData struct {
tag string tag string
superView View superView View
defaultParams Params
} }
// InitCustomView initializes fields of CustomView by default values // InitCustomView initializes fields of CustomView by default values
@ -39,9 +30,6 @@ func InitCustomView(customView CustomView, tag string, session Session, params P
return true return true
} }
func (customView *CustomViewData) init(session Session) {
}
// SuperView returns a super view // SuperView returns a super view
func (customView *CustomViewData) SuperView() View { func (customView *CustomViewData) SuperView() View {
return customView.superView return customView.superView
@ -49,12 +37,6 @@ func (customView *CustomViewData) SuperView() View {
func (customView *CustomViewData) setSuperView(view View) { func (customView *CustomViewData) setSuperView(view View) {
customView.superView = view customView.superView = view
customView.defaultParams = Params{}
for tag, value := range view.All() {
if value != nil {
customView.defaultParams[tag] = value
}
}
} }
func (customView *CustomViewData) setTag(tag string) { func (customView *CustomViewData) setTag(tag string) {
@ -63,64 +45,41 @@ func (customView *CustomViewData) setTag(tag string) {
// Get returns a value of the property with name defined by the argument. // Get returns a value of the property with name defined by the argument.
// The type of return value depends on the property. If the property is not set then nil is returned. // The type of return value depends on the property. If the property is not set then nil is returned.
func (customView *CustomViewData) Get(tag PropertyName) any { func (customView *CustomViewData) Get(tag string) any {
return customView.superView.Get(tag) return customView.superView.Get(tag)
} }
func (customView *CustomViewData) getRaw(tag PropertyName) any { func (customView *CustomViewData) getRaw(tag string) any {
return customView.superView.getRaw(tag) return customView.superView.getRaw(tag)
} }
func (customView *CustomViewData) setRaw(tag PropertyName, value any) { func (customView *CustomViewData) setRaw(tag string, value any) {
customView.superView.setRaw(tag, value) customView.superView.setRaw(tag, value)
} }
func (customView *CustomViewData) setContent(value any) bool {
if container, ok := customView.superView.(ViewsContainer); ok {
return container.setContent(value)
}
return false
}
// Set sets the value (second argument) of the property with name defined by the first argument. // Set sets the value (second argument) of the property with name defined by the first argument.
// Return "true" if the value has been set, in the opposite case "false" are returned and // Return "true" if the value has been set, in the opposite case "false" are returned and
// a description of the error is written to the log // a description of the error is written to the log
func (customView *CustomViewData) Set(tag PropertyName, value any) bool { func (customView *CustomViewData) Set(tag string, value any) bool {
return customView.superView.Set(tag, value) return customView.superView.Set(tag, value)
} }
// SetAnimated sets the value (second argument) of the property with name defined by the first argument. func (customView *CustomViewData) SetAnimated(tag string, value any, animation Animation) bool {
// Return "true" if the value has been set, in the opposite case "false" are returned and
// a description of the error is written to the log
func (customView *CustomViewData) SetAnimated(tag PropertyName, value any, animation AnimationProperty) bool {
return customView.superView.SetAnimated(tag, value, animation) return customView.superView.SetAnimated(tag, value, animation)
} }
func (customView *CustomViewData) SetParams(params Params) bool { func (customView *CustomViewData) SetChangeListener(tag string, listener func(View, string)) {
return customView.superView.SetParams(params) customView.superView.SetChangeListener(tag, listener)
}
// SetChangeListener set the function to track the change of the View property
func (customView *CustomViewData) SetChangeListener(tag PropertyName, listener any) bool {
return customView.superView.SetChangeListener(tag, listener)
} }
// Remove removes the property with name defined by the argument // Remove removes the property with name defined by the argument
func (customView *CustomViewData) Remove(tag PropertyName) { func (customView *CustomViewData) Remove(tag string) {
customView.superView.Remove(tag) customView.superView.Remove(tag)
} }
func (customView *CustomViewData) AllTags() []PropertyName {
return customView.superView.AllTags()
}
// AllTags returns an array of the set properties // AllTags returns an array of the set properties
func (customView *CustomViewData) All() iter.Seq2[PropertyName, any] { func (customView *CustomViewData) AllTags() []string {
return customView.superView.All() return customView.superView.AllTags()
}
func (customView *CustomViewData) IsEmpty() bool {
return customView.superView.IsEmpty()
} }
// Clear removes all properties // Clear removes all properties
@ -128,6 +87,10 @@ func (customView *CustomViewData) Clear() {
customView.superView.Clear() customView.superView.Clear()
} }
func (customView *CustomViewData) cssViewStyle(buffer cssBuilder, session Session) {
customView.superView.cssViewStyle(buffer, session)
}
// Session returns a current Session interface // Session returns a current Session interface
func (customView *CustomViewData) Session() Session { func (customView *CustomViewData) Session() Session {
return customView.superView.Session() return customView.superView.Session()
@ -181,12 +144,10 @@ func (customView *CustomViewData) Frame() Frame {
return customView.superView.Frame() return customView.superView.Frame()
} }
// Scroll returns a location and size of a scrollable view in pixels
func (customView *CustomViewData) Scroll() Frame { func (customView *CustomViewData) Scroll() Frame {
return customView.superView.Scroll() return customView.superView.Scroll()
} }
// HasFocus returns "true" if the view has focus
func (customView *CustomViewData) HasFocus() bool { func (customView *CustomViewData) HasFocus() bool {
return customView.superView.HasFocus() return customView.superView.HasFocus()
} }
@ -199,7 +160,7 @@ func (customView *CustomViewData) onItemResize(self View, index string, x, y, wi
customView.superView.onItemResize(customView.superView, index, x, y, width, height) customView.superView.onItemResize(customView.superView, index, x, y, width, height)
} }
func (customView *CustomViewData) handleCommand(self View, command PropertyName, data DataObject) bool { func (customView *CustomViewData) handleCommand(self View, command string, data DataObject) bool {
return customView.superView.handleCommand(customView.superView, command, data) return customView.superView.handleCommand(customView.superView, command, data)
} }
@ -227,8 +188,8 @@ func (customView *CustomViewData) htmlProperties(self View, buffer *strings.Buil
customView.superView.htmlProperties(customView.superView, buffer) customView.superView.htmlProperties(customView.superView, buffer)
} }
func (customView *CustomViewData) htmlDisabledProperty() bool { func (customView *CustomViewData) htmlDisabledProperties(self View, buffer *strings.Builder) {
return customView.superView.htmlDisabledProperty() customView.superView.htmlDisabledProperties(customView.superView, buffer)
} }
func (customView *CustomViewData) cssStyle(self View, builder cssBuilder) { func (customView *CustomViewData) cssStyle(self View, builder cssBuilder) {
@ -282,48 +243,13 @@ func (customView *CustomViewData) RemoveView(index int) View {
return container.RemoveView(index) return container.RemoveView(index)
} }
} }
return nil return nil
} }
func (customView *CustomViewData) RemoveViewByID(id string) View {
if customView.superView != nil {
if container, ok := customView.superView.(ViewsContainer); ok {
return container.RemoveViewByID(id)
}
}
return nil
}
// Remove removes a view from the list of a view children and return it
func (customView *CustomViewData) ViewIndex(view View) int {
if customView.superView != nil {
if container, ok := customView.superView.(ViewsContainer); ok {
return container.ViewIndex(view)
}
}
return -1
}
func (customView *CustomViewData) excludeTags() []PropertyName {
if customView.superView != nil {
exclude := []PropertyName{}
for tag, value := range customView.defaultParams {
if value == customView.superView.getRaw(tag) {
exclude = append(exclude, tag)
}
}
return exclude
}
return nil
}
// String convert internal representation of a [CustomViewData] into a string.
func (customView *CustomViewData) String() string { func (customView *CustomViewData) String() string {
if customView.superView != nil { if customView.superView != nil {
buffer := allocStringBuilder() return getViewString(customView)
defer freeStringBuilder(buffer)
writeViewStyle(customView.tag, customView, buffer, "", customView.excludeTags())
return buffer.String()
} }
return customView.tag + " { }" return customView.tag + " { }"
} }
@ -334,40 +260,22 @@ func (customView *CustomViewData) setScroll(x, y, width, height float64) {
} }
} }
// Transition returns the transition animation of the property(tag). Returns nil is there is no transition animation. func (customView *CustomViewData) Transition(tag string) Animation {
func (customView *CustomViewData) Transition(tag PropertyName) AnimationProperty {
if customView.superView != nil { if customView.superView != nil {
return customView.superView.Transition(tag) return customView.superView.Transition(tag)
} }
return nil return nil
} }
// Transitions returns a map of transition animations. The result is always non-nil. func (customView *CustomViewData) Transitions() map[string]Animation {
func (customView *CustomViewData) Transitions() map[PropertyName]AnimationProperty {
if customView.superView != nil { if customView.superView != nil {
return customView.superView.Transitions() return customView.superView.Transitions()
} }
return map[PropertyName]AnimationProperty{} return map[string]Animation{}
} }
// SetTransition sets the transition animation for the property if "animation" argument is not nil, and func (customView *CustomViewData) SetTransition(tag string, animation Animation) {
// removes the transition animation of the property if "animation" argument is nil.
// The "tag" argument is the property name.
func (customView *CustomViewData) SetTransition(tag PropertyName, animation AnimationProperty) {
if customView.superView != nil { if customView.superView != nil {
customView.superView.SetTransition(tag, animation) customView.superView.SetTransition(tag, animation)
} }
} }
func (customView *CustomViewData) LoadFile(file FileInfo, result func(FileInfo, []byte)) {
if customView.superView != nil {
customView.superView.LoadFile(file, result)
}
}
func (customView *CustomViewData) binding() any {
if customView.superView != nil {
return customView.superView.binding()
}
return nil
}

564
data.go
View File

@ -1,105 +1,48 @@
package rui package rui
import ( import (
"errors"
"fmt"
"iter"
"slices"
"strings" "strings"
"unicode" "unicode"
) )
// DataValue interface of a data node value // DataValue interface of a data node value
type DataValue interface { type DataValue interface {
// IsObject returns "true" if data value is an object
IsObject() bool IsObject() bool
// Object returns data value as a data object
Object() DataObject Object() DataObject
// Value returns value as a string
Value() string Value() string
} }
// DataObject interface of a data object // DataObject interface of a data object
type DataObject interface { type DataObject interface {
DataValue DataValue
// Tag returns data object tag
Tag() string Tag() string
// Properties() returns an iterator to access the properties
Properties() iter.Seq[DataNode]
// PropertyCount returns properties count
PropertyCount() int PropertyCount() int
// Property returns a data node corresponding to a property with specific index
Property(index int) DataNode Property(index int) DataNode
PropertyWithTag(tag string) DataNode
// PropertyByTag returns a data node corresponding to a property tag
PropertyByTag(tag string) DataNode
// PropertyValue returns a string value of a property with a specific tag
PropertyValue(tag string) (string, bool) PropertyValue(tag string) (string, bool)
// PropertyObject returns an object value of a property with a specific tag
PropertyObject(tag string) DataObject PropertyObject(tag string) DataObject
// SetPropertyValue sets a string value of a property with a specific tag
SetPropertyValue(tag, value string) SetPropertyValue(tag, value string)
// SetPropertyObject sets an object value of a property with a specific tag
SetPropertyObject(tag string, object DataObject) SetPropertyObject(tag string, object DataObject)
// ToParams create a params(map) representation of a data object
ToParams() Params
// PropertyByTag removes a data node corresponding to a property tag and returns it
RemovePropertyByTag(tag string) DataNode
} }
// DataNodeType defines the type of DataNode
type DataNodeType int
// Constants which are used to describe a node type, see [DataNode]
const ( const (
// TextNode - node is the pair "tag - text value". Syntax: <tag> = <text> // TextNode - node is the pair "tag - text value". Syntax: <tag> = <text>
TextNode DataNodeType = 0 TextNode = 0
// ObjectNode - node is the pair "tag - object". Syntax: <tag> = <object name>{...} // ObjectNode - node is the pair "tag - object". Syntax: <tag> = <object name>{...}
ObjectNode DataNodeType = 1 ObjectNode = 1
// ArrayNode - node is the pair "tag - object". Syntax: <tag> = [...] // ArrayNode - node is the pair "tag - object". Syntax: <tag> = [...]
ArrayNode DataNodeType = 2 ArrayNode = 2
) )
// DataNode interface of a data node // DataNode interface of a data node
type DataNode interface { type DataNode interface {
// Tag returns a tag name
Tag() string Tag() string
Type() int
// Type returns a node type. Possible values are TextNode, ObjectNode and ArrayNode
Type() DataNodeType
// Text returns node text
Text() string Text() string
// Object returns node as object if that node type is an object
Object() DataObject Object() DataObject
// ArraySize returns array size if that node type is an array
ArraySize() int ArraySize() int
// ArrayElement returns a value of an array if that node type is an array
ArrayElement(index int) DataValue ArrayElement(index int) DataValue
ArrayElements() []DataValue
// ArrayElements returns an array of objects if that node is an array
Array() []DataValue
// ArrayElements returns an iterator to access the array elements of objects if that node is an array
ArrayElements() iter.Seq[DataValue]
// ArrayAsParams returns an array of a params(map) if that node is an array
ArrayAsParams() []Params
} }
/******************************************************************************/ /******************************************************************************/
@ -149,16 +92,6 @@ func (object *dataObject) Tag() string {
return object.tag return object.tag
} }
func (object *dataObject) Properties() iter.Seq[DataNode] {
return func(yield func(DataNode) bool) {
for _, node := range object.property {
if !yield(node) {
return
}
}
}
}
func (object *dataObject) PropertyCount() int { func (object *dataObject) PropertyCount() int {
if object.property != nil { if object.property != nil {
return len(object.property) return len(object.property)
@ -173,7 +106,7 @@ func (object *dataObject) Property(index int) DataNode {
return object.property[index] return object.property[index]
} }
func (object *dataObject) PropertyByTag(tag string) DataNode { func (object *dataObject) PropertyWithTag(tag string) DataNode {
if object.property != nil { if object.property != nil {
for _, node := range object.property { for _, node := range object.property {
if node.Tag() == tag { if node.Tag() == tag {
@ -184,44 +117,22 @@ func (object *dataObject) PropertyByTag(tag string) DataNode {
return nil return nil
} }
func (object *dataObject) RemovePropertyByTag(tag string) DataNode {
if object.property != nil {
for i, node := range object.property {
if node.Tag() == tag {
switch i {
case 0:
object.property = object.property[1:]
case len(object.property) - 1:
object.property = object.property[:len(object.property)-1]
default:
object.property = append(object.property[:i], object.property[i+1:]...)
}
return node
}
}
}
return nil
}
func (object *dataObject) PropertyValue(tag string) (string, bool) { func (object *dataObject) PropertyValue(tag string) (string, bool) {
if node := object.PropertyByTag(tag); node != nil && node.Type() == TextNode { if node := object.PropertyWithTag(tag); node != nil && node.Type() == TextNode {
return node.Text(), true return node.Text(), true
} }
return "", false return "", false
} }
func (object *dataObject) PropertyObject(tag string) DataObject { func (object *dataObject) PropertyObject(tag string) DataObject {
if node := object.PropertyByTag(tag); node != nil && node.Type() == ObjectNode { if node := object.PropertyWithTag(tag); node != nil && node.Type() == ObjectNode {
return node.Object() return node.Object()
} }
return nil return nil
} }
func (object *dataObject) setNode(node DataNode) { func (object *dataObject) setNode(node DataNode) {
if len(object.property) == 0 { if object.property == nil || len(object.property) == 0 {
object.property = []DataNode{node} object.property = []DataNode{node}
} else { } else {
tag := node.Tag() tag := node.Tag()
@ -248,50 +159,10 @@ func (object *dataObject) SetPropertyValue(tag, value string) {
// SetPropertyObject - set a property with tag by object // SetPropertyObject - set a property with tag by object
func (object *dataObject) SetPropertyObject(tag string, obj DataObject) { func (object *dataObject) SetPropertyObject(tag string, obj DataObject) {
if obj != nil {
node := new(dataNode) node := new(dataNode)
node.tag = tag node.tag = tag
node.value = obj node.value = obj
object.setNode(node) object.setNode(node)
} else {
object.RemovePropertyByTag(tag)
}
}
func (object *dataObject) ToParams() Params {
params := Params{}
for _, node := range object.property {
switch node.Type() {
case TextNode:
if text := node.Text(); text != "" {
params[PropertyName(node.Tag())] = text
}
case ObjectNode:
if obj := node.Object(); obj != nil {
params[PropertyName(node.Tag())] = node.Object()
}
case ArrayNode:
array := []any{}
for i := range node.ArraySize() {
if data := node.ArrayElement(i); data != nil {
if data.IsObject() {
if obj := data.Object(); obj != nil {
array = append(array, obj)
}
} else if text := data.Value(); text != "" {
array = append(array, text)
}
}
}
if len(array) > 0 {
params[PropertyName(node.Tag())] = array
}
}
}
return params
} }
/******************************************************************************/ /******************************************************************************/
@ -305,7 +176,7 @@ func (node *dataNode) Tag() string {
return node.tag return node.tag
} }
func (node *dataNode) Type() DataNodeType { func (node *dataNode) Type() int {
if node.array != nil { if node.array != nil {
return ArrayNode return ArrayNode
} }
@ -343,86 +214,61 @@ func (node *dataNode) ArrayElement(index int) DataValue {
return nil return nil
} }
func (node *dataNode) Array() []DataValue { func (node *dataNode) ArrayElements() []DataValue {
if node.array != nil { if node.array != nil {
return slices.Clone(node.array) return node.array
} }
return []DataValue{node.value} return []DataValue{}
} }
func (node *dataNode) ArrayElements() iter.Seq[DataValue] { // ParseDataText - parse text and return DataNode
return func(yield func(DataValue) bool) { func ParseDataText(text string) DataObject {
if node.array != nil {
for _, element := range node.array {
if !yield(element) {
return
}
}
} else {
yield(node.value)
}
}
}
func (node *dataNode) ArrayAsParams() []Params { if strings.ContainsAny(text, "\r") {
result := []Params{} text = strings.Replace(text, "\r\n", "\n", -1)
if node.array != nil { text = strings.Replace(text, "\r", "\n", -1)
for _, data := range node.array {
if data.IsObject() {
if obj := data.Object(); obj != nil {
if params := obj.ToParams(); len(params) > 0 {
result = append(result, params)
} }
} data := append([]rune(text), rune(0))
} pos := 0
} size := len(data) - 1
} line := 1
return result lineStart := 0
}
type dataParser struct { skipSpaces := func(skipNewLine bool) {
data []rune for pos < size {
size int switch data[pos] {
pos int
line int
lineStart int
}
func (parser *dataParser) skipSpaces(skipNewLine bool) {
for parser.pos < parser.size {
switch parser.data[parser.pos] {
case '\n': case '\n':
if !skipNewLine { if !skipNewLine {
return return
} }
parser.line++ line++
parser.lineStart = parser.pos + 1 lineStart = pos + 1
case '/': case '/':
if parser.pos+1 < parser.size { if pos+1 < size {
switch parser.data[parser.pos+1] { switch data[pos+1] {
case '/': case '/':
parser.pos += 2 pos += 2
for parser.pos < parser.size && parser.data[parser.pos] != '\n' { for pos < size && data[pos] != '\n' {
parser.pos++ pos++
} }
parser.pos-- pos--
case '*': case '*':
parser.pos += 3 pos += 3
for { for {
if parser.pos >= parser.size { if pos >= size {
ErrorLog("Unexpected end of file") ErrorLog("Unexpected end of file")
return return
} }
if parser.data[parser.pos-1] == '*' && parser.data[parser.pos] == '/' { if data[pos-1] == '*' && data[pos] == '/' {
break break
} }
if parser.data[parser.pos-1] == '\n' { if data[pos-1] == '\n' {
parser.line++ line++
parser.lineStart = parser.pos lineStart = pos
} }
parser.pos++ pos++
} }
default: default:
@ -434,72 +280,75 @@ func (parser *dataParser) skipSpaces(skipNewLine bool) {
// do nothing // do nothing
default: default:
if !unicode.IsSpace(parser.data[parser.pos]) { if !unicode.IsSpace(data[pos]) {
return return
} }
} }
parser.pos++ pos++
}
} }
}
func (parser *dataParser) parseTag() (string, error) { parseTag := func() (string, bool) {
parser.skipSpaces(true) skipSpaces(true)
startPos := parser.pos startPos := pos
switch parser.data[parser.pos] { if data[pos] == '`' {
case '`': pos++
parser.pos++
startPos++ startPos++
for parser.data[parser.pos] != '`' { for data[pos] != '`' {
parser.pos++ pos++
if parser.pos >= parser.size { if pos >= size {
return string(parser.data[startPos:parser.size]), errors.New("unexpected end of text") ErrorLog("Unexpected end of text")
return string(data[startPos:size]), false
} }
} }
str := string(parser.data[startPos:parser.pos]) str := string(data[startPos:pos])
parser.pos++ pos++
return str, nil return str, true
case '\'', '"': } else if data[pos] == '\'' || data[pos] == '"' {
stopSymbol := parser.data[parser.pos]
parser.pos++ stopSymbol := data[pos]
pos++
startPos++ startPos++
slash := false slash := false
for stopSymbol != parser.data[parser.pos] { for stopSymbol != data[pos] {
if parser.data[parser.pos] == '\\' { if data[pos] == '\\' {
parser.pos += 2 pos += 2
slash = true slash = true
} else { } else {
parser.pos++ pos++
} }
if parser.pos >= parser.size { if pos >= size {
return string(parser.data[startPos:parser.size]), errors.New("unexpected end of text") ErrorLog("Unexpected end of text")
return string(data[startPos:size]), false
} }
} }
if !slash { if !slash {
str := string(parser.data[startPos:parser.pos]) str := string(data[startPos:pos])
parser.pos++ pos++
parser.skipSpaces(false) skipSpaces(false)
return str, nil return str, true
} }
buffer := make([]rune, parser.pos-startPos+1) buffer := make([]rune, pos-startPos+1)
n1 := 0 n1 := 0
n2 := startPos n2 := startPos
invalidEscape := func() (string, error) { invalidEscape := func() (string, bool) {
str := string(parser.data[startPos:parser.pos]) str := string(data[startPos:pos])
parser.pos++ pos++
return str, fmt.Errorf(`invalid escape sequence in "%s" (position %d)`, str, n2-2-startPos) ErrorLogF("Invalid escape sequence in \"%s\" (position %d)", str, n2-2-startPos)
return str, false
} }
for n2 < parser.pos { for n2 < pos {
if parser.data[n2] != '\\' { if data[n2] != '\\' {
buffer[n1] = parser.data[n2] buffer[n1] = data[n2]
n2++ n2++
} else { } else {
n2 += 2 n2 += 2
switch parser.data[n2-1] { switch data[n2-1] {
case 'n': case 'n':
buffer[n1] = '\n' buffer[n1] = '\n'
@ -519,12 +368,12 @@ func (parser *dataParser) parseTag() (string, error) {
buffer[n1] = '\\' buffer[n1] = '\\'
case 'x', 'X': case 'x', 'X':
if n2+2 > parser.pos { if n2+2 > pos {
return invalidEscape() return invalidEscape()
} }
x := 0 x := 0
for range 2 { for i := 0; i < 2; i++ {
ch := parser.data[n2] ch := data[n2]
if ch >= '0' && ch <= '9' { if ch >= '0' && ch <= '9' {
x = x*16 + int(ch-'0') x = x*16 + int(ch-'0')
} else if ch >= 'a' && ch <= 'f' { } else if ch >= 'a' && ch <= 'f' {
@ -539,12 +388,12 @@ func (parser *dataParser) parseTag() (string, error) {
buffer[n1] = rune(x) buffer[n1] = rune(x)
case 'u', 'U': case 'u', 'U':
if n2+4 > parser.pos { if n2+4 > pos {
return invalidEscape() return invalidEscape()
} }
x := 0 x := 0
for range 4 { for i := 0; i < 4; i++ {
ch := parser.data[n2] ch := data[n2]
if ch >= '0' && ch <= '9' { if ch >= '0' && ch <= '9' {
x = x*16 + int(ch-'0') x = x*16 + int(ch-'0')
} else if ch >= 'a' && ch <= 'f' { } else if ch >= 'a' && ch <= 'f' {
@ -559,84 +408,97 @@ func (parser *dataParser) parseTag() (string, error) {
buffer[n1] = rune(x) buffer[n1] = rune(x)
default: default:
str := string(parser.data[startPos:parser.pos]) str := string(data[startPos:pos])
return str, fmt.Errorf(`invalid escape sequence in "%s" (position %d)`, str, n2-2-startPos) ErrorLogF("Invalid escape sequence in \"%s\" (position %d)", str, n2-2-startPos)
return str, false
} }
} }
n1++ n1++
} }
parser.pos++ pos++
parser.skipSpaces(false) skipSpaces(false)
return string(buffer[0:n1]), nil return string(buffer[0:n1]), true
} }
for parser.pos < parser.size && !parser.stopSymbol(parser.data[parser.pos]) { stopSymbol := func(symbol rune) bool {
parser.pos++ if unicode.IsSpace(symbol) {
return true
}
for _, sym := range []rune{'=', '{', '}', '[', ']', ',', ' ', '\t', '\n', '\'', '"', '`', '/'} {
if sym == symbol {
return true
}
}
return false
} }
endPos := parser.pos for pos < size && !stopSymbol(data[pos]) {
parser.skipSpaces(false) pos++
}
endPos := pos
skipSpaces(false)
if startPos == endPos { if startPos == endPos {
//ErrorLog("empty tag") ErrorLog("empty tag")
return "", nil return "", false
}
return string(data[startPos:endPos]), true
} }
return string(parser.data[startPos:endPos]), nil
}
func (parser *dataParser) stopSymbol(symbol rune) bool { var parseObject func(tag string) DataObject
return unicode.IsSpace(symbol) || var parseArray func() []DataValue
slices.Contains([]rune{'=', '{', '}', '[', ']', ',', ' ', '\t', '\n', '\'', '"', '`', '/'}, symbol)
}
func (parser *dataParser) parseNode() (DataNode, error) { parseNode := func() DataNode {
var tag string var tag string
var err error var ok bool
if tag, err = parser.parseTag(); err != nil { if tag, ok = parseTag(); !ok {
return nil, err return nil
} }
parser.skipSpaces(true) skipSpaces(true)
if parser.data[parser.pos] != '=' { if data[pos] != '=' {
return nil, fmt.Errorf("expected '=' after a tag name (line: %d, position: %d)", parser.line, parser.pos-parser.lineStart) ErrorLogF("expected '=' after a tag name (line: %d, position: %d)", line, pos-lineStart)
return nil
} }
parser.pos++ pos++
parser.skipSpaces(true) skipSpaces(true)
switch parser.data[parser.pos] { switch data[pos] {
case '[': case '[':
node := new(dataNode) node := new(dataNode)
node.tag = tag node.tag = tag
if node.array, err = parser.parseArray(); err != nil { if node.array = parseArray(); node.array == nil {
return nil, err return nil
} }
return node, nil return node
case '{': case '{':
node := new(dataNode) node := new(dataNode)
node.tag = tag node.tag = tag
if node.value, err = parser.parseObject("_"); err != nil { if node.value = parseObject("_"); node.value == nil {
return nil, err return nil
} }
return node, nil return node
case '}', ']', '=': case '}', ']', '=':
return nil, fmt.Errorf(`expected '[', '{' or a tag name after '=' (line: %d, position: %d)`, parser.line, parser.pos-parser.lineStart) ErrorLogF("Expected '[', '{' or a tag name after '=' (line: %d, position: %d)", line, pos-lineStart)
return nil
default: default:
var str string var str string
if str, err = parser.parseTag(); err != nil { if str, ok = parseTag(); !ok {
return nil, err return nil
} }
node := new(dataNode) node := new(dataNode)
node.tag = tag node.tag = tag
if parser.data[parser.pos] == '{' { if data[pos] == '{' {
if node.value, err = parser.parseObject(str); err != nil { if node.value = parseObject(str); node.value == nil {
return nil, err return nil
} }
} else { } else {
val := new(dataStringValue) val := new(dataStringValue)
@ -644,88 +506,91 @@ func (parser *dataParser) parseNode() (DataNode, error) {
node.value = val node.value = val
} }
return node, nil return node
}
} }
}
func (parser *dataParser) parseObject(tag string) (DataObject, error) { parseObject = func(tag string) DataObject {
if parser.data[parser.pos] != '{' { if data[pos] != '{' {
return nil, fmt.Errorf(`expected '{' (line: %d, position: %d)`, parser.line, parser.pos-parser.lineStart) ErrorLogF("Expected '{' (line: %d, position: %d)", line, pos-lineStart)
return nil
} }
parser.pos++ pos++
obj := new(dataObject) obj := new(dataObject)
obj.tag = tag obj.tag = tag
obj.property = []DataNode{} obj.property = []DataNode{}
for parser.pos < parser.size { for pos < size {
parser.skipSpaces(true) var node DataNode
if parser.data[parser.pos] == '}' {
parser.pos++ skipSpaces(true)
parser.skipSpaces(false) if data[pos] == '}' {
return obj, nil pos++
skipSpaces(false)
return obj
} }
node, err := parser.parseNode() if node = parseNode(); node == nil {
if err != nil { return nil
return nil, err
} }
obj.property = append(obj.property, node) obj.property = append(obj.property, node)
if parser.data[parser.pos] == '}' { if data[pos] == '}' {
parser.pos++ pos++
parser.skipSpaces(true) skipSpaces(true)
return obj, nil return obj
} else if parser.data[parser.pos] != ',' && parser.data[parser.pos] != '\n' { } else if data[pos] != ',' && data[pos] != '\n' {
return nil, fmt.Errorf(`expected '}', '\n' or ',' (line: %d, position: %d)`, parser.line, parser.pos-parser.lineStart) ErrorLogF(`Expected '}', '\n' or ',' (line: %d, position: %d)`, line, pos-lineStart)
return nil
} }
if data[pos] != '\n' {
if parser.data[parser.pos] != '\n' { pos++
parser.pos++
} }
skipSpaces(true)
parser.skipSpaces(true) for data[pos] == ',' {
for parser.data[parser.pos] == ',' { pos++
parser.pos++ skipSpaces(true)
parser.skipSpaces(true)
} }
} }
return nil, errors.New("unexpected end of text") ErrorLog("Unexpected end of text")
} return nil
}
func (parser *dataParser) parseArray() ([]DataValue, error) { parseArray = func() []DataValue {
parser.pos++ pos++
parser.skipSpaces(true) skipSpaces(true)
array := []DataValue{} array := []DataValue{}
for parser.pos < parser.size { for pos < size {
parser.skipSpaces(true) var tag string
for parser.data[parser.pos] == ',' && parser.pos < parser.size { var ok bool
parser.pos++
parser.skipSpaces(true) skipSpaces(true)
for data[pos] == ',' && pos < size {
pos++
skipSpaces(true)
} }
if parser.pos >= parser.size { if pos >= size {
break break
} }
if parser.data[parser.pos] == ']' { if data[pos] == ']' {
parser.pos++ pos++
parser.skipSpaces(true) skipSpaces(true)
return array, nil return array
} }
tag, err := parser.parseTag() if tag, ok = parseTag(); !ok {
if err != nil { return nil
return nil, err
} }
if parser.data[parser.pos] == '{' { if data[pos] == '{' {
obj, err := parser.parseObject(tag) obj := parseObject(tag)
if err != nil { if obj == nil {
return nil, err return nil
} }
array = append(array, obj) array = append(array, obj)
} else { } else {
@ -734,11 +599,12 @@ func (parser *dataParser) parseArray() ([]DataValue, error) {
array = append(array, val) array = append(array, val)
} }
switch parser.data[parser.pos] { switch data[pos] {
case ']', ',', '\n': case ']', ',', '\n':
default: default:
return nil, fmt.Errorf(`expected ']' or ',' (line: %d, position: %d)`, parser.line, parser.pos-parser.lineStart) ErrorLogF("Expected ']' or ',' (line: %d, position: %d)", line, pos-lineStart)
return nil
} }
/* /*
@ -754,28 +620,12 @@ func (parser *dataParser) parseArray() ([]DataValue, error) {
*/ */
} }
return nil, errors.New("unexpected end of text") ErrorLog("Unexpected end of text")
} return nil
}
// ParseDataText - parse text and return DataNode
func ParseDataText(text string) (DataObject, error) { if tag, ok := parseTag(); ok {
return parseObject(tag)
if strings.ContainsAny(text, "\r") { }
text = strings.ReplaceAll(text, "\r\n", "\n") return nil
text = strings.ReplaceAll(text, "\r", "\n")
}
parser := dataParser{
data: append([]rune(text), rune(0)),
pos: 0,
line: 1,
lineStart: 0,
}
parser.size = len(parser.data) - 1
tag, err := parser.parseTag()
if err != nil {
return nil, err
}
return parser.parseObject(tag)
} }

View File

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

View File

@ -6,6 +6,10 @@ import (
func TestParseDataText(t *testing.T) { func TestParseDataText(t *testing.T) {
SetErrorLog(func(text string) {
t.Error(text)
})
text := `obj1 { text := `obj1 {
key1 = val1, key1 = val1,
key2=obj2{ key2=obj2{
@ -23,10 +27,8 @@ func TestParseDataText(t *testing.T) {
key3 = "\n \t \\ \r \" ' \X4F\x4e \U01Ea",` + key3 = "\n \t \\ \r \" ' \X4F\x4e \U01Ea",` +
"key4=`" + `\n \t \\ \r \" ' \x8F \UF80a` + "`\r}" "key4=`" + `\n \t \\ \r \" ' \x8F \UF80a` + "`\r}"
obj, err := ParseDataText(text) obj := ParseDataText(text)
if err != nil { if obj != nil {
t.Error(err)
} else {
if obj.Tag() != "obj1" { if obj.Tag() != "obj1" {
t.Error(`obj.Tag() != "obj1"`) t.Error(`obj.Tag() != "obj1"`)
} }
@ -73,7 +75,7 @@ func TestParseDataText(t *testing.T) {
t.Errorf(`obj.PropertyValue("key5") result: ("%s",%v)`, val, ok) t.Errorf(`obj.PropertyValue("key5") result: ("%s",%v)`, val, ok)
} }
testKey := func(obj DataObject, index int, tag string, nodeType DataNodeType) DataNode { testKey := func(obj DataObject, index int, tag string, nodeType int) DataNode {
key := obj.Property(index) key := obj.Property(index)
if key == nil { if key == nil {
t.Errorf(`%s.Property(%d) == nil`, obj.Tag(), index) t.Errorf(`%s.Property(%d) == nil`, obj.Tag(), index)
@ -116,7 +118,7 @@ func TestParseDataText(t *testing.T) {
type testKeyData struct { type testKeyData struct {
tag string tag string
nodeType DataNodeType nodeType int
} }
data := []testKeyData{ data := []testKeyData{
@ -171,6 +173,9 @@ func TestParseDataText(t *testing.T) {
} }
} }
SetErrorLog(func(text string) {
})
failText := []string{ failText := []string{
" ", " ",
"obj[]", "obj[]",
@ -199,7 +204,7 @@ func TestParseDataText(t *testing.T) {
} }
for _, txt := range failText { for _, txt := range failText {
if _, err := ParseDataText(txt); err == nil { if obj := ParseDataText(txt); obj != nil {
t.Errorf("result ParseDataText(\"%s\") must be fail", txt) t.Errorf("result ParseDataText(\"%s\") must be fail", txt)
} }
} }

View File

@ -1,124 +1,29 @@
package rui package rui
import ( import (
"fmt"
"strconv" "strconv"
"strings" "strings"
"time" "time"
) )
// Constants for [DatePicker] specific properties and events.
const ( const (
// DateChangedEvent is the constant for "date-changed" property tag. DateChangedEvent = "date-changed"
// DatePickerMin = "date-picker-min"
// Used by DatePicker. DatePickerMax = "date-picker-max"
// Occur when date picker value has been changed. DatePickerStep = "date-picker-step"
// DatePickerValue = "date-picker-value"
// General listener format:
// func(picker rui.DatePicker, newDate time.Time, oldDate time.Time)
//
// where:
// - picker - Interface of a date picker which generated this event,
// - newDate - New date value,
// - oldDate - Old date value.
//
// Allowed listener formats:
// func(picker rui.DatePicker, newDate time.Time)
// func(newDate time.Time, oldDate time.Time)
// func(newDate time.Time)
// func(picker rui.DatePicker)
// func()
DateChangedEvent PropertyName = "date-changed"
// DatePickerMin is the constant for "date-picker-min" property tag.
//
// Used by DatePicker.
// Minimum date value.
//
// Supported types: time.Time, string.
//
// Internal type is time.Time, other types converted to it during assignment.
//
// Conversion rules:
// string - values of this type parsed and converted to time.Time. The following formats are supported:
// - "YYYYMMDD" - "20240102".
// - "Mon-DD-YYYY" - "Jan-02-24".
// - "Mon-DD-YY" - "Jan-02-2024".
// - "DD-Mon-YYYY" - "02-Jan-2024".
// - "YYYY-MM-DD" - "2024-01-02".
// - "Month DD, YYYY" - "January 02, 2024".
// - "DD Month YYYY" - "02 January 2024".
// - "MM/DD/YYYY" - "01/02/2024".
// - "MM/DD/YY" - "01/02/24".
// - "MMDDYY" - "010224".
DatePickerMin PropertyName = "date-picker-min"
// DatePickerMax is the constant for "date-picker-max" property tag.
//
// Used by DatePicker.
// Maximum date value.
//
// Supported types: time.Time, string.
//
// Internal type is time.Time, other types converted to it during assignment.
//
// Conversion rules:
// string - values of this type parsed and converted to time.Time. The following formats are supported:
// - "YYYYMMDD" - "20240102".
// - "Mon-DD-YYYY" - "Jan-02-24".
// - "Mon-DD-YY" - "Jan-02-2024".
// - "DD-Mon-YYYY" - "02-Jan-2024".
// - "YYYY-MM-DD" - "2024-01-02".
// - "Month DD, YYYY" - "January 02, 2024".
// - "DD Month YYYY" - "02 January 2024".
// - "MM/DD/YYYY" - "01/02/2024".
// - "MM/DD/YY" - "01/02/24".
// - "MMDDYY" - "010224".
DatePickerMax PropertyName = "date-picker-max"
// DatePickerStep is the constant for "date-picker-step" property tag.
//
// Used by DatePicker.
// Date change step in days.
//
// Supported types: int, string.
//
// Values:
// positive value - Step value in days used to increment or decrement date.
DatePickerStep PropertyName = "date-picker-step"
// DatePickerValue is the constant for "date-picker-value" property tag.
//
// Used by DatePicker.
// Current value.
//
// Supported types: time.Time, string.
//
// Internal type is time.Time, other types converted to it during assignment.
//
// Conversion rules:
// string - values of this type parsed and converted to time.Time. The following formats are supported:
// - "YYYYMMDD" - "20240102".
// - "Mon-DD-YYYY" - "Jan-02-24".
// - "Mon-DD-YY" - "Jan-02-2024".
// - "DD-Mon-YYYY" - "02-Jan-2024".
// - "YYYY-MM-DD" - "2024-01-02".
// - "Month DD, YYYY" - "January 02, 2024".
// - "DD Month YYYY" - "02 January 2024".
// - "MM/DD/YYYY" - "01/02/2024".
// - "MM/DD/YY" - "01/02/24".
// - "MMDDYY" - "010224".
DatePickerValue PropertyName = "date-picker-value"
dateFormat = "2006-01-02" dateFormat = "2006-01-02"
) )
// DatePicker represent a DatePicker view // DatePicker - DatePicker view
type DatePicker interface { type DatePicker interface {
View View
} }
type datePickerData struct { type datePickerData struct {
viewData viewData
dateChangedListeners []func(DatePicker, time.Time)
} }
// NewDatePicker create new DatePicker object and return it // NewDatePicker create new DatePicker object and return it
@ -130,37 +35,106 @@ func NewDatePicker(session Session, params Params) DatePicker {
} }
func newDatePicker(session Session) View { func newDatePicker(session Session) View {
return new(datePickerData) // NewDatePicker(session, nil) return NewDatePicker(session, nil)
} }
func (picker *datePickerData) init(session Session) { func (picker *datePickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "DatePicker" picker.tag = "DatePicker"
picker.hasHtmlDisabled = true picker.dateChangedListeners = []func(DatePicker, time.Time){}
picker.normalize = normalizeDatePickerTag }
picker.set = picker.setFunc
picker.get = picker.getFunc func (picker *datePickerData) String() string {
picker.changed = picker.propertyChanged return getViewString(picker)
} }
func (picker *datePickerData) Focusable() bool { func (picker *datePickerData) Focusable() bool {
return true return true
} }
func normalizeDatePickerTag(tag PropertyName) PropertyName { func (picker *datePickerData) normalizeTag(tag string) string {
tag = defaultNormalize(tag) tag = strings.ToLower(tag)
switch tag { switch tag {
case Type, Min, Max, Step, Value: case Type, Min, Max, Step, Value:
return "date-picker-" + tag return "date-picker-" + tag
} }
return normalizeDataListTag(tag) return tag
} }
func stringToDate(value string) (time.Time, bool) { func (picker *datePickerData) Remove(tag string) {
picker.remove(picker.normalizeTag(tag))
}
func (picker *datePickerData) remove(tag string) {
switch tag {
case DateChangedEvent:
if len(picker.dateChangedListeners) > 0 {
picker.dateChangedListeners = []func(DatePicker, time.Time){}
picker.propertyChangedEvent(tag)
}
return
case DatePickerMin:
delete(picker.properties, DatePickerMin)
if picker.created {
removeProperty(picker.htmlID(), Min, picker.session)
}
case DatePickerMax:
delete(picker.properties, DatePickerMax)
if picker.created {
removeProperty(picker.htmlID(), Max, picker.session)
}
case DatePickerStep:
delete(picker.properties, DatePickerStep)
if picker.created {
removeProperty(picker.htmlID(), Step, picker.session)
}
case DatePickerValue:
if _, ok := picker.properties[DatePickerValue]; ok {
delete(picker.properties, DatePickerValue)
date := GetDatePickerValue(picker)
if picker.created {
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, picker.htmlID(), date.Format(dateFormat)))
}
for _, listener := range picker.dateChangedListeners {
listener(picker, date)
}
} else {
return
}
default:
picker.viewData.remove(tag)
return
}
picker.propertyChangedEvent(tag)
}
func (picker *datePickerData) Set(tag string, value any) bool {
return picker.set(picker.normalizeTag(tag), value)
}
func (picker *datePickerData) set(tag string, value any) bool {
if value == nil {
picker.remove(tag)
return true
}
setTimeValue := func(tag string) (time.Time, bool) {
switch value := value.(type) {
case time.Time:
picker.properties[tag] = value
return value, true
case string:
if text, ok := picker.Session().resolveConstants(value); ok {
format := "20060102" format := "20060102"
if strings.ContainsRune(value, '-') { if strings.ContainsRune(text, '-') {
if part := strings.Split(value, "-"); len(part) == 3 { if part := strings.Split(text, "-"); len(part) == 3 {
if part[0] != "" && part[0][0] > '9' { if part[0] != "" && part[0][0] > '9' {
if len(part[2]) == 2 { if len(part[2]) == 2 {
format = "Jan-02-06" format = "Jan-02-06"
@ -173,133 +147,122 @@ func stringToDate(value string) (time.Time, bool) {
format = "2006-01-02" format = "2006-01-02"
} }
} }
} else if strings.ContainsRune(value, ' ') { } else if strings.ContainsRune(text, ' ') {
if part := strings.Split(value, " "); len(part) == 3 { if part := strings.Split(text, " "); len(part) == 3 {
if part[0] != "" && part[0][0] > '9' { if part[0] != "" && part[0][0] > '9' {
format = "January 02, 2006" format = "January 02, 2006"
} else { } else {
format = "02 January 2006" format = "02 January 2006"
} }
} }
} else if strings.ContainsRune(value, '/') { } else if strings.ContainsRune(text, '/') {
if part := strings.Split(value, "/"); len(part) == 3 { if part := strings.Split(text, "/"); len(part) == 3 {
if len(part[2]) == 2 { if len(part[2]) == 2 {
format = "01/02/06" format = "01/02/06"
} else { } else {
format = "01/02/2006" format = "01/02/2006"
} }
} }
} else if len(value) == 6 { } else if len(text) == 6 {
format = "010206" format = "010206"
} }
if date, err := time.Parse(format, value); err == nil { if date, err := time.Parse(format, text); err == nil {
picker.properties[tag] = value
return date, true return date, true
} }
return time.Now(), false
}
func (picker *datePickerData) getFunc(tag PropertyName) any {
switch tag {
case DateChangedEvent:
if listeners := getTwoArgEventRawListeners[DatePicker, time.Time](picker, nil, tag); len(listeners) > 0 {
return listeners
}
return nil
}
return picker.viewData.getFunc(tag)
}
func (picker *datePickerData) setFunc(tag PropertyName, value any) []PropertyName {
setDateValue := func(tag PropertyName) []PropertyName {
switch value := value.(type) {
case time.Time:
picker.setRaw(tag, value)
return []PropertyName{tag}
case string:
if ok, _ := isConstantName(value); ok {
picker.setRaw(tag, value)
return []PropertyName{tag}
}
if date, ok := stringToDate(value); ok {
picker.setRaw(tag, date)
return []PropertyName{tag}
} }
} }
notCompatibleType(tag, value) notCompatibleType(tag, value)
return nil return time.Now(), false
} }
switch tag { switch tag {
case DatePickerMin, DatePickerMax:
return setDateValue(tag)
case DatePickerStep:
return setIntProperty(picker, DatePickerStep, value)
case DatePickerValue:
picker.setRaw("old-date", GetDatePickerValue(picker))
return setDateValue(tag)
case DateChangedEvent:
return setTwoArgEventListener[DatePicker, time.Time](picker, tag, value)
case DataList:
return setDataList(picker, value, dateFormat)
}
return picker.viewData.setFunc(tag, value)
}
func (picker *datePickerData) propertyChanged(tag PropertyName) {
session := picker.Session()
switch tag {
case DatePickerMin: case DatePickerMin:
if date, ok := GetDatePickerMin(picker); ok { old, oldOK := getDateProperty(picker, DatePickerMin, Min)
session.updateProperty(picker.htmlID(), "min", date.Format(dateFormat)) if date, ok := setTimeValue(DatePickerMin); ok {
} else { if !oldOK || date != old {
session.removeProperty(picker.htmlID(), "min") if picker.created {
updateProperty(picker.htmlID(), Min, date.Format(dateFormat), picker.session)
}
picker.propertyChangedEvent(tag)
}
return true
} }
case DatePickerMax: case DatePickerMax:
if date, ok := GetDatePickerMax(picker); ok { old, oldOK := getDateProperty(picker, DatePickerMax, Max)
session.updateProperty(picker.htmlID(), "max", date.Format(dateFormat)) if date, ok := setTimeValue(DatePickerMax); ok {
} else { if !oldOK || date != old {
session.removeProperty(picker.htmlID(), "max") if picker.created {
updateProperty(picker.htmlID(), Max, date.Format(dateFormat), picker.session)
}
picker.propertyChangedEvent(tag)
}
return true
} }
case DatePickerStep: case DatePickerStep:
if step := GetDatePickerStep(picker); step > 0 { oldStep := GetDatePickerStep(picker)
session.updateProperty(picker.htmlID(), "step", strconv.Itoa(step)) if picker.setIntProperty(DatePickerStep, value) {
if step := GetDatePickerStep(picker); oldStep != step {
if picker.created {
if step > 0 {
updateProperty(picker.htmlID(), Step, strconv.Itoa(step), picker.session)
} else { } else {
session.removeProperty(picker.htmlID(), "step") removeProperty(picker.htmlID(), Step, picker.session)
}
}
picker.propertyChangedEvent(tag)
}
return true
} }
case DatePickerValue: case DatePickerValue:
date := GetDatePickerValue(picker) oldDate := GetDatePickerValue(picker)
session.callFunc("setInputValue", picker.htmlID(), date.Format(dateFormat)) if date, ok := setTimeValue(DatePickerValue); ok {
if date != oldDate {
if picker.created {
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, picker.htmlID(), date.Format(dateFormat)))
}
for _, listener := range picker.dateChangedListeners {
listener(picker, date)
}
picker.propertyChangedEvent(tag)
}
return true
}
if listeners := getTwoArgEventListeners[DatePicker, time.Time](picker, nil, DateChangedEvent); len(listeners) > 0 { case DateChangedEvent:
oldDate := time.Now() listeners, ok := valueToEventListeners[DatePicker, time.Time](value)
if value := picker.getRaw("old-date"); value != nil { if !ok {
if date, ok := value.(time.Time); ok { notCompatibleType(tag, value)
oldDate = date return false
} } else if listeners == nil {
} listeners = []func(DatePicker, time.Time){}
for _, listener := range listeners {
listener.Run(picker, date, oldDate)
}
} }
picker.dateChangedListeners = listeners
picker.propertyChangedEvent(tag)
return true
default: default:
picker.viewData.propertyChanged(tag) return picker.viewData.set(tag, value)
}
return false
}
func (picker *datePickerData) Get(tag string) any {
return picker.get(picker.normalizeTag(tag))
}
func (picker *datePickerData) get(tag string) any {
switch tag {
case DateChangedEvent:
return picker.dateChangedListeners
default:
return picker.viewData.get(tag)
} }
} }
@ -307,16 +270,6 @@ func (picker *datePickerData) htmlTag() string {
return "input" return "input"
} }
func (picker *datePickerData) htmlSubviews(self View, buffer *strings.Builder) {
dataListHtmlSubviews(self, buffer, func(text string, session Session) string {
text, _ = session.resolveConstants(text)
if date, ok := stringToDate(text); ok {
return date.Format(dateFormat)
}
return text
})
}
func (picker *datePickerData) htmlProperties(self View, buffer *strings.Builder) { func (picker *datePickerData) htmlProperties(self View, buffer *strings.Builder) {
picker.viewData.htmlProperties(self, buffer) picker.viewData.htmlProperties(self, buffer)
@ -348,11 +301,16 @@ func (picker *datePickerData) htmlProperties(self View, buffer *strings.Builder)
if picker.getRaw(ClickEvent) == nil { if picker.getRaw(ClickEvent) == nil {
buffer.WriteString(` onclick="stopEventPropagation(this, event)"`) buffer.WriteString(` onclick="stopEventPropagation(this, event)"`)
} }
dataListHtmlProperties(picker, buffer)
} }
func (picker *datePickerData) handleCommand(self View, command PropertyName, data DataObject) bool { func (picker *datePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
if IsDisabled(self) {
buffer.WriteString(` disabled`)
}
picker.viewData.htmlDisabledProperties(self, buffer)
}
func (picker *datePickerData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "textChanged": case "textChanged":
if text, ok := data.PropertyValue("text"); ok { if text, ok := data.PropertyValue("text"); ok {
@ -360,10 +318,9 @@ func (picker *datePickerData) handleCommand(self View, command PropertyName, dat
oldValue := GetDatePickerValue(picker) oldValue := GetDatePickerValue(picker)
picker.properties[DatePickerValue] = value picker.properties[DatePickerValue] = value
if value != oldValue { if value != oldValue {
for _, listener := range getTwoArgEventListeners[DatePicker, time.Time](picker, nil, DateChangedEvent) { for _, listener := range picker.dateChangedListeners {
listener.Run(picker, value, oldValue) listener(picker, value)
} }
picker.runChangeListener(DatePickerValue)
} }
} }
} }
@ -373,7 +330,7 @@ func (picker *datePickerData) handleCommand(self View, command PropertyName, dat
return picker.viewData.handleCommand(self, command, data) return picker.viewData.handleCommand(self, command, data)
} }
func getDateProperty(view View, mainTag, shortTag PropertyName) (time.Time, bool) { func getDateProperty(view View, mainTag, shortTag string) (time.Time, bool) {
valueToTime := func(value any) (time.Time, bool) { valueToTime := func(value any) (time.Time, bool) {
if value != nil { if value != nil {
switch value := value.(type) { switch value := value.(type) {
@ -382,7 +339,7 @@ func getDateProperty(view View, mainTag, shortTag PropertyName) (time.Time, bool
case string: case string:
if text, ok := view.Session().resolveConstants(value); ok { if text, ok := view.Session().resolveConstants(value); ok {
if result, ok := stringToDate(text); ok { if result, err := time.Parse(dateFormat, text); err == nil {
return result, true return result, true
} }
} }
@ -396,25 +353,24 @@ func getDateProperty(view View, mainTag, shortTag PropertyName) (time.Time, bool
return result, true return result, true
} }
for _, tag := range []PropertyName{mainTag, shortTag} { if value := valueFromStyle(view, shortTag); value != nil {
if value := valueFromStyle(view, tag); value != nil {
if result, ok := valueToTime(value); ok { if result, ok := valueToTime(value); ok {
return result, true return result, true
} }
} }
} }
}
return time.Now(), false return time.Now(), false
} }
// GetDatePickerMin returns the min date of DatePicker subview and "true" as the second value if the min date is set, // GetDatePickerMin returns the min date of DatePicker subview and "true" as the second value if the min date is set,
// "false" as the second value otherwise. // "false" as the second value otherwise.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDatePickerMin(view View, subviewID ...string) (time.Time, bool) { func GetDatePickerMin(view View, subviewID ...string) (time.Time, bool) {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
return getDateProperty(view, DatePickerMin, Min) return getDateProperty(view, DatePickerMin, Min)
} }
return time.Now(), false return time.Now(), false
@ -422,30 +378,30 @@ func GetDatePickerMin(view View, subviewID ...string) (time.Time, bool) {
// GetDatePickerMax returns the max date of DatePicker subview and "true" as the second value if the min date is set, // GetDatePickerMax returns the max date of DatePicker subview and "true" as the second value if the min date is set,
// "false" as the second value otherwise. // "false" as the second value otherwise.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDatePickerMax(view View, subviewID ...string) (time.Time, bool) { func GetDatePickerMax(view View, subviewID ...string) (time.Time, bool) {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
return getDateProperty(view, DatePickerMax, Max) return getDateProperty(view, DatePickerMax, Max)
} }
return time.Now(), false return time.Now(), false
} }
// GetDatePickerStep returns the date changing step in days of DatePicker subview. // GetDatePickerStep returns the date changing step in days of DatePicker subview.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDatePickerStep(view View, subviewID ...string) int { func GetDatePickerStep(view View, subviewID ...string) int {
return intStyledProperty(view, subviewID, DatePickerStep, 0) return intStyledProperty(view, subviewID, DatePickerStep, 0)
} }
// GetDatePickerValue returns the date of DatePicker subview. // GetDatePickerValue returns the date of DatePicker subview.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDatePickerValue(view View, subviewID ...string) time.Time { func GetDatePickerValue(view View, subviewID ...string) time.Time {
if view = getSubview(view, subviewID); view == nil { if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view == nil {
return time.Now() return time.Now()
} }
date, _ := getDateProperty(view, DatePickerValue, Value) date, _ := getDateProperty(view, DatePickerValue, Value)
@ -454,18 +410,7 @@ func GetDatePickerValue(view View, subviewID ...string) time.Time {
// GetDateChangedListeners returns the DateChangedListener list of an DatePicker subview. // GetDateChangedListeners returns the DateChangedListener list of an DatePicker subview.
// If there are no listeners then the empty list is returned // If there are no listeners then the empty list is returned
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetDateChangedListeners(view View, subviewID ...string) []func(DatePicker, time.Time) {
// - func(rui.DatePicker, time.Time, time.Time), return getEventListeners[DatePicker, time.Time](view, subviewID, DateChangedEvent)
// - func(rui.DatePicker, time.Time),
// - func(rui.DatePicker),
// - func(time.Time, time.Time),
// - func(time.Time),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDateChangedListeners(view View, subviewID ...string) []any {
return getTwoArgEventRawListeners[DatePicker, time.Time](view, subviewID, DateChangedEvent)
} }

View File

@ -23,9 +23,6 @@ theme {
ruiTabTextColor = #FF404040, ruiTabTextColor = #FF404040,
ruiCurrentTabColor = #FFFFFFFF, ruiCurrentTabColor = #FFFFFFFF,
ruiCurrentTabTextColor = #FF000000, ruiCurrentTabTextColor = #FF000000,
ruiTooltipBackground = #FFFFFFFF,
ruiTooltipTextColor = #FF000000,
ruiTooltipShadowColor = #FF808080,
}, },
colors:dark = _{ colors:dark = _{
ruiTextColor = #FFE0E0E0, ruiTextColor = #FFE0E0E0,
@ -46,9 +43,6 @@ theme {
ruiTabTextColor = #FFE0E0E0, ruiTabTextColor = #FFE0E0E0,
ruiCurrentTabColor = #FF000000, ruiCurrentTabColor = #FF000000,
ruiCurrentTabTextColor = #FFFFFFFF, ruiCurrentTabTextColor = #FFFFFFFF,
ruiTooltipBackground = #FF303030,
ruiTooltipTextColor = #FFDDDDDD,
ruiTooltipShadowColor = #FFDDDDDD,
}, },
constants = _{ constants = _{
ruiButtonHorizontalPadding = 16px, ruiButtonHorizontalPadding = 16px,
@ -82,7 +76,8 @@ theme {
background-color = @ruiBackgroundColor, background-color = @ruiBackgroundColor,
accent-color = @ruiHighlightColor, accent-color = @ruiHighlightColor,
}, },
ruiEnabledButton { ruiButton {
align = center,
padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding", padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding",
margin = @ruiButtonMargin, margin = @ruiButtonMargin,
radius = @ruiButtonRadius, radius = @ruiButtonRadius,
@ -91,6 +86,7 @@ theme {
border = _{width = 1px, style = solid, color = @ruiButtonTextColor} border = _{width = 1px, style = solid, color = @ruiButtonTextColor}
}, },
ruiDisabledButton { ruiDisabledButton {
align = center,
padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding", padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding",
margin = @ruiButtonMargin, margin = @ruiButtonMargin,
radius = @ruiButtonRadius, radius = @ruiButtonRadius,
@ -98,34 +94,14 @@ theme {
text-color = @ruiButtonDisabledTextColor, text-color = @ruiButtonDisabledTextColor,
border = _{width = 1px, style = solid, color = @ruiButtonDisabledTextColor} border = _{width = 1px, style = solid, color = @ruiButtonDisabledTextColor}
}, },
ruiEnabledButton:hover { ruiButton:hover {
text-color = @ruiTextColor, text-color = @ruiTextColor,
background-color = @ruiBackgroundColor, background-color = @ruiBackgroundColor,
}, },
ruiEnabledButton:focus { ruiButton:focus {
shadow = _{spread-radius = @ruiButtonHighlightDilation, blur = @ruiButtonHighlightBlur, color = @ruiHighlightColor }, shadow = _{spread-radius = @ruiButtonHighlightDilation, blur = @ruiButtonHighlightBlur, color = @ruiHighlightColor },
}, },
ruiEnabledButton:active { ruiButton:active {
background-color = @ruiButtonActiveColor
},
ruiDefaultButton {
align = center,
padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding",
margin = @ruiButtonMargin,
radius = @ruiButtonRadius,
background-color = @ruiButtonColor,
text-color = @ruiButtonTextColor,
text-weight = bold,
border = _{width = 1px, style = solid, color = @ruiButtonTextColor}
},
ruiDefaultButton:hover {
text-color = @ruiTextColor,
background-color = @ruiBackgroundColor,
},
ruiDefaultButton:focus {
shadow = _{spread-radius = @ruiButtonHighlightDilation, blur = @ruiButtonHighlightBlur, color = @ruiHighlightColor },
},
ruiDefaultButton:active {
background-color = @ruiButtonActiveColor background-color = @ruiButtonActiveColor
}, },
ruiCheckbox { ruiCheckbox {
@ -134,8 +110,8 @@ theme {
margin = 2px, margin = 2px,
}, },
ruiCheckbox:focus { ruiCheckbox:focus {
outline = _{style = solid, color = @ruiHighlightColor, width = 2px }, margin = 0,
outline-offset = -1px, border = _{style = solid, color = @ruiHighlightColor, width = 2px },
}, },
ruiListItem { ruiListItem {
radius = 4px, radius = 4px,

View File

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

View File

@ -1,70 +0,0 @@
package rui
import (
"bytes"
"math/rand"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
)
type downloadFile struct {
filename string
path string
data []byte
}
var currentDownloadId = int(rand.Int31())
var downloadFiles = map[string]downloadFile{}
func (session *sessionData) startDownload(file downloadFile) {
currentDownloadId++
id := strconv.Itoa(currentDownloadId)
downloadFiles[id] = file
session.callFunc("startDownload", id, file.filename)
}
func serveDownloadFile(id string, w http.ResponseWriter, r *http.Request) bool {
if file, ok := downloadFiles[id]; ok {
delete(downloadFiles, id)
if file.data != nil {
http.ServeContent(w, r, file.filename, time.Now(), bytes.NewReader(file.data))
return true
} else if _, err := os.Stat(file.path); err == nil {
http.ServeFile(w, r, file.path)
return true
}
}
return false
}
// DownloadFile starts downloading the file on the client side.
func (session *sessionData) DownloadFile(path string) {
if _, err := os.Stat(path); err != nil {
ErrorLog(err.Error())
return
}
_, filename := filepath.Split(path)
session.startDownload(downloadFile{
filename: filename,
path: path,
data: nil,
})
}
// DownloadFileData starts downloading the file on the client side. Arguments specify the name of the downloaded file and its contents
func (session *sessionData) DownloadFileData(filename string, data []byte) {
if data == nil {
ErrorLog("Invalid download data. Must be not nil.")
return
}
session.startDownload(downloadFile{
filename: filename,
path: "",
data: data,
})
}

View File

@ -1,722 +0,0 @@
package rui
import (
"encoding/base64"
"fmt"
"maps"
"strings"
)
const (
// DragData is the constant for "drag-data" property tag.
//
// Used by View:
//
// Supported types: map[string]string.
DragData PropertyName = "drag-data"
// DragImage is the constant for "drag-image" property tag.
//
// Used by View:
// An url of image to use for the drag feedback image.
//
// Supported type: string.
DragImage PropertyName = "drag-image"
// DragImageXOffset is the constant for "drag-image-x-offset" property tag.
//
// Used by View:
// The horizontal offset in pixels within the drag feedback image.
//
// Supported types: float, int, string.
DragImageXOffset PropertyName = "drag-image-x-offset"
// DragImageYOffset is the constant for "drag-image-y-offset" property tag.
//
// Used by View.
// The vertical offset in pixels within the drag feedback image.
//
// Supported types: float, int, string.
DragImageYOffset PropertyName = "drag-image-y-offset"
// DropEffect is the constant for "drag-effect" property tag.
//
// Used by View.
// Controls the feedback (typically visual) the user is given during a drag and drop operation.
// It will affect which cursor is displayed while dragging. For example, when the user hovers over a target drop element,
// the browser's cursor may indicate which type of operation will occur.
//
// Supported types: int, string.
//
// Values:
// - 0 (DropEffectUndefined) or "undefined" - The property value is not defined (default value).
// - 1 (DropEffectCopy) or "copy" - A copy of the source item may be made at the new location.
// - 2 (DropEffectMove) or "move" - An item may be moved to a new location.
// - 4 (DropEffectLink) or "link" - A link may be established to the source at the new location.
DropEffect PropertyName = "drag-effect"
// DropEffectAllowed is the constant for "drop-effect-allowed" property tag.
//
// Used by View.
// Specifies the effect that is allowed for a drag operation.
// The copy operation is used to indicate that the data being dragged will be copied
// from its present location to the drop location.
// The move operation is used to indicate that the data being dragged will be moved,
// and the link operation is used to indicate that some form of relationship
// or connection will be created between the source and drop locations.
//
// Supported types: int, string.
//
// Values:
// - 0 (DropEffectUndefined) or "undefined" - The property value is not defined (default value). Equivalent to DropEffectAll
// - 1 (DropEffectCopy) or "copy" - A copy of the source item may be made at the new location.
// - 2 (DropEffectMove) or "move" - An item may be moved to a new location.
// - 3 (DropEffectLink) or "link" - A link may be established to the source at the new location.
// - 4 (DropEffectCopyMove) or "copy|move" - A copy or move operation is permitted.
// - 5 (DropEffectCopyLink) or "copy|link" - A copy or link operation is permitted.
// - 6 (DropEffectLinkMove) or "link|move" - A link or move operation is permitted.
// - 7 (DropEffectAll) or "all" or "copy|move|link" - All operations are permitted.
DropEffectAllowed PropertyName = "drag-effect-allowed"
// DragStartEvent is the constant for "drag-start-event" property tag.
//
// Used by View.
// Fired when the user starts dragging an element or text selection.
//
// General listener format:
// func(view rui.View, event rui.DragAndDropEvent).
//
// where:
// - view - Interface of a view which generated this event,
// - event - event parameters.
//
// Allowed listener formats:
// func(view rui.View)
// func(rui.DragAndDropEvent)
// func()
DragStartEvent PropertyName = "drag-start-event"
// DragEndEvent is the constant for "drag-end-event" property tag.
//
// Used by View.
// Fired when a drag operation ends (by releasing a mouse button or hitting the escape key).
//
// General listener format:
// func(view rui.View, event rui.DragAndDropEvent).
//
// where:
// - view - Interface of a view which generated this event,
// - event - event parameters.
//
// Allowed listener formats:
// func(view rui.View)
// func(rui.DragAndDropEvent)
// func()
DragEndEvent PropertyName = "drag-end-event"
// DragEnterEvent is the constant for "drag-enter-event" property tag.
//
// Used by View.
// Fired when a dragged element or text selection enters a valid drop target.
//
// General listener format:
// func(view rui.View, event rui.DragAndDropEvent).
//
// where:
// - view - Interface of a view which generated this event,
// - event - event parameters.
//
// Allowed listener formats:
// func(view rui.View)
// func(rui.DragAndDropEvent)
// func()
DragEnterEvent PropertyName = "drag-enter-event"
// DragLeaveEvent is the constant for "drag-leave-event" property tag.
//
// Used by View.
// Fired when a dragged element or text selection leaves a valid drop target.
//
// General listener format:
// func(view rui.View, event rui.DragAndDropEvent).
//
// where:
// - view - Interface of a view which generated this event,
// - event - event parameters.
//
// Allowed listener formats:
// func(view rui.View)
// func(rui.DragAndDropEvent)
// func()
DragLeaveEvent PropertyName = "drag-leave-event"
// DragOverEvent is the constant for "drag-over-event" property tag.
//
// Used by View.
// Fired when an element or text selection is being dragged over a valid drop target (every few hundred milliseconds).
//
// General listener format:
// func(view rui.View, event rui.DragAndDropEvent).
//
// where:
// - view - Interface of a view which generated this event,
// - event - event parameters.
//
// Allowed listener formats:
// func(view rui.View)
// func(rui.DragAndDropEvent)
// func()
DragOverEvent PropertyName = "drag-over-event"
// DropEvent is the constant for "drop-event" property tag.
//
// Used by View.
// Fired when an element or text selection is dropped on a valid drop target.
//
// General listener format:
// func(view rui.View, event rui.DragAndDropEvent).
//
// where:
// - view - Interface of a view which generated this event,
// - event - event parameters.
//
// Allowed listener formats:
// func(view rui.View)
// func(rui.DragAndDropEvent)
// func()
DropEvent PropertyName = "drop-event"
// DropEffectUndefined - the value of the "drop-effect" and "drop-effect-allowed" properties: the value is not defined (default value).
DropEffectUndefined = 0
// DropEffectNone - the value of the DropEffect field of the DragEvent struct: the item may not be dropped.
DropEffectNone = 0
// DropEffectCopy - the value of the "drop-effect" and "drop-effect-allowed" properties: a copy of the source item may be made at the new location.
DropEffectCopy = 1
// DropEffectMove - the value of the "drop-effect" and "drop-effect-allowed" properties: an item may be moved to a new location.
DropEffectMove = 2
// DropEffectLink - the value of the "drop-effect" and "drop-effect-allowed" properties: a link may be established to the source at the new location.
DropEffectLink = 4
// DropEffectCopyMove - the value of the "drop-effect-allowed" property: a copy or move operation is permitted.
DropEffectCopyMove = DropEffectCopy + DropEffectMove
// DropEffectCopyLink - the value of the "drop-effect-allowed" property: a copy or link operation is permitted.
DropEffectCopyLink = DropEffectCopy + DropEffectLink
// DropEffectLinkMove - the value of the "drop-effect-allowed" property: a link or move operation is permitted.
DropEffectLinkMove = DropEffectLink + DropEffectMove
// DropEffectAll - the value of the "drop-effect-allowed" property: all operations (copy, move, and link) are permitted (default value).
DropEffectAll = DropEffectCopy + DropEffectMove + DropEffectLink
)
// MouseEvent represent a mouse event
type DragAndDropEvent struct {
MouseEvent
Data map[string]string
Files []FileInfo
Target View
EffectAllowed int
DropEffect int
}
func (event *DragAndDropEvent) init(session Session, data DataObject) {
event.MouseEvent.init(data)
event.Data = map[string]string{}
if value, ok := data.PropertyValue("data"); ok {
data := strings.Split(value, ";")
for _, line := range data {
pair := strings.Split(line, ":")
if len(pair) == 2 {
mime, err := base64.StdEncoding.DecodeString(pair[0])
if err != nil {
ErrorLog(err.Error())
} else {
val, err := base64.StdEncoding.DecodeString(pair[1])
if err == nil {
event.Data[string(mime)] = string(val)
} else {
ErrorLog(err.Error())
}
}
}
}
}
if targetId, ok := data.PropertyValue("target"); ok {
event.Target = session.viewByHTMLID(targetId)
}
if effect, ok := data.PropertyValue("effect-allowed"); ok {
for i, value := range []string{"undefined", "copy", "move", "copyMove", "link", "copyLink", "linkMove", "all"} {
if value == effect {
event.EffectAllowed = i
break
}
}
}
if effect, ok := data.PropertyValue("drop-effect"); ok && effect != "" {
for i, value := range []string{"none", "copy", "move", "", "link"} {
if value == effect {
event.DropEffect = i
break
}
}
}
event.Files = parseFilesTag(data)
}
func stringToDropEffect(text string) (int, bool) {
text = strings.Trim(text, " \t\n")
if n, ok := enumStringToInt(text, []string{"", "copy", "move", "", "link"}, false); ok {
switch n {
case DropEffectUndefined, DropEffectCopy, DropEffectMove, DropEffectLink:
return n, true
}
}
return 0, false
}
func (view *viewData) setDropEffect(value any) []PropertyName {
if !setSimpleProperty(view, DropEffect, value) {
if text, ok := value.(string); ok {
if n, ok := stringToDropEffect(text); ok {
if n == DropEffectUndefined {
view.setRaw(DropEffect, nil)
} else {
view.setRaw(DropEffect, n)
}
} else {
invalidPropertyValue(DropEffect, value)
return nil
}
} else if i, ok := isInt(value); ok {
switch i {
case DropEffectUndefined:
view.setRaw(DropEffect, nil)
case DropEffectCopy, DropEffectMove, DropEffectLink:
view.setRaw(DropEffect, i)
default:
invalidPropertyValue(DropEffect, value)
return nil
}
} else {
notCompatibleType(DropEffect, value)
return nil
}
}
return []PropertyName{DropEffect}
}
func stringToDropEffectAllowed(text string) (int, bool) {
if strings.ContainsRune(text, '|') {
elements := strings.Split(text, "|")
result := 0
for _, element := range elements {
if n, ok := stringToDropEffect(element); ok && n != DropEffectUndefined {
result |= n
} else {
return 0, false
}
}
return result, true
}
text = strings.Trim(text, " \t\n")
if text != "" {
if n, ok := enumStringToInt(text, []string{"undefined", "copy", "move", "", "link", "", "", "all"}, false); ok {
return n, true
}
}
return 0, false
}
func (view *viewData) setDropEffectAllowed(value any) []PropertyName {
if !setSimpleProperty(view, DropEffectAllowed, value) {
if text, ok := value.(string); ok {
if n, ok := stringToDropEffectAllowed(text); ok {
if n == DropEffectUndefined {
view.setRaw(DropEffectAllowed, nil)
} else {
view.setRaw(DropEffectAllowed, n)
}
} else {
invalidPropertyValue(DropEffectAllowed, value)
return nil
}
} else {
n, ok := isInt(value)
if !ok {
notCompatibleType(DropEffectAllowed, value)
return nil
}
if n == DropEffectUndefined {
view.setRaw(DropEffectAllowed, nil)
} else if n > DropEffectUndefined && n <= DropEffectAll {
view.setRaw(DropEffectAllowed, n)
} else {
notCompatibleType(DropEffectAllowed, value)
return nil
}
}
}
return []PropertyName{DropEffectAllowed}
}
func handleDragAndDropEvents(view View, tag PropertyName, data DataObject) {
listeners := getOneArgEventListeners[View, DragAndDropEvent](view, nil, tag)
if len(listeners) > 0 {
var event DragAndDropEvent
event.init(view.Session(), data)
for _, listener := range listeners {
listener.Run(view, event)
}
}
}
func base64DragData(view View) string {
if value := view.getRaw(DragData); value != nil {
if data, ok := value.(map[string]string); ok && len(data) > 0 {
buf := allocStringBuilder()
defer freeStringBuilder(buf)
for mime, value := range data {
if buf.Len() > 0 {
buf.WriteRune(';')
}
buf.WriteString(base64.StdEncoding.EncodeToString([]byte(mime)))
buf.WriteRune(':')
buf.WriteString(base64.StdEncoding.EncodeToString([]byte(value)))
}
return buf.String()
}
}
return ""
}
func dragAndDropHtml(view View, buffer *strings.Builder) {
if len(getOneArgEventListeners[View, DragAndDropEvent](view, nil, DropEvent)) > 0 {
buffer.WriteString(`ondragover="dragOverEvent(this, event)" ondrop="dropEvent(this, event)" `)
if len(getOneArgEventListeners[View, DragAndDropEvent](view, nil, DragOverEvent)) > 0 {
buffer.WriteString(`data-drag-over="1" `)
}
}
if dragData := base64DragData(view); dragData != "" {
buffer.WriteString(`draggable="true" data-drag="`)
buffer.WriteString(dragData)
buffer.WriteString(`" ondragstart="dragStartEvent(this, event)" `)
} else if len(getOneArgEventListeners[View, DragAndDropEvent](view, nil, DragStartEvent)) > 0 {
buffer.WriteString(` ondragstart="dragStartEvent(this, event)" `)
}
enterEvent := false
switch GetDropEffect(view) {
case DropEffectCopy:
buffer.WriteString(` data-drop-effect="copy" ondragenter="dragEnterEvent(this, event)"`)
enterEvent = true
case DropEffectMove:
buffer.WriteString(` data-drop-effect="move" ondragenter="dragEnterEvent(this, event)"`)
enterEvent = true
case DropEffectLink:
buffer.WriteString(` data-drop-effect="link" ondragenter="dragEnterEvent(this, event)"`)
enterEvent = true
}
if enterEvent {
viewEventsHtml[DragAndDropEvent](view, []PropertyName{DragEndEvent, DragLeaveEvent}, buffer)
} else {
viewEventsHtml[DragAndDropEvent](view, []PropertyName{DragEndEvent, DragEnterEvent, DragLeaveEvent}, buffer)
}
if img := GetDragImage(view); img != "" {
buffer.WriteString(` data-drag-image="`)
buffer.WriteString(img)
buffer.WriteString(`" `)
}
if f := GetDragImageXOffset(view); f != 0 {
buffer.WriteString(` data-drag-image-x="`)
fmt.Fprintf(buffer, "%g", f)
buffer.WriteString(`" `)
}
if f := GetDragImageYOffset(view); f != 0 {
buffer.WriteString(` data-drag-image-y="`)
fmt.Fprintf(buffer, "%g", f)
buffer.WriteString(`" `)
}
effects := []string{"undefined", "copy", "move", "copyMove", "link", "copyLink", "linkMove", "all"}
if n := GetDropEffectAllowed(view); n > 0 && n < len(effects) {
buffer.WriteString(` data-drop-effect-allowed="`)
buffer.WriteString(effects[n])
buffer.WriteString(`" `)
}
}
func (view *viewData) LoadFile(file FileInfo, result func(FileInfo, []byte)) {
if result != nil {
view.fileLoader[file.key()] = result
view.Session().callFunc("loadDropFile", view.htmlID(), file.Name, file.Size)
}
}
// GetDragStartEventListeners returns the "drag-start-event" listener list. If there are no listeners then the empty list is returned.
//
// Result elements can be of the following types:
// - func(rui.View, rui.DragAndDropEvent),
// - func(rui.View),
// - func(rui.DragAndDropEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragStartEventListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, DragAndDropEvent](view, subviewID, DragStartEvent)
}
// GetDragEndEventListeners returns the "drag-end-event" listener list. If there are no listeners then the empty list is returned.
//
// Result elements can be of the following types:
// - func(rui.View, rui.DragAndDropEvent),
// - func(rui.View),
// - func(rui.DragAndDropEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragEndEventListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, DragAndDropEvent](view, subviewID, DragEndEvent)
}
// GetDragEnterEventListeners returns the "drag-enter-event" listener list. If there are no listeners then the empty list is returned.
//
// Result elements can be of the following types:
// - func(rui.View, rui.DragAndDropEvent),
// - func(rui.View),
// - func(rui.DragAndDropEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragEnterEventListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, DragAndDropEvent](view, subviewID, DragEnterEvent)
}
// GetDragLeaveEventListeners returns the "drag-leave-event" listener list. If there are no listeners then the empty list is returned.
//
// Result elements can be of the following types:
// - func(rui.View, rui.DragAndDropEvent),
// - func(rui.View),
// - func(rui.DragAndDropEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragLeaveEventListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, DragAndDropEvent](view, subviewID, DragLeaveEvent)
}
// GetDragOverEventListeners returns the "drag-over-event" listener list. If there are no listeners then the empty list is returned.
//
// Result elements can be of the following types:
// - func(rui.View, rui.DragAndDropEvent),
// - func(rui.View),
// - func(rui.DragAndDropEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragOverEventListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, DragAndDropEvent](view, subviewID, DragOverEvent)
}
// GetDropEventListeners returns the "drag-start-event" listener list. If there are no listeners then the empty list is returned.
//
// Result elements can be of the following types:
// - func(rui.View, rui.DragAndDropEvent),
// - func(rui.View),
// - func(rui.DragAndDropEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDropEventListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, DragAndDropEvent](view, subviewID, DropEvent)
}
// GetDropEventListeners returns the "drag-data" data.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragData(view View, subviewID ...string) map[string]string {
result := map[string]string{}
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(DragData); value != nil {
if data, ok := value.(map[string]string); ok {
maps.Copy(result, data)
}
}
}
return result
}
// GetDragImage returns the drag feedback image.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragImage(view View, subviewID ...string) string {
if view = getSubview(view, subviewID); view != nil {
value := view.getRaw(DragImage)
if value == nil {
value = valueFromStyle(view, DragImage)
}
if value != nil {
if img, ok := value.(string); ok {
img = strings.Trim(img, " \t")
if ok, constName := isConstantName(img); ok {
if img, ok = view.Session().ImageConstant(constName); ok {
return img
}
} else {
return img
}
}
}
}
return ""
}
// GetDragImageXOffset returns the horizontal offset in pixels within the drag feedback image.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragImageXOffset(view View, subviewID ...string) float64 {
return floatStyledProperty(view, subviewID, DragImageXOffset, 0)
}
// GetDragImageYOffset returns the vertical offset in pixels within the drag feedback image.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragImageYOffset(view View, subviewID ...string) float64 {
return floatStyledProperty(view, subviewID, DragImageYOffset, 0)
}
// GetDropEffect returns the effect that is allowed for a drag operation.
// Controls the feedback (typically visual) the user is given during a drag and drop operation.
// It will affect which cursor is displayed while dragging.
//
// Returns one of next values:
// - 0 (DropEffectUndefined) - The value is not defined (all operations are permitted).
// - 1 (DropEffectCopy) - A copy of the source item may be made at the new location.
// - 2 (DropEffectMove) - An item may be moved to a new location.
// - 4 (DropEffectLink) - A link may be established to the source at the new location.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDropEffect(view View, subviewID ...string) int {
if view = getSubview(view, subviewID); view != nil {
value := view.getRaw(DropEffect)
if value == nil {
value = valueFromStyle(view, DropEffect)
}
if value != nil {
switch value := value.(type) {
case int:
return value
case string:
if value, ok := view.Session().resolveConstants(value); ok {
if n, ok := stringToDropEffect(value); ok {
return n
}
}
default:
return DropEffectUndefined
}
}
}
return DropEffectUndefined
}
// GetDropEffectAllowed returns the effect that is allowed for a drag operation.
// The copy operation is used to indicate that the data being dragged will be copied from its present location to the drop location.
// The move operation is used to indicate that the data being dragged will be moved,
// and the link operation is used to indicate that some form of relationship
// or connection will be created between the source and drop locations.
//
// Returns one of next values:
// - 0 (DropEffectUndefined) - The value is not defined (all operations are permitted).
// - 1 (DropEffectCopy) - A copy of the source item may be made at the new location.
// - 2 (DropEffectMove) - An item may be moved to a new location.
// - 4 (DropEffectLink) - A link may be established to the source at the new location.
// - 3 (DropEffectCopyMove) - A copy or move operation is permitted.
// - 5 (DropEffectCopyLink) - A copy or link operation is permitted.
// - 6 (DropEffectLinkMove) - A link or move operation is permitted.
// - 7 (DropEffectAll) - All operations are permitted.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDropEffectAllowed(view View, subviewID ...string) int {
if view = getSubview(view, subviewID); view != nil {
value := view.getRaw(DropEffectAllowed)
if value == nil {
value = valueFromStyle(view, DropEffectAllowed)
}
if value != nil {
switch value := value.(type) {
case int:
return value
case string:
if value, ok := view.Session().resolveConstants(value); ok {
if n, ok := stringToDropEffectAllowed(value); ok {
return n
}
}
default:
return DropEffectUndefined
}
}
}
return DropEffectUndefined
}

View File

@ -1,37 +1,27 @@
package rui package rui
import ( import (
"fmt"
"strconv" "strconv"
"strings" "strings"
) )
// DropDownEvent is the constant for "drop-down-event" property tag. // DropDownEvent is the constant for "drop-down-event" property tag.
// // The "drop-down-event" event occurs when a list item becomes selected.
// Used by DropDownList. // The main listener format: func(DropDownList, int), where the second argument is the item index.
// Occur when a list item becomes selected. const DropDownEvent = "drop-down-event"
//
// General listener format:
//
// func(list rui.DropDownList, index int)
//
// where:
// - list - Interface of a drop down list which generated this event,
// - index - Index of a newly selected item.
//
// Allowed listener formats:
//
// func(index int)
// func(list rui.DropDownList)
// func()
const DropDownEvent PropertyName = "drop-down-event"
// DropDownList represent a DropDownList view // DropDownList - the interface of a drop-down list view
type DropDownList interface { type DropDownList interface {
View View
getItems() []string
} }
type dropDownListData struct { type dropDownListData struct {
viewData viewData
items []string
disabledItems []any
dropDownListener []func(DropDownList, int)
} }
// NewDropDownList create new DropDownList object and return it // NewDropDownList create new DropDownList object and return it
@ -43,162 +33,301 @@ func NewDropDownList(session Session, params Params) DropDownList {
} }
func newDropDownList(session Session) View { func newDropDownList(session Session) View {
return new(dropDownListData) return NewDropDownList(session, nil)
} }
func (list *dropDownListData) init(session Session) { func (list *dropDownListData) init(session Session) {
list.viewData.init(session) list.viewData.init(session)
list.tag = "DropDownList" list.tag = "DropDownList"
list.hasHtmlDisabled = true list.items = []string{}
list.normalize = normalizeDropDownListTag list.disabledItems = []any{}
list.get = list.getFunc list.dropDownListener = []func(DropDownList, int){}
list.set = list.setFunc }
list.changed = list.propertyChanged
func (list *dropDownListData) String() string {
return getViewString(list)
} }
func (list *dropDownListData) Focusable() bool { func (list *dropDownListData) Focusable() bool {
return true return true
} }
func normalizeDropDownListTag(tag PropertyName) PropertyName { func (list *dropDownListData) Remove(tag string) {
tag = defaultNormalize(tag) list.remove(strings.ToLower(tag))
if tag == "separators" {
return ItemSeparators
}
return tag
} }
func (list *dropDownListData) getFunc(tag PropertyName) any { func (list *dropDownListData) remove(tag string) {
switch tag {
case DropDownEvent:
if listeners := getTwoArgEventRawListeners[DropDownList, int](list, nil, tag); len(listeners) > 0 {
return listeners
}
return nil
}
return list.viewData.getFunc(tag)
}
func (list *dropDownListData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag { switch tag {
case Items: case Items:
if items, ok := anyToStringArray(value, ""); ok { if len(list.items) > 0 {
return setArrayPropertyValue(list, tag, items) list.items = []string{}
if list.created {
updateInnerHTML(list.htmlID(), list.session)
}
list.propertyChangedEvent(tag)
} }
notCompatibleType(Items, value)
return nil
case DisabledItems, ItemSeparators: case DisabledItems:
if items, ok := parseIndicesArray(value); ok { if len(list.disabledItems) > 0 {
return setArrayPropertyValue(list, tag, items) list.disabledItems = []any{}
if list.created {
updateInnerHTML(list.htmlID(), list.session)
}
list.propertyChangedEvent(tag)
} }
notCompatibleType(tag, value)
return nil
case DropDownEvent: case DropDownEvent:
return setTwoArgEventListener[DropDownList, int](list, tag, value) if len(list.dropDownListener) > 0 {
list.dropDownListener = []func(DropDownList, int){}
case Current: list.propertyChangedEvent(tag)
list.setRaw("old-current", GetCurrent(list))
return setIntProperty(list, Current, value)
} }
return list.viewData.setFunc(tag, value) case Current:
oldCurrent := GetCurrent(list)
delete(list.properties, Current)
if oldCurrent != 0 {
if list.created {
list.session.runScript(fmt.Sprintf(`selectDropDownListItem('%s', %d)`, list.htmlID(), 0))
}
list.onSelectedItemChanged(0)
}
default:
list.viewData.remove(tag)
return
}
} }
func (list *dropDownListData) propertyChanged(tag PropertyName) { func (list *dropDownListData) Set(tag string, value any) bool {
return list.set(strings.ToLower(tag), value)
}
func (list *dropDownListData) set(tag string, value any) bool {
if value == nil {
list.remove(tag)
return true
}
switch tag { switch tag {
case Items, DisabledItems, ItemSeparators: case Items:
updateInnerHTML(list.htmlID(), list.Session()) return list.setItems(value)
case DisabledItems:
return list.setDisabledItems(value)
case DropDownEvent:
listeners, ok := valueToEventListeners[DropDownList, int](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(DropDownList, int){}
}
list.dropDownListener = listeners
list.propertyChangedEvent(tag)
return true
case Current: case Current:
current := GetCurrent(list) oldCurrent := GetCurrent(list)
list.Session().callFunc("selectDropDownListItem", list.htmlID(), current) if !list.setIntProperty(Current, value) {
return false
oldCurrent, _ := intProperty(list, "old-current", list.Session(), -1)
for _, listener := range getTwoArgEventListeners[DropDownList, int](list, nil, DropDownEvent) {
listener.Run(list, current, oldCurrent)
} }
default: if current := GetCurrent(list); oldCurrent != current {
list.viewData.propertyChanged(tag) if list.created {
list.session.runScript(fmt.Sprintf(`selectDropDownListItem('%s', %d)`, list.htmlID(), current))
} }
list.onSelectedItemChanged(current)
}
return true
}
return list.viewData.set(tag, value)
} }
func intArrayToStringArray[T int | uint | int8 | uint8 | int16 | uint16 | int32 | uint32 | int64 | uint64](array []T) []string { func (list *dropDownListData) setItems(value any) bool {
items := make([]string, len(array))
for i, val := range array {
items[i] = strconv.Itoa(int(val))
}
return items
}
func parseIndicesArray(value any) ([]any, bool) {
switch value := value.(type) { switch value := value.(type) {
case int:
return []any{value}, true
case []int:
items := make([]any, len(value))
for i, n := range value {
items[i] = n
}
return items, true
case []any:
items := make([]any, 0, len(value))
for _, val := range value {
if val != nil {
switch val := val.(type) {
case string: case string:
if ok, _ := isConstantName(val); ok { list.items = []string{value}
items = append(items, val)
} else if n, err := strconv.Atoi(val); err == nil {
items = append(items, n)
} else {
return nil, false
}
default:
if n, ok := isInt(val); ok {
items = append(items, n)
} else {
return nil, false
}
}
}
}
return items, true
case []string: case []string:
items := make([]any, 0, len(value)) list.items = value
for _, str := range value {
if str = strings.Trim(str, " \t"); str != "" {
if ok, _ := isConstantName(str); ok {
items = append(items, str)
} else if n, err := strconv.Atoi(str); err == nil {
items = append(items, n)
} else {
return nil, false
}
}
}
return items, true
case string:
return parseIndicesArray(strings.Split(value, ","))
case []DataValue: case []DataValue:
items := make([]string, 0, len(value)) list.items = make([]string, 0, len(value))
for _, val := range value { for _, val := range value {
if !val.IsObject() { if !val.IsObject() {
items = append(items, val.Value()) list.items = append(list.items, val.Value())
} }
} }
return parseIndicesArray(items)
}
return nil, false case []fmt.Stringer:
list.items = make([]string, len(value))
for i, str := range value {
list.items[i] = str.String()
}
case []any:
items := make([]string, 0, len(value))
for _, v := range value {
switch val := v.(type) {
case string:
items = append(items, val)
case fmt.Stringer:
items = append(items, val.String())
case bool:
if val {
items = append(items, "true")
} else {
items = append(items, "false")
}
case float32:
items = append(items, fmt.Sprintf("%g", float64(val)))
case float64:
items = append(items, fmt.Sprintf("%g", val))
case rune:
items = append(items, string(val))
default:
if n, ok := isInt(v); ok {
items = append(items, strconv.Itoa(n))
} else {
notCompatibleType(Items, value)
return false
}
}
}
list.items = items
default:
notCompatibleType(Items, value)
return false
}
if list.created {
updateInnerHTML(list.htmlID(), list.session)
}
list.propertyChangedEvent(Items)
return true
}
func (list *dropDownListData) setDisabledItems(value any) bool {
switch value := value.(type) {
case []int:
list.disabledItems = make([]any, len(value))
for i, n := range value {
list.disabledItems[i] = n
}
case []any:
disabledItems := make([]any, len(value))
for i, val := range value {
if val == nil {
notCompatibleType(DisabledItems, value)
return false
}
switch val := val.(type) {
case string:
if isConstantName(val) {
disabledItems[i] = val
} else {
n, err := strconv.Atoi(val)
if err != nil {
notCompatibleType(DisabledItems, value)
return false
}
disabledItems[i] = n
}
default:
if n, ok := isInt(val); ok {
disabledItems[i] = n
} else {
notCompatibleType(DisabledItems, value)
return false
}
}
}
list.disabledItems = disabledItems
case string:
values := strings.Split(value, ",")
disabledItems := make([]any, len(values))
for i, str := range values {
str = strings.Trim(str, " ")
if str == "" {
notCompatibleType(DisabledItems, value)
return false
}
if isConstantName(str) {
disabledItems[i] = str
} else {
n, err := strconv.Atoi(str)
if err != nil {
notCompatibleType(DisabledItems, value)
return false
}
disabledItems[i] = n
}
}
list.disabledItems = disabledItems
case []DataValue:
disabledItems := make([]string, 0, len(value))
for _, val := range value {
if !val.IsObject() {
disabledItems = append(disabledItems, val.Value())
}
}
return list.setDisabledItems(disabledItems)
default:
notCompatibleType(DisabledItems, value)
return false
}
if list.created {
updateInnerHTML(list.htmlID(), list.session)
}
list.propertyChangedEvent(Items)
return true
}
func (list *dropDownListData) Get(tag string) any {
return list.get(strings.ToLower(tag))
}
func (list *dropDownListData) get(tag string) any {
switch tag {
case Items:
return list.items
case DisabledItems:
return list.disabledItems
case Current:
result, _ := intProperty(list, Current, list.session, 0)
return result
case DropDownEvent:
return list.dropDownListener
}
return list.viewData.get(tag)
}
func (list *dropDownListData) getItems() []string {
return list.items
} }
func (list *dropDownListData) htmlTag() string { func (list *dropDownListData) htmlTag() string {
@ -206,12 +335,11 @@ func (list *dropDownListData) htmlTag() string {
} }
func (list *dropDownListData) htmlSubviews(self View, buffer *strings.Builder) { func (list *dropDownListData) htmlSubviews(self View, buffer *strings.Builder) {
if items := GetDropDownItems(list); len(items) > 0 { if list.items != nil {
current := GetCurrent(list) current := GetCurrent(list)
notTranslate := GetNotTranslate(list) notTranslate := GetNotTranslate(list)
disabledItems := GetDropDownDisabledItems(list) disabledItems := GetDropDownDisabledItems(list)
separators := GetDropDownItemSeparators(list) for i, item := range list.items {
for i, item := range items {
disabled := false disabled := false
for _, index := range disabledItems { for _, index := range disabledItems {
if i == index { if i == index {
@ -233,12 +361,6 @@ func (list *dropDownListData) htmlSubviews(self View, buffer *strings.Builder) {
buffer.WriteString(item) buffer.WriteString(item)
buffer.WriteString("</option>") buffer.WriteString("</option>")
for _, index := range separators {
if i == index {
buffer.WriteString("<hr>")
break
}
}
} }
} }
} }
@ -248,19 +370,28 @@ func (list *dropDownListData) htmlProperties(self View, buffer *strings.Builder)
buffer.WriteString(` size="1" onchange="dropDownListEvent(this, event)"`) buffer.WriteString(` size="1" onchange="dropDownListEvent(this, event)"`)
} }
func (list *dropDownListData) handleCommand(self View, command PropertyName, data DataObject) bool { func (list *dropDownListData) htmlDisabledProperties(self View, buffer *strings.Builder) {
list.viewData.htmlDisabledProperties(self, buffer)
if IsDisabled(list) {
buffer.WriteString(`disabled`)
}
}
func (list *dropDownListData) onSelectedItemChanged(number int) {
for _, listener := range list.dropDownListener {
listener(list, number)
}
list.propertyChangedEvent(Current)
}
func (list *dropDownListData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "itemSelected": case "itemSelected":
if text, ok := data.PropertyValue("number"); ok { if text, ok := data.PropertyValue("number"); ok {
if number, err := strconv.Atoi(text); err == nil { if number, err := strconv.Atoi(text); err == nil {
items := GetDropDownItems(list) if GetCurrent(list) != number && number >= 0 && number < len(list.items) {
if GetCurrent(list) != number && number >= 0 && number < len(items) {
old := GetCurrent(list)
list.properties[Current] = number list.properties[Current] = number
for _, listener := range getTwoArgEventListeners[DropDownList, int](list, nil, DropDownEvent) { list.onSelectedItemChanged(number)
listener.Run(list, number, old)
}
list.runChangeListener(Current)
} }
} else { } else {
ErrorLog(err.Error()) ErrorLog(err.Error())
@ -274,40 +405,34 @@ func (list *dropDownListData) handleCommand(self View, command PropertyName, dat
} }
// GetDropDownListeners returns the "drop-down-event" listener list. If there are no listeners then the empty list is returned. // GetDropDownListeners returns the "drop-down-event" listener list. If there are no listeners then the empty list is returned.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetDropDownListeners(view View, subviewID ...string) []func(DropDownList, int) {
// - func(rui.DropDownList, int, int), return getEventListeners[DropDownList, int](view, subviewID, DropDownEvent)
// - func(rui.DropDownList, int),
// - func(rui.DropDownList),
// - func(int, int),
// - func(int),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDropDownListeners(view View, subviewID ...string) []any {
return getTwoArgEventRawListeners[DropDownList, int](view, subviewID, DropDownEvent)
} }
// GetDropDownItems return the DropDownList items list. // GetDropDownItems return the DropDownList items list.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDropDownItems(view View, subviewID ...string) []string { func GetDropDownItems(view View, subviewID ...string) []string {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
if value := view.Get(Items); value != nil { view = ViewByID(view, subviewID[0])
if items, ok := value.([]string); ok {
return items
} }
if view != nil {
if list, ok := view.(DropDownList); ok {
return list.getItems()
} }
} }
return []string{} return []string{}
} }
func getIndicesArray(view View, tag PropertyName) []int { // GetDropDownDisabledItems return the list of DropDownList disabled item indexes.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetDropDownDisabledItems(view View, subviewID ...string) []int {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil { if view != nil {
if value := view.Get(tag); value != nil { if value := view.Get(DisabledItems); value != nil {
if values, ok := value.([]any); ok { if values, ok := value.([]any); ok {
count := len(values) count := len(values)
if count > 0 { if count > 0 {
@ -318,8 +443,8 @@ func getIndicesArray(view View, tag PropertyName) []int {
result = append(result, value) result = append(result, value)
case string: case string:
if ok, constName := isConstantName(value); ok { if value != "" && value[0] == '@' {
if val, ok := view.Session().Constant(constName); ok { if val, ok := view.Session().Constant(value[1:]); ok {
if n, err := strconv.Atoi(val); err == nil { if n, err := strconv.Atoi(val); err == nil {
result = append(result, n) result = append(result, n)
} }
@ -334,21 +459,3 @@ func getIndicesArray(view View, tag PropertyName) []int {
} }
return []int{} return []int{}
} }
// GetDropDownDisabledItems return an array of disabled(non selectable) items indices of DropDownList.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDropDownDisabledItems(view View, subviewID ...string) []int {
view = getSubview(view, subviewID)
return getIndicesArray(view, DisabledItems)
}
// GetDropDownItemSeparators return an array of indices of DropDownList items after which a separator should be added.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDropDownItemSeparators(view View, subviewID ...string) []int {
view = getSubview(view, subviewID)
return getIndicesArray(view, ItemSeparators)
}

View File

@ -1,107 +1,48 @@
package rui package rui
import ( import (
"fmt"
"strconv" "strconv"
"strings" "strings"
) )
// Constants for [EditView] specific properties and events
const ( const (
// EditTextChangedEvent is the constant for "edit-text-changed" property tag. // EditTextChangedEvent is the constant for the "edit-text-changed" property tag.
// EditTextChangedEvent = "edit-text-changed"
// Used by EditView. // EditViewType is the constant for the "edit-view-type" property tag.
// Occur when edit view text has been changed. EditViewType = "edit-view-type"
// // EditViewPattern is the constant for the "edit-view-pattern" property tag.
// General listener format: EditViewPattern = "edit-view-pattern"
// func(editView rui.EditView, newText string, oldText string). // Spellcheck is the constant for the "spellcheck" property tag.
// Spellcheck = "spellcheck"
// where:
// - editView - Interface of an edit view which generated this event,
// - newText - New edit view text,
// - oldText - Previous edit view text.
//
// Allowed listener formats:
// - func(editView rui.EditView, newText string)
// - func(newText string, oldText string)
// - func(newText string)
// - func(editView rui.EditView)
// - func()
EditTextChangedEvent PropertyName = "edit-text-changed"
// EditViewType is the constant for "edit-view-type" property tag.
//
// Used by EditView.
// Type of the text input. Default value is "text".
//
// Supported types: int, string.
//
// Values:
// - 0 (SingleLineText) or "text" - One-line text editor.
// - 1 (PasswordText) or "password" - Password editor. The text is hidden by asterisks.
// - 2 (EmailText) or "email" - Single e-mail editor.
// - 3 (EmailsText) or "emails" - Multiple e-mail editor.
// - 4 (URLText) or "url" - Internet address input editor.
// - 5 (PhoneText) or "phone" - Phone number editor.
// - 6 (MultiLineText) or "multiline" - Multi-line text editor.
EditViewType PropertyName = "edit-view-type"
// EditViewPattern is the constant for "edit-view-pattern" property tag.
//
// Used by EditView.
// Regular expression to limit editing of a text.
//
// Supported types: string.
EditViewPattern PropertyName = "edit-view-pattern"
// Spellcheck is the constant for "spellcheck" property tag.
//
// Used by EditView.
// Enable or disable spell checker. Available in SingleLineText and MultiLineText types of edit view. Default value is
// false.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", "1" - Enable spell checker for text.
// - false, 0, "false", "no", "off", "0" - Disable spell checker for text.
Spellcheck PropertyName = "spellcheck"
) )
// Constants for the values of an [EditView] "edit-view-type" property
const ( const (
// SingleLineText - single-line text type of EditView // SingleLineText - single-line text type of EditView
SingleLineText = 0 SingleLineText = 0
// PasswordText - password type of EditView // PasswordText - password type of EditView
PasswordText = 1 PasswordText = 1
// EmailText - e-mail type of EditView. Allows to enter one email // EmailText - e-mail type of EditView. Allows to enter one email
EmailText = 2 EmailText = 2
// EmailsText - e-mail type of EditView. Allows to enter multiple emails separeted by comma
// EmailsText - e-mail type of EditView. Allows to enter multiple emails separated by comma
EmailsText = 3 EmailsText = 3
// URLText - url type of EditView. Allows to enter one url // URLText - url type of EditView. Allows to enter one url
URLText = 4 URLText = 4
// PhoneText - telephone type of EditView. Allows to enter one phone number // PhoneText - telephone type of EditView. Allows to enter one phone number
PhoneText = 5 PhoneText = 5
// MultiLineText - multi-line text type of EditView // MultiLineText - multi-line text type of EditView
MultiLineText = 6 MultiLineText = 6
) )
// EditView represent an EditView view // EditView - grid-container of View
type EditView interface { type EditView interface {
View View
// AppendText appends text to the current text of an EditView view
AppendText(text string) AppendText(text string)
textChanged(newText, oldText string)
} }
type editViewData struct { type editViewData struct {
viewData viewData
textChangeListeners []func(EditView, string)
} }
// NewEditView create new EditView object and return it // NewEditView create new EditView object and return it
@ -113,25 +54,25 @@ func NewEditView(session Session, params Params) EditView {
} }
func newEditView(session Session) View { func newEditView(session Session) View {
return new(editViewData) // NewEditView(session, nil) return NewEditView(session, nil)
} }
func (edit *editViewData) init(session Session) { func (edit *editViewData) init(session Session) {
edit.viewData.init(session) edit.viewData.init(session)
edit.hasHtmlDisabled = true edit.textChangeListeners = []func(EditView, string){}
edit.tag = "EditView" edit.tag = "EditView"
edit.normalize = normalizeEditViewTag }
edit.get = edit.getFunc
edit.set = edit.setFunc func (edit *editViewData) String() string {
edit.changed = edit.propertyChanged return getViewString(edit)
} }
func (edit *editViewData) Focusable() bool { func (edit *editViewData) Focusable() bool {
return true return true
} }
func normalizeEditViewTag(tag PropertyName) PropertyName { func (edit *editViewData) normalizeTag(tag string) string {
tag = defaultNormalize(tag) tag = strings.ToLower(tag)
switch tag { switch tag {
case Type, "edit-type": case Type, "edit-type":
return EditViewType return EditViewType
@ -146,144 +87,307 @@ func normalizeEditViewTag(tag PropertyName) PropertyName {
return EditWrap return EditWrap
} }
return normalizeDataListTag(tag) return tag
} }
func (edit *editViewData) getFunc(tag PropertyName) any { func (edit *editViewData) Remove(tag string) {
switch tag { edit.remove(edit.normalizeTag(tag))
case EditTextChangedEvent:
if listeners := getTwoArgEventRawListeners[EditView, string](edit, nil, tag); len(listeners) > 0 {
return listeners
}
return nil
}
return edit.viewData.getFunc(tag)
} }
func (edit *editViewData) setFunc(tag PropertyName, value any) []PropertyName { func (edit *editViewData) remove(tag string) {
_, exists := edit.properties[tag]
switch tag { switch tag {
case Text:
if text, ok := value.(string); ok {
old := ""
if val := edit.getRaw(Text); val != nil {
if txt, ok := val.(string); ok {
old = txt
}
}
edit.setRaw("old-text", old)
edit.setRaw(tag, text)
return []PropertyName{tag}
}
notCompatibleType(tag, value)
return nil
case Hint: case Hint:
if text, ok := value.(string); ok { if exists {
return setStringPropertyValue(edit, tag, strings.Trim(text, " \t\n")) delete(edit.properties, Hint)
if edit.created {
removeProperty(edit.htmlID(), "placeholder", edit.session)
} }
notCompatibleType(tag, value) edit.propertyChangedEvent(tag)
return nil
case DataList:
setDataList(edit, value, "")
case EditTextChangedEvent:
return setTwoArgEventListener[EditView, string](edit, tag, value)
}
return edit.viewData.setFunc(tag, value)
}
func (edit *editViewData) propertyChanged(tag PropertyName) {
session := edit.Session()
switch tag {
case Text:
text := GetText(edit)
session.callFunc("setInputValue", edit.htmlID(), text)
old := ""
if val := edit.getRaw("old-text"); val != nil {
if txt, ok := val.(string); ok {
old = txt
}
}
edit.textChanged(text, old)
case Hint:
if text := GetHint(edit); text != "" {
session.updateProperty(edit.htmlID(), "placeholder", text)
} else {
session.removeProperty(edit.htmlID(), "placeholder")
} }
case MaxLength: case MaxLength:
if maxLength := GetMaxLength(edit); maxLength > 0 { if exists {
session.updateProperty(edit.htmlID(), "maxlength", strconv.Itoa(maxLength)) delete(edit.properties, MaxLength)
} else { if edit.created {
session.removeProperty(edit.htmlID(), "maxlength") removeProperty(edit.htmlID(), "maxlength", edit.session)
}
edit.propertyChangedEvent(tag)
} }
case ReadOnly: case ReadOnly, Spellcheck:
if IsReadOnly(edit) { if exists {
session.updateProperty(edit.htmlID(), "readonly", "") delete(edit.properties, tag)
} else { if edit.created {
session.removeProperty(edit.htmlID(), "readonly") updateBoolProperty(edit.htmlID(), tag, false, edit.session)
}
edit.propertyChangedEvent(tag)
} }
case Spellcheck: case EditTextChangedEvent:
session.updateProperty(edit.htmlID(), "spellcheck", IsSpellcheck(edit)) if len(edit.textChangeListeners) > 0 {
edit.textChangeListeners = []func(EditView, string){}
edit.propertyChangedEvent(tag)
}
case Text:
if exists {
oldText := GetText(edit)
delete(edit.properties, tag)
if oldText != "" {
edit.textChanged("")
if edit.created {
edit.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, edit.htmlID(), ""))
}
}
}
case EditViewPattern: case EditViewPattern:
if text := GetEditViewPattern(edit); text != "" { if exists {
session.updateProperty(edit.htmlID(), "pattern", text) oldText := GetEditViewPattern(edit)
} else { delete(edit.properties, tag)
session.removeProperty(edit.htmlID(), "pattern") if oldText != "" {
if edit.created {
removeProperty(edit.htmlID(), Pattern, edit.session)
}
edit.propertyChangedEvent(tag)
}
} }
case EditViewType: case EditViewType:
updateInnerHTML(edit.parentHTMLID(), session) if exists {
oldType := GetEditViewType(edit)
delete(edit.properties, tag)
if oldType != 0 {
if edit.created {
updateInnerHTML(edit.parentHTMLID(), edit.session)
}
edit.propertyChangedEvent(tag)
}
}
case EditWrap: case EditWrap:
if wrap := IsEditViewWrap(edit); wrap { if exists {
session.updateProperty(edit.htmlID(), "wrap", "soft") oldWrap := IsEditViewWrap(edit)
delete(edit.properties, tag)
if GetEditViewType(edit) == MultiLineText {
if wrap := IsEditViewWrap(edit); wrap != oldWrap {
if edit.created {
if wrap {
updateProperty(edit.htmlID(), "wrap", "soft", edit.session)
} else { } else {
session.updateProperty(edit.htmlID(), "wrap", "off") updateProperty(edit.htmlID(), "wrap", "off", edit.session)
}
}
edit.propertyChangedEvent(tag)
}
}
} }
case DataList:
updateInnerHTML(edit.htmlID(), session)
default: default:
edit.viewData.propertyChanged(tag) edit.viewData.remove(tag)
return
} }
} }
func (edit *editViewData) Set(tag string, value any) bool {
return edit.set(edit.normalizeTag(tag), value)
}
func (edit *editViewData) set(tag string, value any) bool {
if value == nil {
edit.remove(tag)
return true
}
switch tag {
case Text:
oldText := GetText(edit)
if text, ok := value.(string); ok {
edit.properties[Text] = text
if text = GetText(edit); oldText != text {
edit.textChanged(text)
if edit.created {
if GetEditViewType(edit) == MultiLineText {
updateInnerHTML(edit.htmlID(), edit.Session())
} else {
text = strings.ReplaceAll(text, `"`, `\"`)
text = strings.ReplaceAll(text, `'`, `\'`)
text = strings.ReplaceAll(text, "\n", `\n`)
text = strings.ReplaceAll(text, "\r", `\r`)
edit.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, edit.htmlID(), text))
}
}
}
return true
}
return false
case Hint:
oldText := GetHint(edit)
if text, ok := value.(string); ok {
edit.properties[Hint] = text
if text = GetHint(edit); oldText != text {
if edit.created {
if text != "" {
updateProperty(edit.htmlID(), "placeholder", text, edit.session)
} else {
removeProperty(edit.htmlID(), "placeholder", edit.session)
}
}
edit.propertyChangedEvent(tag)
}
return true
}
return false
case MaxLength:
oldMaxLength := GetMaxLength(edit)
if edit.setIntProperty(MaxLength, value) {
if maxLength := GetMaxLength(edit); maxLength != oldMaxLength {
if edit.created {
if maxLength > 0 {
updateProperty(edit.htmlID(), "maxlength", strconv.Itoa(maxLength), edit.session)
} else {
removeProperty(edit.htmlID(), "maxlength", edit.session)
}
}
edit.propertyChangedEvent(tag)
}
return true
}
return false
case ReadOnly:
if edit.setBoolProperty(ReadOnly, value) {
if edit.created {
if IsReadOnly(edit) {
updateProperty(edit.htmlID(), ReadOnly, "", edit.session)
} else {
removeProperty(edit.htmlID(), ReadOnly, edit.session)
}
}
edit.propertyChangedEvent(tag)
return true
}
return false
case Spellcheck:
if edit.setBoolProperty(Spellcheck, value) {
if edit.created {
updateBoolProperty(edit.htmlID(), Spellcheck, IsSpellcheck(edit), edit.session)
}
edit.propertyChangedEvent(tag)
return true
}
return false
case EditViewPattern:
oldText := GetEditViewPattern(edit)
if text, ok := value.(string); ok {
edit.properties[EditViewPattern] = text
if text = GetEditViewPattern(edit); oldText != text {
if edit.created {
if text != "" {
updateProperty(edit.htmlID(), Pattern, text, edit.session)
} else {
removeProperty(edit.htmlID(), Pattern, edit.session)
}
}
edit.propertyChangedEvent(tag)
}
return true
}
return false
case EditViewType:
oldType := GetEditViewType(edit)
if edit.setEnumProperty(EditViewType, value, enumProperties[EditViewType].values) {
if GetEditViewType(edit) != oldType {
if edit.created {
updateInnerHTML(edit.parentHTMLID(), edit.session)
}
edit.propertyChangedEvent(tag)
}
return true
}
return false
case EditWrap:
oldWrap := IsEditViewWrap(edit)
if edit.setBoolProperty(EditWrap, value) {
if GetEditViewType(edit) == MultiLineText {
if wrap := IsEditViewWrap(edit); wrap != oldWrap {
if edit.created {
if wrap {
updateProperty(edit.htmlID(), "wrap", "soft", edit.session)
} else {
updateProperty(edit.htmlID(), "wrap", "off", edit.session)
}
}
edit.propertyChangedEvent(tag)
}
}
return true
}
return false
case EditTextChangedEvent:
listeners, ok := valueToEventListeners[EditView, string](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(EditView, string){}
}
edit.textChangeListeners = listeners
edit.propertyChangedEvent(tag)
return true
}
return edit.viewData.set(tag, value)
}
func (edit *editViewData) Get(tag string) any {
return edit.get(edit.normalizeTag(tag))
}
func (edit *editViewData) get(tag string) any {
if tag == EditTextChangedEvent {
return edit.textChangeListeners
}
return edit.viewData.get(tag)
}
func (edit *editViewData) AppendText(text string) { func (edit *editViewData) AppendText(text string) {
if GetEditViewType(edit) == MultiLineText { if GetEditViewType(edit) == MultiLineText {
if value := edit.getRaw(Text); value != nil { if value := edit.getRaw(Text); value != nil {
if textValue, ok := value.(string); ok { if textValue, ok := value.(string); ok {
oldText := textValue
textValue += text textValue += text
edit.properties[Text] = textValue edit.properties[Text] = textValue
edit.session.callFunc("appendToInnerHTML", edit.htmlID(), text)
edit.session.callFunc("appendToInputValue", edit.htmlID(), text) text := strings.ReplaceAll(text, `"`, `\"`)
edit.textChanged(textValue, oldText) text = strings.ReplaceAll(text, `'`, `\'`)
text = strings.ReplaceAll(text, "\n", `\n`)
text = strings.ReplaceAll(text, "\r", `\r`)
edit.session.runScript(`appendToInnerHTML("` + edit.htmlID() + `", "` + text + `")`)
edit.textChanged(textValue)
return return
} }
} }
edit.Set(Text, text) edit.set(Text, text)
} else { } else {
edit.Set(Text, GetText(edit)+text) edit.set(Text, GetText(edit)+text)
} }
} }
func (edit *editViewData) textChanged(newText, oldText string) { func (edit *editViewData) textChanged(newText string) {
for _, listener := range getTwoArgEventListeners[EditView, string](edit, nil, EditTextChangedEvent) { for _, listener := range edit.textChangeListeners {
listener.Run(edit, newText, oldText) listener(edit, newText)
} }
edit.runChangeListener(Text) edit.propertyChangedEvent(Text)
} }
func (edit *editViewData) htmlTag() string { func (edit *editViewData) htmlTag() string {
@ -293,17 +397,6 @@ func (edit *editViewData) htmlTag() string {
return "input" return "input"
} }
func (edit *editViewData) htmlSubviews(self View, buffer *strings.Builder) {
if GetEditViewType(edit) == MultiLineText {
if text := GetText(edit); text != "" {
buffer.WriteString(text)
}
}
dataListHtmlSubviews(self, buffer, func(text string, session Session) string {
return text
})
}
func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) { func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) {
edit.viewData.htmlProperties(self, buffer) edit.viewData.htmlProperties(self, buffer)
@ -359,10 +452,7 @@ func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) {
if strings.ContainsRune(text, '"') { if strings.ContainsRune(text, '"') {
text = strings.ReplaceAll(text, `"`, `&#34;`) text = strings.ReplaceAll(text, `"`, `&#34;`)
} }
if strings.ContainsRune(text, '\n') { return textToJS(text)
text = strings.ReplaceAll(text, "\n", `\n`)
}
return text
} }
if hint := GetHint(edit); hint != "" { if hint := GetHint(edit); hint != "" {
@ -385,18 +475,29 @@ func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) {
buffer.WriteByte('"') buffer.WriteByte('"')
} }
} }
dataListHtmlProperties(edit, buffer)
} }
func (edit *editViewData) handleCommand(self View, command PropertyName, data DataObject) bool { func (edit *editViewData) htmlDisabledProperties(self View, buffer *strings.Builder) {
if IsDisabled(self) {
buffer.WriteString(` disabled`)
}
edit.viewData.htmlDisabledProperties(self, buffer)
}
func (edit *editViewData) htmlSubviews(self View, buffer *strings.Builder) {
if GetEditViewType(edit) == MultiLineText {
buffer.WriteString(textToJS(GetText(edit)))
}
}
func (edit *editViewData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "textChanged": case "textChanged":
oldText := GetText(edit) oldText := GetText(edit)
if text, ok := data.PropertyValue("text"); ok { if text, ok := data.PropertyValue("text"); ok {
edit.setRaw(Text, text) edit.properties[Text] = text
if text != oldText { if text := GetText(edit); text != oldText {
edit.textChanged(text, oldText) edit.textChanged(text)
} }
} }
return true return true
@ -408,7 +509,10 @@ func (edit *editViewData) handleCommand(self View, command PropertyName, data Da
// GetText returns a text of the EditView subview. // GetText returns a text of the EditView subview.
// If the second argument (subviewID) is not specified or it is "" then a text of the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then a text of the first argument (view) is returned.
func GetText(view View, subviewID ...string) string { func GetText(view View, subviewID ...string) string {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.getRaw(Text); value != nil { if value := view.getRaw(Text); value != nil {
if text, ok := value.(string); ok { if text, ok := value.(string); ok {
return text return text
@ -421,34 +525,25 @@ func GetText(view View, subviewID ...string) string {
// GetHint returns a hint text of the subview. // GetHint returns a hint text of the subview.
// If the second argument (subviewID) is not specified or it is "" then a text of the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then a text of the first argument (view) is returned.
func GetHint(view View, subviewID ...string) string { func GetHint(view View, subviewID ...string) string {
view = getSubview(view, subviewID) if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
session := view.Session() }
text := ""
if view != nil { if view != nil {
var ok bool if text, ok := stringProperty(view, Hint, view.Session()); ok {
text, ok = stringProperty(view, Hint, view.Session())
if !ok {
if value := valueFromStyle(view, Hint); value != nil {
if text, ok = value.(string); ok {
if text, ok = session.resolveConstants(text); !ok {
text = ""
}
} else {
text = ""
}
}
}
}
if text != "" && !GetNotTranslate(view) {
text, _ = session.GetString(text)
}
return text return text
}
if value := valueFromStyle(view, Hint); value != nil {
if text, ok := value.(string); ok {
if text, ok = view.Session().resolveConstants(text); ok {
return text
}
}
}
}
return ""
} }
// GetMaxLength returns a maximal length of EditView. If a maximal length is not limited then 0 is returned // GetMaxLength returns a maximal lenght of EditView. If a maximal lenght is not limited then 0 is returned
// If the second argument (subviewID) is not specified or it is "" then a value of the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then a value of the first argument (view) is returned.
func GetMaxLength(view View, subviewID ...string) int { func GetMaxLength(view View, subviewID ...string) int {
return intStyledProperty(view, subviewID, MaxLength, 0) return intStyledProperty(view, subviewID, MaxLength, 0)
@ -461,45 +556,31 @@ func IsReadOnly(view View, subviewID ...string) bool {
} }
// IsSpellcheck returns a value of the Spellcheck property of EditView. // IsSpellcheck returns a value of the Spellcheck property of EditView.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func IsSpellcheck(view View, subviewID ...string) bool { func IsSpellcheck(view View, subviewID ...string) bool {
return boolStyledProperty(view, subviewID, Spellcheck, false) return boolStyledProperty(view, subviewID, Spellcheck, false)
} }
// GetTextChangedListeners returns the TextChangedListener list of an EditView or MultiLineEditView subview. // GetTextChangedListeners returns the TextChangedListener list of an EditView or MultiLineEditView subview.
// If there are no listeners then the empty list is returned // If there are no listeners then the empty list is returned
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetTextChangedListeners(view View, subviewID ...string) []func(EditView, string) {
// - func(rui.EditView, string, string), return getEventListeners[EditView, string](view, subviewID, EditTextChangedEvent)
// - func(rui.EditView, string),
// - func(rui.EditView),
// - func(string, string),
// - func(string),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTextChangedListeners(view View, subviewID ...string) []any {
return getTwoArgEventRawListeners[EditView, string](view, subviewID, EditTextChangedEvent)
} }
// GetEditViewType returns a value of the Type property of EditView. // GetEditViewType returns a value of the Type property of EditView.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetEditViewType(view View, subviewID ...string) int { func GetEditViewType(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, EditViewType, SingleLineText, false) return enumStyledProperty(view, subviewID, EditViewType, SingleLineText, false)
} }
// GetEditViewPattern returns a value of the Pattern property of EditView. // GetEditViewPattern returns a value of the Pattern property of EditView.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetEditViewPattern(view View, subviewID ...string) string { func GetEditViewPattern(view View, subviewID ...string) string {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if pattern, ok := stringProperty(view, EditViewPattern, view.Session()); ok { if pattern, ok := stringProperty(view, EditViewPattern, view.Session()); ok {
return pattern return pattern
} }
@ -515,17 +596,13 @@ func GetEditViewPattern(view View, subviewID ...string) string {
} }
// IsEditViewWrap returns a value of the EditWrap property of MultiLineEditView. // IsEditViewWrap returns a value of the EditWrap property of MultiLineEditView.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func IsEditViewWrap(view View, subviewID ...string) bool { func IsEditViewWrap(view View, subviewID ...string) bool {
return boolStyledProperty(view, subviewID, EditWrap, false) return boolStyledProperty(view, subviewID, EditWrap, false)
} }
// AppendEditText appends the text to the EditView content. // AppendEditText appends the text to the EditView content.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func AppendEditText(view View, subviewID string, text string) { func AppendEditText(view View, subviewID string, text string) {
if subviewID != "" { if subviewID != "" {
if edit := EditViewByID(view, subviewID); edit != nil { if edit := EditViewByID(view, subviewID); edit != nil {
@ -539,10 +616,8 @@ func AppendEditText(view View, subviewID string, text string) {
} }
} }
// GetCaretColor returns the color of the text input caret. // GetCaretColor returns the color of the text input carret.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetCaretColor(view View, subviewID ...string) Color { func GetCaretColor(view View, subviewID ...string) Color {
return colorStyledProperty(view, subviewID, CaretColor, false) return colorStyledProperty(view, subviewID, CaretColor, false)
} }

262
events.go
View File

@ -1,262 +0,0 @@
package rui
import (
"reflect"
"strings"
)
var eventJsFunc = map[PropertyName]struct{ jsEvent, jsFunc string }{
FocusEvent: {jsEvent: "onfocus", jsFunc: "focusEvent"},
LostFocusEvent: {jsEvent: "onblur", jsFunc: "blurEvent"},
KeyDownEvent: {jsEvent: "onkeydown", jsFunc: "keyDownEvent"},
KeyUpEvent: {jsEvent: "onkeyup", jsFunc: "keyUpEvent"},
ClickEvent: {jsEvent: "onclick", jsFunc: "clickEvent"},
DoubleClickEvent: {jsEvent: "ondblclick", jsFunc: "doubleClickEvent"},
MouseDown: {jsEvent: "onmousedown", jsFunc: "mouseDownEvent"},
MouseUp: {jsEvent: "onmouseup", jsFunc: "mouseUpEvent"},
MouseMove: {jsEvent: "onmousemove", jsFunc: "mouseMoveEvent"},
MouseOut: {jsEvent: "onmouseout", jsFunc: "mouseOutEvent"},
MouseOver: {jsEvent: "onmouseover", jsFunc: "mouseOverEvent"},
ContextMenuEvent: {jsEvent: "oncontextmenu", jsFunc: "contextMenuEvent"},
PointerDown: {jsEvent: "onpointerdown", jsFunc: "pointerDownEvent"},
PointerUp: {jsEvent: "onpointerup", jsFunc: "pointerUpEvent"},
PointerMove: {jsEvent: "onpointermove", jsFunc: "pointerMoveEvent"},
PointerCancel: {jsEvent: "onpointercancel", jsFunc: "pointerCancelEvent"},
PointerOut: {jsEvent: "onpointerout", jsFunc: "pointerOutEvent"},
PointerOver: {jsEvent: "onpointerover", jsFunc: "pointerOverEvent"},
TouchStart: {jsEvent: "ontouchstart", jsFunc: "touchStartEvent"},
TouchEnd: {jsEvent: "ontouchend", jsFunc: "touchEndEvent"},
TouchMove: {jsEvent: "ontouchmove", jsFunc: "touchMoveEvent"},
TouchCancel: {jsEvent: "ontouchcancel", jsFunc: "touchCancelEvent"},
TransitionRunEvent: {jsEvent: "ontransitionrun", jsFunc: "transitionRunEvent"},
TransitionStartEvent: {jsEvent: "ontransitionstart", jsFunc: "transitionStartEvent"},
TransitionEndEvent: {jsEvent: "ontransitionend", jsFunc: "transitionEndEvent"},
TransitionCancelEvent: {jsEvent: "ontransitioncancel", jsFunc: "transitionCancelEvent"},
AnimationStartEvent: {jsEvent: "onanimationstart", jsFunc: "animationStartEvent"},
AnimationEndEvent: {jsEvent: "onanimationend", jsFunc: "animationEndEvent"},
AnimationIterationEvent: {jsEvent: "onanimationiteration", jsFunc: "animationIterationEvent"},
AnimationCancelEvent: {jsEvent: "onanimationcancel", jsFunc: "animationCancelEvent"},
DragEndEvent: {jsEvent: "ondragend", jsFunc: "dragEndEvent"},
DragEnterEvent: {jsEvent: "ondragenter", jsFunc: "dragEnterEvent"},
DragLeaveEvent: {jsEvent: "ondragleave", jsFunc: "dragLeaveEvent"},
}
func viewEventsHtml[T any](view View, events []PropertyName, buffer *strings.Builder) {
for _, tag := range events {
if js, ok := eventJsFunc[tag]; ok {
if value := getOneArgEventListeners[View, T](view, nil, tag); len(value) > 0 {
buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
}
}
}
}
func updateEventListenerHtml(view View, tag PropertyName) {
if js, ok := eventJsFunc[tag]; ok {
value := view.getRaw(tag)
session := view.Session()
htmlID := view.htmlID()
if value == nil {
session.removeProperty(view.htmlID(), js.jsEvent)
} else {
session.updateProperty(htmlID, js.jsEvent, js.jsFunc+"(this, event)")
}
}
}
type noArgListener[V View] interface {
Run(V)
rawListener() any
}
type noArgListener0[V View] struct {
fn func()
}
type noArgListenerV[V View] struct {
fn func(V)
}
type noArgListenerBinding[V View] struct {
name string
}
func newNoArgListener0[V View](fn func()) noArgListener[V] {
obj := new(noArgListener0[V])
obj.fn = fn
return obj
}
func (data *noArgListener0[V]) Run(_ V) {
data.fn()
}
func (data *noArgListener0[V]) rawListener() any {
return data.fn
}
func newNoArgListenerV[V View](fn func(V)) noArgListener[V] {
obj := new(noArgListenerV[V])
obj.fn = fn
return obj
}
func (data *noArgListenerV[V]) Run(view V) {
data.fn(view)
}
func (data *noArgListenerV[V]) rawListener() any {
return data.fn
}
func newNoArgListenerBinding[V View](name string) noArgListener[V] {
obj := new(noArgListenerBinding[V])
obj.name = name
return obj
}
func (data *noArgListenerBinding[V]) Run(view V) {
bind := view.binding()
if bind == nil {
ErrorLogF(`There is no a binding object for call "%s"`, data.name)
return
}
val := reflect.ValueOf(bind)
method := val.MethodByName(data.name)
if !method.IsValid() {
ErrorLogF(`The "%s" method is not valid`, data.name)
return
}
methodType := method.Type()
var args []reflect.Value = nil
switch methodType.NumIn() {
case 0:
args = []reflect.Value{}
case 1:
if equalType(methodType.In(0), reflect.TypeOf(view)) {
args = []reflect.Value{reflect.ValueOf(view)}
}
}
if args != nil {
method.Call(args)
} else {
ErrorLogF(`Unsupported prototype of "%s" method`, data.name)
}
}
func equalType(inType reflect.Type, argType reflect.Type) bool {
return inType == argType || (inType.Kind() == reflect.Interface && argType.Implements(inType))
}
func (data *noArgListenerBinding[V]) rawListener() any {
return data.name
}
func valueToNoArgEventListeners[V View](value any) ([]noArgListener[V], bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case []noArgListener[V]:
return value, true
case noArgListener[V]:
return []noArgListener[V]{value}, true
case string:
return []noArgListener[V]{newNoArgListenerBinding[V](value)}, true
case func(V):
return []noArgListener[V]{newNoArgListenerV(value)}, true
case func():
return []noArgListener[V]{newNoArgListener0[V](value)}, true
case []func(V):
result := make([]noArgListener[V], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newNoArgListenerV(fn))
}
}
return result, len(result) > 0
case []func():
result := make([]noArgListener[V], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newNoArgListener0[V](fn))
}
}
return result, len(result) > 0
case []any:
result := make([]noArgListener[V], 0, len(value))
for _, v := range value {
if v != nil {
switch v := v.(type) {
case func(V):
result = append(result, newNoArgListenerV(v))
case func():
result = append(result, newNoArgListener0[V](v))
case string:
result = append(result, newNoArgListenerBinding[V](v))
default:
return nil, false
}
}
}
return result, len(result) > 0
}
return nil, false
}
func setNoArgEventListener[V View](view View, tag PropertyName, value any) []PropertyName {
if listeners, ok := valueToNoArgEventListeners[V](value); ok {
return setArrayPropertyValue(view, tag, listeners)
}
notCompatibleType(tag, value)
return nil
}
func getNoArgEventListeners[V View](view View, subviewID []string, tag PropertyName) []noArgListener[V] {
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(tag); value != nil {
if result, ok := value.([]noArgListener[V]); ok {
return result
}
}
}
return []noArgListener[V]{}
}
func getNoArgEventRawListeners[V View](view View, subviewID []string, tag PropertyName) []any {
listeners := getNoArgEventListeners[V](view, subviewID, tag)
result := make([]any, len(listeners))
for i, l := range listeners {
result[i] = l.rawListener()
}
return result
}
func getNoArgBinding[V View](listeners []noArgListener[V]) string {
for _, listener := range listeners {
raw := listener.rawListener()
if text, ok := raw.(string); ok && text != "" {
return text
}
}
return ""
}

View File

@ -1,271 +0,0 @@
package rui
import (
"reflect"
)
type oneArgListener[V View, E any] interface {
Run(V, E)
rawListener() any
}
type oneArgListener0[V View, E any] struct {
fn func()
}
type oneArgListenerV[V View, E any] struct {
fn func(V)
}
type oneArgListenerE[V View, E any] struct {
fn func(E)
}
type oneArgListenerVE[V View, E any] struct {
fn func(V, E)
}
type oneArgListenerBinding[V View, E any] struct {
name string
}
func newOneArgListener0[V View, E any](fn func()) oneArgListener[V, E] {
obj := new(oneArgListener0[V, E])
obj.fn = fn
return obj
}
func (data *oneArgListener0[V, E]) Run(_ V, _ E) {
data.fn()
}
func (data *oneArgListener0[V, E]) rawListener() any {
return data.fn
}
func newOneArgListenerV[V View, E any](fn func(V)) oneArgListener[V, E] {
obj := new(oneArgListenerV[V, E])
obj.fn = fn
return obj
}
func (data *oneArgListenerV[V, E]) Run(view V, _ E) {
data.fn(view)
}
func (data *oneArgListenerV[V, E]) rawListener() any {
return data.fn
}
func newOneArgListenerE[V View, E any](fn func(E)) oneArgListener[V, E] {
obj := new(oneArgListenerE[V, E])
obj.fn = fn
return obj
}
func (data *oneArgListenerE[V, E]) Run(_ V, event E) {
data.fn(event)
}
func (data *oneArgListenerE[V, E]) rawListener() any {
return data.fn
}
func newOneArgListenerVE[V View, E any](fn func(V, E)) oneArgListener[V, E] {
obj := new(oneArgListenerVE[V, E])
obj.fn = fn
return obj
}
func (data *oneArgListenerVE[V, E]) Run(view V, arg E) {
data.fn(view, arg)
}
func (data *oneArgListenerVE[V, E]) rawListener() any {
return data.fn
}
func newOneArgListenerBinding[V View, E any](name string) oneArgListener[V, E] {
obj := new(oneArgListenerBinding[V, E])
obj.name = name
return obj
}
func (data *oneArgListenerBinding[V, E]) Run(view V, event E) {
bind := view.binding()
if bind == nil {
ErrorLogF(`There is no a binding object for call "%s"`, data.name)
return
}
val := reflect.ValueOf(bind)
method := val.MethodByName(data.name)
if !method.IsValid() {
ErrorLogF(`The "%s" method is not valid`, data.name)
return
}
methodType := method.Type()
var args []reflect.Value = nil
switch methodType.NumIn() {
case 0:
args = []reflect.Value{}
case 1:
inType := methodType.In(0)
if equalType(inType, reflect.TypeOf(event)) {
args = []reflect.Value{reflect.ValueOf(event)}
} else if equalType(inType, reflect.TypeOf(view)) {
args = []reflect.Value{reflect.ValueOf(view)}
}
case 2:
if equalType(methodType.In(0), reflect.TypeOf(view)) &&
equalType(methodType.In(1), reflect.TypeOf(event)) {
args = []reflect.Value{reflect.ValueOf(view), reflect.ValueOf(event)}
}
}
if args != nil {
method.Call(args)
} else {
ErrorLogF(`Unsupported prototype of "%s" method`, data.name)
}
}
func (data *oneArgListenerBinding[V, E]) rawListener() any {
return data.name
}
func valueToOneArgEventListeners[V View, E any](value any) ([]oneArgListener[V, E], bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case []oneArgListener[V, E]:
return value, true
case oneArgListener[V, E]:
return []oneArgListener[V, E]{value}, true
case string:
return []oneArgListener[V, E]{newOneArgListenerBinding[V, E](value)}, true
case func(V, E):
return []oneArgListener[V, E]{newOneArgListenerVE(value)}, true
case func(V):
return []oneArgListener[V, E]{newOneArgListenerV[V, E](value)}, true
case func(E):
return []oneArgListener[V, E]{newOneArgListenerE[V](value)}, true
case func():
return []oneArgListener[V, E]{newOneArgListener0[V, E](value)}, true
case []func(V, E):
result := make([]oneArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newOneArgListenerVE(fn))
}
}
return result, len(result) > 0
case []func(E):
result := make([]oneArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newOneArgListenerE[V](fn))
}
}
return result, len(result) > 0
case []func(V):
result := make([]oneArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newOneArgListenerV[V, E](fn))
}
}
return result, len(result) > 0
case []func():
result := make([]oneArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newOneArgListener0[V, E](fn))
}
}
return result, len(result) > 0
case []any:
result := make([]oneArgListener[V, E], 0, len(value))
for _, v := range value {
if v != nil {
switch v := v.(type) {
case func(V, E):
result = append(result, newOneArgListenerVE(v))
case func(E):
result = append(result, newOneArgListenerE[V](v))
case func(V):
result = append(result, newOneArgListenerV[V, E](v))
case func():
result = append(result, newOneArgListener0[V, E](v))
case string:
result = append(result, newOneArgListenerBinding[V, E](v))
default:
return nil, false
}
}
}
return result, len(result) > 0
}
return nil, false
}
func setOneArgEventListener[V View, T any](view View, tag PropertyName, value any) []PropertyName {
if listeners, ok := valueToOneArgEventListeners[V, T](value); ok {
return setArrayPropertyValue(view, tag, listeners)
}
notCompatibleType(tag, value)
return nil
}
func getOneArgEventListeners[V View, E any](view View, subviewID []string, tag PropertyName) []oneArgListener[V, E] {
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(tag); value != nil {
if result, ok := value.([]oneArgListener[V, E]); ok {
return result
}
}
}
return []oneArgListener[V, E]{}
}
func getOneArgEventRawListeners[V View, E any](view View, subviewID []string, tag PropertyName) []any {
listeners := getOneArgEventListeners[V, E](view, subviewID, tag)
result := make([]any, len(listeners))
for i, l := range listeners {
result[i] = l.rawListener()
}
return result
}
func getOneArgBinding[V View, E any](listeners []oneArgListener[V, E]) string {
for _, listener := range listeners {
raw := listener.rawListener()
if text, ok := raw.(string); ok && text != "" {
return text
}
}
return ""
}

View File

@ -1,345 +0,0 @@
package rui
import "reflect"
type twoArgListener[V View, E any] interface {
Run(V, E, E)
rawListener() any
}
type twoArgListener0[V View, E any] struct {
fn func()
}
type twoArgListenerV[V View, E any] struct {
fn func(V)
}
type twoArgListenerE[V View, E any] struct {
fn func(E)
}
type twoArgListenerVE[V View, E any] struct {
fn func(V, E)
}
type twoArgListenerEE[V View, E any] struct {
fn func(E, E)
}
type twoArgListenerVEE[V View, E any] struct {
fn func(V, E, E)
}
type twoArgListenerBinding[V View, E any] struct {
name string
}
func newTwoArgListener0[V View, E any](fn func()) twoArgListener[V, E] {
obj := new(twoArgListener0[V, E])
obj.fn = fn
return obj
}
func (data *twoArgListener0[V, E]) Run(_ V, _ E, _ E) {
data.fn()
}
func (data *twoArgListener0[V, E]) rawListener() any {
return data.fn
}
func newTwoArgListenerV[V View, E any](fn func(V)) twoArgListener[V, E] {
obj := new(twoArgListenerV[V, E])
obj.fn = fn
return obj
}
func (data *twoArgListenerV[V, E]) Run(view V, _ E, _ E) {
data.fn(view)
}
func (data *twoArgListenerV[V, E]) rawListener() any {
return data.fn
}
func newTwoArgListenerE[V View, E any](fn func(E)) twoArgListener[V, E] {
obj := new(twoArgListenerE[V, E])
obj.fn = fn
return obj
}
func (data *twoArgListenerE[V, E]) Run(_ V, arg E, _ E) {
data.fn(arg)
}
func (data *twoArgListenerE[V, E]) rawListener() any {
return data.fn
}
func newTwoArgListenerVE[V View, E any](fn func(V, E)) twoArgListener[V, E] {
obj := new(twoArgListenerVE[V, E])
obj.fn = fn
return obj
}
func (data *twoArgListenerVE[V, E]) Run(view V, arg E, _ E) {
data.fn(view, arg)
}
func (data *twoArgListenerVE[V, E]) rawListener() any {
return data.fn
}
func newTwoArgListenerEE[V View, E any](fn func(E, E)) twoArgListener[V, E] {
obj := new(twoArgListenerEE[V, E])
obj.fn = fn
return obj
}
func (data *twoArgListenerEE[V, E]) Run(_ V, arg1 E, arg2 E) {
data.fn(arg1, arg2)
}
func (data *twoArgListenerEE[V, E]) rawListener() any {
return data.fn
}
func newTwoArgListenerVEE[V View, E any](fn func(V, E, E)) twoArgListener[V, E] {
obj := new(twoArgListenerVEE[V, E])
obj.fn = fn
return obj
}
func (data *twoArgListenerVEE[V, E]) Run(view V, arg1 E, arg2 E) {
data.fn(view, arg1, arg2)
}
func (data *twoArgListenerVEE[V, E]) rawListener() any {
return data.fn
}
func newTwoArgListenerBinding[V View, E any](name string) twoArgListener[V, E] {
obj := new(twoArgListenerBinding[V, E])
obj.name = name
return obj
}
func (data *twoArgListenerBinding[V, E]) Run(view V, arg1 E, arg2 E) {
bind := view.binding()
if bind == nil {
ErrorLogF(`There is no a binding object for call "%s"`, data.name)
return
}
val := reflect.ValueOf(bind)
method := val.MethodByName(data.name)
if !method.IsValid() {
ErrorLogF(`The "%s" method is not valid`, data.name)
return
}
methodType := method.Type()
var args []reflect.Value = nil
switch methodType.NumIn() {
case 0:
args = []reflect.Value{}
case 1:
inType := methodType.In(0)
if equalType(inType, reflect.TypeOf(arg1)) {
args = []reflect.Value{reflect.ValueOf(arg1)}
} else if equalType(inType, reflect.TypeOf(view)) {
args = []reflect.Value{reflect.ValueOf(view)}
}
case 2:
inType0 := methodType.In(0)
inType1 := methodType.In(1)
if equalType(inType0, reflect.TypeOf(view)) && equalType(inType1, reflect.TypeOf(arg1)) {
args = []reflect.Value{reflect.ValueOf(view), reflect.ValueOf(arg1)}
} else if equalType(inType0, reflect.TypeOf(arg1)) && equalType(inType1, reflect.TypeOf(arg2)) {
args = []reflect.Value{reflect.ValueOf(arg1), reflect.ValueOf(arg2)}
}
case 3:
if equalType(methodType.In(0), reflect.TypeOf(view)) &&
equalType(methodType.In(1), reflect.TypeOf(arg1)) &&
equalType(methodType.In(2), reflect.TypeOf(arg2)) {
args = []reflect.Value{reflect.ValueOf(view), reflect.ValueOf(arg1), reflect.ValueOf(arg2)}
}
}
if args != nil {
method.Call(args)
} else {
ErrorLogF(`Unsupported prototype of "%s" method`, data.name)
}
}
func (data *twoArgListenerBinding[V, E]) rawListener() any {
return data.name
}
func valueToTwoArgEventListeners[V View, E any](value any) ([]twoArgListener[V, E], bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case []twoArgListener[V, E]:
return value, true
case twoArgListener[V, E]:
return []twoArgListener[V, E]{value}, true
case string:
return []twoArgListener[V, E]{newTwoArgListenerBinding[V, E](value)}, true
case func(V, E):
return []twoArgListener[V, E]{newTwoArgListenerVE(value)}, true
case func(V):
return []twoArgListener[V, E]{newTwoArgListenerV[V, E](value)}, true
case func(E):
return []twoArgListener[V, E]{newTwoArgListenerE[V](value)}, true
case func():
return []twoArgListener[V, E]{newTwoArgListener0[V, E](value)}, true
case func(E, E):
return []twoArgListener[V, E]{newTwoArgListenerEE[V](value)}, true
case func(V, E, E):
return []twoArgListener[V, E]{newTwoArgListenerVEE(value)}, true
case []func(V, E):
result := make([]twoArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newTwoArgListenerVE(fn))
}
}
return result, len(result) > 0
case []func(E):
result := make([]twoArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newTwoArgListenerE[V](fn))
}
}
return result, len(result) > 0
case []func(V):
result := make([]twoArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newTwoArgListenerV[V, E](fn))
}
}
return result, len(result) > 0
case []func():
result := make([]twoArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newTwoArgListener0[V, E](fn))
}
}
return result, len(result) > 0
case []func(E, E):
result := make([]twoArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newTwoArgListenerEE[V](fn))
}
}
return result, len(result) > 0
case []func(V, E, E):
result := make([]twoArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newTwoArgListenerVEE(fn))
}
}
return result, len(result) > 0
case []any:
result := make([]twoArgListener[V, E], 0, len(value))
for _, v := range value {
if v != nil {
switch v := v.(type) {
case func(V, E):
result = append(result, newTwoArgListenerVE(v))
case func(E):
result = append(result, newTwoArgListenerE[V](v))
case func(V):
result = append(result, newTwoArgListenerV[V, E](v))
case func():
result = append(result, newTwoArgListener0[V, E](v))
case func(E, E):
result = append(result, newTwoArgListenerEE[V](v))
case func(V, E, E):
result = append(result, newTwoArgListenerVEE(v))
case string:
result = append(result, newTwoArgListenerBinding[V, E](v))
default:
return nil, false
}
}
}
return result, len(result) > 0
}
return nil, false
}
func setTwoArgEventListener[V View, T any](view View, tag PropertyName, value any) []PropertyName {
if listeners, ok := valueToTwoArgEventListeners[V, T](value); ok {
return setArrayPropertyValue(view, tag, listeners)
}
notCompatibleType(tag, value)
return nil
}
func getTwoArgEventListeners[V View, E any](view View, subviewID []string, tag PropertyName) []twoArgListener[V, E] {
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(tag); value != nil {
if result, ok := value.([]twoArgListener[V, E]); ok {
return result
}
}
}
return []twoArgListener[V, E]{}
}
func getTwoArgEventRawListeners[V View, E any](view View, subviewID []string, tag PropertyName) []any {
listeners := getTwoArgEventListeners[V, E](view, subviewID, tag)
result := make([]any, len(listeners))
for i, l := range listeners {
result[i] = l.rawListener()
}
return result
}
func getTwoArgBinding[V View, E any](listeners []twoArgListener[V, E]) string {
for _, listener := range listeners {
raw := listener.rawListener()
if text, ok := raw.(string); ok && text != "" {
return text
}
}
return ""
}

View File

@ -1,77 +1,38 @@
package rui package rui
import ( import (
"encoding/base64"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"time" "time"
) )
// Constants for [FilePicker] specific properties and events
const ( const (
// FileSelectedEvent is the constant for "file-selected-event" property tag. // FileSelectedEvent is the constant for "file-selected-event" property tag.
// // The "file-selected-event" is fired when user selects file(s) in the FilePicker.
// Used by FilePicker. FileSelectedEvent = "file-selected-event"
// Fired when user selects file(s).
//
// General listener format:
// func(picker rui.FilePicker, files []rui.FileInfo).
//
// where:
// picker - Interface of a file picker which generated this event,
// files - Array of description of selected files.
//
// Allowed listener formats:
// func(picker rui.FilePicker)
// func(files []rui.FileInfo)
// func()
FileSelectedEvent PropertyName = "file-selected-event"
// Accept is the constant for "accept" property tag. // Accept is the constant for "accept" property tag.
// // The "accept" property of the FilePicker sets the list of allowed file extensions or MIME types.
// Used by FilePicker. Accept = "accept"
// Set the list of allowed file extensions or MIME types.
//
// Supported types: string, []string.
//
// Internal type is string, other types converted to it during assignment.
//
// Conversion rules:
// - string - may contain single value of multiple separated by comma(,).
// - []string - an array of acceptable file extensions or MIME types.
Accept PropertyName = "accept"
// Multiple is the constant for "multiple" property tag. // Multiple is the constant for "multiple" property tag.
// // The "multiple" bool property of the FilePicker sets whether multiple files can be selected
// Used by FilePicker. Multiple = "multiple"
// Controls whether multiple files can be selected.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", "1" - Several files can be selected.
// - false, 0, "false", "no", "off", "0" - Only one file can be selected.
Multiple PropertyName = "multiple"
) )
// FileInfo describes a file which selected in the FilePicker view // FileInfo describes a file which selected in the FilePicker view
type FileInfo struct { type FileInfo struct {
// Name - the file's name. // Name - the file's name.
Name string Name string
// LastModified specifying the date and time at which the file was last modified // LastModified specifying the date and time at which the file was last modified
LastModified time.Time LastModified time.Time
// Size - the size of the file in bytes. // Size - the size of the file in bytes.
Size int64 Size int64
// MimeType - the file's MIME type. // MimeType - the file's MIME type.
MimeType string MimeType string
data []byte
} }
// FilePicker represents the FilePicker view // FilePicker - the control view for the files selecting
type FilePicker interface { type FilePicker interface {
View View
// Files returns the list of selected files. // Files returns the list of selected files.
@ -85,11 +46,11 @@ type FilePicker interface {
type filePickerData struct { type filePickerData struct {
viewData viewData
files []FileInfo files []FileInfo
//loader map[int]func(FileInfo, []byte) fileSelectedListeners []func(FilePicker, []FileInfo)
loader map[int]func(FileInfo, []byte)
} }
func dataToFileInfo(node DataValue) FileInfo { func (file *FileInfo) initBy(node DataValue) {
var file FileInfo
if obj := node.Object(); obj != nil { if obj := node.Object(); obj != nil {
file.Name, _ = obj.PropertyValue("name") file.Name, _ = obj.PropertyValue("name")
file.MimeType, _ = obj.PropertyValue("mime-type") file.MimeType, _ = obj.PropertyValue("mime-type")
@ -106,11 +67,6 @@ func dataToFileInfo(node DataValue) FileInfo {
} }
} }
} }
return file
}
func (file FileInfo) key() string {
return fmt.Sprintf("%s:%d", file.Name, int(file.Size))
} }
// NewFilePicker create new FilePicker object and return it // NewFilePicker create new FilePicker object and return it
@ -122,19 +78,19 @@ func NewFilePicker(session Session, params Params) FilePicker {
} }
func newFilePicker(session Session) View { func newFilePicker(session Session) View {
return new(filePickerData) // NewFilePicker(session, nil) return NewFilePicker(session, nil)
} }
func (picker *filePickerData) init(session Session) { func (picker *filePickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "FilePicker" picker.tag = "FilePicker"
picker.hasHtmlDisabled = true
picker.files = []FileInfo{} picker.files = []FileInfo{}
//picker.loader = map[int]func(FileInfo, []byte){} picker.loader = map[int]func(FileInfo, []byte){}
picker.get = picker.getFunc picker.fileSelectedListeners = []func(FilePicker, []FileInfo){}
picker.set = picker.setFunc }
picker.changed = picker.propertyChanged
func (picker *filePickerData) String() string {
return getViewString(picker)
} }
func (picker *filePickerData) Focusable() bool { func (picker *filePickerData) Focusable() bool {
@ -146,47 +102,75 @@ func (picker *filePickerData) Files() []FileInfo {
} }
func (picker *filePickerData) LoadFile(file FileInfo, result func(FileInfo, []byte)) { func (picker *filePickerData) LoadFile(file FileInfo, result func(FileInfo, []byte)) {
if result != nil { if result == nil {
return
}
for i, info := range picker.files { for i, info := range picker.files {
if info.Name == file.Name && info.Size == file.Size && info.LastModified.Equal(file.LastModified) { if info.Name == file.Name && info.Size == file.Size && info.LastModified == file.LastModified {
if info.data != nil { picker.loader[i] = result
result(info, info.data) picker.Session().runScript(fmt.Sprintf(`loadSelectedFile("%s", %d)`, picker.htmlID(), i))
} else {
picker.fileLoader[info.key()] = func(file FileInfo, data []byte) {
picker.files[i].data = data
result(file, data)
}
picker.Session().callFunc("loadSelectedFile", picker.htmlID(), i)
}
return return
} }
} }
}
picker.viewData.LoadFile(file, result) func (picker *filePickerData) Remove(tag string) {
picker.remove(strings.ToLower(tag))
}
func (picker *filePickerData) remove(tag string) {
switch tag {
case FileSelectedEvent:
if len(picker.fileSelectedListeners) > 0 {
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){}
picker.propertyChangedEvent(tag)
}
case Accept:
delete(picker.properties, tag)
if picker.created {
removeProperty(picker.htmlID(), "accept", picker.Session())
}
picker.propertyChangedEvent(tag)
default:
picker.viewData.remove(tag)
} }
} }
func (picker *filePickerData) getFunc(tag PropertyName) any { func (picker *filePickerData) Set(tag string, value any) bool {
switch tag { return picker.set(strings.ToLower(tag), value)
case FileSelectedEvent:
if listeners := getOneArgEventRawListeners[FilePicker, []FileInfo](picker, nil, tag); len(listeners) > 0 {
return listeners
}
return nil
}
return picker.viewData.getFunc(tag)
} }
func (picker *filePickerData) setFunc(tag PropertyName, value any) []PropertyName { func (picker *filePickerData) set(tag string, value any) bool {
if value == nil {
picker.remove(tag)
return true
}
switch tag { switch tag {
case FileSelectedEvent: case FileSelectedEvent:
return setOneArgEventListener[FilePicker, []FileInfo](picker, tag, value) listeners, ok := valueToEventListeners[FilePicker, []FileInfo](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(FilePicker, []FileInfo){}
}
picker.fileSelectedListeners = listeners
picker.propertyChangedEvent(tag)
return true
case Accept: case Accept:
switch value := value.(type) { switch value := value.(type) {
case string: case string:
return setStringPropertyValue(picker, Accept, strings.Trim(value, " \t\n")) value = strings.Trim(value, " \t\n")
if value == "" {
picker.remove(Accept)
} else {
picker.properties[Accept] = value
}
case []string: case []string:
buffer := allocStringBuilder() buffer := allocStringBuilder()
@ -200,27 +184,29 @@ func (picker *filePickerData) setFunc(tag PropertyName, value any) []PropertyNam
buffer.WriteString(val) buffer.WriteString(val)
} }
} }
return setStringPropertyValue(picker, Accept, buffer.String()) if buffer.Len() == 0 {
} picker.remove(Accept)
notCompatibleType(tag, value)
return nil
}
return picker.viewData.setFunc(tag, value)
}
func (picker *filePickerData) propertyChanged(tag PropertyName) {
switch tag {
case Accept:
session := picker.Session()
if css := acceptPropertyCSS(picker); css != "" {
session.updateProperty(picker.htmlID(), "accept", css)
} else { } else {
session.removeProperty(picker.htmlID(), "accept") picker.properties[Accept] = buffer.String()
} }
default: default:
picker.viewData.propertyChanged(tag) notCompatibleType(tag, value)
return false
}
if picker.created {
if css := picker.acceptCSS(); css != "" {
updateProperty(picker.htmlID(), "accept", css, picker.Session())
} else {
removeProperty(picker.htmlID(), "accept", picker.Session())
}
}
picker.propertyChangedEvent(tag)
return true
default:
return picker.viewData.set(tag, value)
} }
} }
@ -228,10 +214,10 @@ func (picker *filePickerData) htmlTag() string {
return "input" return "input"
} }
func acceptPropertyCSS(view View) string { func (picker *filePickerData) acceptCSS() string {
accept, ok := stringProperty(view, Accept, view.Session()) accept, ok := stringProperty(picker, Accept, picker.Session())
if !ok { if !ok {
if value := valueFromStyle(view, Accept); value != nil { if value := valueFromStyle(picker, Accept); value != nil {
accept, ok = value.(string) accept, ok = value.(string)
} }
} }
@ -244,7 +230,7 @@ func acceptPropertyCSS(view View) string {
if buffer.Len() > 0 { if buffer.Len() > 0 {
buffer.WriteString(", ") buffer.WriteString(", ")
} }
if value[0] != '.' && !strings.ContainsRune(value, '/') { if value[0] != '.' && !strings.Contains(value, "/") {
buffer.WriteRune('.') buffer.WriteRune('.')
} }
buffer.WriteString(value) buffer.WriteString(value)
@ -258,7 +244,7 @@ func acceptPropertyCSS(view View) string {
func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder) { func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder) {
picker.viewData.htmlProperties(self, buffer) picker.viewData.htmlProperties(self, buffer)
if accept := acceptPropertyCSS(picker); accept != "" { if accept := picker.acceptCSS(); accept != "" {
buffer.WriteString(` accept="`) buffer.WriteString(` accept="`)
buffer.WriteString(accept) buffer.WriteString(accept)
buffer.WriteRune('"') buffer.WriteRune('"')
@ -275,27 +261,69 @@ func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder)
} }
} }
func parseFilesTag(data DataObject) []FileInfo { func (picker *filePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
if node := data.PropertyByTag("files"); node != nil && node.Type() == ArrayNode { if IsDisabled(self) {
count := node.ArraySize() buffer.WriteString(` disabled`)
files := make([]FileInfo, count)
for i := range count {
if value := node.ArrayElement(i); value != nil {
files[i] = dataToFileInfo(value)
} }
} picker.viewData.htmlDisabledProperties(self, buffer)
return files
}
return nil
} }
func (picker *filePickerData) handleCommand(self View, command PropertyName, data DataObject) bool { func (picker *filePickerData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "fileSelected": case "fileSelected":
if files := parseFilesTag(data); files != nil { if node := data.PropertyWithTag("files"); node != nil && node.Type() == ArrayNode {
count := node.ArraySize()
files := make([]FileInfo, count)
for i := 0; i < count; i++ {
if value := node.ArrayElement(i); value != nil {
files[i].initBy(value)
}
}
picker.files = files picker.files = files
for _, listener := range getOneArgEventListeners[FilePicker, []FileInfo](picker, nil, FileSelectedEvent) {
listener.Run(picker, files) for _, listener := range picker.fileSelectedListeners {
listener(picker, files)
}
}
return true
case "fileLoaded":
if index, ok := dataIntProperty(data, "index"); ok {
if result, ok := picker.loader[index]; ok {
var file FileInfo
file.initBy(data)
var fileData []byte = nil
if base64Data, ok := data.PropertyValue("data"); ok {
if index := strings.LastIndex(base64Data, ","); index >= 0 {
base64Data = base64Data[index+1:]
}
decode, err := base64.StdEncoding.DecodeString(base64Data)
if err == nil {
fileData = decode
} else {
ErrorLog(err.Error())
}
}
result(file, fileData)
delete(picker.loader, index)
}
}
return true
case "fileLoadingError":
if error, ok := data.PropertyValue("error"); ok {
ErrorLog(error)
}
if index, ok := dataIntProperty(data, "index"); ok {
if result, ok := picker.loader[index]; ok {
if index >= 0 && index < len(picker.files) {
result(picker.files[index], nil)
} else {
result(FileInfo{}, nil)
}
delete(picker.loader, index)
} }
} }
return true return true
@ -329,19 +357,18 @@ func LoadFilePickerFile(view View, subviewID string, file FileInfo, result func(
} }
// IsMultipleFilePicker returns "true" if multiple files can be selected in the FilePicker, "false" otherwise. // IsMultipleFilePicker returns "true" if multiple files can be selected in the FilePicker, "false" otherwise.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func IsMultipleFilePicker(view View, subviewID ...string) bool { func IsMultipleFilePicker(view View, subviewID ...string) bool {
return boolStyledProperty(view, subviewID, Multiple, false) return boolStyledProperty(view, subviewID, Multiple, false)
} }
// GetFilePickerAccept returns sets the list of allowed file extensions or MIME types. // GetFilePickerAccept returns sets the list of allowed file extensions or MIME types.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetFilePickerAccept(view View, subviewID ...string) []string { func GetFilePickerAccept(view View, subviewID ...string) []string {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
accept, ok := stringProperty(view, Accept, view.Session()) accept, ok := stringProperty(view, Accept, view.Session())
if !ok { if !ok {
if value := valueFromStyle(view, Accept); value != nil { if value := valueFromStyle(view, Accept); value != nil {
@ -350,7 +377,7 @@ func GetFilePickerAccept(view View, subviewID ...string) []string {
} }
if ok { if ok {
result := strings.Split(accept, ",") result := strings.Split(accept, ",")
for i := range len(result) { for i := 0; i < len(result); i++ {
result[i] = strings.Trim(result[i], " \t\n") result[i] = strings.Trim(result[i], " \t\n")
} }
return result return result
@ -361,16 +388,7 @@ func GetFilePickerAccept(view View, subviewID ...string) []string {
// GetFileSelectedListeners returns the "file-selected-event" listener list. // GetFileSelectedListeners returns the "file-selected-event" listener list.
// If there are no listeners then the empty list is returned. // If there are no listeners then the empty list is returned.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetFileSelectedListeners(view View, subviewID ...string) []func(FilePicker, []FileInfo) {
// - func(rui.View, []rui.FileInfo), return getEventListeners[FilePicker, []FileInfo](view, subviewID, FileSelectedEvent)
// - func(rui.View),
// - func([]rui.FileInfo),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetFileSelectedListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[FilePicker, []FileInfo](view, subviewID, FileSelectedEvent)
} }

341
filter.go
View File

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

View File

@ -2,74 +2,160 @@ package rui
import "strings" import "strings"
// Constants which represent [View] specific focus events properties
const ( const (
// FocusEvent is the constant for "focus-event" property tag. // FocusEvent is the constant for "focus-event" property tag.
// // The "focus-event" event occurs when the View takes input focus.
// Used by View. // The main listener format:
// Occur when the view takes input focus. // func(View).
// // The additional listener format:
// General listener format:
// func(rui.View).
//
// where:
// view - Interface of a view which generated this event.
//
// Allowed listener formats:
// func(). // func().
FocusEvent PropertyName = "focus-event" FocusEvent = "focus-event"
// LostFocusEvent is the constant for "lost-focus-event" property tag. // LostFocusEvent is the constant for "lost-focus-event" property tag.
// // The "lost-focus-event" event occurs when the View lost input focus.
// Used by View. // The main listener format:
// Occur when the View lost input focus. // func(View).
// // The additional listener format:
// General listener format: // func().
// func(view rui.View). LostFocusEvent = "lost-focus-event"
//
// where:
// view - Interface of a view which generated this event.
//
// Allowed listener formats:
// func()
LostFocusEvent PropertyName = "lost-focus-event"
) )
func valueToNoParamListeners[V any](value any) ([]func(V), bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case func(V):
return []func(V){value}, true
case func():
fn := func(V) {
value()
}
return []func(V){fn}, true
case []func(V):
if len(value) == 0 {
return nil, true
}
for _, fn := range value {
if fn == nil {
return nil, false
}
}
return value, true
case []func():
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(V) {
v()
}
}
return listeners, true
case []any:
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V), count)
for i, v := range value {
if v == nil {
return nil, false
}
switch v := v.(type) {
case func(V):
listeners[i] = v
case func():
listeners[i] = func(V) {
v()
}
default:
return nil, false
}
}
return listeners, true
}
return nil, false
}
var focusEvents = map[string]struct{ jsEvent, jsFunc string }{
FocusEvent: {jsEvent: "onfocus", jsFunc: "focusEvent"},
LostFocusEvent: {jsEvent: "onblur", jsFunc: "blurEvent"},
}
func (view *viewData) setFocusListener(tag string, value any) bool {
listeners, ok := valueToNoParamListeners[View](value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeFocusListener(tag)
} else if js, ok := focusEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
}
} else {
return false
}
return true
}
func (view *viewData) removeFocusListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := focusEvents[tag]; ok {
removeProperty(view.htmlID(), js.jsEvent, view.Session())
}
}
}
func getFocusListeners(view View, subviewID []string, tag string) []func(View) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.Get(tag); value != nil {
if result, ok := value.([]func(View)); ok {
return result
}
}
}
return []func(View){}
}
func focusEventsHtml(view View, buffer *strings.Builder) { func focusEventsHtml(view View, buffer *strings.Builder) {
if view.Focusable() { if view.Focusable() {
for _, tag := range []PropertyName{FocusEvent, LostFocusEvent} { for _, js := range focusEvents {
if js, ok := eventJsFunc[tag]; ok { buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
}
} }
} }
} }
// GetFocusListeners returns a FocusListener list. If there are no listeners then the empty list is returned // GetFocusListeners returns a FocusListener list. If there are no listeners then the empty list is returned
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetFocusListeners(view View, subviewID ...string) []func(View) {
// - func(rui.View), return getFocusListeners(view, subviewID, FocusEvent)
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetFocusListeners(view View, subviewID ...string) []any {
return getNoArgEventRawListeners[View](view, subviewID, FocusEvent)
} }
// GetLostFocusListeners returns a LostFocusListener list. If there are no listeners then the empty list is returned // GetLostFocusListeners returns a LostFocusListener list. If there are no listeners then the empty list is returned
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetLostFocusListeners(view View, subviewID ...string) []func(View) {
// - func(rui.View), return getFocusListeners(view, subviewID, LostFocusEvent)
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetLostFocusListeners(view View, subviewID ...string) []any {
return getNoArgEventRawListeners[View](view, subviewID, LostFocusEvent)
} }

12
go.mod
View File

@ -1,13 +1,5 @@
module github.com/anoshenko/rui module github.com/anoshenko/rui
go 1.24 go 1.18
require ( require github.com/gorilla/websocket v1.5.0
github.com/gorilla/websocket v1.5.3
golang.org/x/crypto v0.37.0
)
require (
golang.org/x/net v0.39.0 // indirect
golang.org/x/text v0.24.0 // indirect
)

18
go.sum
View File

@ -1,16 +1,2 @@
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=

View File

@ -5,116 +5,13 @@ import (
"strings" "strings"
) )
// Constants related to [GridLayout] specific properties and events // GridLayout - grid-container of View
const (
// CellVerticalAlign is the constant for "cell-vertical-align" property tag.
//
// Used by GridLayout, SvgImageView.
//
// Usage in GridLayout:
// Sets the default vertical alignment of GridLayout children within the cell they are occupying.
//
// Supported types: int, string.
//
// Values:
// - 0 (TopAlign) or "top" - Top alignment.
// - 1 (BottomAlign) or "bottom" - Bottom alignment.
// - 2 (CenterAlign) or "center" - Center alignment.
// - 3 (StretchAlign) or "stretch" - Full height stretch.
//
// Usage in SvgImageView:
// Same as "vertical-align".
CellVerticalAlign PropertyName = "cell-vertical-align"
// CellHorizontalAlign is the constant for "cell-horizontal-align" property tag.
//
// Used by GridLayout, SvgImageView.
//
// Usage in GridLayout:
// Sets the default horizontal alignment of GridLayout children within the occupied cell.
//
// Supported types: int, string.
//
// Values:
// - 0 (LeftAlign) or "left" - Left alignment.
// - 1 (RightAlign) or "right" - Right alignment.
// - 2 (CenterAlign) or "center" - Center alignment.
// - 3 (StretchAlign) or "stretch" - Full width stretch.
//
// Usage in SvgImageView:
// Same as "horizontal-align".
CellHorizontalAlign PropertyName = "cell-horizontal-align"
// CellVerticalSelfAlign is the constant for "cell-vertical-self-align" property tag.
//
// Used by GridLayout.
// Sets the vertical alignment of GridLayout children within the cell they are occupying. The property is set for the
// child view of GridLayout.
//
// Supported types: int, string.
//
// Values:
// - 0 (TopAlign) or "top" - Top alignment.
// - 1 (BottomAlign) or "bottom" - Bottom alignment.
// - 2 (CenterAlign) or "center" - Center alignment.
// - 3 (StretchAlign) or "stretch" - Full height stretch.
CellVerticalSelfAlign PropertyName = "cell-vertical-self-align"
// CellHorizontalSelfAlign is the constant for "cell-horizontal-self-align" property tag.
//
// Used by GridLayout.
// Sets the horizontal alignment of GridLayout children within the occupied cell. The property is set for the child view
// of GridLayout.
//
// Supported types: int, string.
//
// Values:
// - 0 (LeftAlign) or "left" - Left alignment.
// - 1 (RightAlign) or "right" - Right alignment.
// - 2 (CenterAlign) or "center" - Center alignment.
// - 3 (StretchAlign) or "stretch" - Full width stretch.
CellHorizontalSelfAlign PropertyName = "cell-horizontal-self-align"
)
// GridAdapter is an interface to define [GridLayout] content. [GridLayout] will query interface functions to populate
// its content
type GridAdapter interface {
// GridColumnCount returns the number of columns in the grid
GridColumnCount() int
// GridRowCount returns the number of rows in the grid
GridRowCount() int
// GridCellContent creates a View at the given cell
GridCellContent(row, column int, session Session) View
}
// GridCellColumnSpanAdapter implements the optional method of the [GridAdapter] interface
type GridCellColumnSpanAdapter interface {
// GridCellColumnSpan returns the number of columns that a cell spans.
// Values less than 1 are ignored.
GridCellColumnSpan(row, column int) int
}
// GridCellColumnSpanAdapter implements the optional method of the [GridAdapter] interface
type GridCellRowSpanAdapter interface {
// GridCellRowSpan returns the number of rows that a cell spans
// Values less than 1 are ignored.
GridCellRowSpan(row, column int) int
}
// GridLayout represents a GridLayout view
type GridLayout interface { type GridLayout interface {
ViewsContainer ViewsContainer
// UpdateContent updates child Views if the "content" property value is set to GridAdapter,
// otherwise does nothing
UpdateGridContent()
} }
type gridLayoutData struct { type gridLayoutData struct {
viewsContainerData viewsContainerData
adapter GridAdapter
} }
// NewGridLayout create new GridLayout object and return it // NewGridLayout create new GridLayout object and return it
@ -126,8 +23,7 @@ func NewGridLayout(session Session, params Params) GridLayout {
} }
func newGridLayout(session Session) View { func newGridLayout(session Session) View {
//return NewGridLayout(session, nil) return NewGridLayout(session, nil)
return new(gridLayoutData)
} }
// Init initialize fields of GridLayout by default values // Init initialize fields of GridLayout by default values
@ -135,22 +31,20 @@ func (gridLayout *gridLayoutData) init(session Session) {
gridLayout.viewsContainerData.init(session) gridLayout.viewsContainerData.init(session)
gridLayout.tag = "GridLayout" gridLayout.tag = "GridLayout"
gridLayout.systemClass = "ruiGridLayout" gridLayout.systemClass = "ruiGridLayout"
gridLayout.adapter = nil
gridLayout.normalize = normalizeGridLayoutTag
gridLayout.get = gridLayout.getFunc
gridLayout.set = gridLayout.setFunc
gridLayout.remove = gridLayout.removeFunc
gridLayout.changed = gridLayout.propertyChanged
} }
func setGridCellSize(properties Properties, tag PropertyName, value any) []PropertyName { func (gridLayout *gridLayoutData) String() string {
return getViewString(gridLayout)
}
func (style *viewStyle) setGridCellSize(tag string, value any) bool {
setValues := func(values []string) bool { setValues := func(values []string) bool {
count := len(values) count := len(values)
if count > 1 { if count > 1 {
sizes := make([]any, count) sizes := make([]any, count)
for i, val := range values { for i, val := range values {
val = strings.Trim(val, " \t\n\r") val = strings.Trim(val, " \t\n\r")
if ok, _ := isConstantName(val); ok { if isConstantName(val) {
sizes[i] = val sizes[i] = val
} else if fn := parseSizeFunc(val); fn != nil { } else if fn := parseSizeFunc(val); fn != nil {
sizes[i] = SizeUnit{Type: SizeFunction, Function: fn} sizes[i] = SizeUnit{Type: SizeFunction, Function: fn}
@ -161,11 +55,11 @@ func setGridCellSize(properties Properties, tag PropertyName, value any) []Prope
return false return false
} }
} }
properties.setRaw(tag, sizes) style.properties[tag] = sizes
} else if ok, _ := isConstantName(values[0]); ok { } else if isConstantName(values[0]) {
properties.setRaw(tag, values[0]) style.properties[tag] = values[0]
} else if size, err := stringToSizeUnit(values[0]); err == nil { } else if size, err := stringToSizeUnit(values[0]); err == nil {
properties.setRaw(tag, size) style.properties[tag] = size
} else { } else {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return false return false
@ -177,41 +71,41 @@ func setGridCellSize(properties Properties, tag PropertyName, value any) []Prope
case CellWidth, CellHeight: case CellWidth, CellHeight:
switch value := value.(type) { switch value := value.(type) {
case SizeUnit, []SizeUnit: case SizeUnit, []SizeUnit:
properties.setRaw(tag, value) style.properties[tag] = value
case string: case string:
if !setValues(strings.Split(value, ",")) { if !setValues(strings.Split(value, ",")) {
return nil return false
} }
case []string: case []string:
if !setValues(value) { if !setValues(value) {
return nil return false
} }
case []DataValue: case []DataValue:
count := len(value) count := len(value)
if count == 0 { if count == 0 {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return nil return false
} }
values := make([]string, count) values := make([]string, count)
for i, val := range value { for i, val := range value {
if val.IsObject() { if val.IsObject() {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return nil return false
} }
values[i] = val.Value() values[i] = val.Value()
} }
if !setValues(values) { if !setValues(values) {
return nil return false
} }
case []any: case []any:
count := len(value) count := len(value)
if count == 0 { if count == 0 {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return nil return false
} }
sizes := make([]any, count) sizes := make([]any, count)
for i, val := range value { for i, val := range value {
@ -220,35 +114,35 @@ func setGridCellSize(properties Properties, tag PropertyName, value any) []Prope
sizes[i] = val sizes[i] = val
case string: case string:
if ok, _ := isConstantName(val); ok { if isConstantName(val) {
sizes[i] = val sizes[i] = val
} else if size, err := stringToSizeUnit(val); err == nil { } else if size, err := stringToSizeUnit(val); err == nil {
sizes[i] = size sizes[i] = size
} else { } else {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return nil return false
} }
default: default:
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return nil return false
} }
} }
properties.setRaw(tag, sizes) style.properties[tag] = sizes
default: default:
notCompatibleType(tag, value) notCompatibleType(tag, value)
return nil return false
} }
return []PropertyName{tag} return true
} }
return nil return false
} }
func gridCellSizesCSS(properties Properties, tag PropertyName, session Session) string { func (style *viewStyle) gridCellSizesCSS(tag string, session Session) string {
switch cellSize := gridCellSizes(properties, tag, session); len(cellSize) { switch cellSize := gridCellSizes(style, tag, session); len(cellSize) {
case 0: case 0:
case 1: case 1:
@ -285,8 +179,8 @@ func gridCellSizesCSS(properties Properties, tag PropertyName, session Session)
return "" return ""
} }
func normalizeGridLayoutTag(tag PropertyName) PropertyName { func (gridLayout *gridLayoutData) normalizeTag(tag string) string {
tag = defaultNormalize(tag) tag = strings.ToLower(tag)
switch tag { switch tag {
case VerticalAlign: case VerticalAlign:
return CellVerticalAlign return CellVerticalAlign
@ -303,166 +197,84 @@ func normalizeGridLayoutTag(tag PropertyName) PropertyName {
return tag return tag
} }
func (gridLayout *gridLayoutData) getFunc(tag PropertyName) any { func (gridLayout *gridLayoutData) Get(tag string) any {
switch tag { return gridLayout.get(gridLayout.normalizeTag(tag))
case Gap: }
func (gridLayout *gridLayoutData) get(tag string) any {
if tag == Gap {
rowGap := GetGridRowGap(gridLayout) rowGap := GetGridRowGap(gridLayout)
columnGap := GetGridColumnGap(gridLayout) columnGap := GetGridColumnGap(gridLayout)
if rowGap.Equal(columnGap) { if rowGap.Equal(columnGap) {
return rowGap return rowGap
} }
return AutoSize() return AutoSize()
case Content:
if gridLayout.adapter != nil {
return gridLayout.adapter
}
} }
return gridLayout.viewsContainerData.getFunc(tag) return gridLayout.viewsContainerData.get(tag)
} }
func (gridLayout *gridLayoutData) removeFunc(tag PropertyName) []PropertyName { func (gridLayout *gridLayoutData) Remove(tag string) {
switch tag { gridLayout.remove(gridLayout.normalizeTag(tag))
case Gap:
result := []PropertyName{}
for _, tag := range []PropertyName{GridRowGap, GridColumnGap} {
if gridLayout.getRaw(tag) != nil {
gridLayout.setRaw(tag, nil)
result = append(result, tag)
}
}
return result
case Content:
if len(gridLayout.views) > 0 || gridLayout.adapter != nil {
gridLayout.views = []View{}
gridLayout.adapter = nil
return []PropertyName{Content}
}
return []PropertyName{}
}
return gridLayout.viewsContainerData.removeFunc(tag)
} }
func (gridLayout *gridLayoutData) setFunc(tag PropertyName, value any) []PropertyName { func (gridLayout *gridLayoutData) remove(tag string) {
switch tag { if tag == Gap {
case Gap: gridLayout.remove(GridRowGap)
result := gridLayout.setFunc(GridRowGap, value) gridLayout.remove(GridColumnGap)
if result != nil { gridLayout.propertyChangedEvent(Gap)
if gap := gridLayout.getRaw(GridRowGap); gap != nil { return
gridLayout.setRaw(GridColumnGap, gap)
result = append(result, GridColumnGap)
}
}
return result
case Content:
if adapter, ok := value.(GridAdapter); ok {
gridLayout.adapter = adapter
gridLayout.createGridContent()
} else if gridLayout.setContent(value) {
gridLayout.adapter = nil
} else {
return nil
}
return []PropertyName{Content}
} }
return gridLayout.viewsContainerData.setFunc(tag, value) gridLayout.viewsContainerData.remove(tag)
} if gridLayout.created {
func (gridLayout *gridLayoutData) propertyChanged(tag PropertyName) {
switch tag { switch tag {
case CellWidth: case CellWidth:
session := gridLayout.Session() updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`,
session.updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`, gridLayout.gridCellSizesCSS(CellWidth, gridLayout.session), gridLayout.session)
gridCellSizesCSS(gridLayout, CellWidth, session))
case CellHeight: case CellHeight:
session := gridLayout.Session() updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`,
session.updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`, gridLayout.gridCellSizesCSS(CellHeight, gridLayout.session), gridLayout.session)
gridCellSizesCSS(gridLayout, CellHeight, session))
default: }
gridLayout.viewsContainerData.propertyChanged(tag)
} }
} }
func (gridLayout *gridLayoutData) createGridContent() bool { func (gridLayout *gridLayoutData) Set(tag string, value any) bool {
if gridLayout.adapter == nil { return gridLayout.set(gridLayout.normalizeTag(tag), value)
return false }
}
adapter := gridLayout.adapter
gridLayout.views = []View{}
session := gridLayout.session
htmlID := gridLayout.htmlID()
isDisabled := IsDisabled(gridLayout)
var columnSpan GridCellColumnSpanAdapter = nil
if span, ok := adapter.(GridCellColumnSpanAdapter); ok {
columnSpan = span
}
var rowSpan GridCellRowSpanAdapter = nil
if span, ok := adapter.(GridCellRowSpanAdapter); ok {
rowSpan = span
}
width := adapter.GridColumnCount()
height := adapter.GridRowCount()
for column := 0; column < width; column++ {
for row := 0; row < height; row++ {
if view := adapter.GridCellContent(row, column, session); view != nil {
view.setParentID(htmlID)
columnCount := 1
if columnSpan != nil {
columnCount = columnSpan.GridCellColumnSpan(row, column)
}
if columnCount > 1 {
view.Set(Column, Range{First: column, Last: column + columnCount - 1})
} else {
view.Set(Column, column)
}
rowCount := 1
if rowSpan != nil {
rowCount = rowSpan.GridCellRowSpan(row, column)
}
if rowCount > 1 {
view.Set(Row, Range{First: row, Last: row + rowCount - 1})
} else {
view.Set(Row, row)
}
if isDisabled {
view.Set(Disabled, true)
}
gridLayout.views = append(gridLayout.views, view)
}
}
}
func (gridLayout *gridLayoutData) set(tag string, value any) bool {
if value == nil {
gridLayout.remove(tag)
return true return true
} }
func (gridLayout *gridLayoutData) UpdateGridContent() { if tag == Gap {
if gridLayout.createGridContent() { return gridLayout.set(GridRowGap, value) && gridLayout.set(GridColumnGap, value)
}
if gridLayout.viewsContainerData.set(tag, value) {
if gridLayout.created { if gridLayout.created {
updateInnerHTML(gridLayout.htmlID(), gridLayout.session) switch tag {
case CellWidth:
updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`,
gridLayout.gridCellSizesCSS(CellWidth, gridLayout.session), gridLayout.session)
case CellHeight:
updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`,
gridLayout.gridCellSizesCSS(CellHeight, gridLayout.session), gridLayout.session)
} }
gridLayout.runChangeListener(Content)
} }
return true
}
return false
} }
func gridCellSizes(properties Properties, tag PropertyName, session Session) []SizeUnit { func gridCellSizes(properties Properties, tag string, session Session) []SizeUnit {
if value := properties.Get(tag); value != nil { if value := properties.Get(tag); value != nil {
switch value := value.(type) { switch value := value.(type) {
case []SizeUnit: case []SizeUnit:
@ -502,37 +314,38 @@ func gridCellSizes(properties Properties, tag PropertyName, session Session) []S
return []SizeUnit{} return []SizeUnit{}
} }
/*
func (gridLayout *gridLayoutData) cssStyle(self View, builder cssBuilder) {
gridLayout.viewsContainerData.cssStyle(self, builder)
}
*/
// GetCellVerticalAlign returns the vertical align of a GridLayout cell content: TopAlign (0), BottomAlign (1), CenterAlign (2), StretchAlign (3) // GetCellVerticalAlign returns the vertical align of a GridLayout cell content: TopAlign (0), BottomAlign (1), CenterAlign (2), StretchAlign (3)
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetCellVerticalAlign(view View, subviewID ...string) int { func GetCellVerticalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, CellVerticalAlign, StretchAlign, false) return enumStyledProperty(view, subviewID, CellVerticalAlign, StretchAlign, false)
} }
// GetCellHorizontalAlign returns the vertical align of a GridLayout cell content: LeftAlign (0), RightAlign (1), CenterAlign (2), StretchAlign (3) // GetCellHorizontalAlign returns the vertical align of a GridLayout cell content: LeftAlign (0), RightAlign (1), CenterAlign (2), StretchAlign (3)
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetCellHorizontalAlign(view View, subviewID ...string) int { func GetCellHorizontalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, CellHorizontalAlign, StretchAlign, false) return enumStyledProperty(view, subviewID, CellHorizontalAlign, StretchAlign, false)
} }
// GetGridAutoFlow returns the value of the "grid-auto-flow" property // GetGridAutoFlow returns the value of the "grid-auto-flow" property
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetGridAutoFlow(view View, subviewID ...string) int { func GetGridAutoFlow(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, GridAutoFlow, 0, false) return enumStyledProperty(view, subviewID, GridAutoFlow, 0, false)
} }
// GetCellWidth returns the width of a GridLayout cell. If the result is an empty array, then the width is not set. // GetCellWidth returns the width of a GridLayout cell. If the result is an empty array, then the width is not set.
// If the result is a single value array, then the width of all cell is equal. // If the result is a single value array, then the width of all cell is equal.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetCellWidth(view View, subviewID ...string) []SizeUnit { func GetCellWidth(view View, subviewID ...string) []SizeUnit {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
return gridCellSizes(view, CellWidth, view.Session()) return gridCellSizes(view, CellWidth, view.Session())
} }
return []SizeUnit{} return []SizeUnit{}
@ -540,28 +353,25 @@ func GetCellWidth(view View, subviewID ...string) []SizeUnit {
// GetCellHeight returns the height of a GridLayout cell. If the result is an empty array, then the height is not set. // GetCellHeight returns the height of a GridLayout cell. If the result is an empty array, then the height is not set.
// If the result is a single value array, then the height of all cell is equal. // If the result is a single value array, then the height of all cell is equal.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetCellHeight(view View, subviewID ...string) []SizeUnit { func GetCellHeight(view View, subviewID ...string) []SizeUnit {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
return gridCellSizes(view, CellHeight, view.Session()) return gridCellSizes(view, CellHeight, view.Session())
} }
return []SizeUnit{} return []SizeUnit{}
} }
// GetGridRowGap returns the gap between GridLayout rows. // GetGridRowGap returns the gap between GridLayout rows.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetGridRowGap(view View, subviewID ...string) SizeUnit { func GetGridRowGap(view View, subviewID ...string) SizeUnit {
return sizeStyledProperty(view, subviewID, GridRowGap, false) return sizeStyledProperty(view, subviewID, GridRowGap, false)
} }
// GetGridColumnGap returns the gap between GridLayout columns. // GetGridColumnGap returns the gap between GridLayout columns.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetGridColumnGap(view View, subviewID ...string) SizeUnit { func GetGridColumnGap(view View, subviewID ...string) SizeUnit {
return sizeStyledProperty(view, subviewID, GridColumnGap, false) return sizeStyledProperty(view, subviewID, GridColumnGap, false)
} }

View File

@ -1,56 +0,0 @@
//go:build !wasm
package rui
import (
"net/http"
"strings"
)
type httpHandler struct {
app *application
prefix string
}
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
path := `/` + strings.TrimPrefix(req.URL.Path, `/`)
req.URL.Path = `/` + strings.TrimPrefix(strings.TrimPrefix(path, h.prefix), `/`)
h.app.ServeHTTP(w, req)
}
}
// NewHandler is used to embed the rui application in third-party web frameworks (net/http, gin, echo...).
//
// Example for echo:
//
// e := echo.New()
// e.Any(`/ui/*`, func()echo.HandlerFunc{
// rui.AddEmbedResources(&resources)
//
// h := rui.NewHandler("/ui", CreateSessionContent, rui.AppParams{
// Title: `Awesome app`,
// Icon: `favicon.png`,
// })
//
// return func(c echo.Context) error {
// h.ServeHTTP(c.Response(), c.Request())
// return nil
// }
// })
func NewHandler(urlPrefix string, createContentFunc func(Session) SessionContent, params AppParams) *httpHandler {
app := new(application)
app.params = params
app.sessions = map[int]sessionInfo{}
app.createContentFunc = createContentFunc
apps = append(apps, app)
h := &httpHandler{
app: app,
prefix: `/` + strings.Trim(urlPrefix, `/`),
}
return h
}

View File

@ -1,47 +1,34 @@
package rui package rui
import ( import "strconv"
"strconv"
)
// ImageLoadingStatus defines type of status of the image loading
type ImageLoadingStatus int
// Constants which represent return values of the LoadingStatus function of an [Image] view
const ( const (
// ImageLoading is the image loading status: in the process of loading // ImageLoading is the image loading status: in the process of loading
ImageLoading ImageLoadingStatus = 0 ImageLoading = 0
// ImageReady is the image loading status: the image is loaded successfully // ImageReady is the image loading status: the image is loaded successfully
ImageReady ImageLoadingStatus = 1 ImageReady = 1
// ImageLoadingError is the image loading status: an error occurred while loading // ImageLoadingError is the image loading status: an error occurred while loading
ImageLoadingError ImageLoadingStatus = 2 ImageLoadingError = 2
) )
// Image defines the image that is used for drawing operations on the Canvas. // Image defines the image that is used for drawing operations on the Canvas.
type Image interface { type Image interface {
// URL returns the url of the image // URL returns the url of the image
URL() string URL() string
// LoadingStatus returns the status of the image loading: ImageLoading (0), ImageReady (1), ImageLoadingError (2)
// LoadingStatus returns the status of the image loading: LoadingStatus() int
// - ImageLoading (0) - in the process of loading;
// - ImageReady (1) - the image is loaded successfully;
// - ImageLoadingError (2) - an error occurred while loading.
LoadingStatus() ImageLoadingStatus
// LoadingError: if LoadingStatus() == ImageLoadingError then returns the error text, "" otherwise // LoadingError: if LoadingStatus() == ImageLoadingError then returns the error text, "" otherwise
LoadingError() string LoadingError() string
setLoadingError(err string) setLoadingError(err string)
// Width returns the width of the image in pixels. While LoadingStatus() != ImageReady returns 0 // Width returns the width of the image in pixels. While LoadingStatus() != ImageReady returns 0
Width() float64 Width() float64
// Height returns the height of the image in pixels. While LoadingStatus() != ImageReady returns 0 // Height returns the height of the image in pixels. While LoadingStatus() != ImageReady returns 0
Height() float64 Height() float64
} }
type imageData struct { type imageData struct {
url string url string
loadingStatus ImageLoadingStatus loadingStatus int
loadingError string loadingError string
width, height float64 width, height float64
listener func(Image) listener func(Image)
@ -55,7 +42,7 @@ func (image *imageData) URL() string {
return image.url return image.url
} }
func (image *imageData) LoadingStatus() ImageLoadingStatus { func (image *imageData) LoadingStatus() int {
return image.loadingStatus return image.loadingStatus
} }
@ -89,13 +76,11 @@ func (manager *imageManager) loadImage(url string, onLoaded func(Image), session
image.listener = onLoaded image.listener = onLoaded
image.loadingStatus = ImageLoading image.loadingStatus = ImageLoading
manager.images[url] = image manager.images[url] = image
session.runScript("loadImage('" + url + "');")
session.callFunc("loadImage", url)
session.sendResponse()
return image return image
} }
func (manager *imageManager) imageLoaded(obj DataObject) { func (manager *imageManager) imageLoaded(obj DataObject, session Session) {
if manager.images == nil { if manager.images == nil {
manager.images = make(map[string]*imageData) manager.images = make(map[string]*imageData)
return return
@ -121,7 +106,7 @@ func (manager *imageManager) imageLoaded(obj DataObject) {
} }
} }
func (manager *imageManager) imageLoadError(obj DataObject) { func (manager *imageManager) imageLoadError(obj DataObject, session Session) {
if manager.images == nil { if manager.images == nil {
manager.images = make(map[string]*imageData) manager.images = make(map[string]*imageData)
return return
@ -143,8 +128,8 @@ func (manager *imageManager) imageLoadError(obj DataObject) {
// LoadImage starts the async image loading by url // LoadImage starts the async image loading by url
func LoadImage(url string, onLoaded func(Image), session Session) Image { func LoadImage(url string, onLoaded func(Image), session Session) Image {
if ok, constName := isConstantName(url); ok { if url != "" && url[0] == '@' {
if image, ok := session.ImageConstant(constName); ok { if image, ok := session.ImageConstant(url[1:]); ok {
url = image url = image
} }
} }

View File

@ -5,63 +5,35 @@ import (
"strings" "strings"
) )
// Constants which represent [ImageView] specific properties and events
const ( const (
// LoadedEvent is the constant for "loaded-event" property tag. // LoadedEvent is the constant for the "loaded-event" property tag.
// // The "loaded-event" event occurs event occurs when the image has been loaded.
// Used by ImageView. LoadedEvent = "loaded-event"
// Occur when the image has been loaded. // ErrorEvent is the constant for the "error-event" property tag.
// // The "error-event" event occurs event occurs when the image loading failed.
// General listener format: ErrorEvent = "error-event"
// func(image rui.ImageView)
//
// where:
// image - Interface of an image view which generated this event.
//
// Allowed listener formats:
// func()
LoadedEvent PropertyName = "loaded-event"
// ErrorEvent is the constant for "error-event" property tag.
//
// Used by ImageView.
// Occur when the image loading has been failed.
//
// General listener format:
// func(image rui.ImageView)
//
// where:
// image - Interface of an image view which generated this event.
//
// Allowed listener formats:
// func()
ErrorEvent PropertyName = "error-event"
// NoneFit - value of the "object-fit" property of an ImageView. The replaced content is not resized // NoneFit - value of the "object-fit" property of an ImageView. The replaced content is not resized
NoneFit = 0 NoneFit = 0
// ContainFit - value of the "object-fit" property of an ImageView. The replaced content // ContainFit - value of the "object-fit" property of an ImageView. The replaced content
// is scaled to maintain its aspect ratio while fitting within the elements content box. // is scaled to maintain its aspect ratio while fitting within the elements content box.
// The entire object is made to fill the box, while preserving its aspect ratio, so the object // 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. // will be "letterboxed" if its aspect ratio does not match the aspect ratio of the box.
ContainFit = 1 ContainFit = 1
// CoverFit - value of the "object-fit" property of an ImageView. The replaced content // CoverFit - value of the "object-fit" property of an ImageView. The replaced content
// is sized to maintain its aspect ratio while filling the elements entire content box. // is sized to maintain its aspect ratio while filling the elements entire content box.
// If the object's aspect ratio does not match the aspect ratio of its box, then the object will be clipped to fit. // If the object's aspect ratio does not match the aspect ratio of its box, then the object will be clipped to fit.
CoverFit = 2 CoverFit = 2
// FillFit - value of the "object-fit" property of an ImageView. The replaced content is sized // FillFit - value of the "object-fit" property of an ImageView. The replaced content is sized
// to fill the elements content box. The entire object will completely fill the box. // to fill the elements content box. The entire object will completely fill the box.
// If the object's aspect ratio does not match the aspect ratio of its box, then the object will be stretched to fit. // If the object's aspect ratio does not match the aspect ratio of its box, then the object will be stretched to fit.
FillFit = 3 FillFit = 3
// ScaleDownFit - value of the "object-fit" property of an ImageView. The content is sized as // ScaleDownFit - value of the "object-fit" property of an ImageView. The content is sized as
// if NoneFit or ContainFit were specified, whichever would result in a smaller concrete object size. // if NoneFit or ContainFit were specified, whichever would result in a smaller concrete object size.
ScaleDownFit = 4 ScaleDownFit = 4
) )
// ImageView represents an ImageView view // ImageView - image View
type ImageView interface { type ImageView interface {
View View
// NaturalSize returns the intrinsic, density-corrected size (width, height) of the image in pixels. // NaturalSize returns the intrinsic, density-corrected size (width, height) of the image in pixels.
@ -88,29 +60,27 @@ func NewImageView(session Session, params Params) ImageView {
} }
func newImageView(session Session) View { func newImageView(session Session) View {
return new(imageViewData) return NewImageView(session, nil)
} }
// Init initialize fields of imageView by default values // Init initialize fields of imageView by default values
func (imageView *imageViewData) init(session Session) { func (imageView *imageViewData) init(session Session) {
imageView.viewData.init(session) imageView.viewData.init(session)
imageView.tag = "ImageView" imageView.tag = "ImageView"
imageView.systemClass = "ruiImageView" //imageView.systemClass = "ruiImageView"
imageView.normalize = normalizeImageViewTag
imageView.get = imageView.getFunc
imageView.set = imageView.setFunc
imageView.changed = imageView.propertyChanged
} }
func normalizeImageViewTag(tag PropertyName) PropertyName { func (imageView *imageViewData) String() string {
tag = defaultNormalize(tag) return getViewString(imageView)
}
func (imageView *imageViewData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag { switch tag {
case "source": case "source":
tag = Source tag = Source
case "src-set", "source-set":
tag = SrcSet
case VerticalAlign: case VerticalAlign:
tag = ImageVerticalAlign tag = ImageVerticalAlign
@ -123,92 +93,108 @@ func normalizeImageViewTag(tag PropertyName) PropertyName {
return tag return tag
} }
func (imageView *imageViewData) getFunc(tag PropertyName) any { func (imageView *imageViewData) Remove(tag string) {
switch tag { imageView.remove(imageView.normalizeTag(tag))
case LoadedEvent, ErrorEvent:
if listeners := getNoArgEventRawListeners[ImageView](imageView, nil, tag); len(listeners) > 0 {
return listeners
}
return nil
}
return imageView.viewData.getFunc(tag)
} }
func (imageView *imageViewData) setFunc(tag PropertyName, value any) []PropertyName { func (imageView *imageViewData) remove(tag string) {
imageView.viewData.remove(tag)
if imageView.created {
switch tag { switch tag {
case Source, SrcSet, AltText: case Source:
if text, ok := value.(string); ok { updateProperty(imageView.htmlID(), "src", "", imageView.session)
return setStringPropertyValue(imageView, tag, text) removeProperty(imageView.htmlID(), "srcset", imageView.session)
}
notCompatibleType(tag, value)
return nil
case LoadedEvent, ErrorEvent: case AltText:
return setNoArgEventListener[ImageView](imageView, tag, value) updateInnerHTML(imageView.htmlID(), imageView.session)
}
return imageView.viewData.setFunc(tag, value) case ImageVerticalAlign, ImageHorizontalAlign:
updateCSSStyle(imageView.htmlID(), imageView.session)
}
}
} }
func (imageView *imageViewData) propertyChanged(tag PropertyName) { func (imageView *imageViewData) Set(tag string, value any) bool {
session := imageView.Session() return imageView.set(imageView.normalizeTag(tag), value)
htmlID := imageView.htmlID() }
func (imageView *imageViewData) set(tag string, value any) bool {
if value == nil {
imageView.remove(tag)
return true
}
switch tag { switch tag {
case Source: case Source:
src, srcset := imageViewSrc(imageView, GetImageViewSource(imageView)) if text, ok := value.(string); ok {
session.updateProperty(htmlID, "src", src) imageView.properties[Source] = text
if srcset != "" { if imageView.created {
session.updateProperty(htmlID, "srcset", srcset) src := text
} else { if src != "" && src[0] == '@' {
session.removeProperty(htmlID, "srcset") src, _ = imageProperty(imageView, Source, imageView.session)
} }
updateProperty(imageView.htmlID(), "src", src, imageView.session)
case SrcSet: if srcset := imageView.srcSet(src); srcset != "" {
_, srcset := imageViewSrc(imageView, GetImageViewSource(imageView)) updateProperty(imageView.htmlID(), "srcset", srcset, imageView.session)
if srcset != "" {
session.updateProperty(htmlID, "srcset", srcset)
} else { } else {
session.removeProperty(htmlID, "srcset") removeProperty(imageView.htmlID(), "srcset", imageView.session)
} }
}
imageView.propertyChangedEvent(Source)
return true
}
notCompatibleType(Source, value)
case AltText: case AltText:
updateInnerHTML(htmlID, session) if text, ok := value.(string); ok {
imageView.properties[AltText] = text
if imageView.created {
updateInnerHTML(imageView.htmlID(), imageView.session)
}
imageView.propertyChangedEvent(Source)
return true
}
notCompatibleType(tag, value)
case ImageVerticalAlign, ImageHorizontalAlign: case LoadedEvent, ErrorEvent:
updateCSSStyle(htmlID, session) if listeners, ok := valueToNoParamListeners[ImageView](value); ok {
if listeners == nil {
delete(imageView.properties, tag)
} else {
imageView.properties[tag] = listeners
}
return true
}
default: default:
imageView.viewData.propertyChanged(tag) if imageView.viewData.set(tag, value) {
if imageView.created {
switch tag {
case ImageVerticalAlign, ImageHorizontalAlign:
updateCSSStyle(imageView.htmlID(), imageView.session)
} }
}
return true
}
}
return false
} }
func imageViewSrcSet(view View, path string) string { func (imageView *imageViewData) Get(tag string) any {
if value := view.getRaw(SrcSet); value != nil { return imageView.viewData.get(imageView.normalizeTag(tag))
if text, ok := value.(string); ok { }
srcset := strings.Split(text, ",")
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for i, src := range srcset {
if i > 0 {
buffer.WriteString(", ")
}
src = strings.Trim(src, " \t\n")
buffer.WriteString(src)
if index := strings.LastIndex(src, "@"); index > 0 {
if ext := strings.LastIndex(src, "."); ext > index {
buffer.WriteRune(' ')
buffer.WriteString(src[index+1 : ext])
}
} else {
buffer.WriteString(" 1x")
}
}
return buffer.String()
}
}
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 { if srcset, ok := resources.imageSrcSets[path]; ok {
buffer := allocStringBuilder() buffer := allocStringBuilder()
defer freeStringBuilder(buffer) defer freeStringBuilder(buffer)
@ -217,7 +203,7 @@ func imageViewSrcSet(view View, path string) string {
buffer.WriteString(", ") buffer.WriteString(", ")
} }
buffer.WriteString(src.path) buffer.WriteString(src.path)
fmt.Fprintf(buffer, " %gx", src.scale) buffer.WriteString(fmt.Sprintf(" %gx", src.scale))
} }
return buffer.String() return buffer.String()
} }
@ -228,31 +214,29 @@ func (imageView *imageViewData) htmlTag() string {
return "img" return "img"
} }
func imageViewSrc(view View, src string) (string, string) { /*
if ok, constName := isConstantName(src); ok { func (imageView *imageViewData) closeHTMLTag() bool {
if image, ok := view.Session().ImageConstant(constName); ok { return false
src = image
} else {
return "", ""
}
}
if src != "" {
return src, imageViewSrcSet(view, src)
}
return "", ""
} }
*/
func (imageView *imageViewData) htmlProperties(self View, buffer *strings.Builder) { func (imageView *imageViewData) htmlProperties(self View, buffer *strings.Builder) {
imageView.viewData.htmlProperties(self, buffer) imageView.viewData.htmlProperties(self, buffer)
if imageResource, ok := imageProperty(imageView, Source, imageView.Session()); ok && imageResource != "" { if imageResource, ok := imageProperty(imageView, Source, imageView.Session()); ok && imageResource != "" {
if src, srcset := imageViewSrc(imageView, imageResource); src != "" { if imageResource[0] == '@' {
if image, ok := imageView.Session().ImageConstant(imageResource[1:]); ok {
imageResource = image
} else {
imageResource = ""
}
}
if imageResource != "" {
buffer.WriteString(` src="`) buffer.WriteString(` src="`)
buffer.WriteString(src) buffer.WriteString(imageResource)
buffer.WriteString(`"`) buffer.WriteString(`"`)
if srcset != "" { if srcset := imageView.srcSet(imageResource); srcset != "" {
buffer.WriteString(` srcset="`) buffer.WriteString(` srcset="`)
buffer.WriteString(srcset) buffer.WriteString(srcset)
buffer.WriteString(`"`) buffer.WriteString(`"`)
@ -262,13 +246,13 @@ func (imageView *imageViewData) htmlProperties(self View, buffer *strings.Builde
if text := GetImageViewAltText(imageView); text != "" { if text := GetImageViewAltText(imageView); text != "" {
buffer.WriteString(` alt="`) buffer.WriteString(` alt="`)
buffer.WriteString(text) buffer.WriteString(textToJS(text))
buffer.WriteString(`"`) buffer.WriteString(`"`)
} }
buffer.WriteString(` onload="imageLoaded(this, event)"`) buffer.WriteString(` onload="imageLoaded(this, event)"`)
if len(getNoArgEventListeners[ImageView](imageView, nil, ErrorEvent)) > 0 { if len(imageView.imageListeners(ErrorEvent)) > 0 {
buffer.WriteString(` onerror="imageError(this, event)"`) buffer.WriteString(` onerror="imageError(this, event)"`)
} }
} }
@ -308,11 +292,11 @@ func (imageView *imageViewData) cssStyle(self View, builder cssBuilder) {
} }
} }
func (imageView *imageViewData) handleCommand(self View, command PropertyName, data DataObject) bool { func (imageView *imageViewData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "imageViewError": case "imageViewError":
for _, listener := range getNoArgEventListeners[ImageView](imageView, nil, ErrorEvent) { for _, listener := range imageView.imageListeners(ErrorEvent) {
listener.Run(imageView) listener(imageView)
} }
case "imageViewLoaded": case "imageViewLoaded":
@ -320,8 +304,8 @@ func (imageView *imageViewData) handleCommand(self View, command PropertyName, d
imageView.naturalHeight = dataFloatProperty(data, "natural-height") imageView.naturalHeight = dataFloatProperty(data, "natural-height")
imageView.currentSrc, _ = data.PropertyValue("current-src") imageView.currentSrc, _ = data.PropertyValue("current-src")
for _, listener := range getNoArgEventListeners[ImageView](imageView, nil, LoadedEvent) { for _, listener := range imageView.imageListeners(LoadedEvent) {
listener.Run(imageView) listener(imageView)
} }
default: default:
@ -341,7 +325,11 @@ func (imageView *imageViewData) CurrentSource() string {
// GetImageViewSource returns the image URL of an ImageView subview. // GetImageViewSource returns the image URL of an ImageView subview.
// If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned // If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned
func GetImageViewSource(view View, subviewID ...string) string { func GetImageViewSource(view View, subviewID ...string) string {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if image, ok := imageProperty(view, Source, view.Session()); ok { if image, ok := imageProperty(view, Source, view.Session()); ok {
return image return image
} }
@ -353,7 +341,11 @@ func GetImageViewSource(view View, subviewID ...string) string {
// GetImageViewAltText returns an alternative text description of an ImageView subview. // GetImageViewAltText returns an alternative text description of an ImageView subview.
// If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned // If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned
func GetImageViewAltText(view View, subviewID ...string) string { func GetImageViewAltText(view View, subviewID ...string) string {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.getRaw(AltText); value != nil { if value := view.getRaw(AltText); value != nil {
if text, ok := value.(string); ok { if text, ok := value.(string); ok {
text, _ = view.Session().GetString(text) text, _ = view.Session().GetString(text)
@ -382,31 +374,3 @@ func GetImageViewVerticalAlign(view View, subviewID ...string) int {
func GetImageViewHorizontalAlign(view View, subviewID ...string) int { func GetImageViewHorizontalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, ImageHorizontalAlign, LeftAlign, false) return enumStyledProperty(view, subviewID, ImageHorizontalAlign, LeftAlign, false)
} }
// GetImageViewErrorEventListeners returns the list of "error-event" event listeners.
// If there are no listeners then the empty list is returned
//
// Result elements can be of the following types:
// - func(rui.ImageView)
// - func()
// - string
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetImageViewErrorEventListeners(view View, subviewID ...string) []any {
return getNoArgEventRawListeners[View](view, subviewID, ErrorEvent)
}
// GetImageViewLoadedEventListeners returns the list of "loaded-event" event listeners.
// If there are no listeners then the empty list is returned
//
// Result elements can be of the following types:
// - func(rui.ImageView)
// - func()
// - string
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetImageViewLoadedEventListeners(view View, subviewID ...string) []any {
return getNoArgEventRawListeners[View](view, subviewID, LoadedEvent)
}

View File

@ -2,388 +2,24 @@ package rui
import "strings" import "strings"
// Constants which represent [View] specific keyboard events properties
const ( const (
// KeyDownEvent is the constant for "key-down-event" property tag. // KeyDown is the constant for "key-down-event" property tag.
// // The "key-down-event" event is fired when a key is pressed.
// Used by View. // The main listener format:
// Is fired when a key is pressed. // func(View, KeyEvent).
// // The additional listener formats:
// General listener format: // func(KeyEvent), func(View), and func().
// KeyDownEvent = "key-down-event"
// func(view rui.View, event rui.KeyEvent).
//
// where:
// - view - Interface of a view which generated this event,
// - event - Key event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.KeyEvent)
// func()
KeyDownEvent PropertyName = "key-down-event"
// KeyUpEvent is the constant for "key-up-event" property tag. // KeyPp is the constant for "key-up-event" property tag.
// // The "key-up-event" event is fired when a key is released.
// Used by View. // The main listener format:
// Is fired when a key is released. // func(View, KeyEvent).
// // The additional listener formats:
// General listener format: // func(KeyEvent), func(View), and func().
// KeyUpEvent = "key-up-event"
// func(view rui.View, event rui.KeyEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Key event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.KeyEvent)
// func()
KeyUpEvent PropertyName = "key-up-event"
) )
// ControlKeyMask represent ORed state of keyboard's control keys like [AltKey], [CtrlKey], [ShiftKey] and [MetaKey]
type ControlKeyMask int
// KeyCode is a string representation the a physical key being pressed.
// The value is not affected by the current keyboard layout or modifier state,
// so a particular key will always have the same value.
type KeyCode string
// Constants for specific keyboard keys.
const (
// AltKey is the mask of the "alt" key
AltKey ControlKeyMask = 1
// CtrlKey is the mask of the "ctrl" key
CtrlKey ControlKeyMask = 2
// ShiftKey is the mask of the "shift" key
ShiftKey ControlKeyMask = 4
// MetaKey is the mask of the "meta" key
MetaKey ControlKeyMask = 8
// KeyA represent "A" key on the keyboard
KeyA KeyCode = "KeyA"
// KeyB represent "B" key on the keyboard
KeyB KeyCode = "KeyB"
// KeyC represent "C" key on the keyboard
KeyC KeyCode = "KeyC"
// KeyD represent "D" key on the keyboard
KeyD KeyCode = "KeyD"
// KeyE represent "E" key on the keyboard
KeyE KeyCode = "KeyE"
// KeyF represent "F" key on the keyboard
KeyF KeyCode = "KeyF"
// KeyG represent "G" key on the keyboard
KeyG KeyCode = "KeyG"
// KeyH represent "H" key on the keyboard
KeyH KeyCode = "KeyH"
// KeyI represent "I" key on the keyboard
KeyI KeyCode = "KeyI"
// KeyJ represent "J" key on the keyboard
KeyJ KeyCode = "KeyJ"
// KeyK represent "K" key on the keyboard
KeyK KeyCode = "KeyK"
// KeyL represent "L" key on the keyboard
KeyL KeyCode = "KeyL"
// KeyM represent "M" key on the keyboard
KeyM KeyCode = "KeyM"
// KeyN represent "N" key on the keyboard
KeyN KeyCode = "KeyN"
// KeyO represent "O" key on the keyboard
KeyO KeyCode = "KeyO"
// KeyP represent "P" key on the keyboard
KeyP KeyCode = "KeyP"
// KeyQ represent "Q" key on the keyboard
KeyQ KeyCode = "KeyQ"
// KeyR represent "R" key on the keyboard
KeyR KeyCode = "KeyR"
// KeyS represent "S" key on the keyboard
KeyS KeyCode = "KeyS"
// KeyT represent "T" key on the keyboard
KeyT KeyCode = "KeyT"
// KeyU represent "U" key on the keyboard
KeyU KeyCode = "KeyU"
// KeyV represent "V" key on the keyboard
KeyV KeyCode = "KeyV"
// KeyW represent "W" key on the keyboard
KeyW KeyCode = "KeyW"
// KeyX represent "X" key on the keyboard
KeyX KeyCode = "KeyX"
// KeyY represent "Y" key on the keyboard
KeyY KeyCode = "KeyY"
// KeyZ represent "Z" key on the keyboard
KeyZ KeyCode = "KeyZ"
// Digit0Key represent "Digit0" key on the keyboard
Digit0Key KeyCode = "Digit0"
// Digit1Key represent "Digit1" key on the keyboard
Digit1Key KeyCode = "Digit1"
// Digit2Key represent "Digit2" key on the keyboard
Digit2Key KeyCode = "Digit2"
// Digit3Key represent "Digit3" key on the keyboard
Digit3Key KeyCode = "Digit3"
// Digit4Key represent "Digit4" key on the keyboard
Digit4Key KeyCode = "Digit4"
// Digit5Key represent "Digit5" key on the keyboard
Digit5Key KeyCode = "Digit5"
// Digit6Key represent "Digit6" key on the keyboard
Digit6Key KeyCode = "Digit6"
// Digit7Key represent "Digit7" key on the keyboard
Digit7Key KeyCode = "Digit7"
// Digit8Key represent "Digit8" key on the keyboard
Digit8Key KeyCode = "Digit8"
// Digit9Key represent "Digit9" key on the keyboard
Digit9Key KeyCode = "Digit9"
// SpaceKey represent "Space" key on the keyboard
SpaceKey KeyCode = "Space"
// MinusKey represent "Minus" key on the keyboard
MinusKey KeyCode = "Minus"
// EqualKey represent "Equal" key on the keyboard
EqualKey KeyCode = "Equal"
// IntlBackslashKey represent "IntlBackslash" key on the keyboard
IntlBackslashKey KeyCode = "IntlBackslash"
// BracketLeftKey represent "BracketLeft" key on the keyboard
BracketLeftKey KeyCode = "BracketLeft"
// BracketRightKey represent "BracketRight" key on the keyboard
BracketRightKey KeyCode = "BracketRight"
// SemicolonKey represent "Semicolon" key on the keyboard
SemicolonKey KeyCode = "Semicolon"
// CommaKey represent "Comma" key on the keyboard
CommaKey KeyCode = "Comma"
// PeriodKey represent "Period" key on the keyboard
PeriodKey KeyCode = "Period"
// QuoteKey represent "Quote" key on the keyboard
QuoteKey KeyCode = "Quote"
// BackquoteKey represent "Backquote" key on the keyboard
BackquoteKey KeyCode = "Backquote"
// SlashKey represent "Slash" key on the keyboard
SlashKey KeyCode = "Slash"
// EscapeKey represent "Escape" key on the keyboard
EscapeKey KeyCode = "Escape"
// EnterKey represent "Enter" key on the keyboard
EnterKey KeyCode = "Enter"
// TabKey represent "Tab" key on the keyboard
TabKey KeyCode = "Tab"
// CapsLockKey represent "CapsLock" key on the keyboard
CapsLockKey KeyCode = "CapsLock"
// DeleteKey represent "Delete" key on the keyboard
DeleteKey KeyCode = "Delete"
// InsertKey represent "Insert" key on the keyboard
InsertKey KeyCode = "Insert"
// HelpKey represent "Help" key on the keyboard
HelpKey KeyCode = "Help"
// BackspaceKey represent "Backspace" key on the keyboard
BackspaceKey KeyCode = "Backspace"
// PrintScreenKey represent "PrintScreen" key on the keyboard
PrintScreenKey KeyCode = "PrintScreen"
// ScrollLockKey represent "ScrollLock" key on the keyboard
ScrollLockKey KeyCode = "ScrollLock"
// PauseKey represent "Pause" key on the keyboard
PauseKey KeyCode = "Pause"
// ContextMenuKey represent "ContextMenu" key on the keyboard
ContextMenuKey KeyCode = "ContextMenu"
// ArrowLeftKey represent "ArrowLeft" key on the keyboard
ArrowLeftKey KeyCode = "ArrowLeft"
// ArrowRightKey represent "ArrowRight" key on the keyboard
ArrowRightKey KeyCode = "ArrowRight"
// ArrowUpKey represent "ArrowUp" key on the keyboard
ArrowUpKey KeyCode = "ArrowUp"
// ArrowDownKey represent "ArrowDown" key on the keyboard
ArrowDownKey KeyCode = "ArrowDown"
// HomeKey represent "Home" key on the keyboard
HomeKey KeyCode = "Home"
// EndKey represent "End" key on the keyboard
EndKey KeyCode = "End"
// PageUpKey represent "PageUp" key on the keyboard
PageUpKey KeyCode = "PageUp"
// PageDownKey represent "PageDown" key on the keyboard
PageDownKey KeyCode = "PageDown"
// F1Key represent "F1" key on the keyboard
F1Key KeyCode = "F1"
// F2Key represent "F2" key on the keyboard
F2Key KeyCode = "F2"
// F3Key represent "F3" key on the keyboard
F3Key KeyCode = "F3"
// F4Key represent "F4" key on the keyboard
F4Key KeyCode = "F4"
// F5Key represent "F5" key on the keyboard
F5Key KeyCode = "F5"
// F6Key represent "F6" key on the keyboard
F6Key KeyCode = "F6"
// F7Key represent "F7" key on the keyboard
F7Key KeyCode = "F7"
// F8Key represent "F8" key on the keyboard
F8Key KeyCode = "F8"
// F9Key represent "F9" key on the keyboard
F9Key KeyCode = "F9"
// F10Key represent "F10" key on the keyboard
F10Key KeyCode = "F10"
// F11Key represent "F11" key on the keyboard
F11Key KeyCode = "F11"
// F12Key represent "F12" key on the keyboard
F12Key KeyCode = "F12"
// F13Key represent "F13" key on the keyboard
F13Key KeyCode = "F13"
// NumLockKey represent "NumLock" key on the keyboard
NumLockKey KeyCode = "NumLock"
// NumpadKey0 represent "Numpad0" key on the keyboard
NumpadKey0 KeyCode = "Numpad0"
// NumpadKey1 represent "Numpad1" key on the keyboard
NumpadKey1 KeyCode = "Numpad1"
// NumpadKey2 represent "Numpad2" key on the keyboard
NumpadKey2 KeyCode = "Numpad2"
// NumpadKey3 represent "Numpad3" key on the keyboard
NumpadKey3 KeyCode = "Numpad3"
// NumpadKey4 represent "Numpad4" key on the keyboard
NumpadKey4 KeyCode = "Numpad4"
// NumpadKey5 represent "Numpad5" key on the keyboard
NumpadKey5 KeyCode = "Numpad5"
// NumpadKey6 represent "Numpad6" key on the keyboard
NumpadKey6 KeyCode = "Numpad6"
// NumpadKey7 represent "Numpad7" key on the keyboard
NumpadKey7 KeyCode = "Numpad7"
// NumpadKey8 represent "Numpad8" key on the keyboard
NumpadKey8 KeyCode = "Numpad8"
// NumpadKey9 represent "Numpad9" key on the keyboard
NumpadKey9 KeyCode = "Numpad9"
// NumpadDecimalKey represent "NumpadDecimal" key on the keyboard
NumpadDecimalKey KeyCode = "NumpadDecimal"
// NumpadEnterKey represent "NumpadEnter" key on the keyboard
NumpadEnterKey KeyCode = "NumpadEnter"
// NumpadAddKey represent "NumpadAdd" key on the keyboard
NumpadAddKey KeyCode = "NumpadAdd"
// NumpadSubtractKey represent "NumpadSubtract" key on the keyboard
NumpadSubtractKey KeyCode = "NumpadSubtract"
// NumpadMultiplyKey represent "NumpadMultiply" key on the keyboard
NumpadMultiplyKey KeyCode = "NumpadMultiply"
// NumpadDivideKey represent "NumpadDivide" key on the keyboard
NumpadDivideKey KeyCode = "NumpadDivide"
// ShiftLeftKey represent "ShiftLeft" key on the keyboard
ShiftLeftKey KeyCode = "ShiftLeft"
// ShiftRightKey represent "ShiftRight" key on the keyboard
ShiftRightKey KeyCode = "ShiftRight"
// ControlLeftKey represent "ControlLeft" key on the keyboard
ControlLeftKey KeyCode = "ControlLeft"
// ControlRightKey represent "ControlRight" key on the keyboard
ControlRightKey KeyCode = "ControlRight"
// AltLeftKey represent "AltLeft" key on the keyboard
AltLeftKey KeyCode = "AltLeft"
// AltRightKey represent "AltRight" key on the keyboard
AltRightKey KeyCode = "AltRight"
// MetaLeftKey represent "MetaLeft" key on the keyboard
MetaLeftKey KeyCode = "MetaLeft"
// MetaRightKey represent "MetaRight" key on the keyboard
MetaRightKey KeyCode = "MetaRight"
)
// KeyEvent represent a keyboard event
type KeyEvent struct { type KeyEvent struct {
// TimeStamp is the time at which the event was created (in milliseconds). // TimeStamp is the time at which the event was created (in milliseconds).
// This value is time since epoch—but in reality, browsers' definitions vary. // This value is time since epoch—but in reality, browsers' definitions vary.
@ -396,7 +32,7 @@ type KeyEvent struct {
// Code holds a string that identifies the physical key being pressed. The value is not affected // Code holds a string that identifies the physical key being pressed. The value is not affected
// by the current keyboard layout or modifier state, so a particular key will always return the same value. // by the current keyboard layout or modifier state, so a particular key will always return the same value.
Code KeyCode Code string
// Repeat == true if a key has been depressed long enough to trigger key repetition, otherwise false. // Repeat == true if a key has been depressed long enough to trigger key repetition, otherwise false.
Repeat bool Repeat bool
@ -423,8 +59,7 @@ func (event *KeyEvent) init(data DataObject) {
} }
event.Key, _ = data.PropertyValue("key") event.Key, _ = data.PropertyValue("key")
code, _ := data.PropertyValue("code") event.Code, _ = data.PropertyValue("code")
event.Code = KeyCode(code)
event.TimeStamp = getTimeStamp(data) event.TimeStamp = getTimeStamp(data)
event.Repeat = getBool("repeat") event.Repeat = getBool("repeat")
event.CtrlKey = getBool("ctrlKey") event.CtrlKey = getBool("ctrlKey")
@ -433,87 +68,207 @@ func (event *KeyEvent) init(data DataObject) {
event.MetaKey = getBool("metaKey") event.MetaKey = getBool("metaKey")
} }
func keyEventsHtml(view View, buffer *strings.Builder) { func valueToEventListeners[V View, E any](value any) ([]func(V, E), bool) {
if len(getOneArgEventListeners[View, KeyEvent](view, nil, KeyDownEvent)) > 0 || if value == nil {
(view.Focusable() && len(getOneArgEventListeners[View, MouseEvent](view, nil, ClickEvent)) > 0) { return nil, true
buffer.WriteString(`onkeydown="keyDownEvent(this, event)" `)
} }
if len(getOneArgEventListeners[View, KeyEvent](view, nil, KeyUpEvent)) > 0 { switch value := value.(type) {
buffer.WriteString(`onkeyup="keyUpEvent(this, event)" `) case func(V, E):
return []func(V, E){value}, true
case func(E):
fn := func(_ V, event E) {
value(event)
}
return []func(V, E){fn}, true
case func(V):
fn := func(view V, _ E) {
value(view)
}
return []func(V, E){fn}, true
case func():
fn := func(V, E) {
value()
}
return []func(V, E){fn}, true
case []func(V, E):
if len(value) == 0 {
return nil, true
}
for _, fn := range value {
if fn == nil {
return nil, false
}
}
return value, true
case []func(E):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(_ V, event E) {
v(event)
}
}
return listeners, true
case []func(V):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view V, _ E) {
v(view)
}
}
return listeners, true
case []func():
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(V, E) {
v()
}
}
return listeners, true
case []any:
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
switch v := v.(type) {
case func(V, E):
listeners[i] = v
case func(E):
listeners[i] = func(_ V, event E) {
v(event)
}
case func(V):
listeners[i] = func(view V, _ E) {
v(view)
}
case func():
listeners[i] = func(V, E) {
v()
}
default:
return nil, false
}
}
return listeners, true
}
return nil, false
}
var keyEvents = map[string]struct{ jsEvent, jsFunc string }{
KeyDownEvent: {jsEvent: "onkeydown", jsFunc: "keyDownEvent"},
KeyUpEvent: {jsEvent: "onkeyup", jsFunc: "keyUpEvent"},
}
func (view *viewData) setKeyListener(tag string, value any) bool {
listeners, ok := valueToEventListeners[View, KeyEvent](value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeKeyListener(tag)
} else if js, ok := keyEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
}
} else {
return false
}
return true
}
func (view *viewData) removeKeyListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := keyEvents[tag]; ok {
removeProperty(view.htmlID(), js.jsEvent, view.Session())
}
} }
} }
func handleKeyEvents(view View, tag PropertyName, data DataObject) { func getEventListeners[V View, E any](view View, subviewID []string, tag string) []func(V, E) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.Get(tag); value != nil {
if result, ok := value.([]func(V, E)); ok {
return result
}
}
}
return []func(V, E){}
}
func keyEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range keyEvents {
if listeners := getEventListeners[View, KeyEvent](view, nil, tag); len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
}
}
}
func handleKeyEvents(view View, tag string, data DataObject) {
listeners := getEventListeners[View, KeyEvent](view, nil, tag)
if len(listeners) > 0 {
var event KeyEvent var event KeyEvent
event.init(data) event.init(data)
listeners := getOneArgEventListeners[View, KeyEvent](view, nil, tag)
if len(listeners) > 0 {
for _, listener := range listeners { for _, listener := range listeners {
listener.Run(view, event) listener(view, event)
}
return
}
if tag == KeyDownEvent && view.Focusable() && (event.Key == " " || event.Key == "Enter") &&
!IsDisabled(view) && GetSemantics(view) != ButtonSemantics {
switch view.Tag() {
case "EditView", "ListView", "TableView", "TabsLayout", "TimePicker", "DatePicker", "AudioPlayer", "VideoPlayer":
return
}
if listeners := getOneArgEventListeners[View, MouseEvent](view, nil, ClickEvent); len(listeners) > 0 {
clickEvent := MouseEvent{
TimeStamp: event.TimeStamp,
Button: PrimaryMouseButton,
Buttons: PrimaryMouseMask,
CtrlKey: event.CtrlKey,
AltKey: event.AltKey,
ShiftKey: event.ShiftKey,
MetaKey: event.MetaKey,
ClientX: view.Frame().Width / 2,
ClientY: view.Frame().Height / 2,
X: view.Frame().Width / 2,
Y: view.Frame().Height / 2,
ScreenX: view.Frame().Left + view.Frame().Width/2,
ScreenY: view.Frame().Top + view.Frame().Height/2,
}
for _, listener := range listeners {
listener.Run(view, clickEvent)
}
} }
} }
} }
// GetKeyDownListeners returns the "key-down-event" listener list. If there are no listeners then the empty list is returned. // GetKeyDownListeners returns the "key-down-event" listener list. If there are no listeners then the empty list is returned.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetKeyDownListeners(view View, subviewID ...string) []func(View, KeyEvent) {
// - func(rui.View, rui.KeyEvent), return getEventListeners[View, KeyEvent](view, subviewID, KeyDownEvent)
// - func(rui.View),
// - func(rui.KeyEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetKeyDownListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, KeyEvent](view, subviewID, KeyDownEvent)
} }
// GetKeyUpListeners returns the "key-up-event" listener list. If there are no listeners then the empty list is returned. // GetKeyUpListeners returns the "key-up-event" listener list. If there are no listeners then the empty list is returned.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetKeyUpListeners(view View, subviewID ...string) []func(View, KeyEvent) {
// - func(rui.View, rui.KeyEvent), return getEventListeners[View, KeyEvent](view, subviewID, KeyUpEvent)
// - func(rui.View),
// - func(rui.KeyEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetKeyUpListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, KeyEvent](view, subviewID, KeyUpEvent)
} }

View File

@ -2,16 +2,8 @@ package rui
// ListAdapter - the list data source // ListAdapter - the list data source
type ListAdapter interface { type ListAdapter interface {
// ListSize returns the number of elements in the list
ListSize() int ListSize() int
// ListItem creates a View of a list item at the given index
ListItem(index int, session Session) View ListItem(index int, session Session) View
}
// ListItemEnabled implements the optional method of ListAdapter interface
type ListItemEnabled interface {
// IsListItemEnabled returns the status (enabled/disabled) of a list item at the given index
IsListItemEnabled(index int) bool IsListItemEnabled(index int) bool
} }

View File

@ -4,44 +4,30 @@ import (
"strings" "strings"
) )
// Constants which represent values of the "orientation" property of the [ListLayout]
const ( const (
// TopDownOrientation - subviews are arranged from top to bottom. Synonym of VerticalOrientation // TopDownOrientation - subviews are arranged from top to bottom. Synonym of VerticalOrientation
TopDownOrientation = 0 TopDownOrientation = 0
// StartToEndOrientation - subviews are arranged from left to right. Synonym of HorizontalOrientation // StartToEndOrientation - subviews are arranged from left to right. Synonym of HorizontalOrientation
StartToEndOrientation = 1 StartToEndOrientation = 1
// BottomUpOrientation - subviews are arranged from bottom to top // BottomUpOrientation - subviews are arranged from bottom to top
BottomUpOrientation = 2 BottomUpOrientation = 2
// EndToStartOrientation - subviews are arranged from right to left // EndToStartOrientation - subviews are arranged from right to left
EndToStartOrientation = 3 EndToStartOrientation = 3
)
// Constants which represent values of the "list-wrap" property of the [ListLayout]
const (
// ListWrapOff - subviews are scrolled and "true" if a new row/column starts // ListWrapOff - subviews are scrolled and "true" if a new row/column starts
ListWrapOff = 0 ListWrapOff = 0
// ListWrapOn - the new row/column starts at bottom/right // ListWrapOn - the new row/column starts at bottom/right
ListWrapOn = 1 ListWrapOn = 1
// ListWrapReverse - the new row/column starts at top/left // ListWrapReverse - the new row/column starts at top/left
ListWrapReverse = 2 ListWrapReverse = 2
) )
// ListLayout represents a ListLayout view // ListLayout - list-container of View
type ListLayout interface { type ListLayout interface {
ViewsContainer ViewsContainer
// UpdateContent updates child Views if the "content" property value is set to ListAdapter,
// otherwise does nothing
UpdateContent()
} }
type listLayoutData struct { type listLayoutData struct {
viewsContainerData viewsContainerData
adapter ListAdapter
} }
// NewListLayout create new ListLayout object and return it // NewListLayout create new ListLayout object and return it
@ -53,8 +39,7 @@ func NewListLayout(session Session, params Params) ListLayout {
} }
func newListLayout(session Session) View { func newListLayout(session Session) View {
//return NewListLayout(session, nil) return NewListLayout(session, nil)
return new(listLayoutData)
} }
// Init initialize fields of ViewsAlignContainer by default values // Init initialize fields of ViewsAlignContainer by default values
@ -62,15 +47,14 @@ func (listLayout *listLayoutData) init(session Session) {
listLayout.viewsContainerData.init(session) listLayout.viewsContainerData.init(session)
listLayout.tag = "ListLayout" listLayout.tag = "ListLayout"
listLayout.systemClass = "ruiListLayout" listLayout.systemClass = "ruiListLayout"
listLayout.normalize = normalizeListLayoutTag
listLayout.get = listLayout.getFunc
listLayout.set = listLayout.setFunc
listLayout.remove = listLayout.removeFunc
listLayout.changed = listLayout.propertyChanged
} }
func normalizeListLayoutTag(tag PropertyName) PropertyName { func (listLayout *listLayoutData) String() string {
tag = defaultNormalize(tag) return getViewString(listLayout)
}
func (listLayout *listLayoutData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag { switch tag {
case "wrap": case "wrap":
tag = ListWrap tag = ListWrap
@ -84,149 +68,98 @@ func normalizeListLayoutTag(tag PropertyName) PropertyName {
return tag return tag
} }
func (listLayout *listLayoutData) getFunc(tag PropertyName) any { func (listLayout *listLayoutData) Get(tag string) any {
switch tag { return listLayout.get(listLayout.normalizeTag(tag))
case Gap: }
func (listLayout *listLayoutData) get(tag string) any {
if tag == Gap {
if rowGap := GetListRowGap(listLayout); rowGap.Equal(GetListColumnGap(listLayout)) { if rowGap := GetListRowGap(listLayout); rowGap.Equal(GetListColumnGap(listLayout)) {
return rowGap return rowGap
} }
return AutoSize() return AutoSize()
case Content:
if listLayout.adapter != nil {
return listLayout.adapter
}
} }
return listLayout.viewsContainerData.getFunc(tag) return listLayout.viewsContainerData.get(tag)
} }
func (listLayout *listLayoutData) removeFunc(tag PropertyName) []PropertyName { func (listLayout *listLayoutData) Remove(tag string) {
switch tag { listLayout.remove(listLayout.normalizeTag(tag))
case Gap:
result := []PropertyName{}
for _, tag := range []PropertyName{ListRowGap, ListColumnGap} {
if listLayout.getRaw(tag) != nil {
listLayout.setRaw(tag, nil)
result = append(result, tag)
}
}
return result
case Content:
result := listLayout.viewsContainerData.removeFunc(Content)
if listLayout.adapter != nil {
listLayout.adapter = nil
return []PropertyName{Content}
}
return result
}
return listLayout.viewsContainerData.removeFunc(tag)
} }
func (listLayout *listLayoutData) setFunc(tag PropertyName, value any) []PropertyName { func (listLayout *listLayoutData) remove(tag string) {
switch tag { if tag == Gap {
case Gap: listLayout.remove(ListRowGap)
result := listLayout.setFunc(ListRowGap, value) listLayout.remove(ListColumnGap)
if result != nil { return
if gap := listLayout.getRaw(ListRowGap); gap != nil {
listLayout.setRaw(ListColumnGap, gap)
result = append(result, ListColumnGap)
} }
} listLayout.viewsContainerData.remove(tag)
return result if listLayout.created {
case Content:
if adapter, ok := value.(ListAdapter); ok {
listLayout.adapter = adapter
listLayout.createContent()
} else if listLayout.setContent(value) {
listLayout.adapter = nil
} else {
return nil
}
return []PropertyName{Content}
}
return listLayout.viewsContainerData.setFunc(tag, value)
}
func (listLayout *listLayoutData) propertyChanged(tag PropertyName) {
switch tag { switch tag {
case Orientation, ListWrap, HorizontalAlign, VerticalAlign: case Orientation, ListWrap, HorizontalAlign, VerticalAlign:
updateCSSStyle(listLayout.htmlID(), listLayout.Session()) updateCSSStyle(listLayout.htmlID(), listLayout.session)
default:
listLayout.viewsContainerData.propertyChanged(tag)
} }
}
}
func (listLayout *listLayoutData) Set(tag string, value any) bool {
return listLayout.set(listLayout.normalizeTag(tag), value)
}
func (listLayout *listLayoutData) set(tag string, value any) bool {
if value == nil {
listLayout.remove(tag)
return true
}
if tag == Gap {
return listLayout.set(ListRowGap, value) && listLayout.set(ListColumnGap, value)
}
if listLayout.viewsContainerData.set(tag, value) {
if listLayout.created {
switch tag {
case Orientation, ListWrap, HorizontalAlign, VerticalAlign:
updateCSSStyle(listLayout.htmlID(), listLayout.session)
}
}
return true
}
return false
} }
func (listLayout *listLayoutData) htmlSubviews(self View, buffer *strings.Builder) { func (listLayout *listLayoutData) htmlSubviews(self View, buffer *strings.Builder) {
if listLayout.views != nil { if listLayout.views != nil {
for _, view := range listLayout.views { for _, view := range listLayout.views {
view.addToCSSStyle(map[string]string{`flex`: `0 0 auto`}) view.addToCSSStyle(map[string]string{`flex`: `0 0 auto`})
viewHTML(view, buffer, "") viewHTML(view, buffer)
} }
} }
} }
func (listLayout *listLayoutData) createContent() bool { // GetListVerticalAlign returns the vertical align of a ListLayout or ListView sibview:
if adapter := listLayout.adapter; adapter != nil {
listLayout.views = []View{}
session := listLayout.session
htmlID := listLayout.htmlID()
isDisabled := IsDisabled(listLayout)
for i := range adapter.ListSize() {
if view := adapter.ListItem(i, session); view != nil {
view.setParentID(htmlID)
if isDisabled {
view.Set(Disabled, true)
}
listLayout.views = append(listLayout.views, view)
}
}
return true
}
return false
}
func (listLayout *listLayoutData) UpdateContent() {
if listLayout.createContent() {
if listLayout.created {
updateInnerHTML(listLayout.htmlID(), listLayout.session)
}
listLayout.runChangeListener(Content)
}
}
// GetListVerticalAlign returns the vertical align of a ListLayout or ListView subview:
// TopAlign (0), BottomAlign (1), CenterAlign (2), or StretchAlign (3) // TopAlign (0), BottomAlign (1), CenterAlign (2), or StretchAlign (3)
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetListVerticalAlign(view View, subviewID ...string) int { func GetListVerticalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, VerticalAlign, TopAlign, false) return enumStyledProperty(view, subviewID, VerticalAlign, TopAlign, false)
} }
// GetListHorizontalAlign returns the vertical align of a ListLayout or ListView subview: // GetListHorizontalAlign returns the vertical align of a ListLayout or ListView subview:
// LeftAlign (0), RightAlign (1), CenterAlign (2), or StretchAlign (3) // LeftAlign (0), RightAlign (1), CenterAlign (2), or StretchAlign (3)
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetListHorizontalAlign(view View, subviewID ...string) int { func GetListHorizontalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, HorizontalAlign, LeftAlign, false) return enumStyledProperty(view, subviewID, HorizontalAlign, LeftAlign, false)
} }
// GetListOrientation returns the orientation of a ListLayout or ListView subview: // GetListOrientation returns the orientation of a ListLayout or ListView subview:
// TopDownOrientation (0), StartToEndOrientation (1), BottomUpOrientation (2), or EndToStartOrientation (3) // TopDownOrientation (0), StartToEndOrientation (1), BottomUpOrientation (2), or EndToStartOrientation (3)
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetListOrientation(view View, subviewID ...string) int { func GetListOrientation(view View, subviewID ...string) int {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if orientation, ok := valueToOrientation(view.Get(Orientation), view.Session()); ok { if orientation, ok := valueToOrientation(view.Get(Orientation), view.Session()); ok {
return orientation return orientation
} }
@ -238,51 +171,24 @@ func GetListOrientation(view View, subviewID ...string) int {
} }
} }
return TopDownOrientation return 0
} }
// GetListWrap returns the wrap type of a ListLayout or ListView subview: // GetListWrap returns the wrap type of a ListLayout or ListView subview:
// ListWrapOff (0), ListWrapOn (1), or ListWrapReverse (2) // ListWrapOff (0), ListWrapOn (1), or ListWrapReverse (2)
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetListWrap(view View, subviewID ...string) int { func GetListWrap(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, ListWrap, ListWrapOff, false) return enumStyledProperty(view, subviewID, ListWrap, ListWrapOff, false)
} }
// GetListRowGap returns the gap between ListLayout or ListView rows. // GetListRowGap returns the gap between ListLayout or ListView rows.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetListRowGap(view View, subviewID ...string) SizeUnit { func GetListRowGap(view View, subviewID ...string) SizeUnit {
return sizeStyledProperty(view, subviewID, ListRowGap, false) return sizeStyledProperty(view, subviewID, ListRowGap, false)
} }
// GetListColumnGap returns the gap between ListLayout or ListView columns. // GetListColumnGap returns the gap between ListLayout or ListView columns.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetListColumnGap(view View, subviewID ...string) SizeUnit { func GetListColumnGap(view View, subviewID ...string) SizeUnit {
return sizeStyledProperty(view, subviewID, ListColumnGap, false) return sizeStyledProperty(view, subviewID, ListColumnGap, false)
} }
// UpdateContent updates child Views of ListLayout/GridLayout subview if the "content" property value is set to ListAdapter/GridAdapter,
// otherwise does nothing.
// If the second argument (subviewID) is not specified or it is "" then the first argument (view) updates.
func UpdateContent(view View, subviewID ...string) {
if view = getSubview(view, subviewID); view != nil {
switch view := view.(type) {
case GridLayout:
view.UpdateGridContent()
case ListLayout:
view.UpdateContent()
case ListView:
view.ReloadListViewData()
case TableView:
view.ReloadTableData()
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -5,205 +5,105 @@ import (
"strings" "strings"
) )
// Constants related to [View] mouse events properties
const ( const (
// ClickEvent is the constant for "click-event" property tag. // ClickEvent is the constant for "click-event" property tag.
// // The "click-event" event occurs when the user clicks on the View.
// Used by View. // The main listener format:
// Occur when the user clicks on the view. // func(View, MouseEvent).
// // The additional listener formats:
// General listener format: // func(MouseEvent), func(View), and func().
// ClickEvent = "click-event"
// func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
ClickEvent PropertyName = "click-event"
// DoubleClickEvent is the constant for "double-click-event" property tag. // DoubleClickEvent is the constant for "double-click-event" property tag.
// // The "double-click-event" event occurs when the user double clicks on the View.
// Used by View. // The main listener format:
// Occur when the user double clicks on the view. // func(View, MouseEvent).
// // The additional listener formats:
// General listener format: // func(MouseEvent), func(View), and func().
// DoubleClickEvent = "double-click-event"
// func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
DoubleClickEvent PropertyName = "double-click-event"
// MouseDown is the constant for "mouse-down" property tag. // MouseDown is the constant for "mouse-down" property tag.
// // The "mouse-down" event is fired at a View when a pointing device button is pressed
// Used by View. // while the pointer is inside the view.
// Is fired at a View when a pointing device button is pressed while the pointer is inside the view. // The main listener format:
// // func(View, MouseEvent).
// General listener format: // The additional listener formats:
// // func(MouseEvent), func(View), and func().
// func(view rui.View, event rui.MouseEvent) MouseDown = "mouse-down"
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
MouseDown PropertyName = "mouse-down"
// MouseUp is the constant for "mouse-up" property tag. // MouseUp is the constant for "mouse-up" property tag.
// // The "mouse-up" event is fired at a View when a button on a pointing device (such as a mouse
// Used by View. // or trackpad) is released while the pointer is located inside it.
// Is fired at a View when a button on a pointing device (such as a mouse or trackpad) is released while the pointer is // "mouse-up" events are the counterpoint to "mouse-down" events.
// located inside it. "mouse-up" events are the counterpoint to "mouse-down" events. // The main listener format:
// // func(View, MouseEvent).
// General listener format: // The additional listener formats:
// // func(MouseEvent), func(View), and func().
// func(view rui.View, event rui.MouseEvent) MouseUp = "mouse-up"
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
MouseUp PropertyName = "mouse-up"
// MouseMove is the constant for "mouse-move" property tag. // MouseMove is the constant for "mouse-move" property tag.
// // The "mouse-move" event is fired at a view when a pointing device (usually a mouse) is moved
// Used by View. // while the cursor's hotspot is inside it.
// Is fired at a view when a pointing device(usually a mouse) is moved while the cursor's hotspot is inside it. // The main listener format:
// // func(View, MouseEvent).
// General listener format: // The additional listener formats:
// // func(MouseEvent), func(View), and func().
// func(view rui.View, event rui.MouseEvent) MouseMove = "mouse-move"
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
MouseMove PropertyName = "mouse-move"
// MouseOut is the constant for "mouse-out" property tag. // MouseOut is the constant for "mouse-out" property tag.
// // The "mouse-out" event is fired at a View when a pointing device (usually a mouse) is used to move
// Used by View. // the cursor so that it is no longer contained within the view or one of its children.
// Is fired at a View when a pointing device (usually a mouse) is used to move the cursor so that it is no longer // "mouse-out" is also delivered to a view if the cursor enters a child view,
// contained within the view or one of its children. "mouse-out" is also delivered to a view if the cursor enters a child // because the child view obscures the visible area of the view.
// view, because the child view obscures the visible area of the view. // The main listener format:
// // func(View, MouseEvent).
// General listener format: // The additional listener formats:
// // func(MouseEvent), func(View), and func().
// func(view rui.View, event rui.MouseEvent) // The additional listener formats:
// // func(MouseEvent), func(View), and func().
// where: MouseOut = "mouse-out"
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
MouseOut PropertyName = "mouse-out"
// MouseOver is the constant for "mouse-over" property tag. // MouseOver is the constant for "mouse-over" property tag.
// // The "mouse-over" event is fired at a View when a pointing device (such as a mouse or trackpad)
// Used by View. // is used to move the cursor onto the view or one of its child views.
// Is fired at a View when a pointing device (such as a mouse or trackpad) is used to move the cursor onto the view or one // The main listener formats:
// of its child views. // func(View, MouseEvent).
// // The additional listener formats:
// General listener format: // func(MouseEvent), func(View), and func().
// MouseOver = "mouse-over"
// func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
MouseOver PropertyName = "mouse-over"
// ContextMenuEvent is the constant for "context-menu-event" property tag. // ContextMenuEvent is the constant for "context-menu-event" property tag.
// // The "context-menu-event" event occurs when the user calls the context menu by the right mouse clicking.
// Used by View. // The main listener format:
// Occur when the user calls the context menu by the right mouse clicking. // func(View, MouseEvent).
// // The additional listener formats:
// General listener format: // func(MouseEvent), func(View), and func().
// ContextMenuEvent = "context-menu-event"
// func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
ContextMenuEvent PropertyName = "context-menu-event"
// PrimaryMouseButton is a number of the main pressed button, usually the left button or the un-initialized state // PrimaryMouseButton is a number of the main pressed button, usually the left button or the un-initialized state
PrimaryMouseButton = 0 PrimaryMouseButton = 0
// AuxiliaryMouseButton is a number of the auxiliary pressed button, usually the wheel button // AuxiliaryMouseButton is a number of the auxiliary pressed button, usually the wheel button
// or the middle button (if present) // or the middle button (if present)
AuxiliaryMouseButton = 1 AuxiliaryMouseButton = 1
// SecondaryMouseButton is a number of the secondary pressed button, usually the right button // SecondaryMouseButton is a number of the secondary pressed button, usually the right button
SecondaryMouseButton = 2 SecondaryMouseButton = 2
// MouseButton4 is a number of the fourth button, typically the Browser Back button // MouseButton4 is a number of the fourth button, typically the Browser Back button
MouseButton4 = 3 MouseButton4 = 3
// MouseButton5 is a number of the fifth button, typically the Browser Forward button // MouseButton5 is a number of the fifth button, typically the Browser Forward button
MouseButton5 = 4 MouseButton5 = 4
// PrimaryMouseMask is the mask of the primary button (usually the left button) // PrimaryMouseMask is the mask of the primary button (usually the left button)
PrimaryMouseMask = 1 PrimaryMouseMask = 1
// SecondaryMouseMask is the mask of the secondary button (usually the right button) // SecondaryMouseMask is the mask of the secondary button (usually the right button)
SecondaryMouseMask = 2 SecondaryMouseMask = 2
// AuxiliaryMouseMask is the mask of the auxiliary button (usually the mouse wheel button or middle button) // AuxiliaryMouseMask is the mask of the auxiliary button (usually the mouse wheel button or middle button)
AuxiliaryMouseMask = 4 AuxiliaryMouseMask = 4
// MouseMask4 is the mask of the 4th button (typically the "Browser Back" button) // MouseMask4 is the mask of the 4th button (typically the "Browser Back" button)
MouseMask4 = 8 MouseMask4 = 8
//MouseMask5 is the mask of the 5th button (typically the "Browser Forward" button) //MouseMask5 is the mask of the 5th button (typically the "Browser Forward" button)
MouseMask5 = 16 MouseMask5 = 16
) )
// MouseEvent represent a mouse event
type MouseEvent struct { type MouseEvent struct {
// TimeStamp is the time at which the event was created (in milliseconds). // TimeStamp is the time at which the event was created (in milliseconds).
// This value is time since epoch—but in reality, browsers' definitions vary. // This value is time since epoch—but in reality, browsers' definitions vary.
@ -244,6 +144,56 @@ type MouseEvent struct {
MetaKey bool MetaKey bool
} }
var mouseEvents = map[string]struct{ jsEvent, jsFunc string }{
ClickEvent: {jsEvent: "onclick", jsFunc: "clickEvent"},
DoubleClickEvent: {jsEvent: "ondblclick", jsFunc: "doubleClickEvent"},
MouseDown: {jsEvent: "onmousedown", jsFunc: "mouseDownEvent"},
MouseUp: {jsEvent: "onmouseup", jsFunc: "mouseUpEvent"},
MouseMove: {jsEvent: "onmousemove", jsFunc: "mouseMoveEvent"},
MouseOut: {jsEvent: "onmouseout", jsFunc: "mouseOutEvent"},
MouseOver: {jsEvent: "onmouseover", jsFunc: "mouseOverEvent"},
ContextMenuEvent: {jsEvent: "oncontextmenu", jsFunc: "contextMenuEvent"},
}
func (view *viewData) setMouseListener(tag string, value any) bool {
listeners, ok := valueToEventListeners[View, MouseEvent](value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeMouseListener(tag)
} else if js, ok := mouseEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
}
} else {
return false
}
return true
}
func (view *viewData) removeMouseListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := mouseEvents[tag]; ok {
removeProperty(view.htmlID(), js.jsEvent, view.Session())
}
}
}
func mouseEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range mouseEvents {
if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View, MouseEvent)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
}
}
}
}
func getTimeStamp(data DataObject) uint64 { func getTimeStamp(data DataObject) uint64 {
if value, ok := data.PropertyValue("timeStamp"); ok { if value, ok := data.PropertyValue("timeStamp"); ok {
if index := strings.Index(value, "."); index > 0 { if index := strings.Index(value, "."); index > 0 {
@ -273,135 +223,63 @@ func (event *MouseEvent) init(data DataObject) {
event.MetaKey = dataBoolProperty(data, "metaKey") event.MetaKey = dataBoolProperty(data, "metaKey")
} }
func handleMouseEvents(view View, tag PropertyName, data DataObject) { func handleMouseEvents(view View, tag string, data DataObject) {
listeners := getOneArgEventListeners[View, MouseEvent](view, nil, tag) listeners := getEventListeners[View, MouseEvent](view, nil, tag)
if len(listeners) > 0 { if len(listeners) > 0 {
var event MouseEvent var event MouseEvent
event.init(data) event.init(data)
for _, listener := range listeners { for _, listener := range listeners {
listener.Run(view, event) listener(view, event)
} }
} }
} }
// GetClickListeners returns the "click-event" listener list. If there are no listeners then the empty list is returned. // GetClickListeners returns the "click-event" listener list. If there are no listeners then the empty list is returned.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetClickListeners(view View, subviewID ...string) []func(View, MouseEvent) {
// - func(View, MouseEvent), return getEventListeners[View, MouseEvent](view, subviewID, ClickEvent)
// - func(View),
// - func(MouseEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetClickListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, MouseEvent](view, subviewID, ClickEvent)
} }
// GetDoubleClickListeners returns the "double-click-event" listener list. If there are no listeners then the empty list is returned. // GetDoubleClickListeners returns the "double-click-event" listener list. If there are no listeners then the empty list is returned.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetDoubleClickListeners(view View, subviewID ...string) []func(View, MouseEvent) {
// - func(View, MouseEvent), return getEventListeners[View, MouseEvent](view, subviewID, DoubleClickEvent)
// - func(View),
// - func(MouseEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDoubleClickListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, MouseEvent](view, subviewID, DoubleClickEvent)
} }
// GetContextMenuListeners returns the "context-menu" listener list. // GetContextMenuListeners returns the "context-menu" listener list.
// If there are no listeners then the empty list is returned. // If there are no listeners then the empty list is returned.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetContextMenuListeners(view View, subviewID ...string) []func(View, MouseEvent) {
// - func(View, MouseEvent), return getEventListeners[View, MouseEvent](view, subviewID, ContextMenuEvent)
// - func(View),
// - func(MouseEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetContextMenuListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, MouseEvent](view, subviewID, ContextMenuEvent)
} }
// GetMouseDownListeners returns the "mouse-down" listener list. If there are no listeners then the empty list is returned. // GetMouseDownListeners returns the "mouse-down" listener list. If there are no listeners then the empty list is returned.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetMouseDownListeners(view View, subviewID ...string) []func(View, MouseEvent) {
// - func(View, MouseEvent), return getEventListeners[View, MouseEvent](view, subviewID, MouseDown)
// - func(View),
// - func(MouseEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetMouseDownListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, MouseEvent](view, subviewID, MouseDown)
} }
// GetMouseUpListeners returns the "mouse-up" listener list. If there are no listeners then the empty list is returned. // GetMouseUpListeners returns the "mouse-up" listener list. If there are no listeners then the empty list is returned.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetMouseUpListeners(view View, subviewID ...string) []func(View, MouseEvent) {
// - func(View, MouseEvent), return getEventListeners[View, MouseEvent](view, subviewID, MouseUp)
// - func(View),
// - func(MouseEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetMouseUpListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, MouseEvent](view, subviewID, MouseUp)
} }
// GetMouseMoveListeners returns the "mouse-move" listener list. If there are no listeners then the empty list is returned. // GetMouseMoveListeners returns the "mouse-move" listener list. If there are no listeners then the empty list is returned.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetMouseMoveListeners(view View, subviewID ...string) []func(View, MouseEvent) {
// - func(View, MouseEvent), return getEventListeners[View, MouseEvent](view, subviewID, MouseMove)
// - func(View),
// - func(MouseEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetMouseMoveListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, MouseEvent](view, subviewID, MouseMove)
} }
// GetMouseOverListeners returns the "mouse-over" listener list. If there are no listeners then the empty list is returned. // GetMouseOverListeners returns the "mouse-over" listener list. If there are no listeners then the empty list is returned.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetMouseOverListeners(view View, subviewID ...string) []func(View, MouseEvent) {
// - func(View, MouseEvent), return getEventListeners[View, MouseEvent](view, subviewID, MouseOver)
// - func(View),
// - func(MouseEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetMouseOverListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, MouseEvent](view, subviewID, MouseOver)
} }
// GetMouseOutListeners returns the "mouse-out" listener list. If there are no listeners then the empty list is returned. // GetMouseOutListeners returns the "mouse-out" listener list. If there are no listeners then the empty list is returned.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetMouseOutListeners(view View, subviewID ...string) []func(View, MouseEvent) {
// - func(View, MouseEvent), return getEventListeners[View, MouseEvent](view, subviewID, MouseOut)
// - func(View),
// - func(MouseEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetMouseOutListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, MouseEvent](view, subviewID, MouseOut)
} }

View File

@ -7,109 +7,30 @@ import (
"strings" "strings"
) )
// Constants related to [NumberPicker] specific properties and events
const ( const (
// NumberChangedEvent is the constant for "number-changed" property tag. NumberChangedEvent = "number-changed"
// NumberPickerType = "number-picker-type"
// Used by NumberPicker. NumberPickerMin = "number-picker-min"
// Set listener(s) that track the change in the entered value. NumberPickerMax = "number-picker-max"
// NumberPickerStep = "number-picker-step"
// General listener format: NumberPickerValue = "number-picker-value"
//
// func(picker rui.NumberPicker, newValue float64, oldValue float64)
//
// where:
// - picker - Interface of a number picker which generated this event,
// - newValue - New value,
// - oldValue - Old Value.
//
// Allowed listener formats:
//
// func(picker rui.NumberPicker, newValue float64)
// func(newValue float64, oldValue float64)
// func(newValue float64)
// func()
NumberChangedEvent PropertyName = "number-changed"
// NumberPickerType is the constant for "number-picker-type" property tag.
//
// Used by NumberPicker.
// Sets the visual representation.
//
// Supported types: int, string.
//
// Values:
// - 0 (NumberEditor) or "editor" - Displayed as an editor.
// - 1 (NumberSlider) or "slider" - Displayed as a slider.
NumberPickerType PropertyName = "number-picker-type"
// NumberPickerMin is the constant for "number-picker-min" property tag.
//
// Used by NumberPicker.
// Set the minimum value. The default value is 0.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
NumberPickerMin PropertyName = "number-picker-min"
// NumberPickerMax is the constant for "number-picker-max" property tag.
//
// Used by NumberPicker.
// Set the maximum value. The default value is 1.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
NumberPickerMax PropertyName = "number-picker-max"
// NumberPickerStep is the constant for "number-picker-step" property tag.
//
// Used by NumberPicker.
// Set the value change step.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
NumberPickerStep PropertyName = "number-picker-step"
// NumberPickerValue is the constant for "number-picker-value" property tag.
//
// Used by NumberPicker.
// Current value. The default value is 0.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
NumberPickerValue PropertyName = "number-picker-value"
// NumberPickerValue is the constant for "number-picker-value" property tag.
//
// Used by NumberPicker.
// Precision of displaying fractional part in editor. The default value is 0 (not used).
//
// Supported types: int, int8...int64, uint, uint8...uint64, string.
//
// Internal type is float, other types converted to it during assignment.
NumberPickerPrecision PropertyName = "number-picker-precision"
) )
// Constants which describe values of the "number-picker-type" property of a [NumberPicker]
const ( const (
// NumberEditor - type of NumberPicker. NumberPicker is presented by editor // NumberEditor - type of NumberPicker. NumberPicker is presented by editor
NumberEditor = 0 NumberEditor = 0
// NumberSlider - type of NumberPicker. NumberPicker is presented by slider // NumberSlider - type of NumberPicker. NumberPicker is presented by slider
NumberSlider = 1 NumberSlider = 1
) )
// NumberPicker represents a NumberPicker view // NumberPicker - NumberPicker view
type NumberPicker interface { type NumberPicker interface {
View View
} }
type numberPickerData struct { type numberPickerData struct {
viewData viewData
numberChangedListeners []func(NumberPicker, float64)
} }
// NewNumberPicker create new NumberPicker object and return it // NewNumberPicker create new NumberPicker object and return it
@ -121,114 +42,146 @@ func NewNumberPicker(session Session, params Params) NumberPicker {
} }
func newNumberPicker(session Session) View { func newNumberPicker(session Session) View {
return new(numberPickerData) return NewNumberPicker(session, nil)
} }
func (picker *numberPickerData) init(session Session) { func (picker *numberPickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "NumberPicker" picker.tag = "NumberPicker"
picker.hasHtmlDisabled = true picker.numberChangedListeners = []func(NumberPicker, float64){}
picker.normalize = normalizeNumberPickerTag }
picker.get = picker.getFunc
picker.set = picker.setFunc func (picker *numberPickerData) String() string {
picker.changed = picker.propertyChanged return getViewString(picker)
} }
func (picker *numberPickerData) Focusable() bool { func (picker *numberPickerData) Focusable() bool {
return true return true
} }
func normalizeNumberPickerTag(tag PropertyName) PropertyName { func (picker *numberPickerData) normalizeTag(tag string) string {
tag = defaultNormalize(tag) tag = strings.ToLower(tag)
switch tag { switch tag {
case Type, Min, Max, Step, Value, "precision": case Type, Min, Max, Step, Value:
return "number-picker-" + tag return "number-picker-" + tag
} }
return normalizeDataListTag(tag) return tag
} }
func (picker *numberPickerData) getFunc(tag PropertyName) any { func (picker *numberPickerData) Remove(tag string) {
switch tag { picker.remove(picker.normalizeTag(tag))
case NumberChangedEvent:
if listeners := getTwoArgEventRawListeners[NumberPicker, float64](picker, nil, tag); len(listeners) > 0 {
return listeners
}
return nil
}
return picker.viewData.getFunc(tag)
} }
func (picker *numberPickerData) setFunc(tag PropertyName, value any) []PropertyName { func (picker *numberPickerData) remove(tag string) {
switch tag { switch tag {
case NumberChangedEvent: case NumberChangedEvent:
return setTwoArgEventListener[NumberPicker, float64](picker, tag, value) if len(picker.numberChangedListeners) > 0 {
picker.numberChangedListeners = []func(NumberPicker, float64){}
picker.propertyChangedEvent(tag)
}
default:
picker.viewData.remove(tag)
picker.propertyChanged(tag)
}
}
func (picker *numberPickerData) Set(tag string, value any) bool {
return picker.set(picker.normalizeTag(tag), value)
}
func (picker *numberPickerData) set(tag string, value any) bool {
if value == nil {
picker.remove(tag)
return true
}
switch tag {
case NumberChangedEvent:
listeners, ok := valueToEventListeners[NumberPicker, float64](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(NumberPicker, float64){}
}
picker.numberChangedListeners = listeners
picker.propertyChangedEvent(tag)
return true
case NumberPickerValue: case NumberPickerValue:
picker.setRaw("old-number", GetNumberPickerValue(picker)) oldValue := GetNumberPickerValue(picker)
min, max := GetNumberPickerMinMax(picker) min, max := GetNumberPickerMinMax(picker)
if picker.setFloatProperty(NumberPickerValue, value, min, max) {
return setFloatProperty(picker, NumberPickerValue, value, min, max) if f, ok := floatProperty(picker, NumberPickerValue, picker.Session(), min); ok && f != oldValue {
newValue, _ := floatTextProperty(picker, NumberPickerValue, picker.Session(), min)
case DataList: if picker.created {
return setDataList(picker, value, "") picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, picker.htmlID(), newValue))
}
for _, listener := range picker.numberChangedListeners {
listener(picker, f)
}
picker.propertyChangedEvent(tag)
}
return true
} }
return picker.viewData.setFunc(tag, value) default:
} if picker.viewData.set(tag, value) {
picker.propertyChanged(tag)
func (picker *numberPickerData) numberFormat() string { return true
if precision := GetNumberPickerPrecision(picker); precision > 0 {
return fmt.Sprintf("%%.%df", precision)
} }
return "%g" }
return false
} }
func (picker *numberPickerData) propertyChanged(tag PropertyName) { func (picker *numberPickerData) propertyChanged(tag string) {
if picker.created {
switch tag { switch tag {
case NumberPickerType: case NumberPickerType:
if GetNumberPickerType(picker) == NumberSlider { if GetNumberPickerType(picker) == NumberSlider {
picker.Session().updateProperty(picker.htmlID(), "type", "range") updateProperty(picker.htmlID(), "type", "range", picker.session)
} else { } else {
picker.Session().updateProperty(picker.htmlID(), "type", "number") updateProperty(picker.htmlID(), "type", "number", picker.session)
} }
case NumberPickerMin: case NumberPickerMin:
min, _ := GetNumberPickerMinMax(picker) min, _ := GetNumberPickerMinMax(picker)
picker.Session().updateProperty(picker.htmlID(), "min", fmt.Sprintf(picker.numberFormat(), min)) updateProperty(picker.htmlID(), Min, strconv.FormatFloat(min, 'f', -1, 32), picker.session)
case NumberPickerMax: case NumberPickerMax:
_, max := GetNumberPickerMinMax(picker) _, max := GetNumberPickerMinMax(picker)
picker.Session().updateProperty(picker.htmlID(), "max", fmt.Sprintf(picker.numberFormat(), max)) updateProperty(picker.htmlID(), Max, strconv.FormatFloat(max, 'f', -1, 32), picker.session)
case NumberPickerStep: case NumberPickerStep:
if step := GetNumberPickerStep(picker); step > 0 { if step := GetNumberPickerStep(picker); step > 0 {
picker.Session().updateProperty(picker.htmlID(), "step", fmt.Sprintf(picker.numberFormat(), step)) updateProperty(picker.htmlID(), Step, strconv.FormatFloat(step, 'f', -1, 32), picker.session)
} else { } else {
picker.Session().updateProperty(picker.htmlID(), "step", "any") updateProperty(picker.htmlID(), Step, "any", picker.session)
} }
case NumberPickerValue: case NumberPickerValue:
value := GetNumberPickerValue(picker) value := GetNumberPickerValue(picker)
format := picker.numberFormat() picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%f')`, picker.htmlID(), value))
picker.Session().callFunc("setInputValue", picker.htmlID(), fmt.Sprintf(format, value)) for _, listener := range picker.numberChangedListeners {
listener(picker, value)
}
}
}
}
if listeners := getTwoArgEventListeners[NumberPicker, float64](picker, nil, NumberChangedEvent); len(listeners) > 0 { func (picker *numberPickerData) Get(tag string) any {
old := 0.0 return picker.get(picker.normalizeTag(tag))
if val := picker.getRaw("old-number"); val != nil { }
if n, ok := val.(float64); ok {
old = n func (picker *numberPickerData) get(tag string) any {
} switch tag {
} case NumberChangedEvent:
if old != value { return picker.numberChangedListeners
for _, listener := range listeners {
listener.Run(picker, value, old)
}
}
}
default: default:
picker.viewData.propertyChanged(tag) return picker.viewData.get(tag)
} }
} }
@ -236,13 +189,6 @@ func (picker *numberPickerData) htmlTag() string {
return "input" return "input"
} }
func (picker *numberPickerData) htmlSubviews(self View, buffer *strings.Builder) {
dataListHtmlSubviews(self, buffer, func(text string, session Session) string {
text, _ = session.resolveConstants(text)
return text
})
}
func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builder) { func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builder) {
picker.viewData.htmlProperties(self, buffer) picker.viewData.htmlProperties(self, buffer)
@ -252,39 +198,43 @@ func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builde
buffer.WriteString(` type="number"`) buffer.WriteString(` type="number"`)
} }
format := picker.numberFormat()
min, max := GetNumberPickerMinMax(picker) min, max := GetNumberPickerMinMax(picker)
if min != math.Inf(-1) { if min != math.Inf(-1) {
buffer.WriteString(` min="`) buffer.WriteString(` min="`)
fmt.Fprintf(buffer, format, min) buffer.WriteString(strconv.FormatFloat(min, 'f', -1, 64))
buffer.WriteByte('"') buffer.WriteByte('"')
} }
if max != math.Inf(1) { if max != math.Inf(1) {
buffer.WriteString(` max="`) buffer.WriteString(` max="`)
fmt.Fprintf(buffer, format, max) buffer.WriteString(strconv.FormatFloat(max, 'f', -1, 64))
buffer.WriteByte('"') buffer.WriteByte('"')
} }
step := GetNumberPickerStep(picker) step := GetNumberPickerStep(picker)
if step != 0 { if step != 0 {
buffer.WriteString(` step="`) buffer.WriteString(` step="`)
fmt.Fprintf(buffer, format, step) buffer.WriteString(strconv.FormatFloat(step, 'f', -1, 64))
buffer.WriteByte('"') buffer.WriteByte('"')
} else { } else {
buffer.WriteString(` step="any"`) buffer.WriteString(` step="any"`)
} }
buffer.WriteString(` value="`) buffer.WriteString(` value="`)
fmt.Fprintf(buffer, format, GetNumberPickerValue(picker)) buffer.WriteString(strconv.FormatFloat(GetNumberPickerValue(picker), 'f', -1, 64))
buffer.WriteByte('"') buffer.WriteByte('"')
buffer.WriteString(` oninput="editViewInputEvent(this)"`) buffer.WriteString(` oninput="editViewInputEvent(this)"`)
dataListHtmlProperties(picker, buffer)
} }
func (picker *numberPickerData) handleCommand(self View, command PropertyName, data DataObject) bool { func (picker *numberPickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
if IsDisabled(self) {
buffer.WriteString(` disabled`)
}
picker.viewData.htmlDisabledProperties(self, buffer)
}
func (picker *numberPickerData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "textChanged": case "textChanged":
if text, ok := data.PropertyValue("text"); ok { if text, ok := data.PropertyValue("text"); ok {
@ -292,10 +242,9 @@ func (picker *numberPickerData) handleCommand(self View, command PropertyName, d
oldValue := GetNumberPickerValue(picker) oldValue := GetNumberPickerValue(picker)
picker.properties[NumberPickerValue] = text picker.properties[NumberPickerValue] = text
if value != oldValue { if value != oldValue {
for _, listener := range getTwoArgEventListeners[NumberPicker, float64](picker, nil, NumberChangedEvent) { for _, listener := range picker.numberChangedListeners {
listener.Run(picker, value, oldValue) listener(picker, value)
} }
picker.runChangeListener(NumberPickerValue)
} }
} }
} }
@ -308,20 +257,20 @@ func (picker *numberPickerData) handleCommand(self View, command PropertyName, d
// GetNumberPickerType returns the type of NumberPicker subview. Valid values: // GetNumberPickerType returns the type of NumberPicker subview. Valid values:
// NumberEditor (0) - NumberPicker is presented by editor (default type); // NumberEditor (0) - NumberPicker is presented by editor (default type);
// NumberSlider (1) - NumberPicker is presented by slider. // NumberSlider (1) - NumberPicker is presented by slider.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetNumberPickerType(view View, subviewID ...string) int { func GetNumberPickerType(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, NumberPickerType, NumberEditor, false) return enumStyledProperty(view, subviewID, NumberPickerType, NumberEditor, false)
} }
// GetNumberPickerMinMax returns the min and max value of NumberPicker subview. // GetNumberPickerMinMax returns the min and max value of NumberPicker subview.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetNumberPickerMinMax(view View, subviewID ...string) (float64, float64) { func GetNumberPickerMinMax(view View, subviewID ...string) (float64, float64) {
view = getSubview(view, subviewID) var pickerType int
pickerType := GetNumberPickerType(view) if len(subviewID) > 0 && subviewID[0] != "" {
pickerType = GetNumberPickerType(view, subviewID[0])
} else {
pickerType = GetNumberPickerType(view)
}
var defMin, defMax float64 var defMin, defMax float64
if pickerType == NumberSlider { if pickerType == NumberSlider {
@ -332,8 +281,8 @@ func GetNumberPickerMinMax(view View, subviewID ...string) (float64, float64) {
defMax = math.Inf(1) defMax = math.Inf(1)
} }
min := floatStyledProperty(view, nil, NumberPickerMin, defMin) min := floatStyledProperty(view, subviewID, NumberPickerMin, defMin)
max := floatStyledProperty(view, nil, NumberPickerMax, defMax) max := floatStyledProperty(view, subviewID, NumberPickerMax, defMax)
if min > max { if min > max {
return max, min return max, min
@ -342,14 +291,16 @@ func GetNumberPickerMinMax(view View, subviewID ...string) (float64, float64) {
} }
// GetNumberPickerStep returns the value changing step of NumberPicker subview. // GetNumberPickerStep returns the value changing step of NumberPicker subview.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetNumberPickerStep(view View, subviewID ...string) float64 { func GetNumberPickerStep(view View, subviewID ...string) float64 {
view = getSubview(view, subviewID) var max float64
_, max := GetNumberPickerMinMax(view) if len(subviewID) > 0 && subviewID[0] != "" {
_, max = GetNumberPickerMinMax(view, subviewID[0])
} else {
_, max = GetNumberPickerMinMax(view)
}
result := floatStyledProperty(view, nil, NumberPickerStep, 0) result := floatStyledProperty(view, subviewID, NumberPickerStep, 0)
if result > max { if result > max {
return max return max
} }
@ -357,37 +308,22 @@ func GetNumberPickerStep(view View, subviewID ...string) float64 {
} }
// GetNumberPickerValue returns the value of NumberPicker subview. // GetNumberPickerValue returns the value of NumberPicker subview.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetNumberPickerValue(view View, subviewID ...string) float64 { func GetNumberPickerValue(view View, subviewID ...string) float64 {
view = getSubview(view, subviewID) var min float64
min, _ := GetNumberPickerMinMax(view) if len(subviewID) > 0 && subviewID[0] != "" {
return floatStyledProperty(view, nil, NumberPickerValue, min) min, _ = GetNumberPickerMinMax(view, subviewID[0])
} else {
min, _ = GetNumberPickerMinMax(view)
}
result := floatStyledProperty(view, subviewID, NumberPickerValue, min)
return result
} }
// GetNumberChangedListeners returns the NumberChangedListener list of an NumberPicker subview. // GetNumberChangedListeners returns the NumberChangedListener list of an NumberPicker subview.
// If there are no listeners then the empty list is returned // If there are no listeners then the empty list is returned
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetNumberChangedListeners(view View, subviewID ...string) []func(NumberPicker, float64) {
// - func(rui.NumberPicker, float64, float64), return getEventListeners[NumberPicker, float64](view, subviewID, NumberChangedEvent)
// - func(rui.NumberPicker, float64),
// - func(rui.NumberPicker),
// - func(float64, float64),
// - func(float64),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetNumberChangedListeners(view View, subviewID ...string) []any {
return getTwoArgEventRawListeners[NumberPicker, float64](view, subviewID, NumberChangedEvent)
}
// GetNumberPickerPrecision returns the precision of displaying fractional part in editor of NumberPicker subview.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetNumberPickerPrecision(view View, subviewID ...string) int {
return intStyledProperty(view, subviewID, NumberPickerPrecision, 0)
} }

View File

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

View File

@ -1,34 +1,27 @@
package rui package rui
import ( import "sort"
"iter"
"slices"
)
// Params defines a type of a parameters list // Params defines a type of a parameters list
type Params map[PropertyName]any type Params map[string]any
// Get returns a value of the property with name defined by the argument. The type of return value depends func (params Params) Get(tag string) any {
// on the property. If the property is not set then nil is returned.
func (params Params) Get(tag PropertyName) any {
return params.getRaw(tag) return params.getRaw(tag)
} }
func (params Params) getRaw(tag PropertyName) any { func (params Params) getRaw(tag string) any {
if value, ok := params[tag]; ok { if value, ok := params[tag]; ok {
return value return value
} }
return nil return nil
} }
// Set sets the value (second argument) of the property with name defined by the first argument. func (params Params) Set(tag string, value any) bool {
// Return "true" if the value has been set, in the opposite case "false" is returned and a description of an error is written to the log
func (params Params) Set(tag PropertyName, value any) bool {
params.setRaw(tag, value) params.setRaw(tag, value)
return true return true
} }
func (params Params) setRaw(tag PropertyName, value any) { func (params Params) setRaw(tag string, value any) {
if value != nil { if value != nil {
params[tag] = value params[tag] = value
} else { } else {
@ -36,38 +29,21 @@ func (params Params) setRaw(tag PropertyName, value any) {
} }
} }
// Remove removes the property with name defined by the argument from a map. func (params Params) Remove(tag string) {
func (params Params) Remove(tag PropertyName) {
delete(params, tag) delete(params, tag)
} }
// Clear removes all properties from a map.
func (params Params) Clear() { func (params Params) Clear() {
for tag := range params { for tag := range params {
delete(params, tag) delete(params, tag)
} }
} }
func (params Params) All() iter.Seq2[PropertyName, any] { func (params Params) AllTags() []string {
return func(yield func(PropertyName, any) bool) { tags := make([]string, 0, len(params))
for tag, value := range params {
if !yield(tag, value) {
return
}
}
}
}
// AllTags returns a sorted slice of all properties.
func (params Params) AllTags() []PropertyName {
tags := make([]PropertyName, 0, len(params))
for t := range params { for t := range params {
tags = append(tags, t) tags = append(tags, t)
} }
slices.Sort(tags) sort.Strings(tags)
return tags return tags
} }
func (params Params) IsEmpty() bool {
return len(params) == 0
}

153
path.go
View File

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

View File

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

1766
popup.go

File diff suppressed because it is too large Load Diff

View File

@ -17,9 +17,7 @@ func ShowMessage(title, text string, session Session) {
} }
// ShowQuestion displays a message with the given title and text and two buttons "Yes" and "No". // 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 "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). // 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()) { func ShowQuestion(title, text string, session Session, onYes func(), onNo func()) {
textView := NewTextView(session, Params{ textView := NewTextView(session, Params{
@ -30,19 +28,8 @@ func ShowQuestion(title, text string, session Session, onYes func(), onNo func()
CloseButton: false, CloseButton: false,
OutsideClose: false, OutsideClose: false,
Buttons: []PopupButton{ Buttons: []PopupButton{
{
Title: "Yes",
Type: DefaultButton,
OnClick: func(popup Popup) {
popup.Dismiss()
if onYes != nil {
onYes()
}
},
},
{ {
Title: "No", Title: "No",
Type: CancelButton,
OnClick: func(popup Popup) { OnClick: func(popup Popup) {
popup.Dismiss() popup.Dismiss()
if onNo != nil { if onNo != nil {
@ -50,6 +37,15 @@ func ShowQuestion(title, text string, session Session, onYes func(), onNo func()
} }
}, },
}, },
{
Title: "Yes",
OnClick: func(popup Popup) {
popup.Dismiss()
if onYes != nil {
onYes()
}
},
},
}, },
} }
if title != "" { if title != "" {
@ -59,7 +55,6 @@ func ShowQuestion(title, text string, session Session, onYes func(), onNo func()
} }
// ShowCancellableQuestion displays a message with the given title and text and three buttons "Yes", "No" and "Cancel". // 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 // 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. // (if it is not nil) is called, respectively.
func ShowCancellableQuestion(title, text string, session Session, onYes func(), onNo func(), onCancel func()) { func ShowCancellableQuestion(title, text string, session Session, onYes func(), onNo func(), onCancel func()) {
@ -73,12 +68,11 @@ func ShowCancellableQuestion(title, text string, session Session, onYes func(),
OutsideClose: false, OutsideClose: false,
Buttons: []PopupButton{ Buttons: []PopupButton{
{ {
Title: "Yes", Title: "Cancel",
Type: DefaultButton,
OnClick: func(popup Popup) { OnClick: func(popup Popup) {
popup.Dismiss() popup.Dismiss()
if onYes != nil { if onCancel != nil {
onYes() onCancel()
} }
}, },
}, },
@ -92,12 +86,11 @@ func ShowCancellableQuestion(title, text string, session Session, onYes func(),
}, },
}, },
{ {
Title: "Cancel", Title: "Yes",
Type: CancelButton,
OnClick: func(popup Popup) { OnClick: func(popup Popup) {
popup.Dismiss() popup.Dismiss()
if onCancel != nil { if onYes != nil {
onCancel() onYes()
} }
}, },
}, },
@ -134,14 +127,10 @@ func (popup *popupMenuData) ListSize() int {
} }
func (popup *popupMenuData) ListItem(index int, session Session) View { func (popup *popupMenuData) ListItem(index int, session Session) View {
view := NewTextView(popup.session, Params{ return NewTextView(popup.session, Params{
Text: popup.items[index], Text: popup.items[index],
Style: "ruiPopupMenuItem", Style: "ruiPopupMenuItem",
}) })
if !popup.IsListItemEnabled(index) {
view.Set(TextColor, "@ruiDisabledTextColor")
}
return view
} }
func (popup *popupMenuData) IsListItemEnabled(index int) bool { func (popup *popupMenuData) IsListItemEnabled(index int) bool {
@ -155,12 +144,9 @@ func (popup *popupMenuData) IsListItemEnabled(index int) bool {
return true return true
} }
// PopupMenuResult is the constant for "popup-menu-result" property tag. // 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
// Used by `Popup`. // a menu item of popup menu is selected.
// Set the function to be called when the menu item of popup menu is selected.
//
// Supported types: `func(index int)`.
const PopupMenuResult = "popup-menu-result" const PopupMenuResult = "popup-menu-result"
// ShowMenu displays the menu. Menu items are set using the Items property. // ShowMenu displays the menu. Menu items are set using the Items property.

View File

@ -5,30 +5,12 @@ import (
"strings" "strings"
) )
// Constants for [ProgressBar] specific properties and events
const ( const (
// ProgressBarMax is the constant for "progress-max" property tag. ProgressBarMax = "progress-max"
// ProgressBarValue = "progress-value"
// Used by ProgressBar.
// Maximum value, default is 1.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
ProgressBarMax PropertyName = "progress-max"
// ProgressBarValue is the constant for "progress-value" property tag.
//
// Used by ProgressBar.
// Current value, default is 0.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
ProgressBarValue PropertyName = "progress-value"
) )
// ProgressBar represents a ProgressBar view // ProgressBar - ProgressBar view
type ProgressBar interface { type ProgressBar interface {
View View
} }
@ -46,18 +28,20 @@ func NewProgressBar(session Session, params Params) ProgressBar {
} }
func newProgressBar(session Session) View { func newProgressBar(session Session) View {
return new(progressBarData) return NewProgressBar(session, nil)
} }
func (progress *progressBarData) init(session Session) { func (progress *progressBarData) init(session Session) {
progress.viewData.init(session) progress.viewData.init(session)
progress.tag = "ProgressBar" progress.tag = "ProgressBar"
progress.normalize = normalizeProgressBarTag
progress.changed = progress.propertyChanged
} }
func normalizeProgressBarTag(tag PropertyName) PropertyName { func (progress *progressBarData) String() string {
tag = defaultNormalize(tag) return getViewString(progress)
}
func (progress *progressBarData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag { switch tag {
case Max, "progress-bar-max", "progressbar-max": case Max, "progress-bar-max", "progressbar-max":
return ProgressBarMax return ProgressBarMax
@ -68,20 +52,41 @@ func normalizeProgressBarTag(tag PropertyName) PropertyName {
return tag return tag
} }
func (progress *progressBarData) propertyChanged(tag PropertyName) { func (progress *progressBarData) Remove(tag string) {
progress.remove(progress.normalizeTag(tag))
}
func (progress *progressBarData) remove(tag string) {
progress.viewData.remove(tag)
progress.propertyChanged(tag)
}
func (progress *progressBarData) propertyChanged(tag string) {
if progress.created {
switch tag { switch tag {
case ProgressBarMax: case ProgressBarMax:
progress.Session().updateProperty(progress.htmlID(), "max", updateProperty(progress.htmlID(), Max, strconv.FormatFloat(GetProgressBarMax(progress), 'f', -1, 32), progress.session)
strconv.FormatFloat(GetProgressBarMax(progress), 'f', -1, 32))
case ProgressBarValue: case ProgressBarValue:
progress.Session().updateProperty(progress.htmlID(), "value", updateProperty(progress.htmlID(), Value, strconv.FormatFloat(GetProgressBarValue(progress), 'f', -1, 32), progress.session)
strconv.FormatFloat(GetProgressBarValue(progress), 'f', -1, 32))
default:
progress.viewData.propertyChanged(tag)
} }
}
}
func (progress *progressBarData) Set(tag string, value any) bool {
return progress.set(progress.normalizeTag(tag), value)
}
func (progress *progressBarData) set(tag string, value any) bool {
if progress.viewData.set(tag, value) {
progress.propertyChanged(tag)
return true
}
return false
}
func (progress *progressBarData) Get(tag string) any {
return progress.get(progress.normalizeTag(tag))
} }
func (progress *progressBarData) htmlTag() string { func (progress *progressBarData) htmlTag() string {
@ -101,17 +106,13 @@ func (progress *progressBarData) htmlProperties(self View, buffer *strings.Build
} }
// GetProgressBarMax returns the max value of ProgressBar subview. // GetProgressBarMax returns the max value of ProgressBar subview.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetProgressBarMax(view View, subviewID ...string) float64 { func GetProgressBarMax(view View, subviewID ...string) float64 {
return floatStyledProperty(view, subviewID, ProgressBarMax, 1) return floatStyledProperty(view, subviewID, ProgressBarMax, 1)
} }
// GetProgressBarValue returns the value of ProgressBar subview. // GetProgressBarValue returns the value of ProgressBar subview.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetProgressBarValue(view View, subviewID ...string) float64 { func GetProgressBarValue(view View, subviewID ...string) float64 {
return floatStyledProperty(view, subviewID, ProgressBarValue, 0) return floatStyledProperty(view, subviewID, ProgressBarValue, 0)
} }

View File

@ -1,8 +1,7 @@
package rui package rui
import ( import (
"iter" "sort"
"slices"
"strings" "strings"
) )
@ -10,205 +9,79 @@ import (
type Properties interface { type Properties interface {
// Get returns a value of the property with name defined by the argument. // Get returns a value of the property with name defined by the argument.
// The type of return value depends on the property. If the property is not set then nil is returned. // The type of return value depends on the property. If the property is not set then nil is returned.
Get(tag PropertyName) any Get(tag string) any
getRaw(tag PropertyName) any getRaw(tag string) any
// Set sets the value (second argument) of the property with name defined by the first argument. // Set sets the value (second argument) of the property with name defined by the first argument.
// Return "true" if the value has been set, in the opposite case "false" are returned and // Return "true" if the value has been set, in the opposite case "false" are returned and
// a description of the error is written to the log // a description of the error is written to the log
Set(tag PropertyName, value any) bool Set(tag string, value any) bool
setRaw(tag PropertyName, value any) setRaw(tag string, value any)
// Remove removes the property with name defined by the argument // Remove removes the property with name defined by the argument
Remove(tag PropertyName) Remove(tag string)
// Clear removes all properties // Clear removes all properties
Clear() Clear()
// All returns an iterator to access the properties
All() iter.Seq2[PropertyName, any]
// AllTags returns an array of the set properties // AllTags returns an array of the set properties
AllTags() []PropertyName AllTags() []string
IsEmpty() bool
} }
type propertyList struct { type propertyList struct {
properties map[PropertyName]any properties map[string]any
normalize func(PropertyName) PropertyName
}
type dataProperty struct {
propertyList
supportedProperties []PropertyName
get func(Properties, PropertyName) any
set func(Properties, PropertyName, any) []PropertyName
remove func(Properties, PropertyName) []PropertyName
}
func defaultNormalize(tag PropertyName) PropertyName {
return PropertyName(strings.ToLower(strings.Trim(string(tag), " \t")))
} }
func (properties *propertyList) init() { func (properties *propertyList) init() {
properties.properties = map[PropertyName]any{} properties.properties = map[string]any{}
properties.normalize = defaultNormalize
//properties.getFunc = properties.getRaw
//properties.set = propertiesSet
//properties.remove = propertiesRemove
} }
func (properties *propertyList) IsEmpty() bool { func (properties *propertyList) Get(tag string) any {
return len(properties.properties) == 0 return properties.getRaw(strings.ToLower(tag))
} }
func (properties *propertyList) getRaw(tag PropertyName) any { func (properties *propertyList) getRaw(tag string) any {
if value, ok := properties.properties[tag]; ok { if value, ok := properties.properties[tag]; ok {
return value return value
} }
return nil return nil
} }
func (properties *propertyList) setRaw(tag PropertyName, value any) { func (properties *propertyList) setRaw(tag string, value any) {
if value == nil {
delete(properties.properties, tag)
} else {
properties.properties[tag] = value properties.properties[tag] = value
}
} }
/* func (properties *propertyList) Remove(tag string) {
func (properties *propertyList) Remove(tag PropertyName) { delete(properties.properties, strings.ToLower(tag))
properties.remove(properties, properties.normalize(tag)) }
}
*/ func (properties *propertyList) remove(tag string) {
delete(properties.properties, tag)
}
func (properties *propertyList) Clear() { func (properties *propertyList) Clear() {
properties.properties = map[PropertyName]any{} properties.properties = map[string]any{}
} }
func (properties *propertyList) All() iter.Seq2[PropertyName, any] { func (properties *propertyList) AllTags() []string {
return func(yield func(PropertyName, any) bool) { tags := make([]string, 0, len(properties.properties))
for tag, value := range properties.properties { for t := range properties.properties {
if !yield(tag, value) { tags = append(tags, t)
return
} }
} sort.Strings(tags)
}
}
func (properties *propertyList) AllTags() []PropertyName {
tags := make([]PropertyName, 0, len(properties.properties))
for tag := range properties.properties {
tags = append(tags, tag)
}
slices.Sort(tags)
return tags return tags
} }
/*
func (properties *propertyList) writeToBuffer(buffer *strings.Builder,
indent string, objectTag string, tags []PropertyName) {
buffer.WriteString(objectTag)
buffer.WriteString(" {\n")
indent2 := indent + "\t"
for _, tag := range tags {
if value, ok := properties.properties[tag]; ok {
text := propertyValueToString(tag, value, indent2)
if text != "" {
buffer.WriteString(indent2)
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
buffer.WriteString(text)
buffer.WriteString(",\n")
}
}
}
buffer.WriteString(indent)
buffer.WriteString("}")
}
*/
func parseProperties(properties Properties, object DataObject) { func parseProperties(properties Properties, object DataObject) {
for node := range object.Properties() { count := object.PropertyCount()
for i := 0; i < count; i++ {
if node := object.Property(i); node != nil {
switch node.Type() { switch node.Type() {
case TextNode: case TextNode:
properties.Set(PropertyName(node.Tag()), node.Text()) properties.Set(node.Tag(), node.Text())
case ObjectNode: case ObjectNode:
properties.Set(PropertyName(node.Tag()), node.Object()) properties.Set(node.Tag(), node.Object())
case ArrayNode: case ArrayNode:
switch node.ArraySize() { properties.Set(node.Tag(), node.ArrayElements())
case 0:
// do nothing
case 1:
if v := node.ArrayElement(0); v.IsObject() {
properties.Set(PropertyName(node.Tag()), v.Object())
} else {
properties.Set(PropertyName(node.Tag()), v.Value())
}
default:
properties.Set(PropertyName(node.Tag()), node.Array())
} }
} }
} }
} }
func propertiesGet(properties Properties, tag PropertyName) any {
return properties.getRaw(tag)
}
func propertiesRemove(properties Properties, tag PropertyName) []PropertyName {
if properties.getRaw(tag) == nil {
return []PropertyName{}
}
properties.setRaw(tag, nil)
return []PropertyName{tag}
}
func (data *dataProperty) init() {
data.propertyList.init()
data.get = propertiesGet
data.set = propertiesSet
data.remove = propertiesRemove
}
func (data *dataProperty) Get(tag PropertyName) any {
return data.get(data, data.normalize(tag))
}
func (data *dataProperty) Remove(tag PropertyName) {
data.remove(data, data.normalize(tag))
}
func (data *dataProperty) writeToBuffer(buffer *strings.Builder, indent string, objectName string, tags []PropertyName) {
buffer.WriteString(objectName)
buffer.WriteString("{ ")
comma := false
for _, tag := range tags {
if value, ok := data.properties[tag]; ok {
text := propertyValueToString(tag, value, indent)
if text != "" {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
buffer.WriteString(text)
comma = true
}
}
}
buffer.WriteString(" }")
}
func (data *dataProperty) writeString(buffer *strings.Builder, indent string) {
data.writeToBuffer(buffer, indent, "_", data.AllTags())
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -2,12 +2,11 @@ package rui
import ( import (
"math" "math"
"slices"
"strconv" "strconv"
"strings" "strings"
) )
var colorProperties = []PropertyName{ var colorProperties = []string{
ColorTag, ColorTag,
BackgroundColor, BackgroundColor,
TextColor, TextColor,
@ -20,24 +19,25 @@ var colorProperties = []PropertyName{
OutlineColor, OutlineColor,
TextLineColor, TextLineColor,
ColorPickerValue, ColorPickerValue,
AccentColor,
} }
/* func isPropertyInList(tag string, list []string) bool {
func isPropertyInList(tag PropertyName, list []PropertyName) bool {
for _, prop := range list { for _, prop := range list {
if prop == tag { if prop == tag {
return true return true
} }
} }
return false return false
} }
*/
var angleProperties = []PropertyName{ var angleProperties = []string{
Rotate,
SkewX,
SkewY,
From, From,
} }
var boolProperties = []PropertyName{ var boolProperties = []string{
Disabled, Disabled,
Focusable, Focusable,
Inset, Inset,
@ -63,12 +63,9 @@ var boolProperties = []PropertyName{
TabCloseButton, TabCloseButton,
Repeating, Repeating,
UserSelect, UserSelect,
ColumnSpanAll,
MoveToFrontAnimation,
HideSummaryMarker,
} }
var intProperties = []PropertyName{ var intProperties = []string{
ZIndex, ZIndex,
TabSize, TabSize,
HeadHeight, HeadHeight,
@ -76,15 +73,16 @@ var intProperties = []PropertyName{
RowSpan, RowSpan,
ColumnSpan, ColumnSpan,
ColumnCount, ColumnCount,
Order,
TabIndex,
MaxLength,
NumberPickerPrecision,
} }
var floatProperties = map[PropertyName]struct{ min, max float64 }{ var floatProperties = map[string]struct{ min, max float64 }{
Opacity: {min: 0, max: 1}, Opacity: {min: 0, max: 1},
ShowOpacity: {min: 0, max: 1}, ScaleX: {min: -math.MaxFloat64, max: math.MaxFloat64},
ScaleY: {min: -math.MaxFloat64, max: math.MaxFloat64},
ScaleZ: {min: -math.MaxFloat64, max: math.MaxFloat64},
RotateX: {min: 0, max: 1},
RotateY: {min: 0, max: 1},
RotateZ: {min: 0, max: 1},
NumberPickerMax: {min: -math.MaxFloat64, max: math.MaxFloat64}, NumberPickerMax: {min: -math.MaxFloat64, max: math.MaxFloat64},
NumberPickerMin: {min: -math.MaxFloat64, max: math.MaxFloat64}, NumberPickerMin: {min: -math.MaxFloat64, max: math.MaxFloat64},
NumberPickerStep: {min: -math.MaxFloat64, max: math.MaxFloat64}, NumberPickerStep: {min: -math.MaxFloat64, max: math.MaxFloat64},
@ -93,94 +91,87 @@ var floatProperties = map[PropertyName]struct{ min, max float64 }{
ProgressBarValue: {min: 0, max: math.MaxFloat64}, ProgressBarValue: {min: 0, max: math.MaxFloat64},
VideoWidth: {min: 0, max: 10000}, VideoWidth: {min: 0, max: 10000},
VideoHeight: {min: 0, max: 10000}, VideoHeight: {min: 0, max: 10000},
PushDuration: {min: 0, max: math.MaxFloat64},
ShowDuration: {min: 0, max: math.MaxFloat64},
DragImageXOffset: {min: -math.MaxFloat64, max: math.MaxFloat64},
DragImageYOffset: {min: -math.MaxFloat64, max: math.MaxFloat64},
} }
var sizeProperties = map[PropertyName]string{ var sizeProperties = map[string]string{
Width: string(Width), Width: Width,
Height: string(Height), Height: Height,
MinWidth: string(MinWidth), MinWidth: MinWidth,
MinHeight: string(MinHeight), MinHeight: MinHeight,
MaxWidth: string(MaxWidth), MaxWidth: MaxWidth,
MaxHeight: string(MaxHeight), MaxHeight: MaxHeight,
Left: string(Left), Left: Left,
Right: string(Right), Right: Right,
Top: string(Top), Top: Top,
Bottom: string(Bottom), Bottom: Bottom,
TextSize: "font-size", TextSize: "font-size",
TextIndent: string(TextIndent), TextIndent: TextIndent,
LetterSpacing: string(LetterSpacing), LetterSpacing: LetterSpacing,
WordSpacing: string(WordSpacing), WordSpacing: WordSpacing,
LineHeight: string(LineHeight), LineHeight: LineHeight,
TextLineThickness: "text-decoration-thickness", TextLineThickness: "text-decoration-thickness",
ListRowGap: "row-gap", ListRowGap: "row-gap",
ListColumnGap: "column-gap", ListColumnGap: "column-gap",
GridRowGap: string(GridRowGap), GridRowGap: GridRowGap,
GridColumnGap: string(GridColumnGap), GridColumnGap: GridColumnGap,
ColumnWidth: string(ColumnWidth), ColumnWidth: ColumnWidth,
ColumnGap: string(ColumnGap), ColumnGap: ColumnGap,
Gap: string(Gap), Gap: Gap,
Margin: string(Margin), Margin: Margin,
MarginLeft: string(MarginLeft), MarginLeft: MarginLeft,
MarginRight: string(MarginRight), MarginRight: MarginRight,
MarginTop: string(MarginTop), MarginTop: MarginTop,
MarginBottom: string(MarginBottom), MarginBottom: MarginBottom,
Padding: string(Padding), Padding: Padding,
PaddingLeft: string(PaddingLeft), PaddingLeft: PaddingLeft,
PaddingRight: string(PaddingRight), PaddingRight: PaddingRight,
PaddingTop: string(PaddingTop), PaddingTop: PaddingTop,
PaddingBottom: string(PaddingBottom), PaddingBottom: PaddingBottom,
BorderWidth: string(BorderWidth), BorderWidth: BorderWidth,
BorderLeftWidth: string(BorderLeftWidth), BorderLeftWidth: BorderLeftWidth,
BorderRightWidth: string(BorderRightWidth), BorderRightWidth: BorderRightWidth,
BorderTopWidth: string(BorderTopWidth), BorderTopWidth: BorderTopWidth,
BorderBottomWidth: string(BorderBottomWidth), BorderBottomWidth: BorderBottomWidth,
OutlineWidth: string(OutlineWidth), OutlineWidth: OutlineWidth,
OutlineOffset: string(OutlineOffset), XOffset: XOffset,
XOffset: string(XOffset), YOffset: YOffset,
YOffset: string(YOffset), BlurRadius: BlurRadius,
BlurRadius: string(BlurRadius), SpreadRadius: SpreadRadius,
SpreadRadius: string(SpreadRadius), Perspective: Perspective,
Perspective: string(Perspective), PerspectiveOriginX: PerspectiveOriginX,
PerspectiveOriginX: string(PerspectiveOriginX), PerspectiveOriginY: PerspectiveOriginY,
PerspectiveOriginY: string(PerspectiveOriginY), OriginX: OriginX,
TransformOriginX: string(TransformOriginX), OriginY: OriginY,
TransformOriginY: string(TransformOriginY), OriginZ: OriginZ,
TransformOriginZ: string(TransformOriginZ), TranslateX: TranslateX,
Radius: string(Radius), TranslateY: TranslateY,
RadiusX: string(RadiusX), TranslateZ: TranslateZ,
RadiusY: string(RadiusY), Radius: Radius,
RadiusTopLeft: string(RadiusTopLeft), RadiusX: RadiusX,
RadiusTopLeftX: string(RadiusTopLeftX), RadiusY: RadiusY,
RadiusTopLeftY: string(RadiusTopLeftY), RadiusTopLeft: RadiusTopLeft,
RadiusTopRight: string(RadiusTopRight), RadiusTopLeftX: RadiusTopLeftX,
RadiusTopRightX: string(RadiusTopRightX), RadiusTopLeftY: RadiusTopLeftY,
RadiusTopRightY: string(RadiusTopRightY), RadiusTopRight: RadiusTopRight,
RadiusBottomLeft: string(RadiusBottomLeft), RadiusTopRightX: RadiusTopRightX,
RadiusBottomLeftX: string(RadiusBottomLeftX), RadiusTopRightY: RadiusTopRightY,
RadiusBottomLeftY: string(RadiusBottomLeftY), RadiusBottomLeft: RadiusBottomLeft,
RadiusBottomRight: string(RadiusBottomRight), RadiusBottomLeftX: RadiusBottomLeftX,
RadiusBottomRightX: string(RadiusBottomRightX), RadiusBottomLeftY: RadiusBottomLeftY,
RadiusBottomRightY: string(RadiusBottomRightY), RadiusBottomRight: RadiusBottomRight,
ItemWidth: string(ItemWidth), RadiusBottomRightX: RadiusBottomRightX,
ItemHeight: string(ItemHeight), RadiusBottomRightY: RadiusBottomRightY,
CenterX: string(CenterX), ItemWidth: ItemWidth,
CenterY: string(CenterX), ItemHeight: ItemHeight,
ArrowSize: "", CenterX: CenterX,
ArrowWidth: "", CenterY: CenterX,
ArrowOffset: "",
} }
type enumPropertyData struct { var enumProperties = map[string]struct {
values []string values []string
cssTag string cssTag string
cssValues []string cssValues []string
} }{
var enumProperties = map[PropertyName]enumPropertyData{
Semantics: { Semantics: {
[]string{"default", "article", "section", "aside", "header", "main", "footer", "navigation", "figure", "figure-caption", "button", "p", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "code"}, []string{"default", "article", "section", "aside", "header", "main", "footer", "navigation", "figure", "figure-caption", "button", "p", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "code"},
"", "",
@ -193,17 +184,17 @@ var enumProperties = map[PropertyName]enumPropertyData{
}, },
Overflow: { Overflow: {
[]string{"hidden", "visible", "scroll", "auto"}, []string{"hidden", "visible", "scroll", "auto"},
string(Overflow), Overflow,
[]string{"hidden", "visible", "scroll", "auto"}, []string{"hidden", "visible", "scroll", "auto"},
}, },
TextAlign: { TextAlign: {
[]string{"left", "right", "center", "justify"}, []string{"left", "right", "center", "justify"},
string(TextAlign), TextAlign,
[]string{"left", "right", "center", "justify"}, []string{"left", "right", "center", "justify"},
}, },
TextTransform: { TextTransform: {
[]string{"none", "capitalize", "lowercase", "uppercase"}, []string{"none", "capitalize", "lowercase", "uppercase"},
string(TextTransform), TextTransform,
[]string{"none", "capitalize", "lowercase", "uppercase"}, []string{"none", "capitalize", "lowercase", "uppercase"},
}, },
TextWeight: { TextWeight: {
@ -213,27 +204,22 @@ var enumProperties = map[PropertyName]enumPropertyData{
}, },
WhiteSpace: { WhiteSpace: {
[]string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"}, []string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"},
string(WhiteSpace), WhiteSpace,
[]string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"}, []string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"},
}, },
WordBreak: { WordBreak: {
[]string{"normal", "break-all", "keep-all", "break-word"}, []string{"normal", "break-all", "keep-all", "break-word"},
string(WordBreak), WordBreak,
[]string{"normal", "break-all", "keep-all", "break-word"}, []string{"normal", "break-all", "keep-all", "break-word"},
}, },
TextOverflow: { TextOverflow: {
[]string{"clip", "ellipsis"}, []string{"clip", "ellipsis"},
string(TextOverflow), TextOverflow,
[]string{"clip", "ellipsis"}, []string{"clip", "ellipsis"},
}, },
TextWrap: {
[]string{"wrap", "nowrap", "balance"},
string(TextWrap),
[]string{"wrap", "nowrap", "balance"},
},
WritingMode: { WritingMode: {
[]string{"horizontal-top-to-bottom", "horizontal-bottom-to-top", "vertical-right-to-left", "vertical-left-to-right"}, []string{"horizontal-top-to-bottom", "horizontal-bottom-to-top", "vertical-right-to-left", "vertical-left-to-right"},
string(WritingMode), WritingMode,
[]string{"horizontal-tb", "horizontal-bt", "vertical-rl", "vertical-lr"}, []string{"horizontal-tb", "horizontal-bt", "vertical-rl", "vertical-lr"},
}, },
TextDirection: { TextDirection: {
@ -253,7 +239,7 @@ var enumProperties = map[PropertyName]enumPropertyData{
}, },
BorderStyle: { BorderStyle: {
[]string{"none", "solid", "dashed", "dotted", "double"}, []string{"none", "solid", "dashed", "dotted", "double"},
string(BorderStyle), BorderStyle,
[]string{"none", "solid", "dashed", "dotted", "double"}, []string{"none", "solid", "dashed", "dotted", "double"},
}, },
TopStyle: { TopStyle: {
@ -278,7 +264,7 @@ var enumProperties = map[PropertyName]enumPropertyData{
}, },
OutlineStyle: { OutlineStyle: {
[]string{"none", "solid", "dashed", "dotted", "double"}, []string{"none", "solid", "dashed", "dotted", "double"},
string(OutlineStyle), OutlineStyle,
[]string{"none", "solid", "dashed", "dotted", "double"}, []string{"none", "solid", "dashed", "dotted", "double"},
}, },
Tabs: { Tabs: {
@ -341,19 +327,9 @@ var enumProperties = map[PropertyName]enumPropertyData{
"justify-items", "justify-items",
[]string{"start", "end", "center", "stretch"}, []string{"start", "end", "center", "stretch"},
}, },
CellVerticalSelfAlign: {
[]string{"top", "bottom", "center", "stretch"},
"align-self",
[]string{"start", "end", "center", "stretch"},
},
CellHorizontalSelfAlign: {
[]string{"left", "right", "center", "stretch"},
"justify-self",
[]string{"start", "end", "center", "stretch"},
},
GridAutoFlow: { GridAutoFlow: {
[]string{"row", "column", "row-dense", "column-dense"}, []string{"row", "column", "row-dense", "column-dense"},
string(GridAutoFlow), GridAutoFlow,
[]string{"row", "column", "row dense", "column dense"}, []string{"row", "column", "row dense", "column dense"},
}, },
ImageVerticalAlign: { ImageVerticalAlign: {
@ -393,7 +369,7 @@ var enumProperties = map[PropertyName]enumPropertyData{
}, },
Cursor: { Cursor: {
[]string{"auto", "default", "none", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", "text", "vertical-text", "alias", "copy", "move", "no-drop", "not-allowed", "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out", "grab", "grabbing"}, []string{"auto", "default", "none", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", "text", "vertical-text", "alias", "copy", "move", "no-drop", "not-allowed", "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out", "grab", "grabbing"},
string(Cursor), Cursor,
[]string{"auto", "default", "none", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", "text", "vertical-text", "alias", "copy", "move", "no-drop", "not-allowed", "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out", "grab", "grabbing"}, []string{"auto", "default", "none", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", "text", "vertical-text", "alias", "copy", "move", "no-drop", "not-allowed", "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out", "grab", "grabbing"},
}, },
Fit: { Fit: {
@ -421,21 +397,6 @@ var enumProperties = map[PropertyName]enumPropertyData{
"background-clip", "background-clip",
[]string{"border-box", "padding-box", "content-box"}, // "text"}, []string{"border-box", "padding-box", "content-box"}, // "text"},
}, },
BackgroundOrigin: {
[]string{"border-box", "padding-box", "content-box"},
"background-origin",
[]string{"border-box", "padding-box", "content-box"},
},
MaskClip: {
[]string{"border-box", "padding-box", "content-box"},
"mask-clip",
[]string{"border-box", "padding-box", "content-box"},
},
MaskOrigin: {
[]string{"border-box", "padding-box", "content-box"},
"background-origin",
[]string{"border-box", "padding-box", "content-box"},
},
Direction: { Direction: {
[]string{"to-top", "to-right-top", "to-right", "to-right-bottom", "to-bottom", "to-left-bottom", "to-left", "to-left-top"}, []string{"to-top", "to-right-top", "to-right", "to-right-bottom", "to-bottom", "to-left-bottom", "to-left", "to-left-top"},
"", "",
@ -486,39 +447,32 @@ var enumProperties = map[PropertyName]enumPropertyData{
"", "",
[]string{"none", "top", "right", "bottom", "left"}, []string{"none", "top", "right", "bottom", "left"},
}, },
MixBlendMode: {
[]string{"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"},
string(MixBlendMode),
[]string{"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"},
},
BackgroundBlendMode: {
[]string{"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"},
string(BackgroundBlendMode),
[]string{"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"},
},
ColumnFill: {
[]string{"balance", "auto"},
string(ColumnFill),
[]string{"balance", "auto"},
},
} }
func notCompatibleType(tag PropertyName, value any) { func notCompatibleType(tag string, value any) {
ErrorLogF(`"%T" type not compatible with "%s" property`, value, string(tag)) ErrorLogF(`"%T" type not compatible with "%s" property`, value, tag)
} }
func invalidPropertyValue(tag PropertyName, value any) { func invalidPropertyValue(tag string, value any) {
ErrorLogF(`Invalid value "%v" of "%s" property`, value, string(tag)) ErrorLogF(`Invalid value "%v" of "%s" property`, value, tag)
} }
func isConstantName(text string) (bool, string) { func isConstantName(text string) bool {
len := len(text) len := len(text)
if len <= 1 || text[0] != '@' || if len <= 1 || text[0] != '@' {
strings.ContainsAny(text, ",;|\"'`+(){}[]<>/\\*&%! \t\n\r") { return false
return false, ""
} }
return true, text[1:] if len > 2 {
last := len - 1
if (text[1] == '`' && text[last] == '`') ||
(text[1] == '"' && text[last] == '"') ||
(text[1] == '\'' && text[last] == '\'') {
return true
}
}
return !strings.ContainsAny(text, ",;|\"'`+(){}[]<>/\\*&%! \t\n\r")
} }
func isInt(value any) (int, bool) { func isInt(value any) (int, bool) {
@ -561,48 +515,26 @@ func isInt(value any) (int, bool) {
return n, true return n, true
} }
func setSimpleProperty(properties Properties, tag PropertyName, value any) bool { func (properties *propertyList) setSimpleProperty(tag string, value any) bool {
if value == nil { if value == nil {
properties.setRaw(tag, nil) delete(properties.properties, tag)
return true return true
} else if text, ok := value.(string); ok { } else if text, ok := value.(string); ok {
text = strings.Trim(text, " \t\n\r") text = strings.Trim(text, " \t\n\r")
if text == "" { if text == "" {
properties.setRaw(tag, nil) delete(properties.properties, tag)
return true return true
} }
if ok, _ := isConstantName(text); ok { if isConstantName(text) {
properties.setRaw(tag, text) properties.properties[tag] = text
return true return true
} }
} }
return false return false
} }
func setStringPropertyValue(properties Properties, tag PropertyName, text any) []PropertyName { func (properties *propertyList) setSizeProperty(tag string, value any) bool {
if text != "" { if !properties.setSimpleProperty(tag, value) {
properties.setRaw(tag, text)
} else if properties.getRaw(tag) != nil {
properties.setRaw(tag, nil)
} else {
return []PropertyName{}
}
return []PropertyName{tag}
}
func setArrayPropertyValue[T any](properties Properties, tag PropertyName, value []T) []PropertyName {
if len(value) > 0 {
properties.setRaw(tag, value)
} else if properties.getRaw(tag) != nil {
properties.setRaw(tag, nil)
} else {
return []PropertyName{}
}
return []PropertyName{tag}
}
func setSizeProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
var size SizeUnit var size SizeUnit
switch value := value.(type) { switch value := value.(type) {
case string: case string:
@ -612,7 +544,7 @@ func setSizeProperty(properties Properties, tag PropertyName, value any) []Prope
size.Function = fn size.Function = fn
} else if size, ok = StringToSizeUnit(value); !ok { } else if size, ok = StringToSizeUnit(value); !ok {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return nil return false
} }
case SizeUnit: case SizeUnit:
size = value size = value
@ -635,29 +567,29 @@ func setSizeProperty(properties Properties, tag PropertyName, value any) []Prope
size.Value = float64(n) size.Value = float64(n)
} else { } else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return nil return false
} }
} }
if size.Type == Auto { if size.Type == Auto {
properties.setRaw(tag, nil) delete(properties.properties, tag)
} else { } else {
properties.setRaw(tag, size) properties.properties[tag] = size
} }
} }
return []PropertyName{tag} return true
} }
func setAngleProperty(properties Properties, tag PropertyName, value any) []PropertyName { func (properties *propertyList) setAngleProperty(tag string, value any) bool {
if !setSimpleProperty(properties, tag, value) { if !properties.setSimpleProperty(tag, value) {
var angle AngleUnit var angle AngleUnit
switch value := value.(type) { switch value := value.(type) {
case string: case string:
var ok bool var ok bool
if angle, ok = StringToAngleUnit(value); !ok { if angle, ok = StringToAngleUnit(value); !ok {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return nil return false
} }
case AngleUnit: case AngleUnit:
angle = value angle = value
@ -673,24 +605,24 @@ func setAngleProperty(properties Properties, tag PropertyName, value any) []Prop
angle = Rad(float64(n)) angle = Rad(float64(n))
} else { } else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return nil return false
} }
} }
properties.setRaw(tag, angle) properties.properties[tag] = angle
} }
return []PropertyName{tag} return true
} }
func setColorProperty(properties Properties, tag PropertyName, value any) []PropertyName { func (properties *propertyList) setColorProperty(tag string, value any) bool {
if !setSimpleProperty(properties, tag, value) { if !properties.setSimpleProperty(tag, value) {
var result Color var result Color
switch value := value.(type) { switch value := value.(type) {
case string: case string:
var err error var err error
if result, err = stringToColor(value); err != nil { if result, err = stringToColor(value); err != nil {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return nil return false
} }
case Color: case Color:
result = value result = value
@ -700,101 +632,105 @@ func setColorProperty(properties Properties, tag PropertyName, value any) []Prop
result = Color(color) result = Color(color)
} else { } else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return nil return false
} }
} }
properties.setRaw(tag, result) if result == 0 {
delete(properties.properties, tag)
} else {
properties.properties[tag] = result
}
} }
return []PropertyName{tag} return true
} }
func setEnumProperty(properties Properties, tag PropertyName, value any, values []string) []PropertyName { func (properties *propertyList) setEnumProperty(tag string, value any, values []string) bool {
if !setSimpleProperty(properties, tag, value) { if !properties.setSimpleProperty(tag, value) {
var n int var n int
if text, ok := value.(string); ok { if text, ok := value.(string); ok {
if n, ok = enumStringToInt(text, values, false); !ok { if n, ok = enumStringToInt(text, values, false); !ok {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return nil return false
} }
} else if i, ok := isInt(value); ok { } else if i, ok := isInt(value); ok {
if i < 0 || i >= len(values) { if i < 0 || i >= len(values) {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return nil return false
} }
n = i n = i
} else { } else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return nil return false
} }
properties.setRaw(tag, n) properties.properties[tag] = n
} }
return []PropertyName{tag} return true
} }
func setBoolProperty(properties Properties, tag PropertyName, value any) []PropertyName { func (properties *propertyList) setBoolProperty(tag string, value any) bool {
if !setSimpleProperty(properties, tag, value) { if !properties.setSimpleProperty(tag, value) {
if text, ok := value.(string); ok { if text, ok := value.(string); ok {
switch strings.ToLower(strings.Trim(text, " \t")) { switch strings.ToLower(strings.Trim(text, " \t")) {
case "true", "yes", "on", "1": case "true", "yes", "on", "1":
properties.setRaw(tag, true) properties.properties[tag] = true
case "false", "no", "off", "0": case "false", "no", "off", "0":
properties.setRaw(tag, false) properties.properties[tag] = false
default: default:
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return nil return false
} }
} else if n, ok := isInt(value); ok { } else if n, ok := isInt(value); ok {
switch n { switch n {
case 1: case 1:
properties.setRaw(tag, true) properties.properties[tag] = true
case 0: case 0:
properties.setRaw(tag, false) properties.properties[tag] = false
default: default:
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return nil return false
} }
} else if b, ok := value.(bool); ok { } else if b, ok := value.(bool); ok {
properties.setRaw(tag, b) properties.properties[tag] = b
} else { } else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return nil return false
} }
} }
return []PropertyName{tag} return true
} }
func setIntProperty(properties Properties, tag PropertyName, value any) []PropertyName { func (properties *propertyList) setIntProperty(tag string, value any) bool {
if !setSimpleProperty(properties, tag, value) { if !properties.setSimpleProperty(tag, value) {
if text, ok := value.(string); ok { if text, ok := value.(string); ok {
n, err := strconv.Atoi(strings.Trim(text, " \t")) n, err := strconv.Atoi(strings.Trim(text, " \t"))
if err != nil { if err != nil {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
ErrorLog(err.Error()) ErrorLog(err.Error())
return nil return false
} }
properties.setRaw(tag, n) properties.properties[tag] = n
} else if n, ok := isInt(value); ok { } else if n, ok := isInt(value); ok {
properties.setRaw(tag, n) properties.properties[tag] = n
} else { } else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return nil return false
} }
} }
return []PropertyName{tag} return true
} }
func setFloatProperty(properties Properties, tag PropertyName, value any, min, max float64) []PropertyName { func (properties *propertyList) setFloatProperty(tag string, value any, min, max float64) bool {
if !setSimpleProperty(properties, tag, value) { if !properties.setSimpleProperty(tag, value) {
f := float64(0) f := float64(0)
switch value := value.(type) { switch value := value.(type) {
case string: case string:
@ -802,14 +738,14 @@ func setFloatProperty(properties Properties, tag PropertyName, value any, min, m
if f, err = strconv.ParseFloat(strings.Trim(value, " \t"), 64); err != nil { if f, err = strconv.ParseFloat(strings.Trim(value, " \t"), 64); err != nil {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
ErrorLog(err.Error()) ErrorLog(err.Error())
return nil return false
} }
if f < min || f > max { if f < min || f > max {
ErrorLogF(`"%T" out of range of "%s" property`, value, tag) ErrorLogF(`"%T" out of range of "%s" property`, value, tag)
return nil return false
} }
properties.setRaw(tag, value) properties.properties[tag] = value
return nil return true
case float32: case float32:
f = float64(value) f = float64(value)
@ -822,70 +758,64 @@ func setFloatProperty(properties Properties, tag PropertyName, value any, min, m
f = float64(n) f = float64(n)
} else { } else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return nil return false
} }
} }
if f >= min && f <= max { if f >= min && f <= max {
properties.setRaw(tag, f) properties.properties[tag] = f
} else { } else {
ErrorLogF(`"%T" out of range of "%s" property`, value, tag) ErrorLogF(`"%T" out of range of "%s" property`, value, tag)
return nil return false
} }
} }
return []PropertyName{tag} return true
} }
func propertiesSet(properties Properties, tag PropertyName, value any) []PropertyName { func (properties *propertyList) Set(tag string, value any) bool {
if _, ok := sizeProperties[tag]; ok { return properties.set(strings.ToLower(tag), value)
return setSizeProperty(properties, tag, value)
}
if valuesData, ok := enumProperties[tag]; ok {
return setEnumProperty(properties, tag, value, valuesData.values)
}
if limits, ok := floatProperties[tag]; ok {
return setFloatProperty(properties, tag, value, limits.min, limits.max)
}
if slices.Contains(colorProperties, tag) {
return setColorProperty(properties, tag, value)
}
if slices.Contains(angleProperties, tag) {
return setAngleProperty(properties, tag, value)
}
if slices.Contains(boolProperties, tag) {
return setBoolProperty(properties, tag, value)
}
if slices.Contains(intProperties, tag) {
return setIntProperty(properties, tag, value)
}
if text, ok := value.(string); ok {
properties.setRaw(tag, text)
return []PropertyName{tag}
}
notCompatibleType(tag, value)
return nil
} }
func (data *dataProperty) Set(tag PropertyName, value any) bool { func (properties *propertyList) set(tag string, value any) bool {
if value == nil { if value == nil {
data.Remove(tag) delete(properties.properties, tag)
return true return true
} }
tag = data.normalize(tag) if _, ok := sizeProperties[tag]; ok {
if slices.Contains(data.supportedProperties, tag) { return properties.setSizeProperty(tag, value)
return data.set(data, tag, value) != nil
} }
ErrorLogF(`"%s" property is not supported`, string(tag)) if valuesData, ok := enumProperties[tag]; ok {
return properties.setEnumProperty(tag, value, valuesData.values)
}
if limits, ok := floatProperties[tag]; ok {
return properties.setFloatProperty(tag, value, limits.min, limits.max)
}
if isPropertyInList(tag, colorProperties) {
return properties.setColorProperty(tag, value)
}
if isPropertyInList(tag, angleProperties) {
return properties.setAngleProperty(tag, value)
}
if isPropertyInList(tag, boolProperties) {
return properties.setBoolProperty(tag, value)
}
if isPropertyInList(tag, intProperties) {
return properties.setIntProperty(tag, value)
}
if text, ok := value.(string); ok {
properties.properties[tag] = text
return true
}
notCompatibleType(tag, value)
return false return false
} }

View File

@ -1,6 +1,5 @@
package rui package rui
// Constants for various specific properties of a views
const ( const (
// Visible - default value of the view Visibility property: View is visible // Visible - default value of the view Visibility property: View is visible
Visible = 0 Visible = 0
@ -149,9 +148,9 @@ const (
WhiteSpacePreLine = 4 WhiteSpacePreLine = 4
// WhiteSpaceBreakSpaces - the behavior is identical to that of WhiteSpacePreWrap, except that: // WhiteSpaceBreakSpaces - the behavior is identical to that of WhiteSpacePreWrap, except that:
// - Any sequence of preserved white space always takes up space, including at the end of the line. // * Any sequence of preserved white space always takes up space, including at the end of the line.
// - A line breaking opportunity exists after every preserved white space character, including between white space characters. // * A line breaking opportunity exists after every preserved white space character, including between white space characters.
// - Such preserved spaces take up space and do not hang, and thus affect the boxs intrinsic sizes (min-content size and max-content size). // * Such preserved spaces take up space and do not hang, and thus affect the boxs intrinsic sizes (min-content size and max-content size).
WhiteSpaceBreakSpaces = 5 WhiteSpaceBreakSpaces = 5
// WordBreakNormal - use the default line break rule. // WordBreakNormal - use the default line break rule.
@ -308,108 +307,4 @@ const (
// "dense" packing algorithm attempts to fill in holes earlier in the grid, if smaller items come up later. // "dense" packing algorithm attempts to fill in holes earlier in the grid, if smaller items come up later.
// This may cause views to appear out-of-order, when doing so would fill in holes left by larger views. // This may cause views to appear out-of-order, when doing so would fill in holes left by larger views.
ColumnDenseAutoFlow = 3 ColumnDenseAutoFlow = 3
// BlendNormal - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the top color, regardless of what the bottom color is.
// The effect is like two opaque pieces of paper overlapping.
BlendNormal = 0
// BlendMultiply - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of multiplying the top and bottom colors.
// A black layer leads to a black final layer, and a white layer leads to no change.
// The effect is like two images printed on transparent film overlapping.
BlendMultiply = 1
// BlendScreen - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of inverting the colors, multiplying them, and inverting that value.
// A black layer leads to no change, and a white layer leads to a white final layer.
// The effect is like two images shone onto a projection screen.
BlendScreen = 2
// BlendOverlay - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of multiply if the bottom color is darker, or screen if the bottom color is lighter.
// This blend mode is equivalent to hard-light but with the layers swapped.
BlendOverlay = 3
// BlendDarken - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is composed of the darkest values of each color channel.
BlendDarken = 4
// BlendLighten - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is composed of the lightest values of each color channel.
BlendLighten = 5
// BlendColorDodge - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of dividing the bottom color by the inverse of the top color.
// A black foreground leads to no change. A foreground with the inverse color of the backdrop leads to a fully lit color.
// This blend mode is similar to screen, but the foreground need only be as light as the inverse of the backdrop to create a fully lit color.
BlendColorDodge = 6
// BlendColorBurn - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of inverting the bottom color, dividing the value by the top color, and inverting that value.
// A white foreground leads to no change. A foreground with the inverse color of the backdrop leads to a black final image.
// This blend mode is similar to multiply, but the foreground need only be as dark as the inverse of the backdrop to make the final image black.
BlendColorBurn = 7
// BlendHardLight - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of multiply if the top color is darker, or screen if the top color is lighter.
// This blend mode is equivalent to overlay but with the layers swapped. The effect is similar to shining a harsh spotlight on the backdrop.
BlendHardLight = 8
// BlendSoftLight - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is similar to hard-light, but softer. This blend mode behaves similar to hard-light.
// The effect is similar to shining a diffused spotlight on the backdrop*.*
BlendSoftLight = 9
// BlendDifference - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of subtracting the darker of the two colors from the lighter one.
// A black layer has no effect, while a white layer inverts the other layer's color.
BlendDifference = 10
// BlendExclusion - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is similar to difference, but with less contrast.
// As with difference, a black layer has no effect, while a white layer inverts the other layer's color.
BlendExclusion = 11
// BlendHue - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color has the hue of the top color, while using the saturation and luminosity of the bottom color.
BlendHue = 12
// BlendSaturation - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color has the saturation of the top color, while using the hue and luminosity of the bottom color.
// A pure gray backdrop, having no saturation, will have no effect.
BlendSaturation = 13
// BlendColor - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color has the hue and saturation of the top color, while using the luminosity of the bottom color.
// The effect preserves gray levels and can be used to colorize the foreground.
BlendColor = 14
// BlendLuminosity - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color has the luminosity of the top color, while using the hue and saturation of the bottom color.
// This blend mode is equivalent to BlendColor, but with the layers swapped.
BlendLuminosity = 15
// ColumnFillBalance - value of the "column-fill" property: content is equally divided between columns.
ColumnFillBalance = 0
// ColumnFillAuto - value of the "column-fill" property:
// Columns are filled sequentially. Content takes up only the room it needs, possibly resulting in some columns remaining empty.
ColumnFillAuto = 1
// TextWrapOn - value of the "text-wrap" property:
// text is wrapped across lines at appropriate characters (for example spaces,
// in languages like English that use space separators) to minimize overflow.
TextWrapOn = 0
// TextWrapOff - value of the "text-wrap" property: text does not wrap across lines.
// It will overflow its containing element rather than breaking onto a new line.
TextWrapOff = 1
// TextWrapBalance - value of the "text-wrap" property: text is wrapped in a way
// that best balances the number of characters on each line, enhancing layout quality
// and legibility. Because counting characters and balancing them across multiple lines
// is computationally expensive, this value is only supported for blocks of text
// spanning a limited number of lines (six or less for Chromium and ten or less for Firefox).
TextWrapBalance = 2
) )

876
radius.go

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -1,52 +1,44 @@
package rui package rui
// ResizeEvent is the constant for "resize-event" property tag. // ResizeEvent is the constant for "resize-event" property tag.
// The "resize-event" is fired when the view changes its size.
// The main listener format:
// //
// Used by View. // func(View, Frame).
// Is fired when the view changes its size.
// //
// General listener format: // The additional listener formats:
// //
// func(view rui.View, frame rui.Frame) // func(Frame), func(View), and func().
// const ResizeEvent = "resize-event"
// where:
// - view - Interface of a view which generated this event,
// - frame - New offset and size of the view's visible area.
//
// Allowed listener formats:
//
// func(frame rui.Frame)
// func(view rui.View)
// func()
const ResizeEvent PropertyName = "resize-event"
func (view *viewData) onResize(self View, x, y, width, height float64) { func (view *viewData) onResize(self View, x, y, width, height float64) {
view.frame.Left = x view.frame.Left = x
view.frame.Top = y view.frame.Top = y
view.frame.Width = width view.frame.Width = width
view.frame.Height = height view.frame.Height = height
for _, listener := range getOneArgEventListeners[View, Frame](view, nil, ResizeEvent) { for _, listener := range GetResizeListeners(view) {
listener.Run(self, view.frame) listener(self, view.frame)
} }
} }
func (view *viewData) onItemResize(self View, index string, x, y, width, height float64) { func (view *viewData) onItemResize(self View, index string, x, y, width, height float64) {
} }
/* func (view *viewData) setFrameListener(tag string, value any) bool {
func setFrameListener(properties Properties, tag PropertyName, value any) bool { listeners, ok := valueToEventListeners[View, Frame](value)
if listeners, ok := valueToOneArgEventListeners[View, Frame](value); ok { if !ok {
if len(listeners) == 0 {
properties.setRaw(tag, nil)
} else {
properties.setRaw(tag, listeners)
}
return true
}
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return false
}
if listeners == nil {
delete(view.properties, tag)
} else {
view.properties[tag] = listeners
}
view.propertyChangedEvent(tag)
return true
} }
*/
func (view *viewData) setNoResizeEvent() { func (view *viewData) setNoResizeEvent() {
view.noResizeEvent = true view.noResizeEvent = true
@ -73,11 +65,11 @@ func (view *viewData) Frame() Frame {
} }
// GetViewFrame returns the size and location of view's viewport. // GetViewFrame returns the size and location of view's viewport.
// // If the second argument (subviewID) is not specified or it is "" then the value of the first argument (view) is returned
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetViewFrame(view View, subviewID ...string) Frame { func GetViewFrame(view View, subviewID ...string) Frame {
view = getSubview(view, subviewID) if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view == nil { if view == nil {
return Frame{} return Frame{}
} }
@ -85,16 +77,7 @@ func GetViewFrame(view View, subviewID ...string) Frame {
} }
// GetResizeListeners returns the list of "resize-event" listeners. If there are no listeners then the empty list is returned // GetResizeListeners returns the list of "resize-event" listeners. If there are no listeners then the empty list is returned
// // If the second argument (subviewID) is not specified or it is "" then the listeners list of the first argument (view) is returned
// Result elements can be of the following types: func GetResizeListeners(view View, subviewID ...string) []func(View, Frame) {
// - func(rui.View, rui.Frame), return getEventListeners[View, Frame](view, subviewID, ResizeEvent)
// - func(rui.View),
// - func(rui.Frame),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetResizeListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, Frame](view, subviewID, ResizeEvent)
} }

View File

@ -3,7 +3,6 @@ package rui
import ( import (
"embed" "embed"
"io" "io"
"io/fs"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -16,7 +15,6 @@ const (
imageDir = "images" imageDir = "images"
themeDir = "themes" themeDir = "themes"
viewDir = "views" viewDir = "views"
popupDir = "popups"
rawDir = "raw" rawDir = "raw"
stringsDir = "strings" stringsDir = "strings"
) )
@ -46,20 +44,19 @@ var resources = resourceManager{
imageSrcSets: map[string][]scaledImage{}, imageSrcSets: map[string][]scaledImage{},
} }
// AddEmbedResources adds embedded resources to the list of application resources
func AddEmbedResources(fs *embed.FS) { func AddEmbedResources(fs *embed.FS) {
resources.embedFS = append(resources.embedFS, fs) resources.embedFS = append(resources.embedFS, fs)
rootDirs := resources.embedRootDirs(fs) rootDirs := embedRootDirs(fs)
for _, dir := range rootDirs { for _, dir := range rootDirs {
switch dir { switch dir {
case imageDir: case imageDir:
resources.scanEmbedImagesDir(fs, dir, "") scanEmbedImagesDir(fs, dir, "")
case themeDir: case themeDir:
resources.scanEmbedThemesDir(fs, dir) scanEmbedThemesDir(fs, dir)
case stringsDir: case stringsDir:
resources.scanEmbedStringsDir(fs, dir) scanEmbedStringsDir(fs, dir)
case viewDir, rawDir: case viewDir, rawDir:
// do nothing // do nothing
@ -70,13 +67,13 @@ func AddEmbedResources(fs *embed.FS) {
if file.IsDir() { if file.IsDir() {
switch file.Name() { switch file.Name() {
case imageDir: case imageDir:
resources.scanEmbedImagesDir(fs, dir+"/"+imageDir, "") scanEmbedImagesDir(fs, dir+"/"+imageDir, "")
case themeDir: case themeDir:
resources.scanEmbedThemesDir(fs, dir+"/"+themeDir) scanEmbedThemesDir(fs, dir+"/"+themeDir)
case stringsDir: case stringsDir:
resources.scanEmbedStringsDir(fs, dir+"/"+stringsDir) scanEmbedStringsDir(fs, dir+"/"+stringsDir)
case viewDir, rawDir: case viewDir, rawDir:
// do nothing // do nothing
@ -88,7 +85,7 @@ func AddEmbedResources(fs *embed.FS) {
} }
} }
func (resources *resourceManager) embedRootDirs(fs *embed.FS) []string { func embedRootDirs(fs *embed.FS) []string {
result := []string{} result := []string{}
if files, err := fs.ReadDir("."); err == nil { if files, err := fs.ReadDir("."); err == nil {
for _, file := range files { for _, file := range files {
@ -100,34 +97,34 @@ func (resources *resourceManager) embedRootDirs(fs *embed.FS) []string {
return result return result
} }
func (resources *resourceManager) scanEmbedThemesDir(fs *embed.FS, dir string) { func scanEmbedThemesDir(fs *embed.FS, dir string) {
if files, err := fs.ReadDir(dir); err == nil { if files, err := fs.ReadDir(dir); err == nil {
for _, file := range files { for _, file := range files {
name := file.Name() name := file.Name()
path := dir + "/" + name path := dir + "/" + name
if file.IsDir() { if file.IsDir() {
resources.scanEmbedThemesDir(fs, path) scanEmbedThemesDir(fs, path)
} else if strings.ToLower(filepath.Ext(name)) == ".rui" { } else if strings.ToLower(filepath.Ext(name)) == ".rui" {
if data, err := fs.ReadFile(path); err == nil { if data, err := fs.ReadFile(path); err == nil {
resources.registerThemeText(string(data)) registerThemeText(string(data))
} }
} }
} }
} }
} }
func (resources *resourceManager) scanEmbedImagesDir(fs *embed.FS, dir, prefix string) { func scanEmbedImagesDir(fs *embed.FS, dir, prefix string) {
if files, err := fs.ReadDir(dir); err == nil { if files, err := fs.ReadDir(dir); err == nil {
for _, file := range files { for _, file := range files {
name := file.Name() name := file.Name()
path := dir + "/" + name path := dir + "/" + name
if file.IsDir() { if file.IsDir() {
resources.scanEmbedImagesDir(fs, path, prefix+name+"/") scanEmbedImagesDir(fs, path, prefix+name+"/")
} else { } else {
ext := strings.ToLower(filepath.Ext(name)) ext := strings.ToLower(filepath.Ext(name))
switch ext { switch ext {
case ".png", ".jpg", ".jpeg", ".svg", ".gif", ".bmp", ".webp": case ".png", ".jpg", ".jpeg", ".svg":
resources.registerImage(fs, path, prefix+name) registerImage(fs, path, prefix+name)
} }
} }
} }
@ -139,7 +136,7 @@ func invalidImageFileFormat(filename string) {
`". Image file name format: name[@x-param].ext (examples: icon.png, icon@1.5x.png)`) `". Image file name format: name[@x-param].ext (examples: icon.png, icon@1.5x.png)`)
} }
func (resources *resourceManager) registerImage(fs *embed.FS, path, filename string) { func registerImage(fs *embed.FS, path, filename string) {
resources.images[filename] = imagePath{fs: fs, path: path} resources.images[filename] = imagePath{fs: fs, path: path}
start := strings.LastIndex(filename, "@") start := strings.LastIndex(filename, "@")
@ -172,16 +169,16 @@ func (resources *resourceManager) registerImage(fs *embed.FS, path, filename str
} }
} }
func (resources *resourceManager) scanImagesDirectory(path, filePrefix string) { func scanImagesDirectory(path, filePrefix string) {
if files, err := os.ReadDir(path); err == nil { if files, err := os.ReadDir(path); err == nil {
for _, file := range files { for _, file := range files {
filename := file.Name() filename := file.Name()
if filename[0] != '.' { if filename[0] != '.' {
newPath := path + `/` + filename newPath := path + `/` + filename
if !file.IsDir() { if !file.IsDir() {
resources.registerImage(nil, newPath, filePrefix+filename) registerImage(nil, newPath, filePrefix+filename)
} else { } else {
resources.scanImagesDirectory(newPath, filePrefix+filename+"/") scanImagesDirectory(newPath, filePrefix+filename+"/")
} }
} }
} }
@ -190,17 +187,17 @@ func (resources *resourceManager) scanImagesDirectory(path, filePrefix string) {
} }
} }
func (resources *resourceManager) scanThemesDir(path string) { func scanThemesDir(path string) {
if files, err := os.ReadDir(path); err == nil { if files, err := os.ReadDir(path); err == nil {
for _, file := range files { for _, file := range files {
filename := file.Name() filename := file.Name()
if filename[0] != '.' { if filename[0] != '.' {
newPath := path + `/` + filename newPath := path + `/` + filename
if file.IsDir() { if file.IsDir() {
resources.scanThemesDir(newPath) scanThemesDir(newPath)
} else if strings.ToLower(filepath.Ext(newPath)) == ".rui" { } else if strings.ToLower(filepath.Ext(newPath)) == ".rui" {
if data, err := os.ReadFile(newPath); err == nil { if data, err := os.ReadFile(newPath); err == nil {
resources.registerThemeText(string(data)) registerThemeText(string(data))
} else { } else {
ErrorLog(err.Error()) ErrorLog(err.Error())
} }
@ -220,21 +217,12 @@ func SetResourcePath(path string) {
resources.path += "/" resources.path += "/"
} }
resources.scanImagesDirectory(resources.path+imageDir, "") scanImagesDirectory(resources.path+imageDir, "")
resources.scanThemesDir(resources.path + themeDir) scanThemesDir(resources.path + themeDir)
resources.scanStringsDir(resources.path + stringsDir) scanStringsDir(resources.path + stringsDir)
} }
func (resources *resourceManager) scanDefaultResourcePath() { func registerThemeText(text string) bool {
if exe, err := os.Executable(); err == nil {
path := filepath.Dir(exe) + "/resources/"
resources.scanImagesDirectory(path+imageDir, "")
resources.scanThemesDir(path + themeDir)
resources.scanStringsDir(path + stringsDir)
}
}
func (resources *resourceManager) registerThemeText(text string) bool {
theme, ok := CreateThemeFromText(text) theme, ok := CreateThemeFromText(text)
if !ok { if !ok {
return false return false
@ -280,12 +268,12 @@ func serveResourceFile(filename string, w http.ResponseWriter, r *http.Request)
if serveEmbed(fs, filename) { if serveEmbed(fs, filename) {
return true return true
} }
for _, dir := range resources.embedRootDirs(fs) { for _, dir := range embedRootDirs(fs) {
if serveEmbed(fs, dir+"/"+filename) { if serveEmbed(fs, dir+"/"+filename) {
return true return true
} }
if subDirs, err := fs.ReadDir(dir); err == nil { if subdirs, err := fs.ReadDir(dir); err == nil {
for _, subdir := range subDirs { for _, subdir := range subdirs {
if subdir.IsDir() { if subdir.IsDir() {
if serveEmbed(fs, dir+"/"+subdir.Name()+"/"+filename) { if serveEmbed(fs, dir+"/"+subdir.Name()+"/"+filename) {
return true return true
@ -326,13 +314,12 @@ func serveResourceFile(filename string, w http.ResponseWriter, r *http.Request)
return false return false
} }
// ReadRawResource returns the contents of the raw resource with the specified name
func ReadRawResource(filename string) []byte { func ReadRawResource(filename string) []byte {
for _, fs := range resources.embedFS { for _, fs := range resources.embedFS {
rootDirs := resources.embedRootDirs(fs) rootDirs := embedRootDirs(fs)
for _, dir := range rootDirs { for _, dir := range rootDirs {
switch dir { switch dir {
case imageDir, themeDir, viewDir, stringsDir: case imageDir, themeDir, viewDir:
// do nothing // do nothing
case rawDir: case rawDir:
@ -364,50 +351,11 @@ func ReadRawResource(filename string) []byte {
return nil return nil
} }
// OpenRawResource returns the contents of the raw resource with the specified name
func OpenRawResource(filename string) fs.File {
for _, fs := range resources.embedFS {
rootDirs := resources.embedRootDirs(fs)
for _, dir := range rootDirs {
switch dir {
case imageDir, themeDir, viewDir, stringsDir:
// do nothing
case rawDir:
if file, err := fs.Open(dir + "/" + filename); err == nil {
return file
}
default:
if file, err := fs.Open(dir + "/" + rawDir + "/" + filename); err == nil {
return file
}
}
}
}
if resources.path != "" {
if file, err := os.Open(resources.path + rawDir + "/" + filename); err == nil {
return file
}
}
if exe, err := os.Executable(); err == nil {
if file, err := os.Open(filepath.Dir(exe) + "/resources/" + rawDir + "/" + filename); err == nil {
return file
}
}
ErrorLogF(`The "%s" raw file don't found`, filename)
return nil
}
// AllRawResources returns the list of all raw resources
func AllRawResources() []string { func AllRawResources() []string {
result := []string{} result := []string{}
for _, fs := range resources.embedFS { for _, fs := range resources.embedFS {
rootDirs := resources.embedRootDirs(fs) rootDirs := embedRootDirs(fs)
for _, dir := range rootDirs { for _, dir := range rootDirs {
switch dir { switch dir {
case imageDir, themeDir, viewDir: case imageDir, themeDir, viewDir:
@ -449,7 +397,6 @@ func AllRawResources() []string {
return result return result
} }
// AllImageResources returns the list of all image resources
func AllImageResources() []string { func AllImageResources() []string {
result := make([]string, 0, len(resources.images)) result := make([]string, 0, len(resources.images))
for image := range resources.images { for image := range resources.images {
@ -459,7 +406,6 @@ func AllImageResources() []string {
return result return result
} }
// AddTheme adds theme to application
func AddTheme(theme Theme) { func AddTheme(theme Theme) {
if theme != nil { if theme != nil {
name := theme.Name() name := theme.Name()

View File

@ -59,7 +59,7 @@ func (writer *ruiWriterData) writeString(str string) {
{old: "\"", new: `\"`}, {old: "\"", new: `\"`},
} }
for _, s := range replace { for _, s := range replace {
str = strings.ReplaceAll(str, s.old, s.new) str = strings.Replace(str, s.old, s.new, -1)
} }
writer.buffer.WriteRune('"') writer.buffer.WriteRune('"')
writer.buffer.WriteString(str) writer.buffer.WriteString(str)

View File

@ -1,32 +1,25 @@
package rui package rui
import "fmt"
// ScrollEvent is the constant for "scroll-event" property tag. // ScrollEvent is the constant for "scroll-event" property tag.
// The "scroll-event" is fired when the content of the view is scrolled.
// The main listener format:
// //
// Used by View. // func(View, Frame).
// Is fired when the content of the view is scrolled.
// //
// General listener format: // The additional listener formats:
// //
// func(view rui.View, frame rui.Frame) // func(Frame), func(View), and func().
// const ScrollEvent = "scroll-event"
// where:
// - view - Interface of a view which generated this event,
// - frame - New offset and size of the view's visible area.
//
// Allowed listener formats:
//
// func(frame rui.Frame)
// func(view rui.View)
// func()
const ScrollEvent PropertyName = "scroll-event"
func (view *viewData) onScroll(self View, x, y, width, height float64) { func (view *viewData) onScroll(self View, x, y, width, height float64) {
view.scroll.Left = x view.scroll.Left = x
view.scroll.Top = y view.scroll.Top = y
view.scroll.Width = width view.scroll.Width = width
view.scroll.Height = height view.scroll.Height = height
for _, listener := range getOneArgEventListeners[View, Frame](view, nil, ScrollEvent) { for _, listener := range GetScrollListeners(view) {
listener.Run(self, view.scroll) listener(self, view.scroll)
} }
} }
@ -42,11 +35,11 @@ func (view *viewData) setScroll(x, y, width, height float64) {
} }
// GetViewScroll returns ... // GetViewScroll returns ...
// // If the second argument (subviewID) is not specified or it is "" then a value of the first argument (view) is returned
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetViewScroll(view View, subviewID ...string) Frame { func GetViewScroll(view View, subviewID ...string) Frame {
view = getSubview(view, subviewID) if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view == nil { if view == nil {
return Frame{} return Frame{}
} }
@ -54,49 +47,40 @@ func GetViewScroll(view View, subviewID ...string) Frame {
} }
// GetScrollListeners returns the list of "scroll-event" listeners. If there are no listeners then the empty list is returned // GetScrollListeners returns the list of "scroll-event" listeners. If there are no listeners then the empty list is returned
// // If the second argument (subviewID) is not specified or it is "" then the listeners list of the first argument (view) is returned
// Result elements can be of the following types: func GetScrollListeners(view View, subviewID ...string) []func(View, Frame) {
// - func(rui.View, rui.Frame), return getEventListeners[View, Frame](view, subviewID, ResizeEvent)
// - func(rui.View),
// - func(rui.Frame),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetScrollListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, Frame](view, subviewID, ScrollEvent)
} }
// ScrollTo scrolls the view's content to the given position. // ScrollTo scrolls the view's content to the given position.
// // If the second argument (subviewID) is "" then the first argument (view) is used
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func ScrollViewTo(view View, subviewID string, x, y float64) { func ScrollViewTo(view View, subviewID string, x, y float64) {
if subviewID != "" { if subviewID != "" {
view = ViewByID(view, subviewID) view = ViewByID(view, subviewID)
} }
if view != nil { if view != nil {
view.Session().callFunc("scrollTo", view.htmlID(), x, y) view.Session().runScript(fmt.Sprintf(`scrollTo("%s", %g, %g)`, view.htmlID(), x, y))
} }
} }
// ScrollViewToEnd scrolls the view's content to the start of view. // ScrollViewToEnd scrolls the view's content to the start of view.
// // If the second argument (subviewID) is not specified or it is "" then the first argument (view) is used
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func ScrollViewToStart(view View, subviewID ...string) { func ScrollViewToStart(view View, subviewID ...string) {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
view.Session().callFunc("scrollToStart", view.htmlID()) view = ViewByID(view, subviewID[0])
}
if view != nil {
view.Session().runScript(`scrollToStart("` + view.htmlID() + `")`)
} }
} }
// ScrollViewToEnd scrolls the view's content to the end of view. // ScrollViewToEnd scrolls the view's content to the end of view.
// // If the second argument (subviewID) is not specified or it is "" then the first argument (view) is used
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func ScrollViewToEnd(view View, subviewID ...string) { func ScrollViewToEnd(view View, subviewID ...string) {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
view.Session().callFunc("scrollToEnd", view.htmlID()) view = ViewByID(view, subviewID[0])
}
if view != nil {
view.Session().runScript(`scrollToEnd("` + view.htmlID() + `")`)
} }
} }

View File

@ -7,37 +7,8 @@ import (
"strings" "strings"
) )
type bridge interface {
startUpdateScript(htmlID string) bool
finishUpdateScript(htmlID string)
callFunc(funcName string, args ...any) bool
updateInnerHTML(htmlID, html string)
appendToInnerHTML(htmlID, html string)
updateCSSProperty(htmlID, property, value string)
updateProperty(htmlID, property string, value any)
removeProperty(htmlID, property string)
sendResponse()
setAnimationCSS(css string)
appendAnimationCSS(css string)
canvasStart(htmlID string)
callCanvasFunc(funcName string, args ...any)
callCanvasVarFunc(v any, funcName string, args ...any)
callCanvasImageFunc(url string, property string, funcName string, args ...any)
createCanvasVar(funcName string, args ...any) any
createPath2D(arg string) any
updateCanvasProperty(property string, value any)
canvasFinish()
canvasTextMetrics(htmlID, font, text string) TextMetrics
htmlPropertyValue(htmlID, name string) string
answerReceived(answer DataObject)
close()
remoteAddr() string
}
// SessionContent is the interface of a session content // SessionContent is the interface of a session content
type SessionContent interface { type SessionContent interface {
// CreateRootView will be called by the library to create a root view of the application
CreateRootView(session Session) View CreateRootView(session Session) View
} }
@ -78,7 +49,7 @@ type Session interface {
// Content returns the SessionContent of session // Content returns the SessionContent of session
Content() SessionContent Content() SessionContent
setContent(content SessionContent) bool setContent(content SessionContent, self Session) bool
// SetTitle sets the text of the browser title/tab // SetTitle sets the text of the browser title/tab
SetTitle(title string) SetTitle(title string)
@ -89,11 +60,11 @@ type Session interface {
RootView() View RootView() View
// Get returns a value of the view (with id defined by the first argument) property with name defined by the second argument. // Get returns a value of the view (with id defined by the first argument) property with name defined by the second argument.
// The type of return value depends on the property. If the property is not set then nil is returned. // The type of return value depends on the property. If the property is not set then nil is returned.
Get(viewID string, tag PropertyName) any Get(viewID, tag string) any
// Set sets the value (third argument) of the property (second argument) of the view with id defined by the first argument. // Set sets the value (third argument) of the property (second argument) of the view with id defined by the first argument.
// Return "true" if the value has been set, in the opposite case "false" are returned and // Return "true" if the value has been set, in the opposite case "false" are returned and
// a description of the error is written to the log // a description of the error is written to the log
Set(viewID string, tag PropertyName, value any) bool Set(viewID, tag string, value any) bool
// DownloadFile downloads (saves) on the client side the file located at the specified path on the server. // DownloadFile downloads (saves) on the client side the file located at the specified path on the server.
DownloadFile(path string) DownloadFile(path string)
@ -102,70 +73,26 @@ type Session interface {
// OpenURL opens the url in the new browser tab // OpenURL opens the url in the new browser tab
OpenURL(url string) OpenURL(url string)
// ClientItem reads value by key from the client-side storage
ClientItem(key string) (string, bool)
// SetClientItem stores a key-value pair in the client-side storage
SetClientItem(key, value string)
// RemoveClientItem removes a key-value pair in the client-side storage
RemoveClientItem(key string)
// RemoveAllClientItems removes all key-value pair from the client-side storage
RemoveAllClientItems()
// SetHotKey sets the function that will be called when the given hotkey is pressed.
// Invoke SetHotKey(..., ..., nil) for remove hotkey function.
SetHotKey(keyCode KeyCode, controlKeys ControlKeyMask, fn func(Session))
// StartTimer starts a timer on the client side.
// The first argument specifies the timer period in milliseconds.
// The second argument specifies a function that will be called on each timer event.
// The result is the id of the timer, which is used to stop the timer
StartTimer(ms int, timerFunc func(Session)) int
// StopTimer the timer with the given id
StopTimer(timerID int)
getCurrentTheme() Theme
registerAnimation(props []AnimatedProperty) string registerAnimation(props []AnimatedProperty) string
resolveConstants(value string) (string, bool) resolveConstants(value string) (string, bool)
checkboxOffImage(accentColor Color) string checkboxOffImage() string
checkboxOnImage(accentColor Color) string checkboxOnImage() string
radiobuttonOffImage() string radiobuttonOffImage() string
radiobuttonOnImage(accentColor Color) string radiobuttonOnImage() string
viewByHTMLID(id string) View viewByHTMLID(id string) View
nextViewID() string nextViewID() string
styleProperty(styleTag string, propertyTag PropertyName) any styleProperty(styleTag, property string) any
setBridge(events chan DataObject, bridge bridge) setBrige(events chan DataObject, brige WebBrige)
writeInitScript(writer *strings.Builder) writeInitScript(writer *strings.Builder)
callFunc(funcName string, args ...any) runScript(script string)
updateInnerHTML(htmlID, html string) runGetterScript(script string) DataObject //, answer chan DataObject)
appendToInnerHTML(htmlID, html string) handleAnswer(data DataObject)
updateCSSProperty(htmlID, property, value string)
updateProperty(htmlID, property string, value any)
removeProperty(htmlID, property string)
startUpdateScript(htmlID string) bool
finishUpdateScript(htmlID string)
sendResponse()
addAnimationCSS(css string)
removeAnimation(keyframe string)
htmlPropertyValue(htmlID, name string) string
canvasStart(htmlID string) bool
callCanvasFunc(funcName string, args ...any)
createCanvasVar(funcName string, args ...any) any
createPath(arg string) any
callCanvasVarFunc(v any, funcName string, args ...any)
callCanvasImageFunc(url string, property string, funcName string, args ...any)
updateCanvasProperty(property string, value any)
canvasFinish()
canvasTextMetrics(htmlID, font, text string) TextMetrics
addToEventsQueue(data DataObject)
handleAnswer(command string, data DataObject) bool
handleRootSize(data DataObject) handleRootSize(data DataObject)
handleResize(data DataObject) handleResize(data DataObject)
handleEvent(command string, data DataObject) handleViewEvent(command string, data DataObject)
close() close()
onStart() onStart()
@ -180,6 +107,10 @@ type Session interface {
popupManager() *popupManager popupManager() *popupManager
imageManager() *imageManager imageManager() *imageManager
startUpdateScript(htmlID string)
updateScript(htmlID string) *strings.Builder
finishUpdateScript(htmlID string)
} }
type sessionData struct { type sessionData struct {
@ -206,16 +137,11 @@ type sessionData struct {
ignoreUpdates bool ignoreUpdates bool
popups *popupManager popups *popupManager
images *imageManager images *imageManager
bridge bridge brige WebBrige
events chan DataObject events chan DataObject
animationCounter int animationCounter int
animationCSS string animationCSS string
updateScripts map[string]*strings.Builder updateScripts map[string]*strings.Builder
clientStorage map[string]string
hotkeys map[string]func(Session)
timers map[int]func(Session)
nextTimerID int
pauseTime int64
} }
func newSession(app Application, id int, customTheme string, params DataObject) Session { func newSession(app Application, id int, customTheme string, params DataObject) Session {
@ -232,10 +158,6 @@ func newSession(app Application, id int, customTheme string, params DataObject)
session.animationCounter = 0 session.animationCounter = 0
session.animationCSS = "" session.animationCSS = ""
session.updateScripts = map[string]*strings.Builder{} session.updateScripts = map[string]*strings.Builder{}
session.clientStorage = map[string]string{}
session.hotkeys = map[string]func(Session){}
session.timers = map[int]func(Session){}
session.nextTimerID = 1
if customTheme != "" { if customTheme != "" {
if theme, ok := CreateThemeFromText(customTheme); ok { if theme, ok := CreateThemeFromText(customTheme); ok {
@ -244,8 +166,38 @@ func newSession(app Application, id int, customTheme string, params DataObject)
} }
} }
if params != nil { if value, ok := params.PropertyValue("touch"); ok {
session.handleSessionInfo(params) 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, ",")
}
if value, ok := params.PropertyValue("dark"); ok {
session.darkTheme = (value == "1" || value == "true")
}
if value, ok := params.PropertyValue("pixel-ratio"); ok {
if f, err := strconv.ParseFloat(value, 64); err != nil {
ErrorLog(err.Error())
} else {
session.pixelRatio = f
}
} }
return session return session
@ -259,19 +211,18 @@ func (session *sessionData) ID() int {
return session.sessionID return session.sessionID
} }
func (session *sessionData) setBridge(events chan DataObject, bridge bridge) { func (session *sessionData) setBrige(events chan DataObject, brige WebBrige) {
session.events = events session.events = events
session.bridge = bridge session.brige = brige
} }
func (session *sessionData) close() { func (session *sessionData) close() {
if session.events != nil { if session.events != nil {
obj, _ := ParseDataText(`session-close{session="` + strconv.Itoa(session.sessionID) + `"}`) session.events <- ParseDataText(`session-close{session="` + strconv.Itoa(session.sessionID) + `"}`)
session.events <- obj
} }
} }
func (session *sessionData) styleProperty(styleTag string, propertyTag PropertyName) any { func (session *sessionData) styleProperty(styleTag, propertyTag string) any {
if style := session.getCurrentTheme().style(styleTag); style != nil { if style := session.getCurrentTheme().style(styleTag); style != nil {
return style.getRaw(propertyTag) return style.getRaw(propertyTag)
} }
@ -301,10 +252,10 @@ func (session *sessionData) Content() SessionContent {
return session.content return session.content
} }
func (session *sessionData) setContent(content SessionContent) bool { func (session *sessionData) setContent(content SessionContent, self Session) bool {
if content != nil { if content != nil {
session.content = content session.content = content
session.rootView = content.CreateRootView(session) session.rootView = content.CreateRootView(self)
if session.rootView != nil { if session.rootView != nil {
session.rootView.setParentID("ruiRootView") session.rootView.setParentID("ruiRootView")
return true return true
@ -328,63 +279,47 @@ func (session *sessionData) writeInitScript(writer *strings.Builder) {
if session.rootView != nil { if session.rootView != nil {
writer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`) writer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`)
buffer := allocStringBuilder() viewHTML(session.rootView, writer)
defer freeStringBuilder(buffer)
viewHTML(session.rootView, buffer, "")
text := strings.ReplaceAll(buffer.String(), "'", `\'`)
writer.WriteString(text)
writer.WriteString("';\nscanElementsSize();") writer.WriteString("';\nscanElementsSize();")
} }
session.updateTooltipConstants()
}
func (session *sessionData) updateTooltipConstants() {
if color, ok := session.Color("ruiTooltipBackground"); ok {
session.bridge.callFunc("setCssVar", "--tooltip-background", color.cssString())
}
if color, ok := session.Color("ruiTooltipTextColor"); ok {
session.bridge.callFunc("setCssVar", "--tooltip-text-color", color.cssString())
}
if color, ok := session.Color("ruiTooltipShadowColor"); ok {
session.bridge.callFunc("setCssVar", "--tooltip-shadow-color", color.cssString())
}
} }
func (session *sessionData) reload() { func (session *sessionData) reload() {
css := appStyles + session.getCurrentTheme().cssText(session) + session.animationCSS
session.bridge.callFunc("setStyles", css)
if session.rootView != nil {
buffer := allocStringBuilder() buffer := allocStringBuilder()
defer freeStringBuilder(buffer) defer freeStringBuilder(buffer)
viewHTML(session.rootView, buffer, "") css := appStyles + session.getCurrentTheme().cssText(session) + session.animationCSS
session.bridge.updateInnerHTML("ruiRootView", buffer.String()) css = strings.ReplaceAll(css, "\n", `\n`)
session.bridge.callFunc("scanElementsSize") css = strings.ReplaceAll(css, "\t", `\t`)
buffer.WriteString(`document.querySelector('style').textContent = "`)
buffer.WriteString(css)
buffer.WriteString("\";\n")
if session.rootView != nil {
buffer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`)
viewHTML(session.rootView, buffer)
buffer.WriteString("';\nscanElementsSize();")
} }
session.updateTooltipConstants() session.runScript(buffer.String())
} }
func (session *sessionData) ignoreViewUpdates() bool { func (session *sessionData) ignoreViewUpdates() bool {
return session.bridge == nil || session.ignoreUpdates return session.brige == nil || session.ignoreUpdates
} }
func (session *sessionData) setIgnoreViewUpdates(ignore bool) { func (session *sessionData) setIgnoreViewUpdates(ignore bool) {
session.ignoreUpdates = ignore session.ignoreUpdates = ignore
} }
func (session *sessionData) Get(viewID string, tag PropertyName) any { func (session *sessionData) Get(viewID, tag string) any {
if view := ViewByID(session.RootView(), viewID); view != nil { if view := ViewByID(session.RootView(), viewID); view != nil {
return view.Get(tag) return view.Get(tag)
} }
return nil return nil
} }
func (session *sessionData) Set(viewID string, tag PropertyName, value any) bool { func (session *sessionData) Set(viewID, tag string, value any) bool {
if view := ViewByID(session.RootView(), viewID); view != nil { if view := ViewByID(session.RootView(), viewID); view != nil {
return view.Set(tag, value) return view.Set(tag, value)
} }
@ -407,231 +342,27 @@ func (session *sessionData) imageManager() *imageManager {
return session.images return session.images
} }
func (session *sessionData) callFunc(funcName string, args ...any) { func (session *sessionData) runScript(script string) {
if session.bridge != nil { if session.brige != nil {
session.bridge.callFunc(funcName, args...) session.brige.WriteMessage(script)
} else { } else {
ErrorLog("No connection") ErrorLog("No connection")
} }
} }
func (session *sessionData) updateInnerHTML(htmlID, html string) { func (session *sessionData) runGetterScript(script string) DataObject { //}, answer chan DataObject) {
if !session.ignoreViewUpdates() { if session.brige != nil {
if session.bridge != nil { return session.brige.RunGetterScript(script)
session.bridge.updateInnerHTML(htmlID, html)
} else {
ErrorLog("No connection")
}
}
}
func (session *sessionData) appendToInnerHTML(htmlID, html string) {
if !session.ignoreViewUpdates() {
if session.bridge != nil {
session.bridge.appendToInnerHTML(htmlID, html)
} else {
ErrorLog("No connection")
}
}
}
func (session *sessionData) updateCSSProperty(htmlID, property, value string) {
if !session.ignoreViewUpdates() {
if session.bridge != nil {
session.bridge.updateCSSProperty(htmlID, property, value)
} else {
ErrorLog("No connection")
}
}
}
func (session *sessionData) updateProperty(htmlID, property string, value any) {
if !session.ignoreViewUpdates() {
if session.bridge != nil {
session.bridge.updateProperty(htmlID, property, value)
} else {
ErrorLog("No connection")
}
}
}
func (session *sessionData) removeProperty(htmlID, property string) {
if !session.ignoreViewUpdates() {
if session.bridge != nil {
session.bridge.removeProperty(htmlID, property)
} else {
ErrorLog("No connection")
}
}
}
func (session *sessionData) startUpdateScript(htmlID string) bool {
if session.bridge != nil {
return session.bridge.startUpdateScript(htmlID)
}
ErrorLog("No connection")
return false
}
func (session *sessionData) finishUpdateScript(htmlID string) {
if session.bridge != nil {
session.bridge.finishUpdateScript(htmlID)
} else {
ErrorLog("No connection")
}
}
func (session *sessionData) sendResponse() {
if session.bridge != nil {
session.bridge.sendResponse()
} else {
ErrorLog("No connection")
}
}
func (session *sessionData) addAnimationCSS(css string) {
session.animationCSS += css
if session.bridge != nil {
session.bridge.appendAnimationCSS(css)
} else {
ErrorLog("No connection")
}
}
func (session *sessionData) removeAnimation(keyframe string) {
css := session.animationCSS
index := strings.Index(css, "@keyframes "+keyframe)
if index < 0 {
return
}
start := strings.IndexRune(css[index:], '{')
if start < 0 {
return
}
n := 1
end := -1
for i := start + index + 1; i < len(css); i++ {
if css[i] == '}' {
n--
if n == 0 {
end = i + 1
if end < len(css) && css[end] == '\n' {
end++
}
break
}
} else if css[i] == '{' {
n++
}
}
if end > index {
session.animationCSS = strings.Trim(css[:index]+css[end:], "\n")
if session.bridge != nil {
session.bridge.setAnimationCSS(session.animationCSS)
} else {
ErrorLog("No connection")
}
}
}
func (session *sessionData) canvasStart(htmlID string) bool {
if session.bridge != nil {
session.bridge.canvasStart(htmlID)
return true
} }
ErrorLog("No connection") ErrorLog("No connection")
return false result := NewDataObject("error")
result.SetPropertyValue("text", "No connection")
return result
} }
func (session *sessionData) callCanvasFunc(funcName string, args ...any) { func (session *sessionData) handleAnswer(data DataObject) {
if session.bridge != nil { session.brige.AnswerReceived(data)
session.bridge.callCanvasFunc(funcName, args...)
}
}
func (session *sessionData) updateCanvasProperty(property string, value any) {
if session.bridge != nil {
session.bridge.updateCanvasProperty(property, value)
}
}
func (session *sessionData) createCanvasVar(funcName string, args ...any) any {
if session.bridge != nil {
return session.bridge.createCanvasVar(funcName, args...)
}
return nil
}
func (session *sessionData) createPath(arg string) any {
if session.bridge != nil {
return session.bridge.createPath2D(arg)
}
return nil
}
func (session *sessionData) callCanvasVarFunc(v any, funcName string, args ...any) {
if session.bridge != nil && v != nil {
session.bridge.callCanvasVarFunc(v, funcName, args...)
}
}
func (session *sessionData) callCanvasImageFunc(url string, property string, funcName string, args ...any) {
if session.bridge != nil {
session.bridge.callCanvasImageFunc(url, property, funcName, args...)
}
}
func (session *sessionData) canvasFinish() {
if session.bridge != nil {
session.bridge.canvasFinish()
}
}
func (session *sessionData) canvasTextMetrics(htmlID, font, text string) TextMetrics {
if session.bridge != nil {
return session.bridge.canvasTextMetrics(htmlID, font, text)
}
ErrorLog("No connection")
return TextMetrics{Width: 0}
}
func (session *sessionData) htmlPropertyValue(htmlID, name string) string {
if session.bridge != nil {
return session.bridge.htmlPropertyValue(htmlID, name)
}
ErrorLog("No connection")
return ""
}
func (session *sessionData) handleAnswer(command string, data DataObject) bool {
switch command {
case "answer":
if session.bridge != nil {
session.bridge.answerReceived(data)
}
case "imageLoaded":
session.imageManager().imageLoaded(data)
case "imageError":
session.imageManager().imageLoadError(data)
default:
return false
}
if session.bridge != nil {
session.bridge.sendResponse()
} else {
ErrorLog("No connection")
}
return true
} }
func (session *sessionData) handleRootSize(data DataObject) { func (session *sessionData) handleRootSize(data DataObject) {
@ -657,8 +388,8 @@ func (session *sessionData) handleRootSize(data DataObject) {
} }
func (session *sessionData) handleResize(data DataObject) { func (session *sessionData) handleResize(data DataObject) {
if node := data.PropertyByTag("views"); node != nil && node.Type() == ArrayNode { if node := data.PropertyWithTag("views"); node != nil && node.Type() == ArrayNode {
for el := range node.ArrayElements() { for _, el := range node.ArrayElements() {
if el.IsObject() { if el.IsObject() {
obj := el.Object() obj := el.Object()
getFloat := func(tag string) float64 { getFloat := func(tag string) float64 {
@ -698,181 +429,27 @@ func (session *sessionData) handleResize(data DataObject) {
} }
} }
func (session *sessionData) handleSessionInfo(params DataObject) { func (session *sessionData) handleViewEvent(command string, data DataObject) {
if value, ok := params.PropertyValue("touch"); ok {
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, ",")
}
if value, ok := params.PropertyValue("dark"); ok {
session.darkTheme = (value == "1" || value == "true")
}
if value, ok := params.PropertyValue("pixel-ratio"); ok {
if f, err := strconv.ParseFloat(value, 64); err != nil {
ErrorLog(err.Error())
} else {
session.pixelRatio = f
}
}
if node := params.PropertyByTag("storage"); node != nil && node.Type() == ObjectNode {
if obj := node.Object(); obj != nil {
for element := range obj.Properties() {
if element.Type() == TextNode {
session.clientStorage[element.Tag()] = element.Text()
}
}
}
}
}
func (session *sessionData) handleEvent(command string, data DataObject) {
switch command {
case "session-pause":
session.onPause()
case "session-resume":
session.onResume()
case "timer":
if text, ok := data.PropertyValue("timerID"); ok {
timerID, err := strconv.Atoi(text)
if err == nil {
if fn, ok := session.timers[timerID]; ok {
fn(session)
} else {
ErrorLog(`Timer (id = ` + text + `) not exists`)
}
} else {
ErrorLog(err.Error())
}
} else {
ErrorLog(`"timerID" property not found`)
}
case "root-size":
session.handleRootSize(data)
case "resize":
session.handleResize(data)
case "sessionInfo":
session.handleSessionInfo(data)
case "storageError":
if text, ok := data.PropertyValue("error"); ok {
ErrorLog(text)
}
default:
if viewID, ok := data.PropertyValue("id"); ok { if viewID, ok := data.PropertyValue("id"); ok {
if viewID != "body" {
if view := session.viewByHTMLID(viewID); view != nil { if view := session.viewByHTMLID(viewID); view != nil {
view.handleCommand(view, PropertyName(command), data) view.handleCommand(view, command, data)
} }
} } else if command != "clickOutsidePopup" {
if command == string(KeyDownEvent) {
var event KeyEvent
event.init(data)
session.hotKey(event)
}
} else {
ErrorLog(`"id" property not found. Event: ` + command) ErrorLog(`"id" property not found. Event: ` + command)
} }
}
session.bridge.sendResponse()
}
func (session *sessionData) hotKey(event KeyEvent) {
popups := session.popupManager().popups
if count := len(popups); count > 0 {
if popups[count-1].keyEvent(event) {
return
}
}
var controlKeys ControlKeyMask = 0
if event.AltKey {
controlKeys |= AltKey
}
if event.CtrlKey {
controlKeys |= CtrlKey
}
if event.MetaKey {
controlKeys |= MetaKey
}
if event.ShiftKey {
controlKeys |= ShiftKey
}
key := hotkeyCode(KeyCode(event.Code), controlKeys)
if fn, ok := session.hotkeys[key]; ok && fn != nil {
fn(session)
}
}
func hotkeyCode(keyCode KeyCode, controlKeys ControlKeyMask) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(strings.ToLower(string(keyCode)))
if controlKeys != 0 {
buffer.WriteRune('-')
if controlKeys&AltKey != 0 {
buffer.WriteRune('a')
}
if controlKeys&CtrlKey != 0 {
buffer.WriteRune('c')
}
if controlKeys&MetaKey != 0 {
buffer.WriteRune('m')
}
if controlKeys&ShiftKey != 0 {
buffer.WriteRune('s')
}
}
return buffer.String()
}
func (session *sessionData) SetHotKey(keyCode KeyCode, controlKeys ControlKeyMask, fn func(Session)) {
hotkey := hotkeyCode(keyCode, controlKeys)
if fn == nil {
delete(session.hotkeys, hotkey)
} else {
session.hotkeys[hotkey] = fn
}
} }
func (session *sessionData) SetTitle(title string) { func (session *sessionData) SetTitle(title string) {
title, _ = session.GetString(title) title, _ = session.GetString(title)
session.callFunc("setTitle", title) session.runScript(`document.title = "` + title + `";`)
} }
func (session *sessionData) SetTitleColor(color Color) { func (session *sessionData) SetTitleColor(color Color) {
session.callFunc("setTitleColor", color.cssString()) session.runScript(`setTitleColor("` + color.cssString() + `");`)
} }
func (session *sessionData) RemoteAddr() string { func (session *sessionData) RemoteAddr() string {
return session.bridge.remoteAddr() return session.brige.remoteAddr()
} }
func (session *sessionData) OpenURL(urlStr string) { func (session *sessionData) OpenURL(urlStr string) {
@ -880,47 +457,5 @@ func (session *sessionData) OpenURL(urlStr string) {
ErrorLog(err.Error()) ErrorLog(err.Error())
return return
} }
session.callFunc("openURL", urlStr) session.runScript(`window.open("` + urlStr + `", "_blank");`)
}
func (session *sessionData) ClientItem(key string) (string, bool) {
value, ok := session.clientStorage[key]
return value, ok
}
func (session *sessionData) SetClientItem(key, value string) {
session.clientStorage[key] = value
session.bridge.callFunc("localStorageSet", key, value)
}
func (session *sessionData) RemoveClientItem(key string) {
delete(session.clientStorage, key)
session.bridge.callFunc("localStorageRemove", key)
}
func (session *sessionData) RemoveAllClientItems() {
session.clientStorage = map[string]string{}
session.bridge.callFunc("localStorageClear")
}
func (session *sessionData) addToEventsQueue(data DataObject) {
session.events <- data
}
func (session *sessionData) StartTimer(ms int, timerFunc func(Session)) int {
timerID := 0
if session.bridge != nil {
timerID = session.nextTimerID
session.nextTimerID++
session.timers[timerID] = timerFunc
session.bridge.callFunc("startTimer", ms, timerID)
}
return timerID
}
func (session *sessionData) StopTimer(timerID int) {
if session.bridge != nil {
session.bridge.callFunc("stopTimer", timerID)
delete(session.timers, timerID)
}
} }

View File

@ -1,45 +1,32 @@
package rui package rui
import "time"
// SessionStartListener is the listener interface of a session start event // SessionStartListener is the listener interface of a session start event
type SessionStartListener interface { type SessionStartListener interface {
// OnStart is a function that is called by the library after the creation of the root view of the application
OnStart(session Session) OnStart(session Session)
} }
// SessionFinishListener is the listener interface of a session start event // SessionFinishListener is the listener interface of a session start event
type SessionFinishListener interface { type SessionFinishListener interface {
// OnFinish is a function that is called by the library when the user closes the application page in the browser
OnFinish(session Session) OnFinish(session Session)
} }
// SessionResumeListener is the listener interface of a session resume event // SessionResumeListener is the listener interface of a session resume event
type SessionResumeListener interface { type SessionResumeListener interface {
// OnResume is a function that is called by the library when the application page in the client's browser becomes
// active and is also called immediately after OnStart
OnResume(session Session) OnResume(session Session)
} }
// SessionPauseListener is the listener interface of a session pause event // SessionPauseListener is the listener interface of a session pause event
type SessionPauseListener interface { type SessionPauseListener interface {
// OnPause is a function that is called by the library when the application page in the client's browser becomes
// inactive and is also called when the user switches to a different browser tab/window, minimizes the browser,
// or switches to another application
OnPause(session Session) OnPause(session Session)
} }
// SessionPauseListener is the listener interface of a session disconnect event // SessionPauseListener is the listener interface of a session disconnect event
type SessionDisconnectListener interface { type SessionDisconnectListener interface {
// OnDisconnect is a function that is called by the library if the server loses connection with the client and
// this happens when the connection is broken
OnDisconnect(session Session) OnDisconnect(session Session)
} }
// SessionPauseListener is the listener interface of a session reconnect event // SessionPauseListener is the listener interface of a session reconnect event
type SessionReconnectListener interface { type SessionReconnectListener interface {
// OnReconnect is a function that is called by the library after the server reconnects with the client
// and this happens when the connection is restored
OnReconnect(session Session) OnReconnect(session Session)
} }
@ -63,25 +50,13 @@ func (session *sessionData) onFinish() {
func (session *sessionData) onPause() { func (session *sessionData) onPause() {
if session.content != nil { if session.content != nil {
session.pauseTime = time.Now().Unix()
if listener, ok := session.content.(SessionPauseListener); ok { if listener, ok := session.content.(SessionPauseListener); ok {
listener.OnPause(session) listener.OnPause(session)
} }
if timeout := session.app.Params().SocketAutoClose; timeout > 0 {
go session.autoClose(session.pauseTime, timeout)
}
}
}
func (session *sessionData) autoClose(start int64, timeout int) {
time.Sleep(time.Second * time.Duration(timeout))
if session.pauseTime == start {
session.bridge.callFunc("closeSocket")
} }
} }
func (session *sessionData) onResume() { func (session *sessionData) onResume() {
session.pauseTime = 0
if session.content != nil { if session.content != nil {
if listener, ok := session.content.(SessionResumeListener); ok { if listener, ok := session.content.(SessionResumeListener); ok {
listener.OnResume(session) listener.OnResume(session)

View File

@ -35,9 +35,11 @@ func (session *sessionData) constant(tag string, prevTags []string) (string, boo
return result, true return result, true
} }
if strings.ContainsAny(result, ", :;|/") { for _, separator := range []string{",", " ", ":", ";", "|", "/"} {
if strings.Contains(result, separator) {
return session.resolveConstantsNext(result, tags) return session.resolveConstantsNext(result, tags)
} }
}
if result[0] != '@' { if result[0] != '@' {
return result, true return result, true
@ -59,7 +61,7 @@ func (session *sessionData) resolveConstants(value string) (string, bool) {
} }
func (session *sessionData) resolveConstantsNext(value string, prevTags []string) (string, bool) { func (session *sessionData) resolveConstantsNext(value string, prevTags []string) (string, bool) {
if !strings.ContainsRune(value, '@') { if !strings.Contains(value, "@") {
return value, true return value, true
} }
@ -201,7 +203,7 @@ func (session *sessionData) SetCustomTheme(name string) bool {
const checkImage = `<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m4 8 3 4 5-8" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"/></svg>` const checkImage = `<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m4 8 3 4 5-8" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"/></svg>`
func (session *sessionData) checkboxImage(checked bool, accentColor Color) string { func (session *sessionData) checkboxImage(checked bool) string {
var borderColor, backgroundColor Color var borderColor, backgroundColor Color
var ok bool var ok bool
@ -215,9 +217,7 @@ func (session *sessionData) checkboxImage(checked bool, accentColor Color) strin
} }
if checked { if checked {
if accentColor != 0 { if backgroundColor, ok = session.Color("ruiHighlightColor"); !ok {
backgroundColor = accentColor
} else if backgroundColor, ok = session.Color("ruiHighlightColor"); !ok {
backgroundColor = 0xFF1A74E8 backgroundColor = 0xFF1A74E8
} }
} else if backgroundColor, ok = session.Color("ruiBackgroundColor"); !ok { } else if backgroundColor, ok = session.Color("ruiBackgroundColor"); !ok {
@ -244,22 +244,16 @@ func (session *sessionData) checkboxImage(checked bool, accentColor Color) strin
return buffer.String() return buffer.String()
} }
func (session *sessionData) checkboxOffImage(accentColor Color) string { func (session *sessionData) checkboxOffImage() string {
if accentColor != 0 {
return session.checkboxImage(false, accentColor)
}
if session.checkboxOff == "" { if session.checkboxOff == "" {
session.checkboxOff = session.checkboxImage(false, accentColor) session.checkboxOff = session.checkboxImage(false)
} }
return session.checkboxOff return session.checkboxOff
} }
func (session *sessionData) checkboxOnImage(accentColor Color) string { func (session *sessionData) checkboxOnImage() string {
if accentColor != 0 {
return session.checkboxImage(true, accentColor)
}
if session.checkboxOn == "" { if session.checkboxOn == "" {
session.checkboxOn = session.checkboxImage(true, accentColor) session.checkboxOn = session.checkboxImage(true)
} }
return session.checkboxOn return session.checkboxOn
} }
@ -291,14 +285,12 @@ func (session *sessionData) radiobuttonOffImage() string {
return session.radiobuttonOff return session.radiobuttonOff
} }
func (session *sessionData) radiobuttonOnImage(accentColor Color) string { func (session *sessionData) radiobuttonOnImage() string {
if session.radiobuttonOn == "" { if session.radiobuttonOn == "" {
var borderColor, backgroundColor Color var borderColor, backgroundColor Color
var ok bool var ok bool
if accentColor != 0 { if borderColor, ok = session.Color("ruiHighlightColor"); !ok {
borderColor = accentColor
} else if borderColor, ok = session.Color("ruiHighlightColor"); !ok {
borderColor = 0xFF1A74E8 borderColor = 0xFF1A74E8
} }
@ -321,7 +313,7 @@ func (session *sessionData) Language() string {
return session.language return session.language
} }
if len(session.languages) > 0 { if session.languages != nil && len(session.languages) > 0 {
return session.languages[0] return session.languages[0]
} }
@ -333,12 +325,15 @@ func (session *sessionData) SetLanguage(lang string) {
if lang != session.language { if lang != session.language {
session.language = lang session.language = lang
if session.rootView != nil && session.bridge != nil { if session.rootView != nil {
buffer := allocStringBuilder() buffer := allocStringBuilder()
defer freeStringBuilder(buffer) defer freeStringBuilder(buffer)
viewHTML(session.rootView, buffer, "") buffer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`)
session.bridge.updateInnerHTML("ruiRootView", buffer.String()) viewHTML(session.rootView, buffer)
buffer.WriteString("';\nscanElementsSize();")
session.runScript(buffer.String())
} }
} }
} }

View File

@ -1,5 +1,34 @@
package rui package rui
import (
"fmt"
"strings"
)
func (session *sessionData) startUpdateScript(htmlID string) {
buffer := allocStringBuilder()
session.updateScripts[htmlID] = buffer
buffer.WriteString("var element = document.getElementById('")
buffer.WriteString(htmlID)
buffer.WriteString("');\nif (element) {\n")
}
func (session *sessionData) updateScript(htmlID string) *strings.Builder {
if buffer, ok := session.updateScripts[htmlID]; ok {
return buffer
}
return nil
}
func (session *sessionData) finishUpdateScript(htmlID string) {
if buffer, ok := session.updateScripts[htmlID]; ok {
buffer.WriteString("scanElementsSize();\n}\n")
session.runScript(buffer.String())
freeStringBuilder(buffer)
delete(session.updateScripts, htmlID)
}
}
func sizeConstant(session Session, tag string) (SizeUnit, bool) { func sizeConstant(session Session, tag string) (SizeUnit, bool) {
if text, ok := session.Constant(tag); ok { if text, ok := session.Constant(tag); ok {
return StringToSizeUnit(text) return StringToSizeUnit(text)
@ -10,10 +39,15 @@ func sizeConstant(session Session, tag string) (SizeUnit, bool) {
func updateCSSStyle(htmlID string, session Session) { func updateCSSStyle(htmlID string, session Session) {
if !session.ignoreViewUpdates() { if !session.ignoreViewUpdates() {
if view := session.viewByHTMLID(htmlID); view != nil { if view := session.viewByHTMLID(htmlID); view != nil {
builder := viewCSSBuilder{buffer: allocStringBuilder()} var builder viewCSSBuilder
builder.buffer = allocStringBuilder()
builder.buffer.WriteString(`updateCSSStyle('`)
builder.buffer.WriteString(view.htmlID())
builder.buffer.WriteString(`', '`)
view.cssStyle(view, &builder) view.cssStyle(view, &builder)
//session.callFunc("updateCSSStyle", view.htmlID(), builder.finish()) builder.buffer.WriteString(`');`)
session.updateProperty(view.htmlID(), "style", builder.finish()) view.Session().runScript(builder.finish())
} }
} }
} }
@ -27,24 +61,94 @@ func updateInnerHTML(htmlID string, session Session) {
view = session.viewByHTMLID(htmlID) view = session.viewByHTMLID(htmlID)
} }
if view != nil { if view != nil {
session.callFunc("hideTooltip")
script := allocStringBuilder() script := allocStringBuilder()
defer freeStringBuilder(script) defer freeStringBuilder(script)
script.Grow(32 * 1024) script.Grow(32 * 1024)
view.htmlSubviews(view, script) view.htmlSubviews(view, script)
session.updateInnerHTML(view.htmlID(), script.String()) view.Session().runScript(fmt.Sprintf(`updateInnerHTML('%v', '%v');`, view.htmlID(), script.String()))
//view.updateEventHandlers()
} }
} }
} }
func appendToInnerHTML(htmlID, content string, session Session) {
if !session.ignoreViewUpdates() {
if view := session.viewByHTMLID(htmlID); view != nil {
view.Session().runScript(fmt.Sprintf(`appendToInnerHTML('%v', '%v');`, view.htmlID(), content))
//view.updateEventHandlers()
}
}
}
func updateProperty(htmlID, property, value string, session Session) {
if !session.ignoreViewUpdates() {
if buffer := session.updateScript(htmlID); buffer != nil {
buffer.WriteString(fmt.Sprintf(`element.setAttribute('%v', '%v');`, property, value))
buffer.WriteRune('\n')
} else {
session.runScript(fmt.Sprintf(`updateProperty('%v', '%v', '%v');`, htmlID, property, value))
}
}
}
func updateCSSProperty(htmlID, property, value string, session Session) {
if !session.ignoreViewUpdates() {
if buffer := session.updateScript(htmlID); buffer != nil {
buffer.WriteString(fmt.Sprintf(`element.style['%v'] = '%v';`, property, value))
buffer.WriteRune('\n')
} else {
session.runScript(fmt.Sprintf(`updateCSSProperty('%v', '%v', '%v');`, htmlID, property, value))
}
}
}
func updateBoolProperty(htmlID, property string, value bool, session Session) {
if !session.ignoreViewUpdates() {
if buffer := session.updateScript(htmlID); buffer != nil {
if value {
buffer.WriteString(fmt.Sprintf(`element.setAttribute('%v', true);`, property))
} else {
buffer.WriteString(fmt.Sprintf(`element.setAttribute('%v', false);`, property))
}
buffer.WriteRune('\n')
} else if value {
session.runScript(fmt.Sprintf(`updateProperty('%v', '%v', true);`, htmlID, property))
} else {
session.runScript(fmt.Sprintf(`updateProperty('%v', '%v', false);`, htmlID, property))
}
}
}
func removeProperty(htmlID, property string, session Session) {
if !session.ignoreViewUpdates() {
if buffer := session.updateScript(htmlID); buffer != nil {
buffer.WriteString(fmt.Sprintf(`if (element.hasAttribute('%v')) { element.removeAttribute('%v');}`, property, property))
buffer.WriteRune('\n')
} else {
session.runScript(fmt.Sprintf(`removeProperty('%v', '%v');`, htmlID, property))
}
}
}
/*
func setDisabled(htmlID string, disabled bool, session Session) {
if !session.ignoreViewUpdates() {
if disabled {
session.runScript(fmt.Sprintf(`setDisabled('%v', true);`, htmlID))
} else {
session.runScript(fmt.Sprintf(`setDisabled('%v', false);`, htmlID))
}
}
}
*/
func viewByHTMLID(id string, startView View) View { func viewByHTMLID(id string, startView View) View {
if startView != nil { if startView != nil {
if startView.htmlID() == id { if startView.htmlID() == id {
return startView return startView
} }
if container, ok := startView.(ParentView); ok { if container, ok := startView.(ParanetView); ok {
for _, view := range container.Views() { for _, view := range container.Views() {
if view != nil { if view != nil {
if v := viewByHTMLID(id, view); v != nil { if v := viewByHTMLID(id, view); v != nil {

296
shadow.go
View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -9,13 +9,13 @@ import (
var stringResources = map[string]map[string]string{} var stringResources = map[string]map[string]string{}
func (resources *resourceManager) scanEmbedStringsDir(fs *embed.FS, dir string) { func scanEmbedStringsDir(fs *embed.FS, dir string) {
if files, err := fs.ReadDir(dir); err == nil { if files, err := fs.ReadDir(dir); err == nil {
for _, file := range files { for _, file := range files {
name := file.Name() name := file.Name()
path := dir + "/" + name path := dir + "/" + name
if file.IsDir() { if file.IsDir() {
resources.scanEmbedStringsDir(fs, path) scanEmbedStringsDir(fs, path)
} else if strings.ToLower(filepath.Ext(name)) == ".rui" { } else if strings.ToLower(filepath.Ext(name)) == ".rui" {
if data, err := fs.ReadFile(path); err == nil { if data, err := fs.ReadFile(path); err == nil {
loadStringResources(string(data)) loadStringResources(string(data))
@ -27,14 +27,14 @@ func (resources *resourceManager) scanEmbedStringsDir(fs *embed.FS, dir string)
} }
} }
func (resources *resourceManager) scanStringsDir(path string) { func scanStringsDir(path string) {
if files, err := os.ReadDir(path); err == nil { if files, err := os.ReadDir(path); err == nil {
for _, file := range files { for _, file := range files {
filename := file.Name() filename := file.Name()
if filename[0] != '.' { if filename[0] != '.' {
newPath := path + `/` + filename newPath := path + `/` + filename
if file.IsDir() { if file.IsDir() {
resources.scanStringsDir(newPath) scanStringsDir(newPath)
} else if strings.ToLower(filepath.Ext(newPath)) == ".rui" { } else if strings.ToLower(filepath.Ext(newPath)) == ".rui" {
if data, err := os.ReadFile(newPath); err == nil { if data, err := os.ReadFile(newPath); err == nil {
loadStringResources(string(data)) loadStringResources(string(data))
@ -50,9 +50,8 @@ func (resources *resourceManager) scanStringsDir(path string) {
} }
func loadStringResources(text string) { func loadStringResources(text string) {
data, err := ParseDataText(text) data := ParseDataText(text)
if err != nil { if data == nil {
ErrorLog(err.Error())
return return
} }
@ -62,8 +61,8 @@ func loadStringResources(text string) {
table = map[string]string{} table = map[string]string{}
} }
for prop := range obj.Properties() { for i := 0; i < obj.PropertyCount(); i++ {
if prop.Type() == TextNode { if prop := obj.Property(i); prop != nil && prop.Type() == TextNode {
table[prop.Tag()] = prop.Text() table[prop.Tag()] = prop.Text()
} }
} }
@ -73,8 +72,8 @@ func loadStringResources(text string) {
tag := data.Tag() tag := data.Tag()
if tag == "strings" { if tag == "strings" {
for prop := range data.Properties() { for i := 0; i < data.PropertyCount(); i++ {
if prop.Type() == ObjectNode { if prop := data.Property(i); prop != nil && prop.Type() == ObjectNode {
parseStrings(prop.Object(), prop.Tag()) parseStrings(prop.Object(), prop.Tag())
} }
} }

View File

@ -1,167 +0,0 @@
package rui
import (
"io"
"net/http"
"os"
"strings"
)
// SvgImageView represents an SvgImageView view
type SvgImageView interface {
View
}
type svgImageViewData struct {
viewData
}
// NewSvgImageView create new SvgImageView object and return it
func NewSvgImageView(session Session, params Params) SvgImageView {
view := new(svgImageViewData)
view.init(session)
setInitParams(view, params)
return view
}
func newSvgImageView(session Session) View {
return new(svgImageViewData) // NewSvgImageView(session, nil)
}
// Init initialize fields of imageView by default values
func (imageView *svgImageViewData) init(session Session) {
imageView.viewData.init(session)
imageView.tag = "SvgImageView"
imageView.systemClass = "ruiSvgImageView"
imageView.normalize = normalizeSvgImageViewTag
imageView.set = imageView.setFunc
imageView.changed = imageView.propertyChanged
}
func normalizeSvgImageViewTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Source, "source":
tag = Content
case VerticalAlign:
tag = CellVerticalAlign
case HorizontalAlign:
tag = CellHorizontalAlign
}
return tag
}
func (imageView *svgImageViewData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case Content:
if text, ok := value.(string); ok {
imageView.setRaw(Content, text)
return []PropertyName{tag}
}
notCompatibleType(Source, value)
return nil
default:
return imageView.viewData.setFunc(tag, value)
}
}
func (imageView *svgImageViewData) propertyChanged(tag PropertyName) {
switch tag {
case Content:
updateInnerHTML(imageView.htmlID(), imageView.Session())
default:
imageView.viewData.propertyChanged(tag)
}
}
func (imageView *svgImageViewData) htmlTag() string {
return "div"
}
func (imageView *svgImageViewData) writeSvg(data []byte, buffer *strings.Builder) {
text := string(data)
index := strings.Index(text, "<svg")
if index > 0 {
text = text[index:]
}
index = strings.Index(text, "\n")
for index >= 0 {
if index > 0 && text[index-1] == '\r' {
buffer.WriteString(text[:index-1])
} else {
buffer.WriteString(text[:index])
}
end := len(text)
index++
for index < end && (text[index] == ' ' || text[index] == '\t' || text[index] == '\r' || text[index] == '\n') {
index++
}
text = text[index:]
index = strings.Index(text, "\n")
}
buffer.WriteString(text)
}
func (imageView *svgImageViewData) htmlSubviews(self View, buffer *strings.Builder) {
if value := imageView.getRaw(Content); value != nil {
if content, ok := value.(string); ok && content != "" {
if strings.HasPrefix(content, "@") {
if name, ok := imageView.session.ImageConstant(content[1:]); ok {
content = name
}
}
if image, ok := resources.images[content]; ok {
if image.fs != nil {
if data, err := image.fs.ReadFile(image.path); err == nil {
imageView.writeSvg(data, buffer)
return
} else {
DebugLog(err.Error())
}
} else if data, err := os.ReadFile(image.path); err == nil {
imageView.writeSvg(data, buffer)
return
} else {
DebugLog(err.Error())
}
}
if strings.HasPrefix(content, "http://") || strings.HasPrefix(content, "https://") {
resp, err := http.Get(content)
if err == nil {
defer resp.Body.Close()
if body, err := io.ReadAll(resp.Body); err == nil {
imageView.writeSvg(body, buffer)
return
}
}
DebugLog(err.Error())
}
buffer.WriteString(content)
}
}
}
// GetSvgImageViewVerticalAlign return the vertical align of an SvgImageView subview: TopAlign (0), BottomAlign (1), CenterAlign (2)
// If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned
func GetSvgImageViewVerticalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, CellVerticalAlign, LeftAlign, false)
}
// GetSvgImageViewHorizontalAlign return the vertical align of an SvgImageView subview: LeftAlign (0), RightAlign (1), CenterAlign (2)
// If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned
func GetSvgImageViewHorizontalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, CellHorizontalAlign, LeftAlign, false)
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +1,22 @@
package rui package rui
func newTableCellView(session Session) *tableCellView { import "strings"
view := new(tableCellView)
view.init(session) func (cell *tableCellView) Set(tag string, value any) bool {
return view return cell.set(strings.ToLower(tag), value)
} }
func (cell *tableCellView) init(session Session) { func (cell *tableCellView) set(tag string, value any) bool {
cell.viewData.init(session) switch tag {
cell.normalize = func(tag PropertyName) PropertyName { case VerticalAlign:
if tag == VerticalAlign { tag = TableVerticalAlign
return TableVerticalAlign
}
return tag
} }
return cell.viewData.set(tag, value)
} }
func (cell *tableCellView) cssStyle(self View, builder cssBuilder) { func (cell *tableCellView) cssStyle(self View, builder cssBuilder) {
session := cell.Session() session := cell.Session()
writeViewStyleCSS(cell, builder, session, false) cell.viewData.cssViewStyle(builder, session)
if value, ok := enumProperty(cell, TableVerticalAlign, session, 0); ok { if value, ok := enumProperty(cell, TableVerticalAlign, session, 0); ok {
builder.add("vertical-align", enumProperties[TableVerticalAlign].values[value]) builder.add("vertical-align", enumProperties[TableVerticalAlign].values[value])
@ -26,20 +24,15 @@ func (cell *tableCellView) cssStyle(self View, builder cssBuilder) {
} }
// GetTableContent returns a TableAdapter which defines the TableView content. // GetTableContent returns a TableAdapter which defines the TableView content.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableContent(view View, subviewID ...string) TableAdapter { func GetTableContent(view View, subviewID ...string) TableAdapter {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
if content := view.getRaw(Content); content != nil { view = ViewByID(view, subviewID[0])
if adapter, ok := content.(TableAdapter); ok {
return adapter
}
}
if obj := view.binding(); obj != nil {
if adapter, ok := obj.(TableAdapter); ok {
return adapter
} }
if view != nil {
if tableView, ok := view.(TableView); ok {
return tableView.content()
} }
} }
@ -47,17 +40,15 @@ func GetTableContent(view View, subviewID ...string) TableAdapter {
} }
// GetTableRowStyle returns a TableRowStyle which defines styles of TableView rows. // GetTableRowStyle returns a TableRowStyle which defines styles of TableView rows.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableRowStyle(view View, subviewID ...string) TableRowStyle { func GetTableRowStyle(view View, subviewID ...string) TableRowStyle {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
for _, tag := range []PropertyName{RowStyle, Content} { view = ViewByID(view, subviewID[0])
if value := view.getRaw(tag); value != nil {
if style, ok := value.(TableRowStyle); ok {
return style
}
} }
if view != nil {
if tableView, ok := view.(TableView); ok {
return tableView.getRowStyle()
} }
} }
@ -65,17 +56,15 @@ func GetTableRowStyle(view View, subviewID ...string) TableRowStyle {
} }
// GetTableColumnStyle returns a TableColumnStyle which defines styles of TableView columns. // GetTableColumnStyle returns a TableColumnStyle which defines styles of TableView columns.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableColumnStyle(view View, subviewID ...string) TableColumnStyle { func GetTableColumnStyle(view View, subviewID ...string) TableColumnStyle {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
for _, tag := range []PropertyName{ColumnStyle, Content} { view = ViewByID(view, subviewID[0])
if value := view.getRaw(tag); value != nil {
if style, ok := value.(TableColumnStyle); ok {
return style
}
} }
if view != nil {
if tableView, ok := view.(TableView); ok {
return tableView.getColumnStyle()
} }
} }
@ -83,54 +72,43 @@ func GetTableColumnStyle(view View, subviewID ...string) TableColumnStyle {
} }
// GetTableCellStyle returns a TableCellStyle which defines styles of TableView cells. // GetTableCellStyle returns a TableCellStyle which defines styles of TableView cells.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableCellStyle(view View, subviewID ...string) TableCellStyle { func GetTableCellStyle(view View, subviewID ...string) TableCellStyle {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
for _, tag := range []PropertyName{CellStyle, Content} { view = ViewByID(view, subviewID[0])
if value := view.getRaw(tag); value != nil {
if style, ok := value.(TableCellStyle); ok {
return style
} }
if view != nil {
if tableView, ok := view.(TableView); ok {
return tableView.getCellStyle()
} }
} }
return nil
}
return nil return nil
} }
// GetTableSelectionMode returns the mode of the TableView elements selection. // GetTableSelectionMode returns the mode of the TableView elements selection.
// Valid values are NoneSelection (0), CellSelection (1), and RowSelection (2). // Valid values are NoneSelection (0), CellSelection (1), and RowSelection (2).
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableSelectionMode(view View, subviewID ...string) int { func GetTableSelectionMode(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, SelectionMode, NoneSelection, false) return enumStyledProperty(view, subviewID, SelectionMode, NoneSelection, false)
} }
// GetTableVerticalAlign returns a vertical align in a TableView cell. Returns one of next values: // GetTableVerticalAlign returns a vertical align in a TavleView cell. Returns one of next values:
// TopAlign (0), BottomAlign (1), CenterAlign (2), and BaselineAlign (3) // TopAlign (0), BottomAlign (1), CenterAlign (2), and BaselineAlign (3)
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableVerticalAlign(view View, subviewID ...string) int { func GetTableVerticalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, TableVerticalAlign, TopAlign, false) return enumStyledProperty(view, subviewID, TableVerticalAlign, TopAlign, false)
} }
// GetTableHeadHeight returns the number of rows in the table header. // GetTableHeadHeight returns the number of rows in the table header.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableHeadHeight(view View, subviewID ...string) int { func GetTableHeadHeight(view View, subviewID ...string) int {
return intStyledProperty(view, subviewID, HeadHeight, 0) return intStyledProperty(view, subviewID, HeadHeight, 0)
} }
// GetTableFootHeight returns the number of rows in the table footer. // GetTableFootHeight returns the number of rows in the table footer.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableFootHeight(view View, subviewID ...string) int { func GetTableFootHeight(view View, subviewID ...string) int {
return intStyledProperty(view, subviewID, FootHeight, 0) return intStyledProperty(view, subviewID, FootHeight, 0)
} }
@ -139,13 +117,17 @@ func GetTableFootHeight(view View, subviewID ...string) int {
// If there is no selected cell/row or the selection mode is NoneSelection (0), // 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. // 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 selection mode is RowSelection (2) then the returned column index is less than 0.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableCurrent(view View, subviewID ...string) CellIndex { func GetTableCurrent(view View, subviewID ...string) CellIndex {
if view = getSubview(view, subviewID); view != nil { if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if selectionMode := GetTableSelectionMode(view); selectionMode != NoneSelection { if selectionMode := GetTableSelectionMode(view); selectionMode != NoneSelection {
return tableViewCurrent(view) if tableView, ok := view.(TableView); ok {
return tableView.getCurrent()
}
} }
} }
return CellIndex{Row: -1, Column: -1} return CellIndex{Row: -1, Column: -1}
@ -153,92 +135,66 @@ func GetTableCurrent(view View, subviewID ...string) CellIndex {
// GetTableCellClickedListeners returns listeners of event which occurs when the user clicks on a table cell. // 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 there are no listeners then the empty list is returned.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetTableCellClickedListeners(view View, subviewID ...string) []func(TableView, int, int) {
// - func(rui.TableView, int, int), if len(subviewID) > 0 && subviewID[0] != "" {
// - func(rui.TableView, int), view = ViewByID(view, subviewID[0])
// - func(rui.TableView), }
// - func(int, int), if view != nil {
// - func(int), if value := view.Get(TableCellClickedEvent); value != nil {
// - func(), if result, ok := value.([]func(TableView, int, int)); ok {
// - string. return result
// }
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned. }
// If it is not specified then a value from the first argument (view) is returned. }
func GetTableCellClickedListeners(view View, subviewID ...string) []any { return []func(TableView, int, int){}
return getTwoArgEventRawListeners[TableView, int](view, subviewID, TableCellClickedEvent)
} }
// GetTableCellSelectedListeners returns listeners of event which occurs when a table cell becomes selected. // 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 there are no listeners then the empty list is returned.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetTableCellSelectedListeners(view View, subviewID ...string) []func(TableView, int, int) {
// - func(rui.TableView, int, int), if len(subviewID) > 0 && subviewID[0] != "" {
// - func(rui.TableView, int), view = ViewByID(view, subviewID[0])
// - func(rui.TableView), }
// - func(int, int), if view != nil {
// - func(int), if value := view.Get(TableCellSelectedEvent); value != nil {
// - func(), if result, ok := value.([]func(TableView, int, int)); ok {
// - string. return result
// }
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned. }
// If it is not specified then a value from the first argument (view) is returned. }
func GetTableCellSelectedListeners(view View, subviewID ...string) []any { return []func(TableView, int, int){}
return getTwoArgEventRawListeners[TableView, int](view, subviewID, TableCellSelectedEvent)
} }
// GetTableRowClickedListeners returns listeners of event which occurs when the user clicks on a table row. // 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 there are no listeners then the empty list is returned.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetTableRowClickedListeners(view View, subviewID ...string) []func(TableView, int) {
// - func(rui.TableView, int), return getEventListeners[TableView, int](view, subviewID, TableRowClickedEvent)
// - func(rui.TableView),
// - func(int),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableRowClickedListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[TableView, int](view, subviewID, TableRowClickedEvent)
} }
// GetTableRowSelectedListeners returns listeners of event which occurs when a table row becomes selected. // 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 there are no listeners then the empty list is returned.
// // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// Result elements can be of the following types: func GetTableRowSelectedListeners(view View, subviewID ...string) []func(TableView, int) {
// - func(rui.TableView, int), return getEventListeners[TableView, int](view, subviewID, TableRowSelectedEvent)
// - func(rui.TableView),
// - func(int),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableRowSelectedListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[TableView, int](view, subviewID, TableRowSelectedEvent)
} }
// ReloadTableViewData updates TableView // ReloadTableViewData updates TableView
// If the second argument (subviewID) is not specified or it is "" then updates the first argument (TableView).
func ReloadTableViewData(view View, subviewID ...string) bool { func ReloadTableViewData(view View, subviewID ...string) bool {
if view = getSubview(view, subviewID); view != nil { var tableView TableView
if tableView, ok := view.(TableView); ok { if len(subviewID) > 0 && subviewID[0] != "" {
if tableView = TableViewByID(view, subviewID[0]); tableView == nil {
return false
}
} else {
var ok bool
if tableView, ok = view.(TableView); !ok {
return false
}
}
tableView.ReloadTableData() tableView.ReloadTableData()
return true return true
}
}
return false
}
// ReloadTableViewCell updates the given table cell.
// If the last argument (subviewID) is not specified or it is "" then updates the cell of the first argument (TableView).
func ReloadTableViewCell(row, column int, view View, subviewID ...string) bool {
if view = getSubview(view, subviewID); view != nil {
if tableView, ok := view.(TableView); ok {
tableView.ReloadCell(row, column)
return true
}
}
return false
} }

File diff suppressed because it is too large Load Diff

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