diff --git a/app_styles.css b/app_styles.css index 19cda67..ba496a7 100644 --- a/app_styles.css +++ b/app_styles.css @@ -79,7 +79,7 @@ ul:focus { } .ruiPopupLayer { - background-color: rgba(128,128,128,0.1); + /*background-color: rgba(128,128,128,0.1);*/ position: absolute; top: 0px; bottom: 0px; diff --git a/color.go b/color.go index ba29f12..cdfdb5f 100644 --- a/color.go +++ b/color.go @@ -11,6 +11,14 @@ import ( // Color - represent color in argb format type Color uint32 +// ARGB creates Color using alpha, red, green and blue components +func ARGB[T int | uint | int8 | uint8](alpha, red, green, blue T) Color { + return ((Color(alpha) & 0xFF) << 24) + + ((Color(red) & 0xFF) << 16) + + ((Color(green) & 0xFF) << 8) + + (Color(blue) & 0xFF) +} + // ARGB - return alpha, red, green and blue components of the color func (color Color) ARGB() (uint8, uint8, uint8, uint8) { return uint8(color >> 24), diff --git a/popup.go b/popup.go index ca963e9..8cc0148 100644 --- a/popup.go +++ b/popup.go @@ -1,6 +1,7 @@ package rui import ( + "fmt" "strings" ) @@ -143,6 +144,48 @@ const ( // See `SizeUnit` description for more details. ArrowWidth PropertyName = "arrow-width" + // ShowTransform is the constant for "show-transform" property tag. + // + // 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. + // + // Supported types: `TransformProperty`, `string`. + // + // See `TransformProperty` description for more details. + // + // Conversion rules: + // `TransformProperty` - stored as is, no conversion performed. + // `string` - string representation of `Transform` interface. Example: "_{translate-x = 10px, scale-y = 1.1}". + ShowTransform = "show-transform" + + // ShowDuration is the constant for "show-duration" property tag. + // + // Used by `Popup`. + // Sets the length of time in seconds that a Popup show/hide animation takes to complete. + // + // Supported types: `float`, `int`, `string`. + // + // Internal type is `float`, other types converted to it during assignment. + ShowDuration = "show-duration" + + // ShowTiming is the constant for "show-timing" property tag. + // + // Used by `StackLayout`. + // Set how a Popup show/hide animation progresses through the duration of each cycle. + // + // Supported types: `string`. + // + // Values: + // "ease"(`EaseTiming`) - Speed increases towards the middle and slows down at the end. + // "ease-in"(`EaseInTiming`) - Speed is slow at first, but increases in the end. + // "ease-out"(`EaseOutTiming`) - Speed is fast at first, but decreases in the end. + // "ease-in-out"(`EaseInOutTiming`) - Speed is slow at first, but quickly increases and at the end it decreases again. + // "linear"(`LinearTiming`) - Constant speed. + ShowTiming = "show-timing" + + ShowOpacity = "show-opacity" + // ArrowOffset is the constant for "arrow-offset" property tag. // // Used by `Popup`. @@ -219,14 +262,21 @@ type Popup interface { html(buffer *strings.Builder) viewByHTMLID(id string) View keyEvent(event KeyEvent) bool + showAnimation() + dissmissAnimation(listener func(PropertyName)) bool } type popupData struct { - layerView View - view View + layerView GridLayout + popupView GridLayout + contentView View buttons []PopupButton cancelable bool dismissListener []func(Popup) + showTransform TransformProperty + showOpacity float64 + showDuration float64 + showTiming string } type popupManager struct { @@ -397,39 +447,15 @@ func (arrow *popupArrow) createView(popupView View) View { return NewGridLayout(session, params) } -func (popup *popupData) init(view View, popupParams Params) { - popup.view = view - popup.cancelable = false - session := view.Session() +func (popup *popupData) layerCellWidth(arrowLocation int, popupParams Params, session Session) []SizeUnit { - columnCount := 3 - rowCount := 3 - popupRow := 1 - popupColumn := 1 - arrow := popupArrow{ - row: 1, - column: 1, - align: CenterAlign, - } - - switch arrow.location, _ = enumProperty(popupParams, Arrow, session, NoneArrow); arrow.location { - case TopArrow: - rowCount = 4 - popupRow = 2 - - case BottomArrow: - rowCount = 4 - arrow.row = 2 - - case LeftArrow: + var columnCount int + switch arrowLocation { + case LeftArrow, RightArrow: columnCount = 4 - popupColumn = 2 - - case RightArrow: - columnCount = 4 - arrow.column = 2 default: + columnCount = 3 } cellWidth := make([]SizeUnit, columnCount) @@ -444,6 +470,19 @@ func (popup *popupData) init(view View, popupParams Params) { cellWidth[0] = Fr(1) cellWidth[columnCount-1] = Fr(1) } + return cellWidth +} + +func (popup *popupData) layerCellHeight(arrowLocation int, popupParams Params, session Session) []SizeUnit { + + var rowCount int + switch arrowLocation { + case TopArrow, BottomArrow: + rowCount = 4 + + default: + rowCount = 3 + } cellHeight := make([]SizeUnit, rowCount) switch vAlign, _ := enumProperty(popupParams, VerticalAlign, session, CenterAlign); vAlign { @@ -458,16 +497,47 @@ func (popup *popupData) init(view View, popupParams Params) { cellHeight[rowCount-1] = Fr(1) } + return cellHeight +} + +func (popup *popupData) init(view View, popupParams Params) { + popup.contentView = view + popup.cancelable = false + session := view.Session() + + popupRow := 1 + popupColumn := 1 + arrow := popupArrow{ + row: 1, + column: 1, + align: CenterAlign, + } + + switch arrow.location, _ = enumProperty(popupParams, Arrow, session, NoneArrow); arrow.location { + case TopArrow: + popupRow = 2 + + case BottomArrow: + arrow.row = 2 + + case LeftArrow: + popupColumn = 2 + + case RightArrow: + arrow.column = 2 + } + layerParams := Params{ Style: "ruiPopupLayer", MaxWidth: Percent(100), MaxHeight: Percent(100), - CellWidth: cellWidth, - CellHeight: cellHeight, + CellWidth: popup.layerCellWidth(arrow.location, popupParams, session), + CellHeight: popup.layerCellHeight(arrow.location, popupParams, session), } params := Params{ Style: "ruiPopup", + ID: "ruiPopup", Row: popupRow, Column: popupColumn, MaxWidth: Percent(100), @@ -483,10 +553,14 @@ func (popup *popupData) init(view View, popupParams Params) { } var closeButton View = nil - outsideClose := false - buttons := []PopupButton{} - titleStyle := "ruiPopupTitle" var title View = nil + outsideClose := false + popup.buttons = []PopupButton{} + titleStyle := "ruiPopupTitle" + + popup.showOpacity = 1.0 + popup.showDuration = 1.0 + popup.showTiming = "easy" for tag, value := range popupParams { if value != nil { @@ -532,10 +606,10 @@ func (popup *popupData) init(view View, popupParams Params) { case Buttons: switch value := value.(type) { case PopupButton: - buttons = []PopupButton{value} + popup.buttons = []PopupButton{value} case []PopupButton: - buttons = value + popup.buttons = value } case Title: @@ -587,13 +661,36 @@ func (popup *popupData) init(view View, popupParams Params) { case ArrowOffset: arrow.off, _ = sizeProperty(popupParams, ArrowOffset, session) + case ShowOpacity: + if opacity, _ := floatProperty(popupParams, ShowOpacity, session, 1); opacity >= 0 && opacity < 1 { + popup.showOpacity = opacity + } + + case ShowTransform: + if transform := valueToTransformProperty(value); transform != nil && !transform.empty() { + popup.showTransform = transform + } + + case ShowDuration: + if duration, _ := floatProperty(popupParams, ShowDuration, session, 1); duration > 0 { + popup.showDuration = duration + } + + case ShowTiming: + if text, ok := value.(string); ok { + text, _ = session.resolveConstants(text) + if isTimingFunctionValid(text) { + popup.showTiming = text + } + } + default: params[tag] = value } } } - popupView := NewGridLayout(view.Session(), params) + popup.popupView = NewGridLayout(view.Session(), params) var popupCellHeight []SizeUnit viewRow := 0 @@ -605,7 +702,7 @@ func (popup *popupData) init(view View, popupParams Params) { if closeButton != nil { titleContent = append(titleContent, closeButton) } - popupView.Append(NewGridLayout(session, Params{ + popup.popupView.Append(NewGridLayout(session, Params{ Row: 0, Style: titleStyle, CellWidth: []any{Fr(1), AutoSize()}, @@ -621,10 +718,9 @@ func (popup *popupData) init(view View, popupParams Params) { } view.Set(Row, viewRow) - popupView.Append(view) + popup.popupView.Append(view) - popup.buttons = buttons - if buttonCount := len(buttons); buttonCount > 0 { + if buttonCount := len(popup.buttons); buttonCount > 0 { buttonsAlign, _ := enumProperty(params, ButtonsAlign, session, RightAlign) popupCellHeight = append(popupCellHeight, AutoSize()) gap, _ := sizeConstant(session, "ruiPopupButtonGap") @@ -641,7 +737,7 @@ func (popup *popupData) init(view View, popupParams Params) { buttonsPanel.Set(Margin, gap) } - for i, button := range buttons { + for i, button := range popup.buttons { title := button.Title if title == "" && button.Type == CancelButton { title = "Cancel" @@ -668,33 +764,82 @@ func (popup *popupData) init(view View, popupParams Params) { buttonsPanel.Append(buttonView) } - popupView.Append(NewGridLayout(session, Params{ + popup.popupView.Append(NewGridLayout(session, Params{ Row: viewRow + 1, CellHorizontalAlign: buttonsAlign, Content: buttonsPanel, })) } - popupView.Set(CellHeight, popupCellHeight) + popup.popupView.Set(CellHeight, popupCellHeight) if arrow.location != NoneArrow { - layerParams[Content] = []View{popupView, arrow.createView(popupView)} + layerParams[Content] = []View{popup.popupView, arrow.createView(popup.popupView)} } else { - layerParams[Content] = []View{popupView} + layerParams[Content] = []View{popup.popupView} } popup.layerView = NewGridLayout(session, layerParams) + + if popup.showOpacity != 1 || popup.showTransform != nil { + animation := NewAnimation(Params{ + Duration: popup.showDuration, + TimingFunction: popup.showTiming, + }) + if popup.showOpacity != 1 { + popup.popupView.Set(Opacity, popup.showOpacity) + popup.popupView.SetTransition(Opacity, animation) + } + if popup.showTransform != nil { + popup.popupView.Set(Transform, popup.showTransform) + popup.popupView.SetTransition(Transform, animation) + } + } else { + session.updateCSSProperty("ruiPopupLayer", "transition", "") + } + if outsideClose { popup.layerView.Set(ClickEvent, popup.cancel) } } -func (popup popupData) View() View { - return popup.view +func (popup *popupData) showAnimation() { + if popup.showOpacity != 1 || popup.showTransform != nil { + htmlID := popup.popupView.htmlID() + session := popup.Session() + if popup.showOpacity != 1 { + session.updateCSSProperty(htmlID, string(Opacity), "1") + } + if popup.showTransform != nil { + session.updateCSSProperty(htmlID, string(Transform), "") + } + } +} + +func (popup *popupData) dissmissAnimation(listener func(PropertyName)) bool { + if popup.showOpacity != 1 || popup.showTransform != nil { + session := popup.Session() + popup.popupView.Set(TransitionEndEvent, listener) + popup.popupView.Set(TransitionCancelEvent, listener) + + htmlID := popup.popupView.htmlID() + if popup.showOpacity != 1 { + session.updateCSSProperty(htmlID, string(Opacity), fmt.Sprintf("%.2f", popup.showOpacity)) + } + if popup.showTransform != nil { + session.updateCSSProperty(htmlID, string(Transform), popup.showTransform.transformCSS(session)) + } + return true + } + return false +} + +func (popup *popupData) View() View { + return popup.contentView } func (popup *popupData) Session() Session { - return popup.view.Session() + return popup.contentView.Session() } func (popup *popupData) cancel() { @@ -709,9 +854,6 @@ func (popup *popupData) cancel() { func (popup *popupData) Dismiss() { popup.Session().popupManager().dismissPopup(popup) - for _, listener := range popup.dismissListener { - listener(popup) - } } func (popup *popupData) Show() { @@ -719,7 +861,6 @@ func (popup *popupData) Show() { } func (popup *popupData) html(buffer *strings.Builder) { - viewHTML(popup.layerView, buffer, "") } @@ -728,6 +869,8 @@ func (popup *popupData) viewByHTMLID(id string) View { } func (popup *popupData) onDismiss() { + popup.Session().callFunc("removeView", popup.layerView.htmlID()) + for _, listener := range popup.dismissListener { listener(popup) } @@ -808,6 +951,7 @@ func (manager *popupManager) showPopup(popup Popup) { session.updateCSSProperty("ruiTooltipLayer", "opacity", "0") session.updateCSSProperty("ruiPopupLayer", "visibility", "visible") session.updateCSSProperty("ruiRoot", "pointer-events", "none") + popup.showAnimation() } func (manager *popupManager) dismissPopup(popup Popup) { @@ -821,31 +965,37 @@ func (manager *popupManager) dismissPopup(popup Popup) { return } - session := popup.Session() - if manager.popups[count-1] == popup { - if count == 1 { - manager.popups = []Popup{} - session.updateCSSProperty("ruiRoot", "pointer-events", "auto") - session.updateCSSProperty("ruiPopupLayer", "visibility", "hidden") - session.updateInnerHTML("ruiPopupLayer", "") - } else { - manager.popups = manager.popups[:count-1] - manager.updatePopupLayerInnerHTML(session) + index := -1 + for n, p := range manager.popups { + if p == popup { + index = n + break } - popup.onDismiss() + } + + if index < 0 { return } - for n, p := range manager.popups { - if p == popup { - if n == 0 { - manager.popups = manager.popups[1:] + session := popup.Session() + listener := func(PropertyName) { + if index == count-1 { + if count == 1 { + manager.popups = []Popup{} + session.updateCSSProperty("ruiRoot", "pointer-events", "auto") + session.updateCSSProperty("ruiPopupLayer", "visibility", "hidden") } else { - manager.popups = append(manager.popups[:n], manager.popups[n+1:]...) + manager.popups = manager.popups[:count-1] } - manager.updatePopupLayerInnerHTML(session) - popup.onDismiss() - return + } else if index == 0 { + manager.popups = manager.popups[1:] + } else { + manager.popups = append(manager.popups[:index], manager.popups[index+1:]...) } + popup.onDismiss() + } + + if !popup.dissmissAnimation(listener) { + listener("") } } diff --git a/stackLayout.go b/stackLayout.go index 1d77b6e..efb49e6 100644 --- a/stackLayout.go +++ b/stackLayout.go @@ -534,6 +534,10 @@ func (layout *stackLayoutData) moveToFrontByIndex(index int, onShow []func(View) layout.views = append(append(layout.views[:index], layout.views[index+1:]...), view) } + if !layout.created { + return + } + session := layout.Session() pageID := view.htmlID() + "page" peekPageID := peekID + "page" @@ -649,23 +653,25 @@ func (layout *stackLayoutData) Append(view View) { layout.views = append(layout.views, view) } - buffer := allocStringBuilder() - defer freeStringBuilder(buffer) + if layout.created { + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) - buffer.WriteString(`
`) - viewHTML(view, buffer, "") - buffer.WriteString(`
`) + buffer.WriteString(`
`) + viewHTML(view, buffer, "") + buffer.WriteString(`
`) - session := layout.Session() - if count > 0 { - session.updateCSSProperty(layout.views[count-1].htmlID()+"page", "visibility", "hidden") - } - session.appendToInnerHTML(stackID, buffer.String()) + session := layout.Session() + if count > 0 { + session.updateCSSProperty(layout.views[count-1].htmlID()+"page", "visibility", "hidden") + } + session.appendToInnerHTML(stackID, buffer.String()) - if listener, ok := layout.changeListener[Content]; ok { - listener(layout, Content) + if listener, ok := layout.changeListener[Content]; ok { + listener(layout, Content) + } } } diff --git a/transform.go b/transform.go index c9a9756..a1c10da 100644 --- a/transform.go +++ b/transform.go @@ -712,7 +712,7 @@ func (style *viewStyle) writeViewTransformCSS(builder cssBuilder, session Sessio } if transform := getTransformProperty(style, Transform); transform != nil { - builder.add(`TransformProperty`, transform.transformCSS(session)) + builder.add(`transform`, transform.transformCSS(session)) } } diff --git a/viewsContainer.go b/viewsContainer.go index 372c13c..0add036 100644 --- a/viewsContainer.go +++ b/viewsContainer.go @@ -75,14 +75,16 @@ func (container *viewsContainerData) Append(view View) { container.views = append(container.views, view) } - buffer := allocStringBuilder() - defer freeStringBuilder(buffer) + if container.created { + buffer := allocStringBuilder() + defer freeStringBuilder(buffer) - viewHTML(view, buffer, "") - container.Session().appendToInnerHTML(htmlID, buffer.String()) + viewHTML(view, buffer, "") + container.Session().appendToInnerHTML(htmlID, buffer.String()) - if listener, ok := container.changeListener[Content]; ok { - listener(container, Content) + if listener, ok := container.changeListener[Content]; ok { + listener(container, Content) + } } } } @@ -102,9 +104,12 @@ func (container *viewsContainerData) Insert(view View, index int) { } else { container.views = append([]View{view}, container.views...) } - updateInnerHTML(htmlID, container.Session()) - if listener, ok := container.changeListener[Content]; ok { - listener(container, Content) + + if container.created { + updateInnerHTML(htmlID, container.Session()) + if listener, ok := container.changeListener[Content]; ok { + listener(container, Content) + } } } } @@ -132,9 +137,11 @@ func (container *viewsContainerData) RemoveView(index int) View { view.setParentID("") - container.Session().callFunc("removeView", view.htmlID()) - if listener, ok := container.changeListener[Content]; ok { - listener(container, Content) + if container.created { + container.Session().callFunc("removeView", view.htmlID()) + if listener, ok := container.changeListener[Content]; ok { + listener(container, Content) + } } return view }