mirror of https://github.com/anoshenko/rui.git
423 lines
11 KiB
Go
423 lines
11 KiB
Go
|
package rui
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
// ViewStyle interface of the style of view
|
||
|
type ViewStyle interface {
|
||
|
Properties
|
||
|
cssViewStyle(buffer cssBuilder, session Session, view View)
|
||
|
}
|
||
|
|
||
|
type viewStyle struct {
|
||
|
propertyList
|
||
|
//transitions map[string]ViewTransition
|
||
|
}
|
||
|
|
||
|
// Range defines range limits. The First and Last value are included in the range
|
||
|
type Range struct {
|
||
|
First, Last int
|
||
|
}
|
||
|
|
||
|
// String returns a string representation of the Range struct
|
||
|
func (r Range) String() string {
|
||
|
if r.First == r.Last {
|
||
|
return fmt.Sprintf("%d", r.First)
|
||
|
}
|
||
|
return fmt.Sprintf("%d:%d", r.First, r.Last)
|
||
|
}
|
||
|
|
||
|
func (r *Range) setValue(value string) bool {
|
||
|
var err error
|
||
|
if strings.Contains(value, ":") {
|
||
|
values := strings.Split(value, ":")
|
||
|
if len(values) != 2 {
|
||
|
ErrorLog("Invalid range value: " + value)
|
||
|
return false
|
||
|
}
|
||
|
if r.First, err = strconv.Atoi(strings.Trim(values[0], " \t\n\r")); err != nil {
|
||
|
ErrorLog(`Invalid first range value "` + value + `" (` + err.Error() + ")")
|
||
|
return false
|
||
|
}
|
||
|
if r.Last, err = strconv.Atoi(strings.Trim(values[1], " \t\n\r")); err != nil {
|
||
|
ErrorLog(`Invalid last range value "` + value + `" (` + err.Error() + ")")
|
||
|
return false
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
if r.First, err = strconv.Atoi(value); err != nil {
|
||
|
ErrorLog(`Invalid range value "` + value + `" (` + err.Error() + ")")
|
||
|
return false
|
||
|
}
|
||
|
r.Last = r.First
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (style *viewStyle) init() {
|
||
|
style.propertyList.init()
|
||
|
//style.shadows = []ViewShadow{}
|
||
|
//style.transitions = map[string]ViewTransition{}
|
||
|
}
|
||
|
|
||
|
// NewViewStyle create new ViewStyle object
|
||
|
func NewViewStyle(params Params) ViewStyle {
|
||
|
style := new(viewStyle)
|
||
|
style.init()
|
||
|
for tag, value := range params {
|
||
|
style.Set(tag, value)
|
||
|
}
|
||
|
return style
|
||
|
}
|
||
|
|
||
|
func (style *viewStyle) cssTextDecoration(session Session) string {
|
||
|
buffer := allocStringBuilder()
|
||
|
defer freeStringBuilder(buffer)
|
||
|
|
||
|
noDecoration := false
|
||
|
if strikethrough, ok := boolProperty(style, Strikethrough, session); ok {
|
||
|
if strikethrough {
|
||
|
buffer.WriteString("line-through")
|
||
|
}
|
||
|
noDecoration = true
|
||
|
}
|
||
|
|
||
|
if overline, ok := boolProperty(style, Overline, session); ok {
|
||
|
if overline {
|
||
|
if buffer.Len() > 0 {
|
||
|
buffer.WriteRune(' ')
|
||
|
}
|
||
|
buffer.WriteString("overline")
|
||
|
}
|
||
|
noDecoration = true
|
||
|
}
|
||
|
|
||
|
if underline, ok := boolProperty(style, Underline, session); ok {
|
||
|
if underline {
|
||
|
if buffer.Len() > 0 {
|
||
|
buffer.WriteRune(' ')
|
||
|
}
|
||
|
buffer.WriteString("underline")
|
||
|
}
|
||
|
noDecoration = true
|
||
|
}
|
||
|
|
||
|
if buffer.Len() == 0 && noDecoration {
|
||
|
return "none"
|
||
|
}
|
||
|
|
||
|
return buffer.String()
|
||
|
}
|
||
|
|
||
|
func split4Values(text string) []string {
|
||
|
values := strings.Split(text, ",")
|
||
|
count := len(values)
|
||
|
switch count {
|
||
|
case 1, 4:
|
||
|
return values
|
||
|
|
||
|
case 2:
|
||
|
if strings.Trim(values[1], " \t\r\n") == "" {
|
||
|
return values[:1]
|
||
|
}
|
||
|
|
||
|
case 5:
|
||
|
if strings.Trim(values[4], " \t\r\n") != "" {
|
||
|
return values[:4]
|
||
|
}
|
||
|
}
|
||
|
return []string{}
|
||
|
}
|
||
|
|
||
|
func (style *viewStyle) backgroundCSS(view View) string {
|
||
|
if value, ok := style.properties[Background]; ok {
|
||
|
if backgrounds, ok := value.([]BackgroundElement); ok {
|
||
|
buffer := allocStringBuilder()
|
||
|
defer freeStringBuilder(buffer)
|
||
|
|
||
|
for _, background := range backgrounds {
|
||
|
if value := background.cssStyle(view); value != "" {
|
||
|
if buffer.Len() > 0 {
|
||
|
buffer.WriteString(", ")
|
||
|
}
|
||
|
buffer.WriteString(value)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if buffer.Len() > 0 {
|
||
|
return buffer.String()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
func (style *viewStyle) cssViewStyle(builder cssBuilder, session Session, view View) {
|
||
|
|
||
|
if margin, ok := boundsProperty(style, Margin, session); ok {
|
||
|
margin.cssValue(Margin, builder)
|
||
|
}
|
||
|
|
||
|
if padding, ok := boundsProperty(style, Padding, session); ok {
|
||
|
padding.cssValue(Padding, builder)
|
||
|
}
|
||
|
|
||
|
if border := getBorder(style, Border); border != nil {
|
||
|
border.cssStyle(builder, session)
|
||
|
border.cssWidth(builder, session)
|
||
|
border.cssColor(builder, session)
|
||
|
}
|
||
|
|
||
|
radius := getRadius(style, session)
|
||
|
radius.cssValue(builder)
|
||
|
|
||
|
if outline := getOutline(style); outline != nil {
|
||
|
outline.ViewOutline(session).cssValue(builder)
|
||
|
}
|
||
|
|
||
|
if z, ok := intProperty(style, ZIndex, session, 0); ok {
|
||
|
builder.add(ZIndex, strconv.Itoa(z))
|
||
|
}
|
||
|
|
||
|
if opacity, ok := floatProperty(style, Opacity, session, 1.0); ok && opacity >= 0 && opacity <= 1 {
|
||
|
builder.add(Opacity, strconv.FormatFloat(opacity, 'f', 3, 32))
|
||
|
}
|
||
|
|
||
|
if n, ok := intProperty(style, ColumnCount, session, 0); ok && n > 0 {
|
||
|
builder.add(ColumnCount, strconv.Itoa(n))
|
||
|
}
|
||
|
|
||
|
for _, tag := range []string{
|
||
|
Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight, Left, Right, Top, Bottom,
|
||
|
TextSize, TextIndent, LetterSpacing, WordSpacing, LineHeight, TextLineThickness,
|
||
|
GridRowGap, GridColumnGap, ColumnGap, ColumnWidth} {
|
||
|
|
||
|
if size, ok := sizeProperty(style, tag, session); ok && size.Type != Auto {
|
||
|
cssTag, ok := sizeProperties[tag]
|
||
|
if !ok {
|
||
|
cssTag = tag
|
||
|
}
|
||
|
builder.add(cssTag, size.cssString(""))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
colorProperties := []struct{ property, cssTag string }{
|
||
|
{BackgroundColor, BackgroundColor},
|
||
|
{TextColor, "color"},
|
||
|
{TextLineColor, "text-decoration-color"},
|
||
|
}
|
||
|
for _, p := range colorProperties {
|
||
|
if color, ok := colorProperty(style, p.property, session); ok && color != 0 {
|
||
|
builder.add(p.cssTag, color.cssString())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if value, ok := enumProperty(style, BackgroundClip, session, 0); ok {
|
||
|
builder.add(BackgroundClip, enumProperties[BackgroundClip].values[value])
|
||
|
}
|
||
|
|
||
|
if background := style.backgroundCSS(view); background != "" {
|
||
|
builder.add("background", background)
|
||
|
}
|
||
|
|
||
|
if font, ok := stringProperty(style, FontName, session); ok && font != "" {
|
||
|
builder.add(`font-family`, font)
|
||
|
}
|
||
|
|
||
|
writingMode := 0
|
||
|
for _, tag := range []string{
|
||
|
TextAlign, TextTransform, TextWeight, TextLineStyle, WritingMode, TextDirection,
|
||
|
VerticalTextOrientation, CellVerticalAlign, CellHorizontalAlign, Cursor, WhiteSpace,
|
||
|
WordBreak, TextOverflow, Float, TableVerticalAlign} {
|
||
|
|
||
|
if data, ok := enumProperties[tag]; ok {
|
||
|
if tag != VerticalTextOrientation || (writingMode != VerticalLeftToRight && writingMode != VerticalRightToLeft) {
|
||
|
if value, ok := enumProperty(style, tag, session, 0); ok {
|
||
|
cssValue := data.values[value]
|
||
|
if cssValue != "" {
|
||
|
builder.add(data.cssTag, cssValue)
|
||
|
}
|
||
|
|
||
|
if tag == WritingMode {
|
||
|
writingMode = value
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for _, prop := range []struct{ tag, cssTag, off, on string }{
|
||
|
{tag: Italic, cssTag: "font-style", off: "normal", on: "italic"},
|
||
|
{tag: SmallCaps, cssTag: "font-variant", off: "normal", on: "small-caps"},
|
||
|
} {
|
||
|
if flag, ok := boolProperty(style, prop.tag, session); ok {
|
||
|
if flag {
|
||
|
builder.add(prop.cssTag, prop.on)
|
||
|
} else {
|
||
|
builder.add(prop.cssTag, prop.off)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if text := style.cssTextDecoration(session); text != "" {
|
||
|
builder.add("text-decoration", text)
|
||
|
}
|
||
|
|
||
|
if css := shadowCSS(style, Shadow, session); css != "" {
|
||
|
builder.add("box-shadow", css)
|
||
|
}
|
||
|
|
||
|
if css := shadowCSS(style, TextShadow, session); css != "" {
|
||
|
builder.add("text-shadow", css)
|
||
|
}
|
||
|
|
||
|
if value, ok := style.properties[ColumnSeparator]; ok {
|
||
|
if separator, ok := value.(ColumnSeparatorProperty); ok {
|
||
|
if css := separator.cssValue(session); css != "" {
|
||
|
builder.add("column-rule", css)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if avoid, ok := boolProperty(style, AvoidBreak, session); ok {
|
||
|
if avoid {
|
||
|
builder.add("break-inside", "avoid")
|
||
|
} else {
|
||
|
builder.add("break-inside", "auto")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
wrap, _ := enumProperty(style, Wrap, session, 0)
|
||
|
orientation, ok := getOrientation(style, session)
|
||
|
if ok || wrap > 0 {
|
||
|
cssText := enumProperties[Orientation].cssValues[orientation]
|
||
|
switch wrap {
|
||
|
case WrapOn:
|
||
|
cssText += " wrap"
|
||
|
|
||
|
case WrapReverse:
|
||
|
cssText += " wrap-reverse"
|
||
|
}
|
||
|
builder.add(`flex-flow`, cssText)
|
||
|
}
|
||
|
|
||
|
rows := (orientation == StartToEndOrientation || orientation == EndToStartOrientation)
|
||
|
|
||
|
var hAlignTag, vAlignTag string
|
||
|
if rows {
|
||
|
hAlignTag = `justify-content`
|
||
|
vAlignTag = `align-items`
|
||
|
} else {
|
||
|
hAlignTag = `align-items`
|
||
|
vAlignTag = `justify-content`
|
||
|
}
|
||
|
|
||
|
if align, ok := enumProperty(style, HorizontalAlign, session, LeftAlign); ok {
|
||
|
switch align {
|
||
|
case LeftAlign:
|
||
|
if (!rows && wrap == WrapReverse) || orientation == EndToStartOrientation {
|
||
|
builder.add(hAlignTag, `flex-end`)
|
||
|
} else {
|
||
|
builder.add(hAlignTag, `flex-start`)
|
||
|
}
|
||
|
case RightAlign:
|
||
|
if (!rows && wrap == WrapReverse) || orientation == EndToStartOrientation {
|
||
|
builder.add(hAlignTag, `flex-start`)
|
||
|
} else {
|
||
|
builder.add(hAlignTag, `flex-end`)
|
||
|
}
|
||
|
case CenterAlign:
|
||
|
builder.add(hAlignTag, `center`)
|
||
|
|
||
|
case StretchAlign:
|
||
|
if rows {
|
||
|
builder.add(hAlignTag, `space-between`)
|
||
|
} else {
|
||
|
builder.add(hAlignTag, `stretch`)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if align, ok := enumProperty(style, VerticalAlign, session, LeftAlign); ok {
|
||
|
switch align {
|
||
|
case TopAlign:
|
||
|
if (rows && wrap == WrapReverse) || orientation == BottomUpOrientation {
|
||
|
builder.add(vAlignTag, `flex-end`)
|
||
|
} else {
|
||
|
builder.add(vAlignTag, `flex-start`)
|
||
|
}
|
||
|
case BottomAlign:
|
||
|
if (rows && wrap == WrapReverse) || orientation == BottomUpOrientation {
|
||
|
builder.add(vAlignTag, `flex-start`)
|
||
|
} else {
|
||
|
builder.add(vAlignTag, `flex-end`)
|
||
|
}
|
||
|
case CenterAlign:
|
||
|
builder.add(vAlignTag, `center`)
|
||
|
|
||
|
case StretchAlign:
|
||
|
if rows {
|
||
|
builder.add(hAlignTag, `stretch`)
|
||
|
} else {
|
||
|
builder.add(hAlignTag, `space-between`)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if r, ok := rangeProperty(style, Row, session); ok {
|
||
|
builder.add("grid-row-start", strconv.Itoa(r.First+1))
|
||
|
builder.add("grid-row-end", strconv.Itoa(r.Last+2))
|
||
|
}
|
||
|
if r, ok := rangeProperty(style, Column, session); ok {
|
||
|
builder.add("grid-column-start", strconv.Itoa(r.First+1))
|
||
|
builder.add("grid-column-end", strconv.Itoa(r.Last+2))
|
||
|
}
|
||
|
if text := style.gridCellSizesCSS(CellWidth, session); text != "" {
|
||
|
builder.add(`grid-template-columns`, text)
|
||
|
}
|
||
|
if text := style.gridCellSizesCSS(CellHeight, session); text != "" {
|
||
|
builder.add(`grid-template-rows`, text)
|
||
|
}
|
||
|
|
||
|
style.writeViewTransformCSS(builder, session)
|
||
|
|
||
|
if clip := getClipShape(style, Clip, session); clip != nil && clip.valid(session) {
|
||
|
builder.add(`clip-path`, clip.cssStyle(session))
|
||
|
}
|
||
|
|
||
|
if clip := getClipShape(style, ShapeOutside, session); clip != nil && clip.valid(session) {
|
||
|
builder.add(`shape-outside`, clip.cssStyle(session))
|
||
|
}
|
||
|
|
||
|
if value := style.getRaw(Filter); value != nil {
|
||
|
if filter, ok := value.(ViewFilter); ok {
|
||
|
if text := filter.cssStyle(session); text != "" {
|
||
|
builder.add(`filter`, text)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
/*
|
||
|
if len(style.transitions) > 0 {
|
||
|
buffer := allocStringBuilder()
|
||
|
defer freeStringBuilder(buffer)
|
||
|
|
||
|
for property, transition := range style.transitions {
|
||
|
if buffer.Len() > 0 {
|
||
|
buffer.WriteString(`, `)
|
||
|
}
|
||
|
buffer.WriteString(property)
|
||
|
transition.cssWrite(buffer, session)
|
||
|
}
|
||
|
|
||
|
if buffer.Len() > 0 {
|
||
|
builder.add(`transition`, buffer.String())
|
||
|
}
|
||
|
}
|
||
|
*/
|
||
|
|
||
|
// TODO text-shadow
|
||
|
}
|