You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
446 lines
17 KiB
446 lines
17 KiB
# -*- tcl -*- |
|
# Maintenance Instruction: leave the 999999.xxx.x as is and use punkshell 'dev make' or bin/punkmake to update from <pkg>-buildversion.txt |
|
# module template: shellspy/src/decktemplates/vendor/punk/modules/template_module-0.0.4.tm |
|
# |
|
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem. |
|
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository. |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# (C) 2025 |
|
# |
|
# @@ Meta Begin |
|
# Application punk::layout 999999.0a1.0 |
|
# Meta platform tcl |
|
# Meta license MIT |
|
# @@ Meta End |
|
|
|
|
|
#EXPERIMENTAL |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
## Requirements |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
|
|
package require Tcl 8.6- |
|
package require punk::args |
|
|
|
#experimental layout library based on Nic Barker's Clay |
|
# CLAY(CLAY_ID("parent"), { .layout = { .padding = CLAY_PADDING_ALL(8) } }) { |
|
# // Child element 1 |
|
# CLAY_TEXT(CLAY_STRING("Hello World"), CLAY_TEXT_CONFIG({ .fontSize = 16 })); |
|
# // Child element 2 with red background |
|
# CLAY(CLAY_ID("child"), { .backgroundColor = COLOR_RED }) { |
|
# // etc |
|
# } |
|
# } |
|
|
|
#Tcl translation? |
|
#CLAY ?-opt <val>..? ?childblock? |
|
#eg |
|
#CLAY -id <string> -layout <dict> -rectangle <dict> { childblock } |
|
#where childblock is curly braced code block |
|
|
|
# clay -id "parent" -layout [dict create .padding [padding_all 8]] { |
|
# clay_text "Hello World" [clay_textconfig {.fontsize 16}] |
|
# clay -id "child" -config {.backgroundColor red} { |
|
# # etc |
|
# } |
|
# } |
|
|
|
#for padding on a terminal - we are limited by cell_size - which is not square. (and may vary on terminals??) |
|
# e.g punk::console::cell_size returns cell wxh |
|
# -> 10x20 |
|
# (either queried from terminal - or defaults to 10x20 if can't be queried) |
|
#so the minimum 'even' padding we can have is 2 space for horizontal, 1 line vertical |
|
#we can pad a minimum of 1 space horizontall (10 units) and a minimum of 1 line vertically (20 units) |
|
# CLAY_PADDING {<l> <r> <t> <b>} |
|
#how then should we handle .padding {16 16 8 8} - quantize by roundup to h 10 and v 20? |
|
# -> {20 20 20 20} ? |
|
# or round to nearest vertical and horizontal quanta? |
|
# -> {20 20 0 0} |
|
#either way - {16 16 16 16} goes to {20 20 20 20} - which is *close* to equal v h padding |
|
# - but not perfect visually because fonts within a cell have baseline, topline etc internal padding too? |
|
|
|
#building a ui ? |
|
#clay_begin_layout |
|
# // build UI here |
|
#set rendercommands [clay_end_layout] |
|
#some_render_command $rendercommands |
|
|
|
|
|
|
|
|
|
#eg2 (from introducing clay video https://www.youtube.com/watch?v=DYWTw19_8r4&t=2s) |
|
#something like the following? |
|
# (stopped at around the scroll point - need to work out console mouse features. punk::console::mouse_enable ...) |
|
|
|
#extracted out common layout 'sizing' for grow in both directions |
|
#set layoutExpand { |
|
# width CLAY_SIZING_GROW |
|
# heigth CLAY_SIZING GROW |
|
# } |
|
#extracted out common background panel |
|
#set contentBackgroundConfig { |
|
# colour Term-grey |
|
# frametype arc |
|
# } |
|
#proc RenderHeaderButton {} {} ;#?? |
|
|
|
#set documents [dict create\ |
|
# Squirrels "The secret life of Squirrels"\ |
|
# "Lorem Ipsum" "Orem ipsum dolor sit ..\netc blah"\ |
|
# "Vacuum instructions" "Chapter 3: Getting Started"\ |
|
# "Article 4" "Article 4"\ |
|
# "Article 5" "Article 5"\ |
|
# ] |
|
|
|
#set selectedDocumentIndex 0 |
|
|
|
|
|
#clay_begin_layout |
|
#clay -id "OuterContainer"\ |
|
# -item [clay_rectangle {colour web-red}] |
|
# -layout [clay_layout [dict create\ |
|
# layoutDirection CLAY_TOP_TO_BOTTOM\ |
|
# sizing $layoutExpand\ |
|
# padding {16 16}\ |
|
# childGap 16\ |
|
# ]]\ |
|
# { |
|
# # Child elements |
|
# clay -id "HeaderBar"\ |
|
# -item [clay_rectangle $contentBackgroundConfig]\ |
|
# -layout [clay_layout { |
|
# sizing { |
|
# height 60 |
|
# width CLAY_SIZING_GROW |
|
# } |
|
# padding {8 8} |
|
# childGap 8 |
|
# childAlignment { |
|
# y CLAY_ALIGN_Y_CENTER |
|
# } |
|
# }]\ |
|
# { |
|
# # Header buttons go here |
|
# RenderheaderButton [clay_string "File"] |
|
# RenderheaderButton [clay_string "Edit"] |
|
# #push last 3 buttons to rhs by using a layout in between to take up the middle space |
|
# CLAY -layout {sizing {width CLAY_SIZING_GROW}} {} |
|
# RenderheaderButton [clay_string "Upload"] |
|
# RenderheaderButton [clay_string "Media"] |
|
# RenderheaderButton [clay_string "Support"] |
|
# } |
|
# clay -id "LowerContent"\ |
|
# -layout [dict create sizing $layoutExpand childGap 16]\ |
|
# { |
|
# clay -id "Sidebar"\ |
|
# -item [clay_rectangle $contentBackgroundConfig]\ |
|
# -layout { |
|
# layoutDirection CLAY_TOP_TO_BOTTOM |
|
# padding {8 8} |
|
# childgap {20} |
|
# sizing { |
|
# width = 60 |
|
# height = CLAY_SIZING_GROW |
|
# }\ |
|
# }\ |
|
# { |
|
# #render dynamic data |
|
# dict for {title data} $documents { |
|
# clay -layout {padding {8 8}\ |
|
# { |
|
# #fontId, fontSize not really practical on terminal |
|
# clay_text $title [clay_text_config { |
|
# textColor cyan |
|
# }] |
|
# } |
|
# } |
|
# } |
|
# clay -id "MainContent"\ |
|
# -item [clay_rectangle $contentBackgroundConfig]\ |
|
# -scroll {vertical true}\ |
|
# -layout [dict create\ |
|
# layoutDirection = CLAY_TOP_TO_BOTTOM\ |
|
# childGap 20\ |
|
# padding {16 16 20 20}\ |
|
# sizing $layoutExpand\ |
|
# ]\ |
|
# { |
|
# set selectedTitle [lindex [dict keys $documents] $selectedDocumentIndex] |
|
# clay_text $selectedTitle [clay_text_config { |
|
# textcolour {web-white bold} |
|
# }] |
|
# clay_text [dict get $documents $selectedTitle] [clay_text_config { |
|
# textcolour {web-white} |
|
# }] |
|
# } |
|
# } |
|
# } |
|
#set rendercommands [clay_end_layout] |
|
|
|
#CLAY_SIZING_GROW |
|
#CLAY_SIZING_FIT ;#default |
|
|
|
tcl::namespace::eval punk::layout { |
|
variable PUNKARGS |
|
namespace eval argdoc { |
|
variable PUNKARGS |
|
namespace eval argdoc { |
|
#non-colour SGR codes |
|
set I "\x1b\[3m" ;# [a+ italic] |
|
set NI "\x1b\[23m" ;# [a+ noitalic] |
|
set B "\x1b\[1m" ;# [a+ bold] |
|
set N "\x1b\[22m" ;# [a+ normal] |
|
set T "\x1b\[1\;4m" ;# [a+ bold underline] |
|
set NT "\x1b\[22\;24m\x1b\[4:0m" ;# [a+ normal nounderline] |
|
} |
|
} |
|
|
|
|
|
#elementDeclaration |
|
|
|
#data structure (CLAY UIElement struct equivalent) |
|
#set eg_element [dict create\ |
|
# position {1 1}\ |
|
# size {80 24}\ |
|
# children [list]\ |
|
#] |
|
|
|
|
|
#elementConfig |
|
#set eg_item [dict create\ |
|
# type rectangle\ |
|
# color {red bold}\ |
|
# borderColor {yellow} |
|
# frametype {}\ |
|
#] |
|
# etc |
|
proc DrawRectangle {element} { |
|
set position [dict get $element position] ;#x y |
|
set size [dict get $element size] ;#width height |
|
lassign $size w h |
|
set bg [dict get $element color] |
|
set borderColor [dict get $element borderColor] |
|
set frametype [dict get $element frametype] |
|
if {$frametype eq ""} { |
|
#much faster than textblock::frame |
|
set content [textblock::block $w $h "[a+ $bg] [a]"] |
|
} else { |
|
#slow - and frame background colours will always overlap the borders so we can never get a nicely filled arc frame for example |
|
#The best bordered |
|
set content [textblock::frame -type $frametype -ansibase [a+ $bg] -ansiborder [a+ $borderColor] -width $w -height $h " "] |
|
} |
|
} |
|
proc RenderElement {element} { |
|
|
|
} |
|
|
|
|
|
#TODO |
|
#The clay_begin_layout - clay_end_layout structure suggests the main API may be better represented as an oo object maintaining state |
|
#we may need to have multiple instances running concurrently |
|
#Clay conceptually runs in 'immediate' mode - ie recalculating each full layout each time we need to render |
|
|
|
variable rendercommands |
|
proc clay_begin_layout {} { |
|
variable rendercommands |
|
set rendercommands [dict create] |
|
#...? |
|
} |
|
proc clay_end_layout {} { |
|
variable rendercommands |
|
|
|
#...? |
|
|
|
return $rendercommands |
|
} |
|
|
|
|
|
namespace eval argdoc { |
|
variable PUNKARGS |
|
lappend PUNKARGS [list { |
|
@id -id ::punk::layout::clay |
|
@cmd -name punk::layout::clay |
|
@opts |
|
-id -type string |
|
-item -type dict |
|
-layout -type dict |
|
-backgroundColor -type list -help\ |
|
"ANSI colour codes as documented in punk::ansi::a?" |
|
@values -min 0 -max 1 |
|
childscript -type script |
|
}] |
|
} |
|
#For initial proof of concept - we will use punk::args for *parsing* as well as documenting |
|
#For final version - the extra overhead of punk::args::parse may not be suitable for large numbers of |
|
#calls in 'immediate' mode - so punk::args::parse should be used on 'unhappy' paths only, |
|
#with a fast switch statement used for actual option parsing. |
|
#As performance is likely to be an issue - we should also avoid allowing 'prefixes' for arguments |
|
proc clay {args} { |
|
set argd [punk::args::parse $args withid ::punk::layout::clay] ;#temporary during concept development - todo: change to manual parsing for performance. |
|
|
|
} |
|
# |
|
|
|
} |
|
|
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
# Secondary API namespace |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
tcl::namespace::eval punk::layout::lib { |
|
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase |
|
tcl::namespace::path [tcl::namespace::parent] |
|
|
|
|
|
#STEPS |
|
#Fit Sizing -> Grow Sizing -> Positions -> Draw |
|
#tree traversal |
|
#(reverse breadth first) (breadth first) |
|
# via OpenElement/CloseElement? (expand our grow containers |
|
# into any available space) |
|
|
|
proc OpenElement {element} { |
|
#pseudo... |
|
#set layoutDirection $layoutconfig.layoutDirection |
|
#if {$layoutDirection eq "CLAY_LEFT_TO_RIGHT"} { |
|
#} else { |
|
# #CLAY_TOP_TO_BOTTOM |
|
#} |
|
} |
|
proc CloseElement {element} { |
|
#element.parent.width += element.width |
|
#element.parent.height += element.height |
|
|
|
} |
|
|
|
proc SizeContainersAlongAxis {is_x_axis totalSizeToDistribute} { |
|
} |
|
proc CalculateFinalLayout {} { |
|
} |
|
|
|
|
|
|
|
} |
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
|
|
|
|
|
|
#tcl::namespace::eval punk::layout::system { |
|
#} |
|
|
|
|
|
# == === === === === === === === === === === === === === === |
|
# Sample 'about' function with punk::args documentation |
|
# == === === === === === === === === === === === === === === |
|
tcl::namespace::eval punk::layout { |
|
tcl::namespace::export {[a-z]*} ;# Convention: export all lowercase |
|
variable PUNKARGS |
|
variable PUNKARGS_aliases |
|
|
|
lappend PUNKARGS [list { |
|
@id -id "(package)punk::layout" |
|
@package -name "punk::layout" -help\ |
|
"EXPERIMENTAL - not done" |
|
}] |
|
|
|
namespace eval argdoc { |
|
#namespace for custom argument documentation |
|
proc package_name {} { |
|
return punk::layout |
|
} |
|
proc about_topics {} { |
|
#info commands results are returned in an arbitrary order (like array keys) |
|
set topic_funs [info commands [namespace current]::get_topic_*] |
|
set about_topics [list] |
|
foreach f $topic_funs { |
|
set tail [namespace tail $f] |
|
lappend about_topics [string range $tail [string length get_topic_] end] |
|
} |
|
#Adjust this function or 'default_topics' if a different order is required |
|
return [lsort $about_topics] |
|
} |
|
proc default_topics {} {return [list Description *]} |
|
|
|
# ------------------------------------------------------------- |
|
# get_topic_ functions add more to auto-include in about topics |
|
# ------------------------------------------------------------- |
|
proc get_topic_Description {} { |
|
punk::args::lib::tstr [string trim { |
|
package punk::layout |
|
An experiment in using CLAY UI style layout on the terminal |
|
INCOMPLETE... |
|
status: notes and started framework only - nothing usable. |
|
} \n] |
|
} |
|
proc get_topic_License {} { |
|
return "MIT" |
|
} |
|
proc get_topic_Version {} { |
|
return "$::punk::layout::version" |
|
} |
|
proc get_topic_Contributors {} { |
|
set authors {{Julian Noble <julian@precisium.com.au}} |
|
set contributors "" |
|
foreach a $authors { |
|
append contributors $a \n |
|
} |
|
if {[string index $contributors end] eq "\n"} { |
|
set contributors [string range $contributors 0 end-1] |
|
} |
|
return $contributors |
|
} |
|
proc get_topic_concepts {} { |
|
punk::args::lib::tstr -return string { |
|
see Nic Barker's youtube video: How Clay's UI Layout Algorithm Works |
|
https://www.youtube.com/watch?v=by9lQvpvMIc |
|
} |
|
} |
|
# ------------------------------------------------------------- |
|
} |
|
|
|
# we re-use the argument definition from punk::args::standard_about and override some items |
|
set overrides [dict create] |
|
dict set overrides @id -id "::punk::layout::about" |
|
dict set overrides @cmd -name "punk::layout::about" |
|
dict set overrides @cmd -help [string trim [punk::args::lib::tstr { |
|
About punk::layout |
|
}] \n] |
|
dict set overrides topic -choices [list {*}[punk::layout::argdoc::about_topics] *] |
|
dict set overrides topic -choicerestricted 1 |
|
dict set overrides topic -default [punk::layout::argdoc::default_topics] ;#if -default is present 'topic' will always appear in parsed 'values' dict |
|
set newdef [punk::args::resolved_def -antiglobs -package_about_namespace -override $overrides ::punk::args::package::standard_about *] |
|
lappend PUNKARGS [list $newdef] |
|
proc about {args} { |
|
package require punk::args |
|
#standard_about accepts additional choices for topic - but we need to normalize any abbreviations to full topic name before passing on |
|
set argd [punk::args::parse $args withid ::punk::layout::about] |
|
lassign [dict values $argd] _leaders opts values _received |
|
punk::args::package::standard_about -package_about_namespace ::punk::layout::argdoc {*}$opts {*}[dict get $values topic] |
|
} |
|
} |
|
# end of sample 'about' function |
|
# == === === === === === === === === === === === === === === |
|
|
|
|
|
# ----------------------------------------------------------------------------- |
|
# register namespace(s) to have PUNKARGS,PUNKARGS_aliases variables checked |
|
# ----------------------------------------------------------------------------- |
|
# variable PUNKARGS |
|
# variable PUNKARGS_aliases |
|
namespace eval ::punk::args::register { |
|
#use fully qualified so 8.6 doesn't find existing var in global namespace |
|
lappend ::punk::args::register::NAMESPACES ::punk::layout |
|
} |
|
# ----------------------------------------------------------------------------- |
|
|
|
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ |
|
## Ready |
|
package provide punk::layout [tcl::namespace::eval punk::layout { |
|
variable pkg punk::layout |
|
variable version |
|
set version 999999.0a1.0 |
|
}] |
|
return |
|
|
|
|