From 4f1969975d8466a561537bed62d5b3c0fe96e646 Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Sat, 7 Jun 2025 10:51:01 +0300 Subject: [PATCH 01/21] Added drag-and-drop support --- app_scripts.js | 102 ++++++++++++++++++- dragAndDrop.go | 260 +++++++++++++++++++++++++++++++++++++++++++++++++ events.go | 3 + propertySet.go | 38 +++----- view.go | 169 ++++++++++++++++++++++++++++++-- 5 files changed, 539 insertions(+), 33 deletions(-) create mode 100644 dragAndDrop.go diff --git a/app_scripts.js b/app_scripts.js index df8abb9..13f2c88 100644 --- a/app_scripts.js +++ b/app_scripts.js @@ -2124,4 +2124,104 @@ function createPath2D(svg) { } else { return new Path2D(); } -} \ No newline at end of file +} + +function stringToBase64(str) { + const bytes = new TextEncoder().encode(str); + const binString = String.fromCodePoint(...bytes); + return btoa(binString); +} + +function base64ToString(base64) { + const binString = atob(base64); + const bytes = Uint8Array.from(binString, (m) => m.codePointAt(0)); + const result = new TextDecoder().decode(bytes); + return result; +} + +function dragAndDropEvent(element, event, tag) { + event.stopPropagation(); + //event.preventDefault() + + let message = tag + "{session=" + sessionID + ",id=" + element.id + mouseEventData(element, event); + if (event.dataTransfer) { + if (event.target) { + message += ",target=" + event.target.id; + } + let dataText = "" + for (const item of event.dataTransfer.items) { + const data = event.dataTransfer.getData(item.type); + if (data) { + if (dataText != "") { + dataText += ";"; + } + dataText += stringToBase64(item.type) + ":" + stringToBase64(data); + } + } + if (dataText != "") { + message += ',data="' + dataText + '"'; + } + } + message += "}"; + sendMessage(message); +} + +function dragStartEvent(element, event) { + const data = element.getAttribute("data-drag"); + if (data) { + const elements = data.split(";"); + for (const line of elements) { + const pair = line.split(":"); + if (pair.length == 2) { + event.dataTransfer.setData(base64ToString(pair[0]), base64ToString(pair[1])); + } + } + } + + const image = element.getAttribute("data-drag-image"); + if (image) { + let x = element.getAttribute("data-drag-image-x"); + if (!x) { + x = 0; + } + + let y = element.getAttribute("data-drag-image-y"); + if (!y) { + y = 0; + } + + let img = new Image(); + img.src = image; + event.dataTransfer.setDragImage(img, x, y); + } + + // TODO drag effect + + dragAndDropEvent(element, event, "drag-start-event"); +} + +function dragEndEvent(element, event) { + dragAndDropEvent(element, event, "drag-end-event") +} + +function dragEnterEvent(element, event) { + dragAndDropEvent(element, event, "drag-enter-event") +} + +function dragLeaveEvent(element, event) { + dragAndDropEvent(element, event, "drag-leave-event") +} + +function dragOverEvent(element, event) { + event.preventDefault(); + if (element.getAttribute("data-drag-over") == "1") { + dragAndDropEvent(element, event, "drag-over-event") + } else { + event.stopPropagation(); + } +} + +function dropEvent(element, event) { + event.preventDefault(); + dragAndDropEvent(element, event, "drop-event") +} diff --git a/dragAndDrop.go b/dragAndDrop.go new file mode 100644 index 0000000..579017f --- /dev/null +++ b/dragAndDrop.go @@ -0,0 +1,260 @@ +package rui + +import ( + "encoding/base64" + "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" + + // 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: + // + 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: + // + 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: + // + 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: + // + 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: + // + 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: + // + DropEvent PropertyName = "drop-event" +) + +// MouseEvent represent a mouse event +type DragAndDropEvent struct { + MouseEvent + Data map[string]string + Target View +} + +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) + } +} + +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(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)" `) + } + + viewEventsHtml[DragAndDropEvent](view, []PropertyName{DragEndEvent, DragEnterEvent, DragLeaveEvent}, buffer) + + session := view.Session() + if img, ok := stringProperty(view, DragImage, session); ok && img != "" { + img = strings.Trim(img, " \t") + if img[0] == '@' { + if img, ok = session.ImageConstant(img[1:]); ok { + buffer.WriteString(` data-drag-image="`) + buffer.WriteString(img) + buffer.WriteString(`" `) + } + } else { + buffer.WriteString(` data-drag-image="`) + buffer.WriteString(img) + buffer.WriteString(`" `) + } + } + + if f, ok := floatTextProperty(view, DragImageXOffset, session, 0); ok { + buffer.WriteString(` data-drag-image-x="`) + buffer.WriteString(f) + buffer.WriteString(`" `) + } + + if f, ok := floatTextProperty(view, DragImageYOffset, session, 0); ok { + buffer.WriteString(` data-drag-image-y="`) + buffer.WriteString(f) + buffer.WriteString(`" `) + } +} + +// GetDragStartEventListeners returns the "drag-start-event" listener list. If there are no listeners then the empty list is returned. +// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +func GetDragStartEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) { + return getOneArgEventListeners[View, DragAndDropEvent](view, subviewID, DragStartEvent) +} + +// GetDragEndEventListeners returns the "drag-end-event" listener list. If there are no listeners then the empty list is returned. +// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +func GetDragEndEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) { + return getOneArgEventListeners[View, DragAndDropEvent](view, subviewID, DragEndEvent) +} + +// GetDragEnterEventListeners returns the "drag-enter-event" listener list. If there are no listeners then the empty list is returned. +// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +func GetDragEnterEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) { + return getOneArgEventListeners[View, DragAndDropEvent](view, subviewID, DragEnterEvent) +} + +// GetDragLeaveEventListeners returns the "drag-leave-event" listener list. If there are no listeners then the empty list is returned. +// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +func GetDragLeaveEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) { + return getOneArgEventListeners[View, DragAndDropEvent](view, subviewID, DragLeaveEvent) +} + +// GetDragOverEventListeners returns the "drag-over-event" listener list. If there are no listeners then the empty list is returned. +// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +func GetDragOverEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) { + return getOneArgEventListeners[View, DragAndDropEvent](view, subviewID, DragOverEvent) +} + +// GetDropEventListeners returns the "drag-start-event" listener list. If there are no listeners then the empty list is returned. +// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +func GetDropEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) { + return getOneArgEventListeners[View, DragAndDropEvent](view, subviewID, DropEvent) +} + +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 +} diff --git a/events.go b/events.go index 8bf6a9d..84c344b 100644 --- a/events.go +++ b/events.go @@ -33,6 +33,9 @@ var eventJsFunc = map[PropertyName]struct{ jsEvent, jsFunc string }{ 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 valueToNoArgEventListeners[V any](value any) ([]func(V), bool) { diff --git a/propertySet.go b/propertySet.go index 6cac614..c788382 100644 --- a/propertySet.go +++ b/propertySet.go @@ -2,6 +2,7 @@ package rui import ( "math" + "slices" "strconv" "strings" ) @@ -22,15 +23,16 @@ var colorProperties = []PropertyName{ AccentColor, } -func isPropertyInList(tag PropertyName, list []PropertyName) bool { - for _, prop := range list { - if prop == tag { - return true +/* + func isPropertyInList(tag PropertyName, list []PropertyName) bool { + for _, prop := range list { + if prop == tag { + return true + } } + return false } - return false -} - +*/ var angleProperties = []PropertyName{ From, } @@ -91,6 +93,8 @@ var floatProperties = map[PropertyName]struct{ min, max float64 }{ VideoWidth: {min: 0, max: 10000}, VideoHeight: {min: 0, max: 10000}, PushDuration: {min: 0, max: math.MaxFloat64}, + DragImageXOffset: {min: -math.MaxFloat64, max: math.MaxFloat64}, + DragImageYOffset: {min: -math.MaxFloat64, max: math.MaxFloat64}, } var sizeProperties = map[PropertyName]string{ @@ -849,19 +853,19 @@ func propertiesSet(properties Properties, tag PropertyName, value any) []Propert return setFloatProperty(properties, tag, value, limits.min, limits.max) } - if isPropertyInList(tag, colorProperties) { + if slices.Contains(colorProperties, tag) { return setColorProperty(properties, tag, value) } - if isPropertyInList(tag, angleProperties) { + if slices.Contains(angleProperties, tag) { return setAngleProperty(properties, tag, value) } - if isPropertyInList(tag, boolProperties) { + if slices.Contains(boolProperties, tag) { return setBoolProperty(properties, tag, value) } - if isPropertyInList(tag, intProperties) { + if slices.Contains(intProperties, tag) { return setIntProperty(properties, tag, value) } @@ -874,18 +878,6 @@ func propertiesSet(properties Properties, tag PropertyName, value any) []Propert return nil } -/* -func (properties *propertyList) Set(tag PropertyName, value any) bool { - tag = properties.normalize(tag) - if value == nil { - properties.remove(properties, tag) - return true - } - - return properties.set(properties, tag, value) != nil -} -*/ - func (data *dataProperty) Set(tag PropertyName, value any) bool { if value == nil { data.Remove(tag) diff --git a/view.go b/view.go index 2338651..2a46471 100644 --- a/view.go +++ b/view.go @@ -2,6 +2,7 @@ package rui import ( "fmt" + "maps" "strconv" "strings" ) @@ -399,6 +400,85 @@ func (view *viewData) setFunc(tag PropertyName, value any) []PropertyName { case ResizeEvent, ScrollEvent: return setOneArgEventListener[View, Frame](view, tag, value) + + case DragData: + switch value := value.(type) { + case map[string]string: + if len(value) == 0 { + view.setRaw(DragData, nil) + } else { + view.setRaw(DragData, maps.Clone(value)) + } + + case string: + if value == "" { + view.setRaw(DragData, nil) + } else { + data := map[string]string{} + for _, line := range strings.Split(value, ";") { + index := strings.IndexRune(line, ':') + if index < 0 { + invalidPropertyValue(DragData, value) + return nil + } + mime := line[:index] + val := line[index+1:] + if len(mime) > 0 || len(val) > 0 { + data[mime] = val + } + } + + if len(data) == 0 { + view.setRaw(DragData, nil) + } else { + view.setRaw(DragData, data) + } + } + + case DataObject: + data := map[string]string{} + count := value.PropertyCount() + for i := range count { + node := value.Property(i) + if node.Type() == TextNode { + data[node.Tag()] = node.Text() + } else { + invalidPropertyValue(DragData, value) + return nil + } + } + if len(data) == 0 { + view.setRaw(DragData, nil) + } else { + view.setRaw(DragData, data) + } + + case DataNode: + switch value.Type() { + case TextNode: + return view.setFunc(DragData, value.Text()) + + case ObjectNode: + return view.setFunc(DragData, value.Object()) + } + invalidPropertyValue(DragData, value) + return nil + + case DataValue: + if value.IsObject() { + return view.setFunc(DragData, value.Object()) + } + return view.setFunc(DragData, value.Value()) + + default: + notCompatibleType(DragData, value) + } + + return []PropertyName{DragData} + + case DragStartEvent, DragEndEvent, DragEnterEvent, DragLeaveEvent, DragOverEvent, DropEvent: + return setOneArgEventListener[View, DragAndDropEvent](view, tag, value) + } return viewStyleSet(view, tag, value) @@ -724,10 +804,81 @@ func (view *viewData) propertyChanged(tag PropertyName) { PointerDown, PointerUp, PointerMove, PointerOut, PointerOver, PointerCancel, TouchStart, TouchEnd, TouchMove, TouchCancel, TransitionRunEvent, TransitionStartEvent, TransitionEndEvent, TransitionCancelEvent, - AnimationStartEvent, AnimationEndEvent, AnimationIterationEvent, AnimationCancelEvent: + AnimationStartEvent, AnimationEndEvent, AnimationIterationEvent, AnimationCancelEvent, + DragEndEvent, DragEnterEvent, DragLeaveEvent: updateEventListenerHtml(view, tag) + case DragStartEvent: + if view.getRaw(DragStartEvent) != nil || view.getRaw(DragData) != nil { + session.updateProperty(htmlID, "ondragstart", "dragStartEvent(this, event)") + } else { + session.removeProperty(view.htmlID(), "ondragstart") + } + + case DropEvent: + if view.getRaw(DropEvent) != nil { + session.updateProperty(htmlID, "ondrop", "dropEvent(this, event)") + session.updateProperty(htmlID, "ondragover", "dragOverEvent(this, event)") + if view.getRaw(DragOverEvent) != nil { + session.updateProperty(htmlID, "data-drag-over", "1") + } else { + session.removeProperty(view.htmlID(), "data-drag-over") + } + } else { + session.removeProperty(view.htmlID(), "ondrop") + session.removeProperty(view.htmlID(), "ondragover") + } + + case DragOverEvent: + if view.getRaw(DragOverEvent) != nil { + session.updateProperty(htmlID, "data-drag-over", "1") + } else { + session.removeProperty(view.htmlID(), "data-drag-over") + } + + case DragData: + if data := base64DragData(view); data != "" { + session.updateProperty(htmlID, "draggable", "true") + session.updateProperty(htmlID, "data-drag", data) + session.updateProperty(htmlID, "ondragstart", "dragStartEvent(this, event)") + } else { + session.removeProperty(view.htmlID(), "draggable") + session.removeProperty(view.htmlID(), "data-drag") + if view.getRaw(DragStartEvent) == nil { + session.removeProperty(view.htmlID(), "ondragstart") + } + } + + case DragImage: + if img, ok := stringProperty(view, DragImage, view.session); ok && img != "" { + img = strings.Trim(img, " \t") + if img[0] == '@' { + img, ok = view.session.ImageConstant(img[1:]) + if !ok { + session.removeProperty(view.htmlID(), "data-drag-image") + return + } + } + session.updateProperty(htmlID, "data-drag-image", img) + } else { + session.removeProperty(view.htmlID(), "data-drag-image") + } + + case DragImageXOffset: + if f, ok := floatTextProperty(view, DragImageXOffset, session, 0); ok { + session.updateProperty(htmlID, "data-drag-image-x", f) + } else { + session.removeProperty(view.htmlID(), "data-drag-image-x") + } + + case DragImageYOffset: + if f, ok := floatTextProperty(view, DragImageYOffset, session, 0); ok { + session.updateProperty(htmlID, "data-drag-image-y", f) + } else { + session.removeProperty(view.htmlID(), "data-drag-image-y") + } + case DataList: updateInnerHTML(view.htmlID(), view.Session()) @@ -891,18 +1042,12 @@ func viewHTML(view View, buffer *strings.Builder, htmlTag string) { keyEventsHtml(view, buffer) viewEventsHtml[MouseEvent](view, []PropertyName{ClickEvent, DoubleClickEvent, MouseDown, MouseUp, MouseMove, MouseOut, MouseOver, ContextMenuEvent}, buffer) - //mouseEventsHtml(view, buffer, hasTooltip) - viewEventsHtml[PointerEvent](view, []PropertyName{PointerDown, PointerUp, PointerMove, PointerOut, PointerOver, PointerCancel}, buffer) - //pointerEventsHtml(view, buffer) - viewEventsHtml[TouchEvent](view, []PropertyName{TouchStart, TouchEnd, TouchMove, TouchCancel}, buffer) - //touchEventsHtml(view, buffer) - viewEventsHtml[string](view, []PropertyName{TransitionRunEvent, TransitionStartEvent, TransitionEndEvent, TransitionCancelEvent, AnimationStartEvent, AnimationEndEvent, AnimationIterationEvent, AnimationCancelEvent}, buffer) - //transitionEventsHtml(view, buffer) - //animationEventsHtml(view, buffer) + + dragAndDropHtml(view, buffer) buffer.WriteRune('>') view.htmlSubviews(view, buffer) @@ -952,6 +1097,12 @@ func (view *viewData) handleCommand(self View, command PropertyName, data DataOb case TouchStart, TouchEnd, TouchMove, TouchCancel: handleTouchEvents(self, command, data) + case DragStartEvent: + handleDragAndDropEvents(self, command, data) + + case DragEndEvent, DragEnterEvent, DragLeaveEvent, DragOverEvent, DropEvent: + handleDragAndDropEvents(self, command, data) + case FocusEvent: view.hasFocus = true for _, listener := range getNoArgEventListeners[View](view, nil, command) { From c9744168ba203c564d46c443b932b4b5a60e3c60 Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Sat, 7 Jun 2025 12:19:17 +0300 Subject: [PATCH 02/21] Added "drag effect" property --- app_scripts.js | 5 +++ dragAndDrop.go | 94 +++++++++++++++++++++++++++++++++++++++++++++++--- propertySet.go | 5 +++ view.go | 10 ++++-- 4 files changed, 108 insertions(+), 6 deletions(-) diff --git a/app_scripts.js b/app_scripts.js index 13f2c88..c729388 100644 --- a/app_scripts.js +++ b/app_scripts.js @@ -2193,6 +2193,11 @@ function dragStartEvent(element, event) { let img = new Image(); img.src = image; event.dataTransfer.setDragImage(img, x, y); + + let effect = element.getAttribute("data-drag-effect"); + if (effect) { + event.dataTransfer.effectAllowed = effect; + } } // TODO drag effect diff --git a/dragAndDrop.go b/dragAndDrop.go index 579017f..8a0c5e4 100644 --- a/dragAndDrop.go +++ b/dragAndDrop.go @@ -2,6 +2,7 @@ package rui import ( "encoding/base64" + "fmt" "maps" "strings" ) @@ -38,6 +39,29 @@ const ( // Supported types: float, int, string. DragImageYOffset PropertyName = "drag-image-y-offset" + // DragEffect is the constant for "drag-effect" 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 (DragEffectAll) or "all" - All operations are permitted (defaut value). + // - 1 (DragEffectCopy) or "copy" - A copy of the source item may be made at the new location. + // - 2 (DragEffectMove) or "move" - An item may be moved to a new location. + // - 3 (DragEffectLink) or "link" - A link may be established to the source at the new location. + // - 4 (DragEffectCopyMove) or "copyMove" - A copy or move operation is permitted. + // - 5 (DragEffectCopyLink) or "copyLink" - A copy or link operation is permitted. + // - 6 (DragEffectLinkMove) or "linkMove" - A link or move operation is permitted. + // - 7 (DragEffectNone) or "none" - The item may not be dropped. + DragEffect PropertyName = "drag-effect" + // DragStartEvent is the constant for "drag-start-event" property tag. // // Used by View. @@ -91,6 +115,30 @@ const ( // General listener format: // DropEvent PropertyName = "drop-event" + + // DragEffectAll - the value of the "drag-effect" property: all operations (copy, move, and link) are permitted (defaut value). + DragEffectAll = 0 + + // DragEffectCopy - the value of the "drag-effect" property: a copy of the source item may be made at the new location. + DragEffectCopy = 1 + + // DragEffectMove - the value of the "drag-effect" property: an item may be moved to a new location. + DragEffectMove = 2 + + // DragEffectLink - the value of the "drag-effect" property: a link may be established to the source at the new location. + DragEffectLink = 3 + + // DragEffectCopyMove - the value of the "drag-effect" property: a copy or move operation is permitted. + DragEffectCopyMove = 4 + + // DragEffectCopyLink - the value of the "drag-effect" property: a copy or link operation is permitted. + DragEffectCopyLink = 5 + + // DragEffectLinkMove - the value of the "drag-effect" property: a link or move operation is permitted. + DragEffectLinkMove = 6 + + // DragEffectNone - the value of the "drag-effect" property: the item may not be dropped. + DragEffectNone = 7 ) // MouseEvent represent a mouse event @@ -197,15 +245,22 @@ func dragAndDropHtml(view View, buffer *strings.Builder) { } } - if f, ok := floatTextProperty(view, DragImageXOffset, session, 0); ok { + if f := GetDragImageXOffset(view); f != 0 { buffer.WriteString(` data-drag-image-x="`) - buffer.WriteString(f) + buffer.WriteString(fmt.Sprintf("%g", f)) buffer.WriteString(`" `) } - if f, ok := floatTextProperty(view, DragImageYOffset, session, 0); ok { + if f := GetDragImageYOffset(view); f != 0 { buffer.WriteString(` data-drag-image-y="`) - buffer.WriteString(f) + buffer.WriteString(fmt.Sprintf("%g", f)) + buffer.WriteString(`" `) + } + + effects := enumProperties[DragEffect].cssValues + if n := GetDragEffect(view); n > 0 && n < len(effects) { + buffer.WriteString(` data-drag-effect="`) + buffer.WriteString(effects[n]) buffer.WriteString(`" `) } } @@ -258,3 +313,34 @@ func GetDragData(view View, subviewID ...string) map[string]string { return result } + +// GetDragImageXOffset returns the horizontal offset in pixels within the drag feedback image.. +// If the second argument (subviewID) is not specified or it is "" 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.. +// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +func GetDragImageYOffset(view View, subviewID ...string) float64 { + return floatStyledProperty(view, subviewID, DragImageYOffset, 0) +} + +// GetDragEffect 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 (DragEffectAll) or "all" - All operations are permitted (defaut value). +// - 1 (DragEffectCopy) or "copy" - A copy of the source item may be made at the new location. +// - 2 (DragEffectMove) or "move" - An item may be moved to a new location. +// - 3 (DragEffectLink) or "link" - A link may be established to the source at the new location. +// - 4 (DragEffectCopyMove) or "copyMove" - A copy or move operation is permitted. +// - 5 (DragEffectCopyLink) or "copyLink" - A copy or link operation is permitted. +// - 6 (DragEffectLinkMove) or "linkMove" - A link or move operation is permitted. +// - 7 (DragEffectNone) or "none" - The item may not be dropped. +// +// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +func GetDragEffect(view View, subviewID ...string) int { + return enumStyledProperty(view, subviewID, DragEffect, DragEffectAll, true) +} diff --git a/propertySet.go b/propertySet.go index c788382..a6eec7d 100644 --- a/propertySet.go +++ b/propertySet.go @@ -496,6 +496,11 @@ var enumProperties = map[PropertyName]enumPropertyData{ string(ColumnFill), []string{"balance", "auto"}, }, + DragEffect: { + []string{"all", "copy", "move", "link", "copyMove", "copyLink", "linkMove", "none"}, + "", + []string{"all", "copy", "move", "link", "copyMove", "copyLink", "linkMove", "none"}, + }, } func notCompatibleType(tag PropertyName, value any) { diff --git a/view.go b/view.go index 2a46471..8adfbe1 100644 --- a/view.go +++ b/view.go @@ -866,19 +866,25 @@ func (view *viewData) propertyChanged(tag PropertyName) { } case DragImageXOffset: - if f, ok := floatTextProperty(view, DragImageXOffset, session, 0); ok { + if f := GetDragImageXOffset(view); f != 0 { session.updateProperty(htmlID, "data-drag-image-x", f) } else { session.removeProperty(view.htmlID(), "data-drag-image-x") } case DragImageYOffset: - if f, ok := floatTextProperty(view, DragImageYOffset, session, 0); ok { + if f := GetDragImageXOffset(view); f != 0 { session.updateProperty(htmlID, "data-drag-image-y", f) } else { session.removeProperty(view.htmlID(), "data-drag-image-y") } + case DragEffect: + effects := enumProperties[DragEffect].cssValues + if n := GetDragEffect(view); n > 0 && n < len(effects) { + session.updateProperty(htmlID, "data-drag-effect", effects[n]) + } + case DataList: updateInnerHTML(view.htmlID(), view.Session()) From b76e3e56d8445baad6e1b434f8e53918b957ec5d Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Mon, 9 Jun 2025 19:35:14 +0300 Subject: [PATCH 03/21] Added "drop-effect-allowed" property --- app_scripts.js | 39 ++++-- dragAndDrop.go | 322 ++++++++++++++++++++++++++++++++++++++++++------- editView.go | 4 +- propertySet.go | 5 - view.go | 73 +++++++---- 5 files changed, 357 insertions(+), 86 deletions(-) diff --git a/app_scripts.js b/app_scripts.js index c729388..4aacd91 100644 --- a/app_scripts.js +++ b/app_scripts.js @@ -2144,10 +2144,10 @@ function dragAndDropEvent(element, event, tag) { //event.preventDefault() let message = tag + "{session=" + sessionID + ",id=" + element.id + mouseEventData(element, event); + if (event.target) { + message += ",target=" + event.target.id; + } if (event.dataTransfer) { - if (event.target) { - message += ",target=" + event.target.id; - } let dataText = "" for (const item of event.dataTransfer.items) { const data = event.dataTransfer.getData(item.type); @@ -2161,7 +2161,25 @@ function dragAndDropEvent(element, event, tag) { if (dataText != "") { message += ',data="' + dataText + '"'; } + + dataText = "" + for (const file of event.dataTransfer.files) { + if (dataText != "") { + dataText += ";"; + } + dataText += file.name; + } + if (dataText != "") { + message += ',files="' + dataText + '"'; + } + if (event.dataTransfer.effectAllowed && event.dataTransfer.effectAllowed != "uninitialized") { + message += ',effect-allowed="' + event.dataTransfer.effectAllowed + '"'; + } + if (event.dataTransfer.dropEffect) { + message += ',drop-effect="' + event.dataTransfer.dropEffect + '"'; + } } + message += "}"; sendMessage(message); } @@ -2193,14 +2211,12 @@ function dragStartEvent(element, event) { let img = new Image(); img.src = image; event.dataTransfer.setDragImage(img, x, y); - - let effect = element.getAttribute("data-drag-effect"); - if (effect) { - event.dataTransfer.effectAllowed = effect; - } } - // TODO drag effect + let allowed = element.getAttribute("data-drop-effect-allowed"); + if (allowed) { + event.dataTransfer.effectAllowed = allowed; + } dragAndDropEvent(element, event, "drag-start-event"); } @@ -2210,6 +2226,11 @@ function dragEndEvent(element, event) { } function dragEnterEvent(element, event) { + let effect = element.getAttribute("data-drop-effect"); + if (effect) { + event.dataTransfer.dropEffect = effect; + } + dragAndDropEvent(element, event, "drag-enter-event") } diff --git a/dragAndDrop.go b/dragAndDrop.go index 8a0c5e4..156ae62 100644 --- a/dragAndDrop.go +++ b/dragAndDrop.go @@ -39,7 +39,23 @@ const ( // Supported types: float, int, string. DragImageYOffset PropertyName = "drag-image-y-offset" - // DragEffect is the constant for "drag-effect" property tag. + // 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 (defaut 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. @@ -52,15 +68,15 @@ const ( // Supported types: int, string. // // Values: - // - 0 (DragEffectAll) or "all" - All operations are permitted (defaut value). - // - 1 (DragEffectCopy) or "copy" - A copy of the source item may be made at the new location. - // - 2 (DragEffectMove) or "move" - An item may be moved to a new location. - // - 3 (DragEffectLink) or "link" - A link may be established to the source at the new location. - // - 4 (DragEffectCopyMove) or "copyMove" - A copy or move operation is permitted. - // - 5 (DragEffectCopyLink) or "copyLink" - A copy or link operation is permitted. - // - 6 (DragEffectLinkMove) or "linkMove" - A link or move operation is permitted. - // - 7 (DragEffectNone) or "none" - The item may not be dropped. - DragEffect PropertyName = "drag-effect" + // - 0 (DropEffectUndefined) or "undefined" - The property value is not defined (defaut 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. // @@ -116,36 +132,42 @@ const ( // DropEvent PropertyName = "drop-event" - // DragEffectAll - the value of the "drag-effect" property: all operations (copy, move, and link) are permitted (defaut value). - DragEffectAll = 0 + // DropEffectUndefined - the value of the "drop-effect" and "drop-effect-allowed" properties: the value is not defined (default value). + DropEffectUndefined = 0 - // DragEffectCopy - the value of the "drag-effect" property: a copy of the source item may be made at the new location. - DragEffectCopy = 1 + // DropEffectNone - the value of the DropEffect field of the DragEvent struct: the item may not be dropped. + DropEffectNone = 0 - // DragEffectMove - the value of the "drag-effect" property: an item may be moved to a new location. - DragEffectMove = 2 + // 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 - // DragEffectLink - the value of the "drag-effect" property: a link may be established to the source at the new location. - DragEffectLink = 3 + // DropEffectMove - the value of the "drop-effect" and "drop-effect-allowed" properties: an item may be moved to a new location. + DropEffectMove = 2 - // DragEffectCopyMove - the value of the "drag-effect" property: a copy or move operation is permitted. - DragEffectCopyMove = 4 + // 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 - // DragEffectCopyLink - the value of the "drag-effect" property: a copy or link operation is permitted. - DragEffectCopyLink = 5 + // DropEffectCopyMove - the value of the "drop-effect-allowed" property: a copy or move operation is permitted. + DropEffectCopyMove = DropEffectCopy + DropEffectMove - // DragEffectLinkMove - the value of the "drag-effect" property: a link or move operation is permitted. - DragEffectLinkMove = 6 + // DropEffectCopyLink - the value of the "drop-effect-allowed" property: a copy or link operation is permitted. + DropEffectCopyLink = DropEffectCopy + DropEffectLink - // DragEffectNone - the value of the "drag-effect" property: the item may not be dropped. - DragEffectNone = 7 + // 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 (defaut value). + DropEffectAll = DropEffectCopy + DropEffectMove + DropEffectLink ) // MouseEvent represent a mouse event type DragAndDropEvent struct { MouseEvent - Data map[string]string - Target View + Data map[string]string + Files string + Target View + EffectAllowed int + DropEffect int } func (event *DragAndDropEvent) init(session Session, data DataObject) { @@ -175,6 +197,136 @@ func (event *DragAndDropEvent) init(session Session, data DataObject) { 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 + } + } + } + + // TODO files +} + +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.Contains(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) { @@ -227,7 +379,26 @@ func dragAndDropHtml(view View, buffer *strings.Builder) { buffer.WriteString(` ondragstart="dragStartEvent(this, event)" `) } - viewEventsHtml[DragAndDropEvent](view, []PropertyName{DragEndEvent, DragEnterEvent, DragLeaveEvent}, buffer) + 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) + } session := view.Session() if img, ok := stringProperty(view, DragImage, session); ok && img != "" { @@ -257,9 +428,9 @@ func dragAndDropHtml(view View, buffer *strings.Builder) { buffer.WriteString(`" `) } - effects := enumProperties[DragEffect].cssValues - if n := GetDragEffect(view); n > 0 && n < len(effects) { - buffer.WriteString(` data-drag-effect="`) + effects := []string{"undifined", "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(`" `) } @@ -326,21 +497,84 @@ func GetDragImageYOffset(view View, subviewID ...string) float64 { return floatStyledProperty(view, subviewID, DragImageYOffset, 0) } -// GetDragEffect returns the effect that is allowed for a drag operation. +// 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. +// +// If the second argument (subviewID) is not specified or it is "" 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 (DragEffectAll) or "all" - All operations are permitted (defaut value). -// - 1 (DragEffectCopy) or "copy" - A copy of the source item may be made at the new location. -// - 2 (DragEffectMove) or "move" - An item may be moved to a new location. -// - 3 (DragEffectLink) or "link" - A link may be established to the source at the new location. -// - 4 (DragEffectCopyMove) or "copyMove" - A copy or move operation is permitted. -// - 5 (DragEffectCopyLink) or "copyLink" - A copy or link operation is permitted. -// - 6 (DragEffectLinkMove) or "linkMove" - A link or move operation is permitted. -// - 7 (DragEffectNone) or "none" - The item may not be dropped. +// 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. // // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetDragEffect(view View, subviewID ...string) int { - return enumStyledProperty(view, subviewID, DragEffect, DragEffectAll, true) +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 } diff --git a/editView.go b/editView.go index 0b81faa..c65a783 100644 --- a/editView.go +++ b/editView.go @@ -261,9 +261,9 @@ func (edit *editViewData) AppendText(text string) { return } } - edit.setRaw(Text, text) + edit.Set(Text, text) } else { - edit.setRaw(Text, GetText(edit)+text) + edit.Set(Text, GetText(edit)+text) } } diff --git a/propertySet.go b/propertySet.go index a6eec7d..c788382 100644 --- a/propertySet.go +++ b/propertySet.go @@ -496,11 +496,6 @@ var enumProperties = map[PropertyName]enumPropertyData{ string(ColumnFill), []string{"balance", "auto"}, }, - DragEffect: { - []string{"all", "copy", "move", "link", "copyMove", "copyLink", "linkMove", "none"}, - "", - []string{"all", "copy", "move", "link", "copyMove", "copyLink", "linkMove", "none"}, - }, } func notCompatibleType(tag PropertyName, value any) { diff --git a/view.go b/view.go index 8adfbe1..a632583 100644 --- a/view.go +++ b/view.go @@ -479,6 +479,11 @@ func (view *viewData) setFunc(tag PropertyName, value any) []PropertyName { case DragStartEvent, DragEndEvent, DragEnterEvent, DragLeaveEvent, DragOverEvent, DropEvent: return setOneArgEventListener[View, DragAndDropEvent](view, tag, value) + case DropEffect: + return view.setDropEffect(value) + + case DropEffectAllowed: + return view.setDropEffectAllowed(value) } return viewStyleSet(view, tag, value) @@ -509,16 +514,16 @@ func (view *viewData) propertyChanged(tag PropertyName) { switch tag { case TabIndex: - if value, ok := intProperty(view, TabIndex, view.Session(), 0); ok { - session.updateProperty(view.htmlID(), "tabindex", strconv.Itoa(value)) + if value, ok := intProperty(view, TabIndex, session, 0); ok { + session.updateProperty(htmlID, "tabindex", strconv.Itoa(value)) } else if view.Focusable() { - session.updateProperty(view.htmlID(), "tabindex", "0") + session.updateProperty(htmlID, "tabindex", "0") } else { - session.updateProperty(view.htmlID(), "tabindex", "-1") + session.updateProperty(htmlID, "tabindex", "-1") } case Style, StyleDisabled: - session.updateProperty(view.htmlID(), "class", view.htmlClass(IsDisabled(view))) + session.updateProperty(htmlID, "class", view.htmlClass(IsDisabled(view))) case Disabled: tabIndex := GetTabIndex(view, htmlID) @@ -775,7 +780,7 @@ func (view *viewData) propertyChanged(tag PropertyName) { case PerspectiveOriginX, PerspectiveOriginY: x, y := GetPerspectiveOrigin(view) - session.updateCSSProperty(htmlID, "perspective-origin", transformOriginCSS(x, y, AutoSize(), view.Session())) + session.updateCSSProperty(htmlID, "perspective-origin", transformOriginCSS(x, y, AutoSize(), session)) case BackfaceVisible: if GetBackfaceVisible(view) { @@ -786,7 +791,7 @@ func (view *viewData) propertyChanged(tag PropertyName) { case TransformOriginX, TransformOriginY, TransformOriginZ: x, y, z := getTransformOrigin(view, session) - session.updateCSSProperty(htmlID, "transform-origin", transformOriginCSS(x, y, z, view.Session())) + session.updateCSSProperty(htmlID, "transform-origin", transformOriginCSS(x, y, z, session)) case Transform: css := "" @@ -813,7 +818,7 @@ func (view *viewData) propertyChanged(tag PropertyName) { if view.getRaw(DragStartEvent) != nil || view.getRaw(DragData) != nil { session.updateProperty(htmlID, "ondragstart", "dragStartEvent(this, event)") } else { - session.removeProperty(view.htmlID(), "ondragstart") + session.removeProperty(htmlID, "ondragstart") } case DropEvent: @@ -823,18 +828,18 @@ func (view *viewData) propertyChanged(tag PropertyName) { if view.getRaw(DragOverEvent) != nil { session.updateProperty(htmlID, "data-drag-over", "1") } else { - session.removeProperty(view.htmlID(), "data-drag-over") + session.removeProperty(htmlID, "data-drag-over") } } else { - session.removeProperty(view.htmlID(), "ondrop") - session.removeProperty(view.htmlID(), "ondragover") + session.removeProperty(htmlID, "ondrop") + session.removeProperty(htmlID, "ondragover") } case DragOverEvent: if view.getRaw(DragOverEvent) != nil { session.updateProperty(htmlID, "data-drag-over", "1") } else { - session.removeProperty(view.htmlID(), "data-drag-over") + session.removeProperty(htmlID, "data-drag-over") } case DragData: @@ -843,50 +848,66 @@ func (view *viewData) propertyChanged(tag PropertyName) { session.updateProperty(htmlID, "data-drag", data) session.updateProperty(htmlID, "ondragstart", "dragStartEvent(this, event)") } else { - session.removeProperty(view.htmlID(), "draggable") - session.removeProperty(view.htmlID(), "data-drag") + session.removeProperty(htmlID, "draggable") + session.removeProperty(htmlID, "data-drag") if view.getRaw(DragStartEvent) == nil { - session.removeProperty(view.htmlID(), "ondragstart") + session.removeProperty(htmlID, "ondragstart") } } case DragImage: - if img, ok := stringProperty(view, DragImage, view.session); ok && img != "" { + if img, ok := stringProperty(view, DragImage, session); ok && img != "" { img = strings.Trim(img, " \t") if img[0] == '@' { - img, ok = view.session.ImageConstant(img[1:]) + img, ok = session.ImageConstant(img[1:]) if !ok { - session.removeProperty(view.htmlID(), "data-drag-image") + session.removeProperty(htmlID, "data-drag-image") return } } session.updateProperty(htmlID, "data-drag-image", img) } else { - session.removeProperty(view.htmlID(), "data-drag-image") + session.removeProperty(htmlID, "data-drag-image") } case DragImageXOffset: if f := GetDragImageXOffset(view); f != 0 { session.updateProperty(htmlID, "data-drag-image-x", f) } else { - session.removeProperty(view.htmlID(), "data-drag-image-x") + session.removeProperty(htmlID, "data-drag-image-x") } case DragImageYOffset: if f := GetDragImageXOffset(view); f != 0 { session.updateProperty(htmlID, "data-drag-image-y", f) } else { - session.removeProperty(view.htmlID(), "data-drag-image-y") + session.removeProperty(htmlID, "data-drag-image-y") } - case DragEffect: - effects := enumProperties[DragEffect].cssValues - if n := GetDragEffect(view); n > 0 && n < len(effects) { - session.updateProperty(htmlID, "data-drag-effect", effects[n]) + case DropEffect: + effect := GetDropEffect(view) + switch effect { + case DropEffectCopy: + session.updateProperty(htmlID, "data-drop-effect", "copy") + case DropEffectMove: + session.updateProperty(htmlID, "data-drop-effect", "move") + case DropEffectLink: + session.updateProperty(htmlID, "data-drop-effect", "link") + default: + session.removeProperty(htmlID, "data-drop-effect") + } + + case DropEffectAllowed: + effect := GetDropEffectAllowed(view) + if effect >= DropEffectCopy && effect >= DropEffectAll { + values := []string{"undifined", "copy", "move", "copyMove", "link", "copyLink", "linkMove", "all"} + session.updateProperty(htmlID, "data-drop-effect-allowed", values[effect]) + } else { + session.removeProperty(htmlID, "data-drop-effect-allowed") } case DataList: - updateInnerHTML(view.htmlID(), view.Session()) + updateInnerHTML(htmlID, session) case Opacity: if f, ok := floatTextProperty(view, Opacity, session, 0); ok { From 4f07435637c59b28634da843a8784b460032f6cb Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:20:41 +0300 Subject: [PATCH 04/21] Added drag-and-drop of files --- animation.go | 6 +- app_scripts.js | 76 ++++++++++++++++++------- background.go | 2 +- backgroundRadialGradient.go | 4 +- border.go | 2 +- customView.go | 6 ++ data.go | 6 +- dragAndDrop.go | 11 +++- filePicker.go | 110 +++++++++++++++--------------------- filter.go | 2 +- listLayout.go | 3 +- listView.go | 6 +- popup.go | 2 +- properties.go | 2 +- session.go | 2 +- strings.go | 4 +- tableView.go | 4 +- theme.go | 20 +++---- touchEvents.go | 2 +- transform.go | 2 +- utils.go | 9 +++ view.go | 45 +++++++++++++++ viewStyleSet.go | 4 +- 23 files changed, 206 insertions(+), 124 deletions(-) diff --git a/animation.go b/animation.go index b170980..122692b 100644 --- a/animation.go +++ b/animation.go @@ -243,7 +243,7 @@ func parseAnimation(obj DataObject) AnimationProperty { animation := new(animationData) animation.init() - for i := 0; i < obj.PropertyCount(); i++ { + for i := range obj.PropertyCount() { if node := obj.Property(i); node != nil { tag := PropertyName(node.Tag()) if node.Type() == TextNode { @@ -491,7 +491,7 @@ func animationSet(properties Properties, tag PropertyName, value any) []Property case DataNode: parseObject := func(obj DataObject) (AnimatedProperty, bool) { result := AnimatedProperty{} - for i := 0; i < obj.PropertyCount(); i++ { + for i := range obj.PropertyCount() { if node := obj.Property(i); node.Type() == TextNode { propTag := strings.ToLower(node.Tag()) switch propTag { @@ -1050,7 +1050,7 @@ func setAnimationProperty(properties Properties, tag PropertyName, value any) bo case DataNode: animations := []AnimationProperty{} result := true - for i := 0; i < value.ArraySize(); i++ { + for i := range value.ArraySize() { if obj := value.ArrayElement(i).Object(); obj != nil { if anim := parseAnimation(obj); anim.hasAnimatedProperty() { animations = append(animations, anim) diff --git a/app_scripts.js b/app_scripts.js index 4aacd91..0de1018 100644 --- a/app_scripts.js +++ b/app_scripts.js @@ -1008,20 +1008,26 @@ function setInputValue(elementId, text) { } } +function filesTextForMessage(files) { + let message = "files=["; + for(let i = 0; i < files.length; i++) { + if (i > 0) { + message += ","; + } + message += "_{name=\"" + files[i].name + + "\",last-modified=" + files[i].lastModified + + ",size=" + files[i].size + + ",mime-type=\"" + files[i].type + "\"}"; + } + message += "]"; + return message +} + function fileSelectedEvent(element) { const files = element.files; if (files) { - let message = "fileSelected{session=" + sessionID + ",id=" + element.id + ",files=["; - for(let i = 0; i < files.length; i++) { - if (i > 0) { - message += ","; - } - message += "_{name=\"" + files[i].name + - "\",last-modified=" + files[i].lastModified + - ",size=" + files[i].size + - ",mime-type=\"" + files[i].type + "\"}"; - } - sendMessage(message + "]}"); + let message = "fileSelected{session=" + sessionID + ",id=" + element.id + "," + filesTextForMessage(files) + "}"; + sendMessage(message); } } @@ -2162,19 +2168,18 @@ function dragAndDropEvent(element, event, tag) { message += ',data="' + dataText + '"'; } - dataText = "" - for (const file of event.dataTransfer.files) { - if (dataText != "") { - dataText += ";"; - } - dataText += file.name; - } - if (dataText != "") { - message += ',files="' + dataText + '"'; + const files = event.dataTransfer.files + if (files && files.length > 0) { + message += "," + filesTextForMessage(files) + element["dragFiles"] = files; + } else { + element["dragFiles"] = null; } + if (event.dataTransfer.effectAllowed && event.dataTransfer.effectAllowed != "uninitialized") { message += ',effect-allowed="' + event.dataTransfer.effectAllowed + '"'; } + if (event.dataTransfer.dropEffect) { message += ',drop-effect="' + event.dataTransfer.dropEffect + '"'; } @@ -2251,3 +2256,34 @@ function dropEvent(element, event) { event.preventDefault(); dragAndDropEvent(element, event, "drop-event") } + +function loadDropFile(elementId, name, size) { + const element = document.getElementById(elementId); + if (element) { + const files = element["dragFiles"]; + if (files) { + for(let i = 0; i < files.length; i++) { + const file = files[i] + if (file.name == name && file.size == size) { + const reader = new FileReader(); + reader.onload = function() { + sendMessage("fileLoaded{session=" + sessionID + ",id=" + element.id + + ",name=`" + name + + "`,size=" + size + + ",last-modified=" + file.lastModified + + ",mime-type=\"" + file.type + + "\",data=`" + reader.result + "`}"); + } + reader.onerror = function(error) { + sendMessage("fileLoadingError{session=" + sessionID + ",id=" + element.id + ",name=\"" + name + "\",size=" + size + ",error=`" + error + "`}"); + } + reader.readAsDataURL(file); + return + } + } + } + sendMessage("fileLoadingError{session=" + sessionID + ",id=" + element.id + ",name=`" + name + "`,size=" + size + ",error=`File not found`}"); + } else { + sendMessage("fileLoadingError{session=" + sessionID + ",id=" + element.id + ",name=`" + name + "`,size=" + size + ",error=`Invalid View id`}"); + } +} diff --git a/background.go b/background.go index 592cb68..79a4c2f 100644 --- a/background.go +++ b/background.go @@ -67,7 +67,7 @@ func createBackground(obj DataObject) BackgroundElement { } count := obj.PropertyCount() - for i := 0; i < count; i++ { + for i := range count { if node := obj.Property(i); node.Type() == TextNode { if value := node.Text(); value != "" { result.Set(PropertyName(node.Tag()), value) diff --git a/backgroundRadialGradient.go b/backgroundRadialGradient.go index d126899..17cb41a 100644 --- a/backgroundRadialGradient.go +++ b/backgroundRadialGradient.go @@ -279,7 +279,7 @@ func (gradient *backgroundRadialGradient) cssStyle(session Session) string { } buffer.WriteString("ellipse ") shapeText = "" - for i := 0; i < count; i++ { + for i := range count { buffer.WriteString(value[i].cssString("50%", session)) buffer.WriteString(" ") } @@ -291,7 +291,7 @@ func (gradient *backgroundRadialGradient) cssStyle(session Session) string { } buffer.WriteString("ellipse ") shapeText = "" - for i := 0; i < count; i++ { + for i := range count { if value[i] != nil { switch value := value[i].(type) { case SizeUnit: diff --git a/border.go b/border.go index a4cfef7..45d0db6 100644 --- a/border.go +++ b/border.go @@ -433,7 +433,7 @@ func (border *borderProperty) String() string { func (border *borderProperty) setBorderObject(obj DataObject) bool { result := true - for i := 0; i < obj.PropertyCount(); i++ { + for i := range obj.PropertyCount() { if node := obj.Property(i); node != nil { tag := PropertyName(node.Tag()) switch node.Type() { diff --git a/customView.go b/customView.go index e8f8afb..5523e8a 100644 --- a/customView.go +++ b/customView.go @@ -346,3 +346,9 @@ func (customView *CustomViewData) SetTransition(tag PropertyName, animation Anim customView.superView.SetTransition(tag, animation) } } + +func (customView *CustomViewData) LoadFile(file FileInfo, result func(FileInfo, []byte)) { + if customView.superView != nil { + customView.superView.LoadFile(file, result) + } +} diff --git a/data.go b/data.go index 44bad40..b1cec3d 100644 --- a/data.go +++ b/data.go @@ -222,7 +222,7 @@ func (object *dataObject) ToParams() Params { case ArrayNode: array := []any{} - for i := 0; i < node.ArraySize(); i++ { + for i := range node.ArraySize() { if data := node.ArrayElement(i); data != nil { if data.IsObject() { if obj := data.Object(); obj != nil { @@ -465,7 +465,7 @@ func ParseDataText(text string) DataObject { return invalidEscape() } x := 0 - for i := 0; i < 2; i++ { + for range 2 { ch := data[n2] if ch >= '0' && ch <= '9' { x = x*16 + int(ch-'0') @@ -485,7 +485,7 @@ func ParseDataText(text string) DataObject { return invalidEscape() } x := 0 - for i := 0; i < 4; i++ { + for range 4 { ch := data[n2] if ch >= '0' && ch <= '9' { x = x*16 + int(ch-'0') diff --git a/dragAndDrop.go b/dragAndDrop.go index 156ae62..ae86a9e 100644 --- a/dragAndDrop.go +++ b/dragAndDrop.go @@ -164,7 +164,7 @@ const ( type DragAndDropEvent struct { MouseEvent Data map[string]string - Files string + Files []FileInfo Target View EffectAllowed int DropEffect int @@ -216,7 +216,7 @@ func (event *DragAndDropEvent) init(session Session, data DataObject) { } } - // TODO files + event.Files = parseFilesTag(data) } func stringToDropEffect(text string) (int, bool) { @@ -436,6 +436,13 @@ func dragAndDropHtml(view View, buffer *strings.Builder) { } } +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. // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. func GetDragStartEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) { diff --git a/filePicker.go b/filePicker.go index bc2296e..8c8b0b6 100644 --- a/filePicker.go +++ b/filePicker.go @@ -1,7 +1,7 @@ package rui import ( - "encoding/base64" + "fmt" "strconv" "strings" "time" @@ -67,6 +67,8 @@ type FileInfo struct { // MimeType - the file's MIME type. MimeType string + + data []byte } // FilePicker represents the FilePicker view @@ -82,11 +84,12 @@ type FilePicker interface { type filePickerData struct { viewData - files []FileInfo - loader map[int]func(FileInfo, []byte) + files []FileInfo + //loader map[int]func(FileInfo, []byte) } -func (file *FileInfo) initBy(node DataValue) { +func dataToFileInfo(node DataValue) FileInfo { + var file FileInfo if obj := node.Object(); obj != nil { file.Name, _ = obj.PropertyValue("name") file.MimeType, _ = obj.PropertyValue("mime-type") @@ -103,6 +106,11 @@ func (file *FileInfo) initBy(node DataValue) { } } } + 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 @@ -122,7 +130,7 @@ func (picker *filePickerData) init(session Session) { picker.tag = "FilePicker" picker.hasHtmlDisabled = true picker.files = []FileInfo{} - picker.loader = map[int]func(FileInfo, []byte){} + //picker.loader = map[int]func(FileInfo, []byte){} picker.set = picker.setFunc picker.changed = picker.propertyChanged @@ -137,16 +145,23 @@ func (picker *filePickerData) Files() []FileInfo { } func (picker *filePickerData) LoadFile(file FileInfo, result func(FileInfo, []byte)) { - if result == nil { - return - } - - for i, info := range picker.files { - if info.Name == file.Name && info.Size == file.Size && info.LastModified == file.LastModified { - picker.loader[i] = result - picker.Session().callFunc("loadSelectedFile", picker.htmlID(), i) - return + if result != nil { + for i, info := range picker.files { + if info.Name == file.Name && info.Size == file.Size && info.LastModified == file.LastModified { + if info.data != nil { + result(info, info.data) + } 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 + } } + + picker.viewData.LoadFile(file, result) } } @@ -248,65 +263,30 @@ func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder) } } +func parseFilesTag(data DataObject) []FileInfo { + if node := data.PropertyByTag("files"); node != nil && node.Type() == ArrayNode { + count := node.ArraySize() + files := make([]FileInfo, count) + for i := range count { + if value := node.ArrayElement(i); value != nil { + files[i] = dataToFileInfo(value) + } + } + return files + } + return nil +} + func (picker *filePickerData) handleCommand(self View, command PropertyName, data DataObject) bool { switch command { case "fileSelected": - if node := data.PropertyByTag("files"); node != nil && node.Type() == ArrayNode { - count := node.ArraySize() - files := make([]FileInfo, count) - for i := 0; i < count; i++ { - if value := node.ArrayElement(i); value != nil { - files[i].initBy(value) - } - } + if files := parseFilesTag(data); files != nil { picker.files = files - for _, listener := range GetFileSelectedListeners(picker) { 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 picker.viewData.handleCommand(self, command, data) @@ -354,7 +334,7 @@ func GetFilePickerAccept(view View, subviewID ...string) []string { } if ok { result := strings.Split(accept, ",") - for i := 0; i < len(result); i++ { + for i := range len(result) { result[i] = strings.Trim(result[i], " \t\n") } return result diff --git a/filter.go b/filter.go index 11d701a..850dc5c 100644 --- a/filter.go +++ b/filter.go @@ -159,7 +159,7 @@ func NewFilterProperty(params Params) FilterProperty { func newFilterProperty(obj DataObject) FilterProperty { filter := new(filterData) filter.init() - for i := 0; i < obj.PropertyCount(); i++ { + for i := range obj.PropertyCount() { if node := obj.Property(i); node != nil { tag := node.Tag() switch node.Type() { diff --git a/listLayout.go b/listLayout.go index 0709416..55acf8a 100644 --- a/listLayout.go +++ b/listLayout.go @@ -176,8 +176,7 @@ func (listLayout *listLayoutData) createContent() bool { htmlID := listLayout.htmlID() isDisabled := IsDisabled(listLayout) - count := adapter.ListSize() - for i := 0; i < count; i++ { + for i := range adapter.ListSize() { if view := adapter.ListItem(i, session); view != nil { view.setParentID(htmlID) if isDisabled { diff --git a/listView.go b/listView.go index d647cf7..c90b392 100644 --- a/listView.go +++ b/listView.go @@ -428,7 +428,7 @@ func (listView *listViewData) ReloadListViewData() { listView.itemFrame = make([]Frame, itemCount) } - for i := 0; i < itemCount; i++ { + for i := range itemCount { listView.items[i] = adapter.ListItem(i, listView.Session()) } } else if len(listView.items) > 0 { @@ -632,7 +632,7 @@ func (listView *listViewData) checkboxSubviews(adapter ListAdapter, buffer *stri current := GetCurrent(listView) checkedItems := GetListViewCheckedItems(listView) - for i := 0; i < count; i++ { + for i := range count { buffer.WriteString(`
= 0 { + base64Data = base64Data[index+1:] + } + decode, err := base64.StdEncoding.DecodeString(base64Data) + if err == nil { + listener(file, decode) + } else { + ErrorLog(err.Error()) + } + } + } + return true + + case "fileLoadingError": + file := dataToFileInfo(data) + key := file.key() + + if error, ok := data.PropertyValue("error"); ok { + ErrorLogF(`Load "%s" file error: %s`, file.Name, error) + } + + if listener := view.fileLoader[key]; listener != nil { + delete(view.fileLoader, key) + listener(file, nil) + } + return true + default: return false } diff --git a/viewStyleSet.go b/viewStyleSet.go index 184c1e8..04af494 100644 --- a/viewStyleSet.go +++ b/viewStyleSet.go @@ -64,7 +64,7 @@ func setTransitionProperty(properties Properties, value any) bool { return false case ArrayNode: - for i := 0; i < value.ArraySize(); i++ { + for i := range value.ArraySize() { if obj := value.ArrayElement(i).Object(); obj != nil { if !setObject(obj) { return false @@ -133,7 +133,7 @@ func setTransitionProperty(properties Properties, value any) bool { case ArrayNode: result := true - for i := 0; i < value.ArraySize(); i++ { + for i := range value.ArraySize() { if obj := value.ArrayElement(i).Object(); obj != nil { result = setObject(obj) && result } else { From ec2c5393f1945a71434c658a7b64d64ec10c6503 Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Wed, 11 Jun 2025 17:54:06 +0300 Subject: [PATCH 05/21] Updated Readme --- CHANGELOG.md | 5 ++ README-ru.md | 188 ++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 191 +++++++++++++++++++++++++++++++++++++++++++++++++ dragAndDrop.go | 47 +++++++----- 4 files changed, 415 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f57102d..43687ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# v0.19.0 + +* Added support of drag-and-drop +* Added LoadFile method to View interface + # v0.18.2 * fixed typo: GetShadowPropertys -> GetShadowProperty diff --git a/README-ru.md b/README-ru.md index 973dd32..13a7518 100644 --- a/README-ru.md +++ b/README-ru.md @@ -2287,6 +2287,194 @@ radius необходимо передать nil которые прокручивают view, соответственно, в заданную позицию, начало и конец +### Drag and drop + +#### Свойство "drag-data" + +Для того чтобы сделать View перетаскиваемым ему неоходимо задать свойство "drag-data" (константа DragData). +Данное свойство задает множество перетаскиваемых данных в виде ключ:значение и имеет тип: + + map[string]string + +В качестве ключей рекомендуется использовать mime-тип значения. +Например, если в перетаскиваемыми данными асляется текст, то ключем должен быть "text/plain", если jpeg-изображение, то "image/jpg" и т.п. +Но это только рекомендация, ключем может быть любой текст. + +Пример + + view.Set(rui.DragData, map[string]string { + "text/plain": "Drag-and-drop text", + "text/html" : "Drag-and-drop<\b> text", + "my-key" : "my-data", + }) + +Получить значение данного свойства можно с помощью функции + + GetDragData(view View, subviewID ...string) map[string]string + + +#### Свойство "drag-image" + +По умолчанию при перетаскивании перемещается весь View. Часто это бывает не удобно, например если View очень большой. + +Свойство "drag-image" (константа DragImage) типа string позволяет задать картинку, которая будет отображаться вместо View при перетаскивании. +В качестве значения "drag-image" задаеться: +* Имя изображения в ресурсах приложения +* Константа изображения +* URL изображения + +Пример + + view.Set(rui.DragImage, "image.png") + +Получить значение данного свойства можно с помощью функции + + func GetDragImage(view View, subviewID ...string) string { + +#### Свойства "drag-image-x-offset" и "drag-image-y-offset" + +Свойства "drag-image-x-offset" и "drag-image-y-offset" (константы DragImageXOffset и DragImageXOffset) типа float задают смещение в пикселях перетаскиваемой картинки относительно курсора мыши. +По умолчанию курсор мыши привязан к верхнему левому углу картинки. Данные свойства используются только если задано свойство "drag-image". + +Пример + + view.SetParams(rui.Params { + rui.DragImage : "image.png", + rui.DragImageXOffset: 10, + rui.DragImageYOffset: -15, + }) + +Получить значение данных свойств можно с помощью функций + + func GetDragImageXOffset(view View, subviewID ...string) float64 + func GetDragImageYOffset(view View, subviewID ...string) float64 + +#### События + +Слушатели событий drag-and-drop имеет следующий формат: + + func(View, DragAndDropEvent) + +где DragAndDropEvent расширяет MouseEvent и описана как + + type DragAndDropEvent struct { + MouseEvent + Data map[string]string + Files []FileInfo + Target View + EffectAllowed int + DropEffect int + } + +Можно также использовать слушателей следующих форматов: + +* func(DragAndDropEvent) +* func(View) +* func() + +Поля структуры DragAndDropEvent содержат следующие данные + +* Data - данные перетаскивания заданные свойством "drag-data" перетаскиваемого View и дополнительные данные заданные браузером (например, Chrome добавляет html-код элемента). Данное поле может быть пустым, если перетаскиваются файлы + +* Files - список перетаскиваемых файлов. Используется только событием "drop-event" (для всех других событий этот список равен nil). Также если вы перетаскиваете View, то этот список будет равным nil. + +* Target - принимающий View. Имеет значение отличное от nil, если перетаскиваемый объект находится над View который может принять перетаскиваемый элемент + +* EffectAllowed - показывает список разрешенных эффектов перемещения для курсора мыши: DropEffectCopy, DropEffectMove, DropEffectLink, DropEffectCopyMove, DropEffectCopyLink, DropEffectLinkMove и DropEffectAll. + +* DropEffect - используется только в событии "drag-end" и имеет значение DropEffectCopy, DropEffectMove или DropEffectLink если операция перетаскивания завершилась успешно и DropEffectNone в противоположном случае. + +#### Событие "drop-event" + +Для того чтобы View смог принимать перетаскиваемые данные (View или файлы) вы должны определить для него слушателя события "drop-event" (константа DropEvent). Как и все событий drag-and-drop он имеет следующий формат: + + func(View, DragAndDropEvent) + +Система сама ничего не делает с перетаскиваемыми данными. Вы должны добавить код выполняющий какие либо действия с перетаскиваемыми данными. Следующий пример добавляет картинку из ресурсов или из перетаскиваемого файла в ListLayout + + listLayout.Set(rui.DropEvent, func(_ rui.View, event rui.DragAndDropEvent)) { + if len(event.Files) > 0 { + for _, file := range event.Files { + switch file.MimeType { + case "image/png", "image/jpeg", "image/gif", "image/svg+xml": + listLayout.LoadFile(file, func(file rui.FileInfo, data []byte) { + if data != nil { + listLayout.Append(rui.NewImageView(session, rui.Params{ + rui.Source: rui.InlineFileFromData(data, file.MimeType), + })) + } + }) + } + } + } else { + for _, mime := range []string {"image/png", "image/jpeg", "image/gif", "image/svg+xml"} { + if src, ok := event.Data[mime]; ok { + list.Append(rui.NewImageView(session, rui.Params{ + rui.Source: src, + })) + } + } + } + } + +Получить список слушателей данного события можно с помощью функции: + + func GetDropEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) + +#### События "drag-start-event" и "drag-end-event" + +События "drag-start-event" и "drag-end-event" генерируются только для перетаскиваемого View. +Событие "drag-start-event" при старте перетаскивания, а "drag-end-event" по окончании перетаскивания. + +Слушатели данных событий как и всех событий drag-and-drop имеют следующий формат: + + func(View, DragAndDropEvent) + +Для данных событий поле Target структуры DragAndDropEvent, всегда равно nil. + +Для события "drag-end-event" используется поле DropEffect структуры DragAndDropEvent. +Оно будет иметь значение DropEffectCopy, DropEffectMove или DropEffectLink если операция перетаскивания завершилась успешно и DropEffectNone в противоположном случае. + +Получить список слушателей данных событий можно с помощью функции: + + func GetDragStartEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) + func GetDragEndEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) + +#### События "drag-enter-event", "drag-leave-event" и "drag-over-event" + +События "drag-enter-event", "drag-leave-event" и "drag-over-event" генерируются только для View приемника перетескиваемого объекта. + +Событие "drag-enter-event" генерируется когда перетаскиваемый оъект входит в область View приемника. + +Событие "drag-leave-event" генерируется когда перетаскиваемый оъект покидает область View приемника. + +Событие "drag-over-event" генерируется c определенным интервалом (несколько раз в секунду) пока перетаскиваемый оъект находится область View приемника. + +Пример, меняем цвет рамки с серой на красную когда перетаскиваемый оъект находится над областью приемника + + view.SetParams(rui.Params{ + rui.DragEnterEvent: func(view rui.View, event rui.DragAndDropEvent)) { + view.Set(rui.Border, rui.NewBorder(rui.Params{ + rui.Style: rui.SolidLine, + rui.ColorTag: rui.Red, + rui.Width: rui.Px(1), + })) + }, + rui.DragLeaveEvent: func(view rui.View, event rui.DragAndDropEvent)) { + view.Set(rui.Border, rui.NewBorder(rui.Params{ + rui.Style: rui.SolidLine, + rui.ColorTag: rui.Gray, + rui.Width: rui.Px(1), + })) + }, + }) + +Получить список слушателей данных событий можно с помощью функции: + + func GetDragEnterEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) + func GetDragLeaveEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) + func GetDragOverEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) + ## ViewsContainer Интерфейс ViewsContainer, реализующий View, описывает контейнер содержащий несколько diff --git a/README.md b/README.md index 908d7f8..95b59e6 100644 --- a/README.md +++ b/README.md @@ -2265,6 +2265,197 @@ The following global functions can be used for manual scrolling which scroll the view, respectively, to the given position, start and end +### Drag and drop + +#### "drag-data" property + +To make a View draggable, it is necessary to set the "drag-data" property (DragData constant). +This property specifies a set of draggable data in the form of key:value and has the type: + + map[string]string + +It is recommended to use the mime type of the value as keys. +For example, if the dragged data contains text, the key should be "text/plain", if it is a jpeg image, then "image/jpg", etc. +But this is only a recommendation, the key can be any text. + +Example + + view.Set(rui.DragData, map[string]string { + "text/plain": "Drag-and-drop text", + "text/html" : "Drag-and-drop<\b> text", + "my-key" : "my-data", + }) + +You can get the value of this property using the function + + GetDragData(view View, subviewID ...string) map[string]string + + +#### "drag-image" property + +By default, the entire View is moved when dragging. This is often inconvenient, for example if the View is very large. + +The "drag-image" string property (DragImage constant) allows to specify an image that will be displayed instead of the View when dragging. +The "drag-image" value is set to: +* Image name in the application resources +* Image constant +* Image URL + +Example + + view.Set(rui.DragImage, "image.png") + +You can get the value of this property using the function + + func GetDragImage(view View, subviewID ...string) string { + +#### The "drag-image-x-offset" and "drag-image-y-offset" properties + +The "drag-image-x-offset" and "drag-image-y-offset" float properties (the DragImageXOffset and DragImageXOffset constants) specify the offset in pixels of the dragged image relative to the mouse cursor. + +By default, the mouse cursor is anchored to the upper left corner of the image. These properties are used only if the "drag-image" property is set. + +Example + + view.SetParams(rui.Params { + rui.DragImage : "image.png", + rui.DragImageXOffset: 10, + rui.DragImageYOffset: -15, + }) + +You can get the value of these properties using functions + + func GetDragImageXOffset(view View, subviewID ...string) float64 + func GetDragImageYOffset(view View, subviewID ...string) float64 + +#### Events + +Drag-and-drop event listeners have the following format: + + func(View, DragAndDropEvent) + +where DragAndDropEvent extends MouseEvent and is declared as + + type DragAndDropEvent struct { + MouseEvent + Data map[string]string + Files []FileInfo + Target View + EffectAllowed int + DropEffect int + } + +You can also use listeners of the following formats: + +* func(DragAndDropEvent) +* func(View) +* func() + +The fields of the DragAndDropEvent structure contain the following data + +* Data - drag data specified by the "drag-data" property of the dragged View and additional data specified by the browser (for example, Chrome adds the html code of the element). This field can be empty if files are dragged + +* Files - a list of files to drag. Used only by the "drop-event" event (for all other events, this list is nil). Also, if you drag a View, this list will be nil. + +* Target - the receiving View. Has a value different from nil if the dragged object is above a View that can accept the dragged element + +* EffectAllowed - shows a list of allowed mouse cursor move effects: DropEffectCopy, DropEffectMove, DropEffectLink, DropEffectCopyMove, DropEffectCopyLink, DropEffectLinkMove and DropEffectAll. + +* DropEffect - used only in the "drag-end" event and has a value of DropEffectCopy, DropEffectMove or DropEffectLink if the drag operation was successful and DropEffectNone otherwise. + +#### The "drop-event" event + +In order for the View to accept dragged data (Views or files), you must define a "drop-event" (DropEvent constant) event listener for it. +Like all drag-and-drop events, it has the following format: + + func(View, DragAndDropEvent) + +The system itself does nothing with the dragged data. You must add code that performs some actions with the dragged data. +The following example adds an image from resources or from a dragged file to the ListLayout + + listLayout.Set(rui.DropEvent, func(_ rui.View, event rui.DragAndDropEvent)) { + if len(event.Files) > 0 { + for _, file := range event.Files { + switch file.MimeType { + case "image/png", "image/jpeg", "image/gif", "image/svg+xml": + listLayout.LoadFile(file, func(file rui.FileInfo, data []byte) { + if data != nil { + listLayout.Append(rui.NewImageView(session, rui.Params{ + rui.Source: rui.InlineFileFromData(data, file.MimeType), + })) + } + }) + } + } + } else { + for _, mime := range []string {"image/png", "image/jpeg", "image/gif", "image/svg+xml"} { + if src, ok := event.Data[mime]; ok { + list.Append(rui.NewImageView(session, rui.Params{ + rui.Source: src, + })) + } + } + } + } + +You can get a list of listeners for a given event using the function: + + func GetDropEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) + +#### Events "drag-start-event" and "drag-end-event" + +Events "drag-start-event" and "drag-end-event" are generated only for the dragged View. +Event "drag-start-event" when dragging starts, and "drag-end-event" when dragging ends. + +Listeners for these events, like all drag-and-drop events, have the following format: + + func(View, DragAndDropEvent) + +For these events, the Target field of the DragAndDropEvent structure is always nil. + +For the "drag-end-event" event, the DropEffect field of the DragAndDropEvent structure is used. +It will have the value DropEffectCopy, DropEffectMove or DropEffectLink if the drag operation was successful and DropEffectNone otherwise. + +You can get a list of listeners for these events using the function: + + func GetDragStartEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) + func GetDragEndEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) + +#### Events "drag-enter-event", "drag-leave-event" and "drag-over-event" + +Events "drag-enter-event", "drag-leave-event" and "drag-over-event" are generated only for the receiver View of the dragged object. + +Event "drag-enter-event" is generated when the dragged object enters the receiver View area. + +Event "drag-leave-event" is generated when the dragged object leaves the receiver View area. + +Event "drag-over-event" is generated at a certain interval (several times per second) while the dragged object is in the receiver View area. + +Example, changing the border color from gray to red when the dragged object is over the receiver area + + view.SetParams(rui.Params{ + rui.DragEnterEvent: func(view rui.View, event rui.DragAndDropEvent)) { + view.Set(rui.Border, rui.NewBorder(rui.Params{ + rui.Style: rui.SolidLine, + rui.ColorTag: rui.Red, + rui.Width: rui.Px(1), + })) + }, + rui.DragLeaveEvent: func(view rui.View, event rui.DragAndDropEvent)) { + view.Set(rui.Border, rui.NewBorder(rui.Params{ + rui.Style: rui.SolidLine, + rui.ColorTag: rui.Gray, + rui.Width: rui.Px(1), + })) + }, + }) + +You can get a list of listeners for these events using the function: + + func GetDragEnterEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) + func GetDragLeaveEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) + func GetDragOverEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) + ## ViewsContainer The ViewsContainer interface, which implements View, describes a container that contains diff --git a/dragAndDrop.go b/dragAndDrop.go index ae86a9e..92e2af6 100644 --- a/dragAndDrop.go +++ b/dragAndDrop.go @@ -400,20 +400,10 @@ func dragAndDropHtml(view View, buffer *strings.Builder) { viewEventsHtml[DragAndDropEvent](view, []PropertyName{DragEndEvent, DragEnterEvent, DragLeaveEvent}, buffer) } - session := view.Session() - if img, ok := stringProperty(view, DragImage, session); ok && img != "" { - img = strings.Trim(img, " \t") - if img[0] == '@' { - if img, ok = session.ImageConstant(img[1:]); ok { - buffer.WriteString(` data-drag-image="`) - buffer.WriteString(img) - buffer.WriteString(`" `) - } - } else { - buffer.WriteString(` data-drag-image="`) - buffer.WriteString(img) - buffer.WriteString(`" `) - } + if img := GetDragImage(view); img != "" { + buffer.WriteString(` data-drag-image="`) + buffer.WriteString(img) + buffer.WriteString(`" `) } if f := GetDragImageXOffset(view); f != 0 { @@ -492,13 +482,38 @@ func GetDragData(view View, subviewID ...string) map[string]string { return result } -// GetDragImageXOffset returns the horizontal offset in pixels within the drag feedback image.. +// GetDragImage returns the drag feedback image. +// If the second argument (subviewID) is not specified or it is "" 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 img != "" && img[0] == '@' { + if img, ok = view.Session().ImageConstant(img[1:]); ok { + return img + } + } else { + return img + } + } + } + } + return "" +} + +// GetDragImageXOffset returns the horizontal offset in pixels within the drag feedback image. // If the second argument (subviewID) is not specified or it is "" 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.. +// GetDragImageYOffset returns the vertical offset in pixels within the drag feedback image. // If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. func GetDragImageYOffset(view View, subviewID ...string) float64 { return floatStyledProperty(view, subviewID, DragImageYOffset, 0) From 3c3c09b043efc7e43addb05a080eff721d014a35 Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Tue, 17 Jun 2025 21:08:16 +0300 Subject: [PATCH 06/21] Added binding support --- animation.go | 6 +- animationEvents.go | 202 +++++++++--------- animationRun.go | 26 +-- background.go | 18 +- checkbox.go | 59 +++--- colorPicker.go | 8 +- columnLayout.go | 8 +- customView.go | 7 + dataList.go | 4 +- datePicker.go | 20 +- detailsView.go | 12 +- dragAndDrop.go | 114 +++++++--- dropDownList.go | 16 +- editView.go | 28 ++- events.go | 510 +++++++++++++++++++++++++++++++++------------ events1arg.go | 266 +++++++++++++++++++++++ filePicker.go | 27 ++- focusEvents.go | 8 +- gridLayout.go | 28 ++- keyEvents.go | 44 ++-- listLayout.go | 24 ++- listView.go | 109 +++++++--- mouseEvents.go | 122 ++++++++--- numberPicker.go | 24 ++- pointerEvents.go | 92 ++++++-- popup.go | 2 +- progressBar.go | 8 +- propertyNames.go | 2 + resizeEvent.go | 23 +- scrollEvent.go | 35 +++- stackLayout.go | 18 +- tableView.go | 73 +------ tableViewUtils.go | 74 +++++-- tabsLayout.go | 168 ++------------- textView.go | 4 +- timePicker.go | 20 +- touchEvents.go | 62 ++++-- transform.go | 36 +++- view.go | 46 ++-- viewUtils.go | 112 +++++++--- 40 files changed, 1674 insertions(+), 791 deletions(-) create mode 100644 events1arg.go diff --git a/animation.go b/animation.go index 122692b..2adece7 100644 --- a/animation.go +++ b/animation.go @@ -209,7 +209,7 @@ type animationData struct { usageCounter int view View listener func(view View, animation AnimationProperty, event PropertyName) - oldListeners map[PropertyName][]func(View, PropertyName) + oldListeners map[PropertyName][]oneArgListener[View, PropertyName] oldAnimation []AnimationProperty } @@ -1083,7 +1083,9 @@ func SetAnimated(rootView View, viewID string, tag PropertyName, value any, anim } // IsAnimationPaused returns "true" if an animation of the subview is paused, "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 IsAnimationPaused(view View, subviewID ...string) bool { return boolStyledProperty(view, subviewID, AnimationPaused, false) } diff --git a/animationEvents.go b/animationEvents.go index e785007..a5f00ac 100644 --- a/animationEvents.go +++ b/animationEvents.go @@ -156,45 +156,6 @@ const ( AnimationIterationEvent PropertyName = "animation-iteration-event" ) -/* -func setTransitionListener(properties Properties, tag PropertyName, value any) bool { - if listeners, ok := valueToOneArgEventListeners[View, string](value); ok { - if len(listeners) == 0 { - properties.setRaw(tag, nil) - } else { - properties.setRaw(tag, listeners) - } - return true - } - notCompatibleType(tag, value) - return false -} - -func (view *viewData) removeTransitionListener(tag PropertyName) { - delete(view.properties, tag) - if view.created { - if js, ok := eventJsFunc[tag]; ok { - view.session.removeProperty(view.htmlID(), js.jsEvent) - } - } -} - -func transitionEventsHtml(view View, buffer *strings.Builder) { - for _, tag := range []PropertyName{TransitionRunEvent, TransitionStartEvent, TransitionEndEvent, TransitionCancelEvent} { - if value := view.getRaw(tag); value != nil { - if js, ok := eventJsFunc[tag]; ok { - if listeners, ok := value.([]func(View, string)); ok && len(listeners) > 0 { - buffer.WriteString(js.jsEvent) - buffer.WriteString(`="`) - buffer.WriteString(js.jsFunc) - buffer.WriteString(`(this, event)" `) - } - } - } - } -} -*/ - func (view *viewData) handleTransitionEvents(tag PropertyName, data DataObject) { if propertyName, ok := data.PropertyValue("property"); ok { property := PropertyName(propertyName) @@ -208,50 +169,11 @@ func (view *viewData) handleTransitionEvents(tag PropertyName, data DataObject) } for _, listener := range getOneArgEventListeners[View, PropertyName](view, nil, tag) { - listener(view, property) + listener.Run(view, property) } } } -/* - func setAnimationListener(properties Properties, tag PropertyName, value any) bool { - if listeners, ok := valueToOneArgEventListeners[View, string](value); ok { - if len(listeners) == 0 { - properties.setRaw(tag, nil) - } else { - properties.setRaw(tag, listeners) - } - return true - } - notCompatibleType(tag, value) - return false - } - -func (view *viewData) removeAnimationListener(tag PropertyName) { - delete(view.properties, tag) - if view.created { - if js, ok := eventJsFunc[tag]; ok { - view.session.removeProperty(view.htmlID(), js.jsEvent) - } - } -} - -func animationEventsHtml(view View, buffer *strings.Builder) { - for _, tag := range []PropertyName{AnimationStartEvent, AnimationEndEvent, AnimationIterationEvent, AnimationCancelEvent} { - if value := view.getRaw(tag); value != nil { - if js, ok := eventJsFunc[tag]; ok { - if listeners, ok := value.([]func(View, string)); ok && len(listeners) > 0 { - buffer.WriteString(js.jsEvent) - buffer.WriteString(`="`) - buffer.WriteString(js.jsFunc) - buffer.WriteString(`(this, event)" `) - } - } - } - } -} -*/ - func (view *viewData) handleAnimationEvents(tag PropertyName, data DataObject) { if listeners := getOneArgEventListeners[View, string](view, nil, tag); len(listeners) > 0 { id := "" @@ -263,63 +185,135 @@ func (view *viewData) handleAnimationEvents(tag PropertyName, data DataObject) { } } for _, listener := range listeners { - listener(view, id) + listener.Run(view, id) } } } // GetTransitionRunListeners returns the "transition-run-event" listener list. // If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetTransitionRunListeners(view View, subviewID ...string) []func(View, string) { - return getOneArgEventListeners[View, string](view, subviewID, TransitionRunEvent) +// +// Result elements can be of the following types: +// - func(rui.View, string), +// - 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. // If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetTransitionStartListeners(view View, subviewID ...string) []func(View, string) { - return getOneArgEventListeners[View, string](view, subviewID, TransitionStartEvent) +// +// Result elements can be of the following types: +// - func(rui.View, string), +// - 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. // If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetTransitionEndListeners(view View, subviewID ...string) []func(View, string) { - return getOneArgEventListeners[View, string](view, subviewID, TransitionEndEvent) +// +// Result elements can be of the following types: +// - func(rui.View, string), +// - 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. // If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetTransitionCancelListeners(view View, subviewID ...string) []func(View, string) { - return getOneArgEventListeners[View, string](view, subviewID, TransitionCancelEvent) +// +// Result elements can be of the following types: +// - func(rui.View, string), +// - 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. // If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetAnimationStartListeners(view View, subviewID ...string) []func(View, string) { - return getOneArgEventListeners[View, string](view, subviewID, AnimationStartEvent) +// +// Result elements can be of the following types: +// - func(rui.View, string), +// - 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. // If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetAnimationEndListeners(view View, subviewID ...string) []func(View, string) { - return getOneArgEventListeners[View, string](view, subviewID, AnimationEndEvent) +// +// Result elements can be of the following types: +// - func(rui.View, string), +// - 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. // If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetAnimationCancelListeners(view View, subviewID ...string) []func(View, string) { - return getOneArgEventListeners[View, string](view, subviewID, AnimationCancelEvent) +// +// Result elements can be of the following types: +// - func(rui.View, string), +// - 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. // If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetAnimationIterationListeners(view View, subviewID ...string) []func(View, string) { - return getOneArgEventListeners[View, string](view, subviewID, AnimationIterationEvent) +// +// Result elements can be of the following types: +// - func(rui.View, string), +// - 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) } diff --git a/animationRun.go b/animationRun.go index e9f1eeb..64c3c3d 100644 --- a/animationRun.go +++ b/animationRun.go @@ -1,5 +1,7 @@ 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") @@ -13,28 +15,22 @@ func (animation *animationData) Start(view View, listener func(view View, animat 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][]func(View, PropertyName){} + animation.oldListeners = map[PropertyName][]oneArgListener[View, PropertyName]{} setListeners := func(event PropertyName, listener func(View, PropertyName)) { - var listeners []func(View, PropertyName) = nil - if value := view.Get(event); value != nil { - if oldListeners, ok := value.([]func(View, PropertyName)); ok && len(oldListeners) > 0 { - listeners = oldListeners - } - } - - if listeners == nil { - view.Set(event, listener) - } else { - animation.oldListeners[event] = listeners - view.Set(event, append(listeners, listener)) + 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) @@ -49,7 +45,7 @@ func (animation *animationData) Start(view View, listener func(view View, animat func (animation *animationData) finish() { if animation.view != nil { for _, event := range []PropertyName{AnimationStartEvent, AnimationEndEvent, AnimationCancelEvent, AnimationIterationEvent} { - if listeners, ok := animation.oldListeners[event]; ok { + if listeners, ok := animation.oldListeners[event]; ok && len(listeners) > 0 { animation.view.Set(event, listeners) } else { animation.view.Remove(event) @@ -63,7 +59,7 @@ func (animation *animationData) finish() { animation.view.Set(Animation, "") } - animation.oldListeners = map[PropertyName][]func(View, PropertyName){} + animation.oldListeners = map[PropertyName][]oneArgListener[View, PropertyName]{} animation.view = nil animation.listener = nil diff --git a/background.go b/background.go index 79a4c2f..d55d12d 100644 --- a/background.go +++ b/background.go @@ -268,14 +268,16 @@ func backgroundStyledPropery(view View, subviewID []string, tag PropertyName) [] // GetBackground returns the view background. // -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// 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. // -// 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 GetMask(view View, subviewID ...string) []BackgroundElement { return backgroundStyledPropery(view, subviewID, Mask) } @@ -284,7 +286,8 @@ func GetMask(view View, subviewID ...string) []BackgroundElement { // // BorderBox (0), PaddingBox (1), ContentBox (2) // -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// 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) } @@ -293,7 +296,8 @@ func GetBackgroundClip(view View, subviewID ...string) int { // // BorderBox (0), PaddingBox (1), ContentBox (2) // -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// 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) } @@ -302,7 +306,8 @@ func GetBackgroundOrigin(view View, subviewID ...string) int { // // BorderBox (0), PaddingBox (1), ContentBox (2) // -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// 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) } @@ -311,7 +316,8 @@ func GetMaskClip(view View, subviewID ...string) int { // // BorderBox (0), PaddingBox (1), ContentBox (2) // -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// 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) } diff --git a/checkbox.go b/checkbox.go index ce13e1d..2909158 100644 --- a/checkbox.go +++ b/checkbox.go @@ -53,8 +53,8 @@ func (button *checkboxData) init(session Session) { button.remove = button.removeFunc button.changed = button.propertyChanged - button.setRaw(ClickEvent, []func(View, MouseEvent){checkboxClickListener}) - button.setRaw(KeyDownEvent, []func(View, KeyEvent){checkboxKeyListener}) + button.setRaw(ClickEvent, []oneArgListener[View, MouseEvent]{newOneArgListenerVE(checkboxClickListener)}) + button.setRaw(KeyDownEvent, []oneArgListener[View, KeyEvent]{newOneArgListenerVE(checkboxKeyListener)}) } func (button *checkboxData) Focusable() bool { @@ -67,9 +67,9 @@ func (button *checkboxData) propertyChanged(tag PropertyName) { case Checked: session := button.Session() checked := IsCheckboxChecked(button) - if listeners := GetCheckboxChangedListeners(button); len(listeners) > 0 { + if listeners := getOneArgEventListeners[Checkbox, bool](button, nil, CheckboxChangedEvent); len(listeners) > 0 { for _, listener := range listeners { - listener(button, checked) + listener.Run(button, checked) } } @@ -103,7 +103,7 @@ 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, checkboxClickListener) + listeners = append(listeners, newOneArgListenerVE(checkboxClickListener)) button.setRaw(tag, listeners) return []PropertyName{tag} } @@ -111,7 +111,7 @@ func (button *checkboxData) setFunc(tag PropertyName, value any) []PropertyName case KeyDownEvent: if listeners, ok := valueToOneArgEventListeners[View, KeyEvent](value); ok && listeners != nil { - listeners = append(listeners, checkboxKeyListener) + listeners = append(listeners, newOneArgListenerVE(checkboxKeyListener)) button.setRaw(tag, listeners) return []PropertyName{tag} } @@ -134,31 +134,17 @@ func (button *checkboxData) setFunc(tag PropertyName, value any) []PropertyName func (button *checkboxData) removeFunc(tag PropertyName) []PropertyName { switch tag { case ClickEvent: - button.setRaw(ClickEvent, []func(View, MouseEvent){checkboxClickListener}) + button.setRaw(ClickEvent, []oneArgListener[View, MouseEvent]{newOneArgListenerVE(checkboxClickListener)}) return []PropertyName{ClickEvent} case KeyDownEvent: - button.setRaw(KeyDownEvent, []func(View, KeyEvent){checkboxKeyListener}) + button.setRaw(KeyDownEvent, []oneArgListener[View, KeyEvent]{newOneArgListenerVE(checkboxKeyListener)}) return []PropertyName{ClickEvent} } return button.viewsContainerData.removeFunc(tag) } -/* -func (button *checkboxData) changedCheckboxState(state bool) { - for _, listener := range GetCheckboxChangedListeners(button) { - listener(button, state) - } - - buffer := allocStringBuilder() - defer freeStringBuilder(buffer) - - button.htmlCheckbox(buffer, state) - button.Session().updateInnerHTML(button.htmlID()+"checkbox", buffer.String()) -} -*/ - func checkboxClickListener(view View, _ MouseEvent) { view.Set(Checked, !IsCheckboxChecked(view)) BlurView(view) @@ -302,26 +288,41 @@ func checkboxVerticalAlignCSS(view View) string { } // 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 { return boolStyledProperty(view, subviewID, Checked, false) } // 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 { return enumStyledProperty(view, subviewID, CheckboxVerticalAlign, LeftAlign, false) } // 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 { return enumStyledProperty(view, subviewID, CheckboxHorizontalAlign, TopAlign, false) } // GetCheckboxChangedListeners returns the CheckboxChangedListener list of an Checkbox subview. -// If there are no listeners then the empty list is returned -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetCheckboxChangedListeners(view View, subviewID ...string) []func(Checkbox, bool) { - return getOneArgEventListeners[Checkbox, bool](view, subviewID, CheckboxChangedEvent) +// 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) } diff --git a/colorPicker.go b/colorPicker.go index b1016f5..f205094 100644 --- a/colorPicker.go +++ b/colorPicker.go @@ -172,7 +172,9 @@ func (picker *colorPickerData) handleCommand(self View, command PropertyName, da } // 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 { if view = getSubview(view, subviewID); view != nil { if value, ok := colorProperty(view, ColorPickerValue, view.Session()); ok { @@ -191,7 +193,9 @@ func GetColorPickerValue(view View, subviewID ...string) Color { // GetColorChangedListeners returns the ColorChangedListener list of an ColorPicker subview. // If there are no listeners then the empty list is returned -// If the second argument (subviewID) is 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 GetColorChangedListeners(view View, subviewID ...string) []func(ColorPicker, Color, Color) { return getTwoArgEventListeners[ColorPicker, Color](view, subviewID, ColorChangedEvent) } diff --git a/columnLayout.go b/columnLayout.go index 2a0a64a..9cfad54 100644 --- a/columnLayout.go +++ b/columnLayout.go @@ -249,13 +249,17 @@ func GetColumnSeparatorColor(view View, subviewID ...string) Color { // GetColumnFill returns a "column-fill" property value of the subview. // Returns one of next values: ColumnFillBalance (0) or ColumnFillAuto (1) -// 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 GetColumnFill(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, ColumnFill, ColumnFillBalance, true) } // IsColumnSpanAll returns a "column-span-all" property value of the 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 IsColumnSpanAll(view View, subviewID ...string) bool { return boolStyledProperty(view, subviewID, ColumnSpanAll, false) } diff --git a/customView.go b/customView.go index 5523e8a..35411d6 100644 --- a/customView.go +++ b/customView.go @@ -352,3 +352,10 @@ func (customView *CustomViewData) LoadFile(file FileInfo, result func(FileInfo, customView.superView.LoadFile(file, result) } } + +func (customView *CustomViewData) binding() any { + if customView.superView != nil { + return customView.superView.binding() + } + return nil +} diff --git a/dataList.go b/dataList.go index 2c1bb11..0efdd56 100644 --- a/dataList.go +++ b/dataList.go @@ -312,7 +312,9 @@ func dataListHtmlProperties(view View, buffer *strings.Builder) { } // GetDataList returns the data list of an editor. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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) diff --git a/datePicker.go b/datePicker.go index bff18c6..54da1c4 100644 --- a/datePicker.go +++ b/datePicker.go @@ -400,7 +400,9 @@ func getDateProperty(view View, mainTag, shortTag PropertyName) (time.Time, bool // GetDatePickerMin returns the min date of DatePicker subview and "true" as the second value if the min date is set, // "false" as the second value otherwise. -// If the second argument (subviewID) is 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) { if view = getSubview(view, subviewID); view != nil { return getDateProperty(view, DatePickerMin, Min) @@ -410,7 +412,9 @@ 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, // "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) { if view = getSubview(view, subviewID); view != nil { return getDateProperty(view, DatePickerMax, Max) @@ -419,13 +423,17 @@ func GetDatePickerMax(view View, subviewID ...string) (time.Time, bool) { } // 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 { return intStyledProperty(view, subviewID, DatePickerStep, 0) } // 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 { if view = getSubview(view, subviewID); view == nil { return time.Now() @@ -436,7 +444,9 @@ func GetDatePickerValue(view View, subviewID ...string) time.Time { // GetDateChangedListeners returns the DateChangedListener list of an DatePicker subview. // If there are no listeners then the empty list is returned -// If the second argument (subviewID) is 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 GetDateChangedListeners(view View, subviewID ...string) []func(DatePicker, time.Time, time.Time) { return getTwoArgEventListeners[DatePicker, time.Time](view, subviewID, DateChangedEvent) } diff --git a/detailsView.go b/detailsView.go index c4b9401..2296083 100644 --- a/detailsView.go +++ b/detailsView.go @@ -202,7 +202,9 @@ func (detailsView *detailsViewData) handleCommand(self View, command PropertyNam } // 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 { if view = getSubview(view, subviewID); view != nil { if value := view.Get(Summary); value != nil { @@ -219,13 +221,17 @@ func GetDetailsSummary(view View, subviewID ...string) View { } // 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 { return boolStyledProperty(view, subviewID, Expanded, false) } // IsDetailsExpanded returns a value of the HideSummaryMarker property of DetailsView. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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) } diff --git a/dragAndDrop.go b/dragAndDrop.go index 92e2af6..f0fa70e 100644 --- a/dragAndDrop.go +++ b/dragAndDrop.go @@ -336,7 +336,7 @@ func handleDragAndDropEvents(view View, tag PropertyName, data DataObject) { event.init(view.Session(), data) for _, listener := range listeners { - listener(view, event) + listener.Run(view, event) } } } @@ -434,41 +434,99 @@ func (view *viewData) LoadFile(file FileInfo, result func(FileInfo, []byte)) { } // GetDragStartEventListeners returns the "drag-start-event" listener list. If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetDragStartEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) { - return getOneArgEventListeners[View, DragAndDropEvent](view, subviewID, DragStartEvent) +// +// 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetDragEndEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) { - return getOneArgEventListeners[View, DragAndDropEvent](view, subviewID, DragEndEvent) +// +// 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetDragEnterEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) { - return getOneArgEventListeners[View, DragAndDropEvent](view, subviewID, DragEnterEvent) +// +// 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetDragLeaveEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) { - return getOneArgEventListeners[View, DragAndDropEvent](view, subviewID, DragLeaveEvent) +// +// 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetDragOverEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) { - return getOneArgEventListeners[View, DragAndDropEvent](view, subviewID, DragOverEvent) +// +// 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetDropEventListeners(view View, subviewID ...string) []func(View, DragAndDropEvent) { - return getOneArgEventListeners[View, DragAndDropEvent](view, subviewID, DropEvent) +// +// 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 { @@ -483,7 +541,9 @@ func GetDragData(view View, subviewID ...string) map[string]string { } // GetDragImage returns the drag feedback image. -// 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 GetDragImage(view View, subviewID ...string) string { if view = getSubview(view, subviewID); view != nil { value := view.getRaw(DragImage) @@ -508,13 +568,17 @@ func GetDragImage(view View, subviewID ...string) string { } // GetDragImageXOffset returns the horizontal offset in pixels within the drag feedback image. -// 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 GetDragImageXOffset(view View, subviewID ...string) float64 { return floatStyledProperty(view, subviewID, DragImageXOffset, 0) } // GetDragImageYOffset returns the vertical offset in pixels within the drag feedback image. -// 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 GetDragImageYOffset(view View, subviewID ...string) float64 { return floatStyledProperty(view, subviewID, DragImageYOffset, 0) } @@ -529,7 +593,8 @@ func GetDragImageYOffset(view View, subviewID ...string) float64 { // - 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. // -// 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 GetDropEffect(view View, subviewID ...string) int { if view = getSubview(view, subviewID); view != nil { value := view.getRaw(DropEffect) @@ -573,7 +638,8 @@ func GetDropEffect(view View, subviewID ...string) int { // - 6 (DropEffectLinkMove) - A link or move operation is permitted. // - 7 (DropEffectAll) - All operations are permitted. // -// 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 GetDropEffectAllowed(view View, subviewID ...string) int { if view = getSubview(view, subviewID); view != nil { value := view.getRaw(DropEffectAllowed) diff --git a/dropDownList.go b/dropDownList.go index f7fd3e6..830a655 100644 --- a/dropDownList.go +++ b/dropDownList.go @@ -264,13 +264,17 @@ 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. -// 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 GetDropDownListeners(view View, subviewID ...string) []func(DropDownList, int, int) { return getTwoArgEventListeners[DropDownList, int](view, subviewID, DropDownEvent) } // GetDropDownItems return the DropDownList items list. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 { if view = getSubview(view, subviewID); view != nil { if value := view.Get(Items); value != nil { @@ -313,14 +317,18 @@ func getIndicesArray(view View, tag PropertyName) []int { } // GetDropDownDisabledItems return an array of disabled(non selectable) items indices of DropDownList. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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. -// 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 GetDropDownItemSeparators(view View, subviewID ...string) []int { view = getSubview(view, subviewID) return getIndicesArray(view, ItemSeparators) diff --git a/editView.go b/editView.go index c65a783..ece94fe 100644 --- a/editView.go +++ b/editView.go @@ -451,26 +451,34 @@ func IsReadOnly(view View, subviewID ...string) bool { } // 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 { return boolStyledProperty(view, subviewID, Spellcheck, false) } // GetTextChangedListeners returns the TextChangedListener list of an EditView or MultiLineEditView subview. // If there are no listeners then the empty list is returned -// If the second argument (subviewID) is 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 GetTextChangedListeners(view View, subviewID ...string) []func(EditView, string, string) { return getTwoArgEventListeners[EditView, string](view, subviewID, EditTextChangedEvent) } // 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 { return enumStyledProperty(view, subviewID, EditViewType, SingleLineText, false) } // 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 { if view = getSubview(view, subviewID); view != nil { if pattern, ok := stringProperty(view, EditViewPattern, view.Session()); ok { @@ -488,13 +496,17 @@ func GetEditViewPattern(view View, subviewID ...string) string { } // 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 { return boolStyledProperty(view, subviewID, EditWrap, false) } // 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) { if subviewID != "" { if edit := EditViewByID(view, subviewID); edit != nil { @@ -509,7 +521,9 @@ func AppendEditText(view View, subviewID string, text string) { } // GetCaretColor returns the color of the text input caret. -// 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 { return colorStyledProperty(view, subviewID, CaretColor, false) } diff --git a/events.go b/events.go index 84c344b..f663c47 100644 --- a/events.go +++ b/events.go @@ -1,6 +1,9 @@ package rui -import "strings" +import ( + "reflect" + "strings" +) var eventJsFunc = map[PropertyName]struct{ jsEvent, jsFunc string }{ FocusEvent: {jsEvent: "onfocus", jsFunc: "focusEvent"}, @@ -38,12 +41,203 @@ var eventJsFunc = map[PropertyName]struct{ jsEvent, jsFunc string }{ DragLeaveEvent: {jsEvent: "ondragleave", jsFunc: "dragLeaveEvent"}, } -func valueToNoArgEventListeners[V any](value any) ([]func(V), bool) { +/* +type oneArgListener[V View, E any] interface { + call(V, E) + listener() func(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]) call(_ V, _ E) { + data.fn() +} + +func (data *oneArgListener0[V, E]) listener() func(V, E) { + return data.call +} + +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]) call(view V, _ E) { + data.fn(view) +} + +func (data *oneArgListenerV[V, E]) listener() func(V, E) { + return data.call +} + +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]) call(_ V, event E) { + data.fn(event) +} + +func (data *oneArgListenerE[V, E]) listener() func(V, E) { + return data.call +} + +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]) call(view V, arg E) { + data.fn(view, arg) +} + +func (data *oneArgListenerVE[V, E]) listener() func(V, E) { + return data.fn +} + +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]) call(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 inType == reflect.TypeOf(view) { + args = []reflect.Value{reflect.ValueOf(view)} + } else if inType == reflect.TypeOf(event) { + args = []reflect.Value{reflect.ValueOf(event)} + } + + case 2: + if methodType.In(0) == reflect.TypeOf(view) && 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]) listener() func(V, E) { + return data.call +} + +func (data *oneArgListenerBinding[V, E]) rawListener() any { + return data.name +} +*/ + +func valueToNoArgEventListeners[V any](view View, value any) ([]func(V), bool) { if value == nil { return nil, true } switch value := value.(type) { + case string: + fn := func(arg V) { + bind := view.binding() + if bind == nil { + ErrorLogF(`There is no a binding object for call "%s"`, value) + return + } + + val := reflect.ValueOf(bind) + method := val.MethodByName(value) + if !method.IsValid() { + ErrorLogF(`The "%s" method is not valid`, value) + return + } + + methodType := method.Type() + var args []reflect.Value = nil + switch methodType.NumIn() { + case 0: + args = []reflect.Value{} + + case 1: + inType := methodType.In(0) + if inType == reflect.TypeOf(arg) { + args = []reflect.Value{reflect.ValueOf(arg)} + } + } + + if args != nil { + method.Call(args) + } else { + ErrorLogF(`Unsupported prototype of "%s" method`, value) + } + } + return []func(V){fn}, true + case func(V): return []func(V){value}, true @@ -109,137 +303,153 @@ func valueToNoArgEventListeners[V any](value any) ([]func(V), bool) { return nil, false } -func valueToOneArgEventListeners[V View, E any](value any) ([]func(V, E), bool) { +/* + func valueToOneArgEventListeners[V View, E any](view View, value any) ([]oneArgListener[V, E], bool) { + if value == nil { + return nil, true + } + + switch value := value.(type) { + case string: + return []oneArgListener[V, E]{newOneArgListenerBinding[V, E](value)}, true + + case func(V, E): + return []oneArgListener[V, E]{newOneArgListenerVE[V, E](value)}, true + + case func(V): + return []oneArgListener[V, E]{newOneArgListenerV[V, E](value)}, true + + case func(E): + return []oneArgListener[V, E]{newOneArgListenerE[V, E](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[V, E](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, E](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, E](v)) + + case func(E): + result = append(result, newOneArgListenerE[V, E](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 valueToTwoArgEventListeners[V View, E any](view View, value any) ([]func(V, E, E), bool) { if value == nil { return nil, true } switch value := value.(type) { - case func(V, E): - return []func(V, E){value}, true - - case func(E): - fn := func(_ V, event E) { - value(event) - } - return []func(V, E){fn}, true - - case func(V): - fn := func(view V, _ E) { - value(view) - } - return []func(V, E){fn}, true - - case func(): - fn := func(V, E) { - value() - } - return []func(V, E){fn}, true - - case []func(V, E): - if len(value) == 0 { - return nil, true - } - for _, fn := range value { - if fn == nil { - return nil, false + case string: + fn := func(view V, val1 E, val2 E) { + bind := view.binding() + if bind == nil { + ErrorLogF(`There is no a binding object for call "%s"`, value) + return } - } - 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 + val := reflect.ValueOf(bind) + method := val.MethodByName(value) + if !method.IsValid() { + ErrorLogF(`The "%s" method is not valid`, value) + return } - 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 + methodType := method.Type() + var args []reflect.Value = nil + switch methodType.NumIn() { + case 0: + args = []reflect.Value{} - 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 1: + inType := methodType.In(0) + if inType == reflect.TypeOf(view) { + args = []reflect.Value{reflect.ValueOf(view)} + } else if inType == reflect.TypeOf(val1) { + args = []reflect.Value{reflect.ValueOf(val1)} } - case func(V): - listeners[i] = func(view V, _ E) { - v(view) + case 2: + valType := reflect.TypeOf(val1) + if methodType.In(0) == reflect.TypeOf(view) && methodType.In(1) == valType { + args = []reflect.Value{reflect.ValueOf(view), reflect.ValueOf(val1)} + } else if methodType.In(0) == valType && methodType.In(1) == valType { + args = []reflect.Value{reflect.ValueOf(val1), reflect.ValueOf(val2)} } - case func(): - listeners[i] = func(V, E) { - v() + case 3: + valType := reflect.TypeOf(val1) + if methodType.In(0) == reflect.TypeOf(view) && methodType.In(1) == valType && methodType.In(2) == valType { + args = []reflect.Value{reflect.ValueOf(view), reflect.ValueOf(val1), reflect.ValueOf(val2)} } + } - default: - return nil, false + if args != nil { + method.Call(args) + } else { + ErrorLogF(`Unsupported prototype of "%s" method`, value) } } - return listeners, true - } + return []func(V, E, E){fn}, true - return nil, false -} - -func valueToTwoArgEventListeners[V View, E any](value any) ([]func(V, E, E), bool) { - if value == nil { - return nil, true - } - - switch value := value.(type) { case func(V, E, E): return []func(V, E, E){value}, true @@ -424,17 +634,27 @@ func getNoArgEventListeners[V View](view View, subviewID []string, tag PropertyN return []func(V){} } -func getOneArgEventListeners[V View, E any](view View, subviewID []string, tag PropertyName) []func(V, E) { - if view = getSubview(view, subviewID); view != nil { - if value := view.Get(tag); value != nil { - if result, ok := value.([]func(V, E)); ok { - return result +/* + 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.Get(tag); value != nil { + if result, ok := value.([]oneArgListener[V, E]); ok { + return result + } } } + return []oneArgListener[V, E]{} } - return []func(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 getTwoArgEventListeners[V View, E any](view View, subviewID []string, tag PropertyName) []func(V, E, E) { if view = getSubview(view, subviewID); view != nil { if value := view.Get(tag); value != nil { @@ -446,12 +666,12 @@ func getTwoArgEventListeners[V View, E any](view View, subviewID []string, tag P return []func(V, E, E){} } -func setNoArgEventListener[V View](properties Properties, tag PropertyName, value any) []PropertyName { - if listeners, ok := valueToNoArgEventListeners[V](value); ok { +func setNoArgEventListener[V View](view View, tag PropertyName, value any) []PropertyName { + if listeners, ok := valueToNoArgEventListeners[V](view, value); ok { if len(listeners) > 0 { - properties.setRaw(tag, listeners) - } else if properties.getRaw(tag) != nil { - properties.setRaw(tag, nil) + view.setRaw(tag, listeners) + } else if view.getRaw(tag) != nil { + view.setRaw(tag, nil) } else { return []PropertyName{} } @@ -461,12 +681,13 @@ func setNoArgEventListener[V View](properties Properties, tag PropertyName, valu return nil } -func setOneArgEventListener[V View, T any](properties Properties, tag PropertyName, value any) []PropertyName { - if listeners, ok := valueToOneArgEventListeners[V, T](value); ok { +/* +func setOneArgEventListener[V View, T any](view View, tag PropertyName, value any) []PropertyName { + if listeners, ok := valueToOneArgEventListeners[V, T](view, value); ok { if len(listeners) > 0 { - properties.setRaw(tag, listeners) - } else if properties.getRaw(tag) != nil { - properties.setRaw(tag, nil) + view.setRaw(tag, listeners) + } else if view.getRaw(tag) != nil { + view.setRaw(tag, nil) } else { return []PropertyName{} } @@ -475,16 +696,18 @@ func setOneArgEventListener[V View, T any](properties Properties, tag PropertyNa notCompatibleType(tag, value) return nil } +*/ -func setTwoArgEventListener[V View, T any](properties Properties, tag PropertyName, value any) []PropertyName { - listeners, ok := valueToTwoArgEventListeners[V, T](value) +func setTwoArgEventListener[V View, T any](view View, tag PropertyName, value any) []PropertyName { + listeners, ok := valueToTwoArgEventListeners[V, T](view, value) if !ok { notCompatibleType(tag, value) return nil - } else if len(listeners) > 0 { - properties.setRaw(tag, listeners) - } else if properties.getRaw(tag) != nil { - properties.setRaw(tag, nil) + } + if len(listeners) > 0 { + view.setRaw(tag, listeners) + } else if view.getRaw(tag) != nil { + view.setRaw(tag, nil) } else { return []PropertyName{} } @@ -493,9 +716,17 @@ func setTwoArgEventListener[V View, T any](properties Properties, tag PropertyNa func viewEventsHtml[T any](view View, events []PropertyName, buffer *strings.Builder) { for _, tag := range events { - if value := view.getRaw(tag); value != nil { + if js, ok := eventJsFunc[tag]; ok { + if value := getOneArgEventListeners[View, T](view, nil, tag); len(value) > 0 { + buffer.WriteString(js.jsEvent) + buffer.WriteString(`="`) + buffer.WriteString(js.jsFunc) + buffer.WriteString(`(this, event)" `) + } + } + /*if value := view.getRaw(tag); value != nil { if js, ok := eventJsFunc[tag]; ok { - if listeners, ok := value.([]func(View, T)); ok && len(listeners) > 0 { + if listeners, ok := value.([] func(View, T)); ok && len(listeners) > 0 { buffer.WriteString(js.jsEvent) buffer.WriteString(`="`) buffer.WriteString(js.jsFunc) @@ -503,6 +734,7 @@ func viewEventsHtml[T any](view View, events []PropertyName, buffer *strings.Bui } } } + */ } } diff --git a/events1arg.go b/events1arg.go new file mode 100644 index 0000000..77004b3 --- /dev/null +++ b/events1arg.go @@ -0,0 +1,266 @@ +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 inType == reflect.TypeOf(view) { + args = []reflect.Value{reflect.ValueOf(view)} + } else if inType == reflect.TypeOf(event) { + args = []reflect.Value{reflect.ValueOf(event)} + } + + case 2: + if methodType.In(0) == reflect.TypeOf(view) && 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 { + if len(listeners) > 0 { + view.setRaw(tag, listeners) + } else if view.getRaw(tag) != nil { + view.setRaw(tag, nil) + } else { + return []PropertyName{} + } + return []PropertyName{tag} + } + 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.Get(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 +} diff --git a/filePicker.go b/filePicker.go index 8c8b0b6..9735497 100644 --- a/filePicker.go +++ b/filePicker.go @@ -282,8 +282,8 @@ func (picker *filePickerData) handleCommand(self View, command PropertyName, dat case "fileSelected": if files := parseFilesTag(data); files != nil { picker.files = files - for _, listener := range GetFileSelectedListeners(picker) { - listener(picker, files) + for _, listener := range getOneArgEventListeners[FilePicker, []FileInfo](picker, nil, FileSelectedEvent) { + listener.Run(picker, files) } } return true @@ -317,13 +317,17 @@ func LoadFilePickerFile(view View, subviewID string, file FileInfo, result func( } // 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 { return boolStyledProperty(view, subviewID, Multiple, false) } // 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 { if view = getSubview(view, subviewID); view != nil { accept, ok := stringProperty(view, Accept, view.Session()) @@ -345,7 +349,16 @@ func GetFilePickerAccept(view View, subviewID ...string) []string { // GetFileSelectedListeners returns the "file-selected-event" listener list. // If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetFileSelectedListeners(view View, subviewID ...string) []func(FilePicker, []FileInfo) { - return getOneArgEventListeners[FilePicker, []FileInfo](view, subviewID, FileSelectedEvent) +// +// Result elements can be of the following types: +// - func(rui.View, []rui.FileInfo), +// - 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) } diff --git a/focusEvents.go b/focusEvents.go index 281e128..e091cbc 100644 --- a/focusEvents.go +++ b/focusEvents.go @@ -49,13 +49,17 @@ func focusEventsHtml(view View, buffer *strings.Builder) { } // GetFocusListeners returns a FocusListener list. If there are no listeners then the empty list is returned -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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) []func(View) { return getNoArgEventListeners[View](view, subviewID, FocusEvent) } // GetLostFocusListeners returns a LostFocusListener list. If there are no listeners then the empty list is returned -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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) []func(View) { return getNoArgEventListeners[View](view, subviewID, LostFocusEvent) } diff --git a/gridLayout.go b/gridLayout.go index ff1474c..619d416 100644 --- a/gridLayout.go +++ b/gridLayout.go @@ -506,26 +506,34 @@ func gridCellSizes(properties Properties, tag PropertyName, session Session) []S } // 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 { 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) -// 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 { return enumStyledProperty(view, subviewID, CellHorizontalAlign, StretchAlign, false) } // 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 { 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. // 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 { if view = getSubview(view, subviewID); view != nil { return gridCellSizes(view, CellWidth, view.Session()) @@ -535,7 +543,9 @@ 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. // 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 { if view = getSubview(view, subviewID); view != nil { return gridCellSizes(view, CellHeight, view.Session()) @@ -544,13 +554,17 @@ func GetCellHeight(view View, subviewID ...string) []SizeUnit { } // 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 { return sizeStyledProperty(view, subviewID, GridRowGap, false) } // 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 { return sizeStyledProperty(view, subviewID, GridColumnGap, false) } diff --git a/keyEvents.go b/keyEvents.go index a26419f..22f7621 100644 --- a/keyEvents.go +++ b/keyEvents.go @@ -434,15 +434,13 @@ func (event *KeyEvent) init(data DataObject) { } func keyEventsHtml(view View, buffer *strings.Builder) { - if len(getOneArgEventListeners[View, KeyEvent](view, nil, KeyDownEvent)) > 0 { + if len(getOneArgEventListeners[View, KeyEvent](view, nil, KeyDownEvent)) > 0 || + (view.Focusable() && len(getOneArgEventListeners[View, MouseEvent](view, nil, ClickEvent)) > 0) { + buffer.WriteString(`onkeydown="keyDownEvent(this, event)" `) - } else if view.Focusable() { - if len(getOneArgEventListeners[View, MouseEvent](view, nil, ClickEvent)) > 0 { - buffer.WriteString(`onkeydown="keyDownEvent(this, event)" `) - } } - if listeners := getOneArgEventListeners[View, KeyEvent](view, nil, KeyUpEvent); len(listeners) > 0 { + if len(getOneArgEventListeners[View, KeyEvent](view, nil, KeyUpEvent)) > 0 { buffer.WriteString(`onkeyup="keyUpEvent(this, event)" `) } } @@ -454,7 +452,7 @@ func handleKeyEvents(view View, tag PropertyName, data DataObject) { if len(listeners) > 0 { for _, listener := range listeners { - listener(view, event) + listener.Run(view, event) } return } @@ -477,20 +475,38 @@ func handleKeyEvents(view View, tag PropertyName, data DataObject) { ScreenY: view.Frame().Top + view.Frame().Height/2, } for _, listener := range listeners { - listener(view, clickEvent) + listener.Run(view, clickEvent) } } } } // GetKeyDownListeners returns the "key-down-event" listener list. If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetKeyDownListeners(view View, subviewID ...string) []func(View, KeyEvent) { - return getOneArgEventListeners[View, KeyEvent](view, subviewID, KeyDownEvent) +// +// Result elements can be of the following types: +// - func(rui.View, rui.KeyEvent), +// - 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetKeyUpListeners(view View, subviewID ...string) []func(View, KeyEvent) { - return getOneArgEventListeners[View, KeyEvent](view, subviewID, KeyUpEvent) +// +// Result elements can be of the following types: +// - func(rui.View, rui.KeyEvent), +// - 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) } diff --git a/listLayout.go b/listLayout.go index 55acf8a..48366e8 100644 --- a/listLayout.go +++ b/listLayout.go @@ -205,21 +205,27 @@ func (listLayout *listLayoutData) UpdateContent() { // GetListVerticalAlign returns the vertical align of a ListLayout or ListView sibview: // TopAlign (0), BottomAlign (1), CenterAlign (2), or StretchAlign (3) -// If the second argument (subviewID) is 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 { return enumStyledProperty(view, subviewID, VerticalAlign, TopAlign, false) } // GetListHorizontalAlign returns the vertical align of a ListLayout or ListView subview: // LeftAlign (0), RightAlign (1), CenterAlign (2), or StretchAlign (3) -// If the second argument (subviewID) is 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 { return enumStyledProperty(view, subviewID, HorizontalAlign, LeftAlign, false) } // GetListOrientation returns the orientation of a ListLayout or ListView subview: // TopDownOrientation (0), StartToEndOrientation (1), BottomUpOrientation (2), or EndToStartOrientation (3) -// If the second argument (subviewID) is 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 { if view = getSubview(view, subviewID); view != nil { if orientation, ok := valueToOrientation(view.Get(Orientation), view.Session()); ok { @@ -238,19 +244,25 @@ func GetListOrientation(view View, subviewID ...string) int { // GetListWrap returns the wrap type of a ListLayout or ListView subview: // 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 { return enumStyledProperty(view, subviewID, ListWrap, ListWrapOff, false) } // 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 { return sizeStyledProperty(view, subviewID, ListRowGap, false) } // 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 { return sizeStyledProperty(view, subviewID, ListColumnGap, false) } diff --git a/listView.go b/listView.go index c90b392..e55d4a3 100644 --- a/listView.go +++ b/listView.go @@ -122,19 +122,13 @@ type ListView interface { // ReloadListViewData updates ListView content ReloadListViewData() - //getCheckedItems() []int getItemFrames() []Frame } type listViewData struct { viewData - //adapter ListAdapter - //clickedListeners []func(ListView, int) - //selectedListeners []func(ListView, int) - //checkedListeners []func(ListView, []int) items []View itemFrame []Frame - //checkedItem []int } // NewListView creates the new list view @@ -279,7 +273,7 @@ func (listView *listViewData) propertyChanged(tag PropertyName) { if listeners := getOneArgEventListeners[ListView, int](listView, nil, ListItemSelectedEvent); len(listeners) > 0 { current := GetCurrent(listView) for _, listener := range listeners { - listener(listView, current) + listener.Run(listView, current) } } @@ -288,7 +282,7 @@ func (listView *listViewData) propertyChanged(tag PropertyName) { if listeners := getOneArgEventListeners[ListView, []int](listView, nil, ListItemCheckedEvent); len(listeners) > 0 { checked := GetListViewCheckedItems(listView) for _, listener := range listeners { - listener(listView, checked) + listener.Run(listView, checked) } } @@ -966,7 +960,7 @@ func (listView *listViewData) handleCommand(self View, command PropertyName, dat func (listView *listViewData) handleCurrent(number int) { listView.properties[Current] = number for _, listener := range getOneArgEventListeners[ListView, int](listView, nil, ListItemSelectedEvent) { - listener(listView, number) + listener.Run(listView, number) } if listener, ok := listView.changeListener[Current]; ok { listener(listView, Current) @@ -1032,12 +1026,12 @@ func (listView *listViewData) onItemClick(number int) { } for _, listener := range getOneArgEventListeners[ListView, []int](listView, nil, ListItemCheckedEvent) { - listener(listView, checkedItem) + listener.Run(listView, checkedItem) } } for _, listener := range getOneArgEventListeners[ListView, int](listView, nil, ListItemClickedEvent) { - listener(listView, number) + listener.Run(listView, number) } } @@ -1054,58 +1048,97 @@ func (listView *listViewData) onItemResize(self View, index string, x, y, width, } // GetVerticalAlign return the vertical align of a list: 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 GetVerticalAlign(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, VerticalAlign, TopAlign, false) } // GetHorizontalAlign return the vertical align of a list/checkbox: 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 GetHorizontalAlign(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, HorizontalAlign, LeftAlign, false) } // GetListItemClickedListeners returns a ListItemClickedListener of the ListView. // If there are no listeners then the empty list is returned -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetListItemClickedListeners(view View, subviewID ...string) []func(ListView, int) { - return getOneArgEventListeners[ListView, int](view, subviewID, ListItemClickedEvent) +// +// Result elements can be of the following types: +// - func(rui.ListView, int), +// - func(rui.ListView), +// - 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 GetListItemClickedListeners(view View, subviewID ...string) []any { + return getOneArgEventRawListeners[ListView, int](view, subviewID, ListItemClickedEvent) } // GetListItemSelectedListeners returns a ListItemSelectedListener of the ListView. // If there are no listeners then the empty list is returned -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetListItemSelectedListeners(view View, subviewID ...string) []func(ListView, int) { - return getOneArgEventListeners[ListView, int](view, subviewID, ListItemSelectedEvent) +// +// Result elements can be of the following types: +// - func(rui.ListView, int), +// - func(rui.ListView), +// - 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 GetListItemSelectedListeners(view View, subviewID ...string) []any { + return getOneArgEventRawListeners[ListView, int](view, subviewID, ListItemSelectedEvent) } // GetListItemCheckedListeners returns a ListItemCheckedListener of the ListView. // If there are no listeners then the empty list is returned -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetListItemCheckedListeners(view View, subviewID ...string) []func(ListView, []int) { - return getOneArgEventListeners[ListView, []int](view, subviewID, ListItemCheckedEvent) +// +// Result elements can be of the following types: +// - func(rui.ListView, []int), +// - func(rui.ListView), +// - 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 GetListItemCheckedListeners(view View, subviewID ...string) []any { + return getOneArgEventRawListeners[ListView, []int](view, subviewID, ListItemCheckedEvent) } // GetListItemWidth returns the width of a ListView item. -// 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 GetListItemWidth(view View, subviewID ...string) SizeUnit { return sizeStyledProperty(view, subviewID, ItemWidth, false) } // GetListItemHeight returns the height of a ListView item. -// 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 GetListItemHeight(view View, subviewID ...string) SizeUnit { return sizeStyledProperty(view, subviewID, ItemHeight, false) } // GetListViewCheckbox returns the ListView checkbox type: NoneCheckbox (0), SingleCheckbox (1), or MultipleCheckbox (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 GetListViewCheckbox(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, ItemCheckbox, 0, false) } // GetListViewCheckedItems returns the array of ListView checked items. -// 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 GetListViewCheckedItems(view View, subviewID ...string) []int { if view = getSubview(view, subviewID); view != nil { if value := view.getRaw(Checked); value != nil { @@ -1138,34 +1171,44 @@ func IsListViewCheckedItem(view View, subviewID string, index int) bool { // GetListViewCheckboxVerticalAlign returns the vertical align of the ListView checkbox: // TopAlign (0), BottomAlign (1), CenterAlign (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 GetListViewCheckboxVerticalAlign(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, CheckboxVerticalAlign, TopAlign, false) } // GetListViewCheckboxHorizontalAlign returns the horizontal align of the ListView checkbox: // LeftAlign (0), RightAlign (1), CenterAlign (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 GetListViewCheckboxHorizontalAlign(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, CheckboxHorizontalAlign, LeftAlign, false) } // GetListItemVerticalAlign returns the vertical align of the ListView item content: // TopAlign (0), BottomAlign (1), CenterAlign (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 GetListItemVerticalAlign(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, ItemVerticalAlign, TopAlign, false) } // ItemHorizontalAlign returns the horizontal align of the ListView item 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 GetListItemHorizontalAlign(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, ItemHorizontalAlign, LeftAlign, false) } // GetListItemFrame - returns the location and size of the ListView item in pixels. -// 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 GetListItemFrame(view View, subviewID string, index int) Frame { if subviewID != "" { view = ViewByID(view, subviewID) @@ -1182,7 +1225,9 @@ func GetListItemFrame(view View, subviewID string, index int) Frame { } // GetListViewAdapter - returns the ListView adapter. -// 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 GetListViewAdapter(view View, subviewID ...string) ListAdapter { if view = getSubview(view, subviewID); view != nil { if value := view.Get(Items); value != nil { diff --git a/mouseEvents.go b/mouseEvents.go index a184483..6d53e42 100644 --- a/mouseEvents.go +++ b/mouseEvents.go @@ -280,56 +280,128 @@ func handleMouseEvents(view View, tag PropertyName, data DataObject) { event.init(data) for _, listener := range listeners { - listener(view, event) + listener.Run(view, event) } } } // GetClickListeners returns the "click-event" listener list. If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetClickListeners(view View, subviewID ...string) []func(View, MouseEvent) { - return getOneArgEventListeners[View, MouseEvent](view, subviewID, ClickEvent) +// +// Result elements can be of the following types: +// - func(View, MouseEvent), +// - 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetDoubleClickListeners(view View, subviewID ...string) []func(View, MouseEvent) { - return getOneArgEventListeners[View, MouseEvent](view, subviewID, DoubleClickEvent) +// +// Result elements can be of the following types: +// - func(View, MouseEvent), +// - 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. // If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetContextMenuListeners(view View, subviewID ...string) []func(View, MouseEvent) { - return getOneArgEventListeners[View, MouseEvent](view, subviewID, ContextMenuEvent) +// +// Result elements can be of the following types: +// - func(View, MouseEvent), +// - 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetMouseDownListeners(view View, subviewID ...string) []func(View, MouseEvent) { - return getOneArgEventListeners[View, MouseEvent](view, subviewID, MouseDown) +// +// Result elements can be of the following types: +// - func(View, MouseEvent), +// - 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetMouseUpListeners(view View, subviewID ...string) []func(View, MouseEvent) { - return getOneArgEventListeners[View, MouseEvent](view, subviewID, MouseUp) +// +// Result elements can be of the following types: +// - func(View, MouseEvent), +// - 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetMouseMoveListeners(view View, subviewID ...string) []func(View, MouseEvent) { - return getOneArgEventListeners[View, MouseEvent](view, subviewID, MouseMove) +// +// Result elements can be of the following types: +// - func(View, MouseEvent), +// - 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetMouseOverListeners(view View, subviewID ...string) []func(View, MouseEvent) { - return getOneArgEventListeners[View, MouseEvent](view, subviewID, MouseOver) +// +// Result elements can be of the following types: +// - func(View, MouseEvent), +// - 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetMouseOutListeners(view View, subviewID ...string) []func(View, MouseEvent) { - return getOneArgEventListeners[View, MouseEvent](view, subviewID, MouseOut) +// +// Result elements can be of the following types: +// - func(View, MouseEvent), +// - 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) } diff --git a/numberPicker.go b/numberPicker.go index c664bc0..17963b0 100644 --- a/numberPicker.go +++ b/numberPicker.go @@ -298,13 +298,17 @@ func (picker *numberPickerData) handleCommand(self View, command PropertyName, d // GetNumberPickerType returns the type of NumberPicker subview. Valid values: // NumberEditor (0) - NumberPicker is presented by editor (default type); // NumberSlider (1) - NumberPicker is presented by slider. -// If the second argument (subviewID) is 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 { return enumStyledProperty(view, subviewID, NumberPickerType, NumberEditor, false) } // 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) { view = getSubview(view, subviewID) pickerType := GetNumberPickerType(view) @@ -328,7 +332,9 @@ func GetNumberPickerMinMax(view View, subviewID ...string) (float64, float64) { } // GetNumberPickerStep returns the value changing step of NumberPicker subview. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 { view = getSubview(view, subviewID) _, max := GetNumberPickerMinMax(view) @@ -341,7 +347,9 @@ func GetNumberPickerStep(view View, subviewID ...string) float64 { } // GetNumberPickerValue returns the value of NumberPicker subview. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 { view = getSubview(view, subviewID) min, _ := GetNumberPickerMinMax(view) @@ -350,13 +358,17 @@ func GetNumberPickerValue(view View, subviewID ...string) float64 { // GetNumberChangedListeners returns the NumberChangedListener list of an NumberPicker subview. // If there are no listeners then the empty list is returned -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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) []func(NumberPicker, float64, float64) { return getTwoArgEventListeners[NumberPicker, float64](view, subviewID, NumberChangedEvent) } // GetNumberPickerPrecision returns the precision of displaying fractional part in editor of NumberPicker subview. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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) } diff --git a/pointerEvents.go b/pointerEvents.go index 57d226b..1621f8f 100644 --- a/pointerEvents.go +++ b/pointerEvents.go @@ -192,42 +192,96 @@ func handlePointerEvents(view View, tag PropertyName, data DataObject) { event.init(data) for _, listener := range listeners { - listener(view, event) + listener.Run(view, event) } } // GetPointerDownListeners returns the "pointer-down" listener list. If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetPointerDownListeners(view View, subviewID ...string) []func(View, PointerEvent) { - return getOneArgEventListeners[View, PointerEvent](view, subviewID, PointerDown) +// +// Result elements can be of the following types: +// - func(View, PointerEvent), +// - 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetPointerUpListeners(view View, subviewID ...string) []func(View, PointerEvent) { - return getOneArgEventListeners[View, PointerEvent](view, subviewID, PointerUp) +// +// Result elements can be of the following types: +// - func(View, PointerEvent), +// - 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetPointerMoveListeners(view View, subviewID ...string) []func(View, PointerEvent) { - return getOneArgEventListeners[View, PointerEvent](view, subviewID, PointerMove) +// +// Result elements can be of the following types: +// - func(View, PointerEvent), +// - 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetPointerCancelListeners(view View, subviewID ...string) []func(View, PointerEvent) { - return getOneArgEventListeners[View, PointerEvent](view, subviewID, PointerCancel) +// +// Result elements can be of the following types: +// - func(View, PointerEvent), +// - 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetPointerOverListeners(view View, subviewID ...string) []func(View, PointerEvent) { - return getOneArgEventListeners[View, PointerEvent](view, subviewID, PointerOver) +// +// Result elements can be of the following types: +// - func(View, PointerEvent), +// - 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. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetPointerOutListeners(view View, subviewID ...string) []func(View, PointerEvent) { - return getOneArgEventListeners[View, PointerEvent](view, subviewID, PointerOut) +// +// Result elements can be of the following types: +// - func(View, PointerEvent), +// - 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) } diff --git a/popup.go b/popup.go index 93ad67b..cc77b41 100644 --- a/popup.go +++ b/popup.go @@ -649,7 +649,7 @@ func (popup *popupData) init(view View, popupParams Params) { } case DismissEvent: - if listeners, ok := valueToNoArgEventListeners[Popup](value); ok { + if listeners, ok := valueToNoArgEventListeners[Popup](popup.contentView, value); ok { if listeners != nil { popup.dismissListener = listeners } diff --git a/progressBar.go b/progressBar.go index d37b23c..723b583 100644 --- a/progressBar.go +++ b/progressBar.go @@ -101,13 +101,17 @@ func (progress *progressBarData) htmlProperties(self View, buffer *strings.Build } // 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 { return floatStyledProperty(view, subviewID, ProgressBarMax, 1) } // 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 { return floatStyledProperty(view, subviewID, ProgressBarValue, 0) } diff --git a/propertyNames.go b/propertyNames.go index b2259d9..b327ca2 100644 --- a/propertyNames.go +++ b/propertyNames.go @@ -2723,4 +2723,6 @@ const ( // // Supported types: string. Tooltip PropertyName = "tooltip" + + Binding PropertyName = "binding" ) diff --git a/resizeEvent.go b/resizeEvent.go index bae4fbc..f102b84 100644 --- a/resizeEvent.go +++ b/resizeEvent.go @@ -25,8 +25,8 @@ func (view *viewData) onResize(self View, x, y, width, height float64) { view.frame.Top = y view.frame.Width = width view.frame.Height = height - for _, listener := range GetResizeListeners(view) { - listener(self, view.frame) + for _, listener := range getOneArgEventListeners[View, Frame](view, nil, ResizeEvent) { + listener.Run(self, view.frame) } } @@ -73,7 +73,9 @@ func (view *viewData) Frame() Frame { } // GetViewFrame returns the size and location of view's viewport. -// If the second argument (subviewID) is not specified or it is "" then the value of the first argument (view) is returned +// +// 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 { view = getSubview(view, subviewID) if view == nil { @@ -83,7 +85,16 @@ func GetViewFrame(view View, subviewID ...string) Frame { } // GetResizeListeners returns the list of "resize-event" listeners. If there are no listeners then the empty list is returned -// If the second argument (subviewID) is not specified or it is "" then the listeners list of the first argument (view) is returned -func GetResizeListeners(view View, subviewID ...string) []func(View, Frame) { - return getOneArgEventListeners[View, Frame](view, subviewID, ResizeEvent) +// +// Result elements can be of the following types: +// - func(rui.View, rui.Frame), +// - 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) } diff --git a/scrollEvent.go b/scrollEvent.go index 294538e..2b457ee 100644 --- a/scrollEvent.go +++ b/scrollEvent.go @@ -25,8 +25,8 @@ func (view *viewData) onScroll(self View, x, y, width, height float64) { view.scroll.Top = y view.scroll.Width = width view.scroll.Height = height - for _, listener := range GetScrollListeners(view) { - listener(self, view.scroll) + for _, listener := range getOneArgEventListeners[View, Frame](view, nil, ScrollEvent) { + listener.Run(self, view.scroll) } } @@ -42,7 +42,9 @@ func (view *viewData) setScroll(x, y, width, height float64) { } // GetViewScroll returns ... -// If the second argument (subviewID) is not specified or it is "" then a value of the first argument (view) is returned +// +// 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 { view = getSubview(view, subviewID) if view == nil { @@ -52,13 +54,24 @@ func GetViewScroll(view View, subviewID ...string) Frame { } // GetScrollListeners returns the list of "scroll-event" listeners. If there are no listeners then the empty list is returned -// If the second argument (subviewID) is not specified or it is "" then the listeners list of the first argument (view) is returned -func GetScrollListeners(view View, subviewID ...string) []func(View, Frame) { - return getOneArgEventListeners[View, Frame](view, subviewID, ResizeEvent) +// +// Result elements can be of the following types: +// - func(rui.View, rui.Frame), +// - 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. -// 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) { if subviewID != "" { view = ViewByID(view, subviewID) @@ -69,7 +82,9 @@ func ScrollViewTo(view View, subviewID string, x, y float64) { } // ScrollViewToEnd scrolls the view's content to the start of view. -// If the second argument (subviewID) is not specified or it is "" then the first argument (view) is used +// +// 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) { if view = getSubview(view, subviewID); view != nil { view.Session().callFunc("scrollToStart", view.htmlID()) @@ -77,7 +92,9 @@ func ScrollViewToStart(view View, subviewID ...string) { } // ScrollViewToEnd scrolls the view's content to the end of view. -// If the second argument (subviewID) is not specified or it is "" then the first argument (view) is used +// +// 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) { if view = getSubview(view, subviewID); view != nil { view.Session().callFunc("scrollToEnd", view.htmlID()) diff --git a/stackLayout.go b/stackLayout.go index ef9a238..327bc5c 100644 --- a/stackLayout.go +++ b/stackLayout.go @@ -334,7 +334,9 @@ func (layout *stackLayoutData) init(session Session) { layout.remove = layout.removeFunc layout.changed = layout.propertyChanged - layout.setRaw(TransitionEndEvent, []func(View, PropertyName){layout.transitionFinished}) + layout.setRaw(TransitionEndEvent, []oneArgListener[View, PropertyName]{ + newOneArgListenerVE(layout.transitionFinished), + }) if session.TextDirection() == RightToLeftDirection { layout.setRaw(PushTransform, NewTransformProperty(Params{TranslateX: Percent(-100)})) } else { @@ -434,7 +436,7 @@ func (layout *stackLayoutData) setFunc(tag PropertyName, value any) []PropertyNa // TODO listeners, ok := valueToOneArgEventListeners[View, PropertyName](value) if ok && listeners != nil { - listeners = append(listeners, layout.transitionFinished) + listeners = append(listeners, newOneArgListenerVE(layout.transitionFinished)) layout.setRaw(TransitionEndEvent, listeners) return []PropertyName{tag} } @@ -464,7 +466,9 @@ func (layout *stackLayoutData) propertyChanged(tag PropertyName) { func (layout *stackLayoutData) removeFunc(tag PropertyName) []PropertyName { switch tag { case TransitionEndEvent: - layout.setRaw(TransitionEndEvent, []func(View, PropertyName){layout.transitionFinished}) + layout.setRaw(TransitionEndEvent, []oneArgListener[View, PropertyName]{ + newOneArgListenerVE(layout.transitionFinished), + }) return []PropertyName{tag} } return layout.viewsContainerData.removeFunc(tag) @@ -914,7 +918,9 @@ func (layout *stackLayoutData) htmlSubviews(self View, buffer *strings.Builder) } // IsMoveToFrontAnimation returns "true" if an animation is used when calling the MoveToFront/MoveToFrontByID method of StackLayout interface. -// 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 IsMoveToFrontAnimation(view View, subviewID ...string) bool { if view = getSubview(view, subviewID); view != nil { if value, ok := boolProperty(view, MoveToFrontAnimation, view.Session()); ok { @@ -950,7 +956,9 @@ func GetPushTiming(view View, subviewID ...string) string { // GetPushTransform returns the start transform (translation, scale and rotation over x, y and z axes as well as a distortion) // for an animated pushing of a child view. // The default value is nil (no transform). -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 GetPushTransform(view View, subviewID ...string) TransformProperty { return transformStyledProperty(view, subviewID, PushTransform) } diff --git a/tableView.go b/tableView.go index 6ce13d4..0bd784b 100644 --- a/tableView.go +++ b/tableView.go @@ -592,14 +592,6 @@ func (table *tableViewData) init(session Session) { table.tag = "TableView" table.cellViews = []View{} table.cellFrame = []Frame{} - /* - table.cellSelectedListener = []func(TableView, int, int){} - table.cellClickedListener = []func(TableView, int, int){} - table.rowSelectedListener = []func(TableView, int){} - table.rowClickedListener = []func(TableView, int){} - table.current.Row = -1 - table.current.Column = -1 - */ table.normalize = normalizeTableViewTag table.set = table.setFunc table.changed = table.propertyChanged @@ -955,63 +947,6 @@ func tableViewCurrentInactiveStyle(view View) string { return "ruiCurrentTableCell" } -/* -func (table *tableViewData) valueToCellListeners(value any) []func(TableView, int, int) { - if value == nil { - return []func(TableView, int, int){} - } - - switch value := value.(type) { - case func(TableView, int, int): - return []func(TableView, int, int){value} - - case func(int, int): - fn := func(_ TableView, row, column int) { - value(row, column) - } - return []func(TableView, int, int){fn} - - case []func(TableView, int, int): - return value - - case []func(int, int): - listeners := make([]func(TableView, int, int), len(value)) - for i, val := range value { - if val == nil { - return nil - } - listeners[i] = func(_ TableView, row, column int) { - val(row, column) - } - } - return listeners - - case []any: - listeners := make([]func(TableView, int, int), len(value)) - for i, val := range value { - if val == nil { - return nil - } - switch val := val.(type) { - case func(TableView, int, int): - listeners[i] = val - - case func(int, int): - listeners[i] = func(_ TableView, row, column int) { - val(row, column) - } - - default: - return nil - } - } - return listeners - } - - return nil -} -*/ - func (table *tableViewData) htmlTag() string { return "table" } @@ -1735,8 +1670,8 @@ func (table *tableViewData) handleCommand(self View, command PropertyName, data listener(table, Current) } - for _, listener := range GetTableRowSelectedListeners(table) { - listener(table, row) + for _, listener := range getOneArgEventListeners[TableView, int](table, nil, TableRowSelectedEvent) { + listener.Run(table, row) } } @@ -1761,8 +1696,8 @@ func (table *tableViewData) handleCommand(self View, command PropertyName, data case "rowClick": if row, ok := dataIntProperty(data, "row"); ok { - for _, listener := range GetTableRowClickedListeners(table) { - listener(table, row) + for _, listener := range getOneArgEventListeners[TableView, int](table, nil, TableRowClickedEvent) { + listener.Run(table, row) } } diff --git a/tableViewUtils.go b/tableViewUtils.go index 152a8e6..69b77e9 100644 --- a/tableViewUtils.go +++ b/tableViewUtils.go @@ -26,7 +26,9 @@ func (cell *tableCellView) cssStyle(self View, builder cssBuilder) { } // GetTableContent returns a TableAdapter which defines the TableView content. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 { if view = getSubview(view, subviewID); view != nil { if content := view.getRaw(Content); content != nil { @@ -40,7 +42,9 @@ func GetTableContent(view View, subviewID ...string) TableAdapter { } // GetTableRowStyle returns a TableRowStyle which defines styles of TableView rows. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 { if view = getSubview(view, subviewID); view != nil { for _, tag := range []PropertyName{RowStyle, Content} { @@ -56,7 +60,9 @@ func GetTableRowStyle(view View, subviewID ...string) TableRowStyle { } // GetTableColumnStyle returns a TableColumnStyle which defines styles of TableView columns. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 { if view = getSubview(view, subviewID); view != nil { for _, tag := range []PropertyName{ColumnStyle, Content} { @@ -72,7 +78,9 @@ func GetTableColumnStyle(view View, subviewID ...string) TableColumnStyle { } // GetTableCellStyle returns a TableCellStyle which defines styles of TableView cells. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 { if view = getSubview(view, subviewID); view != nil { for _, tag := range []PropertyName{CellStyle, Content} { @@ -90,26 +98,34 @@ func GetTableCellStyle(view View, subviewID ...string) TableCellStyle { // GetTableSelectionMode returns the mode of the TableView elements selection. // Valid values are NoneSelection (0), CellSelection (1), and RowSelection (2). -// If the second argument (subviewID) is 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 { return enumStyledProperty(view, subviewID, SelectionMode, NoneSelection, false) } // GetTableVerticalAlign returns a vertical align in a TableView cell. Returns one of next values: // 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 { return enumStyledProperty(view, subviewID, TableVerticalAlign, TopAlign, false) } // 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 { return intStyledProperty(view, subviewID, HeadHeight, 0) } // 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 { return intStyledProperty(view, subviewID, FootHeight, 0) } @@ -118,7 +134,9 @@ func GetTableFootHeight(view View, subviewID ...string) int { // If there is no selected cell/row or the selection mode is NoneSelection (0), // then a value of the row and column index less than 0 is returned. // If the selection mode is RowSelection (2) then the returned column index is less than 0. -// If the second argument (subviewID) is 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 { if view = getSubview(view, subviewID); view != nil { if selectionMode := GetTableSelectionMode(view); selectionMode != NoneSelection { @@ -130,30 +148,52 @@ func GetTableCurrent(view View, subviewID ...string) CellIndex { // GetTableCellClickedListeners returns listeners of event which occurs when the user clicks on a table cell. // If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is 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 GetTableCellClickedListeners(view View, subviewID ...string) []func(TableView, int, int) { return getTwoArgEventListeners[TableView, int](view, subviewID, TableCellClickedEvent) } // GetTableCellSelectedListeners returns listeners of event which occurs when a table cell becomes selected. // If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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) []func(TableView, int, int) { return getTwoArgEventListeners[TableView, int](view, subviewID, TableCellSelectedEvent) } // GetTableRowClickedListeners returns listeners of event which occurs when the user clicks on a table row. // If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetTableRowClickedListeners(view View, subviewID ...string) []func(TableView, int) { - return getOneArgEventListeners[TableView, int](view, subviewID, TableRowClickedEvent) +// +// Result elements can be of the following types: +// - func(rui.TableView, int), +// - 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. // If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetTableRowSelectedListeners(view View, subviewID ...string) []func(TableView, int) { - return getOneArgEventListeners[TableView, int](view, subviewID, TableRowSelectedEvent) +// +// Result elements can be of the following types: +// - func(rui.TableView, int), +// - 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 diff --git a/tabsLayout.go b/tabsLayout.go index 7a2ee21..5fb6b40 100644 --- a/tabsLayout.go +++ b/tabsLayout.go @@ -238,155 +238,6 @@ func (tabsLayout *tabsLayoutData) propertyChanged(tag PropertyName) { } } -/* - func (tabsLayout *tabsLayoutData) valueToTabListeners(value any) []func(TabsLayout, int, int) { - if value == nil { - return []func(TabsLayout, int, int){} - } - - switch value := value.(type) { - case func(TabsLayout, int, int): - return []func(TabsLayout, int, int){value} - - case func(TabsLayout, int): - fn := func(view TabsLayout, current, _ int) { - value(view, current) - } - return []func(TabsLayout, int, int){fn} - - case func(TabsLayout): - fn := func(view TabsLayout, _, _ int) { - value(view) - } - return []func(TabsLayout, int, int){fn} - - case func(int, int): - fn := func(_ TabsLayout, current, old int) { - value(current, old) - } - return []func(TabsLayout, int, int){fn} - - case func(int): - fn := func(_ TabsLayout, current, _ int) { - value(current) - } - return []func(TabsLayout, int, int){fn} - - case func(): - fn := func(TabsLayout, int, int) { - value() - } - return []func(TabsLayout, int, int){fn} - - case []func(TabsLayout, int, int): - return value - - case []func(TabsLayout, int): - listeners := make([]func(TabsLayout, int, int), len(value)) - for i, val := range value { - if val == nil { - return nil - } - listeners[i] = func(view TabsLayout, current, _ int) { - val(view, current) - } - } - return listeners - - case []func(TabsLayout): - listeners := make([]func(TabsLayout, int, int), len(value)) - for i, val := range value { - if val == nil { - return nil - } - listeners[i] = func(view TabsLayout, _, _ int) { - val(view) - } - } - return listeners - - case []func(int, int): - listeners := make([]func(TabsLayout, int, int), len(value)) - for i, val := range value { - if val == nil { - return nil - } - listeners[i] = func(_ TabsLayout, current, old int) { - val(current, old) - } - } - return listeners - - case []func(int): - listeners := make([]func(TabsLayout, int, int), len(value)) - for i, val := range value { - if val == nil { - return nil - } - listeners[i] = func(_ TabsLayout, current, _ int) { - val(current) - } - } - return listeners - - case []func(): - listeners := make([]func(TabsLayout, int, int), len(value)) - for i, val := range value { - if val == nil { - return nil - } - listeners[i] = func(TabsLayout, int, int) { - val() - } - } - return listeners - - case []any: - listeners := make([]func(TabsLayout, int, int), len(value)) - for i, val := range value { - if val == nil { - return nil - } - switch val := val.(type) { - case func(TabsLayout, int, int): - listeners[i] = val - - case func(TabsLayout, int): - listeners[i] = func(view TabsLayout, current, _ int) { - val(view, current) - } - - case func(TabsLayout): - listeners[i] = func(view TabsLayout, _, _ int) { - val(view) - } - - case func(int, int): - listeners[i] = func(_ TabsLayout, current, old int) { - val(current, old) - } - - case func(int): - listeners[i] = func(_ TabsLayout, current, _ int) { - val(current) - } - - case func(): - listeners[i] = func(TabsLayout, int, int) { - val() - } - - default: - return nil - } - } - return listeners - } - - return nil - } -*/ - func (tabsLayout *tabsLayoutData) tabsLocation() int { tabs, _ := enumProperty(tabsLayout, Tabs, tabsLayout.session, 0) return tabs @@ -504,7 +355,7 @@ func (tabsLayout *tabsLayoutData) ListItem(index int, session Session) View { Content: "✕", ClickEvent: func() { for _, listener := range getOneArgEventListeners[TabsLayout, int](tabsLayout, nil, TabCloseEvent) { - listener(tabsLayout, index) + listener.Run(tabsLayout, index) } }, })) @@ -892,7 +743,7 @@ func (tabsLayout *tabsLayoutData) handleCommand(self View, command PropertyName, if numberText, ok := data.PropertyValue("number"); ok { if number, err := strconv.Atoi(numberText); err == nil { for _, listener := range getOneArgEventListeners[TabsLayout, int](tabsLayout, nil, TabCloseEvent) { - listener(tabsLayout, number) + listener.Run(tabsLayout, number) } } } @@ -900,3 +751,18 @@ func (tabsLayout *tabsLayoutData) handleCommand(self View, command PropertyName, } return tabsLayout.viewsContainerData.handleCommand(self, command, data) } + +// GetTabCloseEventListeners returns the "tab-close-event" listener list. If there are no listeners then the empty list is returned. +// +// Result elements can be of the following types: +// - func(rui.TabsLayout, int), +// - func(rui.TabsLayout), +// - 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 GetTabCloseEventListeners(view View, subviewID ...string) []any { + return getOneArgEventRawListeners[TabsLayout, int](view, subviewID, TabCloseEvent) +} diff --git a/textView.go b/textView.go index a21413f..efcab65 100644 --- a/textView.go +++ b/textView.go @@ -111,7 +111,9 @@ func (textView *textViewData) htmlSubviews(self View, buffer *strings.Builder) { // GetTextOverflow returns a value of the "text-overflow" property: // TextOverflowClip (0) or TextOverflowEllipsis (1). -// 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 GetTextOverflow(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, TextOverflow, SingleLineText, false) } diff --git a/timePicker.go b/timePicker.go index a85ad18..9d8dea2 100644 --- a/timePicker.go +++ b/timePicker.go @@ -373,7 +373,9 @@ func getTimeProperty(view View, mainTag, shortTag PropertyName) (time.Time, bool // GetTimePickerMin returns the min time of TimePicker subview and "true" as the second value if the min time is set, // "false" as the second value otherwise. -// If the second argument (subviewID) is 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 GetTimePickerMin(view View, subviewID ...string) (time.Time, bool) { if view = getSubview(view, subviewID); view != nil { return getTimeProperty(view, TimePickerMin, Min) @@ -383,7 +385,9 @@ func GetTimePickerMin(view View, subviewID ...string) (time.Time, bool) { // GetTimePickerMax returns the max time of TimePicker subview and "true" as the second value if the min time is set, // "false" as the second value otherwise. -// If the second argument (subviewID) is 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 GetTimePickerMax(view View, subviewID ...string) (time.Time, bool) { if view = getSubview(view, subviewID); view != nil { return getTimeProperty(view, TimePickerMax, Max) @@ -392,13 +396,17 @@ func GetTimePickerMax(view View, subviewID ...string) (time.Time, bool) { } // GetTimePickerStep returns the time changing step in seconds of TimePicker 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 GetTimePickerStep(view View, subviewID ...string) int { return intStyledProperty(view, subviewID, TimePickerStep, 60) } // GetTimePickerValue returns the time of TimePicker subview. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 GetTimePickerValue(view View, subviewID ...string) time.Time { if view = getSubview(view, subviewID); view == nil { return time.Now() @@ -409,7 +417,9 @@ func GetTimePickerValue(view View, subviewID ...string) time.Time { // GetTimeChangedListeners returns the TimeChangedListener list of an TimePicker subview. // If there are no listeners then the empty list is returned -// If the second argument (subviewID) is 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 GetTimeChangedListeners(view View, subviewID ...string) []func(TimePicker, time.Time, time.Time) { return getTwoArgEventListeners[TimePicker, time.Time](view, subviewID, TimeChangedEvent) } diff --git a/touchEvents.go b/touchEvents.go index 68d0d50..4a1a73e 100644 --- a/touchEvents.go +++ b/touchEvents.go @@ -193,30 +193,66 @@ func handleTouchEvents(view View, tag PropertyName, data DataObject) { event.init(data) for _, listener := range listeners { - listener(view, event) + listener.Run(view, event) } } // GetTouchStartListeners returns the "touch-start" listener list. If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetTouchStartListeners(view View, subviewID ...string) []func(View, TouchEvent) { - return getOneArgEventListeners[View, TouchEvent](view, subviewID, TouchStart) +// +// Result elements can be of the following types: +// - func(rui.View, rui.TouchEvent), +// - func(rui.View), +// - func(rui.TouchEvent), +// - 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 GetTouchStartListeners(view View, subviewID ...string) []any { + return getOneArgEventRawListeners[View, TouchEvent](view, subviewID, TouchStart) } // GetTouchEndListeners returns the "touch-end" listener list. If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetTouchEndListeners(view View, subviewID ...string) []func(View, TouchEvent) { - return getOneArgEventListeners[View, TouchEvent](view, subviewID, TouchEnd) +// +// Result elements can be of the following types: +// - func(rui.View, rui.TouchEvent), +// - func(rui.View), +// - func(rui.TouchEvent), +// - 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 GetTouchEndListeners(view View, subviewID ...string) []any { + return getOneArgEventRawListeners[View, TouchEvent](view, subviewID, TouchEnd) } // GetTouchMoveListeners returns the "touch-move" listener list. If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetTouchMoveListeners(view View, subviewID ...string) []func(View, TouchEvent) { - return getOneArgEventListeners[View, TouchEvent](view, subviewID, TouchMove) +// +// Result elements can be of the following types: +// - func(rui.View, rui.TouchEvent), +// - func(rui.View), +// - func(rui.TouchEvent), +// - 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 GetTouchMoveListeners(view View, subviewID ...string) []any { + return getOneArgEventRawListeners[View, TouchEvent](view, subviewID, TouchMove) } // GetTouchCancelListeners returns the "touch-cancel" listener list. If there are no listeners then the empty list is returned. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. -func GetTouchCancelListeners(view View, subviewID ...string) []func(View, TouchEvent) { - return getOneArgEventListeners[View, TouchEvent](view, subviewID, TouchCancel) +// +// Result elements can be of the following types: +// - func(rui.View, rui.TouchEvent), +// - func(rui.View), +// - func(rui.TouchEvent), +// - 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 GetTouchCancelListeners(view View, subviewID ...string) []any { + return getOneArgEventRawListeners[View, TouchEvent](view, subviewID, TouchCancel) } diff --git a/transform.go b/transform.go index e52cfda..6a0c600 100644 --- a/transform.go +++ b/transform.go @@ -647,7 +647,9 @@ func transformOriginCSS(x, y, z SizeUnit, session Session) string { // GetTransform returns a view transform: translation, scale and rotation over x, y and z axes as well as a distortion of a view along x and y axes. // The default value is nil (no transform) -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 GetTransform(view View, subviewID ...string) TransformProperty { return transformStyledProperty(view, subviewID, Transform) } @@ -655,14 +657,18 @@ func GetTransform(view View, subviewID ...string) TransformProperty { // GetPerspective returns a distance between the z = 0 plane and the user in order to give a 3D-positioned // element some perspective. Each 3D element with z > 0 becomes larger; each 3D-element with z < 0 becomes smaller. // The default value is 0 (no 3D effects). -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 GetPerspective(view View, subviewID ...string) SizeUnit { return sizeStyledProperty(view, subviewID, Perspective, false) } // GetPerspectiveOrigin returns a x- and y-coordinate of the position at which the viewer is looking. // It is used as the vanishing point by the Perspective property. The default value is (50%, 50%). -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 GetPerspectiveOrigin(view View, subviewID ...string) (SizeUnit, SizeUnit) { view = getSubview(view, subviewID) if view == nil { @@ -675,14 +681,18 @@ func GetPerspectiveOrigin(view View, subviewID ...string) (SizeUnit, SizeUnit) { // visible when turned towards the user. Values: // true - the back face is visible when turned towards the user (default value). // false - the back face is hidden, effectively making the element invisible when turned away from the user. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 GetBackfaceVisible(view View, subviewID ...string) bool { return boolStyledProperty(view, subviewID, BackfaceVisible, false) } // GetTransformOrigin returns a x-, y-, and z-coordinate of the point around which a view transformation is applied. // The default value is (50%, 50%, 50%). -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 GetTransformOrigin(view View, subviewID ...string) (SizeUnit, SizeUnit, SizeUnit) { view = getSubview(view, subviewID) if view == nil { @@ -692,7 +702,9 @@ func GetTransformOrigin(view View, subviewID ...string) (SizeUnit, SizeUnit, Siz } // GetTranslate returns a x-, y-, and z-axis translation value of a 2D/3D translation -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 GetTranslate(view View, subviewID ...string) (SizeUnit, SizeUnit, SizeUnit) { if transform := GetTransform(view, subviewID...); transform != nil { return transform.getTranslate(view.Session()) @@ -702,7 +714,9 @@ func GetTranslate(view View, subviewID ...string) (SizeUnit, SizeUnit, SizeUnit) // GetSkew returns a angles to use to distort the element along the abscissa (x-axis) // and the ordinate (y-axis). The default value is 0. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 GetSkew(view View, subviewID ...string) (AngleUnit, AngleUnit) { if transform := GetTransform(view, subviewID...); transform != nil { x, y, _ := transform.getSkew(view.Session()) @@ -712,7 +726,9 @@ func GetSkew(view View, subviewID ...string) (AngleUnit, AngleUnit) { } // GetScale returns a x-, y-, and z-axis scaling value of a 2D/3D scale. The default value is 1. -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 GetScale(view View, subviewID ...string) (float64, float64, float64) { if transform := GetTransform(view, subviewID...); transform != nil { session := view.Session() @@ -725,7 +741,9 @@ func GetScale(view View, subviewID ...string) (float64, float64, float64) { } // GetRotate returns a x-, y, z-coordinate of the vector denoting the axis of rotation, and the angle of the view rotation -// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. +// +// 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 GetRotate(view View, subviewID ...string) (float64, float64, float64, AngleUnit) { if transform := GetTransform(view, subviewID...); transform != nil { session := view.Session() diff --git a/view.go b/view.go index 72fc4a8..2b2ab01 100644 --- a/view.go +++ b/view.go @@ -92,6 +92,7 @@ type View interface { addToCSSStyle(addCSS map[string]string) exscludeTags() []PropertyName htmlDisabledProperty() bool + binding() any onResize(self View, x, y, width, height float64) onItemResize(self View, index string, x, y, width, height float64) @@ -195,6 +196,18 @@ func (view *viewData) ID() string { return view.viewID } +func (view *viewData) binding() any { + if result := view.getRaw(Binding); result != nil { + return result + } + + if parent := view.Parent(); parent != nil { + return parent.binding() + } + + return nil +} + func (view *viewData) ViewByID(id string) View { if id == view.ID() { if v := view.session.viewByHTMLID(view.htmlID()); v != nil { @@ -304,6 +317,14 @@ func (view *viewData) removeFunc(tag PropertyName) []PropertyName { changedTags = []PropertyName{} } + case Binding: + if view.getRaw(Binding) != nil { + view.setRaw(Binding, nil) + changedTags = []PropertyName{Binding} + } else { + changedTags = []PropertyName{} + } + case Animation: if val := view.getRaw(Animation); val != nil { if animations, ok := val.([]AnimationProperty); ok { @@ -336,6 +357,10 @@ func (view *viewData) setFunc(tag PropertyName, value any) []PropertyName { notCompatibleType(ID, value) return nil + case Binding: + view.setRaw(Binding, value) + return []PropertyName{Binding} + case Animation: oldAnimations := []AnimationProperty{} if val := view.getRaw(Animation); val != nil { @@ -386,20 +411,15 @@ func (view *viewData) setFunc(tag PropertyName, value any) []PropertyName { case TransitionRunEvent, TransitionStartEvent, TransitionEndEvent, TransitionCancelEvent: result := setOneArgEventListener[View, PropertyName](view, tag, value) if result == nil { - result = setOneArgEventListener[View, string](view, tag, value) - if result != nil { - if listeners, ok := view.getRaw(tag).([]func(View, string)); ok { - newListeners := make([]func(View, PropertyName), len(listeners)) - for i, listener := range listeners { - newListeners[i] = func(view View, name PropertyName) { - listener(view, string(name)) - } - } - view.setRaw(tag, newListeners) - return result + if listeners, ok := valueToOneArgEventListeners[View, string](view); ok && len(listeners) > 0 { + newListeners := make([]oneArgListener[View, PropertyName], len(listeners)) + for i, listener := range listeners { + newListeners[i] = newOneArgListenerVE(func(view View, name PropertyName) { + listener.Run(view, string(name)) + }) } - view.setRaw(tag, nil) - return nil + view.setRaw(tag, newListeners) + result = []PropertyName{tag} } } return result diff --git a/viewUtils.go b/viewUtils.go index 3f5f921..42cc6b0 100644 --- a/viewUtils.go +++ b/viewUtils.go @@ -355,31 +355,41 @@ func GetTextShadows(view View, subviewID ...string) []ShadowProperty { } // GetBackgroundColor returns a background color of the 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 GetBackgroundColor(view View, subviewID ...string) Color { return colorStyledProperty(view, subviewID, BackgroundColor, false) } // GetAccentColor returns the accent color for UI controls generated by some elements. -// 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 GetAccentColor(view View, subviewID ...string) Color { return colorStyledProperty(view, subviewID, AccentColor, false) } // GetFontName returns the subview font. -// 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 GetFontName(view View, subviewID ...string) string { return stringStyledProperty(view, nil, FontName, true) } // GetTextColor returns a text color of the 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 GetTextColor(view View, subviewID ...string) Color { return colorStyledProperty(view, subviewID, TextColor, true) } // GetTextSize returns a text size of the 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 GetTextSize(view View, subviewID ...string) SizeUnit { return sizeStyledProperty(view, subviewID, TextSize, true) } @@ -392,7 +402,9 @@ func GetTabSize(view View, subviewID ...string) int { // GetTextWeight returns a text weight of the subview. Returns one of next values: // 1, 2, 3, 4 (normal text), 5, 6, 7 (bold text), 8 and 9 -// If the second argument (subviewID) is 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 GetTextWeight(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, TextWeight, NormalFont, true) } @@ -401,7 +413,8 @@ func GetTextWeight(view View, subviewID ...string) int { // // LeftAlign = 0, RightAlign = 1, CenterAlign = 2, JustifyAlign = 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 GetTextAlign(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, TextAlign, LeftAlign, true) } @@ -410,89 +423,116 @@ func GetTextAlign(view View, subviewID ...string) int { // // TextWrapOn = 0, TextWrapOff = 1, TextWrapBalance = 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 GetTextWrap(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, TextWrap, TextWrapOn, true) } // GetTextIndent returns a text indent of the 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 GetTextIndent(view View, subviewID ...string) SizeUnit { return sizeStyledProperty(view, subviewID, TextIndent, true) } // GetLetterSpacing returns a letter spacing of the 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 GetLetterSpacing(view View, subviewID ...string) SizeUnit { return sizeStyledProperty(view, subviewID, LetterSpacing, true) } // GetWordSpacing returns a word spacing of the 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 GetWordSpacing(view View, subviewID ...string) SizeUnit { return sizeStyledProperty(view, subviewID, WordSpacing, true) } // GetLineHeight returns a height of a text line of the 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 GetLineHeight(view View, subviewID ...string) SizeUnit { return sizeStyledProperty(view, subviewID, LineHeight, true) } // IsItalic returns "true" if a text font of the subview is displayed in italics, "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 IsItalic(view View, subviewID ...string) bool { return boolStyledProperty(view, subviewID, Italic, true) } // IsSmallCaps returns "true" if a text font of the subview is displayed in small caps, "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 IsSmallCaps(view View, subviewID ...string) bool { return boolStyledProperty(view, subviewID, SmallCaps, true) } // IsStrikethrough returns "true" if a text font of the subview is displayed strikethrough, "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 IsStrikethrough(view View, subviewID ...string) bool { return boolStyledProperty(view, subviewID, Strikethrough, true) } // IsOverline returns "true" if a text font of the subview is displayed overlined, "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 IsOverline(view View, subviewID ...string) bool { return boolStyledProperty(view, subviewID, Overline, true) } // IsUnderline returns "true" if a text font of the subview is displayed underlined, "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 IsUnderline(view View, subviewID ...string) bool { return boolStyledProperty(view, subviewID, Underline, true) } // GetTextLineThickness returns the stroke thickness of the decoration line that // is used on text in an element, such as a line-through, underline, or overline. -// If the second argument (subviewID) is 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 GetTextLineThickness(view View, subviewID ...string) SizeUnit { return sizeStyledProperty(view, subviewID, TextLineThickness, true) } // GetTextLineStyle returns the stroke style of the decoration line that // is used on text in an element, such as a line-through, underline, or overline. -// If the second argument (subviewID) is 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 GetTextLineStyle(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, TextLineStyle, SolidLine, true) } // GetTextLineColor returns the stroke color of the decoration line that // is used on text in an element, such as a line-through, underline, or overline. -// If the second argument (subviewID) is 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 GetTextLineColor(view View, subviewID ...string) Color { return colorStyledProperty(view, subviewID, TextLineColor, true) } // GetTextTransform returns a text transform of the subview. Return one of next values: // NoneTextTransform (0), CapitalizeTextTransform (1), LowerCaseTextTransform (2) or UpperCaseTextTransform (3) -// If the second argument (subviewID) is 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 GetTextTransform(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, TextTransform, NoneTextTransform, true) } @@ -500,14 +540,18 @@ func GetTextTransform(view View, subviewID ...string) int { // GetWritingMode returns whether lines of text are laid out horizontally or vertically, as well as // the direction in which blocks progress. Valid values are HorizontalTopToBottom (0), // HorizontalBottomToTop (1), VerticalRightToLeft (2) and VerticalLeftToRight (3) -// If the second argument (subviewID) is 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 GetWritingMode(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, WritingMode, HorizontalTopToBottom, true) } // GetTextDirection - returns a direction of text, table columns, and horizontal overflow. // Valid values are SystemTextDirection (0), LeftToRightDirection (1), and RightToLeftDirection (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 GetTextDirection(view View, subviewID ...string) int { if view == nil { return SystemTextDirection @@ -519,7 +563,9 @@ func GetTextDirection(view View, subviewID ...string) int { // GetVerticalTextOrientation returns a orientation of the text characters in a line. It only affects text // in vertical mode (when "writing-mode" is "vertical-right-to-left" or "vertical-left-to-right"). // Valid values are MixedTextOrientation (0), UprightTextOrientation (1), and SidewaysTextOrientation (2). -// If the second argument (subviewID) is 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 GetVerticalTextOrientation(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, VerticalTextOrientation, MixedTextOrientation, true) } @@ -761,7 +807,9 @@ func BlurViewByID(viewID string, session Session) { } // GetCurrent returns the index of the selected item (<0 if there is no a selected item) or the current view index (StackLayout, TabsLayout). -// 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 GetCurrent(view View, subviewID ...string) int { defaultValue := -1 if view = getSubview(view, subviewID); view != nil { @@ -775,7 +823,9 @@ func GetCurrent(view View, subviewID ...string) int { } // IsUserSelect returns "true" if the user can select text, "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 IsUserSelect(view View, subviewID ...string) bool { if view = getSubview(view, subviewID); view != nil { value, _ := isUserSelect(view) @@ -821,7 +871,8 @@ func isUserSelect(view View) (bool, bool) { // BlendSoftLight (9), BlendDifference (10), BlendExclusion (11), BlendHue (12), // BlendSaturation (13), BlendColor (14), BlendLuminosity (15) // -// 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 GetMixBlendMode(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, MixBlendMode, BlendNormal, true) } @@ -833,13 +884,16 @@ func GetMixBlendMode(view View, subviewID ...string) int { // BlendSoftLight (9), BlendDifference (10), BlendExclusion (11), BlendHue (12), // BlendSaturation (13), BlendColor (14), BlendLuminosity (15) // -// 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 GetBackgroundBlendMode(view View, subviewID ...string) int { return enumStyledProperty(view, subviewID, BackgroundBlendMode, BlendNormal, true) } // GetTooltip returns a tooltip text of the 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 GetTooltip(view View, subviewID ...string) string { if view = getSubview(view, subviewID); view != nil { if value := view.Get(Tooltip); value != nil { From 4b002998784cd97dd22cd86605c4e34aff82e833 Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:24:53 +0300 Subject: [PATCH 07/21] Improved binding for 2 args listeners --- colorPicker.go | 21 +- datePicker.go | 21 +- dropDownList.go | 21 +- editView.go | 17 +- events.go | 591 ++-------------------------------------------- events2arg.go | 339 ++++++++++++++++++++++++++ numberPicker.go | 21 +- tableView.go | 19 +- tableViewUtils.go | 26 +- tabsLayout.go | 21 +- timePicker.go | 21 +- 11 files changed, 502 insertions(+), 616 deletions(-) create mode 100644 events2arg.go diff --git a/colorPicker.go b/colorPicker.go index f205094..66ecf34 100644 --- a/colorPicker.go +++ b/colorPicker.go @@ -106,13 +106,13 @@ func (picker *colorPickerData) propertyChanged(tag PropertyName) { color := GetColorPickerValue(picker) picker.Session().callFunc("setInputValue", picker.htmlID(), color.rgbString()) - if listeners := GetColorChangedListeners(picker); len(listeners) > 0 { + if listeners := getTwoArgEventListeners[ColorPicker, Color](picker, nil, ColorChangedEvent); len(listeners) > 0 { oldColor := Color(0) if value := picker.getRaw("old-color"); value != nil { oldColor = value.(Color) } for _, listener := range listeners { - listener(picker, color, oldColor) + listener.Run(picker, color, oldColor) } } @@ -156,8 +156,8 @@ func (picker *colorPickerData) handleCommand(self View, command PropertyName, da oldColor := GetColorPickerValue(picker) picker.properties[ColorPickerValue] = color if color != oldColor { - for _, listener := range GetColorChangedListeners(picker) { - listener(picker, color, oldColor) + for _, listener := range getTwoArgEventListeners[ColorPicker, Color](picker, nil, ColorChangedEvent) { + listener.Run(picker, color, oldColor) } if listener, ok := picker.changeListener[ColorPickerValue]; ok { listener(picker, ColorPickerValue) @@ -194,8 +194,17 @@ func GetColorPickerValue(view View, subviewID ...string) Color { // GetColorChangedListeners returns the ColorChangedListener list of an ColorPicker subview. // If there are no listeners then the empty list is returned // +// Result elements can be of the following types: +// - func(rui.ColorPicker, rui.Color, rui.Color), +// - 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) []func(ColorPicker, Color, Color) { - return getTwoArgEventListeners[ColorPicker, Color](view, subviewID, ColorChangedEvent) +func GetColorChangedListeners(view View, subviewID ...string) []any { + return getTwoArgEventRawListeners[ColorPicker, Color](view, subviewID, ColorChangedEvent) } diff --git a/datePicker.go b/datePicker.go index 54da1c4..3cfe639 100644 --- a/datePicker.go +++ b/datePicker.go @@ -274,7 +274,7 @@ func (picker *datePickerData) propertyChanged(tag PropertyName) { date := GetDatePickerValue(picker) session.callFunc("setInputValue", picker.htmlID(), date.Format(dateFormat)) - if listeners := GetDateChangedListeners(picker); len(listeners) > 0 { + if listeners := getTwoArgEventListeners[DatePicker, time.Time](picker, nil, DateChangedEvent); len(listeners) > 0 { oldDate := time.Now() if value := picker.getRaw("old-date"); value != nil { if date, ok := value.(time.Time); ok { @@ -282,7 +282,7 @@ func (picker *datePickerData) propertyChanged(tag PropertyName) { } } for _, listener := range listeners { - listener(picker, date, oldDate) + listener.Run(picker, date, oldDate) } } @@ -348,8 +348,8 @@ func (picker *datePickerData) handleCommand(self View, command PropertyName, dat oldValue := GetDatePickerValue(picker) picker.properties[DatePickerValue] = value if value != oldValue { - for _, listener := range GetDateChangedListeners(picker) { - listener(picker, value, oldValue) + for _, listener := range getTwoArgEventListeners[DatePicker, time.Time](picker, nil, DateChangedEvent) { + listener.Run(picker, value, oldValue) } if listener, ok := picker.changeListener[DatePickerValue]; ok { listener(picker, DatePickerValue) @@ -445,8 +445,17 @@ func GetDatePickerValue(view View, subviewID ...string) time.Time { // GetDateChangedListeners returns the DateChangedListener list of an DatePicker subview. // If there are no listeners then the empty list is returned // +// Result elements can be of the following types: +// - func(rui.DatePicker, time.Time, time.Time), +// - 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) []func(DatePicker, time.Time, time.Time) { - return getTwoArgEventListeners[DatePicker, time.Time](view, subviewID, DateChangedEvent) +func GetDateChangedListeners(view View, subviewID ...string) []any { + return getTwoArgEventRawListeners[DatePicker, time.Time](view, subviewID, DateChangedEvent) } diff --git a/dropDownList.go b/dropDownList.go index 830a655..1dd6802 100644 --- a/dropDownList.go +++ b/dropDownList.go @@ -104,8 +104,8 @@ func (list *dropDownListData) propertyChanged(tag PropertyName) { list.Session().callFunc("selectDropDownListItem", list.htmlID(), current) oldCurrent, _ := intProperty(list, "old-current", list.Session(), -1) - for _, listener := range GetDropDownListeners(list) { - listener(list, current, oldCurrent) + for _, listener := range getTwoArgEventListeners[DropDownList, int](list, nil, DropDownEvent) { + listener.Run(list, current, oldCurrent) } default: @@ -245,8 +245,8 @@ func (list *dropDownListData) handleCommand(self View, command PropertyName, dat if GetCurrent(list) != number && number >= 0 && number < len(items) { old := GetCurrent(list) list.properties[Current] = number - for _, listener := range GetDropDownListeners(list) { - listener(list, number, old) + for _, listener := range getTwoArgEventListeners[DropDownList, int](list, nil, DropDownEvent) { + listener.Run(list, number, old) } if listener, ok := list.changeListener[Current]; ok { listener(list, Current) @@ -265,10 +265,19 @@ 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. // +// Result elements can be of the following types: +// - func(rui.DropDownList, int, int), +// - 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) []func(DropDownList, int, int) { - return getTwoArgEventListeners[DropDownList, int](view, subviewID, DropDownEvent) +func GetDropDownListeners(view View, subviewID ...string) []any { + return getTwoArgEventRawListeners[DropDownList, int](view, subviewID, DropDownEvent) } // GetDropDownItems return the DropDownList items list. diff --git a/editView.go b/editView.go index ece94fe..5040e29 100644 --- a/editView.go +++ b/editView.go @@ -268,8 +268,8 @@ func (edit *editViewData) AppendText(text string) { } func (edit *editViewData) textChanged(newText, oldText string) { - for _, listener := range GetTextChangedListeners(edit) { - listener(edit, newText, oldText) + for _, listener := range getTwoArgEventListeners[EditView, string](edit, nil, EditTextChangedEvent) { + listener.Run(edit, newText, oldText) } if listener, ok := edit.changeListener[Text]; ok { listener(edit, Text) @@ -461,10 +461,19 @@ func IsSpellcheck(view View, subviewID ...string) bool { // GetTextChangedListeners returns the TextChangedListener list of an EditView or MultiLineEditView subview. // If there are no listeners then the empty list is returned // +// Result elements can be of the following types: +// - func(rui.EditView, string, string), +// - 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) []func(EditView, string, string) { - return getTwoArgEventListeners[EditView, string](view, subviewID, EditTextChangedEvent) +func GetTextChangedListeners(view View, subviewID ...string) []any { + return getTwoArgEventRawListeners[EditView, string](view, subviewID, EditTextChangedEvent) } // GetEditViewType returns a value of the Type property of EditView. diff --git a/events.go b/events.go index f663c47..f8319b2 100644 --- a/events.go +++ b/events.go @@ -41,161 +41,32 @@ var eventJsFunc = map[PropertyName]struct{ jsEvent, jsFunc string }{ DragLeaveEvent: {jsEvent: "ondragleave", jsFunc: "dragLeaveEvent"}, } -/* -type oneArgListener[V View, E any] interface { - call(V, E) - listener() func(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]) call(_ V, _ E) { - data.fn() -} - -func (data *oneArgListener0[V, E]) listener() func(V, E) { - return data.call -} - -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]) call(view V, _ E) { - data.fn(view) -} - -func (data *oneArgListenerV[V, E]) listener() func(V, E) { - return data.call -} - -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]) call(_ V, event E) { - data.fn(event) -} - -func (data *oneArgListenerE[V, E]) listener() func(V, E) { - return data.call -} - -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]) call(view V, arg E) { - data.fn(view, arg) -} - -func (data *oneArgListenerVE[V, E]) listener() func(V, E) { - return data.fn -} - -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]) call(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 inType == reflect.TypeOf(view) { - args = []reflect.Value{reflect.ValueOf(view)} - } else if inType == reflect.TypeOf(event) { - args = []reflect.Value{reflect.ValueOf(event)} - } - - case 2: - if methodType.In(0) == reflect.TypeOf(view) && methodType.In(1) == reflect.TypeOf(event) { - args = []reflect.Value{reflect.ValueOf(view), reflect.ValueOf(event)} +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)" `) + } } } +} - if args != nil { - method.Call(args) - } else { - ErrorLogF(`Unsupported prototype of "%s" method`, data.name) +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)") + } } } -func (data *oneArgListenerBinding[V, E]) listener() func(V, E) { - return data.call -} - -func (data *oneArgListenerBinding[V, E]) rawListener() any { - return data.name -} -*/ - func valueToNoArgEventListeners[V any](view View, value any) ([]func(V), bool) { if value == nil { return nil, true @@ -303,326 +174,6 @@ func valueToNoArgEventListeners[V any](view View, value any) ([]func(V), bool) { return nil, false } -/* - func valueToOneArgEventListeners[V View, E any](view View, value any) ([]oneArgListener[V, E], bool) { - if value == nil { - return nil, true - } - - switch value := value.(type) { - case string: - return []oneArgListener[V, E]{newOneArgListenerBinding[V, E](value)}, true - - case func(V, E): - return []oneArgListener[V, E]{newOneArgListenerVE[V, E](value)}, true - - case func(V): - return []oneArgListener[V, E]{newOneArgListenerV[V, E](value)}, true - - case func(E): - return []oneArgListener[V, E]{newOneArgListenerE[V, E](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[V, E](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, E](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, E](v)) - - case func(E): - result = append(result, newOneArgListenerE[V, E](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 valueToTwoArgEventListeners[V View, E any](view View, value any) ([]func(V, E, E), bool) { - if value == nil { - return nil, true - } - - switch value := value.(type) { - case string: - fn := func(view V, val1 E, val2 E) { - bind := view.binding() - if bind == nil { - ErrorLogF(`There is no a binding object for call "%s"`, value) - return - } - - val := reflect.ValueOf(bind) - method := val.MethodByName(value) - if !method.IsValid() { - ErrorLogF(`The "%s" method is not valid`, value) - return - } - - methodType := method.Type() - var args []reflect.Value = nil - switch methodType.NumIn() { - case 0: - args = []reflect.Value{} - - case 1: - inType := methodType.In(0) - if inType == reflect.TypeOf(view) { - args = []reflect.Value{reflect.ValueOf(view)} - } else if inType == reflect.TypeOf(val1) { - args = []reflect.Value{reflect.ValueOf(val1)} - } - - case 2: - valType := reflect.TypeOf(val1) - if methodType.In(0) == reflect.TypeOf(view) && methodType.In(1) == valType { - args = []reflect.Value{reflect.ValueOf(view), reflect.ValueOf(val1)} - } else if methodType.In(0) == valType && methodType.In(1) == valType { - args = []reflect.Value{reflect.ValueOf(val1), reflect.ValueOf(val2)} - } - - case 3: - valType := reflect.TypeOf(val1) - if methodType.In(0) == reflect.TypeOf(view) && methodType.In(1) == valType && methodType.In(2) == valType { - args = []reflect.Value{reflect.ValueOf(view), reflect.ValueOf(val1), reflect.ValueOf(val2)} - } - } - - if args != nil { - method.Call(args) - } else { - ErrorLogF(`Unsupported prototype of "%s" method`, value) - } - } - return []func(V, E, E){fn}, true - - case func(V, E, E): - return []func(V, E, E){value}, true - - case func(V, E): - fn := func(v V, val, _ E) { - value(v, val) - } - return []func(V, E, E){fn}, true - - case func(E, E): - fn := func(_ V, val, old E) { - value(val, old) - } - return []func(V, E, E){fn}, true - - case func(E): - fn := func(_ V, val, _ E) { - value(val) - } - return []func(V, E, E){fn}, true - - case func(V): - fn := func(v V, _, _ E) { - value(v) - } - return []func(V, E, E){fn}, true - - case func(): - fn := func(V, E, E) { - value() - } - return []func(V, E, E){fn}, true - - case []func(V, E, E): - if len(value) == 0 { - return nil, true - } - for _, fn := range value { - if fn == nil { - return nil, false - } - } - return value, true - - case []func(V, E): - count := len(value) - if count == 0 { - return nil, true - } - listeners := make([]func(V, E, E), count) - for i, fn := range value { - if fn == nil { - return nil, false - } - listeners[i] = func(view V, val, _ E) { - fn(view, val) - } - } - return listeners, true - - case []func(E): - count := len(value) - if count == 0 { - return nil, true - } - listeners := make([]func(V, E, E), count) - for i, fn := range value { - if fn == nil { - return nil, false - } - listeners[i] = func(_ V, val, _ E) { - fn(val) - } - } - return listeners, true - - case []func(E, E): - count := len(value) - if count == 0 { - return nil, true - } - listeners := make([]func(V, E, E), count) - for i, fn := range value { - if fn == nil { - return nil, false - } - listeners[i] = func(_ V, val, old E) { - fn(val, old) - } - } - return listeners, true - - case []func(V): - count := len(value) - if count == 0 { - return nil, true - } - listeners := make([]func(V, E, E), count) - for i, fn := range value { - if fn == nil { - return nil, false - } - listeners[i] = func(view V, _, _ E) { - fn(view) - } - } - return listeners, true - - case []func(): - count := len(value) - if count == 0 { - return nil, true - } - listeners := make([]func(V, E, E), count) - for i, fn := range value { - if fn == nil { - return nil, false - } - listeners[i] = func(V, E, E) { - fn() - } - } - return listeners, true - - case []any: - count := len(value) - if count == 0 { - return nil, true - } - listeners := make([]func(V, E, E), count) - for i, v := range value { - if v == nil { - return nil, false - } - switch fn := v.(type) { - case func(V, E, E): - listeners[i] = fn - - case func(V, E): - listeners[i] = func(view V, val, _ E) { - fn(view, val) - } - - case func(E, E): - listeners[i] = func(_ V, val, old E) { - fn(val, old) - } - - case func(E): - listeners[i] = func(_ V, val, _ E) { - fn(val) - } - - case func(V): - listeners[i] = func(view V, _, _ E) { - fn(view) - } - - case func(): - listeners[i] = func(V, E, E) { - fn() - } - - default: - return nil, false - } - } - return listeners, true - } - - return nil, false -} - func getNoArgEventListeners[V View](view View, subviewID []string, tag PropertyName) []func(V) { if view = getSubview(view, subviewID); view != nil { if value := view.Get(tag); value != nil { @@ -634,38 +185,6 @@ func getNoArgEventListeners[V View](view View, subviewID []string, tag PropertyN return []func(V){} } -/* - 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.Get(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 getTwoArgEventListeners[V View, E any](view View, subviewID []string, tag PropertyName) []func(V, E, E) { - if view = getSubview(view, subviewID); view != nil { - if value := view.Get(tag); value != nil { - if result, ok := value.([]func(V, E, E)); ok { - return result - } - } - } - return []func(V, E, E){} -} - func setNoArgEventListener[V View](view View, tag PropertyName, value any) []PropertyName { if listeners, ok := valueToNoArgEventListeners[V](view, value); ok { if len(listeners) > 0 { @@ -680,73 +199,3 @@ func setNoArgEventListener[V View](view View, tag PropertyName, value any) []Pro notCompatibleType(tag, value) return nil } - -/* -func setOneArgEventListener[V View, T any](view View, tag PropertyName, value any) []PropertyName { - if listeners, ok := valueToOneArgEventListeners[V, T](view, value); ok { - if len(listeners) > 0 { - view.setRaw(tag, listeners) - } else if view.getRaw(tag) != nil { - view.setRaw(tag, nil) - } else { - return []PropertyName{} - } - return []PropertyName{tag} - } - notCompatibleType(tag, value) - return nil -} -*/ - -func setTwoArgEventListener[V View, T any](view View, tag PropertyName, value any) []PropertyName { - listeners, ok := valueToTwoArgEventListeners[V, T](view, value) - if !ok { - notCompatibleType(tag, value) - return nil - } - if len(listeners) > 0 { - view.setRaw(tag, listeners) - } else if view.getRaw(tag) != nil { - view.setRaw(tag, nil) - } else { - return []PropertyName{} - } - return []PropertyName{tag} -} - -func viewEventsHtml[T any](view View, events []PropertyName, buffer *strings.Builder) { - for _, tag := range events { - if 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)" `) - } - } - /*if value := view.getRaw(tag); value != nil { - if js, ok := eventJsFunc[tag]; ok { - if listeners, ok := value.([] func(View, T)); ok && len(listeners) > 0 { - buffer.WriteString(js.jsEvent) - buffer.WriteString(`="`) - buffer.WriteString(js.jsFunc) - buffer.WriteString(`(this, event)" `) - } - } - } - */ - } -} - -func updateEventListenerHtml(view View, tag PropertyName) { - if js, ok := eventJsFunc[tag]; ok { - value := view.getRaw(tag) - session := view.Session() - htmlID := view.htmlID() - if value == nil { - session.removeProperty(view.htmlID(), js.jsEvent) - } else { - session.updateProperty(htmlID, js.jsEvent, js.jsFunc+"(this, event)") - } - } -} diff --git a/events2arg.go b/events2arg.go new file mode 100644 index 0000000..2ad256e --- /dev/null +++ b/events2arg.go @@ -0,0 +1,339 @@ +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 inType == reflect.TypeOf(view) { + args = []reflect.Value{reflect.ValueOf(view)} + } else if inType == reflect.TypeOf(arg1) { + args = []reflect.Value{reflect.ValueOf(arg1)} + } + + case 2: + valType := reflect.TypeOf(arg1) + if methodType.In(0) == reflect.TypeOf(view) && methodType.In(1) == valType { + args = []reflect.Value{reflect.ValueOf(view), reflect.ValueOf(arg1)} + } else if methodType.In(0) == valType && methodType.In(1) == valType { + args = []reflect.Value{reflect.ValueOf(arg1), reflect.ValueOf(arg2)} + } + + case 3: + valType := reflect.TypeOf(arg1) + if methodType.In(0) == reflect.TypeOf(view) && methodType.In(1) == valType && methodType.In(2) == valType { + 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 { + if len(listeners) > 0 { + view.setRaw(tag, listeners) + } else if view.getRaw(tag) != nil { + view.setRaw(tag, nil) + } else { + return []PropertyName{} + } + return []PropertyName{tag} + } + 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.Get(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 +} diff --git a/numberPicker.go b/numberPicker.go index 17963b0..a2ec402 100644 --- a/numberPicker.go +++ b/numberPicker.go @@ -201,7 +201,7 @@ func (picker *numberPickerData) propertyChanged(tag PropertyName) { format := picker.numberFormat() picker.Session().callFunc("setInputValue", picker.htmlID(), fmt.Sprintf(format, value)) - if listeners := GetNumberChangedListeners(picker); len(listeners) > 0 { + if listeners := getTwoArgEventListeners[NumberPicker, float64](picker, nil, NumberChangedEvent); len(listeners) > 0 { old := 0.0 if val := picker.getRaw("old-number"); val != nil { if n, ok := val.(float64); ok { @@ -210,7 +210,7 @@ func (picker *numberPickerData) propertyChanged(tag PropertyName) { } if old != value { for _, listener := range listeners { - listener(picker, value, old) + listener.Run(picker, value, old) } } } @@ -280,8 +280,8 @@ func (picker *numberPickerData) handleCommand(self View, command PropertyName, d oldValue := GetNumberPickerValue(picker) picker.properties[NumberPickerValue] = text if value != oldValue { - for _, listener := range GetNumberChangedListeners(picker) { - listener(picker, value, oldValue) + for _, listener := range getTwoArgEventListeners[NumberPicker, float64](picker, nil, NumberChangedEvent) { + listener.Run(picker, value, oldValue) } if listener, ok := picker.changeListener[NumberPickerValue]; ok { listener(picker, NumberPickerValue) @@ -359,10 +359,19 @@ func GetNumberPickerValue(view View, subviewID ...string) float64 { // GetNumberChangedListeners returns the NumberChangedListener list of an NumberPicker subview. // If there are no listeners then the empty list is returned // +// Result elements can be of the following types: +// - func(rui.NumberPicker, float64, float64), +// - 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) []func(NumberPicker, float64, float64) { - return getTwoArgEventListeners[NumberPicker, float64](view, subviewID, NumberChangedEvent) +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. diff --git a/tableView.go b/tableView.go index 0bd784b..20c147e 100644 --- a/tableView.go +++ b/tableView.go @@ -845,8 +845,17 @@ func (table *tableViewData) propertyChanged(tag PropertyName) { current := tableViewCurrent(table) session.callFunc("setTableCellCursorByID", htmlID, current.Row, current.Column) + for _, listener := range getTwoArgEventListeners[TableView, int](table, nil, TableCellSelectedEvent) { + listener.Run(table, current.Row, current.Column) + } + case RowSelection: - session.callFunc("setTableRowCursorByID", htmlID, tableViewCurrent(table).Row) + current := tableViewCurrent(table) + session.callFunc("setTableRowCursorByID", htmlID, current.Row) + + for _, listener := range getOneArgEventListeners[TableView, int](table, nil, TableRowSelectedEvent) { + listener.Run(table, current.Row) + } } case Gap: @@ -1687,8 +1696,8 @@ func (table *tableViewData) handleCommand(self View, command PropertyName, data listener(table, Current) } - for _, listener := range GetTableCellSelectedListeners(table) { - listener(table, row, column) + for _, listener := range getTwoArgEventListeners[TableView, int](table, nil, TableCellSelectedEvent) { + listener.Run(table, row, column) } } } @@ -1704,8 +1713,8 @@ func (table *tableViewData) handleCommand(self View, command PropertyName, data case "cellClick": if row, ok := dataIntProperty(data, "row"); ok { if column, ok := dataIntProperty(data, "column"); ok { - for _, listener := range GetTableCellClickedListeners(table) { - listener(table, row, column) + for _, listener := range getTwoArgEventListeners[TableView, int](table, nil, TableCellClickedEvent) { + listener.Run(table, row, column) } } } diff --git a/tableViewUtils.go b/tableViewUtils.go index 69b77e9..3d1da55 100644 --- a/tableViewUtils.go +++ b/tableViewUtils.go @@ -149,19 +149,37 @@ func GetTableCurrent(view View, subviewID ...string) CellIndex { // 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. // +// Result elements can be of the following types: +// - func(rui.TableView, int, int), +// - func(rui.TableView, int), +// - func(rui.TableView), +// - 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 GetTableCellClickedListeners(view View, subviewID ...string) []func(TableView, int, int) { - return getTwoArgEventListeners[TableView, int](view, subviewID, TableCellClickedEvent) +func GetTableCellClickedListeners(view View, subviewID ...string) []any { + return getTwoArgEventRawListeners[TableView, int](view, subviewID, TableCellClickedEvent) } // GetTableCellSelectedListeners returns listeners of event which occurs when a table cell becomes selected. // If there are no listeners then the empty list is returned. // +// Result elements can be of the following types: +// - func(rui.TableView, int, int), +// - func(rui.TableView, int), +// - func(rui.TableView), +// - 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 GetTableCellSelectedListeners(view View, subviewID ...string) []func(TableView, int, int) { - return getTwoArgEventListeners[TableView, int](view, subviewID, TableCellSelectedEvent) +func GetTableCellSelectedListeners(view View, subviewID ...string) []any { + return getTwoArgEventRawListeners[TableView, int](view, subviewID, TableCellSelectedEvent) } // GetTableRowClickedListeners returns listeners of event which occurs when the user clicks on a table row. diff --git a/tabsLayout.go b/tabsLayout.go index 5fb6b40..07bc95c 100644 --- a/tabsLayout.go +++ b/tabsLayout.go @@ -211,7 +211,7 @@ func (tabsLayout *tabsLayoutData) propertyChanged(tag PropertyName) { if listeners := getTwoArgEventListeners[TabsLayout, int](tabsLayout, nil, CurrentTabChangedEvent); len(listeners) > 0 { oldCurrent, _ := intProperty(tabsLayout, "old-current", session, -1) for _, listener := range listeners { - listener(tabsLayout, current, oldCurrent) + listener.Run(tabsLayout, current, oldCurrent) } } @@ -427,7 +427,7 @@ func (tabsLayout *tabsLayoutData) Insert(view View, index int) { func (tabsLayout *tabsLayoutData) currentChanged(newCurrent, oldCurrent int) { for _, listener := range getTwoArgEventListeners[TabsLayout, int](tabsLayout, nil, CurrentTabChangedEvent) { - listener(tabsLayout, newCurrent, oldCurrent) + listener.Run(tabsLayout, newCurrent, oldCurrent) } if listener, ok := tabsLayout.changeListener[Current]; ok { listener(tabsLayout, Current) @@ -766,3 +766,20 @@ func (tabsLayout *tabsLayoutData) handleCommand(self View, command PropertyName, func GetTabCloseEventListeners(view View, subviewID ...string) []any { return getOneArgEventRawListeners[TabsLayout, int](view, subviewID, TabCloseEvent) } + +// GetCurrentTabChangedEventListeners returns the "current-tab-changed" listener list. If there are no listeners then the empty list is returned. +// +// Result elements can be of the following types: +// - func(rui.TabsLayout, int, int), +// - func(rui.TabsLayout, int), +// - func(rui.TabsLayout), +// - 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 GetCurrentTabChangedEventListeners(view View, subviewID ...string) []any { + return getTwoArgEventRawListeners[TabsLayout, int](view, subviewID, CurrentTabChangedEvent) +} diff --git a/timePicker.go b/timePicker.go index 9d8dea2..2b35e8a 100644 --- a/timePicker.go +++ b/timePicker.go @@ -246,7 +246,7 @@ func (picker *timePickerData) propertyChanged(tag PropertyName) { value := GetTimePickerValue(picker) session.callFunc("setInputValue", picker.htmlID(), value.Format(timeFormat)) - if listeners := GetTimeChangedListeners(picker); len(listeners) > 0 { + if listeners := getTwoArgEventListeners[TimePicker, time.Time](picker, nil, TimeChangedEvent); len(listeners) > 0 { oldTime := time.Now() if val := picker.getRaw("old-time"); val != nil { if time, ok := val.(time.Time); ok { @@ -254,7 +254,7 @@ func (picker *timePickerData) propertyChanged(tag PropertyName) { } } for _, listener := range listeners { - listener(picker, value, oldTime) + listener.Run(picker, value, oldTime) } } @@ -320,8 +320,8 @@ func (picker *timePickerData) handleCommand(self View, command PropertyName, dat oldValue := GetTimePickerValue(picker) picker.properties[TimePickerValue] = value if value != oldValue { - for _, listener := range GetTimeChangedListeners(picker) { - listener(picker, value, oldValue) + for _, listener := range getTwoArgEventListeners[TimePicker, time.Time](picker, nil, TimeChangedEvent) { + listener.Run(picker, value, oldValue) } if listener, ok := picker.changeListener[TimePickerValue]; ok { listener(picker, TimePickerValue) @@ -418,8 +418,17 @@ func GetTimePickerValue(view View, subviewID ...string) time.Time { // GetTimeChangedListeners returns the TimeChangedListener list of an TimePicker subview. // If there are no listeners then the empty list is returned // +// Result elements can be of the following types: +// - func(rui.TimePicker, time.Time, time.Time), +// - func(rui.TimePicker, time.Time), +// - func(rui.TimePicker), +// - 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 GetTimeChangedListeners(view View, subviewID ...string) []func(TimePicker, time.Time, time.Time) { - return getTwoArgEventListeners[TimePicker, time.Time](view, subviewID, TimeChangedEvent) +func GetTimeChangedListeners(view View, subviewID ...string) []any { + return getTwoArgEventRawListeners[TimePicker, time.Time](view, subviewID, TimeChangedEvent) } From f2fb9483252d65427934490e7029e769f8eeb33c Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Wed, 18 Jun 2025 14:19:57 +0300 Subject: [PATCH 08/21] Improved binding for no args listeners --- events.go | 178 +++++++++++++++++++++++++++++++++++++++++++++++-- focusEvents.go | 18 +++-- imageView.go | 32 ++++++++- popup.go | 160 +++++++++++++++++++++++++++++++++++++++++++- view.go | 4 +- 5 files changed, 374 insertions(+), 18 deletions(-) diff --git a/events.go b/events.go index f8319b2..8b086a7 100644 --- a/events.go +++ b/events.go @@ -67,6 +67,96 @@ func updateEventListenerHtml(view View, tag PropertyName) { } } +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: + inType := methodType.In(0) + if inType == 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 (data *noArgListenerBinding[V]) rawListener() any { + return data.name +} + +/* func valueToNoArgEventListeners[V any](view View, value any) ([]func(V), bool) { if value == nil { return nil, true @@ -173,20 +263,74 @@ func valueToNoArgEventListeners[V any](view View, value any) ([]func(V), bool) { return nil, false } +*/ -func getNoArgEventListeners[V View](view View, subviewID []string, tag PropertyName) []func(V) { - if view = getSubview(view, subviewID); view != nil { - if value := view.Get(tag); value != nil { - if result, ok := value.([]func(V)); ok { - return result +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 []func(V){} + + return nil, false } func setNoArgEventListener[V View](view View, tag PropertyName, value any) []PropertyName { - if listeners, ok := valueToNoArgEventListeners[V](view, value); ok { + if listeners, ok := valueToNoArgEventListeners[V](value); ok { if len(listeners) > 0 { view.setRaw(tag, listeners) } else if view.getRaw(tag) != nil { @@ -199,3 +343,23 @@ func setNoArgEventListener[V View](view View, tag PropertyName, value any) []Pro 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.Get(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 +} diff --git a/focusEvents.go b/focusEvents.go index e091cbc..9d09ff8 100644 --- a/focusEvents.go +++ b/focusEvents.go @@ -50,16 +50,26 @@ func focusEventsHtml(view View, buffer *strings.Builder) { // GetFocusListeners returns a FocusListener list. If there are no listeners then the empty list is returned // +// Result elements can be of the following types: +// - func(rui.View), +// - 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) []func(View) { - return getNoArgEventListeners[View](view, subviewID, FocusEvent) +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 // +// Result elements can be of the following types: +// - func(rui.View), +// - 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) []func(View) { - return getNoArgEventListeners[View](view, subviewID, LostFocusEvent) +func GetLostFocusListeners(view View, subviewID ...string) []any { + return getNoArgEventRawListeners[View](view, subviewID, LostFocusEvent) } diff --git a/imageView.go b/imageView.go index 9053748..2be78c5 100644 --- a/imageView.go +++ b/imageView.go @@ -300,7 +300,7 @@ func (imageView *imageViewData) handleCommand(self View, command PropertyName, d switch command { case "imageViewError": for _, listener := range getNoArgEventListeners[ImageView](imageView, nil, ErrorEvent) { - listener(imageView) + listener.Run(imageView) } case "imageViewLoaded": @@ -309,7 +309,7 @@ func (imageView *imageViewData) handleCommand(self View, command PropertyName, d imageView.currentSrc, _ = data.PropertyValue("current-src") for _, listener := range getNoArgEventListeners[ImageView](imageView, nil, LoadedEvent) { - listener(imageView) + listener.Run(imageView) } default: @@ -370,3 +370,31 @@ func GetImageViewVerticalAlign(view View, subviewID ...string) int { func GetImageViewHorizontalAlign(view View, subviewID ...string) int { 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) +} diff --git a/popup.go b/popup.go index cc77b41..1b50ec8 100644 --- a/popup.go +++ b/popup.go @@ -2,6 +2,7 @@ package rui import ( "fmt" + "reflect" "strings" ) @@ -281,13 +282,30 @@ type Popup interface { dissmissAnimation(listener func(PropertyName)) bool } +type popupListener interface { + Run(Popup) + rawListener() any +} + +type popupListener0 struct { + fn func() +} + +type popupListener1 struct { + fn func(Popup) +} + +type popupListenerBinding struct { + name string +} + type popupData struct { layerView GridLayout popupView GridLayout contentView View buttons []PopupButton cancelable bool - dismissListener []func(Popup) + dismissListener []popupListener showTransform TransformProperty showOpacity float64 showDuration float64 @@ -649,7 +667,7 @@ func (popup *popupData) init(view View, popupParams Params) { } case DismissEvent: - if listeners, ok := valueToNoArgEventListeners[Popup](popup.contentView, value); ok { + if listeners, ok := valueToPopupEventListeners(value); ok { if listeners != nil { popup.dismissListener = listeners } @@ -887,7 +905,7 @@ func (popup *popupData) onDismiss() { popup.Session().callFunc("removeView", popup.layerView.htmlID()) for _, listener := range popup.dismissListener { - listener(popup) + listener.Run(popup) } } @@ -1014,3 +1032,139 @@ func (manager *popupManager) dismissPopup(popup Popup) { listener("") } } + +func newPopupListener0(fn func()) popupListener { + obj := new(popupListener0) + obj.fn = fn + return obj +} + +func (data *popupListener0) Run(_ Popup) { + data.fn() +} + +func (data *popupListener0) rawListener() any { + return data.fn +} + +func newPopupListener1(fn func(Popup)) popupListener { + obj := new(popupListener1) + obj.fn = fn + return obj +} + +func (data *popupListener1) Run(popup Popup) { + data.fn(popup) +} + +func (data *popupListener1) rawListener() any { + return data.fn +} + +func newPopupListenerBinding(name string) popupListener { + obj := new(popupListenerBinding) + obj.name = name + return obj +} + +func (data *popupListenerBinding) Run(popup Popup) { + bind := popup.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 inType == reflect.TypeOf(popup) { + args = []reflect.Value{reflect.ValueOf(popup)} + } + } + + if args != nil { + method.Call(args) + } else { + ErrorLogF(`Unsupported prototype of "%s" method`, data.name) + } +} + +func (data *popupListenerBinding) rawListener() any { + return data.name +} + +func valueToPopupEventListeners(value any) ([]popupListener, bool) { + if value == nil { + return nil, true + } + + switch value := value.(type) { + case []popupListener: + return value, true + + case popupListener: + return []popupListener{value}, true + + case string: + return []popupListener{newPopupListenerBinding(value)}, true + + case func(Popup): + return []popupListener{newPopupListener1(value)}, true + + case func(): + return []popupListener{newPopupListener0(value)}, true + + case []func(Popup): + result := make([]popupListener, 0, len(value)) + for _, fn := range value { + if fn != nil { + result = append(result, newPopupListener1(fn)) + } + } + return result, len(result) > 0 + + case []func(): + result := make([]popupListener, 0, len(value)) + for _, fn := range value { + if fn != nil { + result = append(result, newPopupListener0(fn)) + } + } + return result, len(result) > 0 + + case []any: + result := make([]popupListener, 0, len(value)) + for _, v := range value { + if v != nil { + switch v := v.(type) { + case func(Popup): + result = append(result, newPopupListener1(v)) + + case func(): + result = append(result, newPopupListener0(v)) + + case string: + result = append(result, newPopupListenerBinding(v)) + + default: + return nil, false + } + } + } + return result, len(result) > 0 + } + + return nil, false +} diff --git a/view.go b/view.go index 2b2ab01..92a7107 100644 --- a/view.go +++ b/view.go @@ -1162,13 +1162,13 @@ func (view *viewData) handleCommand(self View, command PropertyName, data DataOb case FocusEvent: view.hasFocus = true for _, listener := range getNoArgEventListeners[View](view, nil, command) { - listener(self) + listener.Run(self) } case LostFocusEvent: view.hasFocus = false for _, listener := range getNoArgEventListeners[View](view, nil, command) { - listener(self) + listener.Run(self) } case TransitionRunEvent, TransitionStartEvent, TransitionEndEvent, TransitionCancelEvent: From 24aeeb515be25c6767da514361bf95593511df13 Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Thu, 19 Jun 2025 14:51:40 +0300 Subject: [PATCH 09/21] Added binding support for MediaPlayer error event --- events.go | 9 +- events1arg.go | 9 +- events2arg.go | 9 +- mediaPlayer.go | 610 +++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 495 insertions(+), 142 deletions(-) diff --git a/events.go b/events.go index 8b086a7..21b232f 100644 --- a/events.go +++ b/events.go @@ -331,14 +331,7 @@ func valueToNoArgEventListeners[V View](value any) ([]noArgListener[V], bool) { func setNoArgEventListener[V View](view View, tag PropertyName, value any) []PropertyName { if listeners, ok := valueToNoArgEventListeners[V](value); ok { - if len(listeners) > 0 { - view.setRaw(tag, listeners) - } else if view.getRaw(tag) != nil { - view.setRaw(tag, nil) - } else { - return []PropertyName{} - } - return []PropertyName{tag} + return setArrayPropertyValue(view, tag, listeners) } notCompatibleType(tag, value) return nil diff --git a/events1arg.go b/events1arg.go index 77004b3..8e233d0 100644 --- a/events1arg.go +++ b/events1arg.go @@ -232,14 +232,7 @@ func valueToOneArgEventListeners[V View, E any](value any) ([]oneArgListener[V, func setOneArgEventListener[V View, T any](view View, tag PropertyName, value any) []PropertyName { if listeners, ok := valueToOneArgEventListeners[V, T](value); ok { - if len(listeners) > 0 { - view.setRaw(tag, listeners) - } else if view.getRaw(tag) != nil { - view.setRaw(tag, nil) - } else { - return []PropertyName{} - } - return []PropertyName{tag} + return setArrayPropertyValue(view, tag, listeners) } notCompatibleType(tag, value) return nil diff --git a/events2arg.go b/events2arg.go index 2ad256e..8608a28 100644 --- a/events2arg.go +++ b/events2arg.go @@ -305,14 +305,7 @@ func valueToTwoArgEventListeners[V View, E any](value any) ([]twoArgListener[V, func setTwoArgEventListener[V View, T any](view View, tag PropertyName, value any) []PropertyName { if listeners, ok := valueToTwoArgEventListeners[V, T](value); ok { - if len(listeners) > 0 { - view.setRaw(tag, listeners) - } else if view.getRaw(tag) != nil { - view.setRaw(tag, nil) - } else { - return []PropertyName{} - } - return []PropertyName{tag} + return setArrayPropertyValue(view, tag, listeners) } notCompatibleType(tag, value) return nil diff --git a/mediaPlayer.go b/mediaPlayer.go index 83a5c9a..41db0e6 100644 --- a/mediaPlayer.go +++ b/mediaPlayer.go @@ -3,6 +3,7 @@ package rui import ( "fmt" "math" + "reflect" "strconv" "strings" ) @@ -488,7 +489,7 @@ const ( // - player - Interface of a player which generated this event, // - code - Error code. See below, // - message - Error message, - + // // Error codes: // - 0 (PlayerErrorUnknown) - Unknown error, // - 1 (PlayerErrorAborted) - Fetching the associated resource was interrupted by a user request, @@ -610,7 +611,7 @@ func (player *mediaPlayerData) setFunc(tag PropertyName, value any) []PropertyNa return setOneArgEventListener[MediaPlayer, float64](player, tag, value) case PlayerErrorEvent: - if listeners, ok := valueToPlayerErrorListeners(value); ok { + if listeners, ok := valueToMediaPlayerErrorListeners(value); ok { return setArrayPropertyValue(player, tag, listeners) } notCompatibleType(tag, value) @@ -677,131 +678,132 @@ func setMediaPlayerSource(properties Properties, value any) []PropertyName { return []PropertyName{Source} } -func valueToPlayerErrorListeners(value any) ([]func(MediaPlayer, int, string), bool) { - if value == nil { - return nil, true - } - - switch value := value.(type) { - case func(MediaPlayer, int, string): - return []func(MediaPlayer, int, string){value}, true - - case func(int, string): - fn := func(_ MediaPlayer, code int, message string) { - value(code, message) - } - return []func(MediaPlayer, int, string){fn}, true - - case func(MediaPlayer): - fn := func(player MediaPlayer, _ int, _ string) { - value(player) - } - return []func(MediaPlayer, int, string){fn}, true - - case func(): - fn := func(MediaPlayer, int, string) { - value() - } - return []func(MediaPlayer, int, string){fn}, true - - case []func(MediaPlayer, int, string): - if len(value) == 0 { +/* + func valueToPlayerErrorListeners(value any) ([]func(MediaPlayer, int, string), bool) { + if value == nil { return nil, true } - for _, fn := range value { - if fn == nil { - return nil, false - } - } - return value, true - case []func(int, string): - count := len(value) - if count == 0 { - return nil, true - } - listeners := make([]func(MediaPlayer, int, string), count) - for i, v := range value { - if v == nil { - return nil, false - } - listeners[i] = func(_ MediaPlayer, code int, message string) { - v(code, message) - } - } - return listeners, true + switch value := value.(type) { + case func(MediaPlayer, int, string): + return []func(MediaPlayer, int, string){value}, true - case []func(MediaPlayer): - count := len(value) - if count == 0 { - return nil, true - } - listeners := make([]func(MediaPlayer, int, string), count) - for i, v := range value { - if v == nil { - return nil, false + case func(int, string): + fn := func(_ MediaPlayer, code int, message string) { + value(code, message) } - listeners[i] = func(player MediaPlayer, _ int, _ string) { - v(player) - } - } - return listeners, true + return []func(MediaPlayer, int, string){fn}, true - case []func(): - count := len(value) - if count == 0 { - return nil, true - } - listeners := make([]func(MediaPlayer, int, string), count) - for i, v := range value { - if v == nil { - return nil, false + case func(MediaPlayer): + fn := func(player MediaPlayer, _ int, _ string) { + value(player) } - listeners[i] = func(MediaPlayer, int, string) { - v() - } - } - return listeners, true + return []func(MediaPlayer, int, string){fn}, true - case []any: - count := len(value) - if count == 0 { - return nil, true - } - listeners := make([]func(MediaPlayer, int, string), count) - for i, v := range value { - if v == nil { - return nil, false + case func(): + fn := func(MediaPlayer, int, string) { + value() } - switch v := v.(type) { - case func(MediaPlayer, int, string): - listeners[i] = v + return []func(MediaPlayer, int, string){fn}, true - case func(int, string): + case []func(MediaPlayer, int, string): + if len(value) == 0 { + return nil, true + } + for _, fn := range value { + if fn == nil { + return nil, false + } + } + return value, true + + case []func(int, string): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(MediaPlayer, int, string), count) + for i, v := range value { + if v == nil { + return nil, false + } listeners[i] = func(_ MediaPlayer, code int, message string) { v(code, message) } + } + return listeners, true - case func(MediaPlayer): + case []func(MediaPlayer): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(MediaPlayer, int, string), count) + for i, v := range value { + if v == nil { + return nil, false + } listeners[i] = func(player MediaPlayer, _ int, _ string) { v(player) } + } + return listeners, true - case func(): + case []func(): + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(MediaPlayer, int, string), count) + for i, v := range value { + if v == nil { + return nil, false + } listeners[i] = func(MediaPlayer, int, string) { v() } - - default: - return nil, false } + return listeners, true + + case []any: + count := len(value) + if count == 0 { + return nil, true + } + listeners := make([]func(MediaPlayer, int, string), count) + for i, v := range value { + if v == nil { + return nil, false + } + switch v := v.(type) { + case func(MediaPlayer, int, string): + listeners[i] = v + + case func(int, string): + listeners[i] = func(_ MediaPlayer, code int, message string) { + v(code, message) + } + + case func(MediaPlayer): + listeners[i] = func(player MediaPlayer, _ int, _ string) { + v(player) + } + + case func(): + listeners[i] = func(MediaPlayer, int, string) { + v() + } + + default: + return nil, false + } + } + return listeners, true } - return listeners, true + + return nil, false } - - return nil, false -} - +*/ func mediaPlayerEvents() map[PropertyName]string { return map[PropertyName]string{ AbortEvent: "onabort", @@ -981,31 +983,23 @@ func (player *mediaPlayerData) handleCommand(self View, command PropertyName, da PlayingEvent, ProgressEvent, SeekedEvent, SeekingEvent, StalledEvent, SuspendEvent, WaitingEvent: - if value := player.getRaw(command); value != nil { - if listeners, ok := value.([]func(MediaPlayer)); ok { - for _, listener := range listeners { - listener(player) - } - } + for _, listener := range getNoArgEventListeners[MediaPlayer](player, nil, command) { + listener.Run(player) } case TimeUpdateEvent, DurationChangedEvent, RateChangedEvent, VolumeChangedEvent: - if value := player.getRaw(command); value != nil { - if listeners, ok := value.([]func(MediaPlayer, float64)); ok { - time := dataFloatProperty(data, "value") - for _, listener := range listeners { - listener(player, time) - } - } + time := dataFloatProperty(data, "value") + for _, listener := range getOneArgEventListeners[MediaPlayer, float64](player, nil, command) { + listener.Run(player, time) } case PlayerErrorEvent: if value := player.getRaw(command); value != nil { - if listeners, ok := value.([]func(MediaPlayer, int, string)); ok { + if listeners, ok := value.([]mediaPlayerErrorListener); ok { code, _ := dataIntProperty(data, "code") message, _ := data.PropertyValue("message") for _, listener := range listeners { - listener(player, code, message) + listener.Run(player, code, message) } } } @@ -1254,3 +1248,383 @@ func IsMediaPlayerPaused(view View, playerID string) bool { ErrorLog(`The found View is not MediaPlayer`) return false } + +type mediaPlayerErrorListener interface { + Run(MediaPlayer, int, string) + rawListener() any +} + +type mediaPlayerErrorListener0 struct { + fn func() +} + +type mediaPlayerErrorListenerP struct { + fn func(MediaPlayer) +} + +type mediaPlayerErrorListenerI struct { + fn func(int) +} + +type mediaPlayerErrorListenerS struct { + fn func(string) +} + +type mediaPlayerErrorListenerPI struct { + fn func(MediaPlayer, int) +} + +type mediaPlayerErrorListenerPS struct { + fn func(MediaPlayer, string) +} + +type mediaPlayerErrorListenerIS struct { + fn func(int, string) +} + +type mediaPlayerErrorListenerPIS struct { + fn func(MediaPlayer, int, string) +} + +type mediaPlayerErrorListenerBinding struct { + name string +} + +func newMediaPlayerErrorListener0(fn func()) mediaPlayerErrorListener { + obj := new(mediaPlayerErrorListener0) + obj.fn = fn + return obj +} + +func (data *mediaPlayerErrorListener0) Run(_ MediaPlayer, _ int, _ string) { + data.fn() +} + +func (data *mediaPlayerErrorListener0) rawListener() any { + return data.fn +} + +func newMediaPlayerErrorListenerP(fn func(MediaPlayer)) mediaPlayerErrorListener { + obj := new(mediaPlayerErrorListenerP) + obj.fn = fn + return obj +} + +func (data *mediaPlayerErrorListenerP) Run(player MediaPlayer, _ int, _ string) { + data.fn(player) +} + +func (data *mediaPlayerErrorListenerP) rawListener() any { + return data.fn +} + +func newMediaPlayerErrorListenerI(fn func(int)) mediaPlayerErrorListener { + obj := new(mediaPlayerErrorListenerI) + obj.fn = fn + return obj +} + +func (data *mediaPlayerErrorListenerI) Run(_ MediaPlayer, code int, _ string) { + data.fn(code) +} + +func (data *mediaPlayerErrorListenerI) rawListener() any { + return data.fn +} + +func newMediaPlayerErrorListenerS(fn func(string)) mediaPlayerErrorListener { + obj := new(mediaPlayerErrorListenerS) + obj.fn = fn + return obj +} + +func (data *mediaPlayerErrorListenerS) Run(_ MediaPlayer, _ int, message string) { + data.fn(message) +} + +func (data *mediaPlayerErrorListenerS) rawListener() any { + return data.fn +} + +func newMediaPlayerErrorListenerPI(fn func(MediaPlayer, int)) mediaPlayerErrorListener { + obj := new(mediaPlayerErrorListenerPI) + obj.fn = fn + return obj +} + +func (data *mediaPlayerErrorListenerPI) Run(player MediaPlayer, code int, _ string) { + data.fn(player, code) +} + +func (data *mediaPlayerErrorListenerPI) rawListener() any { + return data.fn +} + +func newMediaPlayerErrorListenerPS(fn func(MediaPlayer, string)) mediaPlayerErrorListener { + obj := new(mediaPlayerErrorListenerPS) + obj.fn = fn + return obj +} + +func (data *mediaPlayerErrorListenerPS) Run(player MediaPlayer, _ int, message string) { + data.fn(player, message) +} + +func (data *mediaPlayerErrorListenerPS) rawListener() any { + return data.fn +} + +func newMediaPlayerErrorListenerIS(fn func(int, string)) mediaPlayerErrorListener { + obj := new(mediaPlayerErrorListenerIS) + obj.fn = fn + return obj +} + +func (data *mediaPlayerErrorListenerIS) Run(_ MediaPlayer, code int, message string) { + data.fn(code, message) +} + +func (data *mediaPlayerErrorListenerIS) rawListener() any { + return data.fn +} + +func newMediaPlayerErrorListenerPIS(fn func(MediaPlayer, int, string)) mediaPlayerErrorListener { + obj := new(mediaPlayerErrorListenerPIS) + obj.fn = fn + return obj +} + +func (data *mediaPlayerErrorListenerPIS) Run(player MediaPlayer, code int, message string) { + data.fn(player, code, message) +} + +func (data *mediaPlayerErrorListenerPIS) rawListener() any { + return data.fn +} + +func newMediaPlayerErrorListenerBinding(name string) mediaPlayerErrorListener { + obj := new(mediaPlayerErrorListenerBinding) + obj.name = name + return obj +} + +func (data *mediaPlayerErrorListenerBinding) Run(player MediaPlayer, code int, message string) { + bind := player.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: + switch methodType.In(0) { + case reflect.TypeOf(player): + args = []reflect.Value{reflect.ValueOf(player)} + case reflect.TypeOf(code): + args = []reflect.Value{reflect.ValueOf(code)} + case reflect.TypeOf(message): + args = []reflect.Value{reflect.ValueOf(message)} + } + + case 2: + in0 := methodType.In(0) + in1 := methodType.In(1) + if in0 == reflect.TypeOf(player) { + if in1 == reflect.TypeOf(code) { + args = []reflect.Value{reflect.ValueOf(player), reflect.ValueOf(code)} + } else if in1 == reflect.TypeOf(message) { + args = []reflect.Value{reflect.ValueOf(player), reflect.ValueOf(message)} + } + } else if in0 == reflect.TypeOf(code) && in1 == reflect.TypeOf(message) { + args = []reflect.Value{reflect.ValueOf(code), reflect.ValueOf(message)} + } + + case 3: + if methodType.In(0) == reflect.TypeOf(player) && + methodType.In(1) == reflect.TypeOf(code) && + methodType.In(2) == reflect.TypeOf(message) { + args = []reflect.Value{ + reflect.ValueOf(player), + reflect.ValueOf(code), + reflect.ValueOf(message), + } + } + } + + if args != nil { + method.Call(args) + } else { + ErrorLogF(`Unsupported prototype of "%s" method`, data.name) + } +} + +func (data *mediaPlayerErrorListenerBinding) rawListener() any { + return data.name +} + +func valueToMediaPlayerErrorListeners(value any) ([]mediaPlayerErrorListener, bool) { + if value == nil { + return nil, true + } + + switch value := value.(type) { + case []mediaPlayerErrorListener: + return value, true + + case mediaPlayerErrorListener: + return []mediaPlayerErrorListener{value}, true + + case string: + return []mediaPlayerErrorListener{newMediaPlayerErrorListenerBinding(value)}, true + + case func(): + return []mediaPlayerErrorListener{newMediaPlayerErrorListener0(value)}, true + + case func(MediaPlayer): + return []mediaPlayerErrorListener{newMediaPlayerErrorListenerP(value)}, true + + case func(int): + return []mediaPlayerErrorListener{newMediaPlayerErrorListenerI(value)}, true + + case func(string): + return []mediaPlayerErrorListener{newMediaPlayerErrorListenerS(value)}, true + + case func(MediaPlayer, int): + return []mediaPlayerErrorListener{newMediaPlayerErrorListenerPI(value)}, true + + case func(MediaPlayer, string): + return []mediaPlayerErrorListener{newMediaPlayerErrorListenerPS(value)}, true + + case func(int, string): + return []mediaPlayerErrorListener{newMediaPlayerErrorListenerIS(value)}, true + + case func(MediaPlayer, int, string): + return []mediaPlayerErrorListener{newMediaPlayerErrorListenerPIS(value)}, true + + case []func(): + result := make([]mediaPlayerErrorListener, 0, len(value)) + for _, fn := range value { + if fn != nil { + result = append(result, newMediaPlayerErrorListener0(fn)) + } + } + return result, len(result) > 0 + + case []func(MediaPlayer): + result := make([]mediaPlayerErrorListener, 0, len(value)) + for _, fn := range value { + if fn != nil { + result = append(result, newMediaPlayerErrorListenerP(fn)) + } + } + return result, len(result) > 0 + + case []func(int): + result := make([]mediaPlayerErrorListener, 0, len(value)) + for _, fn := range value { + if fn != nil { + result = append(result, newMediaPlayerErrorListenerI(fn)) + } + } + return result, len(result) > 0 + + case []func(string): + result := make([]mediaPlayerErrorListener, 0, len(value)) + for _, fn := range value { + if fn != nil { + result = append(result, newMediaPlayerErrorListenerS(fn)) + } + } + return result, len(result) > 0 + + case []func(MediaPlayer, int): + result := make([]mediaPlayerErrorListener, 0, len(value)) + for _, fn := range value { + if fn != nil { + result = append(result, newMediaPlayerErrorListenerPI(fn)) + } + } + return result, len(result) > 0 + + case []func(MediaPlayer, string): + result := make([]mediaPlayerErrorListener, 0, len(value)) + for _, fn := range value { + if fn != nil { + result = append(result, newMediaPlayerErrorListenerPS(fn)) + } + } + return result, len(result) > 0 + + case []func(int, string): + result := make([]mediaPlayerErrorListener, 0, len(value)) + for _, fn := range value { + if fn != nil { + result = append(result, newMediaPlayerErrorListenerIS(fn)) + } + } + return result, len(result) > 0 + + case []func(MediaPlayer, int, string): + result := make([]mediaPlayerErrorListener, 0, len(value)) + for _, fn := range value { + if fn != nil { + result = append(result, newMediaPlayerErrorListenerPIS(fn)) + } + } + return result, len(result) > 0 + + case []any: + result := make([]mediaPlayerErrorListener, 0, len(value)) + for _, v := range value { + if v != nil { + switch v := v.(type) { + case func(): + result = append(result, newMediaPlayerErrorListener0(v)) + + case func(MediaPlayer): + result = append(result, newMediaPlayerErrorListenerP(v)) + + case func(int): + result = append(result, newMediaPlayerErrorListenerI(v)) + + case func(string): + result = append(result, newMediaPlayerErrorListenerS(v)) + + case func(MediaPlayer, int): + result = append(result, newMediaPlayerErrorListenerPI(v)) + + case func(MediaPlayer, string): + result = append(result, newMediaPlayerErrorListenerPS(v)) + + case func(int, string): + result = append(result, newMediaPlayerErrorListenerIS(v)) + + case func(MediaPlayer, int, string): + result = append(result, newMediaPlayerErrorListenerPIS(v)) + + case string: + result = append(result, newMediaPlayerErrorListenerBinding(v)) + + default: + return nil, false + } + } + } + return result, len(result) > 0 + } + + return nil, false +} From 2f07584b372b67b549be9d39b452fad865309dd3 Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Thu, 19 Jun 2025 17:31:39 +0300 Subject: [PATCH 10/21] Added binding to View.String() --- dragAndDrop.go | 54 +++++++++++++ events.go | 119 +++------------------------ events1arg.go | 10 +++ events2arg.go | 10 +++ mediaPlayer.go | 10 +++ viewStyle.go | 216 +++++++++++++++++++++++++++++++++++++++++++------ 6 files changed, 286 insertions(+), 133 deletions(-) diff --git a/dragAndDrop.go b/dragAndDrop.go index f0fa70e..0c1a006 100644 --- a/dragAndDrop.go +++ b/dragAndDrop.go @@ -84,7 +84,16 @@ const ( // 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. @@ -93,7 +102,16 @@ const ( // 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. @@ -102,7 +120,16 @@ const ( // 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. @@ -111,7 +138,16 @@ const ( // 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. @@ -120,7 +156,16 @@ const ( // 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. @@ -129,7 +174,16 @@ const ( // 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). diff --git a/events.go b/events.go index 21b232f..204fe0f 100644 --- a/events.go +++ b/events.go @@ -156,115 +156,6 @@ func (data *noArgListenerBinding[V]) rawListener() any { return data.name } -/* -func valueToNoArgEventListeners[V any](view View, value any) ([]func(V), bool) { - if value == nil { - return nil, true - } - - switch value := value.(type) { - case string: - fn := func(arg V) { - bind := view.binding() - if bind == nil { - ErrorLogF(`There is no a binding object for call "%s"`, value) - return - } - - val := reflect.ValueOf(bind) - method := val.MethodByName(value) - if !method.IsValid() { - ErrorLogF(`The "%s" method is not valid`, value) - return - } - - methodType := method.Type() - var args []reflect.Value = nil - switch methodType.NumIn() { - case 0: - args = []reflect.Value{} - - case 1: - inType := methodType.In(0) - if inType == reflect.TypeOf(arg) { - args = []reflect.Value{reflect.ValueOf(arg)} - } - } - - if args != nil { - method.Call(args) - } else { - ErrorLogF(`Unsupported prototype of "%s" method`, value) - } - } - return []func(V){fn}, true - - case func(V): - return []func(V){value}, true - - case func(): - fn := func(V) { - value() - } - return []func(V){fn}, true - - case []func(V): - if len(value) == 0 { - return nil, true - } - for _, fn := range value { - if fn == nil { - return nil, false - } - } - return value, true - - case []func(): - count := len(value) - if count == 0 { - return nil, true - } - listeners := make([]func(V), count) - for i, v := range value { - if v == nil { - return nil, false - } - listeners[i] = func(V) { - v() - } - } - return listeners, true - - case []any: - count := len(value) - if count == 0 { - return nil, true - } - listeners := make([]func(V), count) - for i, v := range value { - if v == nil { - return nil, false - } - switch v := v.(type) { - case func(V): - listeners[i] = v - - case func(): - listeners[i] = func(V) { - v() - } - - default: - return nil, false - } - } - return listeners, true - } - - return nil, false -} -*/ - func valueToNoArgEventListeners[V View](value any) ([]noArgListener[V], bool) { if value == nil { return nil, true @@ -356,3 +247,13 @@ func getNoArgEventRawListeners[V View](view View, subviewID []string, tag Proper } 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 "" +} diff --git a/events1arg.go b/events1arg.go index 8e233d0..02ad212 100644 --- a/events1arg.go +++ b/events1arg.go @@ -257,3 +257,13 @@ func getOneArgEventRawListeners[V View, E any](view View, subviewID []string, ta } 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 "" +} diff --git a/events2arg.go b/events2arg.go index 8608a28..54aeea2 100644 --- a/events2arg.go +++ b/events2arg.go @@ -330,3 +330,13 @@ func getTwoArgEventRawListeners[V View, E any](view View, subviewID []string, ta } 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 "" +} diff --git a/mediaPlayer.go b/mediaPlayer.go index 41db0e6..c2a48de 100644 --- a/mediaPlayer.go +++ b/mediaPlayer.go @@ -1628,3 +1628,13 @@ func valueToMediaPlayerErrorListeners(value any) ([]mediaPlayerErrorListener, bo return nil, false } + +func getMediaPlayerErrorListenerBinding(listeners []mediaPlayerErrorListener) string { + for _, listener := range listeners { + raw := listener.rawListener() + if text, ok := raw.(string); ok && text != "" { + return text + } + } + return "" +} diff --git a/viewStyle.go b/viewStyle.go index 4986545..2d269ac 100644 --- a/viewStyle.go +++ b/viewStyle.go @@ -2,9 +2,10 @@ package rui import ( "fmt" - "sort" + "slices" "strconv" "strings" + "time" ) // ViewStyle interface of the style of view @@ -551,26 +552,118 @@ func viewStyleGet(style Properties, tag PropertyName) any { } func supportedPropertyValue(value any) bool { - switch value.(type) { - case string: + switch value := value.(type) { + case string, bool, float32, float64, int, stringWriter, fmt.Stringer: + return true + case []string: - case bool: - case float32: - case float64: - case int: - case stringWriter: - case fmt.Stringer: + return len(value) > 0 + case []ShadowProperty: + return len(value) > 0 + case []View: + return len(value) > 0 + case []any: + return len(value) > 0 + case []BackgroundElement: + return len(value) > 0 + case []BackgroundGradientPoint: + return len(value) > 0 + case []BackgroundGradientAngle: + return len(value) > 0 + case map[PropertyName]AnimationProperty: + return len(value) > 0 + + case []noArgListener[View]: + return getNoArgBinding(value) != "" + + case []noArgListener[ImageView]: + return getNoArgBinding(value) != "" + + case []noArgListener[MediaPlayer]: + return getNoArgBinding(value) != "" + + case []oneArgListener[View, KeyEvent]: + return getOneArgBinding(value) != "" + + case []oneArgListener[View, MouseEvent]: + return getOneArgBinding(value) != "" + + case []oneArgListener[View, TouchEvent]: + return getOneArgBinding(value) != "" + + case []oneArgListener[View, PointerEvent]: + return getOneArgBinding(value) != "" + + case []oneArgListener[View, PropertyName]: + return getOneArgBinding(value) != "" + + case []oneArgListener[View, string]: + return getOneArgBinding(value) != "" + + case []oneArgListener[View, Frame]: + return getOneArgBinding(value) != "" + + case []oneArgListener[View, DragAndDropEvent]: + return getOneArgBinding(value) != "" + + case []oneArgListener[Checkbox, bool]: + return getOneArgBinding(value) != "" + + case []oneArgListener[FilePicker, []FileInfo]: + return getOneArgBinding(value) != "" + + case []oneArgListener[ListView, int]: + return getOneArgBinding(value) != "" + + case []oneArgListener[ListView, []int]: + return getOneArgBinding(value) != "" + + case []oneArgListener[MediaPlayer, float64]: + return getOneArgBinding(value) != "" + + case []oneArgListener[TableView, int]: + return getOneArgBinding(value) != "" + + case []oneArgListener[TabsLayout, int]: + return getOneArgBinding(value) != "" + + case []twoArgListener[ColorPicker, Color]: + return getTwoArgBinding(value) != "" + + case []twoArgListener[DatePicker, time.Time]: + return getTwoArgBinding(value) != "" + + case []twoArgListener[TimePicker, time.Time]: + return getTwoArgBinding(value) != "" + + case []twoArgListener[DropDownList, int]: + return getTwoArgBinding(value) != "" + + case []twoArgListener[EditView, string]: + return getTwoArgBinding(value) != "" + + case []twoArgListener[NumberPicker, float64]: + return getTwoArgBinding(value) != "" + + case []twoArgListener[TableView, int]: + return getTwoArgBinding(value) != "" + + case []twoArgListener[TabsLayout, int]: + return getTwoArgBinding(value) != "" + + case []mediaPlayerErrorListener: + return getMediaPlayerErrorListenerBinding(value) != "" + default: return false } - return true } func writePropertyValue(buffer *strings.Builder, tag PropertyName, value any, indent string) { @@ -791,9 +884,7 @@ func writePropertyValue(buffer *strings.Builder, tag PropertyName, value any, in for tag := range value { tags = append(tags, tag) } - sort.Slice(tags, func(i, j int) bool { - return tags[i] < tags[j] - }) + slices.Sort(tags) buffer.WriteString("[\n") indent2 := indent + "\t" for _, tag := range tags { @@ -806,6 +897,87 @@ func writePropertyValue(buffer *strings.Builder, tag PropertyName, value any, in buffer.WriteString(indent) buffer.WriteRune(']') } + + case []noArgListener[View]: + buffer.WriteString(getNoArgBinding(value)) + + case []noArgListener[ImageView]: + buffer.WriteString(getNoArgBinding(value)) + + case []noArgListener[MediaPlayer]: + buffer.WriteString(getNoArgBinding(value)) + + case []oneArgListener[View, KeyEvent]: + buffer.WriteString(getOneArgBinding(value)) + + case []oneArgListener[View, MouseEvent]: + buffer.WriteString(getOneArgBinding(value)) + + case []oneArgListener[View, TouchEvent]: + buffer.WriteString(getOneArgBinding(value)) + + case []oneArgListener[View, PointerEvent]: + buffer.WriteString(getOneArgBinding(value)) + + case []oneArgListener[View, PropertyName]: + buffer.WriteString(getOneArgBinding(value)) + + case []oneArgListener[View, string]: + buffer.WriteString(getOneArgBinding(value)) + + case []oneArgListener[View, Frame]: + buffer.WriteString(getOneArgBinding(value)) + + case []oneArgListener[View, DragAndDropEvent]: + buffer.WriteString(getOneArgBinding(value)) + + case []oneArgListener[Checkbox, bool]: + buffer.WriteString(getOneArgBinding(value)) + + case []oneArgListener[FilePicker, []FileInfo]: + buffer.WriteString(getOneArgBinding(value)) + + case []oneArgListener[ListView, int]: + buffer.WriteString(getOneArgBinding(value)) + + case []oneArgListener[ListView, []int]: + buffer.WriteString(getOneArgBinding(value)) + + case []oneArgListener[MediaPlayer, float64]: + buffer.WriteString(getOneArgBinding(value)) + + case []oneArgListener[TableView, int]: + buffer.WriteString(getOneArgBinding(value)) + + case []oneArgListener[TabsLayout, int]: + buffer.WriteString(getOneArgBinding(value)) + + case []twoArgListener[ColorPicker, Color]: + buffer.WriteString(getTwoArgBinding(value)) + + case []twoArgListener[DatePicker, time.Time]: + buffer.WriteString(getTwoArgBinding(value)) + + case []twoArgListener[TimePicker, time.Time]: + buffer.WriteString(getTwoArgBinding(value)) + + case []twoArgListener[DropDownList, int]: + buffer.WriteString(getTwoArgBinding(value)) + + case []twoArgListener[EditView, string]: + buffer.WriteString(getTwoArgBinding(value)) + + case []twoArgListener[NumberPicker, float64]: + buffer.WriteString(getTwoArgBinding(value)) + + case []twoArgListener[TableView, int]: + buffer.WriteString(getTwoArgBinding(value)) + + case []twoArgListener[TabsLayout, int]: + buffer.WriteString(getTwoArgBinding(value)) + + case []mediaPlayerErrorListener: + buffer.WriteString(getMediaPlayerErrorListenerBinding(value)) } } @@ -815,19 +987,15 @@ func writeViewStyle(name string, view Properties, buffer *strings.Builder, inden indent += "\t" writeProperty := func(tag PropertyName, value any) { - for _, exclude := range excludeTags { - if exclude == tag { - return + if !slices.Contains(excludeTags, tag) { + if supportedPropertyValue(value) { + buffer.WriteString(indent) + buffer.WriteString(string(tag)) + buffer.WriteString(" = ") + writePropertyValue(buffer, tag, value, indent) + buffer.WriteString(",\n") } } - - if supportedPropertyValue(value) { - buffer.WriteString(indent) - buffer.WriteString(string(tag)) - buffer.WriteString(" = ") - writePropertyValue(buffer, tag, value, indent) - buffer.WriteString(",\n") - } } tags := view.AllTags() From cb4d197bb74f1b17bf0b9834a15572db726a45d0 Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:36:44 +0300 Subject: [PATCH 11/21] Bug fixing --- viewStyle.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/viewStyle.go b/viewStyle.go index 2d269ac..3984fbc 100644 --- a/viewStyle.go +++ b/viewStyle.go @@ -733,8 +733,8 @@ func writePropertyValue(buffer *strings.Builder, tag PropertyName, value any, in writeString(text) buffer.WriteString(",\n") } + buffer.WriteString(indent) } - buffer.WriteString(indent) buffer.WriteRune(']') } From d633c80155ae9d79c2ca4077ac6882c857239fdf Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:56:42 +0300 Subject: [PATCH 12/21] Optimisation --- data.go | 6 +++--- filePicker.go | 2 +- viewStyle.go | 4 ++-- viewsContainer.go | 7 ++++--- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/data.go b/data.go index b1cec3d..fa87fa7 100644 --- a/data.go +++ b/data.go @@ -384,7 +384,8 @@ func ParseDataText(text string) DataObject { parseTag := func() (string, bool) { skipSpaces(true) startPos := pos - if data[pos] == '`' { + switch data[pos] { + case '`': pos++ startPos++ for data[pos] != '`' { @@ -398,8 +399,7 @@ func ParseDataText(text string) DataObject { pos++ return str, true - } else if data[pos] == '\'' || data[pos] == '"' { - + case '\'', '"': stopSymbol := data[pos] pos++ startPos++ diff --git a/filePicker.go b/filePicker.go index 9735497..85327cc 100644 --- a/filePicker.go +++ b/filePicker.go @@ -147,7 +147,7 @@ func (picker *filePickerData) Files() []FileInfo { func (picker *filePickerData) LoadFile(file FileInfo, result func(FileInfo, []byte)) { if result != nil { for i, info := range picker.files { - if info.Name == file.Name && info.Size == file.Size && info.LastModified == file.LastModified { + if info.Name == file.Name && info.Size == file.Size && info.LastModified.Equal(file.LastModified) { if info.data != nil { result(info, info.data) } else { diff --git a/viewStyle.go b/viewStyle.go index 3984fbc..3d417c0 100644 --- a/viewStyle.go +++ b/viewStyle.go @@ -746,10 +746,10 @@ func writePropertyValue(buffer *strings.Builder, tag PropertyName, value any, in } case float32: - buffer.WriteString(fmt.Sprintf("%g", float64(value))) + fmt.Fprintf(buffer, "%g", float64(value)) case float64: - buffer.WriteString(fmt.Sprintf("%g", value)) + fmt.Fprintf(buffer, "%g", value) case int: if prop, ok := enumProperties[tag]; ok && value >= 0 && value < len(prop.values) { diff --git a/viewsContainer.go b/viewsContainer.go index 970e069..162c778 100644 --- a/viewsContainer.go +++ b/viewsContainer.go @@ -131,11 +131,12 @@ func (container *viewsContainerData) removeView(index int) View { } view := container.views[index] - if index == 0 { + switch index { + case 0: container.views = container.views[1:] - } else if index == count-1 { + case count - 1: container.views = container.views[:index] - } else { + default: container.views = append(container.views[:index], container.views[index+1:]...) } From 0433f460e48e377331fab2846b6e8e05450b65de Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Sun, 22 Jun 2025 21:04:01 +0300 Subject: [PATCH 13/21] Added change listener binding --- colorPicker.go | 2 +- customView.go | 4 +-- data.go | 13 +++++--- data_test.go | 4 +-- datePicker.go | 2 +- detailsView.go | 2 +- dropDownList.go | 2 +- editView.go | 2 +- events.go | 7 ++-- events1arg.go | 10 +++--- events2arg.go | 19 ++++++----- gridLayout.go | 5 +-- listLayout.go | 5 +-- listView.go | 4 +-- mediaPlayer.go | 22 ++++++------- numberPicker.go | 2 +- stackLayout.go | 19 ++--------- tableView.go | 4 +-- tabsLayout.go | 12 ++----- timePicker.go | 2 +- view.go | 81 ++++++++++++++++++++++++++++++++++++++--------- viewStyle.go | 25 +++++++++++++++ viewsContainer.go | 17 +++++----- 23 files changed, 163 insertions(+), 102 deletions(-) diff --git a/colorPicker.go b/colorPicker.go index 66ecf34..eddd1fa 100644 --- a/colorPicker.go +++ b/colorPicker.go @@ -160,7 +160,7 @@ func (picker *colorPickerData) handleCommand(self View, command PropertyName, da listener.Run(picker, color, oldColor) } if listener, ok := picker.changeListener[ColorPickerValue]; ok { - listener(picker, ColorPickerValue) + listener.Run(picker, ColorPickerValue) } } } diff --git a/customView.go b/customView.go index 35411d6..75b75e3 100644 --- a/customView.go +++ b/customView.go @@ -98,8 +98,8 @@ func (customView *CustomViewData) SetParams(params Params) bool { } // SetChangeListener set the function to track the change of the View property -func (customView *CustomViewData) SetChangeListener(tag PropertyName, listener func(View, PropertyName)) { - customView.superView.SetChangeListener(tag, listener) +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 diff --git a/data.go b/data.go index fa87fa7..ae7f74a 100644 --- a/data.go +++ b/data.go @@ -49,14 +49,17 @@ type DataObject interface { ToParams() Params } +// DataNodeType defines the type of DataNode +type DataNodeType int + // Constants which are used to describe a node type, see [DataNode] const ( // TextNode - node is the pair "tag - text value". Syntax: = - TextNode = 0 + TextNode DataNodeType = 0 // ObjectNode - node is the pair "tag - object". Syntax: = {...} - ObjectNode = 1 + ObjectNode DataNodeType = 1 // ArrayNode - node is the pair "tag - object". Syntax: = [...] - ArrayNode = 2 + ArrayNode DataNodeType = 2 ) // DataNode interface of a data node @@ -65,7 +68,7 @@ type DataNode interface { Tag() string // Type returns a node type. Possible values are TextNode, ObjectNode and ArrayNode - Type() int + Type() DataNodeType // Text returns node text Text() string @@ -253,7 +256,7 @@ func (node *dataNode) Tag() string { return node.tag } -func (node *dataNode) Type() int { +func (node *dataNode) Type() DataNodeType { if node.array != nil { return ArrayNode } diff --git a/data_test.go b/data_test.go index 806e318..2ab257a 100644 --- a/data_test.go +++ b/data_test.go @@ -75,7 +75,7 @@ func TestParseDataText(t *testing.T) { t.Errorf(`obj.PropertyValue("key5") result: ("%s",%v)`, val, ok) } - testKey := func(obj DataObject, index int, tag string, nodeType int) DataNode { + testKey := func(obj DataObject, index int, tag string, nodeType DataNodeType) DataNode { key := obj.Property(index) if key == nil { t.Errorf(`%s.Property(%d) == nil`, obj.Tag(), index) @@ -118,7 +118,7 @@ func TestParseDataText(t *testing.T) { type testKeyData struct { tag string - nodeType int + nodeType DataNodeType } data := []testKeyData{ diff --git a/datePicker.go b/datePicker.go index 3cfe639..f923686 100644 --- a/datePicker.go +++ b/datePicker.go @@ -352,7 +352,7 @@ func (picker *datePickerData) handleCommand(self View, command PropertyName, dat listener.Run(picker, value, oldValue) } if listener, ok := picker.changeListener[DatePickerValue]; ok { - listener(picker, DatePickerValue) + listener.Run(picker, DatePickerValue) } } } diff --git a/detailsView.go b/detailsView.go index 2296083..48701d5 100644 --- a/detailsView.go +++ b/detailsView.go @@ -193,7 +193,7 @@ func (detailsView *detailsViewData) handleCommand(self View, command PropertyNam if n, ok := dataIntProperty(data, "open"); ok { detailsView.properties[Expanded] = (n != 0) if listener, ok := detailsView.changeListener[Expanded]; ok { - listener(detailsView, Expanded) + listener.Run(detailsView, Expanded) } } return true diff --git a/dropDownList.go b/dropDownList.go index 1dd6802..a639bf2 100644 --- a/dropDownList.go +++ b/dropDownList.go @@ -249,7 +249,7 @@ func (list *dropDownListData) handleCommand(self View, command PropertyName, dat listener.Run(list, number, old) } if listener, ok := list.changeListener[Current]; ok { - listener(list, Current) + listener.Run(list, Current) } } } else { diff --git a/editView.go b/editView.go index 5040e29..d318a44 100644 --- a/editView.go +++ b/editView.go @@ -272,7 +272,7 @@ func (edit *editViewData) textChanged(newText, oldText string) { listener.Run(edit, newText, oldText) } if listener, ok := edit.changeListener[Text]; ok { - listener(edit, Text) + listener.Run(edit, Text) } } diff --git a/events.go b/events.go index 204fe0f..05484f4 100644 --- a/events.go +++ b/events.go @@ -139,8 +139,7 @@ func (data *noArgListenerBinding[V]) Run(view V) { args = []reflect.Value{} case 1: - inType := methodType.In(0) - if inType == reflect.TypeOf(view) { + if equalType(methodType.In(0), reflect.TypeOf(view)) { args = []reflect.Value{reflect.ValueOf(view)} } } @@ -152,6 +151,10 @@ func (data *noArgListenerBinding[V]) Run(view V) { } } +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 } diff --git a/events1arg.go b/events1arg.go index 02ad212..8a5217c 100644 --- a/events1arg.go +++ b/events1arg.go @@ -106,6 +106,7 @@ func (data *oneArgListenerBinding[V, E]) Run(view V, event E) { } methodType := method.Type() + var args []reflect.Value = nil switch methodType.NumIn() { case 0: @@ -113,14 +114,15 @@ func (data *oneArgListenerBinding[V, E]) Run(view V, event E) { case 1: inType := methodType.In(0) - if inType == reflect.TypeOf(view) { - args = []reflect.Value{reflect.ValueOf(view)} - } else if inType == reflect.TypeOf(event) { + 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 methodType.In(0) == reflect.TypeOf(view) && methodType.In(1) == reflect.TypeOf(event) { + if equalType(methodType.In(0), reflect.TypeOf(view)) && + equalType(methodType.In(1), reflect.TypeOf(event)) { args = []reflect.Value{reflect.ValueOf(view), reflect.ValueOf(event)} } } diff --git a/events2arg.go b/events2arg.go index 54aeea2..475e03c 100644 --- a/events2arg.go +++ b/events2arg.go @@ -140,6 +140,7 @@ func (data *twoArgListenerBinding[V, E]) Run(view V, arg1 E, arg2 E) { } methodType := method.Type() + var args []reflect.Value = nil switch methodType.NumIn() { case 0: @@ -147,23 +148,25 @@ func (data *twoArgListenerBinding[V, E]) Run(view V, arg1 E, arg2 E) { case 1: inType := methodType.In(0) - if inType == reflect.TypeOf(view) { - args = []reflect.Value{reflect.ValueOf(view)} - } else if inType == reflect.TypeOf(arg1) { + 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: - valType := reflect.TypeOf(arg1) - if methodType.In(0) == reflect.TypeOf(view) && methodType.In(1) == valType { + 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 methodType.In(0) == valType && methodType.In(1) == valType { + } else if equalType(inType0, reflect.TypeOf(arg1)) && equalType(inType1, reflect.TypeOf(arg2)) { args = []reflect.Value{reflect.ValueOf(arg1), reflect.ValueOf(arg2)} } case 3: - valType := reflect.TypeOf(arg1) - if methodType.In(0) == reflect.TypeOf(view) && methodType.In(1) == valType && methodType.In(2) == valType { + 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)} } } diff --git a/gridLayout.go b/gridLayout.go index 619d416..88f4573 100644 --- a/gridLayout.go +++ b/gridLayout.go @@ -458,10 +458,7 @@ func (gridLayout *gridLayoutData) UpdateGridContent() { if gridLayout.created { updateInnerHTML(gridLayout.htmlID(), gridLayout.session) } - - if listener, ok := gridLayout.changeListener[Content]; ok { - listener(gridLayout, Content) - } + gridLayout.contentChanged() } } diff --git a/listLayout.go b/listLayout.go index 48366e8..2952a0c 100644 --- a/listLayout.go +++ b/listLayout.go @@ -196,10 +196,7 @@ func (listLayout *listLayoutData) UpdateContent() { if listLayout.created { updateInnerHTML(listLayout.htmlID(), listLayout.session) } - - if listener, ok := listLayout.changeListener[Content]; ok { - listener(listLayout, Content) - } + listLayout.contentChanged() } } diff --git a/listView.go b/listView.go index e55d4a3..f685114 100644 --- a/listView.go +++ b/listView.go @@ -963,7 +963,7 @@ func (listView *listViewData) handleCurrent(number int) { listener.Run(listView, number) } if listener, ok := listView.changeListener[Current]; ok { - listener(listView, Current) + listener.Run(listView, Current) } } @@ -1022,7 +1022,7 @@ func (listView *listViewData) onItemClick(number int) { setArrayPropertyValue(listView, Checked, checkedItem) if listener, ok := listView.changeListener[Checked]; ok { - listener(listView, Checked) + listener.Run(listView, Checked) } for _, listener := range getOneArgEventListeners[ListView, []int](listView, nil, ListItemCheckedEvent) { diff --git a/mediaPlayer.go b/mediaPlayer.go index c2a48de..a6f1976 100644 --- a/mediaPlayer.go +++ b/mediaPlayer.go @@ -1430,32 +1430,32 @@ func (data *mediaPlayerErrorListenerBinding) Run(player MediaPlayer, code int, m args = []reflect.Value{} case 1: - switch methodType.In(0) { - case reflect.TypeOf(player): + inType := methodType.In(0) + if equalType(inType, reflect.TypeOf(player)) { args = []reflect.Value{reflect.ValueOf(player)} - case reflect.TypeOf(code): + } else if equalType(inType, reflect.TypeOf(code)) { args = []reflect.Value{reflect.ValueOf(code)} - case reflect.TypeOf(message): + } else if equalType(inType, reflect.TypeOf(message)) { args = []reflect.Value{reflect.ValueOf(message)} } case 2: in0 := methodType.In(0) in1 := methodType.In(1) - if in0 == reflect.TypeOf(player) { - if in1 == reflect.TypeOf(code) { + if equalType(in0, reflect.TypeOf(player)) { + if equalType(in1, reflect.TypeOf(code)) { args = []reflect.Value{reflect.ValueOf(player), reflect.ValueOf(code)} - } else if in1 == reflect.TypeOf(message) { + } else if equalType(in1, reflect.TypeOf(message)) { args = []reflect.Value{reflect.ValueOf(player), reflect.ValueOf(message)} } - } else if in0 == reflect.TypeOf(code) && in1 == reflect.TypeOf(message) { + } else if equalType(in0, reflect.TypeOf(code)) && equalType(in1, reflect.TypeOf(message)) { args = []reflect.Value{reflect.ValueOf(code), reflect.ValueOf(message)} } case 3: - if methodType.In(0) == reflect.TypeOf(player) && - methodType.In(1) == reflect.TypeOf(code) && - methodType.In(2) == reflect.TypeOf(message) { + if equalType(methodType.In(0), reflect.TypeOf(player)) && + equalType(methodType.In(1), reflect.TypeOf(code)) && + equalType(methodType.In(2), reflect.TypeOf(message)) { args = []reflect.Value{ reflect.ValueOf(player), reflect.ValueOf(code), diff --git a/numberPicker.go b/numberPicker.go index a2ec402..b7f44c9 100644 --- a/numberPicker.go +++ b/numberPicker.go @@ -284,7 +284,7 @@ func (picker *numberPickerData) handleCommand(self View, command PropertyName, d listener.Run(picker, value, oldValue) } if listener, ok := picker.changeListener[NumberPickerValue]; ok { - listener(picker, NumberPickerValue) + listener.Run(picker, NumberPickerValue) } } } diff --git a/stackLayout.go b/stackLayout.go index 327bc5c..ed9caef 100644 --- a/stackLayout.go +++ b/stackLayout.go @@ -610,12 +610,6 @@ func (layout *stackLayoutData) moveToFrontByIndex(index int, onShow []func(View) session.updateCSSProperty(peekPageID, "transform", transformCSS) } -func (layout *stackLayoutData) contentChanged() { - if listener, ok := layout.changeListener[Content]; ok { - listener(layout, Content) - } -} - func (layout *stackLayoutData) RemovePeek() View { return layout.RemoveView(len(layout.views) - 1) } @@ -683,9 +677,7 @@ func (layout *stackLayoutData) Append(view View) { } session.appendToInnerHTML(stackID, buffer.String()) - if listener, ok := layout.changeListener[Content]; ok { - listener(layout, Content) - } + layout.contentChanged() } } @@ -721,9 +713,7 @@ func (layout *stackLayoutData) Insert(view View, index int) { session := layout.Session() session.appendToInnerHTML(stackID, buffer.String()) - if listener, ok := layout.changeListener[Content]; ok { - listener(layout, Content) - } + layout.contentChanged() } // Remove removes view from list and return it @@ -754,10 +744,7 @@ func (layout *stackLayoutData) RemoveView(index int) View { } layout.Session().callFunc("removeView", view.htmlID()+"page") - - if listener, ok := layout.changeListener[Content]; ok { - listener(layout, Content) - } + layout.contentChanged() return view } diff --git a/tableView.go b/tableView.go index 20c147e..3fabef2 100644 --- a/tableView.go +++ b/tableView.go @@ -1676,7 +1676,7 @@ func (table *tableViewData) handleCommand(self View, command PropertyName, data current.Row = row table.setRaw(Current, current.Row) if listener, ok := table.changeListener[Current]; ok { - listener(table, Current) + listener.Run(table, Current) } for _, listener := range getOneArgEventListeners[TableView, int](table, nil, TableRowSelectedEvent) { @@ -1693,7 +1693,7 @@ func (table *tableViewData) handleCommand(self View, command PropertyName, data current.Column = column table.setRaw(Current, current.Row) if listener, ok := table.changeListener[Current]; ok { - listener(table, Current) + listener.Run(table, Current) } for _, listener := range getTwoArgEventListeners[TableView, int](table, nil, TableCellSelectedEvent) { diff --git a/tabsLayout.go b/tabsLayout.go index 07bc95c..c11eb97 100644 --- a/tabsLayout.go +++ b/tabsLayout.go @@ -403,9 +403,7 @@ func (tabsLayout *tabsLayoutData) Append(view View) { view.SetChangeListener(TabCloseButton, tabsLayout.updateTabCloseButton) if tabsLayout.created { updateInnerHTML(tabsLayout.htmlID(), tabsLayout.Session()) - if listener, ok := tabsLayout.changeListener[Content]; ok { - listener(tabsLayout, Content) - } + tabsLayout.contentChanged() tabsLayout.Set(Current, len(tabsLayout.views)-1) } } @@ -429,9 +427,7 @@ func (tabsLayout *tabsLayoutData) currentChanged(newCurrent, oldCurrent int) { for _, listener := range getTwoArgEventListeners[TabsLayout, int](tabsLayout, nil, CurrentTabChangedEvent) { listener.Run(tabsLayout, newCurrent, oldCurrent) } - if listener, ok := tabsLayout.changeListener[Current]; ok { - listener(tabsLayout, Current) - } + tabsLayout.contentChanged() } // Remove removes view from list and return it @@ -457,9 +453,7 @@ func (tabsLayout *tabsLayoutData) RemoveView(index int) View { tabsLayout.Set(Current, newCurrent) } updateInnerHTML(tabsLayout.htmlID(), tabsLayout.Session()) - if listener, ok := tabsLayout.changeListener[Content]; ok { - listener(tabsLayout, Content) - } + tabsLayout.contentChanged() } else if newCurrent != oldCurrent { tabsLayout.setRaw(Current, newCurrent) } diff --git a/timePicker.go b/timePicker.go index 2b35e8a..db7261e 100644 --- a/timePicker.go +++ b/timePicker.go @@ -324,7 +324,7 @@ func (picker *timePickerData) handleCommand(self View, command PropertyName, dat listener.Run(picker, value, oldValue) } if listener, ok := picker.changeListener[TimePickerValue]; ok { - listener(picker, TimePickerValue) + listener.Run(picker, TimePickerValue) } } diff --git a/view.go b/view.go index 92a7107..44ebe6b 100644 --- a/view.go +++ b/view.go @@ -30,6 +30,8 @@ func (frame Frame) Bottom() float64 { return frame.Top + frame.Height } +const changeListeners PropertyName = "change-listeners" + // View represents a base view interface type View interface { ViewStyle @@ -66,8 +68,18 @@ type View interface { // a description of the error is written to the log SetAnimated(tag PropertyName, value any, animation AnimationProperty) bool - // SetChangeListener set the function to track the change of the View property - SetChangeListener(tag PropertyName, listener func(View, PropertyName)) + // SetChangeListener set the function (the second argument) to track the change of the View property (the first argument). + // + // Allowed listener function formats: + // + // func(view rui.View, property rui.PropertyName) + // func(view rui.View) + // func(property rui.PropertyName) + // func() + // string + // + // If the second argument is given as a string, it specifies the name of the binding function. + SetChangeListener(tag PropertyName, listener any) bool // HasFocus returns 'true' if the view has focus HasFocus() bool @@ -110,7 +122,7 @@ type viewData struct { _htmlID string parentID string systemClass string - changeListener map[PropertyName]func(View, PropertyName) + changeListener map[PropertyName]oneArgListener[View, PropertyName] singleTransition map[PropertyName]AnimationProperty addCSS map[string]string frame Frame @@ -162,7 +174,7 @@ func (view *viewData) init(session Session) { view.changed = view.propertyChanged view.tag = "View" view.session = session - view.changeListener = map[PropertyName]func(View, PropertyName){} + view.changeListener = map[PropertyName]oneArgListener[View, PropertyName]{} view.addCSS = map[string]string{} //view.animation = map[string]AnimationEndListener{} view.singleTransition = map[PropertyName]AnimationProperty{} @@ -242,11 +254,8 @@ func (view *viewData) Remove(tag PropertyName) { if view.created && len(changedTags) > 0 { for _, tag := range changedTags { view.changed(tag) - } - - for _, tag := range changedTags { if listener, ok := view.changeListener[tag]; ok { - listener(view, tag) + listener.Run(view, tag) } } } @@ -273,11 +282,8 @@ func (view *viewData) Set(tag PropertyName, value any) bool { if view.created && len(changedTags) > 0 { for _, tag := range changedTags { view.changed(tag) - } - - for _, tag := range changedTags { if listener, ok := view.changeListener[tag]; ok { - listener(view, tag) + listener.Run(view, tag) } } } @@ -295,7 +301,8 @@ func normalizeViewTag(tag PropertyName) PropertyName { } func (view *viewData) getFunc(tag PropertyName) any { - if tag == ID { + switch tag { + case ID: if id := view.ID(); id != "" { return id } else { @@ -361,6 +368,29 @@ func (view *viewData) setFunc(tag PropertyName, value any) []PropertyName { view.setRaw(Binding, value) return []PropertyName{Binding} + case changeListeners: + switch value := value.(type) { + case DataObject: + for i := range value.PropertyCount() { + node := value.Property(i) + if node.Type() == TextNode { + if text := node.Text(); text != "" { + view.changeListener[PropertyName(node.Tag())] = newOneArgListenerBinding[View, PropertyName](text) + } + } + } + if len(view.changeListener) > 0 { + view.setRaw(changeListeners, view.changeListener) + } + return []PropertyName{tag} + + case DataNode: + if value.Type() == ObjectNode { + return view.setFunc(tag, value.Object()) + } + } + return []PropertyName{} + case Animation: oldAnimations := []AnimationProperty{} if val := view.getRaw(Animation); val != nil { @@ -1251,12 +1281,33 @@ func (view *viewData) handleCommand(self View, command PropertyName, data DataOb } -func (view *viewData) SetChangeListener(tag PropertyName, listener func(View, PropertyName)) { +func (view *viewData) SetChangeListener(tag PropertyName, listener any) bool { if listener == nil { delete(view.changeListener, tag) } else { - view.changeListener[tag] = listener + switch listener := listener.(type) { + case func(): + view.changeListener[tag] = newOneArgListener0[View, PropertyName](listener) + + case func(View): + view.changeListener[tag] = newOneArgListenerV[View, PropertyName](listener) + + case func(PropertyName): + view.changeListener[tag] = newOneArgListenerE[View](listener) + + case func(View, PropertyName): + view.changeListener[tag] = newOneArgListenerVE(listener) + + case string: + view.changeListener[tag] = newOneArgListenerBinding[View, PropertyName](listener) + + default: + return false + } + + view.setRaw(changeListeners, view.changeListener) } + return true } func (view *viewData) HasFocus() bool { diff --git a/viewStyle.go b/viewStyle.go index 3d417c0..db488bb 100644 --- a/viewStyle.go +++ b/viewStyle.go @@ -661,6 +661,14 @@ func supportedPropertyValue(value any) bool { case []mediaPlayerErrorListener: return getMediaPlayerErrorListenerBinding(value) != "" + case map[PropertyName]oneArgListener[View, PropertyName]: + for _, listener := range value { + if text, ok := listener.rawListener().(string); ok && text != "" { + return true + } + } + return false + default: return false } @@ -978,6 +986,23 @@ func writePropertyValue(buffer *strings.Builder, tag PropertyName, value any, in case []mediaPlayerErrorListener: buffer.WriteString(getMediaPlayerErrorListenerBinding(value)) + + case map[PropertyName]oneArgListener[View, PropertyName]: + buffer.WriteString("_{") + for key, listener := range value { + if text, ok := listener.rawListener().(string); ok && text != "" { + buffer.WriteRune('\n') + buffer.WriteString(indent) + buffer.WriteRune('\t') + writeString(string(key)) + buffer.WriteString(" = ") + writeString(text) + buffer.WriteRune(',') + } + buffer.WriteRune('\n') + buffer.WriteString(indent) + buffer.WriteRune('}') + } } } diff --git a/viewsContainer.go b/viewsContainer.go index 162c778..c004b42 100644 --- a/viewsContainer.go +++ b/viewsContainer.go @@ -85,10 +85,13 @@ func (container *viewsContainerData) Append(view View) { viewHTML(view, buffer, "") container.Session().appendToInnerHTML(container.htmlID(), buffer.String()) + container.contentChanged() + } +} - if listener, ok := container.changeListener[Content]; ok { - listener(container, Content) - } +func (container *viewsContainerData) contentChanged() { + if listener, ok := container.changeListener[Content]; ok { + listener.Run(container, Content) } } @@ -113,9 +116,7 @@ func (container *viewsContainerData) insert(view View, index int) bool { func (container *viewsContainerData) Insert(view View, index int) { if container.insert(view, index) && container.created { updateInnerHTML(container.htmlID(), container.Session()) - if listener, ok := container.changeListener[Content]; ok { - listener(container, Content) - } + container.contentChanged() } } @@ -149,9 +150,7 @@ func (container *viewsContainerData) RemoveView(index int) View { view := container.removeView(index) if view != nil && container.created { container.Session().callFunc("removeView", view.htmlID()) - if listener, ok := container.changeListener[Content]; ok { - listener(container, Content) - } + container.contentChanged() } return view } From bbbaf28aba704ac6a4f09b6e18ac92a9144fb735 Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Mon, 23 Jun 2025 16:59:24 +0300 Subject: [PATCH 14/21] Optimisation --- animation.go | 18 +++++++++--------- canvas.go | 2 +- data.go | 16 +++++----------- dragAndDrop.go | 4 ++-- imageView.go | 2 +- numberPicker.go | 8 ++++---- resizable.go | 2 +- sizeFunc.go | 4 ++-- tableView.go | 12 ++++++------ theme.go | 12 ++++++------ view.go | 4 ++-- webBridge.go | 2 +- 12 files changed, 40 insertions(+), 46 deletions(-) diff --git a/animation.go b/animation.go index 2adece7..c35c1f6 100644 --- a/animation.go +++ b/animation.go @@ -2,6 +2,7 @@ package rui import ( "fmt" + "maps" "math" "strconv" "strings" @@ -661,7 +662,7 @@ func (animation *animationData) animationCSS(session Session) string { buffer.WriteString(animation.keyFramesName) if duration, ok := floatProperty(animation, Duration, session, 1); ok && duration > 0 { - buffer.WriteString(fmt.Sprintf(" %gs ", duration)) + fmt.Fprintf(buffer, " %gs ", duration) } else { buffer.WriteString(" 1s ") } @@ -669,7 +670,7 @@ func (animation *animationData) animationCSS(session Session) string { buffer.WriteString(timingFunctionCSS(animation, TimingFunction, session)) if delay, ok := floatProperty(animation, Delay, session, 0); ok && delay > 0 { - buffer.WriteString(fmt.Sprintf(" %gs", delay)) + fmt.Fprintf(buffer, " %gs", delay) } else { buffer.WriteString(" 0s") } @@ -678,7 +679,7 @@ func (animation *animationData) animationCSS(session Session) string { if iterationCount == 0 { iterationCount = 1 } - buffer.WriteString(fmt.Sprintf(" %d ", iterationCount)) + fmt.Fprintf(buffer, " %d ", iterationCount) } else { buffer.WriteString(" infinite ") } @@ -699,7 +700,7 @@ func (animation *animationData) animationCSS(session Session) string { func (animation *animationData) transitionCSS(buffer *strings.Builder, session Session) { if duration, ok := floatProperty(animation, Duration, session, 1); ok && duration > 0 { - buffer.WriteString(fmt.Sprintf(" %gs ", duration)) + fmt.Fprintf(buffer, " %gs ", duration) } else { buffer.WriteString(" 1s ") } @@ -707,7 +708,7 @@ func (animation *animationData) transitionCSS(buffer *strings.Builder, session S buffer.WriteString(timingFunctionCSS(animation, TimingFunction, session)) if delay, ok := floatProperty(animation, Delay, session, 0); ok && delay > 0 { - buffer.WriteString(fmt.Sprintf(" %gs", delay)) + fmt.Fprintf(buffer, " %gs", delay) } } @@ -979,11 +980,10 @@ func (style *viewStyle) Transition(tag PropertyName) AnimationProperty { } func (style *viewStyle) Transitions() map[PropertyName]AnimationProperty { - result := map[PropertyName]AnimationProperty{} - for tag, animation := range getTransitionProperty(style) { - result[tag] = animation + if transitions := getTransitionProperty(style); transitions != nil { + return maps.Clone(transitions) } - return result + return map[PropertyName]AnimationProperty{} } func (style *viewStyle) SetTransition(tag PropertyName, animation AnimationProperty) { diff --git a/canvas.go b/canvas.go index f537997..653a369 100644 --- a/canvas.go +++ b/canvas.go @@ -575,7 +575,7 @@ func (canvas *canvasData) SetLineDash(dash []float64, offset float64) { for _, val := range dash { buffer.WriteRune(lead) lead = ',' - buffer.WriteString(fmt.Sprintf("%g", val)) + fmt.Fprintf(buffer, "%g", val)) } buffer.WriteRune(']') diff --git a/data.go b/data.go index ae7f74a..c3cc88f 100644 --- a/data.go +++ b/data.go @@ -1,6 +1,7 @@ package rui import ( + "slices" "strings" "unicode" ) @@ -321,8 +322,8 @@ func (node *dataNode) ArrayAsParams() []Params { func ParseDataText(text string) DataObject { if strings.ContainsAny(text, "\r") { - text = strings.Replace(text, "\r\n", "\n", -1) - text = strings.Replace(text, "\r", "\n", -1) + text = strings.ReplaceAll(text, "\r\n", "\n") + text = strings.ReplaceAll(text, "\r", "\n") } data := append([]rune(text), rune(0)) pos := 0 @@ -518,15 +519,8 @@ func ParseDataText(text string) DataObject { } stopSymbol := func(symbol rune) bool { - if unicode.IsSpace(symbol) { - return true - } - for _, sym := range []rune{'=', '{', '}', '[', ']', ',', ' ', '\t', '\n', '\'', '"', '`', '/'} { - if sym == symbol { - return true - } - } - return false + return unicode.IsSpace(symbol) || + slices.Contains([]rune{'=', '{', '}', '[', ']', ',', ' ', '\t', '\n', '\'', '"', '`', '/'}, symbol) } for pos < size && !stopSymbol(data[pos]) { diff --git a/dragAndDrop.go b/dragAndDrop.go index 0c1a006..d24be28 100644 --- a/dragAndDrop.go +++ b/dragAndDrop.go @@ -462,13 +462,13 @@ func dragAndDropHtml(view View, buffer *strings.Builder) { if f := GetDragImageXOffset(view); f != 0 { buffer.WriteString(` data-drag-image-x="`) - buffer.WriteString(fmt.Sprintf("%g", f)) + fmt.Fprintf(buffer, "%g", f) buffer.WriteString(`" `) } if f := GetDragImageYOffset(view); f != 0 { buffer.WriteString(` data-drag-image-y="`) - buffer.WriteString(fmt.Sprintf("%g", f)) + fmt.Fprintf(buffer, "%g", f) buffer.WriteString(`" `) } diff --git a/imageView.go b/imageView.go index 2be78c5..9411d89 100644 --- a/imageView.go +++ b/imageView.go @@ -205,7 +205,7 @@ func imageViewSrcSet(view View, path string) string { buffer.WriteString(", ") } buffer.WriteString(src.path) - buffer.WriteString(fmt.Sprintf(" %gx", src.scale)) + fmt.Fprintf(buffer, " %gx", src.scale) } return buffer.String() } diff --git a/numberPicker.go b/numberPicker.go index b7f44c9..da94424 100644 --- a/numberPicker.go +++ b/numberPicker.go @@ -244,27 +244,27 @@ func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builde min, max := GetNumberPickerMinMax(picker) if min != math.Inf(-1) { buffer.WriteString(` min="`) - buffer.WriteString(fmt.Sprintf(format, min)) + fmt.Fprintf(buffer, format, min) buffer.WriteByte('"') } if max != math.Inf(1) { buffer.WriteString(` max="`) - buffer.WriteString(fmt.Sprintf(format, max)) + fmt.Fprintf(buffer, format, max) buffer.WriteByte('"') } step := GetNumberPickerStep(picker) if step != 0 { buffer.WriteString(` step="`) - buffer.WriteString(fmt.Sprintf(format, step)) + fmt.Fprintf(buffer, format, step) buffer.WriteByte('"') } else { buffer.WriteString(` step="any"`) } buffer.WriteString(` value="`) - buffer.WriteString(fmt.Sprintf(format, GetNumberPickerValue(picker))) + fmt.Fprintf(buffer, format, GetNumberPickerValue(picker)) buffer.WriteByte('"') buffer.WriteString(` oninput="editViewInputEvent(this)"`) diff --git a/resizable.go b/resizable.go index 571fa7e..183c22c 100644 --- a/resizable.go +++ b/resizable.go @@ -354,7 +354,7 @@ func (resizable *resizableData) htmlSubviews(self View, buffer *strings.Builder) } writePos := func(x1, x2, y1, y2 int) { - buffer.WriteString(fmt.Sprintf(` grid-column-start: %d; grid-column-end: %d; grid-row-start: %d; grid-row-end: %d;">`, x1, x2, y1, y2)) + fmt.Fprintf(buffer, ` grid-column-start: %d; grid-column-end: %d; grid-row-start: %d; grid-row-end: %d;">`, x1, x2, y1, y2) } //htmlID := resizable.htmlID() diff --git a/sizeFunc.go b/sizeFunc.go index 59fb218..b5996cf 100644 --- a/sizeFunc.go +++ b/sizeFunc.go @@ -208,7 +208,7 @@ func (data *sizeFuncData) writeString(topFunc string, buffer *strings.Builder) { buffer.WriteString(arg.String()) case float64: - buffer.WriteString(fmt.Sprintf("%g", arg)) + fmt.Fprintf(buffer, "%g", arg) } } @@ -302,7 +302,7 @@ func (data *sizeFuncData) writeCSS(topFunc string, buffer *strings.Builder, sess buffer.WriteString(arg.String()) case float64: - buffer.WriteString(fmt.Sprintf("%g", arg)) + fmt.Fprintf(buffer, "%g", arg) } } diff --git a/tableView.go b/tableView.go index 3fabef2..039ec27 100644 --- a/tableView.go +++ b/tableView.go @@ -1270,10 +1270,10 @@ func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) { buffer.WriteString(string(value)) case float32: - buffer.WriteString(fmt.Sprintf("%g", float64(value))) + fmt.Fprintf(buffer, "%g", float64(value)) case float64: - buffer.WriteString(fmt.Sprintf("%g", value)) + fmt.Fprintf(buffer, "%g", value) case bool: if value { @@ -1284,7 +1284,7 @@ func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) { default: if n, ok := isInt(value); ok { - buffer.WriteString(fmt.Sprintf("%d", n)) + fmt.Fprintf(buffer, "%d", n) } else { buffer.WriteString("") } @@ -1537,10 +1537,10 @@ func (table *tableViewData) writeCellHtml(adapter TableAdapter, row, column int, buffer.WriteString(string(value)) case float32: - buffer.WriteString(fmt.Sprintf("%g", float64(value))) + fmt.Fprintf(buffer, "%g", float64(value)) case float64: - buffer.WriteString(fmt.Sprintf("%g", value)) + fmt.Fprintf(buffer, "%g", value) case bool: accentColor := Color(0) @@ -1555,7 +1555,7 @@ func (table *tableViewData) writeCellHtml(adapter TableAdapter, row, column int, default: if n, ok := isInt(value); ok { - buffer.WriteString(fmt.Sprintf("%d", n)) + fmt.Fprintf(buffer, "%d", n) } else { buffer.WriteString("") } diff --git a/theme.go b/theme.go index 8bb5f9e..de11522 100644 --- a/theme.go +++ b/theme.go @@ -975,10 +975,10 @@ func (theme *theme) String() string { buffer.WriteString(":landscape") } if maxWidth > 0 { - buffer.WriteString(fmt.Sprintf(":width%d", maxWidth)) + fmt.Fprintf(buffer, ":width%d", maxWidth) } if maxHeight > 0 { - buffer.WriteString(fmt.Sprintf(":height%d", maxHeight)) + fmt.Fprintf(buffer, ":height%d", maxHeight) } buffer.WriteString(" = [\n") @@ -1014,21 +1014,21 @@ func (theme *theme) String() string { } if media.MinWidth > 0 { - buffer.WriteString(fmt.Sprintf(":width%d-", media.MinWidth)) + fmt.Fprintf(buffer, ":width%d-", media.MinWidth) if media.MaxWidth > 0 { buffer.WriteString(strconv.Itoa(media.MaxWidth)) } } else if media.MaxWidth > 0 { - buffer.WriteString(fmt.Sprintf(":width%d", media.MaxWidth)) + fmt.Fprintf(buffer, ":width%d", media.MaxWidth) } if media.MinHeight > 0 { - buffer.WriteString(fmt.Sprintf(":height%d-", media.MinHeight)) + fmt.Fprintf(buffer, ":height%d-", media.MinHeight) if media.MaxHeight > 0 { buffer.WriteString(strconv.Itoa(media.MaxHeight)) } } else if media.MaxHeight > 0 { - buffer.WriteString(fmt.Sprintf(":height%d", media.MaxHeight)) + fmt.Fprintf(buffer, ":height%d", media.MaxHeight) } buffer.WriteString(" = [\n") diff --git a/view.go b/view.go index 44ebe6b..982b806 100644 --- a/view.go +++ b/view.go @@ -1066,8 +1066,8 @@ func (view *viewData) htmlProperties(self View, buffer *strings.Builder) { } if view.frame.Left != 0 || view.frame.Top != 0 || view.frame.Width != 0 || view.frame.Height != 0 { - buffer.WriteString(fmt.Sprintf(` data-left="%g" data-top="%g" data-width="%g" data-height="%g"`, - view.frame.Left, view.frame.Top, view.frame.Width, view.frame.Height)) + fmt.Fprintf(buffer, ` data-left="%g" data-top="%g" data-width="%g" data-height="%g"`, + view.frame.Left, view.frame.Top, view.frame.Width, view.frame.Height) } } diff --git a/webBridge.go b/webBridge.go index 9c94a70..d167b7b 100644 --- a/webBridge.go +++ b/webBridge.go @@ -190,7 +190,7 @@ func (bridge *webBridge) argToString(arg any) (string, bool) { for _, val := range arg { buffer.WriteRune(lead) lead = ',' - buffer.WriteString(fmt.Sprintf("%g", val)) + fmt.Fprintf(buffer, "%g", val) } buffer.WriteRune(']') return buffer.String(), true From 73b14ed78a97ac518197b900210e856fc8eda021 Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:53:36 +0300 Subject: [PATCH 15/21] Adde binding parameter to CreateView functions --- detailsView.go | 2 +- listView.go | 2 +- resizable.go | 2 +- viewFactory.go | 138 ++++++++++++++++++++++++++-------------------- viewsContainer.go | 9 ++- 5 files changed, 86 insertions(+), 67 deletions(-) diff --git a/detailsView.go b/detailsView.go index 48701d5..5d4417e 100644 --- a/detailsView.go +++ b/detailsView.go @@ -92,7 +92,7 @@ func (detailsView *detailsViewData) setFunc(tag PropertyName, value any) []Prope value.setParentID(detailsView.htmlID()) case DataObject: - if view := CreateViewFromObject(detailsView.Session(), value); view != nil { + if view := CreateViewFromObject(detailsView.Session(), value, nil); view != nil { detailsView.setRaw(Summary, view) view.setParentID(detailsView.htmlID()) } else { diff --git a/listView.go b/listView.go index f685114..5103631 100644 --- a/listView.go +++ b/listView.go @@ -336,7 +336,7 @@ func (listView *listViewData) setItems(value any) []PropertyName { items := make([]View, len(value)) for i, val := range value { if val.IsObject() { - if view := CreateViewFromObject(session, val.Object()); view != nil { + if view := CreateViewFromObject(session, val.Object(), nil); view != nil { items[i] = view } else { return nil diff --git a/resizable.go b/resizable.go index 183c22c..96297b0 100644 --- a/resizable.go +++ b/resizable.go @@ -118,7 +118,7 @@ func (resizable *resizableData) setFunc(tag PropertyName, value any) []PropertyN newContent = value case DataObject: - if newContent = CreateViewFromObject(resizable.Session(), value); newContent == nil { + if newContent = CreateViewFromObject(resizable.Session(), value, nil); newContent == nil { return nil } diff --git a/viewFactory.go b/viewFactory.go index 2fa553c..dbd70d6 100644 --- a/viewFactory.go +++ b/viewFactory.go @@ -6,7 +6,7 @@ import ( "strings" ) -var viewCreators = map[string]func(Session) View{ +var systemViewCreators = map[string]func(Session) View{ "View": newView, "ColumnLayout": newColumnLayout, "ListLayout": newListLayout, @@ -36,84 +36,104 @@ var viewCreators = map[string]func(Session) View{ "VideoPlayer": newVideoPlayer, } +var viewCreate map[string]func(Session) View = nil + +func viewCreators() map[string]func(Session) View { + if viewCreate == nil { + viewCreate = map[string]func(Session) View{} + for tag, fn := range systemViewCreators { + viewCreate[strings.ToLower(tag)] = fn + } + } + return viewCreate +} + // RegisterViewCreator register function of creating view func RegisterViewCreator(tag string, creator func(Session) View) bool { - builtinViews := []string{ - "View", - "ViewsContainer", - "ColumnLayout", - "ListLayout", - "GridLayout", - "StackLayout", - "TabsLayout", - "AbsoluteLayout", - "Resizable", - "DetailsView", - "TextView", - "Button", - "Checkbox", - "DropDownList", - "ProgressBar", - "NumberPicker", - "ColorPicker", - "DatePicker", - "TimePicker", - "EditView", - "ListView", - "CanvasView", - "ImageView", - "TableView", - } - - for _, name := range builtinViews { - if name == tag { + loTag := strings.ToLower(tag) + for name := range systemViewCreators { + if name == loTag { + ErrorLog(`It is forbidden to override the function of ` + tag + ` creating`) return false } } - viewCreators[tag] = creator + viewCreators()[loTag] = creator return true } -// CreateViewFromObject create new View and initialize it by Node data -func CreateViewFromObject(session Session, object DataObject) View { - tag := object.Tag() - - if creator, ok := viewCreators[tag]; ok { - if !session.ignoreViewUpdates() { - session.setIgnoreViewUpdates(true) - defer session.setIgnoreViewUpdates(false) - } - view := creator(session) - view.init(session) - if customView, ok := view.(CustomView); ok { - if !InitCustomView(customView, tag, session, nil) { - return nil - } - } - parseProperties(view, object) - return view +// CreateViewFromObject create new View and initialize it by DataObject data. Parameters: +// - session - the session to which the view will be attached (should not be nil); +// - object - datas describing View; +// - binding - object assigned to the Binding property (may be nil). +// +// If the function fails, it returns nil and an error message is written to the log. +func CreateViewFromObject(session Session, object DataObject, binding any) View { + if session == nil { + ErrorLog(`Session must not be nil`) + return nil } - ErrorLog(`Unknown view type "` + object.Tag() + `"`) - return nil + tag := object.Tag() + creator, ok := viewCreators()[strings.ToLower(tag)] + if !ok { + ErrorLog(`Unknown view type "` + tag + `"`) + return nil + } + + if !session.ignoreViewUpdates() { + session.setIgnoreViewUpdates(true) + defer session.setIgnoreViewUpdates(false) + } + + view := creator(session) + view.init(session) + if customView, ok := view.(CustomView); ok { + if !InitCustomView(customView, tag, session, nil) { + return nil + } + } + parseProperties(view, object) + if binding != nil { + view.setRaw(Binding, binding) + } + return view } -// CreateViewFromText create new View and initialize it by content of text -func CreateViewFromText(session Session, text string) View { +// CreateViewFromText create new View and initialize it by content of text. Parameters: +// - session - the session to which the view will be attached (should not be nil); +// - text - text describing View; +// - binding - object assigned to the Binding property (optional parameter). +// +// If the function fails, it returns nil and an error message is written to the log. +func CreateViewFromText(session Session, text string, binding ...any) View { if data := ParseDataText(text); data != nil { - return CreateViewFromObject(session, data) + var b any = nil + if len(binding) > 0 { + b = binding[0] + } + return CreateViewFromObject(session, data, b) } return nil } // CreateViewFromResources create new View and initialize it by the content of -// the resource file from "views" directory -func CreateViewFromResources(session Session, name string) View { +// the resource file from "views" directory. Parameters: +// - session - the session to which the view will be attached (should not be nil); +// - name - file name in the views folder of the application resources (it is not necessary to specify the .rui extension, it is added automatically); +// - binding - object assigned to the Binding property (optional parameter). +// +// If the function fails, it returns nil and an error message is written to the log. +func CreateViewFromResources(session Session, name string, binding ...any) View { if strings.ToLower(filepath.Ext(name)) != ".rui" { name += ".rui" } + var b any = nil + if len(binding) > 0 { + b = binding[0] + } + for _, fs := range resources.embedFS { rootDirs := resources.embedRootDirs(fs) for _, dir := range rootDirs { @@ -124,14 +144,14 @@ func CreateViewFromResources(session Session, name string) View { case viewDir: if data, err := fs.ReadFile(dir + "/" + name); err == nil { if data := ParseDataText(string(data)); data != nil { - return CreateViewFromObject(session, data) + return CreateViewFromObject(session, data, b) } } default: if data, err := fs.ReadFile(dir + "/" + viewDir + "/" + name); err == nil { if data := ParseDataText(string(data)); data != nil { - return CreateViewFromObject(session, data) + return CreateViewFromObject(session, data, b) } } } @@ -141,7 +161,7 @@ func CreateViewFromResources(session Session, name string) View { if resources.path != "" { if data, err := os.ReadFile(resources.path + viewDir + "/" + name); err == nil { if data := ParseDataText(string(data)); data != nil { - return CreateViewFromObject(session, data) + return CreateViewFromObject(session, data, b) } } } diff --git a/viewsContainer.go b/viewsContainer.go index c004b42..3d6d870 100644 --- a/viewsContainer.go +++ b/viewsContainer.go @@ -174,9 +174,8 @@ func (container *viewsContainerData) htmlSubviews(self View, buffer *strings.Bui func viewFromTextValue(text string, session Session) View { if strings.Contains(text, "{") && strings.Contains(text, "}") { - data := ParseDataText(text) - if data != nil { - if view := CreateViewFromObject(session, data); view != nil { + if data := ParseDataText(text); data != nil { + if view := CreateViewFromObject(session, data, nil); view != nil { return view } } @@ -277,7 +276,7 @@ func (container *viewsContainerData) setContent(value any) bool { container.views = views case DataObject: - if view := CreateViewFromObject(session, value); view != nil { + if view := CreateViewFromObject(session, value, nil); view != nil { container.views = []View{view} } else { return false @@ -287,7 +286,7 @@ func (container *viewsContainerData) setContent(value any) bool { views := []View{} for _, data := range value { if data.IsObject() { - if view := CreateViewFromObject(session, data.Object()); view != nil { + if view := CreateViewFromObject(session, data.Object(), nil); view != nil { views = append(views, view) } } else { From e618377c1148566c33e66960709523886701c424 Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Tue, 24 Jun 2025 18:41:56 +0300 Subject: [PATCH 16/21] Added binding support for canvas "draw-function" --- canvasView.go | 39 ++++++++++++++++++++++++++++++++++----- image.go | 24 +++++++++++++++++------- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/canvasView.go b/canvasView.go index 1c6ea10..19f46d9 100644 --- a/canvasView.go +++ b/canvasView.go @@ -1,5 +1,7 @@ package rui +import "reflect" + // DrawFunction is the constant for "draw-function" property tag. // // Used by `CanvasView`. @@ -55,7 +57,7 @@ func (canvasView *canvasViewData) removeFunc(tag PropertyName) []PropertyName { if tag == DrawFunction { if canvasView.getRaw(DrawFunction) != nil { canvasView.setRaw(DrawFunction, nil) - canvasView.Redraw() + //canvasView.Redraw() return []PropertyName{DrawFunction} } return []PropertyName{} @@ -66,9 +68,14 @@ func (canvasView *canvasViewData) removeFunc(tag PropertyName) []PropertyName { func (canvasView *canvasViewData) setFunc(tag PropertyName, value any) []PropertyName { if tag == DrawFunction { - if fn, ok := value.(func(Canvas)); ok { - canvasView.setRaw(DrawFunction, fn) - } else { + switch value := value.(type) { + case func(Canvas): + canvasView.setRaw(DrawFunction, value) + + case string: + canvasView.setRaw(DrawFunction, value) + + default: notCompatibleType(tag, value) return nil } @@ -94,8 +101,30 @@ func (canvasView *canvasViewData) Redraw() { canvas := newCanvas(canvasView) canvas.ClearRect(0, 0, canvasView.frame.Width, canvasView.frame.Height) if value := canvasView.getRaw(DrawFunction); value != nil { - if drawer, ok := value.(func(Canvas)); ok { + switch drawer := value.(type) { + 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 + } + + 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() diff --git a/image.go b/image.go index 0091f0a..bb17a52 100644 --- a/image.go +++ b/image.go @@ -4,34 +4,44 @@ import ( "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 ( // ImageLoading is the image loading status: in the process of loading - ImageLoading = 0 + ImageLoading ImageLoadingStatus = 0 // ImageReady is the image loading status: the image is loaded successfully - ImageReady = 1 + ImageReady ImageLoadingStatus = 1 // ImageLoadingError is the image loading status: an error occurred while loading - ImageLoadingError = 2 + ImageLoadingError ImageLoadingStatus = 2 ) // Image defines the image that is used for drawing operations on the Canvas. type Image interface { // URL returns the url of the image URL() string - // LoadingStatus returns the status of the image loading: ImageLoading (0), ImageReady (1), ImageLoadingError (2) - LoadingStatus() int + + // LoadingStatus returns the status of the image loading: + // - 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() string setLoadingError(err string) + // Width returns the width of the image in pixels. While LoadingStatus() != ImageReady returns 0 Width() float64 + // Height returns the height of the image in pixels. While LoadingStatus() != ImageReady returns 0 Height() float64 } type imageData struct { url string - loadingStatus int + loadingStatus ImageLoadingStatus loadingError string width, height float64 listener func(Image) @@ -45,7 +55,7 @@ func (image *imageData) URL() string { return image.url } -func (image *imageData) LoadingStatus() int { +func (image *imageData) LoadingStatus() ImageLoadingStatus { return image.loadingStatus } From 4cec7fef260f387b4a611938641c64dd617f15af Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Tue, 24 Jun 2025 18:46:41 +0300 Subject: [PATCH 17/21] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43687ca..000ac9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +# v0.20.0 + +* Added support of binding +* Added "binding" argument to CreateViewFromResources, CreateViewFromText, and CreateViewFromObject functions + # v0.19.0 * Added support of drag-and-drop From 2dd8d8d256625e1ca67bac07ec9b4b7b94443512 Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Tue, 24 Jun 2025 19:31:38 +0300 Subject: [PATCH 18/21] Updated readme --- CHANGELOG.md | 2 +- README-ru.md | 64 +++++++++++++++++++++++++++++++++++++++++++++ README.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 134 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 000ac9b..91903e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ # v0.18.2 -* fixed typo: GetShadowPropertys -> GetShadowProperty +* fixed typo: GetShadowProperties -> GetShadowProperty # v0.18.0 diff --git a/README-ru.md b/README-ru.md index 13a7518..7b37861 100644 --- a/README-ru.md +++ b/README-ru.md @@ -5704,6 +5704,70 @@ Safari и Firefox. Для получения объекта используется метод Object. Для получения элементов массива используются методы ArraySize, ArrayElement и ArrayElements +## Связывание (binding) + +Механизм связывания предназначен для задания обработчиков событий и слушателей изменений в ресурсах приложений. + +Рассмотрим пример: + +Файл ресурсов описывающий кнопку button.rui + + Button { + click-event = ButtonClick, + } + +Код для создания этой кнопки из ресурсов + + type button struct { + view rui.View + } + + func createButton(session rui.Session) rui.View { + b := new(button) + b.view = rui.CreateViewFromResources(session, "button.rui", b) + return b.view + } + + func (button *button) ButtonClick() { + rui.DebugLog("Button clicked") + } + +В данном примере в файле ресурсов указано, что в качестве обработчика клика надо использовать функцию с именем "ButtonClick". +При создании View из ресeрсов с помощью функции CreateViewFromResources в качестве третьего параметра задается объект связывания. +При возникновении события "click-event" система будет искать в связанном объекте один из следующих методов + + ButtonClick() + ButtonClick(rui.View) + ButtonClick(rui.MouseEvent) + ButtonClick(rui.View, rui.MouseEvent) + +Система найдет метод ButtonClick() и вызовет его. + +Теперь рассотрим как добавить отслеживание изменения свойств. +Для этого добавим в пример слушателя изменения свойства "background-color" в свойство "change-listeners" + + Button { + click-event = ButtonClick, + change-listeners = { + background-color = BackgroundColorChanged, + }, + } + +И добавим соответствующий метод + + func (button *button) BackgroundColorChanged() { + rui.DebugLog("Background color changed") + } + +Данный метод может иметь одно из следующих описаний + + BackgroundColorChanged() + BackgroundColorChanged(rui.View) + BackgroundColorChanged(rui.PropertyName) + BackgroundColorChanged(rui.View, rui.PropertyName) + +Важное замечание: Все методы вызываемые через связывание должны быть публичными (начинаться с большой буквы) + ## Ресурсы Ресурсы (картинки, темы, переводы и т.д.) с которыми работает приложение должны размещаться по diff --git a/README.md b/README.md index 95b59e6..02fa351 100644 --- a/README.md +++ b/README.md @@ -520,7 +520,7 @@ For the "edit-text-changed" event, this * func(newText string) * []func(editor EditView, newText string) * []func(newText string) -* []any содержащий только func(editor EditView, newText string) и func(newText string) +* []any containing only func(editor EditView, newText string) и func(newText string) And the "edit-text-changed" property always stores and returns []func(EditView, string). @@ -1062,7 +1062,7 @@ The ShadowProperty text representation has the following format: You can get the value of "shadow" property using the function - func GetShadowPropertys(view View, subviewID ...string) []ShadowProperty + func GetShadowProperties(view View, subviewID ...string) []ShadowProperty If no shadow is specified, then this function will return an empty array @@ -5720,6 +5720,71 @@ using the SetResourcePath function before creating the Application: app.Start("localhost:8000") } +## Binding + +The binding mechanism is designed to set event handlers and listeners for changes in application resources. + +Let's look at an example: + +Resource file ("button.rui") describing the button + + Button { + click-event = ButtonClick, + } + +Code to create this button from resources + + type button struct { + view rui.View + } + + func createButton(session rui.Session) rui.View { + b := new(button) + b.view = rui.CreateViewFromResources(session, "button.rui", b) + return b.view + } + + func (button *button) ButtonClick() { + rui.DebugLog("Button clicked") + } + +In this example, the resource file specifies that a function named "ButtonClick" should be used as the click handler. +When creating a View from resources using the CreateViewFromResources function, the binding object is specified as the third parameter. +When a "click-event" occurs, the system will look for one of the following methods in the binding object + + ButtonClick() + ButtonClick(rui.View) + ButtonClick(rui.MouseEvent) + ButtonClick(rui.View, rui.MouseEvent) + +The system will find the ButtonClick() method and call it. + +Now let's look at how to add property change tracking. + +To do this, let's add a property change listener "background-color" to the property "change-listeners" in the example. + + Button { + click-event = ButtonClick, + change-listeners = { + background-color = BackgroundColorChanged, + }, + } + +And we will add the corresponding method + + func (button *button) BackgroundColorChanged() { + rui.DebugLog("Background color changed") + } + +This method can have one of the following descriptions + + BackgroundColorChanged() + BackgroundColorChanged(rui.View) + BackgroundColorChanged(rui.PropertyName) + BackgroundColorChanged(rui.View, rui.PropertyName) + +Important note: All methods called via binding must be public (start with a capital letter) + ## Images for screens with different pixel densities If you need to add separate images to the resources for screens with different pixel densities, @@ -5919,7 +5984,7 @@ The library defines a number of constants and styles. You can override them in y System styles that you can override: -| Style name | Описание | +| Style name | Description | |---------------------|---------------------------------------------------------------------| | ruiApp | This style is used to set the default text style (font, size, etc.) | | ruiView | Default View Style | @@ -6011,7 +6076,7 @@ Translation files must have the "rui" extension and the following format = , … }, - <язык 2> = _{ + = _{ = , = , … From b0185726db0e92054581f4dac98064d5390769b7 Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Wed, 25 Jun 2025 13:53:08 +0300 Subject: [PATCH 19/21] Added ViewCreateListener interface --- README-ru.md | 53 +++++++++++++++++++++++++++++--------------------- README.md | 21 ++++++++++++++------ popup.go | 21 +++++++++++--------- viewFactory.go | 12 +++++++++++- 4 files changed, 69 insertions(+), 38 deletions(-) diff --git a/README-ru.md b/README-ru.md index 7b37861..2b7ba93 100644 --- a/README-ru.md +++ b/README-ru.md @@ -161,8 +161,8 @@ SizeUnit объявлена как | "sub(, )" | SubSize(arg0, arg1 any) | находит разность значений аргументов | | "mul(, )" | MulSize(arg0, arg1 any) | находит результат умножения значений аргументов | | "div(, )" | DivSize(arg0, arg1 any) | находит результат деления значений аргументов | -| "rem(, )" | ModSize(arg0, arg1 any) | находит остаток деления значений аргументов, результат имеет тотже знак что и делимое | -| "mod(, )" | ModSize(arg0, arg1 any) | находит остаток деления значений аргументов, результат имеет тотже знак что и делитель | +| "rem(, )" | ModSize(arg0, arg1 any) | находит остаток деления значений аргументов, результат имеет тот же знак что и делимое | +| "mod(, )" | ModSize(arg0, arg1 any) | находит остаток деления значений аргументов, результат имеет тот же знак что и делитель | | "round(, )" | RoundSize(arg0, arg1 any) | округляет первый аргумент до ближайшего целого числа кратного второму аргументу | | "round-up(, )" | RoundUpSize(arg0, arg1 any) | округляет первый аргумент до ближайшего большего целого числа, кратного второму аргументу | | "round-down(, )" | RoundDownSize(arg0, arg1 any) | округляет первый аргумент до ближайшего меньшего целого числа кратного второму аргументу | @@ -1086,7 +1086,7 @@ RadiusProperty, а не структура BoxRadius. Получить стру Получить значение данного свойства можно с помощью функции - func GetShadowPropertys(view View, subviewID ...string) []ShadowProperty + func GetShadowProperties(view View, subviewID ...string) []ShadowProperty Если тень не задана, то данная функция вернет пустой массив @@ -2291,14 +2291,14 @@ radius необходимо передать nil #### Свойство "drag-data" -Для того чтобы сделать View перетаскиваемым ему неоходимо задать свойство "drag-data" (константа DragData). +Для того чтобы сделать View перетаскиваемым ему необходимо задать свойство "drag-data" (константа DragData). Данное свойство задает множество перетаскиваемых данных в виде ключ:значение и имеет тип: map[string]string В качестве ключей рекомендуется использовать mime-тип значения. -Например, если в перетаскиваемыми данными асляется текст, то ключем должен быть "text/plain", если jpeg-изображение, то "image/jpg" и т.п. -Но это только рекомендация, ключем может быть любой текст. +Например, если в перетаскиваемыми данными является текст, то ключом должен быть "text/plain", если jpeg-изображение, то "image/jpg" и т.п. +Но это только рекомендация, ключом может быть любой текст. Пример @@ -2318,7 +2318,7 @@ radius необходимо передать nil По умолчанию при перетаскивании перемещается весь View. Часто это бывает не удобно, например если View очень большой. Свойство "drag-image" (константа DragImage) типа string позволяет задать картинку, которая будет отображаться вместо View при перетаскивании. -В качестве значения "drag-image" задаеться: +В качестве значения "drag-image" задается: * Имя изображения в ресурсах приложения * Константа изображения * URL изображения @@ -2442,15 +2442,15 @@ radius необходимо передать nil #### События "drag-enter-event", "drag-leave-event" и "drag-over-event" -События "drag-enter-event", "drag-leave-event" и "drag-over-event" генерируются только для View приемника перетескиваемого объекта. +События "drag-enter-event", "drag-leave-event" и "drag-over-event" генерируются только для View приемника перетаскиваемого объекта. -Событие "drag-enter-event" генерируется когда перетаскиваемый оъект входит в область View приемника. +Событие "drag-enter-event" генерируется когда перетаскиваемый объект входит в область View приемника. -Событие "drag-leave-event" генерируется когда перетаскиваемый оъект покидает область View приемника. +Событие "drag-leave-event" генерируется когда перетаскиваемый объект покидает область View приемника. -Событие "drag-over-event" генерируется c определенным интервалом (несколько раз в секунду) пока перетаскиваемый оъект находится область View приемника. +Событие "drag-over-event" генерируется c определенным интервалом (несколько раз в секунду) пока перетаскиваемый объект находится область View приемника. -Пример, меняем цвет рамки с серой на красную когда перетаскиваемый оъект находится над областью приемника +Пример, меняем цвет рамки с серой на красную когда перетаскиваемый объект находится над областью приемника view.SetParams(rui.Params{ rui.DragEnterEvent: func(view rui.View, event rui.DragAndDropEvent)) { @@ -3834,7 +3834,7 @@ float32, float64, int, int8…int64, uint, uint8…uint64. Между пунктами списка можно добавлять разделителями. Для этого используется свойство "item-separators" (константа ItemSeparators). Данному свойству присваивается массив индексов пунктов после которых необходимо добавить разделители. -Свойству "item-separators" могут присваиваться теже типы данных что и свойству "disabled-items". +Свойству "item-separators" могут присваиваться те же типы данных что и свойству "disabled-items". Прочитать значение свойства "item-separators" можно с помощью функции func GetDropDownItemSeparators(view View, subviewID ...string) []int @@ -4085,7 +4085,7 @@ int свойство "current" (константа Current). Значение "c с помощью int свойств "checkbox-horizontal-align" и "checkbox-vertical-align" (константы CheckboxHorizontalAlign и CheckboxVerticalAlign) -Свойство "checkbox-horizontal-align" (константа СheckboxHorizontalAlign) может принимать следующие значения: +Свойство "checkbox-horizontal-align" (константа CheckboxHorizontalAlign) может принимать следующие значения: | Значение | Константа | Имя | Расположение чекбокса | |:--------:|--------------|----------|-------------------------------------------------| @@ -4647,7 +4647,7 @@ rotation - угол поворота эллипса относительно ц Метод NewPath() создает пустую фигуру. Далее вы должны описать фигуру используя методы интерфейса Path Метод NewPathFromSvg(data string) Path создает фигуру описанную в параметре data. -Параметр data является описанием фигуры в формате елемента svg изображения. Например +Параметр data является описанием фигуры в формате элемента svg изображения. Например path := canvas.NewPathFromSvg("M 30,0 C 30,0 27,8.6486 17,21.622 7,34.595 0,40 0,40 0,40 6,44.3243 17,58.378 28,72.432 30,80 30,80 30,80 37.8387,65.074 43,58.378 53,45.405 60,40 60,40 60,40 53,34.5946 43,21.622 33,8.649 30,0 30,0 Z") @@ -4827,7 +4827,7 @@ AudioPlayer и VideoPlayer это элементы которые предназ ### Свойство "src" -Свойство "src" (константа Source) задает один или несколько источников медиафайлов. Свойство "src" может принимать +Свойство "src" (константа Source) задает один или несколько источников медиа файлов. Свойство "src" может принимать значение следующих типов: * string, @@ -4872,7 +4872,7 @@ AudioPlayer и VideoPlayer это элементы которые предназ |:--------:|-----------------|------------|----------------------------------------------------------------------------------------| | 0 | PreloadNone | "none" | Медиа файл не должен быть предварительно загружен | | 1 | PreloadMetadata | "metadata" | Предварительно загружаются только метаданные | -| 2 | PreloadAuto | "auto" | Весь медиафайл может быть загружен, даже если пользователь не должен его использовать. | +| 2 | PreloadAuto | "auto" | Весь медиа файл может быть загружен, даже если пользователь не должен его использовать.| Значение по умолчанию PreloadAuto (2) @@ -5723,9 +5723,11 @@ Safari и Firefox. } func createButton(session rui.Session) rui.View { - b := new(button) - b.view = rui.CreateViewFromResources(session, "button.rui", b) - return b.view + return rui.CreateViewFromResources(session, "button.rui", new(button)) + } + + func (button *button) OnCreate(view rui.View) { + button.view = view } func (button *button) ButtonClick() { @@ -5733,7 +5735,7 @@ Safari и Firefox. } В данном примере в файле ресурсов указано, что в качестве обработчика клика надо использовать функцию с именем "ButtonClick". -При создании View из ресeрсов с помощью функции CreateViewFromResources в качестве третьего параметра задается объект связывания. +При создании View из ресурсов с помощью функции CreateViewFromResources в качестве третьего параметра задается объект связывания. При возникновении события "click-event" система будет искать в связанном объекте один из следующих методов ButtonClick() @@ -5743,7 +5745,14 @@ Safari и Firefox. Система найдет метод ButtonClick() и вызовет его. -Теперь рассотрим как добавить отслеживание изменения свойств. +Также для связанного объекта может задаваться опциональный метод + + OnCreate(view rui.View) + +Данный метод вызывается функциями CreateViewFromText, CreateViewFromResources и CreateViewFromObject после создания View. +В данном примере этот метод используется для сохранения указателя на созданный View. + +Теперь рассмотрим как добавить отслеживание изменения свойств. Для этого добавим в пример слушателя изменения свойства "background-color" в свойство "change-listeners" Button { diff --git a/README.md b/README.md index 02fa351..02840cc 100644 --- a/README.md +++ b/README.md @@ -2135,7 +2135,7 @@ You can get lists of pointer event listeners using the functions: ### Touch events -These events are used to track multipoint touches. Single touches emulate mouse events. +These events are used to track multi point touches. Single touches emulate mouse events. If you do not need to track multi-point touches, then it is easier to use mouse events | Event | Constant | Description | @@ -5726,22 +5726,24 @@ The binding mechanism is designed to set event handlers and listeners for change Let's look at an example: -Resource file ("button.rui") describing the button +The resource file ("button.rui") describing the button: Button { click-event = ButtonClick, } -Code to create this button from resources +The code to create this button from resources type button struct { view rui.View } func createButton(session rui.Session) rui.View { - b := new(button) - b.view = rui.CreateViewFromResources(session, "button.rui", b) - return b.view + return rui.CreateViewFromResources(session, "button.rui", new(button)) + } + + func (button *button) OnCreate(view rui.View) { + button.view = view } func (button *button) ButtonClick() { @@ -5759,6 +5761,13 @@ When a "click-event" occurs, the system will look for one of the following metho The system will find the ButtonClick() method and call it. +The optional method OnCreate can also be specified for the associated object + + OnCreate(view rui.View) + +This method is called by the CreateViewFromText, CreateViewFromResources, and CreateViewFromObject functions after the View has been created. +In this example, this method is used to save a pointer to the created View. + Now let's look at how to add property change tracking. To do this, let's add a property change listener "background-color" to the property "change-listeners" in the example. diff --git a/popup.go b/popup.go index 1b50ec8..560b519 100644 --- a/popup.go +++ b/popup.go @@ -151,7 +151,7 @@ const ( // // Used by Popup. // Specify start translation, scale and rotation over x, y and z axes as well as a distortion - // for an animated Popup showing/hidding. + // for an animated Popup showing/hiding. // // Supported types: TransformProperty, string. // @@ -279,7 +279,7 @@ type Popup interface { viewByHTMLID(id string) View keyEvent(event KeyEvent) bool showAnimation() - dissmissAnimation(listener func(PropertyName)) bool + dismissAnimation(listener func(PropertyName)) bool } type popupListener interface { @@ -849,7 +849,7 @@ func (popup *popupData) showAnimation() { } } -func (popup *popupData) dissmissAnimation(listener func(PropertyName)) bool { +func (popup *popupData) dismissAnimation(listener func(PropertyName)) bool { if popup.showOpacity != 1 || popup.showTransform != nil { session := popup.Session() popup.popupView.Set(TransitionEndEvent, listener) @@ -1012,23 +1012,26 @@ func (manager *popupManager) dismissPopup(popup Popup) { session := popup.Session() listener := func(PropertyName) { - if index == count-1 { + switch index { + case 0: if count == 1 { manager.popups = []Popup{} session.updateCSSProperty("ruiRoot", "pointer-events", "auto") session.updateCSSProperty("ruiPopupLayer", "visibility", "hidden") } else { - manager.popups = manager.popups[:count-1] + manager.popups = manager.popups[1:] } - } else if index == 0 { - manager.popups = manager.popups[1:] - } else { + + case count - 1: + manager.popups = manager.popups[:count-1] + + default: manager.popups = append(manager.popups[:index], manager.popups[index+1:]...) } popup.onDismiss() } - if !popup.dissmissAnimation(listener) { + if !popup.dismissAnimation(listener) { listener("") } } diff --git a/viewFactory.go b/viewFactory.go index dbd70d6..a4e7d68 100644 --- a/viewFactory.go +++ b/viewFactory.go @@ -36,6 +36,13 @@ var systemViewCreators = map[string]func(Session) View{ "VideoPlayer": newVideoPlayer, } +// ViewCreateListener is the listener interface of a view create event +type ViewCreateListener interface { + // OnCreate is a function of binding object that is called by the CreateViewFromText, CreateViewFromResources, + // and CreateViewFromObject functions after the creation of a view + OnCreate(view View) +} + var viewCreate map[string]func(Session) View = nil func viewCreators() map[string]func(Session) View { @@ -64,7 +71,7 @@ func RegisterViewCreator(tag string, creator func(Session) View) bool { // CreateViewFromObject create new View and initialize it by DataObject data. Parameters: // - session - the session to which the view will be attached (should not be nil); -// - object - datas describing View; +// - object - data describing View; // - binding - object assigned to the Binding property (may be nil). // // If the function fails, it returns nil and an error message is written to the log. @@ -96,6 +103,9 @@ func CreateViewFromObject(session Session, object DataObject, binding any) View parseProperties(view, object) if binding != nil { view.setRaw(Binding, binding) + if listener, ok := binding.(ViewCreateListener); ok { + listener.OnCreate(view) + } } return view } From 3090a0e94fa76a00a824843e96f9c4e9d91ff7a1 Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Wed, 25 Jun 2025 14:36:04 +0300 Subject: [PATCH 20/21] Spell Checking --- backgroundRadialGradient.go | 2 +- canvas.go | 4 ++-- columnSeparator.go | 4 ++-- customView.go | 10 +++++----- dragAndDrop.go | 8 ++++---- listLayout.go | 2 +- listView.go | 6 +++--- numberPicker.go | 4 ++-- resources.go | 4 ++-- tableView.go | 2 +- view.go | 6 +++--- viewStyle.go | 4 ++-- 12 files changed, 28 insertions(+), 28 deletions(-) diff --git a/backgroundRadialGradient.go b/backgroundRadialGradient.go index 17cb41a..9a1026e 100644 --- a/backgroundRadialGradient.go +++ b/backgroundRadialGradient.go @@ -43,7 +43,7 @@ type backgroundRadialGradient struct { // // The following properties can be used: // - "gradient" (Gradient) - Describes gradient stop points. This is a mandatory property while describing background gradients. -// - "center-x" (CenterX), "center-y" (CenterY) - Defines the gradient center point cooordinates. +// - "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. diff --git a/canvas.go b/canvas.go index 653a369..f76f6a8 100644 --- a/canvas.go +++ b/canvas.go @@ -201,7 +201,7 @@ type Canvas interface { // SetConicGradientFillStyle sets a conic gradient around a point // to use inside shapes - // * x, y - coordinates of the center of the conic gradient in pilels; + // * x, y - coordinates of the center of the conic gradient in piles; // * startAngle - the angle at which to begin the gradient, in radians. The angle starts from a line going horizontally right from the center, and proceeds clockwise. // * startColor - the start color; // * endColor - the end color; @@ -210,7 +210,7 @@ type Canvas interface { // SetConicGradientFillStyle sets a conic gradient around a point // to use inside shapes - // * x, y - coordinates of the center of the conic gradient in pilels; + // * x, y - coordinates of the center of the conic gradient in piles; // * startAngle - the angle at which to begin the gradient, in radians. The angle starts from a line going horizontally right from the center, and proceeds clockwise. // * startColor - the start color; // * endColor - the end color; diff --git a/columnSeparator.go b/columnSeparator.go index 5de3574..7112f0c 100644 --- a/columnSeparator.go +++ b/columnSeparator.go @@ -93,12 +93,12 @@ func NewColumnSeparator(style int, color Color, width SizeUnit) ColumnSeparatorP func (separator *columnSeparatorProperty) init() { separator.dataProperty.init() - separator.normalize = normalizeVolumnSeparatorTag + separator.normalize = normalizeColumnSeparatorTag separator.set = columnSeparatorSet separator.supportedProperties = []PropertyName{Style, Width, ColorTag} } -func normalizeVolumnSeparatorTag(tag PropertyName) PropertyName { +func normalizeColumnSeparatorTag(tag PropertyName) PropertyName { tag = defaultNormalize(tag) switch tag { case ColumnSeparatorStyle, "separator-style": diff --git a/customView.go b/customView.go index 75b75e3..7a715a4 100644 --- a/customView.go +++ b/customView.go @@ -292,15 +292,15 @@ func (customView *CustomViewData) ViewIndex(view View) int { return -1 } -func (customView *CustomViewData) exscludeTags() []PropertyName { +func (customView *CustomViewData) excludeTags() []PropertyName { if customView.superView != nil { - exsclude := []PropertyName{} + exclude := []PropertyName{} for tag, value := range customView.defaultParams { if value == customView.superView.getRaw(tag) { - exsclude = append(exsclude, tag) + exclude = append(exclude, tag) } } - return exsclude + return exclude } return nil } @@ -310,7 +310,7 @@ func (customView *CustomViewData) String() string { if customView.superView != nil { buffer := allocStringBuilder() defer freeStringBuilder(buffer) - writeViewStyle(customView.tag, customView, buffer, "", customView.exscludeTags()) + writeViewStyle(customView.tag, customView, buffer, "", customView.excludeTags()) return buffer.String() } return customView.tag + " { }" diff --git a/dragAndDrop.go b/dragAndDrop.go index d24be28..243a4c1 100644 --- a/dragAndDrop.go +++ b/dragAndDrop.go @@ -49,7 +49,7 @@ const ( // Supported types: int, string. // // Values: - // - 0 (DropEffectUndefined) or "undefined" - The property value is not defined (defaut value). + // - 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. @@ -68,7 +68,7 @@ const ( // Supported types: int, string. // // Values: - // - 0 (DropEffectUndefined) or "undefined" - The property value is not defined (defaut value). Equivalent to DropEffectAll + // - 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. @@ -210,7 +210,7 @@ const ( // 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 (defaut value). + // DropEffectAll - the value of the "drop-effect-allowed" property: all operations (copy, move, and link) are permitted (default value). DropEffectAll = DropEffectCopy + DropEffectMove + DropEffectLink ) @@ -472,7 +472,7 @@ func dragAndDropHtml(view View, buffer *strings.Builder) { buffer.WriteString(`" `) } - effects := []string{"undifined", "copy", "move", "copyMove", "link", "copyLink", "linkMove", "all"} + 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]) diff --git a/listLayout.go b/listLayout.go index 2952a0c..1503029 100644 --- a/listLayout.go +++ b/listLayout.go @@ -200,7 +200,7 @@ func (listLayout *listLayoutData) UpdateContent() { } } -// GetListVerticalAlign returns the vertical align of a ListLayout or ListView sibview: +// GetListVerticalAlign returns the vertical align of a ListLayout or ListView 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. diff --git a/listView.go b/listView.go index 5103631..376ebd7 100644 --- a/listView.go +++ b/listView.go @@ -534,7 +534,7 @@ func (listView *listViewData) getDivs(checkbox, hCheckboxAlign, vCheckboxAlign i return onDivBuilder.String(), offDivBuilder.String(), contentBuilder.String() } -func (listView *listViewData) checkboxItemDiv(checkbox, hCheckboxAlign, vCheckboxAlign int) string { +func (listView *listViewData) checkboxItemDiv(hCheckboxAlign, vCheckboxAlign int) string { itemStyleBuilder := allocStringBuilder() defer freeStringBuilder(itemStyleBuilder) @@ -621,7 +621,7 @@ func (listView *listViewData) checkboxSubviews(adapter ListAdapter, buffer *stri hCheckboxAlign := GetListViewCheckboxHorizontalAlign(listView) vCheckboxAlign := GetListViewCheckboxVerticalAlign(listView) - itemDiv := listView.checkboxItemDiv(checkbox, hCheckboxAlign, vCheckboxAlign) + itemDiv := listView.checkboxItemDiv(hCheckboxAlign, vCheckboxAlign) onDiv, offDiv, contentDiv := listView.getDivs(checkbox, hCheckboxAlign, vCheckboxAlign) current := GetCurrent(listView) @@ -728,7 +728,7 @@ func (listView *listViewData) updateCheckboxItem(index int, checked bool) { buffer := allocStringBuilder() defer freeStringBuilder(buffer) - buffer.WriteString(listView.checkboxItemDiv(checkbox, hCheckboxAlign, vCheckboxAlign)) + buffer.WriteString(listView.checkboxItemDiv(hCheckboxAlign, vCheckboxAlign)) if checked { buffer.WriteString(onDiv) } else { diff --git a/numberPicker.go b/numberPicker.go index da94424..e6a7594 100644 --- a/numberPicker.go +++ b/numberPicker.go @@ -166,8 +166,8 @@ func (picker *numberPickerData) setFunc(tag PropertyName, value any) []PropertyN } func (picker *numberPickerData) numberFormat() string { - if precission := GetNumberPickerPrecision(picker); precission > 0 { - return fmt.Sprintf("%%.%df", precission) + if precision := GetNumberPickerPrecision(picker); precision > 0 { + return fmt.Sprintf("%%.%df", precision) } return "%g" } diff --git a/resources.go b/resources.go index 999b85d..5569b7f 100644 --- a/resources.go +++ b/resources.go @@ -401,7 +401,7 @@ func OpenRawResource(filename string) fs.File { return nil } -// AllRawResources returns the list of all raw resouces +// AllRawResources returns the list of all raw resources func AllRawResources() []string { result := []string{} @@ -448,7 +448,7 @@ func AllRawResources() []string { return result } -// AllImageResources returns the list of all image resouces +// AllImageResources returns the list of all image resources func AllImageResources() []string { result := make([]string, 0, len(resources.images)) for image := range resources.images { diff --git a/tableView.go b/tableView.go index 039ec27..8f198e1 100644 --- a/tableView.go +++ b/tableView.go @@ -1303,7 +1303,7 @@ func (table *tableViewData) htmlSubviews(self View, buffer *strings.Builder) { if columnStyle := GetTableColumnStyle(table); columnStyle != nil { buffer.WriteString("") - for column := 0; column < columnCount; column++ { + for column := range columnCount { cssBuilder.buffer.Reset() if styles := columnStyle.ColumnStyle(column); styles != nil { view.Clear() diff --git a/view.go b/view.go index 982b806..d52891f 100644 --- a/view.go +++ b/view.go @@ -102,7 +102,7 @@ type View interface { htmlProperties(self View, buffer *strings.Builder) cssStyle(self View, builder cssBuilder) addToCSSStyle(addCSS map[string]string) - exscludeTags() []PropertyName + excludeTags() []PropertyName htmlDisabledProperty() bool binding() any @@ -959,7 +959,7 @@ func (view *viewData) propertyChanged(tag PropertyName) { case DropEffectAllowed: effect := GetDropEffectAllowed(view) if effect >= DropEffectCopy && effect >= DropEffectAll { - values := []string{"undifined", "copy", "move", "copyMove", "link", "copyLink", "linkMove", "all"} + values := []string{"undefined", "copy", "move", "copyMove", "link", "copyLink", "linkMove", "all"} session.updateProperty(htmlID, "data-drop-effect-allowed", values[effect]) } else { session.removeProperty(htmlID, "data-drop-effect-allowed") @@ -1321,6 +1321,6 @@ func (view *viewData) String() string { return buffer.String() } -func (view *viewData) exscludeTags() []PropertyName { +func (view *viewData) excludeTags() []PropertyName { return nil } diff --git a/viewStyle.go b/viewStyle.go index db488bb..2aa4518 100644 --- a/viewStyle.go +++ b/viewStyle.go @@ -799,14 +799,14 @@ func writePropertyValue(buffer *strings.Builder, tag PropertyName, value any, in buffer.WriteString("[]") case 1: - writeViewStyle(value[0].Tag(), value[0], buffer, indent, value[0].exscludeTags()) + writeViewStyle(value[0].Tag(), value[0], buffer, indent, value[0].excludeTags()) default: buffer.WriteString("[\n") indent2 := indent + "\t" for _, v := range value { buffer.WriteString(indent2) - writeViewStyle(v.Tag(), v, buffer, indent2, v.exscludeTags()) + writeViewStyle(v.Tag(), v, buffer, indent2, v.excludeTags()) buffer.WriteString(",\n") } From c3c8b9e858ca20afe3c60d63dc8c1d009a43ae4e Mon Sep 17 00:00:00 2001 From: Alexei Anoshenko <2277098+anoshenko@users.noreply.github.com> Date: Wed, 25 Jun 2025 17:42:32 +0300 Subject: [PATCH 21/21] Changed ParseDataText function return values --- CHANGELOG.md | 1 + appServer.go | 201 ++++++------ background.go | 48 ++- border.go | 5 +- data.go | 805 ++++++++++++++++++++++++---------------------- data_test.go | 15 +- popup.go | 19 ++ session.go | 3 +- strings.go | 5 +- theme.go | 7 +- transform.go | 5 +- viewFactory.go | 32 +- viewsContainer.go | 8 +- 13 files changed, 609 insertions(+), 545 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91903e6..29c1e64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ * Added support of binding * Added "binding" argument to CreateViewFromResources, CreateViewFromText, and CreateViewFromObject functions +* Changed ParseDataText function return values # v0.19.0 diff --git a/appServer.go b/appServer.go index 8a2db52..ee71774 100644 --- a/appServer.go +++ b/appServer.go @@ -164,64 +164,68 @@ func (app *application) postHandler(w http.ResponseWriter, req *http.Request) { DebugLog(message) } - if obj := ParseDataText(message); obj != nil { - var session Session = nil - var response chan string = nil + obj, err := ParseDataText(message) + if err != nil { + ErrorLog(err.Error()) + return + } - 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) - } + 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) - for len(response) > 0 { - io.WriteString(w, <-response) - } } } } @@ -241,58 +245,61 @@ func (app *application) socketReader(bridge *wsBridge) { DebugLog("🖥️ -> " + message) } - if obj := ParseDataText(message); obj != nil { - command := obj.Tag() - switch command { - case "startSession": - answer := "" + 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 + } - 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 - } + default: + if !session.handleAnswer(command, obj) { + events <- obj } } } diff --git a/background.go b/background.go index d55d12d..920064b 100644 --- a/background.go +++ b/background.go @@ -78,6 +78,16 @@ func createBackground(obj DataObject) BackgroundElement { return result } +func parseBackgroundText(text string) BackgroundElement { + obj, err := ParseDataText(text) + if err != nil { + ErrorLog(err.Error()) + return nil + } + + return createBackground(obj) +} + func parseBackgroundValue(value any) []BackgroundElement { switch value := value.(type) { @@ -96,12 +106,8 @@ func parseBackgroundValue(value any) []BackgroundElement { } else { return nil } - } else if obj := ParseDataText(el.Value()); obj != nil { - if element := createBackground(obj); element != nil { - background = append(background, element) - } else { - return nil - } + } else if element := parseBackgroundText(el.Value()); element != nil { + background = append(background, element) } else { return nil } @@ -125,21 +131,15 @@ func parseBackgroundValue(value any) []BackgroundElement { return background case string: - if obj := ParseDataText(value); obj != nil { - if element := createBackground(obj); element != nil { - return []BackgroundElement{element} - } + if element := parseBackgroundText(value); element != nil { + return []BackgroundElement{element} } case []string: elements := make([]BackgroundElement, 0, len(value)) - for _, element := range value { - if obj := ParseDataText(element); obj != nil { - if element := createBackground(obj); element != nil { - elements = append(elements, element) - } else { - return nil - } + for _, text := range value { + if element := parseBackgroundText(text); element != nil { + elements = append(elements, element) } else { return nil } @@ -148,18 +148,14 @@ func parseBackgroundValue(value any) []BackgroundElement { case []any: elements := make([]BackgroundElement, 0, len(value)) - for _, element := range value { - switch element := element.(type) { + for _, val := range value { + switch val := val.(type) { case BackgroundElement: - elements = append(elements, element) + elements = append(elements, val) case string: - if obj := ParseDataText(element); obj != nil { - if element := createBackground(obj); element != nil { - elements = append(elements, element) - } else { - return nil - } + if element := parseBackgroundText(val); element != nil { + elements = append(elements, element) } else { return nil } diff --git a/border.go b/border.go index 45d0db6..ff07794 100644 --- a/border.go +++ b/border.go @@ -610,7 +610,10 @@ func borderSet(properties Properties, tag PropertyName, value any) []PropertyNam case Left, Right, Top, Bottom: switch value := value.(type) { case string: - if obj := ParseDataText(value); obj != nil { + obj, err := ParseDataText(value) + if err != nil { + ErrorLog(err.Error()) + } else { return setSingleBorderObject(tag, obj) } diff --git a/data.go b/data.go index c3cc88f..edc6998 100644 --- a/data.go +++ b/data.go @@ -1,6 +1,8 @@ package rui import ( + "errors" + "fmt" "slices" "strings" "unicode" @@ -48,6 +50,9 @@ type DataObject interface { // 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 @@ -162,6 +167,28 @@ func (object *dataObject) PropertyByTag(tag string) DataNode { 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) { if node := object.PropertyByTag(tag); node != nil && node.Type() == TextNode { return node.Text(), true @@ -318,404 +345,402 @@ func (node *dataNode) ArrayAsParams() []Params { return result } +type dataParser struct { + data []rune + size int + pos int + line int + lineStart int +} + +func (parser *dataParser) skipSpaces(skipNewLine bool) { + for parser.pos < parser.size { + switch parser.data[parser.pos] { + case '\n': + if !skipNewLine { + return + } + parser.line++ + parser.lineStart = parser.pos + 1 + + case '/': + if parser.pos+1 < parser.size { + switch parser.data[parser.pos+1] { + case '/': + parser.pos += 2 + for parser.pos < parser.size && parser.data[parser.pos] != '\n' { + parser.pos++ + } + parser.pos-- + + case '*': + parser.pos += 3 + for { + if parser.pos >= parser.size { + ErrorLog("Unexpected end of file") + return + } + if parser.data[parser.pos-1] == '*' && parser.data[parser.pos] == '/' { + break + } + if parser.data[parser.pos-1] == '\n' { + parser.line++ + parser.lineStart = parser.pos + } + parser.pos++ + } + + default: + return + } + } + + case ' ', '\t': + // do nothing + + default: + if !unicode.IsSpace(parser.data[parser.pos]) { + return + } + } + parser.pos++ + } +} + +func (parser *dataParser) parseTag() (string, error) { + parser.skipSpaces(true) + startPos := parser.pos + switch parser.data[parser.pos] { + case '`': + parser.pos++ + startPos++ + for parser.data[parser.pos] != '`' { + parser.pos++ + if parser.pos >= parser.size { + return string(parser.data[startPos:parser.size]), errors.New("unexpected end of text") + } + } + str := string(parser.data[startPos:parser.pos]) + parser.pos++ + return str, nil + + case '\'', '"': + stopSymbol := parser.data[parser.pos] + parser.pos++ + startPos++ + slash := false + for stopSymbol != parser.data[parser.pos] { + if parser.data[parser.pos] == '\\' { + parser.pos += 2 + slash = true + } else { + parser.pos++ + } + if parser.pos >= parser.size { + return string(parser.data[startPos:parser.size]), errors.New("unexpected end of text") + } + } + + if !slash { + str := string(parser.data[startPos:parser.pos]) + parser.pos++ + parser.skipSpaces(false) + return str, nil + } + + buffer := make([]rune, parser.pos-startPos+1) + n1 := 0 + n2 := startPos + + invalidEscape := func() (string, error) { + str := string(parser.data[startPos:parser.pos]) + parser.pos++ + return str, fmt.Errorf(`invalid escape sequence in "%s" (position %d)`, str, n2-2-startPos) + } + + for n2 < parser.pos { + if parser.data[n2] != '\\' { + buffer[n1] = parser.data[n2] + n2++ + } else { + n2 += 2 + switch parser.data[n2-1] { + case 'n': + buffer[n1] = '\n' + + case 'r': + buffer[n1] = '\r' + + case 't': + buffer[n1] = '\t' + + case '"': + buffer[n1] = '"' + + case '\'': + buffer[n1] = '\'' + + case '\\': + buffer[n1] = '\\' + + case 'x', 'X': + if n2+2 > parser.pos { + return invalidEscape() + } + x := 0 + for range 2 { + ch := parser.data[n2] + if ch >= '0' && ch <= '9' { + x = x*16 + int(ch-'0') + } else if ch >= 'a' && ch <= 'f' { + x = x*16 + int(ch-'a'+10) + } else if ch >= 'A' && ch <= 'F' { + x = x*16 + int(ch-'A'+10) + } else { + return invalidEscape() + } + n2++ + } + buffer[n1] = rune(x) + + case 'u', 'U': + if n2+4 > parser.pos { + return invalidEscape() + } + x := 0 + for range 4 { + ch := parser.data[n2] + if ch >= '0' && ch <= '9' { + x = x*16 + int(ch-'0') + } else if ch >= 'a' && ch <= 'f' { + x = x*16 + int(ch-'a'+10) + } else if ch >= 'A' && ch <= 'F' { + x = x*16 + int(ch-'A'+10) + } else { + return invalidEscape() + } + n2++ + } + buffer[n1] = rune(x) + + default: + str := string(parser.data[startPos:parser.pos]) + return str, fmt.Errorf(`invalid escape sequence in "%s" (position %d)`, str, n2-2-startPos) + } + } + n1++ + } + + parser.pos++ + parser.skipSpaces(false) + return string(buffer[0:n1]), nil + } + + for parser.pos < parser.size && !parser.stopSymbol(parser.data[parser.pos]) { + parser.pos++ + } + + endPos := parser.pos + parser.skipSpaces(false) + if startPos == endPos { + //ErrorLog("empty tag") + return "", nil + } + return string(parser.data[startPos:endPos]), nil +} + +func (parser *dataParser) stopSymbol(symbol rune) bool { + return unicode.IsSpace(symbol) || + slices.Contains([]rune{'=', '{', '}', '[', ']', ',', ' ', '\t', '\n', '\'', '"', '`', '/'}, symbol) +} + +func (parser *dataParser) parseNode() (DataNode, error) { + var tag string + var err error + + if tag, err = parser.parseTag(); err != nil { + return nil, err + } + + parser.skipSpaces(true) + if parser.data[parser.pos] != '=' { + return nil, fmt.Errorf("expected '=' after a tag name (line: %d, position: %d)", parser.line, parser.pos-parser.lineStart) + } + + parser.pos++ + parser.skipSpaces(true) + switch parser.data[parser.pos] { + case '[': + node := new(dataNode) + node.tag = tag + + if node.array, err = parser.parseArray(); err != nil { + return nil, err + } + return node, nil + + case '{': + node := new(dataNode) + node.tag = tag + if node.value, err = parser.parseObject("_"); err != nil { + return nil, err + } + return node, nil + + case '}', ']', '=': + return nil, fmt.Errorf(`expected '[', '{' or a tag name after '=' (line: %d, position: %d)`, parser.line, parser.pos-parser.lineStart) + + default: + var str string + if str, err = parser.parseTag(); err != nil { + return nil, err + } + + node := new(dataNode) + node.tag = tag + + if parser.data[parser.pos] == '{' { + if node.value, err = parser.parseObject(str); err != nil { + return nil, err + } + } else { + val := new(dataStringValue) + val.value = str + node.value = val + } + + return node, nil + } +} + +func (parser *dataParser) parseObject(tag string) (DataObject, error) { + if parser.data[parser.pos] != '{' { + return nil, fmt.Errorf(`expected '{' (line: %d, position: %d)`, parser.line, parser.pos-parser.lineStart) + } + parser.pos++ + + obj := new(dataObject) + obj.tag = tag + obj.property = []DataNode{} + + for parser.pos < parser.size { + parser.skipSpaces(true) + if parser.data[parser.pos] == '}' { + parser.pos++ + parser.skipSpaces(false) + return obj, nil + } + + node, err := parser.parseNode() + if err != nil { + return nil, err + } + + obj.property = append(obj.property, node) + if parser.data[parser.pos] == '}' { + parser.pos++ + parser.skipSpaces(true) + return obj, nil + } else if parser.data[parser.pos] != ',' && parser.data[parser.pos] != '\n' { + return nil, fmt.Errorf(`expected '}', '\n' or ',' (line: %d, position: %d)`, parser.line, parser.pos-parser.lineStart) + } + + if parser.data[parser.pos] != '\n' { + parser.pos++ + } + + parser.skipSpaces(true) + for parser.data[parser.pos] == ',' { + parser.pos++ + parser.skipSpaces(true) + } + } + + return nil, errors.New("unexpected end of text") +} + +func (parser *dataParser) parseArray() ([]DataValue, error) { + parser.pos++ + parser.skipSpaces(true) + + array := []DataValue{} + + for parser.pos < parser.size { + parser.skipSpaces(true) + for parser.data[parser.pos] == ',' && parser.pos < parser.size { + parser.pos++ + parser.skipSpaces(true) + } + + if parser.pos >= parser.size { + break + } + + if parser.data[parser.pos] == ']' { + parser.pos++ + parser.skipSpaces(true) + return array, nil + } + + tag, err := parser.parseTag() + if err != nil { + return nil, err + } + + if parser.data[parser.pos] == '{' { + obj, err := parser.parseObject(tag) + if err != nil { + return nil, err + } + array = append(array, obj) + } else { + val := new(dataStringValue) + val.value = tag + array = append(array, val) + } + + switch parser.data[parser.pos] { + case ']', ',', '\n': + + default: + return nil, fmt.Errorf(`expected ']' or ',' (line: %d, position: %d)`, parser.line, parser.pos-parser.lineStart) + } + + /* + if data[pos] == ']' { + pos++ + skipSpaces() + return array, nil + } else if data[pos] != ',' { + return nil, fmt.Errorf("Expected ']' or ',' (line: %d, position: %d)", line, pos-lineStart) + } + pos++ + skipSpaces() + */ + } + + return nil, errors.New("unexpected end of text") +} + // ParseDataText - parse text and return DataNode -func ParseDataText(text string) DataObject { +func ParseDataText(text string) (DataObject, error) { if strings.ContainsAny(text, "\r") { text = strings.ReplaceAll(text, "\r\n", "\n") text = strings.ReplaceAll(text, "\r", "\n") } - data := append([]rune(text), rune(0)) - pos := 0 - size := len(data) - 1 - line := 1 - lineStart := 0 - skipSpaces := func(skipNewLine bool) { - for pos < size { - switch data[pos] { - case '\n': - if !skipNewLine { - return - } - line++ - lineStart = pos + 1 - - case '/': - if pos+1 < size { - switch data[pos+1] { - case '/': - pos += 2 - for pos < size && data[pos] != '\n' { - pos++ - } - pos-- - - case '*': - pos += 3 - for { - if pos >= size { - ErrorLog("Unexpected end of file") - return - } - if data[pos-1] == '*' && data[pos] == '/' { - break - } - if data[pos-1] == '\n' { - line++ - lineStart = pos - } - pos++ - } - - default: - return - } - } - - case ' ', '\t': - // do nothing - - default: - if !unicode.IsSpace(data[pos]) { - return - } - } - pos++ - } + parser := dataParser{ + data: append([]rune(text), rune(0)), + pos: 0, + line: 1, + lineStart: 0, } + parser.size = len(parser.data) - 1 - parseTag := func() (string, bool) { - skipSpaces(true) - startPos := pos - switch data[pos] { - case '`': - pos++ - startPos++ - for data[pos] != '`' { - pos++ - if pos >= size { - ErrorLog("Unexpected end of text") - return string(data[startPos:size]), false - } - } - str := string(data[startPos:pos]) - pos++ - return str, true - - case '\'', '"': - stopSymbol := data[pos] - pos++ - startPos++ - slash := false - for stopSymbol != data[pos] { - if data[pos] == '\\' { - pos += 2 - slash = true - } else { - pos++ - } - if pos >= size { - ErrorLog("Unexpected end of text") - return string(data[startPos:size]), false - } - } - - if !slash { - str := string(data[startPos:pos]) - pos++ - skipSpaces(false) - return str, true - } - - buffer := make([]rune, pos-startPos+1) - n1 := 0 - n2 := startPos - - invalidEscape := func() (string, bool) { - str := string(data[startPos:pos]) - pos++ - ErrorLogF("Invalid escape sequence in \"%s\" (position %d)", str, n2-2-startPos) - return str, false - } - - for n2 < pos { - if data[n2] != '\\' { - buffer[n1] = data[n2] - n2++ - } else { - n2 += 2 - switch data[n2-1] { - case 'n': - buffer[n1] = '\n' - - case 'r': - buffer[n1] = '\r' - - case 't': - buffer[n1] = '\t' - - case '"': - buffer[n1] = '"' - - case '\'': - buffer[n1] = '\'' - - case '\\': - buffer[n1] = '\\' - - case 'x', 'X': - if n2+2 > pos { - return invalidEscape() - } - x := 0 - for range 2 { - ch := data[n2] - if ch >= '0' && ch <= '9' { - x = x*16 + int(ch-'0') - } else if ch >= 'a' && ch <= 'f' { - x = x*16 + int(ch-'a'+10) - } else if ch >= 'A' && ch <= 'F' { - x = x*16 + int(ch-'A'+10) - } else { - return invalidEscape() - } - n2++ - } - buffer[n1] = rune(x) - - case 'u', 'U': - if n2+4 > pos { - return invalidEscape() - } - x := 0 - for range 4 { - ch := data[n2] - if ch >= '0' && ch <= '9' { - x = x*16 + int(ch-'0') - } else if ch >= 'a' && ch <= 'f' { - x = x*16 + int(ch-'a'+10) - } else if ch >= 'A' && ch <= 'F' { - x = x*16 + int(ch-'A'+10) - } else { - return invalidEscape() - } - n2++ - } - buffer[n1] = rune(x) - - default: - str := string(data[startPos:pos]) - ErrorLogF("Invalid escape sequence in \"%s\" (position %d)", str, n2-2-startPos) - return str, false - } - } - n1++ - } - - pos++ - skipSpaces(false) - return string(buffer[0:n1]), true - } - - stopSymbol := func(symbol rune) bool { - return unicode.IsSpace(symbol) || - slices.Contains([]rune{'=', '{', '}', '[', ']', ',', ' ', '\t', '\n', '\'', '"', '`', '/'}, symbol) - } - - for pos < size && !stopSymbol(data[pos]) { - pos++ - } - - endPos := pos - skipSpaces(false) - if startPos == endPos { - //ErrorLog("empty tag") - return "", true - } - return string(data[startPos:endPos]), true + tag, err := parser.parseTag() + if err != nil { + return nil, err } - - var parseObject func(tag string) DataObject - var parseArray func() []DataValue - - parseNode := func() DataNode { - var tag string - var ok bool - - if tag, ok = parseTag(); !ok { - return nil - } - - skipSpaces(true) - if data[pos] != '=' { - ErrorLogF("expected '=' after a tag name (line: %d, position: %d)", line, pos-lineStart) - return nil - } - - pos++ - skipSpaces(true) - switch data[pos] { - case '[': - node := new(dataNode) - node.tag = tag - - if node.array = parseArray(); node.array == nil { - return nil - } - return node - - case '{': - node := new(dataNode) - node.tag = tag - if node.value = parseObject("_"); node.value == nil { - return nil - } - return node - - case '}', ']', '=': - ErrorLogF("Expected '[', '{' or a tag name after '=' (line: %d, position: %d)", line, pos-lineStart) - return nil - - default: - var str string - if str, ok = parseTag(); !ok { - return nil - } - - node := new(dataNode) - node.tag = tag - - if data[pos] == '{' { - if node.value = parseObject(str); node.value == nil { - return nil - } - } else { - val := new(dataStringValue) - val.value = str - node.value = val - } - - return node - } - } - - parseObject = func(tag string) DataObject { - if data[pos] != '{' { - ErrorLogF("Expected '{' (line: %d, position: %d)", line, pos-lineStart) - return nil - } - pos++ - - obj := new(dataObject) - obj.tag = tag - obj.property = []DataNode{} - - for pos < size { - var node DataNode - - skipSpaces(true) - if data[pos] == '}' { - pos++ - skipSpaces(false) - return obj - } - - if node = parseNode(); node == nil { - return nil - } - obj.property = append(obj.property, node) - if data[pos] == '}' { - pos++ - skipSpaces(true) - return obj - } else if data[pos] != ',' && data[pos] != '\n' { - ErrorLogF(`Expected '}', '\n' or ',' (line: %d, position: %d)`, line, pos-lineStart) - return nil - } - if data[pos] != '\n' { - pos++ - } - skipSpaces(true) - for data[pos] == ',' { - pos++ - skipSpaces(true) - } - } - - ErrorLog("Unexpected end of text") - return nil - } - - parseArray = func() []DataValue { - pos++ - skipSpaces(true) - - array := []DataValue{} - - for pos < size { - var tag string - var ok bool - - skipSpaces(true) - for data[pos] == ',' && pos < size { - pos++ - skipSpaces(true) - } - - if pos >= size { - break - } - - if data[pos] == ']' { - pos++ - skipSpaces(true) - return array - } - - if tag, ok = parseTag(); !ok { - return nil - } - - if data[pos] == '{' { - obj := parseObject(tag) - if obj == nil { - return nil - } - array = append(array, obj) - } else { - val := new(dataStringValue) - val.value = tag - array = append(array, val) - } - - switch data[pos] { - case ']', ',', '\n': - - default: - ErrorLogF("Expected ']' or ',' (line: %d, position: %d)", line, pos-lineStart) - return nil - } - - /* - if data[pos] == ']' { - pos++ - skipSpaces() - return array, nil - } else if data[pos] != ',' { - return nil, fmt.Errorf("Expected ']' or ',' (line: %d, position: %d)", line, pos-lineStart) - } - pos++ - skipSpaces() - */ - } - - ErrorLog("Unexpected end of text") - return nil - } - - if tag, ok := parseTag(); ok { - return parseObject(tag) - } - return nil + return parser.parseObject(tag) } diff --git a/data_test.go b/data_test.go index 2ab257a..ed26ba3 100644 --- a/data_test.go +++ b/data_test.go @@ -6,10 +6,6 @@ import ( func TestParseDataText(t *testing.T) { - SetErrorLog(func(text string) { - t.Error(text) - }) - text := `obj1 { key1 = val1, key2=obj2{ @@ -27,8 +23,10 @@ func TestParseDataText(t *testing.T) { key3 = "\n \t \\ \r \" ' \X4F\x4e \U01Ea",` + "key4=`" + `\n \t \\ \r \" ' \x8F \UF80a` + "`\r}" - obj := ParseDataText(text) - if obj != nil { + obj, err := ParseDataText(text) + if err != nil { + t.Error(err) + } else { if obj.Tag() != "obj1" { t.Error(`obj.Tag() != "obj1"`) } @@ -173,9 +171,6 @@ func TestParseDataText(t *testing.T) { } } - SetErrorLog(func(text string) { - }) - failText := []string{ " ", "obj[]", @@ -204,7 +199,7 @@ func TestParseDataText(t *testing.T) { } for _, txt := range failText { - if obj := ParseDataText(txt); obj != nil { + if _, err := ParseDataText(txt); err == nil { t.Errorf("result ParseDataText(\"%s\") must be fail", txt) } } diff --git a/popup.go b/popup.go index 560b519..ea18c14 100644 --- a/popup.go +++ b/popup.go @@ -941,6 +941,25 @@ func NewPopup(view View, param Params) Popup { return popup } +/* +func CreatePopupFromObject(session Session, object DataObject, binding any) Popup { + node := object.RemovePropertyByTag(string(Content)) + if node == nil { + ErrorLog(`"content" property not found`) + return nil + } + + switch node.Type() { + case ObjectNode: + + case TextNode: + + default: + ErrorLog(`Unsupported data of "content" property`) + return nil + } +} +*/ // ShowPopup creates a new Popup and shows it func ShowPopup(view View, param Params) Popup { popup := NewPopup(view, param) diff --git a/session.go b/session.go index 54662b1..5c92551 100644 --- a/session.go +++ b/session.go @@ -266,7 +266,8 @@ func (session *sessionData) setBridge(events chan DataObject, bridge bridge) { func (session *sessionData) close() { if session.events != nil { - session.events <- ParseDataText(`session-close{session="` + strconv.Itoa(session.sessionID) + `"}`) + obj, _ := ParseDataText(`session-close{session="` + strconv.Itoa(session.sessionID) + `"}`) + session.events <- obj } } diff --git a/strings.go b/strings.go index 5b2bf26..090ecd9 100644 --- a/strings.go +++ b/strings.go @@ -50,8 +50,9 @@ func (resources *resourceManager) scanStringsDir(path string) { } func loadStringResources(text string) { - data := ParseDataText(text) - if data == nil { + data, err := ParseDataText(text) + if err != nil { + ErrorLog(err.Error()) return } diff --git a/theme.go b/theme.go index de11522..1e425a8 100644 --- a/theme.go +++ b/theme.go @@ -686,8 +686,11 @@ func (theme *theme) addText(themeText string) bool { theme.init() } - data := ParseDataText(themeText) - if data == nil || !data.IsObject() || data.Tag() != "theme" { + data, err := ParseDataText(themeText) + if err != nil { + ErrorLog(err.Error()) + return false + } else if !data.IsObject() || data.Tag() != "theme" { return false } diff --git a/transform.go b/transform.go index 6a0c600..cb5c10c 100644 --- a/transform.go +++ b/transform.go @@ -398,7 +398,10 @@ func valueToTransformProperty(value any) TransformProperty { } case string: - if obj := ParseDataText(value); obj != nil { + obj, err := ParseDataText(value) + if err != nil { + ErrorLog(err.Error()) + } else { return parseObject(obj) } } diff --git a/viewFactory.go b/viewFactory.go index a4e7d68..2017123 100644 --- a/viewFactory.go +++ b/viewFactory.go @@ -117,14 +117,17 @@ func CreateViewFromObject(session Session, object DataObject, binding any) View // // If the function fails, it returns nil and an error message is written to the log. func CreateViewFromText(session Session, text string, binding ...any) View { - if data := ParseDataText(text); data != nil { - var b any = nil - if len(binding) > 0 { - b = binding[0] - } - return CreateViewFromObject(session, data, b) + data, err := ParseDataText(text) + if err != nil { + ErrorLog(err.Error()) + return nil } - return nil + + var b any = nil + if len(binding) > 0 { + b = binding[0] + } + return CreateViewFromObject(session, data, b) } // CreateViewFromResources create new View and initialize it by the content of @@ -153,14 +156,20 @@ func CreateViewFromResources(session Session, name string, binding ...any) View case viewDir: if data, err := fs.ReadFile(dir + "/" + name); err == nil { - if data := ParseDataText(string(data)); data != nil { + data, err := ParseDataText(string(data)) + if err != nil { + ErrorLog(err.Error()) + } else { return CreateViewFromObject(session, data, b) } } default: if data, err := fs.ReadFile(dir + "/" + viewDir + "/" + name); err == nil { - if data := ParseDataText(string(data)); data != nil { + data, err := ParseDataText(string(data)) + if err != nil { + ErrorLog(err.Error()) + } else { return CreateViewFromObject(session, data, b) } } @@ -170,7 +179,10 @@ func CreateViewFromResources(session Session, name string, binding ...any) View if resources.path != "" { if data, err := os.ReadFile(resources.path + viewDir + "/" + name); err == nil { - if data := ParseDataText(string(data)); data != nil { + data, err := ParseDataText(string(data)) + if err != nil { + ErrorLog(err.Error()) + } else { return CreateViewFromObject(session, data, b) } } diff --git a/viewsContainer.go b/viewsContainer.go index 3d6d870..d5050f1 100644 --- a/viewsContainer.go +++ b/viewsContainer.go @@ -173,11 +173,9 @@ func (container *viewsContainerData) htmlSubviews(self View, buffer *strings.Bui } func viewFromTextValue(text string, session Session) View { - if strings.Contains(text, "{") && strings.Contains(text, "}") { - if data := ParseDataText(text); data != nil { - if view := CreateViewFromObject(session, data, nil); view != nil { - return view - } + if data, err := ParseDataText(text); err == nil { + if view := CreateViewFromObject(session, data, nil); view != nil { + return view } } return NewTextView(session, Params{Text: text})