package rui import ( "fmt" "reflect" "slices" "strings" ) // Constants for [Popup] specific properties and events const ( // Title is the constant for "title" property tag. // // Used by Popup, TabsLayout. // // Usage in Popup: // Define the title. // // Supported types: string. // // Usage in TabsLayout: // Set the title of the tab. The property is set for the child view of TabsLayout. // // Supported types: string. Title PropertyName = "title" // TitleStyle is the constant for "title-style" property tag. // // Used by Popup. // Set popup title style. Default title style is "ruiPopupTitle". // // Supported types: string. TitleStyle PropertyName = "title-style" // CloseButton is the constant for "close-button" property tag. // // Used by Popup. // Controls whether a close button can be added to the popup. Default value is false. // // Supported types: bool, int, string. // // Values: // - true, 1, "true", "yes", "on", "1" - Close button will be added to a title bar of a window. // - false, 0, "false", "no", "off", "0" - Popup without a close button. CloseButton PropertyName = "close-button" // OutsideClose is the constant for "outside-close" property tag. // // Used by Popup. // Controls whether popup can be closed by clicking outside of the window. Default value is false. // // Supported types: bool, int, string. // // Values: // - true, 1, "true", "yes", "on", "1" - Clicking outside the popup window will automatically call the Dismiss() method. // - false, 0, "false", "no", "off", "0" - Clicking outside the popup window has no effect. OutsideClose PropertyName = "outside-close" // Buttons is the constant for "buttons" property tag. // // Used by Popup. // Buttons that will be placed at the bottom of the popup. // // Supported types: PopupButton, []PopupButton. // // Internal type is []PopupButton, other types converted to it during assignment. // See PopupButton description for more details. Buttons PropertyName = "buttons" // ButtonsAlign is the constant for "buttons-align" property tag. // // Used by Popup. // Set the horizontal alignment of popup buttons. // // Supported types: int, string. // // Values: // - 0 (LeftAlign) or "left" - Left alignment. // - 1 (RightAlign) or "right" - Right alignment. // - 2 (CenterAlign) or "center" - Center alignment. // - 3 (StretchAlign) or "stretch" - Width alignment. ButtonsAlign PropertyName = "buttons-align" // DismissEvent is the constant for "dismiss-event" property tag. // // Used by Popup. // Used to track the closing state of the Popup. It occurs after the Popup disappears from the screen. // // General listener format: // // func(popup rui.Popup) // // where: // popup - Interface of a popup which generated this event. // // Allowed listener formats: // // func() DismissEvent PropertyName = "dismiss-event" // Arrow is the constant for "arrow" property tag. // // Used by Popup. // Add an arrow to popup. Default value is "none". // // Supported types: int, string. // // Values: // - 0 (NoneArrow) or "none" - No arrow. // - 1 (TopArrow) or "top" - Arrow at the top side of the pop-up window. // - 2 (RightArrow) or "right" - Arrow on the right side of the pop-up window. // - 3 (BottomArrow) or "bottom" - Arrow at the bottom of the pop-up window. // - 4 (LeftArrow) or "left" - Arrow on the left side of the pop-up window. Arrow PropertyName = "arrow" // ArrowAlign is the constant for "arrow-align" property tag. // // Used by Popup. // Set the horizontal alignment of the popup arrow. Default value is "center". // // Supported types: int, string. // // Values: // - 0 (TopAlign/LeftAlign) or "top" - Top/left alignment. // - 1 (BottomAlign/RightAlign) or "bottom" - Bottom/right alignment. // - 2 (CenterAlign) or "center" - Center alignment. ArrowAlign PropertyName = "arrow-align" // ArrowSize is the constant for "arrow-size" property tag. // // Used by Popup. // Set the size(length) of the popup arrow. Default value is 16px defined by @ruiArrowSize constant. // // Supported types: SizeUnit, SizeFunc, string, float, int. // // Internal type is SizeUnit, other types converted to it during assignment. // See SizeUnit description for more details. ArrowSize PropertyName = "arrow-size" // ArrowWidth is the constant for "arrow-width" property tag. // // Used by Popup. // Set the width of the popup arrow. Default value is 16px defined by @ruiArrowWidth constant. // // Supported types: SizeUnit, SizeFunc, string, float, int. // // Internal type is SizeUnit, other types converted to it during assignment. // 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/hiding. // // 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 PropertyName = "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 PropertyName = "show-duration" // ShowTiming is the constant for "show-timing" property tag. // // Used by Popup. // 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. // - "step(n)" (StepTiming(n int) function) - Timing function along stepCount stops along the transition, displaying each stop for equal lengths of time. // - "cubic-bezier(x1, y1, x2, y2)" (CubicBezierTiming(x1, y1, x2, y2 float64) function) - Cubic-Bezier curve timing function. x1 and x2 must be in the range [0, 1]. ShowTiming PropertyName = "show-timing" // ShowOpacity is the constant for "show-opacity" property tag. // // Used by Popup. // In [1..0] range sets the start opacity of Popup show animation (the finish animation opacity is 1). // Opacity is the degree to which content behind the view is hidden, and is the opposite of transparency. // // Supported types: float, int, string. // // Internal type is float, other types converted to it during assignment. ShowOpacity PropertyName = "show-opacity" // ArrowOffset is the constant for "arrow-offset" property tag. // // Used by Popup. // Set the offset of the popup arrow. // // Supported types: SizeUnit, SizeFunc, string, float, int. // // Internal type is SizeUnit, other types converted to it during assignment. // See SizeUnit description for more details. ArrowOffset PropertyName = "arrow-offset" // NoneArrow is value of the popup "arrow" property: no arrow NoneArrow = 0 // TopArrow is value of the popup "arrow" property: // Arrow at the top side of the pop-up window TopArrow = 1 // RightArrow is value of the popup "arrow" property: // Arrow on the right side of the pop-up window RightArrow = 2 // BottomArrow is value of the popup "arrow" property: // Arrow at the bottom of the pop-up window BottomArrow = 3 // LeftArrow is value of the popup "arrow" property: // Arrow on the left side of the pop-up window LeftArrow = 4 ) // Constants which are used as a values of [PopupButtonType] variables const ( // NormalButton is the constant of the popup button type: the normal button NormalButton PopupButtonType = 0 // DefaultButton is the constant of the popup button type: button that fires when the "Enter" key is pressed DefaultButton PopupButtonType = 1 // CancelButton is the constant of the popup button type: button that fires when the "Escape" key is pressed CancelButton PopupButtonType = 2 ) const ( popupLayerID = "ruiPopupLayer" popupArrowID = "ruiPopupArrow" popupButtonsID = "ruiPopupButtons" popupID = "ruiPopup" popupContentID = "ruiPopupContent" popupTitleID = "ruiPopupTitle" ) // PopupButtonType represent popup button type type PopupButtonType int // PopupButton describes a button that will be placed at the bottom of the window. type PopupButton struct { // Title of the button Title string // Type of the button Type PopupButtonType // OnClick is the handler function that gets called when the button is pressed OnClick func(Popup) } type popupButton struct { title string buttonType PopupButtonType onClick popupListener } // Popup represents a Popup view type Popup interface { Properties // View returns a content view of the popup View() View // Session returns current client session Session() Session // Show displays a popup Show() // Dismiss closes a popup Dismiss() onDismiss() html(buffer *strings.Builder) viewByHTMLID(id string) View keyEvent(event KeyEvent) bool showAnimation() dismissAnimation(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 dismiss bool } type popupData struct { propertyList session Session layerView GridLayout popupView GridLayout contentContainer ColumnLayout contentView View } type popupManager struct { popups []Popup } func (popup *popupData) createArrowView(location int) View { session := popup.Session() getSize := func(tag PropertyName, constTag string) SizeUnit { size, _ := sizeProperty(popup, tag, session) if size.Type != Auto && size.Value > 0 { return size } if value, ok := session.Constant(constTag); ok { if size, ok := StringToSizeUnit(value); ok && size.Type != Auto && size.Value > 0 { return size } } return Px(16) } size := getSize(ArrowSize, "ruiArrowSize") width := getSize(ArrowWidth, "ruiArrowWidth") params := Params{BackgroundColor: GetBackgroundColor(popup.popupView)} if shadow := GetShadowProperty(popup.popupView); shadow != nil { params[Shadow] = shadow } if filter := GetBackdropFilter(popup.popupView); filter != nil { params[BackdropFilter] = filter } switch location { case TopArrow: params[Row] = 0 params[Column] = 1 params[Clip] = NewPolygonClip([]any{"0%", "100%", "50%", "0%", "100%", "100%"}) params[Width] = width params[Height] = size case RightArrow: params[Row] = 1 params[Column] = 0 params[Clip] = NewPolygonClip([]any{"0%", "0%", "100%", "50%", "0%", "100%"}) params[Width] = size params[Height] = width case BottomArrow: params[Row] = 0 params[Column] = 1 params[Clip] = NewPolygonClip([]any{"0%", "0%", "50%", "100%", "100%", "0%"}) params[Width] = width params[Height] = size case LeftArrow: params[Row] = 1 params[Column] = 0 params[Clip] = NewPolygonClip([]any{"100%", "0%", "0%", "50%", "100%", "100%"}) params[Width] = size params[Height] = width } arrowView := NewView(session, params) params = Params{ ID: popupArrowID, Content: arrowView, } switch location { case TopArrow: params[Row] = 1 params[Column] = 2 case BottomArrow: params[Row] = 3 params[Column] = 2 case LeftArrow: params[Row] = 2 params[Column] = 1 case RightArrow: params[Row] = 2 params[Column] = 3 } off, _ := sizeProperty(popup, ArrowOffset, session) align, _ := enumProperty(popup, ArrowAlign, session, CenterAlign) if align != CenterAlign && off.Type == Auto { r := GetRadius(popup.popupView) switch location { case TopArrow: switch align { case LeftAlign: off = r.TopLeftX case RightAlign: off = r.TopRightX } case BottomArrow: switch align { case LeftAlign: off = r.BottomLeftX case RightAlign: off = r.BottomRightX } case RightArrow: switch align { case TopAlign: off = r.TopRightY case BottomAlign: off = r.BottomRightY } case LeftArrow: switch align { case TopAlign: off = r.TopLeftY case BottomAlign: off = r.BottomLeftY } } } switch location { case TopArrow, BottomArrow: cellWidth := make([]SizeUnit, 3) switch align { case LeftAlign: cellWidth[0] = off cellWidth[2] = Fr(1) case RightAlign: cellWidth[0] = Fr(1) cellWidth[2] = off default: cellWidth[0] = Fr(1) cellWidth[2] = Fr(1) if off.Type != Auto && off.Value != 0 { arrowView.Set(MarginLeft, off) } } params[CellWidth] = cellWidth case RightArrow, LeftArrow: cellHeight := make([]SizeUnit, 3) switch align { case TopAlign: cellHeight[0] = off cellHeight[2] = Fr(1) case BottomAlign: cellHeight[0] = Fr(1) cellHeight[2] = off default: cellHeight[0] = Fr(1) cellHeight[2] = Fr(1) if off.Type != Auto && off.Value != 0 { arrowView.Set(MarginTop, off) } } params[CellHeight] = cellHeight } return NewGridLayout(session, params) } func (popup *popupData) layerCellWidth() []SizeUnit { cellWidth := make([]SizeUnit, 5) switch hAlign, _ := enumProperty(popup, HorizontalAlign, popup.session, CenterAlign); hAlign { case LeftAlign: cellWidth[4] = Fr(1) case RightAlign: cellWidth[0] = Fr(1) default: cellWidth[0] = Fr(1) cellWidth[4] = Fr(1) } return cellWidth } func (popup *popupData) layerCellHeight() []SizeUnit { cellHeight := make([]SizeUnit, 5) switch vAlign, _ := enumProperty(popup, VerticalAlign, popup.session, CenterAlign); vAlign { case LeftAlign: cellHeight[4] = Fr(1) case RightAlign: cellHeight[0] = Fr(1) default: cellHeight[0] = Fr(1) cellHeight[4] = Fr(1) } return cellHeight } func (popup *popupData) Get(tag PropertyName) any { switch tag = defaultNormalize(tag); tag { case Content: return popup.contentView case "layer-view": if popup.layerView == nil { popup.layerView = popup.createLayerView() } return popup.layerView } return popup.properties[tag] } func (popup *popupData) arrowLocation() int { result, _ := enumProperty(popup, Arrow, popup.session, NoneArrow) return result } func (popup *popupData) supported(tag PropertyName) bool { switch tag { case Row, Column, CellWidth, CellHeight, Gap, GridColumnGap, GridRowGap, CellVerticalAlign, CellHorizontalAlign, CellVerticalSelfAlign, CellHorizontalSelfAlign: return false } return true } func (popup *popupData) Set(tag PropertyName, value any) bool { if value == nil { popup.Remove(tag) return true } switch tag = defaultNormalize(tag); tag { case Buttons: return popup.setButtons(value) case Title: switch value := value.(type) { case string: popup.setRaw(Title, value) case View: popup.setRaw(Title, value) default: notCompatibleType(Title, value) return false } popup.propertyChanged(Title) return true case Content: switch value := value.(type) { case View: popup.contentView = value case DataObject: view := CreateViewFromObject(popup.session, value, nil) if view == nil { return false } popup.contentView = view case string: if len(value) > 0 && value[0] == '@' { view := CreateViewFromResources(popup.session, value[1:]) if view != nil { popup.contentView = view break } } popup.contentView = NewTextView(popup.session, Params{Text: value}) default: notCompatibleType(Buttons, value) return false } if binding := popup.getRaw(Binding); binding != nil { popup.contentView.Set(Binding, binding) } popup.setRaw(Content, popup.contentView) popup.propertyChanged(Content) return true case Binding: popup.contentView.Set(Binding, value) popup.setRaw(Binding, value) popup.propertyChanged(Binding) return true case DismissEvent: if listeners, ok := valueToPopupEventListeners(value); ok { if listeners != nil { popup.setRaw(DismissEvent, listeners) popup.propertyChanged(DismissEvent) return true } } notCompatibleType(tag, value) return false case ShowTransform: return setTransformProperty(popup, tag, value) } if popup.supported(tag) { tags := viewStyleSet(popup, tag, value) if len(tags) > 0 { for _, tag := range tags { popup.propertyChanged(tag) } return true } } else { ErrorLogF(`"%s" property is not supported by the popup.`, string(tag)) } return false } func (popup *popupData) Remove(tag PropertyName) { tag = defaultNormalize(tag) switch tag { case Content: popup.contentView = nil } if popup.supported(tag) { tags := viewStyleRemove(popup, tag) for _, tag := range tags { popup.propertyChanged(tag) } } else { ErrorLogF(`"%s" property is not supported by the popup.`, string(tag)) } } func (popup *popupData) propertyChanged(tag PropertyName) { if popup.layerView == nil { return } switch tag { case Content: popup.layerView.RemoveViewByID(popupID) // TODO } } func (popup *popupData) AllTags() []PropertyName { tags := make([]PropertyName, 0, len(popup.properties)+1) for tag := range popup.properties { tags = append(tags, tag) } if popup.contentView != nil { tags = append(tags, Content) } slices.Sort(tags) return tags } func (popup *popupData) View() View { return popup.contentView } func (popup *popupData) Session() Session { return popup.session } func (popup *popupData) setButtons(value any) bool { popupButtonFromObject := func(obj DataObject) popupButton { var button popupButton button.title, _ = obj.PropertyValue(string(Title)) if text, ok := obj.PropertyValue("type"); ok { text, _ = popup.session.resolveConstants(text) t, _ := enumStringToInt(text, []string{"normal", "default", "cancel"}, true) button.buttonType = PopupButtonType(t) } if fn, ok := obj.PropertyValue("click"); ok { button.onClick = newPopupListenerBinding(fn, true) } else if button.buttonType == CancelButton { button.onClick = newPopupListener0(popup.Dismiss) } return button } switch value := value.(type) { case PopupButton: popup.setRaw(Buttons, []popupButton{ { title: value.Title, buttonType: value.Type, onClick: newPopupListener1(value.OnClick), }, }) case []PopupButton: buttons := make([]popupButton, 0, len(value)) for _, button := range value { buttons = append(buttons, popupButton{ title: button.Title, buttonType: button.Type, onClick: newPopupListener1(button.OnClick), }) } popup.setRaw(Buttons, buttons) case []popupButton: popup.setRaw(Buttons, value) case DataObject: popup.setRaw(Buttons, []popupButton{popupButtonFromObject(value)}) case []DataValue: buttons := make([]popupButton, 0, len(value)) for _, val := range value { if val.IsObject() { buttons = append(buttons, popupButtonFromObject(val.Object())) } else { notCompatibleType(Buttons, val) } } if len(buttons) > 0 { popup.setRaw(Buttons, buttons) } case []any: buttons := make([]popupButton, 0, len(value)) for _, val := range value { switch val := val.(type) { case DataObject: buttons = append(buttons, popupButtonFromObject(val)) case popupButton: buttons = append(buttons, val) case PopupButton: buttons = append(buttons, popupButton{ title: val.Title, buttonType: val.Type, onClick: newPopupListener1(val.OnClick), }) default: notCompatibleType(Buttons, val) } } if len(buttons) > 0 { popup.setRaw(Buttons, buttons) } default: notCompatibleType(Buttons, value) return false } popup.propertyChanged(Buttons) return true } func (popup *popupData) buttons() []popupButton { if value := popup.getRaw(Buttons); value != nil { if result, ok := value.([]popupButton); ok { return result } } return nil } func (popup *popupData) cancel() { if buttons := popup.buttons(); buttons != nil { for _, button := range buttons { if button.buttonType == CancelButton && button.onClick != nil { button.onClick.Run(popup) return } } } popup.Dismiss() } func (popup *popupData) Dismiss() { popup.Session().popupManager().dismissPopup(popup) } func (popup *popupData) Show() { popup.Session().popupManager().showPopup(popup) } func (popup *popupData) showAnimation() { opacity, _ := floatProperty(popup, ShowOpacity, popup.session, 1) transform := getTransformProperty(popup, ShowTransform) if opacity != 1 || transform != nil { htmlID := popup.popupView.htmlID() session := popup.Session() if opacity != 1 { session.updateCSSProperty(htmlID, string(Opacity), "1") } if transform != nil { session.updateCSSProperty(htmlID, string(Transform), "") } } } func (popup *popupData) dismissAnimation(listener func(PropertyName)) bool { opacity, _ := floatProperty(popup, ShowOpacity, popup.session, 1) transform := getTransformProperty(popup, ShowTransform) if opacity != 1 || transform != nil { session := popup.Session() popup.popupView.Set(TransitionEndEvent, listener) popup.popupView.Set(TransitionCancelEvent, listener) htmlID := popup.popupView.htmlID() if opacity != 1 { session.updateCSSProperty(htmlID, string(Opacity), fmt.Sprintf("%.2f", opacity)) } if transform != nil { session.updateCSSProperty(htmlID, string(Transform), transform.transformCSS(session)) } return true } return false } func (popup *popupData) html(buffer *strings.Builder) { if popup.layerView == nil { popup.layerView = popup.createLayerView() } viewHTML(popup.layerView, buffer, "") } func (popup *popupData) viewByHTMLID(id string) View { if popup.layerView != nil { return viewByHTMLID(id, popup.layerView) } return nil } func (popup *popupData) onDismiss() { if popup.layerView != nil { popup.Session().callFunc("removeView", popup.layerView.htmlID()) if value := popup.getRaw(DismissEvent); value != nil { if listeners, ok := value.([]popupListener); ok { for _, listener := range listeners { listener.Run(popup) } } } } } func (popup *popupData) keyEvent(event KeyEvent) bool { if !event.AltKey && !event.CtrlKey && !event.ShiftKey && !event.MetaKey { switch event.Code { case EnterKey: for _, button := range popup.buttons() { if button.buttonType == DefaultButton && button.onClick != nil { button.onClick.Run(popup) return true } } case EscapeKey: cancelable := func() bool { if closeButton, _ := boolProperty(popup, CloseButton, popup.session); closeButton { return true } if outsideClose, _ := boolProperty(popup, OutsideClose, popup.session); outsideClose { return true } for _, button := range popup.buttons() { if button.buttonType == CancelButton { return true } } return false } if cancelable() { popup.cancel() return true } } } return false } func (popup *popupData) createButtonsPanel() GridLayout { buttons := popup.buttons() if buttonCount := len(buttons); buttonCount > 0 { session := popup.session buttonsAlign, _ := enumProperty(popup, ButtonsAlign, session, RightAlign) gap, _ := sizeConstant(session, "ruiPopupButtonGap") cellWidth := []SizeUnit{} for range buttonCount { cellWidth = append(cellWidth, Fr(1)) } buttonsPanel := NewGridLayout(session, Params{ CellWidth: cellWidth, }) if gap.Type != Auto && gap.Value > 0 { buttonsPanel.Set(Gap, gap) buttonsPanel.Set(Margin, gap) } for i, button := range buttons { title := button.title if title == "" && button.buttonType == CancelButton { title = "Cancel" } buttonView := NewButton(session, Params{ Column: i, Content: title, }) if button.onClick != nil { fn := button.onClick.Run buttonView.Set(ClickEvent, func() { fn(popup) }) } else if button.buttonType == CancelButton { buttonView.Set(ClickEvent, popup.cancel) } if button.buttonType == DefaultButton { buttonView.Set(Style, "ruiDefaultButton") } buttonsPanel.Append(buttonView) } return NewGridLayout(session, Params{ ID: popupButtonsID, Column: 0, Row: 2, CellHorizontalAlign: buttonsAlign, Content: buttonsPanel, }) } return nil } func (popup *popupData) createTitleView() GridLayout { session := popup.Session() var closeButton View = nil if hasButton, _ := boolProperty(popup, CloseButton, popup.session); hasButton { closeButton = NewGridLayout(session, Params{ Column: 1, Height: "@ruiPopupTitleHeight", Width: "@ruiPopupTitleHeight", CellHorizontalAlign: CenterAlign, CellVerticalAlign: CenterAlign, TextSize: Px(20), Content: "✕", NotTranslate: true, ClickEvent: popup.cancel, }) } var title View = nil if value := popup.getRaw(Title); value != nil { switch value := value.(type) { case string: if len(value) > 0 && value[0] == '@' { title = CreateViewFromResources(session, value[1:]) if title != nil { break } } title = NewTextView(session, Params{Text: value}) case View: title = value } } if title == nil && closeButton == nil { return nil } titleStyle := "ruiPopupTitle" if style, ok := stringProperty(popup, TitleStyle, session); ok { titleStyle = style } titleContent := []View{} if title != nil { titleContent = append(titleContent, title) } if closeButton != nil { titleContent = append(titleContent, closeButton) } return NewGridLayout(session, Params{ ID: popupTitleID, Row: 0, Column: 0, Style: titleStyle, CellWidth: []any{Fr(1), AutoSize()}, CellVerticalAlign: CenterAlign, PaddingLeft: Px(12), Content: titleContent, }) } func (popup *popupData) createContentContainer() ColumnLayout { params := Params{ ID: popupContentID, Column: 0, Row: 1, } if popup.contentView != nil { params[Content] = popup.contentView } popup.contentContainer = NewColumnLayout(popup.session, params) return popup.contentContainer } func (popup *popupData) createLayerView() GridLayout { session := popup.session params := Params{ Style: "ruiPopup", ID: popupID, Row: 2, Column: 2, MaxWidth: Percent(100), MaxHeight: Percent(100), CellVerticalAlign: StretchAlign, CellHorizontalAlign: StretchAlign, CellHeight: []SizeUnit{AutoSize(), Fr(1), AutoSize()}, ClickEvent: func(View) {}, Shadow: NewShadowProperty(Params{ SpreadRadius: Px(4), Blur: Px(16), ColorTag: "@ruiPopupShadow", }), } popupProperties := []PropertyName{ Content, Title, TitleStyle, CloseButton, OutsideClose, Buttons, ButtonsAlign, DismissEvent, Arrow, ArrowAlign, ArrowSize, ArrowWidth, ArrowOffset, ShowTransform, ShowDuration, ShowTiming, ShowOpacity, VerticalAlign, HorizontalAlign, Margin, Row, Column, CellWidth, CellHeight, CellVerticalAlign, CellHorizontalAlign, } for tag, value := range popup.properties { if !slices.Contains(popupProperties, tag) { params[tag] = value } } views := make([]View, 0, 3) if title := popup.createTitleView(); title != nil { views = append(views, title) } views = append(views, popup.createContentContainer()) if buttons := popup.createButtonsPanel(); buttons != nil { views = append(views, buttons) } params[Content] = views popup.popupView = NewGridLayout(session, params) layerParams := Params{ Style: popupLayerID, MaxWidth: Percent(100), MaxHeight: Percent(100), CellWidth: popup.layerCellWidth(), CellHeight: popup.layerCellHeight(), } if margin, ok := getBounds(popup, Margin, session); ok { layerParams[Padding] = margin } if location := popup.arrowLocation(); location != NoneArrow { layerParams[Content] = []View{popup.popupView, popup.createArrowView(location)} } else { layerParams[Content] = []View{popup.popupView} } popup.layerView = NewGridLayout(session, layerParams) opacity, _ := floatProperty(popup, ShowOpacity, popup.session, 1) transform := getTransformProperty(popup, ShowTransform) if opacity != 1 || transform != nil { duration, _ := floatProperty(popup, ShowDuration, session, 1) timing, ok := stringProperty(popup, ShowTiming, session) if !ok { timing = EaseTiming } animation := NewAnimationProperty(Params{ Duration: duration, TimingFunction: timing, }) if opacity != 1 { popup.popupView.Set(Opacity, opacity) popup.popupView.SetTransition(Opacity, animation) } if transform != nil { popup.popupView.Set(Transform, transform) popup.popupView.SetTransition(Transform, animation) } } else { session.updateCSSProperty(popupLayerID, "transition", "") } outsideClose, _ := boolProperty(popup, OutsideClose, session) if outsideClose { popup.layerView.Set(ClickEvent, popup.cancel) } return popup.layerView } // NewPopup creates a new Popup func NewPopup(view View, param Params) Popup { if view == nil { return nil } popup := new(popupData) popup.session = view.Session() popup.contentView = view popup.properties = map[PropertyName]any{} for tag, value := range param { popup.Set(tag, value) } return popup } // CreatePopupFromObject create new Popup and initialize it by content of object. Parameters: // - session - the session to which the view will be attached (should not be nil); // - text - text describing Popup; // - 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 CreatePopupFromObject(session Session, object DataObject, binding any) Popup { popup := new(popupData) popup.session = session popup.properties = map[PropertyName]any{} for key, value := range object.ToParams() { popup.Set(key, value) } if binding != nil { popup.Set(Binding, binding) } return popup } // CreatePopupFromText create new Popup 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 Popup; // - 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 CreatePopupFromText(session Session, text string, binding any) Popup { data, err := ParseDataText(text) if err != nil { ErrorLog(err.Error()) return nil } return CreatePopupFromObject(session, data, binding) } // ShowPopup creates a new Popup and shows it func ShowPopup(view View, param Params) Popup { popup := NewPopup(view, param) if popup != nil { popup.Show() } return popup } func (manager *popupManager) updatePopupLayerInnerHTML(session Session) { if manager.popups == nil { manager.popups = []Popup{} session.updateInnerHTML(popupLayerID, "") return } buffer := allocStringBuilder() defer freeStringBuilder(buffer) for _, popup := range manager.popups { popup.html(buffer) } session.updateInnerHTML(popupLayerID, buffer.String()) } func (manager *popupManager) showPopup(popup Popup) { if popup == nil { return } session := popup.Session() if len(manager.popups) == 0 { manager.popups = []Popup{popup} } else { manager.popups = append(manager.popups, popup) } session.callFunc("blurCurrent") manager.updatePopupLayerInnerHTML(session) session.updateCSSProperty("ruiTooltipLayer", "visibility", "hidden") session.updateCSSProperty("ruiTooltipLayer", "opacity", "0") session.updateCSSProperty(popupLayerID, "visibility", "visible") session.updateCSSProperty("ruiRoot", "pointer-events", "none") popup.showAnimation() } func (manager *popupManager) dismissPopup(popup Popup) { if manager.popups == nil { manager.popups = []Popup{} return } count := len(manager.popups) if count <= 0 || popup == nil { return } index := -1 for n, p := range manager.popups { if p == popup { index = n break } } if index < 0 { return } session := popup.Session() listener := func(PropertyName) { switch index { case 0: if count == 1 { manager.popups = []Popup{} session.updateCSSProperty("ruiRoot", "pointer-events", "auto") session.updateCSSProperty(popupLayerID, "visibility", "hidden") } else { manager.popups = manager.popups[1:] } case count - 1: manager.popups = manager.popups[:count-1] default: manager.popups = append(manager.popups[:index], manager.popups[index+1:]...) } popup.onDismiss() } if !popup.dismissAnimation(listener) { 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, dismiss bool) popupListener { obj := new(popupListenerBinding) obj.name = name obj.dismiss = dismiss return obj } func (data *popupListenerBinding) runDismiss(popup Popup) bool { if strings.ToLower(data.name) == "dismiss" { popup.Dismiss() return true } return false } func (data *popupListenerBinding) Run(popup Popup) { bind := popup.View().binding() if bind == nil { if !data.dismiss || !data.runDismiss(popup) { 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() { if !data.dismiss || !data.runDismiss(popup) { 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, false)}, 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, false)) default: return nil, false } } } return result, len(result) > 0 } return nil, false }