package rui import ( "fmt" "strings" ) // Constants related to [GridLayout] specific properties and events const ( // CellVerticalAlign is the constant for "cell-vertical-align" property tag. // // Used by `GridLayout`, `SvgImageView`. // // Usage in `GridLayout`: // Sets the default vertical alignment of `GridLayout` children within the cell they are occupying. // // Supported types: `int`, `string`. // // Values: // `0`(`TopAlign`) or "top" - Top alignment. // `1`(`BottomAlign`) or "bottom" - Bottom alignment. // `2`(`CenterAlign`) or "center" - Center alignment. // `3`(`StretchAlign`) or "stretch" - Full height stretch. // // Usage in `SvgImageView`: // Same as "vertical-align". CellVerticalAlign PropertyName = "cell-vertical-align" // CellHorizontalAlign is the constant for "cell-horizontal-align" property tag. // // Used by `GridLayout`, `SvgImageView`. // // Usage in `GridLayout`: // Sets the default horizontal alignment of `GridLayout` children within the occupied cell. // // Supported types: `int`, `string`. // // Values: // `0`(`LeftAlign`) or "left" - Left alignment. // `1`(`RightAlign`) or "right" - Right alignment. // `2`(`CenterAlign`) or "center" - Center alignment. // `3`(`StretchAlign`) or "stretch" - Full width stretch. // // Usage in `SvgImageView`: // Same as "horizontal-align". CellHorizontalAlign PropertyName = "cell-horizontal-align" // CellVerticalSelfAlign is the constant for "cell-vertical-self-align" property tag. // // Used by `GridLayout`. // Sets the vertical alignment of `GridLayout` children within the cell they are occupying. The property is set for the // child view of `GridLayout`. // // Supported types: `int`, `string`. // // Values: // `0`(`TopAlign`) or "top" - Top alignment. // `1`(`BottomAlign`) or "bottom" - Bottom alignment. // `2`(`CenterAlign`) or "center" - Center alignment. // `3`(`StretchAlign`) or "stretch" - Full height stretch. CellVerticalSelfAlign PropertyName = "cell-vertical-self-align" // CellHorizontalSelfAlign is the constant for "cell-horizontal-self-align" property tag. // // Used by `GridLayout`. // Sets the horizontal alignment of `GridLayout` children within the occupied cell. The property is set for the child view // of `GridLayout`. // // Supported types: `int`, `string`. // // Values: // `0`(`LeftAlign`) or "left" - Left alignment. // `1`(`RightAlign`) or "right" - Right alignment. // `2`(`CenterAlign`) or "center" - Center alignment. // `3`(`StretchAlign`) or "stretch" - Full width stretch. CellHorizontalSelfAlign PropertyName = "cell-horizontal-self-align" ) // GridAdapter is an interface to define [GridLayout] content. [GridLayout] will query interface functions to populate // its content type GridAdapter interface { // GridColumnCount returns the number of columns in the grid GridColumnCount() int // GridRowCount returns the number of rows in the grid GridRowCount() int // GridCellContent creates a View at the given cell GridCellContent(row, column int, session Session) View } // GridCellColumnSpanAdapter implements the optional method of the [GridAdapter] interface type GridCellColumnSpanAdapter interface { // GridCellColumnSpan returns the number of columns that a cell spans. // Values ​​less than 1 are ignored. GridCellColumnSpan(row, column int) int } // GridCellColumnSpanAdapter implements the optional method of the [GridAdapter] interface type GridCellRowSpanAdapter interface { // GridCellRowSpan returns the number of rows that a cell spans // Values ​​less than 1 are ignored. GridCellRowSpan(row, column int) int } // GridLayout represents a GridLayout view type GridLayout interface { ViewsContainer // UpdateContent updates child Views if the "content" property value is set to GridAdapter, // otherwise does nothing UpdateGridContent() } type gridLayoutData struct { viewsContainerData adapter GridAdapter } // NewGridLayout create new GridLayout object and return it func NewGridLayout(session Session, params Params) GridLayout { view := new(gridLayoutData) view.init(session) setInitParams(view, params) return view } func newGridLayout(session Session) View { //return NewGridLayout(session, nil) return new(gridLayoutData) } // Init initialize fields of GridLayout by default values func (gridLayout *gridLayoutData) init(session Session) { gridLayout.viewsContainerData.init(session) gridLayout.tag = "GridLayout" gridLayout.systemClass = "ruiGridLayout" gridLayout.adapter = nil gridLayout.normalize = normalizeGridLayoutTag gridLayout.get = gridLayout.getFunc gridLayout.set = gridLayout.setFunc gridLayout.remove = gridLayout.removeFunc gridLayout.changed = gridLayout.propertyChanged } func setGridCellSize(properties Properties, tag PropertyName, value any) []PropertyName { setValues := func(values []string) bool { count := len(values) if count > 1 { sizes := make([]any, count) for i, val := range values { val = strings.Trim(val, " \t\n\r") if isConstantName(val) { sizes[i] = val } else if fn := parseSizeFunc(val); fn != nil { sizes[i] = SizeUnit{Type: SizeFunction, Function: fn} } else if size, err := stringToSizeUnit(val); err == nil { sizes[i] = size } else { invalidPropertyValue(tag, value) return false } } properties.setRaw(tag, sizes) } else if isConstantName(values[0]) { properties.setRaw(tag, values[0]) } else if size, err := stringToSizeUnit(values[0]); err == nil { properties.setRaw(tag, size) } else { invalidPropertyValue(tag, value) return false } return true } switch tag { case CellWidth, CellHeight: switch value := value.(type) { case SizeUnit, []SizeUnit: properties.setRaw(tag, value) case string: if !setValues(strings.Split(value, ",")) { return nil } case []string: if !setValues(value) { return nil } case []DataValue: count := len(value) if count == 0 { invalidPropertyValue(tag, value) return nil } values := make([]string, count) for i, val := range value { if val.IsObject() { invalidPropertyValue(tag, value) return nil } values[i] = val.Value() } if !setValues(values) { return nil } case []any: count := len(value) if count == 0 { invalidPropertyValue(tag, value) return nil } sizes := make([]any, count) for i, val := range value { switch val := val.(type) { case SizeUnit: sizes[i] = val case string: if isConstantName(val) { sizes[i] = val } else if size, err := stringToSizeUnit(val); err == nil { sizes[i] = size } else { invalidPropertyValue(tag, value) return nil } default: invalidPropertyValue(tag, value) return nil } } properties.setRaw(tag, sizes) default: notCompatibleType(tag, value) return nil } return []PropertyName{tag} } return nil } func gridCellSizesCSS(properties Properties, tag PropertyName, session Session) string { switch cellSize := gridCellSizes(properties, tag, session); len(cellSize) { case 0: case 1: if cellSize[0].Type != Auto { return `repeat(auto-fill, ` + cellSize[0].cssString(`auto`, session) + `)` } default: allAuto := true allEqual := true for i, size := range cellSize { if size.Type != Auto { allAuto = false } if i > 0 && !size.Equal(cellSize[0]) { allEqual = false } } if !allAuto { if allEqual { return fmt.Sprintf(`repeat(%d, %s)`, len(cellSize), cellSize[0].cssString(`auto`, session)) } buffer := allocStringBuilder() defer freeStringBuilder(buffer) for _, size := range cellSize { buffer.WriteRune(' ') buffer.WriteString(size.cssString(`auto`, session)) } return buffer.String() } } return "" } func normalizeGridLayoutTag(tag PropertyName) PropertyName { tag = defaultNormalize(tag) switch tag { case VerticalAlign: return CellVerticalAlign case HorizontalAlign: return CellHorizontalAlign case "row-gap": return GridRowGap case ColumnGap: return GridColumnGap } return tag } func (gridLayout *gridLayoutData) getFunc(tag PropertyName) any { switch tag { case Gap: rowGap := GetGridRowGap(gridLayout) columnGap := GetGridColumnGap(gridLayout) if rowGap.Equal(columnGap) { return rowGap } return AutoSize() case Content: if gridLayout.adapter != nil { return gridLayout.adapter } } return gridLayout.viewsContainerData.getFunc(tag) } func (gridLayout *gridLayoutData) removeFunc(tag PropertyName) []PropertyName { switch tag { case Gap: result := []PropertyName{} for _, tag := range []PropertyName{GridRowGap, GridColumnGap} { if gridLayout.getRaw(tag) != nil { gridLayout.setRaw(tag, nil) result = append(result, tag) } } return result case Content: if len(gridLayout.views) > 0 || gridLayout.adapter != nil { gridLayout.views = []View{} gridLayout.adapter = nil return []PropertyName{Content} } return []PropertyName{} } return gridLayout.viewsContainerData.removeFunc(tag) } func (gridLayout *gridLayoutData) setFunc(tag PropertyName, value any) []PropertyName { switch tag { case Gap: result := gridLayout.setFunc(GridRowGap, value) if result != nil { if gap := gridLayout.getRaw(GridRowGap); gap != nil { gridLayout.setRaw(GridColumnGap, gap) result = append(result, GridColumnGap) } } return result case Content: if adapter, ok := value.(GridAdapter); ok { gridLayout.adapter = adapter gridLayout.createGridContent() } else if gridLayout.setContent(value) { gridLayout.adapter = nil } else { return nil } return []PropertyName{Content} } return gridLayout.viewsContainerData.setFunc(tag, value) } func (gridLayout *gridLayoutData) propertyChanged(tag PropertyName) { switch tag { case CellWidth: session := gridLayout.Session() session.updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`, gridCellSizesCSS(gridLayout, CellWidth, session)) case CellHeight: session := gridLayout.Session() session.updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`, gridCellSizesCSS(gridLayout, CellHeight, session)) default: gridLayout.viewsContainerData.propertyChanged(tag) } } func (gridLayout *gridLayoutData) createGridContent() bool { if gridLayout.adapter == nil { return false } adapter := gridLayout.adapter gridLayout.views = []View{} session := gridLayout.session htmlID := gridLayout.htmlID() isDisabled := IsDisabled(gridLayout) var columnSpan GridCellColumnSpanAdapter = nil if span, ok := adapter.(GridCellColumnSpanAdapter); ok { columnSpan = span } var rowSpan GridCellRowSpanAdapter = nil if span, ok := adapter.(GridCellRowSpanAdapter); ok { rowSpan = span } width := adapter.GridColumnCount() height := adapter.GridRowCount() for column := 0; column < width; column++ { for row := 0; row < height; row++ { if view := adapter.GridCellContent(row, column, session); view != nil { view.setParentID(htmlID) columnCount := 1 if columnSpan != nil { columnCount = columnSpan.GridCellColumnSpan(row, column) } if columnCount > 1 { view.Set(Column, Range{First: column, Last: column + columnCount - 1}) } else { view.Set(Column, column) } rowCount := 1 if rowSpan != nil { rowCount = rowSpan.GridCellRowSpan(row, column) } if rowCount > 1 { view.Set(Row, Range{First: row, Last: row + rowCount - 1}) } else { view.Set(Row, row) } if isDisabled { view.Set(Disabled, true) } gridLayout.views = append(gridLayout.views, view) } } } return true } func (gridLayout *gridLayoutData) UpdateGridContent() { if gridLayout.createGridContent() { if gridLayout.created { updateInnerHTML(gridLayout.htmlID(), gridLayout.session) } if listener, ok := gridLayout.changeListener[Content]; ok { listener(gridLayout, Content) } } } func gridCellSizes(properties Properties, tag PropertyName, session Session) []SizeUnit { if value := properties.Get(tag); value != nil { switch value := value.(type) { case []SizeUnit: return value case SizeUnit: return []SizeUnit{value} case []any: result := make([]SizeUnit, len(value)) for i, val := range value { result[i] = AutoSize() switch val := val.(type) { case SizeUnit: result[i] = val case string: if text, ok := session.resolveConstants(val); ok { result[i], _ = stringToSizeUnit(text) } } } return result case string: if text, ok := session.resolveConstants(value); ok { values := strings.Split(text, ",") result := make([]SizeUnit, len(values)) for i, val := range values { result[i], _ = stringToSizeUnit(val) } return result } } } return []SizeUnit{} } /* func (gridLayout *gridLayoutData) cssStyle(self View, builder cssBuilder) { gridLayout.viewsContainerData.cssStyle(self, builder) } */ // GetCellVerticalAlign returns the vertical align of a GridLayout cell content: TopAlign (0), BottomAlign (1), CenterAlign (2), StretchAlign (3) // If the second argument (subviewID) is not specified or it is "" 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. 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. 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. func GetCellWidth(view View, subviewID ...string) []SizeUnit { if len(subviewID) > 0 && subviewID[0] != "" { view = ViewByID(view, subviewID[0]) } if view != nil { return gridCellSizes(view, CellWidth, view.Session()) } return []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. func GetCellHeight(view View, subviewID ...string) []SizeUnit { if len(subviewID) > 0 && subviewID[0] != "" { view = ViewByID(view, subviewID[0]) } if view != nil { return gridCellSizes(view, CellHeight, view.Session()) } return []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. 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. func GetGridColumnGap(view View, subviewID ...string) SizeUnit { return sizeStyledProperty(view, subviewID, GridColumnGap, false) }