# This makes all keyboards into editors automatically, so a keyboard # doesn't need to point at a real editor. May choose to change later, or # exclude keyboards that opt out. When /k/ is a keyboard with /...opts/ &\ /nobody/ wishes /k/ does not create an editor { Wish tag $k is stabilized # Create a synthetic editor above the keyboard page. set editor [list $k editor] Claim $editor is an editor Wish $editor has a canvas When $k has resolved geometry /geom/ { Claim $editor has resolved geometry $geom } When the quad library is /quadLib/ & $k has quad /q/ { Claim $editor has quad [$quadLib move $q up 105%] } Claim $k has created editor $editor Claim $k is typing into $editor } When /k/ is a keyboard with /...opts/ &\ /nobody/ claims /k/ has created editor /any/ &\ /k/ points up at /editor/ & /editor/ is an editor with /...opts/ { Claim $k is typing into $editor } When the program save directory is /programDir/ &\ the editor utils library is /utils/ { # TODO: also don't hardcode this? set margin [list 0.01 0.005 0.005 0.01] ;# CSS order (top, right, bottom, left) set defaults { textScale 0.01 } set editorLib [library create editorLib {margin defaults} { proc getAdvance {em} { # From NeomatrixCode.csv return $(0.5859375 * $em) } proc widthAndHeight {resolvedGeom} { set tagSize [dict get $resolvedGeom tagSize] set left [dict get $resolvedGeom left] set right [dict get $resolvedGeom right] set top [dict get $resolvedGeom top] set bottom [dict get $resolvedGeom bottom] set width $($left + $tagSize + $right) set height $($top + $tagSize + $bottom) return [list $width $height] } # given program and the editor options, figure out how many characters can # fit in this editor proc editorSizeInCharacters {resolvedGeom options} { variable margin set textScale [dict get $options scale] set advance [getAdvance $textScale] lassign [widthAndHeight $resolvedGeom] width height set width $($width - [lindex $margin 3] - $advance*2.5 - [lindex $margin 1]) set height $($height - [lindex $margin 0] - [lindex $margin 2]) set widthInCharacters $(int($width / $advance)) set heightInCharacters $(int($height / $textScale)) return [list $widthInCharacters $heightInCharacters] } }] When /someone/ claims /editor/ is an editor { Claim $editor is an editor with {*}$defaults } When /editor/ is an editor with /...options/ { # HACK: because we partial match if {![info exists options]} { return } set options [dict merge $defaults $options] Wish tag $editor is stabilized # Select which program the editor is viewing When $editor points up with length 0.3 at /program/ { Claim editor $editor has selected program $program } # Initial setup # Load initial text settings if not set When /nobody/ claims editor $editor has font options /...anything/ &\ the saved holds are loaded { # Wait until all the saved holds are loaded, so we don't accidentally # overwrite a saved setting. set textScale [dict get $options textScale] Hold! -save -key font-options-of:$editor \ Claim editor $editor has font options with scale $textScale } # We need the editor's resolved geometry to get its size. # We then use its size to figure out how many characters we can fit in it, # width-wise and height-wise. When $editor has resolved geometry /geom/ &\ editor $editor has font options with /...options/ { Claim editor $editor has viewport size [$editorLib editorSizeInCharacters $geom $options] } # Load in defaults for the editor if it hasn't been initialized set results [Query! /somebody/ claims editor $editor has cursor /anything/] if {[llength $results] == 0} { Hold! -key editor-state-of:$editor { Claim editor $editor has cursor 0 Claim editor $editor has max cursor x 0 Claim editor $editor has viewport position [list 0 0] Claim editor $editor has selection anchor "" Claim editor $editor has undo stack {} Claim editor $editor has redo stack {} Claim editor $editor has last edit type "" } } set clipResults [Query! /somebody/ claims editor $editor has clipboard /anything/] if {[llength $clipResults] == 0} { Hold! -key clipboard-of:$editor Claim editor $editor has clipboard "" } # Load in initial program code (note, this buffer is shared between # all editor instances) When editor $editor has selected program /program/ &\ /program/ has program code /programCode/ &\ the saved holds are loaded { # Check if the code already exists. set results [Query! editor buffer for $program is /anything/] if {[llength $results] == 0} { Hold! -save -key buffer-for:$program \ Claim editor buffer for $program is $programCode } } # Feedback: show that the editor is not active When /nobody/ wishes $editor is outlined green { Wish $editor is outlined blue } } Subscribe: keyboard /keyboard/ claims key Control_b is down with /...options/ { Notify: print code "# blank" } When /keyboard/ is a keyboard with path /kbPath/ /...anything/ &\ /keyboard/ is typing into /editor/ &\ /editor/ is an editor with /...anything/ &\ editor /editor/ has selected program /program/ { Wish $editor is outlined green Subscribe: keyboard $kbPath claims key /key/ is /keyState/ with /...options/ { ForEach! editor $editor has viewport position /vpPos/ &\ editor $editor has viewport size /vpSize/ &\ editor buffer for $program is /code/ &\ editor $editor has max cursor x /maxCursorX/ &\ editor $editor has cursor /cursor/ &\ editor $editor has selection anchor /selAnchor/ &\ editor $editor has clipboard /clipboard/ &\ editor $editor has undo stack /undoStack/ &\ editor $editor has redo stack /redoStack/ &\ editor $editor has last edit type /lastEditType/ &\ editor $editor has font options with /...textOptions/ { lassign $vpPos vpX vpY lassign $vpSize vpWidth vpHeight if {$keyState == "up"} { return } # if this is true, the buffer will remove the hold for program code # (triggering a reinitialization) set resetBuffer false set isUndo false set isRedo false set origCode $code set origCursor $cursor set origMaxCursorX $maxCursorX set hasShift [dict exists $options shift] set isNavKey [expr {$key eq "Left" || $key eq "Right" || $key eq "Up" || $key eq "Down"}] # Handle selection replacement for text-modifying keys set selectionHandled false if {$selAnchor ne ""} { set selStart [min $selAnchor $cursor] set selEnd [max $selAnchor $cursor] if {[dict exists $options printable]} { lassign [$utils replaceRange $code $selStart $selEnd [dict get $options printable]] code cursor maxCursorX set selAnchor "" set selectionHandled true } elseif {$key eq "Delete" || $key eq "Remove"} { lassign [$utils replaceRange $code $selStart $selEnd ""] code cursor maxCursorX set selAnchor "" set selectionHandled true } elseif {$key eq "Return"} { # Delete selection, then fall through to Return handler lassign [$utils replaceRange $code $selStart $selEnd ""] code cursor maxCursorX set selAnchor "" } } if {!$selectionHandled} { if {$hasShift && $isNavKey} { # Shift+Arrow: extend selection if {$selAnchor eq ""} { set selAnchor $cursor } lassign [$utils handleNavigation $key $code $cursor $maxCursorX] cursor maxCursorX } elseif {[dict exists $options printable]} { lassign [$utils insertText $code $cursor [dict get $options printable]] code cursor maxCursorX } else { # Regular navigation clears selection if {$isNavKey} { set selAnchor "" } # general editor functionality lassign [$utils handleNavigation $key $code $cursor $maxCursorX] cursor maxCursorX lassign [$utils handleRemovalAndReturn $key $code $cursor $maxCursorX] code cursor maxCursorX # specific editor functionality switch $key { Control_c { if {$selAnchor ne ""} { set clipText [$utils getSelectedText $code $selAnchor $cursor] Hold! -key clipboard-of:$editor Claim editor $editor has clipboard $clipText } } Control_x { if {$selAnchor ne ""} { set clipText [$utils getSelectedText $code $selAnchor $cursor] Hold! -key clipboard-of:$editor Claim editor $editor has clipboard $clipText set selStart [min $selAnchor $cursor] set selEnd [max $selAnchor $cursor] lassign [$utils replaceRange $code $selStart $selEnd ""] code cursor maxCursorX set selAnchor "" } } Control_v { if {$clipboard ne ""} { if {$selAnchor ne ""} { set selStart [min $selAnchor $cursor] set selEnd [max $selAnchor $cursor] lassign [$utils replaceRange $code $selStart $selEnd $clipboard] code cursor maxCursorX set selAnchor "" } else { lassign [$utils replaceRange $code $cursor $cursor $clipboard] code cursor maxCursorX } } } Control_z { if {[llength $undoStack] > 0} { lappend redoStack [list $code $cursor $maxCursorX] lassign [lindex $undoStack end] code cursor maxCursorX set undoStack [lrange $undoStack 0 end-1] set selAnchor "" set isUndo true set lastEditType "" } } Control_y { if {[llength $redoStack] > 0} { lappend undoStack [list $code $cursor $maxCursorX] lassign [lindex $redoStack end] code cursor maxCursorX set redoStack [lrange $redoStack 0 end-1] set selAnchor "" set isRedo true set lastEditType "" } } Control_r { set resetBuffer true } Control_s { Notify: save code on editor $editor } Control_p { Notify: print code $code # Give the user some feedback Hold! -key printing-alert:$editor \ Wish $editor is labelled "Printing!" sleep 0.25 Hold! -key printing-alert:$editor {} } Control_underscore { # ctrl and - (zoom out) set textScale [dict get $textOptions scale] set textScale [/ $textScale 1.1] Hold! -save -key font-options-of:$editor \ Claim editor $editor has font options with scale $textScale } equal { # Dunno why it registers as equal instead of Control_equal? It works regardless, lol # ctrl and + (zoom in) set textScale [dict get $textOptions scale] set textScale [* $textScale 1.1] Hold! -save -key font-options-of:$editor \ Claim editor $editor has font options with scale $textScale } } } } # Undo stack management with coalescing if {$code ne $origCode && !$isUndo && !$isRedo} { # Determine edit type for coalescing consecutive same-type edits if {[dict exists $options printable]} { set editType "insert" } elseif {$key eq "Delete" || $key eq "Remove"} { set editType "delete" } else { set editType "other" } # Only push when edit type changes or for non-coalescable edits if {$editType ne $lastEditType || $editType eq "other"} { lappend undoStack [list $origCode $origCursor $origMaxCursorX] if {[llength $undoStack] > 50} { set undoStack [lrange $undoStack end-49 end] } } set lastEditType $editType # Any real edit clears the redo stack set redoStack {} } elseif {!$isUndo && !$isRedo} { # Navigation or no-op resets edit type (breaks coalescing) set lastEditType "" } if {$resetBuffer} { set cursor 0 set maxCursorX 0 set selAnchor "" set undoStack {} set redoStack {} set lastEditType "" # remove the edited code to restore the program to its original code Hold! -on builtin-programs/programs.folk -key new-code-for:$program {} file delete "$programDir/[regsub {\.folk$} $program {}].folk.edited" Hold! -save -key buffer-for:$program { When $program has program code /originalCode/ { Claim editor buffer for $program is $originalCode } } } else { Hold! -save -key buffer-for:$program \ Claim editor buffer for $program is $code } lassign [$utils cursorToXy $code $cursor] cursorX cursorY # Be sure to have at least two characters on the left, so we can # see what we're removing. if {[- $cursorX 2] < $vpX} { set vpX [max 0 [- $cursorX 2]] } if {$cursorX >= $vpX + $vpWidth} { set vpX $($cursorX - $vpWidth) } if {$cursorY < $vpY} { set vpY $cursorY } if {$cursorY >= $vpY + $vpHeight - 1} { set vpY $($cursorY - $vpHeight + 1) } Hold! -keep 12ms -key editor-state-of:$editor { Claim editor $editor has viewport position [list $vpX $vpY] Claim editor $editor has cursor $cursor Claim editor $editor has max cursor x $maxCursorX Claim editor $editor has selection anchor $selAnchor Claim editor $editor has undo stack $undoStack Claim editor $editor has redo stack $redoStack Claim editor $editor has last edit type $lastEditType } } } } Subscribe: save code on editor /editor/ { ForEach! editor $editor has selected program /program/ &\ editor buffer for /program/ is /programCode/ { Hold! -on builtin-programs/programs.folk -key new-code-for:$program \ Wish program $program is replaced with \ code $programCode editedTime [clock seconds] set programBase [regsub {\.folk$} $program ""] set editedPath "$programDir/[set programBase].folk.edited" file mkdir [file dirname $editedPath] set fp [open $editedPath w] puts -nonewline $fp $programCode close $fp # Give the user some feedback Hold! -key saved-alert:$editor \ Wish $editor is labelled "Saved!" sleep 0.25 Hold! -key saved-alert:$editor {} } } # calculate cursor position When /editor/ is an editor with /...anything/ &\ editor /editor/ has cursor /cursor/ &\ editor /editor/ has selected program /program/ &\ editor buffer for /program/ is /code/ &\ editor /editor/ has viewport position /vpPos/ &\ editor /editor/ has font options with /...textOptions/ { lassign $vpPos vpX vpY lassign [$utils cursorToXy $code $cursor] cursorX cursorY set textScale [dict get $textOptions scale] set advance [$editorLib getAdvance $textScale] set offsetX $(($cursorX - $vpX) * $advance) set offsetY $(($cursorY - $vpY) * $textScale) Claim editor $editor has cursor position [list $offsetX $offsetY] } # Draw text and cursor When /editor/ is an editor with /...anything/ &\ editor /editor/ has viewport position /vpPos/ &\ editor /editor/ has viewport size /vpSize/ &\ editor /editor/ has font options with /...fontOptions/ { lassign $vpPos vpX vpY lassign $vpSize vpWidth vpHeight set textScale [dict get $fontOptions scale] set advance [$editorLib getAdvance $textScale] When editor $editor has selected program /program/ &\ editor buffer for /program/ is /code/ &\ editor $editor has cursor /cursor/ &\ editor $editor has cursor position /cursorPos/ &\ editor $editor has selection anchor /selAnchor/ { set lineCount [min [- [llength [split $code "\n"]] $vpY] $vpHeight] set lineNumbers [$utils lineNumberView $vpY $lineCount] set marginLeft [lindex $margin 3] set lineNumbersRight $($marginLeft + $advance*1.5) Wish to draw text onto $editor with \ position [list $lineNumbersRight [lindex $margin 0]] \ text $lineNumbers \ scale $textScale anchor topright font NeomatrixCode set text [$utils applyTextViewport $code $vpX $vpY $vpWidth $vpHeight] set pos [list [+ $lineNumbersRight $advance] [lindex $margin 0]] Wish to draw text onto $editor with \ position $pos text $text \ scale $textScale anchor topleft font NeomatrixCode # Draw selection highlight if {$selAnchor ne ""} { set rawStart [min $selAnchor $cursor] set rawEnd [max $selAnchor $cursor] lassign [$utils cursorToXy $code $rawStart] selStartX selStartY lassign [$utils cursorToXy $code $rawEnd] selEndX selEndY set lines [split $code "\n"] for {set ly $selStartY} {$ly <= $selEndY} {incr ly} { if {$ly < $vpY || $ly >= $vpY + $vpHeight} continue set lineLen [string length [lindex $lines $ly]] # Absolute column range for selection on this line if {$ly == $selStartY} { set absStart $selStartX } else { set absStart 0 } if {$ly == $selEndY} { set absEnd $selEndX } else { set absEnd $lineLen } # Clip to viewport set absStart [max $absStart $vpX] set absEnd [min $absEnd [+ $vpX $vpWidth]] if {$absStart >= $absEnd} continue # Convert to display coordinates set dispStart [- $absStart $vpX] set dispEnd [- $absEnd $vpX] set dispRow [- $ly $vpY] set x0 $([lindex $pos 0] + $dispStart * $advance) set x1 $([lindex $pos 0] + $dispEnd * $advance) set y0 $([lindex $pos 1] + $dispRow * $textScale) set y1 $($y0 + $textScale) Wish to draw a quad onto $editor with \ p0 [list $x0 $y0] p1 [list $x1 $y0] \ p2 [list $x1 $y1] p3 [list $x0 $y1] \ color {0.2 0.4 0.8 0.7} layer -1 } } set p1 [vec2 add $cursorPos $pos] set p2 [vec2 add $p1 [list 0 [* $textScale 1.2]]] set s [/ $textScale 6] Wish to draw a circle onto $editor with center $p1 radius $s thickness 0 color green filled true Wish to draw a line onto $editor with points [list $p1 $p2] width $s color green } } # end of library code }