cg/src/cg/03-ui-components.cg

467 lines
14 KiB
Text

empty = ui.rect { w = 0, h = 0 };
centerH = parentW child \
childW = (ui.measure child).width;
ui.positioned { x = (parentW - childW) / 2, y = 0, child = child };
centerV = parentH child \
childH = (ui.measure child).height;
ui.positioned { y = (parentH - childH) / 2, x = 0, child = child };
center = parentW parentH child \
childSize = ui.measure child;
ui.positioned { x = (parentW - childSize.width) / 2, y = (parentH - childSize.height) / 2, child = child };
# button : Record -> ui
button = config \
label = renderText { content = config.label };
labelSize = ui.measure label;
labelWidth = labelSize.width;
defaults = {
h = 30,
w = labelWidth + 20,
strokeWidth = 1,
strokeColor = "#fff",
textColor = "#fff",
label = ""
};
c = { ...defaults, ...config };
ui.clickable {
onClick = config.event,
child = ui.stack {
children = [
ui.rect { w = c.w, h = c.h, strokeColor = c.strokeColor, strokeWidth = c.strokeWidth },
ui.positioned {
x = 10, y = 8,
child = renderText { content = config.label, color = c.textColor }
}
]
}
};
# inputButton (button that turns into an input)
inputButton = config \
label = renderText { content = config.label };
labelSize = ui.measure label;
labelWidth = labelSize.width;
defaults = {
h = 30,
w = labelWidth + 16,
strokeWidth = 1,
strokeColor = "#fff",
textColor = "#fff",
backgroundColor = "transparent",
label = ""
};
c = { ...defaults, ...config };
ui.stateful {
key = c.key,
focusable = True,
init = { editing = False },
update = state event \ event
| Activate \ { state = state.{ editing = True }, emit = [] }
| Submit text \ { state = state.{ editing = False }, emit = [c.onSubmit text] }
| Key { key = "Escape" } \ { state = state.{ editing = False }, emit = [] }
| _ \ { state = state, emit = [] },
view = state emit \
state.editing
| True \ textInput {
key = c.key & "-input",
autoFocus = True,
w = c.w, h = c.h,
strokeWidth = 1,
strokeColor = "#fff",
color = c.textColor,
backgroundColor = c.backgroundColor,
onSubmit = text \ emit (Submit text)
}
| False \ ui.clickable {
onClick = \ emit Activate,
child = ui.stack {
children = [
ui.rect { w = c.w, h = c.h, strokeColor = c.strokeColor, strokeWidth = c.strokeWidth },
ui.positioned {
x = 8, y = 4,
child = label
}
]
}
}
};
# scrollable
scrollable = config \
defaults = {
scrollX = 0,
scrollY = 0,
onScroll = _ \ noOp
};
c = { ...defaults, ...config };
showVBar = c.totalHeight > c.h;
vBarHeight = max 20 (c.h * c.h / c.totalHeight);
vBarY = c.scrollY * c.h / c.totalHeight;
showHBar = c.totalWidth > c.w;
hBarWidth = max 20 (c.w * c.w / c.totalWidth);
hBarX = c.scrollX * c.w / c.totalWidth;
ui.stack {
children = [
ui.scrollable {
w = c.w,
h = c.h,
scrollX = c.scrollX,
scrollY = c.scrollY,
onScroll = c.onScroll,
child = c.child
},
...(showVBar
| True \ [ui.positioned {
x = c.w - 4,
y = vBarY,
child = ui.rect { w = 4, h = vBarHeight, color = "rgba(255,255,255,0.3)", radius = 2 }
}]
| False \ []),
...(showHBar
| True \ [ui.positioned {
x = hBarX,
y = c.h - 4,
child = ui.rect { h = 4, w = hBarWidth, color = "rgba(255,255,255,0.3)", radius = 2 }
}]
| False \ [])
]
};
box = config \
defaults = {
w = 100,
h = 100,
color = "white",
borderTop = 0,
borderBottom = 0,
borderLeft = 0,
borderRight = 0,
borderColor = "transparent",
paddingTop = 0,
paddingLeft = 0
};
c = { ...defaults, ...config };
ui.stack {
children = [
# background
ui.rect { w = c.w, h = c.h, color = c.color },
# top border
ui.rect { w = c.w, h = c.borderTop, color = c.borderColor },
# bottom border
ui.positioned { x = 0, y = c.h - c.borderBottom, child =
ui.rect { w = c.w, h = c.borderBottom, color = c.borderColor }
},
# left border
ui.rect { w = c.borderLeft, h = c.h, color = c.borderColor },
# right border
ui.positioned { x = c.w - c.borderRight, y = 0, child =
ui.rect { w = c.borderRight, h = c.h, color = c.borderColor }
},
# content
ui.positioned { x = c.paddingLeft, y = c.paddingTop, child = c.child }
]
};
textInput = config \
defaults = {
onSubmit = _ \ noOp,
onChange = _ \ noOp,
initialValue = "",
autoFocus = False,
strokeWidth = 0,
strokeColor = "transparent"
};
c = { ...defaults, ...config };
insertChar = text pos char \
before = slice text 0 pos;
after = slice text pos (len text);
before & char & after;
deleteChar = text pos \
(pos == 0
| True \ text
| False \
(before = slice text 0 (pos - 1);
after = slice text pos (len text);
before & after));
calcScrollOffset = text cursorPos scrollOffset inputWidth \
textBeforeCursor = slice text 0 cursorPos;
# cursorX = ui.measureText textBeforeCursor;
cursorX = (measureText { content = textBeforeCursor, scale = 2 }).width;
(cursorX < scrollOffset
| True \ max 0 (cursorX - 20)
| False \
(cursorX > (scrollOffset + inputWidth)
| True \ cursorX - inputWidth + 20
| False \ scrollOffset));
findPosHelper = text targetX index \
(index >= len text)
| True \ len text
| False \ (
# widthSoFar = ui.measureText (slice text 0 index);
widthSoFar = (measureText { content = (slice text 0 index), scale = 2 }).width;
# widthNext = ui.measureText (slice text 0 (index + 1));
widthNext = (measureText { content = (slice text 0 (index + 1)), scale = 2 }).width;
midpoint = (widthSoFar + widthNext) / 2;
(targetX < midpoint
| True \ index
| False \ findPosHelper text targetX (index + 1))
);
findCursorPos = text clickX scrollOffset inputPadding \
adjustedX = clickX + scrollOffset - inputPadding;
findPosHelper text adjustedX 0;
ui.stateful {
key = c.key,
focusable = True,
autoFocus = c.autoFocus,
# init : State
init = {
text = c.initialValue,
focused = c.autoFocus,
cursorPos = len c.initialValue,
scrollOffset = 0
},
# update : State \ Event \ State
update = state event \
s = (hasField "value" c
| True \ (c.value == state.text
| True \ state
| False \ state.{ text = c.value, cursorPos = len c.value, scrollOffset = 0 })
| False \ state);
event
| Key { key = key, printable = True, meta = False, ctrl = False } \ (
newText = insertChar s.text s.cursorPos key;
newCursorPos = s.cursorPos + 1;
newScroll = calcScrollOffset newText newCursorPos s.scrollOffset c.w;
newState = s.{ text = newText, cursorPos = newCursorPos, scrollOffset = newScroll };
{ state = newState, emit = [c.onChange newText] })
| Key { key = "ArrowLeft" } \ (
newCursorPos = max 0 (s.cursorPos - 1);
newScroll = calcScrollOffset s.text newCursorPos s.scrollOffset c.w;
newState = s.{ text = s.text, cursorPos = newCursorPos, scrollOffset = newScroll };
{ state = newState, emit = [] })
| Key { key = "ArrowRight" } \ (
newCursorPos = min (len s.text) (s.cursorPos + 1);
newScroll = calcScrollOffset s.text newCursorPos s.scrollOffset c.w;
newState = s.{ text = s.text, cursorPos = newCursorPos, scrollOffset = newScroll };
{ state = newState, emit = [] })
| Key { key = "Backspace" } \ (
newText = deleteChar s.text s.cursorPos;
newCursorPos = max 0 (s.cursorPos - 1);
newScroll = calcScrollOffset newText newCursorPos s.scrollOffset c.w;
newState = s.{ text = newText, cursorPos = newCursorPos, scrollOffset = newScroll };
{ state = newState, emit = [c.onChange newText] })
| Key { key = "Enter" } \ { state = s, emit = [c.onSubmit s.text] }
| Clicked coords \ (
newCursorPos = findCursorPos s.text coords.x s.scrollOffset 8;
newScroll = calcScrollOffset s.text newCursorPos s.scrollOffset c.w;
newState = s.{ text = s.text, cursorPos = newCursorPos, scrollOffset = newScroll };
{ state = newState, emit = [] })
| Focused \ { state = state.{ focused = True }, emit = [] }
| Blurred \ { state = state.{ focused = False }, emit = [] }
| SetText newText \ (
newState = s.{ text = newText, cursorPos = len newText, scrollOffset = 0 };
{ state = newState, emit = [c.onChange newText] })
| _ \ { state = s, emit = [] },
view = state emit \
text = (hasField "value" c | True \ c.value | False \ state.text);
cursorPos = (text == state.text | True \ state.cursorPos | False \ len text);
textBeforeCursor = slice text 0 cursorPos;
# cursorX = ui.measureText textBeforeCursor;
cursorX = (measureText { content = textBeforeCursor, scale = 2 }).width;
padding = 8;
ui.clip {
w = c.w,
h = c.h,
child = ui.stack {
children = [
ui.rect { w = c.w, h = c.h, color = c.backgroundColor, radius = 0, strokeWidth = c.strokeWidth, strokeColor = c.strokeColor },
ui.positioned {
x = 8 - state.scrollOffset,
y = 0,
# child = ui.positioned { x = 0, y = 12, child = ui.text { content = text, color = c.color } }
child = ui.positioned { x = 0, y = 12, child = renderText { content = text, color = c.color } }
},
(state.focused
| True \ ui.positioned {
x = 8 + cursorX - state.scrollOffset,
y = 8,
child = ui.rect { w = 2, h = 24, color = c.color }
}
| _ \ empty)
]
}
}
};
glyphView = config \
defaults = { scale = 1, color = "#fff" };
c = { ...defaults, ...config };
ui.stack {
children = [
ui.rect { w = c.glyph.w * c.scale, h = c.glyph.h * c.scale, color = "transparent" },
...map (pixel \
ui.positioned {
x = pixel.x * c.scale,
y = pixel.y * c.scale,
child = ui.rect { w = c.scale, h = c.scale, color = c.color }
}
) c.glyph.map
]
};
# renderText : TextConfig \ UI
renderText = config \
defaults = { content = "", scale = 2, color = "#fff", font = "myFont2" };
c = { ...defaults, ...config };
font = getAt [c.font] | Some f \ f | None \ { glyphs = {} };
charW = font.charWidth;
gap = c.scale;
chars = split "" c.content;
chars == []
| True \ ui.rect { w = 0, h = 12 * c.scale, color = "transparent" }
| False \ ui.row {
children = map (char \
getField char font.glyphs
| Some g \ glyphView { glyph = g, scale = c.scale, color = c.color }
| None \ ui.rect { w = charW * c.scale, h = 12 * c.scale }
) chars,
gap = gap
};
# text : String \ UI
text = content \ renderText { content = content };
measureText = config \
defaults = { content = "", scale = 2, font = "myFont2" };
c = { ...defaults, ...config };
font = getAt [c.font] | Some f \ f | None \ { charWidth = 5 };
charW = font.charWidth;
gap = c.scale;
strLen = len config.content;
{
width = strLen * charW * c.scale + max 0 (strLen - 1) * gap,
height = 12 * c.scale
};
sizedText = content \
size = measureText { content = content };
{ ui = renderText { content = content }, w = size.width, h = size.height };
# new CG layout
LayoutChild = Fixed Int (Int \ Int \ UI) | Flex Int (Int \ Int \ UI);
Alignment = Start | Center | End;
Direction = Col | Row;
# @layout
#
# layoutDir
# : Direction \ { w : Int, h : Int } \ List LayoutChild \ { ui : UI, w : Int, h : Int }
# = dir constraints children \
# mainSize = dir | Col \ constraints.h | Row \ constraints.w;
# crossSize = dir | Col \ constraints.w | Row \ constraints.h;
#
# # Pass 1: sum fixed heights, total flex weight
# info = fold (acc child \ child
# | Fixed s _ \ acc.{ fixedMain = acc.fixedMain + s }
# | Flex n _ \ acc.{ totalFlex = acc.totalFlex + n }
# ) { fixedMain = 0, totalFlex = 0 } children;
#
# remaining = mainSize - info.fixedMain;
#
# # Pass 2
# result = fold (acc child \
# allocated = (child
# | Fixed s view \ { s = s, view = view }
# | Flex n view \ { s = remaining * n / info.totalFlex, view = view });
#
# childW = dir | Col \ crossSize | Row \ allocated.s;
# childH = dir | Col \ allocated.s | Row \ crossSize;
# posX = dir | Col \ 0 | Row \ acc.pos;
# posY = dir | Col \ acc.pos | Row \ 0;
#
# rendered = ui.positioned {
# x = posX, y = posY,
# child = allocated.view childW childH
# };
#
# acc.{ pos = acc.pos + allocated.s, children = [...acc.children, rendered] }
# ) { pos = 0, children = [] } children;
#
# { ui = ui.stack { children = result.children }, w = constraints.w, h = constraints.h };
#
# col = layoutDir Col;
# row = layoutDir Row;
#
# align = hAlign vAlign w h child \
# x = hAlign
# | Start \ 0
# | Center \ (w - child.w) / 2
# | End \ w - child.w;
# y = vAlign
# | Start \ 0
# | Center \ (h - child.h) / 2
# | End \ h - child.h;
# { ui = ui.positioned { x = x, y = y, child = child.ui }, w = w, h = h };
#
# @
#
# testApp = name \
# { view = ctx \
# col { w = ctx.w, h = ctx.h } [
# Fixed 40 (w h \ box { w = w, h = h, color = "red", child = text "Header" }),
# Flex 1 (w h \ box { w = w, h = h, color = "#1a1a2e", child = text "Main content" }),
# Fixed 30 (w h \ box { w = w, h = h, color = "blue", child = text "Footer" }),
# ]
# ~ (e \ e.ui)
# };
#
# testApp2 = _ \
# { view = ctx \
# col { w = ctx.w, h = ctx.h } [
# Fixed 40 (w h \ (align Center Center w h (sizedText "Header")).ui),
# Flex 1 (w h \ (row { w = w, h = h } [
# Fixed 200 (w h \ box { w = w, h = h, color = "#2a2a3e", child = text "Sidebar" }),
# Flex 1 (w h \ box { w = w, h = h, color = "#1a1a2e", child = text "Main" }),
# ]).ui),
# Fixed 30 (w h \ (align Center Center w h (sizedText "Footer")).ui),
# ]
# ~ (e \ e.ui)
# };