package rui import ( "fmt" "strconv" "strings" ) // Frame - the location and size of a rectangle area type Frame struct { // Left - the left border Left float64 // Top - the top border Top float64 // Width - the width of a rectangle area Width float64 // Height - the height of a rectangle area Height float64 } // Right returns the right border func (frame Frame) Right() float64 { return frame.Left + frame.Width } // Bottom returns the bottom border func (frame Frame) Bottom() float64 { return frame.Top + frame.Height } // View - base view interface type View interface { ViewStyle fmt.Stringer // Init initializes fields of View by default values Init(session Session) // Session returns the current Session interface Session() Session // Parent returns the parent view Parent() View parentHTMLID() string setParentID(parentID string) // Tag returns the tag of View interface Tag() string // ID returns the id of the view ID() string // Focusable returns true if the view receives the focus Focusable() bool // Frame returns the location and size of the view in pixels Frame() Frame // Scroll returns the location size of the scrolable view in pixels Scroll() Frame // SetAnimated sets the value (second argument) of the property with name defined by the first argument. // Return "true" if the value has been set, in the opposite case "false" are returned and // a description of the error is written to the log SetAnimated(tag string, value any, animation Animation) bool // SetChangeListener set the function to track the change of the View property SetChangeListener(tag string, listener func(View, string)) // HasFocus returns 'true' if the view has focus HasFocus() bool handleCommand(self View, command string, data DataObject) bool htmlClass(disabled bool) string htmlTag() string closeHTMLTag() bool htmlID() string htmlSubviews(self View, buffer *strings.Builder) htmlProperties(self View, buffer *strings.Builder) htmlDisabledProperties(self View, buffer *strings.Builder) cssStyle(self View, builder cssBuilder) addToCSSStyle(addCSS map[string]string) onResize(self View, x, y, width, height float64) onItemResize(self View, index string, x, y, width, height float64) setNoResizeEvent() isNoResizeEvent() bool setScroll(x, y, width, height float64) } // viewData - base implementation of View interface type viewData struct { viewStyle session Session tag string viewID string _htmlID string parentID string systemClass string changeListener map[string]func(View, string) singleTransition map[string]Animation addCSS map[string]string frame Frame scroll Frame noResizeEvent bool created bool hasFocus bool //animation map[string]AnimationEndListener } func newView(session Session) View { view := new(viewData) view.Init(session) return view } func setInitParams(view View, params Params) { if params != nil { session := view.Session() if !session.ignoreViewUpdates() { session.setIgnoreViewUpdates(true) defer session.setIgnoreViewUpdates(false) } for _, tag := range params.AllTags() { if value, ok := params[tag]; ok { view.Set(tag, value) } } } } // NewView create new View object and return it func NewView(session Session, params Params) View { view := new(viewData) view.Init(session) setInitParams(view, params) return view } func (view *viewData) Init(session Session) { view.viewStyle.init() view.tag = "View" view.session = session view.changeListener = map[string]func(View, string){} view.addCSS = map[string]string{} //view.animation = map[string]AnimationEndListener{} view.singleTransition = map[string]Animation{} view.noResizeEvent = false view.created = false } func (view *viewData) Session() Session { return view.session } func (view *viewData) Parent() View { return view.session.viewByHTMLID(view.parentID) } func (view *viewData) parentHTMLID() string { return view.parentID } func (view *viewData) setParentID(parentID string) { view.parentID = parentID } func (view *viewData) Tag() string { return view.tag } func (view *viewData) ID() string { return view.viewID } func (view *viewData) ViewByID(id string) View { if id == view.ID() { if v := view.session.viewByHTMLID(view.htmlID()); v != nil { return v } return view } return nil } func (view *viewData) Focusable() bool { if focus, ok := boolProperty(view, Focusable, view.session); ok { return focus } return false } func (view *viewData) Remove(tag string) { view.remove(strings.ToLower(tag)) } func (view *viewData) remove(tag string) { switch tag { case ID: view.viewID = "" case UserData: delete(view.properties, tag) case Style, StyleDisabled: if _, ok := view.properties[tag]; ok { delete(view.properties, tag) updateProperty(view.htmlID(), "class", view.htmlClass(IsDisabled(view, "")), view.session) } case FocusEvent, LostFocusEvent: view.removeFocusListener(tag) case KeyDownEvent, KeyUpEvent: view.removeKeyListener(tag) case ClickEvent, DoubleClickEvent, MouseDown, MouseUp, MouseMove, MouseOut, MouseOver, ContextMenuEvent: view.removeMouseListener(tag) case PointerDown, PointerUp, PointerMove, PointerOut, PointerOver, PointerCancel: view.removePointerListener(tag) case TouchStart, TouchEnd, TouchMove, TouchCancel: view.removeTouchListener(tag) case TransitionRunEvent, TransitionStartEvent, TransitionEndEvent, TransitionCancelEvent: view.removeTransitionListener(tag) case AnimationStartEvent, AnimationEndEvent, AnimationIterationEvent, AnimationCancelEvent: view.removeAnimationListener(tag) case ResizeEvent, ScrollEvent: delete(view.properties, tag) case Content: if _, ok := view.properties[Content]; ok { delete(view.properties, Content) updateInnerHTML(view.htmlID(), view.session) } default: view.viewStyle.remove(tag) viewPropertyChanged(view, tag) } view.propertyChangedEvent(tag) } func (view *viewData) propertyChangedEvent(tag string) { if listener, ok := view.changeListener[tag]; ok { listener(view, tag) } switch tag { case BorderLeft, BorderRight, BorderTop, BorderBottom, BorderStyle, BorderLeftStyle, BorderRightStyle, BorderTopStyle, BorderBottomStyle, BorderColor, BorderLeftColor, BorderRightColor, BorderTopColor, BorderBottomColor, BorderWidth, BorderLeftWidth, BorderRightWidth, BorderTopWidth, BorderBottomWidth: tag = Border case CellBorderStyle, CellBorderColor, CellBorderWidth, CellBorderLeft, CellBorderLeftStyle, CellBorderLeftColor, CellBorderLeftWidth, CellBorderRight, CellBorderRightStyle, CellBorderRightColor, CellBorderRightWidth, CellBorderTop, CellBorderTopStyle, CellBorderTopColor, CellBorderTopWidth, CellBorderBottom, CellBorderBottomStyle, CellBorderBottomColor, CellBorderBottomWidth: tag = CellBorder case OutlineColor, OutlineStyle, OutlineWidth: tag = Outline case RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY, RadiusTopRight, RadiusTopRightX, RadiusTopRightY, RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY, RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY: tag = Radius case MarginTop, MarginRight, MarginBottom, MarginLeft, "top-margin", "right-margin", "bottom-margin", "left-margin": tag = Margin case PaddingTop, PaddingRight, PaddingBottom, PaddingLeft, "top-padding", "right-padding", "bottom-padding", "left-padding": tag = Padding case CellPaddingTop, CellPaddingRight, CellPaddingBottom, CellPaddingLeft: tag = CellPadding case ColumnSeparatorStyle, ColumnSeparatorWidth, ColumnSeparatorColor: tag = ColumnSeparator default: return } if listener, ok := view.changeListener[tag]; ok { listener(view, tag) } } func (view *viewData) Set(tag string, value any) bool { return view.set(strings.ToLower(tag), value) } func (view *viewData) set(tag string, value any) bool { if value == nil { view.remove(tag) return true } result := func(res bool) bool { if res { view.propertyChangedEvent(tag) } return res } switch tag { case ID: text, ok := value.(string) if !ok { notCompatibleType(ID, value) return false } view.viewID = text case UserData: view.properties[tag] = value case Style, StyleDisabled: text, ok := value.(string) if !ok { notCompatibleType(ID, value) return false } view.properties[tag] = text if view.created { updateProperty(view.htmlID(), "class", view.htmlClass(IsDisabled(view, "")), view.session) } case FocusEvent, LostFocusEvent: return result(view.setFocusListener(tag, value)) case KeyDownEvent, KeyUpEvent: return result(view.setKeyListener(tag, value)) case ClickEvent, DoubleClickEvent, MouseDown, MouseUp, MouseMove, MouseOut, MouseOver, ContextMenuEvent: return result(view.setMouseListener(tag, value)) case PointerDown, PointerUp, PointerMove, PointerOut, PointerOver, PointerCancel: return result(view.setPointerListener(tag, value)) case TouchStart, TouchEnd, TouchMove, TouchCancel: return result(view.setTouchListener(tag, value)) case TransitionRunEvent, TransitionStartEvent, TransitionEndEvent, TransitionCancelEvent: return result(view.setTransitionListener(tag, value)) case AnimationStartEvent, AnimationEndEvent, AnimationIterationEvent, AnimationCancelEvent: return result(view.setAnimationListener(tag, value)) case ResizeEvent, ScrollEvent: return result(view.setFrameListener(tag, value)) default: if !view.viewStyle.set(tag, value) { return false } if view.created { viewPropertyChanged(view, tag) } } view.propertyChangedEvent(tag) return true } func viewPropertyChanged(view *viewData, tag string) { if view.updateTransformProperty(tag) { return } htmlID := view.htmlID() session := view.session switch tag { case Disabled: updateInnerHTML(view.parentHTMLID(), session) return case Visibility: switch GetVisibility(view, "") { case Invisible: updateCSSProperty(htmlID, Visibility, "hidden", session) updateCSSProperty(htmlID, "display", "", session) case Gone: updateCSSProperty(htmlID, Visibility, "hidden", session) updateCSSProperty(htmlID, "display", "none", session) default: updateCSSProperty(htmlID, Visibility, "visible", session) updateCSSProperty(htmlID, "display", "", session) } return case Background: updateCSSProperty(htmlID, Background, view.backgroundCSS(session), session) return case Border: if getBorder(view, Border) == nil { buffer := session.updateScript(htmlID) if buffer == nil { session.startUpdateScript(htmlID) } updateCSSProperty(htmlID, BorderWidth, "", session) updateCSSProperty(htmlID, BorderColor, "", session) updateCSSProperty(htmlID, BorderStyle, "none", session) if buffer == nil { session.finishUpdateScript(htmlID) } return } fallthrough case BorderLeft, BorderRight, BorderTop, BorderBottom: if border := getBorder(view, Border); border != nil { buffer := session.updateScript(htmlID) if buffer == nil { session.startUpdateScript(htmlID) } updateCSSProperty(htmlID, BorderWidth, border.cssWidthValue(session), session) updateCSSProperty(htmlID, BorderColor, border.cssColorValue(session), session) updateCSSProperty(htmlID, BorderStyle, border.cssStyleValue(session), session) if buffer == nil { session.finishUpdateScript(htmlID) } } return case BorderStyle, BorderLeftStyle, BorderRightStyle, BorderTopStyle, BorderBottomStyle: if border := getBorder(view, Border); border != nil { updateCSSProperty(htmlID, BorderStyle, border.cssStyleValue(session), session) } return case BorderColor, BorderLeftColor, BorderRightColor, BorderTopColor, BorderBottomColor: if border := getBorder(view, Border); border != nil { updateCSSProperty(htmlID, BorderColor, border.cssColorValue(session), session) } return case BorderWidth, BorderLeftWidth, BorderRightWidth, BorderTopWidth, BorderBottomWidth: if border := getBorder(view, Border); border != nil { updateCSSProperty(htmlID, BorderWidth, border.cssWidthValue(session), session) } return case Outline, OutlineColor, OutlineStyle, OutlineWidth: updateCSSProperty(htmlID, Outline, GetOutline(view, "").cssString(), session) return case Shadow: updateCSSProperty(htmlID, "box-shadow", shadowCSS(view, Shadow, session), session) return case TextShadow: updateCSSProperty(htmlID, "text-shadow", shadowCSS(view, TextShadow, session), session) return case Radius, RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY, RadiusTopRight, RadiusTopRightX, RadiusTopRightY, RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY, RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY: radius := GetRadius(view, "") updateCSSProperty(htmlID, "border-radius", radius.cssString(), session) return case Margin, MarginTop, MarginRight, MarginBottom, MarginLeft, "top-margin", "right-margin", "bottom-margin", "left-margin": margin := GetMargin(view, "") updateCSSProperty(htmlID, Margin, margin.cssString(), session) return case Padding, PaddingTop, PaddingRight, PaddingBottom, PaddingLeft, "top-padding", "right-padding", "bottom-padding", "left-padding": padding := GetPadding(view, "") updateCSSProperty(htmlID, Padding, padding.cssString(), session) return case AvoidBreak: if avoid, ok := boolProperty(view, AvoidBreak, session); ok { if avoid { updateCSSProperty(htmlID, "break-inside", "avoid", session) } else { updateCSSProperty(htmlID, "break-inside", "auto", session) } } return case Clip: if clip := getClipShape(view, Clip, session); clip != nil && clip.valid(session) { updateCSSProperty(htmlID, `clip-path`, clip.cssStyle(session), session) } else { updateCSSProperty(htmlID, `clip-path`, "none", session) } return case ShapeOutside: if clip := getClipShape(view, ShapeOutside, session); clip != nil && clip.valid(session) { updateCSSProperty(htmlID, ShapeOutside, clip.cssStyle(session), session) } else { updateCSSProperty(htmlID, ShapeOutside, "none", session) } return case Filter: text := "" if value := view.getRaw(tag); value != nil { if filter, ok := value.(ViewFilter); ok { text = filter.cssStyle(session) } } updateCSSProperty(htmlID, tag, text, session) return case BackdropFilter: text := "" if value := view.getRaw(tag); value != nil { if filter, ok := value.(ViewFilter); ok { text = filter.cssStyle(session) } } buffer := session.updateScript(htmlID) if buffer == nil { session.startUpdateScript(htmlID) } updateCSSProperty(htmlID, "-webkit-backdrop-filter", text, session) updateCSSProperty(htmlID, tag, text, session) if buffer == nil { session.finishUpdateScript(htmlID) } return case FontName: if font, ok := stringProperty(view, FontName, session); ok { updateCSSProperty(htmlID, "font-family", font, session) } else { updateCSSProperty(htmlID, "font-family", "", session) } return case Italic: if state, ok := boolProperty(view, tag, session); ok { if state { updateCSSProperty(htmlID, "font-style", "italic", session) } else { updateCSSProperty(htmlID, "font-style", "normal", session) } } else { updateCSSProperty(htmlID, "font-style", "", session) } return case SmallCaps: if state, ok := boolProperty(view, tag, session); ok { if state { updateCSSProperty(htmlID, "font-variant", "small-caps", session) } else { updateCSSProperty(htmlID, "font-variant", "normal", session) } } else { updateCSSProperty(htmlID, "font-variant", "", session) } return case Strikethrough, Overline, Underline: updateCSSProperty(htmlID, "text-decoration", view.cssTextDecoration(session), session) for _, tag2 := range []string{TextLineColor, TextLineStyle, TextLineThickness} { viewPropertyChanged(view, tag2) } return case Transition: view.updateTransitionCSS() return case AnimationTag: updateCSSProperty(htmlID, AnimationTag, view.animationCSS(session), session) return case AnimationPaused: paused, ok := boolProperty(view, AnimationPaused, session) if !ok { updateCSSProperty(htmlID, `animation-play-state`, ``, session) } else if paused { updateCSSProperty(htmlID, `animation-play-state`, `paused`, session) } else { updateCSSProperty(htmlID, `animation-play-state`, `running`, session) } return case ZIndex: if i, ok := intProperty(view, ZIndex, session, 0); ok { updateCSSProperty(htmlID, ZIndex, strconv.Itoa(i), session) } return case Row, Column: if parent := view.parentHTMLID(); parent != "" { updateInnerHTML(parent, session) } return case UserSelect: buffer := session.updateScript(htmlID) if buffer == nil { session.startUpdateScript(htmlID) } if userSelect, ok := boolProperty(view, UserSelect, session); ok { if userSelect { updateCSSProperty(htmlID, "-webkit-user-select", "auto", session) updateCSSProperty(htmlID, "user-select", "auto", session) } else { updateCSSProperty(htmlID, "-webkit-user-select", "none", session) updateCSSProperty(htmlID, "user-select", "none", session) } } else { updateCSSProperty(htmlID, "-webkit-user-select", "", session) updateCSSProperty(htmlID, "user-select", "", session) } if buffer == nil { session.finishUpdateScript(htmlID) } return } if cssTag, ok := sizeProperties[tag]; ok { size, _ := sizeProperty(view, tag, session) updateCSSProperty(htmlID, cssTag, size.cssString(""), session) return } colorTags := map[string]string{ BackgroundColor: BackgroundColor, TextColor: "color", TextLineColor: "text-decoration-color", CaretColor: CaretColor, AccentColor: AccentColor, } if cssTag, ok := colorTags[tag]; ok { if color, ok := colorProperty(view, tag, session); ok { updateCSSProperty(htmlID, cssTag, color.cssString(), session) } else { updateCSSProperty(htmlID, cssTag, "", session) } return } if valuesData, ok := enumProperties[tag]; ok && valuesData.cssTag != "" { n, _ := enumProperty(view, tag, session, 0) updateCSSProperty(htmlID, valuesData.cssTag, valuesData.cssValues[n], session) return } for _, floatTag := range []string{Opacity, ScaleX, ScaleY, ScaleZ, RotateX, RotateY, RotateZ} { if tag == floatTag { if f, ok := floatTextProperty(view, floatTag, session, 0); ok { updateCSSProperty(htmlID, floatTag, f, session) } return } } } func (view *viewData) Get(tag string) any { return view.get(strings.ToLower(tag)) } func (view *viewData) get(tag string) any { if tag == ID { if view.viewID != "" { return view.viewID } else { return nil } } return view.viewStyle.get(tag) } func (view *viewData) htmlTag() string { if semantics := GetSemantics(view, ""); semantics > DefaultSemantics { values := enumProperties[Semantics].cssValues if semantics < len(values) { return values[semantics] } } return "div" } func (view *viewData) closeHTMLTag() bool { return true } func (view *viewData) htmlID() string { if view._htmlID == "" { view._htmlID = view.session.nextViewID() } return view._htmlID } func (view *viewData) htmlSubviews(self View, buffer *strings.Builder) { } func (view *viewData) addToCSSStyle(addCSS map[string]string) { view.addCSS = addCSS } func (view *viewData) cssStyle(self View, builder cssBuilder) { view.viewStyle.cssViewStyle(builder, view.session) switch GetVisibility(view, "") { case Invisible: builder.add(`visibility`, `hidden`) case Gone: builder.add(`display`, `none`) } if view.addCSS != nil { for tag, value := range view.addCSS { builder.add(tag, value) } } } func (view *viewData) htmlProperties(self View, buffer *strings.Builder) { view.created = true if view.frame.Left != 0 || view.frame.Top != 0 || view.frame.Width != 0 || view.frame.Height != 0 { buffer.WriteString(fmt.Sprintf(` data-left="%g" data-top="%g" data-width="%g" data-height="%g"`, view.frame.Left, view.frame.Top, view.frame.Width, view.frame.Height)) } } func (view *viewData) htmlDisabledProperties(self View, buffer *strings.Builder) { if IsDisabled(self, "") { buffer.WriteString(` data-disabled="1"`) } else { buffer.WriteString(` data-disabled="0"`) } } func viewHTML(view View, buffer *strings.Builder) { viewHTMLTag := view.htmlTag() buffer.WriteRune('<') buffer.WriteString(viewHTMLTag) buffer.WriteString(` id="`) buffer.WriteString(view.htmlID()) buffer.WriteRune('"') disabled := IsDisabled(view, "") if cls := view.htmlClass(disabled); cls != "" { buffer.WriteString(` class="`) buffer.WriteString(cls) buffer.WriteRune('"') } var cssBuilder viewCSSBuilder view.cssStyle(view, &cssBuilder) if style := cssBuilder.finish(); style != "" { buffer.WriteString(` style="`) buffer.WriteString(style) buffer.WriteRune('"') } buffer.WriteRune(' ') view.htmlProperties(view, buffer) buffer.WriteRune(' ') view.htmlDisabledProperties(view, buffer) if view.isNoResizeEvent() { buffer.WriteString(` data-noresize="1" `) } else { buffer.WriteRune(' ') } if view.Focusable() && !disabled { buffer.WriteString(`tabindex="0" `) } buffer.WriteString(`onscroll="scrollEvent(this, event)" `) keyEventsHtml(view, buffer) mouseEventsHtml(view, buffer) pointerEventsHtml(view, buffer) touchEventsHtml(view, buffer) focusEventsHtml(view, buffer) transitionEventsHtml(view, buffer) animationEventsHtml(view, buffer) buffer.WriteRune('>') view.htmlSubviews(view, buffer) if view.closeHTMLTag() { buffer.WriteString(`') } } func (view *viewData) htmlClass(disabled bool) string { cls := "ruiView" disabledStyle := false if disabled { if value, ok := stringProperty(view, StyleDisabled, view.Session()); ok && value != "" { cls += " " + value disabledStyle = true } } if !disabledStyle { if value, ok := stringProperty(view, Style, view.Session()); ok { cls += " " + value } } if view.systemClass != "" { cls = view.systemClass + " " + cls } return cls } func (view *viewData) handleCommand(self View, command string, data DataObject) bool { switch command { case KeyDownEvent, KeyUpEvent: if !IsDisabled(self, "") { handleKeyEvents(self, command, data) } case ClickEvent, DoubleClickEvent, MouseDown, MouseUp, MouseMove, MouseOut, MouseOver, ContextMenuEvent: handleMouseEvents(self, command, data) case PointerDown, PointerUp, PointerMove, PointerOut, PointerOver, PointerCancel: handlePointerEvents(self, command, data) case TouchStart, TouchEnd, TouchMove, TouchCancel: handleTouchEvents(self, command, data) case FocusEvent: view.hasFocus = true for _, listener := range getFocusListeners(view, "", command) { listener(self) } case LostFocusEvent: view.hasFocus = false for _, listener := range getFocusListeners(view, "", command) { listener(self) } case TransitionRunEvent, TransitionStartEvent, TransitionEndEvent, TransitionCancelEvent: view.handleTransitionEvents(command, data) case AnimationStartEvent, AnimationEndEvent, AnimationIterationEvent, AnimationCancelEvent: view.handleAnimationEvents(command, data) case "scroll": view.onScroll(view, dataFloatProperty(data, "x"), dataFloatProperty(data, "y"), dataFloatProperty(data, "width"), dataFloatProperty(data, "height")) case "widthChanged": if value, ok := data.PropertyValue("width"); ok { if width, ok := StringToSizeUnit(value); ok { self.setRaw(Width, width) } } case "heightChanged": if value, ok := data.PropertyValue("height"); ok { if height, ok := StringToSizeUnit(value); ok { self.setRaw(Height, height) } } /* case "resize": floatProperty := func(tag string) float64 { if value, ok := data.PropertyValue(tag); ok { if result, err := strconv.ParseFloat(value, 64); err == nil { return result } } return 0 } self.onResize(self, floatProperty("x"), floatProperty("y"), floatProperty("width"), floatProperty("height")) return true */ default: return false } return true } func (view *viewData) SetChangeListener(tag string, listener func(View, string)) { if listener == nil { delete(view.changeListener, tag) } else { view.changeListener[tag] = listener } } func (view *viewData) HasFocus() bool { return view.hasFocus } func (view *viewData) String() string { return getViewString(view) }