diff options
Diffstat (limited to 'src/nigui.nim')
| -rwxr-xr-x | src/nigui.nim | 2254 |
1 files changed, 2254 insertions, 0 deletions
diff --git a/src/nigui.nim b/src/nigui.nim new file mode 100755 index 0000000..4ecdebd --- /dev/null +++ b/src/nigui.nim @@ -0,0 +1,2254 @@ +# NiGui - main file + +# This file contains all common code except extra widgets. +# All public procedures are declared here. +# Platform-specific code will added by "include". + +# Templates for "include": +template useWindows(): bool = defined(windows) and not defined(forceGtk) +template useGtk(): bool = not useWindows() + +# ======================================================================================== +# +# Public Declaration +# +# ======================================================================================== + +# ---------------------------------------------------------------------------------------- +# Simple Types +# ---------------------------------------------------------------------------------------- + +type + Layout* = enum + Layout_Horizontal + Layout_Vertical + + XAlign* = enum + XAlign_Left + XAlign_Right + XAlign_Center + XAlign_Spread + + YAlign* = enum + YAlign_Top + YAlign_Bottom + YAlign_Center + YAlign_Spread + + WidthMode* = enum + WidthMode_Static + WidthMode_Auto + WidthMode_Fill + WidthMode_Expand + + HeightMode* = enum + HeightMode_Static + HeightMode_Auto + HeightMode_Fill + HeightMode_Expand + + MouseButton* = enum + MouseButton_Left + MouseButton_Middle + MouseButton_Right + + Color* = object + red*: byte + green*: byte + blue*: byte + alpha*: byte + + Spacing* = object + left*: int + right*: int + top*: int + bottom*: int + + Timer* = distinct int + + Key* = enum + Key_None + Key_Number0 + Key_Number1 + Key_Number2 + Key_Number3 + Key_Number4 + Key_Number5 + Key_Number6 + Key_Number7 + Key_Number8 + Key_Number9 + Key_A + Key_B + Key_C + Key_D + Key_E + Key_F + Key_G + Key_H + Key_I + Key_J + Key_K + Key_L + Key_M + Key_N + Key_O + Key_P + Key_Q + Key_R + Key_S + Key_T + Key_U + Key_V + Key_W + Key_X + Key_Y + Key_Z + Key_Space + Key_Tab + Key_Return + Key_Escape + Key_Insert + Key_Delete + Key_Backspace + Key_Left + Key_Right + Key_Up + Key_Down + Key_Home + Key_End + Key_PageUp + Key_PageDown + +const inactiveTimer* = 0 + + +# ---------------------------------------------------------------------------------------- +# Widget Types 1/3 +# ---------------------------------------------------------------------------------------- + +type + # Window base type: + + Window* = ref object of RootObj + fDisposed: bool + fTitle: string + fVisible: bool + fWidth, fHeight: int + fClientWidth, fClientHeight: int + fX, fY: int + fControl: Control + fIconPath: string + fOnDispose: WindowDisposeProc + fOnResize: ResizeProc + fOnDropFiles: DropFilesProc + fOnKeyDown: WindowKeyProc + + # Control base type: + + Control* = ref object of RootObj + fDisposed: bool + fParentControl: Control # is nil or object of ContainerImpl + fParentWindow: Window # only set for top level widget + fIndex: int + fVisible: bool + fWidth, fHeight: int + fX, fY: int + fWidthMode: WidthMode + fHeightMode: HeightMode + fMinWidth, fMinHeight: int + fMaxWidth, fMaxHeight: int + fXScrollEnabled, fYScrollEnabled: bool + fXScrollPos, fYScrollPos: int + fScrollableWidth, fScrollableHeight: int + fFontFamily: string + fFontSize: int + fTextColor: Color + fBackgroundColor: Color + fUseDefaultFontFamily: bool + fUseDefaultFontSize: bool + fUseDefaultTextColor: bool + fUseDefaultBackgroundColor: bool + fCanvas: Canvas + fOnDispose: ControlDisposeProc + fOnDraw: DrawProc + fOnMouseButtonDown: MouseButtonProc + fOnMouseButtonUp: MouseButtonProc + fOnClick: ClickProc + # fOnMouseMove: MouseMoveProc + fOnKeyDown: ControlKeyProc + fOnTextChange: TextChangeProc + tag*: string + + # Drawing: + + Canvas* = ref object of RootObj + fWidth: int + fHeight: int + fFontFamily: string + fFontSize: int + fTextColor: Color + fLineColor: Color + fAreaColor: Color + + Image* = ref object of RootObj + fCanvas: Canvas + + # Window events: + + WindowDisposeEvent* = ref object + window*: Window + cancel*: bool + WindowDisposeProc* = proc(event: WindowDisposeEvent) + + ResizeEvent* = ref object + window*: Window + ResizeProc* = proc(event: ResizeEvent) + + DropFilesEvent* = ref object + window*: Window + files*: seq[string] + DropFilesProc* = proc(event: DropFilesEvent) + + WindowKeyEvent* = ref object + window*: Window + key*: Key + unicode*: int + character*: string # UTF-8 character + WindowKeyProc* = proc(event: WindowKeyEvent) + + # Control events: + + ControlDisposeEvent* = ref object + control*: Control + cancel*: bool + ControlDisposeProc* = proc(event: ControlDisposeEvent) + + DrawEvent* = ref object + control*: Control + DrawProc* = proc(event: DrawEvent) + + MouseButtonEvent* = ref object + control*: Control + button*: MouseButton + x*: int + y*: int + MouseButtonProc* = proc(event: MouseButtonEvent) + + ClickEvent* = ref object + control*: Control + ClickProc* = proc(event: ClickEvent) + + ControlKeyEvent* = ref object + control*: Control + key*: Key + unicode*: int + character*: string # UTF-8 character + cancel*: bool + ControlKeyProc* = proc(event: ControlKeyEvent) + + TextChangeEvent* = ref object + control*: Control + TextChangeProc* = proc(event: TextChangeEvent) + + # Other events: + + ErrorHandlerProc* = proc() + + TimerEvent* = ref object + timer*: Timer + data*: pointer + TimerProc* = proc(event: TimerEvent) + +# Platform-specific extension of Window and Control: +when useWindows(): include "nigui/private/windows/platform_types1" +when useGtk(): include "nigui/private/gtk3/platform_types1" + + +# ---------------------------------------------------------------------------------------- +# Widget Types 2/3 +# ---------------------------------------------------------------------------------------- + +type + # Basic controls: + + Container* = ref object of ControlImpl + fFrame: Frame + fChildControls: seq[Control] + + Frame* = ref object of ControlImpl + fText: string + + Button* = ref object of ControlImpl + fText: string + + Label* = ref object of ControlImpl + fText: string + + TextBox* = ref object of ControlImpl + + TextArea* = ref object of ControlImpl + fWrap: bool + + +# Platform-specific extension of basic controls: +when useWindows(): include "nigui/private/windows/platform_types2" +when useGtk(): include "nigui/private/gtk3/platform_types2" + + +# ---------------------------------------------------------------------------------------- +# Widget Types 3/3 +# ---------------------------------------------------------------------------------------- + +type + LayoutContainer* = ref object of ContainerImpl + fLayout: Layout + fXAlign: XAlign + fYAlign: YAlign + fPadding: int + fSpacing: int + + +# ---------------------------------------------------------------------------------------- +# Global Variables +# ---------------------------------------------------------------------------------------- + +var quitOnLastWindowClose* = true +var clickMaxXYMove* = 20 + +# dummy type and object, needed to use get/set properties +type App = object +var app*: App + + +# ---------------------------------------------------------------------------------------- +# Global/App Procedures +# ---------------------------------------------------------------------------------------- + +proc init*(app: App) + +proc run*(app: App) + +proc quit*(app: App) + +proc processEvents*(app: App) + +proc sleep*(app: App, milliSeconds: float) + +proc errorHandler*(app: App): ErrorHandlerProc +proc `errorHandler=`*(app: App, errorHandler: ErrorHandlerProc) + +proc defaultBackgroundColor*(app: App): Color +proc `defaultBackgroundColor=`*(app: App, color: Color) + +proc defaultTextColor*(app: App): Color +proc `defaultTextColor=`*(app: App, color: Color) + +proc defaultFontFamily*(app: App): string +proc `defaultFontFamily=`*(app: App, fontFamily: string) + +proc defaultFontSize*(app: App): int +proc `defaultFontSize=`*(app: App, fontSize: int) + +proc rgb*(red, green, blue: byte, alpha: byte = 255): Color + + +# ---------------------------------------------------------------------------------------- +# Dialogs +# ---------------------------------------------------------------------------------------- + +proc alert*(window: Window, message: string, title = "Message") {.discardable.} + +type OpenFileDialog* = ref object + title*: string + directory*: string + multiple*: bool + files*: seq[string] + +proc newOpenFileDialog*(): OpenFileDialog + +method run*(dialog: OpenFileDialog) + +type SaveFileDialog* = ref object + title*: string + directory*: string + defaultExtension*: string + defaultName*: string + file*: string + +proc newSaveFileDialog*(): SaveFileDialog + +method run*(dialog: SaveFileDialog) + + +# ---------------------------------------------------------------------------------------- +# Timers +# ---------------------------------------------------------------------------------------- + +proc startTimer*(milliSeconds: int, timerProc: TimerProc, data: pointer = nil): Timer {.discardable.} + +proc startRepeatingTimer*(milliSeconds: int, timerProc: TimerProc, data: pointer = nil): Timer {.discardable.} + +proc stop*(timer: var Timer) + + +# ---------------------------------------------------------------------------------------- +# Canvas +# ---------------------------------------------------------------------------------------- + +proc newCanvas*(control: Control = nil): CanvasImpl + +method destroy*(canvas: Canvas) + +method width*(canvas: Canvas): int + +method height*(canvas: Canvas): int + +method fontFamily*(canvas: Canvas): string +method `fontFamily=`*(canvas: Canvas, fontFamily: string) + +method fontSize*(canvas: Canvas): int +method `fontSize=`*(canvas: Canvas, fontSize: int) + +method textColor*(canvas: Canvas): Color +method `textColor=`*(canvas: Canvas, color: Color) + +method lineColor*(canvas: Canvas): Color +method `lineColor=`*(canvas: Canvas, color: Color) + +method areaColor*(canvas: Canvas): Color +method `areaColor=`*(canvas: Canvas, color: Color) + +method drawText*(canvas: Canvas, text: string, x, y = 0) + +method drawTextCentered*(canvas: Canvas, text: string, x, y = 0, width, height = -1) + +method drawLine*(canvas: Canvas, x1, y1, x2, y2: int) + +method drawRectArea*(canvas: Canvas, x, y, width, height: int) + +method drawRectOutline*(canvas: Canvas, x, y, width, height: int) + +method fill*(canvas: Canvas) + +method drawImage*(canvas: Canvas, image: Image, x, y = 0, width, height = -1) + +method setPixel*(canvas: Canvas, x, y: int, color: Color) + +method getTextLineWidth*(canvas: Canvas, text: string): int + +method getTextLineHeight*(canvas: Canvas): int + +method getTextWidth*(canvas: Canvas, text: string): int + + +# ---------------------------------------------------------------------------------------- +# Image +# ---------------------------------------------------------------------------------------- + +proc newImage*(): Image + +method resize*(image: Image, width, height: int) + +method loadFromFile*(image: Image, filePath: string) + +method saveToPngFile*(image: Image, filePath: string) + +method saveToJpegFile*(image: Image, filePath: string, quality = 80) + +method width*(image: Image): int + +method height*(image: Image): int + +method canvas*(image: Image): Canvas + + +# ---------------------------------------------------------------------------------------- +# Window +# ---------------------------------------------------------------------------------------- + +proc newWindow*(title: string = nil): Window +## Constructor for a Window object. +## If the title is nil, it will be set to the application filename. + +proc init*(window: WindowImpl) +## Initialize a WindowImpl object +## Only needed for own constructors. + +proc dispose*(window: var Window) +proc dispose*(window: Window) + +proc disposed*(window: Window): bool + +method visible*(window: Window): bool +method `visible=`*(window: Window, visible: bool) + +method show*(window: Window) + +method showModal*(window: Window, parent: Window) + +method hide*(window: Window) + +method control*(window: Window): Control +method `control=`*(window: Window, control: Control) + +method add*(window: Window, control: Control) + +method title*(window: Window): string +method `title=`*(window: Window, title: string) + +method x*(window: Window): int +method `x=`*(window: Window, x: int) + +method y*(window: Window): int +method `y=`*(window: Window, y: int) + +method centerOnScreen*(window: Window) + +method width*(window: Window): int +method `width=`*(window: Window, width: int) + +method height*(window: Window): int +method `height=`*(window: Window, height: int) + +method clientWidth*(window: Window): int + +method clientHeight*(window: Window): int + +method iconPath*(window: Window): string +method `iconPath=`*(window: Window, iconPath: string) + +method handleDisposeEvent*(window: Window, event: WindowDisposeEvent) + +method handleResizeEvent*(window: Window, event: ResizeEvent) + +method handleKeyDownEvent*(window: Window, event: WindowKeyEvent) + +method handleDropFilesEvent*(window: Window, event: DropFilesEvent) + +method onDispose*(window: Window): WindowDisposeProc +method `onDispose=`*(window: Window, callback: WindowDisposeProc) + +method onResize*(window: Window): ResizeProc +method `onResize=`*(window: Window, callback: ResizeProc) + +method onDropFiles*(window: Window): DropFilesProc +method `onDropFiles=`*(window: Window, callback: DropFilesProc) + +method onKeyDown*(window: Window): WindowKeyProc +method `onKeyDown=`*(window: Window, callback: WindowKeyProc) + + +# ---------------------------------------------------------------------------------------- +# Control +# ---------------------------------------------------------------------------------------- + +proc newControl*(): Control + +proc init*(control: Control) +proc init*(control: ControlImpl) + +proc dispose*(control: var Control) +proc dispose*(control: Control) + +proc disposed*(control: Control): bool + +method visible*(control: Control): bool +method `visible=`*(control: Control, visible: bool) +method show*(control: Control) +method hide*(control: Control) + +# Allow the outside to walk over child widgets +method childControls*(control: Control): seq[Control] + +method parentControl*(control: Control): Control + +method parentWindow*(control: Control): WindowImpl + +method width*(control: Control): int +# Set the control's width to a fixed value (sets widthMode to fixed) +method `width=`*(control: Control, width: int) + +method height*(control: Control): int +# Set the control's height to a fixed value (sets heightMode to fixed) +method `height=`*(control: Control, height: int) + +method minWidth*(control: Control): int +method `minWidth=`*(control: Control, minWidth: int) + +method minHeight*(control: Control): int +method `minHeight=`*(control: Control, minHeight: int) + +method maxWidth*(control: Control): int +method `maxWidth=`*(control: Control, maxWidth: int) + +method maxHeight*(control: Control): int +method `maxHeight=`*(control: Control, maxHeight: int) + +# Set the control's width and height without changing widthMode or heightMode +method setSize*(control: Control, width, height: int) + +method x*(control: Control): int +method `x=`*(control: Control, x: int) + +method y*(control: Control): int +method `y=`*(control: Control, y: int) + +method setPosition*(control: Control, x, y: int) + +method naturalWidth*(control: Control): int + +method naturalHeight*(control: Control): int + +method wantedWidth*(control: Control): int + +method wantedHeight*(control: Control): int + +method focus*(control: Control) + +method getTextLineWidth*(control: Control, text: string): int + +method getTextLineHeight*(control: Control): int + +method getTextWidth*(control: Control, text: string): int + +method `widthMode=`*(control: Control, mode: WidthMode) +method widthMode*(control: Control): WidthMode + +method heightMode*(control: Control): HeightMode +method `heightMode=`*(control: Control, mode: HeightMode) + +method visibleWidth*(control: Control): int + +method visibleHeight*(control: Control): int + +method xScrollPos*(control: Control): int +method `xScrollPos=`*(control: Control, xScrollPos: int) + +method yScrollPos*(control: Control): int +method `yScrollPos=`*(control: Control, yScrollPos: int) + +method scrollableWidth*(control: Control): int +method `scrollableWidth=`*(control: Control, scrollableWidth: int) + +method scrollableHeight*(control: Control): int +method `scrollableHeight=`*(control: Control, scrollableHeight: int) + +method fontFamily*(control: Control): string +method `fontFamily=`*(control: Control, fontFamily: string) +method setFontFamily*(control: Control, fontFamily: string) +method resetFontFamily*(control: Control) + +method fontSize*(control: Control): int +method `fontSize=`*(control: Control, fontSize: int) +method setFontSize*(control: Control, fontSize: int) +method resetFontSize*(control: Control) + +method backgroundColor*(control: Control): Color +method `backgroundColor=`*(control: Control, color: Color) +method setBackgroundColor*(control: Control, color: Color) +method resetBackgroundColor*(control: Control) + +method textColor*(control: Control): Color +method `textColor=`*(control: Control, color: Color) +method setTextColor*(control: Control, color: Color) +method resetTextColor*(control: Control) + +method forceRedraw*(control: Control) + +method canvas*(control: Control): Canvas + +method handleDisposeEvent*(control: Control, event: ControlDisposeEvent) + +method handleDrawEvent*(control: Control, event: DrawEvent) + +method handleMouseButtonDownEvent*(control: Control, event: MouseButtonEvent) + +method handleMouseButtonUpEvent*(control: Control, event: MouseButtonEvent) + +method handleClickEvent*(control: Control, event: ClickEvent) + +method handleKeyDownEvent*(control: Control, event: ControlKeyEvent) + +method handleTextChangeEvent*(control: Control, event: TextChangeEvent) + +method onDispose*(control: Control): ControlDisposeProc +method `onDispose=`*(control: Control, callback: ControlDisposeProc) + +method onDraw*(control: Control): DrawProc +method `onDraw=`*(control: Control, callback: DrawProc) + +method onMouseButtonDown*(control: Control): MouseButtonProc +method `onMouseButtonDown=`*(control: Control, callback: MouseButtonProc) + +method onMouseButtonUp*(control: Control): MouseButtonProc +method `onMouseButtonUp=`*(control: Control, callback: MouseButtonProc) + +method onClick*(control: Control): ClickProc +method `onClick=`*(control: Control, callback: ClickProc) + +method onKeyDown*(control: Control): ControlKeyProc +method `onKeyDown=`*(control: Control, callback: ControlKeyProc) + +method onTextChange*(control: Control): TextChangeProc +method `onTextChange=`*(control: Control, callback: TextChangeProc) + + +# ---------------------------------------------------------------------------------------- +# Container +# ---------------------------------------------------------------------------------------- + +proc newContainer*(): Container + +proc init*(container: Container) +proc init*(container: ContainerImpl) + +method frame*(container: Container): Frame +method `frame=`*(container: Container, frame: Frame) + +method add*(container: Container, control: Control) +method remove*(container: Container, control: Control) + +method getPadding*(container: Container): Spacing + +method setInnerSize*(container: Container, width, height: int) + + +# ---------------------------------------------------------------------------------------- +# LayoutContainer +# ---------------------------------------------------------------------------------------- + +proc newLayoutContainer*(layout: Layout): LayoutContainer + +method layout*(container: LayoutContainer): Layout +method `layout=`*(container: LayoutContainer, layout: Layout) + +method xAlign*(container: LayoutContainer): XAlign +method `xAlign=`*(container: LayoutContainer, xAlign: XAlign) + +method yAlign*(container: LayoutContainer): YAlign +method `yAlign=`*(container: LayoutContainer, yAlign: YAlign) + +method padding*(container: LayoutContainer): int +method `padding=`*(container: LayoutContainer, padding: int) + +method spacing*(container: LayoutContainer): int +method `spacing=`*(container: LayoutContainer, spacing: int) + + +# ---------------------------------------------------------------------------------------- +# Frame +# ---------------------------------------------------------------------------------------- + +proc newFrame*(text = ""): Frame + +proc init*(frame: Frame) +proc init*(frame: NativeFrame) + +method text*(frame: Frame): string +method `text=`*(frame: Frame, text: string) + +method getPadding*(frame: Frame): Spacing + + +# ---------------------------------------------------------------------------------------- +# Button +# ---------------------------------------------------------------------------------------- + +proc newButton*(text = ""): Button + +proc init*(button: Button) +proc init*(button: NativeButton) + +method text*(button: Button): string +method `text=`*(button: Button, text: string) + + +# ---------------------------------------------------------------------------------------- +# Label +# ---------------------------------------------------------------------------------------- + +proc newLabel*(text = ""): Label + +proc init*(label: Label) +proc init*(label: NativeLabel) + +method text*(label: Label): string +method `text=`*(label: Label, text: string) + + +# ---------------------------------------------------------------------------------------- +# TextBox +# ---------------------------------------------------------------------------------------- + +proc newTextBox*(text = ""): TextBox + +proc init*(textBox: TextBox) +proc init*(textBox: NativeTextBox) + +method text*(textBox: TextBox): string +method `text=`*(textBox: TextBox, text: string) + + +# ---------------------------------------------------------------------------------------- +# TextArea +# ---------------------------------------------------------------------------------------- + +proc newTextArea*(text = ""): TextArea + +proc init*(textArea: TextArea) +proc init*(textArea: NativeTextArea) + +method text*(textArea: TextArea): string +method `text=`*(textArea: TextArea, text: string) +method addText*(textArea: TextArea, text: string) +method addLine*(textArea: TextArea, text = "") + +method scrollToBottom*(textArea: TextArea) + +method wrap*(textArea: TextArea): bool +method `wrap=`*(textArea: TextArea, wrap: bool) + + +# ---------------------------------------------------------------------------------------- +# Private Procedures Predeclaration +# ---------------------------------------------------------------------------------------- + +proc raiseError(msg: string, showAlert = true, title = "NiGui Error") + +proc handleException() + +proc runMainLoop() + +proc init(window: Window) + +method destroy(window: Window) + +proc triggerRelayout(window: Window) + +method destroy(control: Control) + +proc triggerRelayout(control: Control) + +proc triggerRelayoutIfModeIsAuto(control: Control) + +method relayout(control: Control, availableWidth, availableHeight: int) + +method realignChildControls(control: Control) + +method setControlPosition(container: Container, control: Control, x, y: int) + +proc countLines(s: string): int + + +# ======================================================================================== +# +# Implementation +# +# ======================================================================================== + +import math +import os +import strutils +import times + + +# ---------------------------------------------------------------------------------------- +# Global Variables +# ---------------------------------------------------------------------------------------- + +var fErrorHandler: ErrorHandlerProc = nil +var windowList: seq[Window] = @[] +var fScrollbarSize = -1 + +# Default style: +var fDefaultBackgroundColor: Color # initialized by platform-specific init() +var fDefaultTextColor: Color # initialized by platform-specific init() +var fDefaultFontFamily = "" +var fDefaultFontSize = 15 + + +# ---------------------------------------------------------------------------------------- +# Global/App Procedures +# ---------------------------------------------------------------------------------------- + +proc raiseError(msg: string, showAlert = true, title = "NiGui Error") = + if showAlert: + alert(nil, msg & "\n\n" & getStackTrace(), title) + raise newException(Exception, msg) + +proc handleException() = + if fErrorHandler == nil: + raiseError(getCurrentExceptionMsg(), true, "Unhandled Exception") + else: + fErrorHandler() + +proc rgb(red, green, blue: byte, alpha: byte = 255): Color = + result.red = red + result.green = green + result.blue = blue + result.alpha = alpha + +proc countLines(s: string): int = strutils.countLines(s) + 1 + +proc sleep(app: App, milliSeconds: float) = + let t = epochTime() + milliSeconds / 1000 + while epochTime() < t: + app.processEvents() + os.sleep(20) + +proc run(app: App) = + while true: + try: + runMainLoop() + break + except: + handleException() + +proc quit(app: App) = quit() + +proc errorHandler(app: App): ErrorHandlerProc = fErrorHandler + +proc `errorHandler=`(app: App, errorHandler: ErrorHandlerProc) = fErrorHandler = errorHandler + +proc defaultBackgroundColor(app: App): Color = fDefaultBackgroundColor + +proc updateBackgroundColor(control: Control) = + if control.fUseDefaultBackgroundColor and control.backgroundColor != fDefaultBackgroundColor: + control.setBackgroundColor(fDefaultBackgroundColor) + for child in control.childControls: + child.updateBackgroundColor() + +proc `defaultBackgroundColor=`(app: App, color: Color) = + fDefaultBackgroundColor = color + for window in windowList: + let control = window.control + if control != nil: + control.updateBackgroundColor() + +proc defaultTextColor(app: App): Color = fDefaultTextColor + +proc updateTextColor(control: Control) = + if control.fUseDefaultTextColor and control.textColor != fDefaultTextColor: + control.setTextColor(fDefaultTextColor) + for child in control.childControls: + child.updateTextColor() + +proc `defaultTextColor=`(app: App, color: Color) = + fDefaultTextColor = color + for window in windowList: + let control = window.control + if control != nil: + control.updateTextColor() + +proc defaultFontFamily(app: App): string = fDefaultFontFamily + +proc updateFontFamily(control: Control) = + if control.fUseDefaultFontFamily and control.fontFamily != fDefaultFontFamily: + control.setFontFamily(fDefaultFontFamily) + for child in control.childControls: + child.updateFontFamily() + +proc `defaultFontFamily=`(app: App, fontFamily: string) = + fDefaultFontFamily = fontFamily + for window in windowList: + let control = window.control + if control != nil: + control.updateFontFamily() + +proc defaultFontSize(app: App): int = fDefaultFontSize + +proc updateFontSize(control: Control) = + if control.fUseDefaultFontSize and control.fontSize != fDefaultFontSize: + control.setFontSize(fDefaultFontSize) + for child in control.childControls: + child.updateFontSize() + +proc `defaultFontSize=`(app: App, fontSize: int) = + fDefaultFontSize = fontSize + for window in windowList: + let control = window.control + if control != nil: + control.updateFontSize() + +proc newOpenFileDialog(): OpenFileDialog = + result = new OpenFileDialog + result.title = "Open File" + result.directory = getCurrentDir() + result.files = @[] + +proc newSaveFileDialog(): SaveFileDialog = + result = new SaveFileDialog + result.title = "Save File" + result.directory = getCurrentDir() + result.defaultExtension = "" + result.defaultName = "" + result.file = "" + + +# ---------------------------------------------------------------------------------------- +# Canvas +# ---------------------------------------------------------------------------------------- + +proc newCanvas(control: Control = nil): CanvasImpl = + result = new CanvasImpl + result.fLineColor = rgb(0, 0, 0) + result.fAreaColor = rgb(0, 0, 0) + if control == nil: + result.fFontFamily = app.defaultFontFamily + result.fFontSize = app.defaultFontSize + result.fTextColor = app.defaultTextColor + else: + result.fFontFamily = control.fontFamily + result.fFontSize = control.fontSize + result.fTextColor = control.textColor + result.fWidth = control.width + result.fHeight = control.height + control.fCanvas = result + +method destroy(canvas: Canvas) = discard + +method width(canvas: Canvas): int = canvas.fWidth + +method height(canvas: Canvas): int= canvas.fHeight + +method fontFamily(canvas: Canvas): string = canvas.fFontFamily + +method `fontFamily=`(canvas: Canvas, fontFamily: string) = canvas.fFontFamily = fontFamily + +method fontSize(canvas: Canvas): int = canvas.fFontSize + +method `fontSize=`(canvas: Canvas, fontSize: int) = canvas.fFontSize = fontSize + +method textColor(canvas: Canvas): Color = canvas.fTextColor + +method `textColor=`(canvas: Canvas, color: Color) = canvas.fTextColor = color + +method lineColor(canvas: Canvas): Color = canvas.fLineColor + +method `lineColor=`(canvas: Canvas, color: Color) = canvas.fLineColor = color + +method areaColor(canvas: Canvas): Color = canvas.fAreaColor + +method `areaColor=`(canvas: Canvas, color: Color) = canvas.fAreaColor = color + +method getTextLineWidth(canvas: Canvas, text: string): int = + result = text.len * 7 + # should be overrriden by CanvasImpl + +method getTextLineHeight(canvas: Canvas): int = + result = 20 + # should be overrriden by CanvasImpl + +method getTextWidth(canvas: Canvas, text: string): int = + result = 0 + for line in text.splitLines: + result = max(result, canvas.getTextLineWidth(line)) + +method drawTextCentered(canvas: Canvas, text: string, x, y = 0, width, height = -1) = + var w = width + if w == -1: + w = canvas.width + var h = height + if h == -1: + h = canvas.height + let rx = x + (w - canvas.getTextWidth(text)) div 2 + let ry = y + (h - canvas.getTextLineHeight()) div 2 + canvas.drawText(text, rx, ry) + +method fill(canvas: Canvas) = canvas.drawRectArea(0, 0, canvas.width, canvas.height) + + +# ---------------------------------------------------------------------------------------- +# Image +# ---------------------------------------------------------------------------------------- + +proc newImage(): Image = + result = new ImageImpl + result.fCanvas = newCanvas() + +method width(image: Image): int = image.canvas.width + +method height(image: Image): int = image.canvas.height + +method canvas(image: Image): Canvas = image.fCanvas + + +# ---------------------------------------------------------------------------------------- +# Window +# ---------------------------------------------------------------------------------------- + +proc newWindow(title: string = nil): Window = + result = new WindowImpl + result.WindowImpl.init() + if title != nil: + result.title = title + + +proc init(window: Window) = + window.fVisible = false + window.fWidth = 640 # do not trigger resize + window.height = 480 # trigger resize + window.fX = -1 # window will be centered on screen + window.fY = -1 + window.title = getAppFilename().extractFilename().changeFileExt("") + var defaultIconPath = getAppFilename().changeFileExt("") & ".png" + if defaultIconPath.fileExists(): + window.iconPath = defaultIconPath + windowList.add(window) + window.triggerRelayout() + + +method destroy(window: Window) = + if window.fControl != nil: + window.fControl.destroy() + # should be extended by WindowImpl + +proc disposeInner(window: Window): bool = + var event = new WindowDisposeEvent + event.window = window + window.handleDisposeEvent(event) + if event.cancel: + return false + window.destroy() + let i = windowList.find(window) + windowList.delete(i) + if quitOnLastWindowClose and windowList.len == 0: + quit() + window.fDisposed = true + return true + +proc dispose(window: var Window) = + if window.disposeInner(): + window = nil + +proc dispose(window: Window) = + discard window.disposeInner() + +proc disposed(window: Window): bool = window == nil or window.fDisposed + +method title(window: Window): string = window.fTitle + +method `title=`(window: Window, title: string) = window.fTitle = title + +method control(window: Window): Control = window.fControl + +method `control=`(window: Window, control: Control) = + window.fControl = control + control.fParentWindow = window + # should be extended by WindowImpl + +method add(window: Window, control: Control) = + if window.control != nil: + raiseError("Window can have only one control.") + window.control = control + +method visible(window: Window): bool = window.fVisible + +method `visible=`(window: Window, visible: bool) = + window.fVisible = visible + if window.x == -1 or window.y == -1: + window.centerOnScreen() + +method show(window: Window) = window.visible = true + +method showModal(window: Window, parent: Window) = + window.visible = true + # should be extended by WindowImpl + +method hide(window: Window) = window.visible = false + +method x(window: Window): int = window.fX + +method `x=`(window: Window, x: int) = + window.fX = x + # should be extended by WindowImpl + +method y(window: Window): int = window.fY + +method `y=`(window: Window, y: int) = + window.fY = y + # should be extended by WindowImpl + +method centerOnScreen(window: Window) = + discard # has to be implemented in WindowImpl + +method width(window: Window): int = window.fWidth + +method height(window: Window): int = window.fHeight + +method `width=`(window: Window, width: int) = + window.fWidth = width + window.triggerRelayout() + var event = new ResizeEvent + event.window = window + window.handleResizeEvent(event) + +method `height=`(window: Window, height: int) = + window.fHeight = height + window.triggerRelayout() + var event = new ResizeEvent + event.window = window + window.handleResizeEvent(event) + +method clientWidth(window: Window): int = window.fClientWidth + +method clientHeight(window: Window): int = window.fClientHeight + +proc triggerRelayout(window: Window) = + if window.control == nil: + return + # echo "" + # echo "WindowImpl:triggerRelayout()" + # echo "window size: " & $window.clientWidth & ", " & $window.clientHeight + window.control.relayout(window.clientWidth, window.clientHeight) + +method iconPath(window: Window): string = window.fIconPath + +method `iconPath=`(window: Window, iconPath: string) = + window.fIconPath = iconPath + # should be extended by WindowImpl + +method handleDisposeEvent(window: Window, event: WindowDisposeEvent) = + # can be overriden by custom window + let callback = window.onDispose + if callback != nil: + callback(event) + +method handleResizeEvent(window: Window, event: ResizeEvent) = + # can be overriden by custom window + let callback = window.onResize + if callback != nil: + callback(event) + +method handleDropFilesEvent(window: Window, event: DropFilesEvent) = + # can be overriden by custom window + let callback = window.onDropFiles + if callback != nil: + callback(event) + +method handleKeyDownEvent(window: Window, event: WindowKeyEvent) = + # can be overriden by custom window + let callback = window.onKeyDown + if callback != nil: + callback(event) + +method onDispose(window: Window): WindowDisposeProc = window.fOnDispose +method `onDispose=`(window: Window, callback: WindowDisposeProc) = window.fOnDispose = callback + +method onResize(window: Window): ResizeProc = window.fOnResize +method `onResize=`(window: Window, callback: ResizeProc) = window.fOnResize = callback + +method onDropFiles(window: Window): DropFilesProc = window.fOnDropFiles +method `onDropFiles=`(window: Window, callback: DropFilesProc) = window.fOnDropFiles = callback + +method onKeyDown(window: Window): WindowKeyProc = window.fOnKeyDown +method `onKeyDown=`(window: Window, callback: WindowKeyProc) = window.fOnKeyDown = callback + + + +# ---------------------------------------------------------------------------------------- +# Control +# ---------------------------------------------------------------------------------------- + +proc newControl(): Control = + result = new ControlImpl + result.ControlImpl.init() + +proc init(control: Control) = + control.tag = "" + control.fWidthMode = WidthMode_Expand + control.fHeightMode = HeightMode_Expand + control.fScrollableWidth = -1 + control.fScrollableHeight = -1 + control.resetFontFamily() + control.resetFontSize() + control.resetTextColor() + control.resetBackgroundColor() + control.show() + # should be extended by WindowImpl + +method destroy(control: Control) = + discard # nothing to do here + # should be extended by WindowImpl + +proc dispose(control: var Control) = + control.destroy() + control = nil + +proc dispose(control: Control) = + control.destroy() + control.fDisposed = true + +proc disposed(control: Control): bool = control == nil or control.fDisposed + +method visible(control: Control): bool = control.fVisible + +method `visible=`(control: Control, visible: bool) = + control.fVisible = visible + control.triggerRelayout() + # should be extended by WindowImpl + +method show(control: Control) = control.visible = true + +method hide(control: Control) = control.visible = false + +method width(control: Control): int = control.fWidth + +method height(control: Control): int = control.fHeight + +method `width=`(control: Control, width: int) = + control.setSize(width, control.fHeight) + control.widthMode = WidthMode_Static + +method `height=`(control: Control, height: int) = + control.setSize(control.fWidth, height) + control.heightMode = HeightMode_Static + +method minWidth(control: Control): int = control.fMinWidth + +method `minWidth=`(control: Control, minWidth: int) = + control.fMinWidth = minWidth + control.triggerRelayout() + +method minHeight(control: Control): int = control.fMinHeight + +method `minHeight=`(control: Control, minHeight: int) = + control.fMinHeight = minHeight + control.triggerRelayout() + +method maxWidth(control: Control): int = control.fMaxWidth + +method `maxWidth=`(control: Control, maxWidth: int) = + control.fMaxWidth = maxWidth + control.triggerRelayout() + +method maxHeight(control: Control): int = control.fMaxHeight + +method `maxHeight=`(control: Control, maxHeight: int) = + control.fMaxHeight = maxHeight + control.triggerRelayout() + +method setSize(control: Control, width, height: int) = + control.fWidth = width + control.fHeight = height + if control.canvas != nil: + control.canvas.fWidth = width + control.canvas.fHeight = height + control.realignChildControls() + # should be extended by ControlImpl + +method setSize(control: ControlImpl, width, height: int) # required pre declaration + +method `x=`(control: Control, x: int) = + if control.fParentControl == nil: + raiseError("Control cannot be moved, when it is not inside a container.") + cast[Container](control.fParentControl).setControlPosition(control, x, control.y) + +method x(control: Control): int = control.fX + +method `y=`(control: Control, y: int) = + if control.fParentControl == nil: + raiseError("Control cannot be moved, when it is not inside a container.") + cast[Container](control.fParentControl).setControlPosition(control, control.x, y) + +method y(control: Control): int = control.fY + +method setPosition(control: Control, x, y: int) = + control.fX = x + control.fY = y + # should be extended by ControlImpl + +method naturalWidth(control: Control): int = control.width + +method naturalHeight(control: Control): int = control.height + +method wantedWidth(control: Control): int = + if control.widthMode == WidthMode_Static: + result = control.width + else: + result = control.naturalWidth + if result != -1 and control.minWidth > result: + result = control.minWidth + if control.maxWidth > 0 and control.maxWidth < result: + result = control.maxWidth + +method wantedHeight(control: Control): int = + if control.heightMode == HeightMode_Static: + result = control.height + else: + result = control.naturalHeight + if result != -1 and control.minHeight > result: + result = control.minHeight + if control.maxHeight > 0 and control.maxHeight < result: + result = control.maxHeight + +method `widthMode=`(control: Control, mode: WidthMode) = + control.fWidthMode = mode + control.triggerRelayout() + +method `heightMode=`(control: Control, mode: HeightMode) = + control.fHeightMode = mode + control.triggerRelayout() + +method widthMode(control: Control): WidthMode = control.fWidthMode + +method heightMode(control: Control): HeightMode = control.fHeightMode + +method childControls(control: Control): seq[Control] = @[] + +method parentControl(control: Control): Control = control.fParentControl + +method parentWindow(control: Control): WindowImpl = + if control.fParentControl == nil: + result = cast[WindowImpl](control.fParentWindow) + else: + result = control.parentControl.parentWindow + +proc triggerRelayout(control: Control) = + var con = control + while con.parentControl != nil: + con = con.parentControl + if con.parentWindow != nil: + con.parentWindow.triggerRelayout() + if control.parentControl != nil: + control.parentControl.realignChildControls() + control.realignChildControls() + +proc triggerRelayoutIfModeIsAuto(control: Control) = + if control.widthMode == WidthMode_Auto or control.heightMode == HeightMode_Auto: + control.triggerRelayout() + +method relayout(control: Control, availableWidth, availableHeight: int) = + # echo "" + # echo control.tag & ".relayout(): " + # echo " available size: " & $availableWidth & ", " & $availableHeight + var width = control.width + var height = control.height + var sizeChanged = false + if control.widthMode == WidthMode_Auto: + let naturalWidth = control.naturalWidth + # echo " naturalWidth: " & $naturalWidth + if naturalWidth == -1: + width = availableWidth + else: + width = min(availableWidth, naturalWidth) + sizeChanged = true + elif control.widthMode in {WidthMode_Expand, WidthMode_Fill}: + width = availableWidth + sizeChanged = true + if control.heightMode == HeightMode_Auto: + let naturalHeight = control.naturalHeight + # echo " naturalHeight: " & $naturalHeight + if naturalHeight == -1: + height = availableHeight + else: + height = min(availableHeight, naturalHeight) + sizeChanged = true + elif control.heightMode in {HeightMode_Expand, HeightMode_Fill}: + height = availableHeight + sizeChanged = true + if sizeChanged: + # echo " new size: " & $width & ", " & $height + control.setSize(width, height) + +method realignChildControls(control: Control) = discard + +method focus(control: Control) = + discard + # should be overrriden by ControlImpl + +method getTextLineWidth(control: Control, text: string): int = + result = text.len * 7 + # should be overrriden by ControlImpl + +method getTextLineHeight(control: Control): int = + result = 20 + # should be overrriden by ControlImpl + +method getTextWidth(control: Control, text: string): int = + result = 0 + for line in text.splitLines: + result = max(result, control.getTextLineWidth(line)) + +method visibleWidth(control: Control): int = + result = control.width + if control.fXScrollEnabled: + result.dec(fScrollbarSize) + +method visibleHeight(control: Control): int = + result = control.height + if control.fYScrollEnabled: + result.dec(fScrollbarSize) + +method xScrollPos(control: Control): int = control.fXScrollPos + +method `xScrollPos=`(control: Control, xScrollPos: int) = + control.fXScrollPos = xScrollPos + +method yScrollPos(control: Control): int = control.fYScrollPos + +method `yScrollPos=`(control: Control, yScrollPos: int) = + control.fYScrollPos = yScrollPos + +method scrollableWidth(control: Control): int = control.fScrollableWidth + +method `scrollableWidth=`(control: Control, scrollableWidth: int) = + control.fScrollableWidth = scrollableWidth + +method scrollableHeight(control: Control): int = control.fScrollableHeight + +method `scrollableHeight=`(control: Control, scrollableHeight: int) = + control.fScrollableHeight = scrollableHeight + +method fontFamily(control: Control): string = control.fFontFamily + +method `fontFamily=`(control: Control, fontFamily: string) = + control.setFontFamily(fontFamily) + control.fUseDefaultFontFamily = false + +method setFontFamily(control: Control, fontFamily: string) = + control.fFontFamily = fontFamily + control.triggerRelayoutIfModeIsAuto() + # should be extended by ControlImpl + +method resetFontFamily(control: Control) = + control.setFontFamily(fDefaultFontFamily) + control.fUseDefaultFontFamily = true + +method fontSize(control: Control): int = control.fFontSize + +method `fontSize=`(control: Control, fontSize: int) = + control.setFontSize(fontSize) + control.fUseDefaultFontSize = false + +method setFontSize(control: Control, fontSize: int) = + control.fFontSize = fontSize + control.triggerRelayoutIfModeIsAuto() + # should be extended by ControlImpl + +method resetFontSize(control: Control) = + control.setFontSize(fDefaultFontSize) + control.fUseDefaultFontSize = true + +method backgroundColor(control: Control): Color = control.fBackgroundColor + +method `backgroundColor=`(control: Control, color: Color) = + control.setBackgroundColor(color) + control.fUseDefaultBackgroundColor = false + +method setBackgroundColor(control: Control, color: Color) = + control.fBackgroundColor = color + control.forceRedraw() + # should be extended by ControlImpl + +method resetBackgroundColor(control: Control) = + control.setBackgroundColor(fDefaultBackgroundColor) + control.fUseDefaultBackgroundColor = true + +method textColor(control: Control): Color = control.fTextColor + +method `textColor=`(control: Control, color: Color) = + control.setTextColor(color) + control.fUseDefaultTextColor = false + +method setTextColor(control: Control, color: Color) = + control.fTextColor = color + control.forceRedraw() + # should be extended by ControlImpl + +method resetTextColor*(control: Control) = + control.setTextColor(fDefaultTextColor) + control.fUseDefaultTextColor = true + +method forceRedraw(control: Control) = + discard + # should be implemented by ControlImpl + +method canvas(control: Control): Canvas = control.fCanvas + +method handleDisposeEvent(control: Control, event: ControlDisposeEvent) = + # can be overriden by custom window + let callback = control.onDispose + if callback != nil: + callback(event) + +method handleDrawEvent(control: Control, event: DrawEvent) = + # can be implemented by custom control + let callback = control.onDraw + if callback != nil: + callback(event) + +method handleMouseButtonDownEvent(control: Control, event: MouseButtonEvent) = + # can be implemented by custom control + let callback = control.onMouseButtonDown + if callback != nil: + callback(event) + +method handleMouseButtonUpEvent(control: Control, event: MouseButtonEvent) = + # can be implemented by custom control + let callback = control.onMouseButtonUp + if callback != nil: + callback(event) + +method handleClickEvent(control: Control, event: ClickEvent) = + # can be overridden by custom button + let callback = control.onClick + if callback != nil: + callback(event) + +method handleKeyDownEvent(control: Control, event: ControlKeyEvent) = + # can be implemented by custom control + let callback = control.onKeyDown + if callback != nil: + callback(event) + +method handleTextChangeEvent(control: Control, event: TextChangeEvent) = + # can be implemented by custom control + let callback = control.onTextChange + if callback != nil: + callback(event) + +method onDispose(control: Control): ControlDisposeProc = control.fOnDispose +method `onDispose=`(control: Control, callback: ControlDisposeProc) = control.fOnDispose = callback + +method onDraw(control: Control): DrawProc = control.fOnDraw +method `onDraw=`(control: Control, callback: DrawProc) = control.fOnDraw = callback + +method onMouseButtonDown(control: Control): MouseButtonProc = control.fOnMouseButtonDown +method `onMouseButtonDown=`(control: Control, callback: MouseButtonProc) = control.fOnMouseButtonDown = callback + +method onMouseButtonUp(control: Control): MouseButtonProc = control.fOnMouseButtonUp +method `onMouseButtonUp=`(control: Control, callback: MouseButtonProc) = control.fOnMouseButtonUp = callback + +method onClick(control: Control): ClickProc = control.fOnClick +method `onClick=`(control: Control, callback: ClickProc) = control.fOnClick = callback + +method onKeyDown(control: Control): ControlKeyProc = control.fOnKeyDown +method `onKeyDown=`(control: Control, callback: ControlKeyProc) = control.fOnKeyDown = callback + +method onTextChange(control: Control): TextChangeProc = control.fOnTextChange +method `onTextChange=`(control: Control, callback: TextChangeProc) = control.fOnTextChange = callback + + +# ---------------------------------------------------------------------------------------- +# Container +# ---------------------------------------------------------------------------------------- + +proc newContainer(): Container = + result = new ContainerImpl + result.ContainerImpl.init() + +proc init(container: Container) = + container.fChildControls = @[] + container.ControlImpl.init() + container.fWidthMode = WidthMode_Auto + container.fHeightMode = HeightMode_Auto + +method frame(container: Container): Frame = container.fFrame + +method `frame=`(container: Container, frame: Frame) = + if frame.fParentControl != nil: + raiseError("Frame can be assigned only to one container.") + container.fFrame = frame + if frame != nil: + frame.fParentControl = container + container.tag = frame.tag + container.triggerRelayout() + if container.frame != nil: + container.frame.setSize(container.width, container.height) + # should be extended by NativeFrame + +method setSize(container: Container, width, height: int) = + procCall container.ControlImpl.setSize(width, height) + if container.frame != nil: + container.frame.setSize(width, height) + +method childControls(container: Container): seq[Control] = container.fChildControls + +method add(container: Container, control: Control) = + if control.fParentControl != nil: + raiseError("Control can be added only to one container.") + container.fChildControls.add(control) + control.fParentControl = container + control.fIndex = 0 + container.triggerRelayout() + +method remove(container: Container, control: Control) = + discard + # if container != control.fParentControl: + # raiseError("control can not be removed because it is not member of the container") + # else: + # let startIndex = control.fIndex + # container.childControls.del(control.fIndex) + # for i in startIndex..container.childControls.high: + # container.childControl[i].fIndex = i + # control.parentControl = nil + +method setControlPosition(container: Container, control: Control, x, y: int) = + control.setPosition(x, y) + container.triggerRelayout() + +method minWidth(container: Container): int = + result = 0 + for control in container.childControls: + if not control.visible: + continue + result = max(result, control.x + control.minWidth) + let padding = container.getPadding() + result.inc(padding.left) + result.inc(padding.right) + result = max(result, container.fMinWidth) + +method minHeight(container: Container): int = + result = 0 + for control in container.childControls: + if not control.visible: + continue + result = max(result, control.y + control.minHeight) + let padding = container.getPadding() + result.inc(padding.top) + result.inc(padding.bottom) + result = max(result, container.fMinHeight) + +method naturalWidth(container: Container): int = + if container.widthMode == WidthMode_Static: + return container.width + if container.widthMode == WidthMode_Expand: + return -1 + result = 0 + for control in container.childControls: + if not control.visible: + continue + if control.widthMode == WidthMode_Expand: + return -1 + result = max(result, control.x + control.wantedWidth) + let padding = container.getPadding() + result.inc(padding.left) + result.inc(padding.right) + if container.frame != nil and container.frame.visible: + result = max(result, container.frame.naturalWidth) + +method naturalHeight(container: Container): int = + if container.heightMode == HeightMode_Static: + return container.height + if container.heightMode == HeightMode_Expand: + return -1 + result = 0 + for control in container.childControls: + if not control.visible: + continue + if control.heightMode == HeightMode_Expand: + return -1 + result = max(result, control.y + control.wantedHeight) + let padding = container.getPadding() + result.inc(padding.top) + result.inc(padding.bottom) + +method getPadding(container: Container): Spacing = + if container.frame != nil and container.frame.visible: + result = container.frame.getPadding() + +method setInnerSize(container: Container, width, height: int) = discard + # should be extended by ContainerImpl + +method updateInnerSize(container: Container, pInnerWidth, pInnerHeight: int) {.base.} = + let padding = container.getPadding() + let clientWidth = container.width - padding.left - padding.right + let clientHeight = container.height - padding.top - padding.bottom + var innerWidth = pInnerWidth + var innerHeight = pInnerHeight + + discard """ container.xScrollEnabled = innerWidth > clientWidth + + if container.xScrollEnabled and innerHeight + 20 > clientHeight: + innerHeight.inc(20) # Space for scrollbar + container.yScrollEnabled = true + else: + container.yScrollEnabled = innerHeight > clientHeight + if container.yScrollEnabled and innerWidth + 20 > clientWidth: + innerWidth.inc(20) # Space for scrollbar + container.xScrollEnabled = true """ + + # TODO: rework + + innerWidth = max(innerWidth, clientWidth) + innerHeight = max(innerHeight, clientHeight) + + container.scrollableWidth = innerWidth + container.scrollableHeight = innerHeight + container.setInnerSize(innerWidth, innerHeight) + +method realignChildControls(container: Container) = + let padding = container.getPadding() + var innerWidth = container.wantedWidth + var innerHeight = container.wantedHeight + if innerWidth == -1: + innerWidth = container.width + if innerHeight == -1: + innerHeight = container.height + container.updateInnerSize(innerWidth - padding.left - padding.right, innerHeight - padding.top - padding.bottom) + +method `onDraw=`(container: ContainerImpl, callback: DrawProc) = raiseError("ContainerImpl does not allow onDraw.") + + +# ---------------------------------------------------------------------------------------- +# LayoutContainer +# ---------------------------------------------------------------------------------------- + +proc newLayoutContainer(layout: Layout): LayoutContainer = + result = new LayoutContainer + result.init() + result.layout = layout + result.xAlign = XAlign_Left + result.yAlign = YAlign_Top + result.spacing = 4 + result.padding = 2 + +method naturalWidth(container: LayoutContainer): int = + # echo container.tag & ".naturalWidth" + if container.widthMode == WidthMode_Static: + return container.width + if container.widthMode == WidthMode_Expand: + return -1 + result = 0 + for control in container.childControls: + if not control.visible: + continue + if control.widthMode == WidthMode_Expand or control.wantedWidth == -1: + return -1 + if container.layout == Layout_Horizontal: + result.inc(control.wantedWidth) + else: + result = max(result, control.wantedWidth) + let padding = container.getPadding() + result.inc(padding.left) + result.inc(padding.right) + result.inc(container.padding * 2) + if container.layout == Layout_Horizontal and container.childControls.len > 1: + result.inc(container.spacing * (container.childControls.len - 1)) + if container.frame != nil and container.frame.visible: + result = max(result, container.frame.naturalWidth) + +method naturalHeight(container: LayoutContainer): int = + if container.heightMode == HeightMode_Static: + return container.height + if container.heightMode == HeightMode_Expand: + return -1 + result = 0 + for control in container.childControls: + if not control.visible: + continue + if control.heightMode == HeightMode_Expand or control.wantedHeight == -1: + return -1 + if container.layout == Layout_Vertical: + result.inc(control.wantedHeight) + else: + result = max(result, control.wantedHeight) + let padding = container.getPadding() + result.inc(padding.top) + result.inc(padding.bottom) + result.inc(container.padding * 2) + if container.layout == Layout_Vertical and container.childControls.len > 1: + result.inc(container.spacing * (container.childControls.len - 1)) + +method minWidth(container: LayoutContainer): int = + result = 0 + for control in container.childControls: + if not control.visible: + continue + if container.layout == Layout_Horizontal: + result.inc(control.minWidth) + else: + result = max(result, control.minWidth) + let padding = container.getPadding() + result.inc(padding.left) + result.inc(padding.right) + result.inc(container.padding * 2) + if container.layout == Layout_Horizontal and container.childControls.len > 1: + result.inc(container.spacing * (container.childControls.len - 1)) + result = max(result, container.fMinWidth) + +method minHeight(container: LayoutContainer): int = + result = 0 + for control in container.childControls: + if not control.visible: + continue + if container.layout == Layout_Vertical: + result.inc(control.minHeight) + else: + result = max(result, control.minHeight) + let padding = container.getPadding() + result.inc(padding.top) + result.inc(padding.bottom) + result.inc(container.padding * 2) + if container.layout == Layout_Vertical and container.childControls.len > 1: + result.inc(container.spacing * (container.childControls.len - 1)) + result = max(result, container.fMinHeight) + +method setControlPosition(container: LayoutContainer, control: Control, x, y: int) = + raiseError("Controls inside a LayoutContainer cannot be moved manually.") + +method layout(container: LayoutContainer): Layout = container.fLayout + +method `layout=`(container: LayoutContainer, layout: Layout) = + container.fLayout = layout + container.triggerRelayout() + +method xAlign(container: LayoutContainer): XAlign = container.fXAlign + +method `xAlign=`(container: LayoutContainer, xAlign: XAlign) = + container.fXAlign = xAlign + container.realignChildControls() + +method yAlign(container: LayoutContainer): YAlign = container.fYAlign + +method `yAlign=`(container: LayoutContainer, yAlign: YAlign) = + container.fYAlign = yAlign + container.realignChildControls() + +method padding(container: LayoutContainer): int = container.fPadding + +method `padding=`(container: LayoutContainer, padding: int) = + container.fPadding = padding + container.triggerRelayout() + +method spacing(container: LayoutContainer): int = container.fSpacing + +method `spacing=`(container: LayoutContainer, spacing: int) = + container.fSpacing = spacing + container.triggerRelayout() + +method realignChildControls(container: LayoutContainer) = + # echo "" + # echo container.tag & ".realignChildControls()" + if container.fChildControls.len == 0: + return + # echo " container size: " & $container.width & ", " & $container.height + let padding = container.getPadding() + let clientWidth = container.width - padding.left - padding.right + let clientHeight = container.height - padding.top - padding.bottom + # echo " client size: " & $clientWidth & ", " & $clientHeight + var minInnerWidth = 0 + var minInnerHeight = 0 + var expandWidthCount = 0 + var expandHeightCount = 0 + + # Calculate minimum needed size: + for control in container.fChildControls: + if not control.visible: + continue + + if control.widthMode == WidthMode_Expand: + if container.layout == Layout_Horizontal: + minInnerWidth.inc(control.minWidth) + expandWidthCount.inc + + elif control.widthMode in {WidthMode_Auto , WidthMode_Static}: + if container.layout == Layout_Horizontal: + if control.wantedWidth == -1: + minInnerWidth.inc(control.minWidth) + expandWidthCount.inc + else: + minInnerWidth.inc(control.wantedWidth) + else: + if control.wantedWidth == -1: + expandWidthCount.inc + else: + minInnerWidth = max(minInnerWidth, control.wantedWidth) + + if control.heightMode == HeightMode_Expand: + if container.layout == Layout_Vertical: + minInnerHeight.inc(control.minHeight) + expandHeightCount.inc + + elif control.heightMode in {HeightMode_Auto, HeightMode_Static}: + if container.layout == Layout_Vertical: + if control.wantedHeight == -1: + minInnerHeight.inc(control.minHeight) + expandHeightCount.inc + else: + minInnerHeight.inc(control.wantedHeight) + else: + if control.wantedHeight == -1: + expandHeightCount.inc + else: + minInnerHeight = max(minInnerHeight, control.wantedHeight) + + # Add padding: + minInnerWidth.inc(container.padding * 2) + minInnerHeight.inc(container.padding * 2) + + # Add spacing: + if container.childControls.len > 1: + if container.layout == Layout_Horizontal: + minInnerWidth.inc(container.spacing * (container.childControls.len - 1)) + if container.layout == Layout_Vertical: + minInnerHeight.inc(container.spacing * (container.childControls.len - 1)) + + container.updateInnerSize(minInnerWidth, minInnerHeight) + + let innerWidth = max(minInnerWidth, clientWidth) + let innerHeight = max(minInnerHeight, clientHeight) + + # Calculate dynamic size: + var dynamicWidth = clientWidth - minInnerWidth + var dynamicHeight = clientHeight - minInnerHeight + dynamicWidth = max(dynamicWidth, 0) + dynamicHeight = max(dynamicHeight, 0) + + # Move and resize controls: + var x = container.padding + var y = container.padding + + if (container.xAlign == XAlign_Center or (container.xAlign == XAlign_Spread and container.childControls.len == 1)) and (container.layout == Layout_Vertical or expandWidthCount == 0): + x.inc(dynamicWidth div 2) + if (container.yAlign == YAlign_Center or (container.yAlign == YAlign_Spread and container.childControls.len == 1)) and (container.layout == Layout_Horizontal or expandHeightCount == 0): + y.inc(dynamicHeight div 2) + + for control in container.fChildControls: + if not control.visible: + continue + # echo " child: " & control.tag + + # Size: + var width = control.width + var height = control.height + # echo "size old: " & $width & ", " $height + if control.widthMode == WidthMode_Expand or control.wantedWidth == -1: + if container.layout == Layout_Horizontal: + if expandWidthCount > 0: + width = control.minWidth + dynamicWidth div expandWidthCount + else: + width = control.minWidth + dynamicWidth + else: + width = clientWidth - container.padding * 2 + elif control.widthMode == WidthMode_Auto: + width = control.wantedWidth + elif control.widthMode == WidthMode_Fill: + width = clientWidth - container.padding * 2 + + if control.minWidth > width: + width = control.minWidth + if control.maxWidth > 0 and control.maxWidth < width: + width = control.maxWidth + + if control.heightMode == HeightMode_Expand or control.wantedHeight == -1: + if container.layout == Layout_Vertical: + if expandHeightCount > 0: + height = control.minHeight + dynamicHeight div expandHeightCount + else: + height = control.minHeight + dynamicHeight + else: + height = clientHeight - container.padding * 2 + elif control.heightMode == HeightMode_Auto: + height = control.wantedHeight + elif control.heightMode == HeightMode_Fill: + height = clientHeight - container.padding * 2 + + if control.minHeight > height: + height = control.minHeight + if control.maxHeight > 0 and control.maxHeight < height: + height = control.maxHeight + + if control.width != width or control.height != height: + # echo " child: " & control.tag + # echo " new size: " & $width & ", " & $height + control.setSize(width, height) + + # Position: + if container.layout == Layout_Vertical or container.childControls.len == 1: + if container.xAlign == XAlign_Center: + x = (innerWidth - width) div 2 + elif container.xAlign == XAlign_Right: + x = innerWidth - width - container.padding + if container.layout == Layout_Horizontal or container.childControls.len == 1: + if container.yAlign == YAlign_Center: + y = (innerHeight - height) div 2 + elif container.yAlign == YAlign_Bottom: + y = innerHeight - height - container.padding + + if control.x != x or control.y != y: + # echo " new pos: " & $x & ", " $y + control.setPosition(x, y) + + # Calculate next position: + case container.layout + of Layout_Horizontal: + x.inc(width) + x.inc(container.spacing) + if container.xAlign == XAlign_Spread and expandWidthCount == 0 and container.childControls.len > 1: + x.inc(dynamicWidth div (container.childControls.len - 1)) + of Layout_Vertical: + y.inc(height) + y.inc(container.spacing) + if container.yAlign == YAlign_Spread and expandHeightCount == 0 and container.childControls.len > 1: + y.inc(dynamicHeight div (container.childControls.len - 1)) + + +# ---------------------------------------------------------------------------------------- +# Frame +# ---------------------------------------------------------------------------------------- + +proc newFrame(text = ""): Frame = + result = new NativeFrame + result.NativeFrame.init() + result.text = text + +proc init(frame: Frame) = + frame.ControlImpl.init() + frame.fText = "" + +method text(frame: Frame): string = frame.fText + +method `text=`(frame: Frame, text: string) = + frame.fText = text + frame.tag = text + # should be extended by NativeFrame + +method getPadding(frame: Frame): Spacing = + result.left = 4 + result.right = 4 + result.top = 4 + result.bottom = 4 + # should be extended by NativeFrame + +method `onDraw=`(container: NativeFrame, callback: DrawProc) = raiseError("NativeFrame does not allow onDraw.") + + +# ---------------------------------------------------------------------------------------- +# Button +# ---------------------------------------------------------------------------------------- + +proc newButton(text = ""): Button = + result = new NativeButton + result.NativeButton.init() + result.text = text + +proc init(button: Button) = + button.ControlImpl.init() + button.fText = "" + button.fOnClick = nil + button.fWidthMode = WidthMode_Auto + button.fHeightMode = HeightMode_Auto + button.minWidth = 15 + button.minHeight = 15 + +method text(button: Button): string = button.fText + +method `text=`(button: Button, text: string) = + button.fText = text + button.tag = text + button.triggerRelayoutIfModeIsAuto() + # should be extended by NativeButton + +method naturalWidth(button: Button): int = button.getTextWidth(button.text) + 20 + +method naturalHeight(button: Button): int = button.getTextLineHeight() * button.text.countLines + 12 + +method `onDraw=`(container: NativeButton, callback: DrawProc) = raiseError("NativeButton does not allow onDraw.") + + +# ---------------------------------------------------------------------------------------- +# Label +# ---------------------------------------------------------------------------------------- + +proc newLabel(text = ""): Label = + result = new NativeLabel + result.NativeLabel.init() + result.text = text + +proc init(label: Label) = + label.ControlImpl.init() + label.fText = "" + label.fWidthMode = WidthMode_Auto + label.fHeightMode = HeightMode_Auto + label.minWidth = 10 + label.minHeight = 10 + +method text(label: Label): string = label.fText + +method `text=`(label: Label, text: string) = + label.fText = text + label.tag = text + label.triggerRelayoutIfModeIsAuto() + +method naturalWidth(label: Label): int = label.getTextWidth(label.text) + +method naturalHeight(label: Label): int = label.getTextLineHeight() * label.text.countLines + +method `onDraw=`(container: NativeLabel, callback: DrawProc) = raiseError("NativeLabel does not allow onDraw.") + + +# ---------------------------------------------------------------------------------------- +# TextBox +# ---------------------------------------------------------------------------------------- + +proc newTextBox(text = ""): TextBox = + result = new NativeTextBox + result.NativeTextBox.init() + result.text = text + +proc init(textBox: TextBox) = + textBox.ControlImpl.init() + textBox.fWidthMode = WidthMode_Expand + textBox.fHeightMode = HeightMode_Auto + textBox.minWidth = 20 + textBox.minHeight = 20 + +method naturalHeight(textBox: TextBox): int = textBox.getTextLineHeight() + +method text(textBox: TextBox): string = discard + # has to be implemented by NativeTextBox + +method `text=`(textBox: TextBox, text: string) = discard + # has to be implemented by NativeTextBox + +method `onDraw=`(container: NativeTextBox, callback: DrawProc) = raiseError("NativeTextBox does not allow onDraw.") + + +# ---------------------------------------------------------------------------------------- +# TextArea +# ---------------------------------------------------------------------------------------- + +proc newTextArea(text = ""): TextArea = + result = new NativeTextArea + result.NativeTextArea.init() + result.text = text + +proc init(textArea: TextArea) = + textArea.ControlImpl.init() + textArea.fWidthMode = WidthMode_Expand + textArea.fHeightMode = HeightMode_Expand + textArea.minWidth = 20 + textArea.minHeight = 20 + textArea.wrap = true + +method text(textArea: TextArea): string = discard + # has to be implemented by NativeTextBox + +method `text=`(textArea: TextArea, text: string) = discard + # has to be implemented by NativeTextBox + +method addText(textArea: TextArea, text: string) = textArea.text = textArea.text & text + +method addLine(textArea: TextArea, text = "") = textArea.addtext(text & "\n") + +method scrollToBottom(textArea: TextArea) = discard + # has to be implemented by NativeTextBox + +method `onDraw=`(container: NativeTextArea, callback: DrawProc) = raiseError("NativeTextArea does not allow onDraw.") + +method wrap(textArea: TextArea): bool = textArea.fWrap + +method `wrap=`(textArea: TextArea, wrap: bool) = + textArea.fWrap = wrap + # should be extended by NativeTextArea + + +# ---------------------------------------------------------------------------------------- +# Platform-specific implementation +# ---------------------------------------------------------------------------------------- + +when useWindows(): include "nigui/private/windows/platform_impl" +when useGtk(): include "nigui/private/gtk3/platform_impl" |
