Last active
April 23, 2024 17:30
-
-
Save luce80/433286c66d98997aff6e69fbd6323a35 to your computer and use it in GitHub Desktop.
Some useful Red View/VID styles
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Red [ | |
title: "Text area with multiple undos" | |
author: @luce80 | |
Rights: "Copyright (C) 2022 Marco Antoniazzi. All rights reserved." | |
License: BSL-1 | |
file: %area-plus.red | |
gist: https://gist.githubusercontent.com/luce80/433286c66d98997aff6e69fbd6323a35/raw/f855edab024f19b1cc71ae9565f29f7403c96408/area-plus.red | |
gist-view: https://gist.github.com/luce80/433286c66d98997aff6e69fbd6323a35#file-area-plus-red | |
date: 26-12-2022 | |
version: 0.5.4 | |
history: [ | |
0.0.0 [05-11-2022 "Started"] | |
0.1.0 [08-11-2022 "Minimum working version"] | |
0.2.0 [17-11-2022 "Minimum working version with diff"] | |
0.3.0 [19-11-2022 "fix selection replacement"] | |
0.4.0 [20-11-2022 "Redone simplified thanks to using on-change, Ctrl+D, indent/outdent"] | |
0.5.2 [24-11-2022 "indent/outdent, fix compatibilty with user actors, fix auto-sync? (??)"] | |
0.5.3 [26-11-2022 "fix selected with tab indent/outdent"] | |
0.5.4 [26-12-2022 "fix when event is none"] | |
] | |
Notes: { | |
Use <Ctrl+Z> as always to undo, use <Ctrl+Shift+Z> to redo. | |
<Ctrl+D> = duplicate selection | |
Selection+<Tab> = indent text with tabs | |
Selection+<Shift+Tab> = outdent text removing tabs | |
Undoing is done with only removes and insertions, there are no changes. This means that a change is made by a remove followed by an insertion, and this | |
means that when undoing a change some text will first disappear! | |
Since currently Red does not permit to move the caret, I am using an ugly-but-better-then-nothing workaround by selecting 1 character. | |
DISCLAIMER: Use at your own risk. It seems to work on my system. | |
} | |
] | |
probedo: func [code [block!] /local result][print [mold code mold/only result: reduce code] :result] | |
probedo: func [code [block!] /local result][] | |
system/view/VID/styles/area-plus: [ | |
default-actor: on-change | |
template: [ | |
type: 'area | |
size: 150x150 ; same as default | |
text: "" | |
font: make font! [ | |
size: 10 | |
] | |
selected-range: none | |
ctrlz: 0 | |
key: none ; cached current key pressed | |
optimize: true | |
old-text: copy "" | |
last-added: copy "" | |
undos: copy [] | |
redos: copy [] | |
undo: func [][actors/undo self] | |
redo: func [][actors/redo self] | |
actors: [ | |
selection: func [text [string!] selection [pair!]][ | |
copy/part at text selection/1 selection/2 - selection/1 + 1 | |
] | |
indent: func [string [string!] selected [pair!] /local start beg end] [ | |
start: at string selected/1 | |
start: any [find/tail/reverse start newline head string] | |
beg: index? start | |
end: selected/2 | |
until [ | |
insert start tab | |
end: end + 1 | |
start: find/tail start newline | |
any [ | |
none? start | |
(index? start) > end | |
] | |
] | |
;head string | |
as-pair beg end | |
] | |
outdent: func [string [string!] selected [pair!] /local start beg end] [ | |
start: at string selected/1 | |
start: any [find/tail/reverse start newline head string] | |
beg: index? start | |
end: selected/2 | |
until [ | |
if start/1 = tab [ | |
remove start | |
end: end - 1 | |
] | |
start: find/tail start newline | |
any [ | |
none? start | |
(index? start) > end | |
] | |
] | |
;head string | |
as-pair beg end | |
] | |
diff: func [string1 [string!] string2 [string!] /local b n1 n2 delta] [ | |
if string1 == string2 [return [i 0 ""]] | |
n1: length? string1 | |
n2: length? string2 | |
delta: absolute n1 - n2 | |
if find/case/match string1 string2 [return reduce ['r n2 copy/part skip string1 n2 delta]] | |
if find/case/match string2 string1 [return reduce ['i n1 copy/part skip string2 n1 delta]] | |
n1: 1 | |
n2: 1 | |
while [(string1/:n1) = (string2/:n2)] [n1: n1 + 1 n2: n2 + 1] | |
b: n1 - 1 | |
n1: length? string1 | |
n2: length? string2 | |
while [(string1/:n1) = (string2/:n2)] [n1: n1 - 1 n2: n2 - 1] | |
;print ["b" b "delta" delta "n1" n1 "n2" n2] | |
reduce either n1 > n2 [ | |
['r b copy/part skip string1 b delta] | |
] [ | |
['i b copy/part skip string2 b delta] | |
] | |
] | |
add-to-undos: func [face [object!] text [string!] /local new old][ | |
if all [ | |
face/ctrlz <= 1 ; FIXME: | |
face/text <> face/last-added | |
;face/text <> "" | |
] [ | |
new: diff face/last-added face/text | |
old: any [last face/undos [i -1 ""]] | |
either all [ | |
face/optimize | |
new/1 = old/1 | |
new/2 = (old/2 + length? old/3) | |
][ | |
; optimize by joining insertions | |
append old/3 new/3 | |
][ | |
append/only face/undos new | |
] | |
clear face/redos ; to stop redoing old undos | |
face/last-added: copy face/text | |
] | |
] | |
undo: func [face [object!] /local act sel asy][ | |
asy: system/view/auto-sync? | |
loop 2 [ ; prettify indent undo | |
;system/view/auto-sync?: yes ;@@ ?? turn-on auto-sync but store its state before | |
if empty? face/undos [exit] | |
act: take/last face/undos | |
append/only face/redos act | |
sel: switch first act [ | |
i ic [face/text: head remove/part skip face/text act/2 length? act/3 (0)] ; if something was inserted, now remove it | |
r rc [face/text: head insert skip face/text act/2 act/3 (length? act/3)] | |
;FIXME: c [change at text act/2 act/4 act/3] | |
] | |
face/old-text: copy face/text | |
face/last-added: copy face/text | |
;@@ fake caret movement with selection :( (until Red will let set the caret position) | |
sel: sel + act/2 | |
if sel = (length? face/text) [sel: sel + 1] ; a little aesthetic improvement | |
face/selected: to-pair sel | |
system/view/auto-sync?: asy | |
if act/1 <> 'ic [break] | |
] | |
if not system/view/auto-sync? [show face] | |
] | |
redo: func [face [object!] /local act sel asy][ | |
asy: system/view/auto-sync? | |
loop 2 [ ; prettify indent redo | |
;system/view/auto-sync?: yes ;@@ ?? turn-on auto-sync but store its state before | |
if empty? face/redos [exit] | |
act: take/last face/redos | |
append/only face/undos act | |
sel: switch first act [ | |
i ic [face/text: head insert skip face/text act/2 act/3 (length? act/3)] | |
r rc [face/text: head remove/part skip face/text act/2 length? act/3 (0)] | |
;FIXME: c [change at text act/2 act/4 act/3] | |
] | |
face/old-text: copy face/text | |
face/last-added: copy face/text | |
;@@ fake caret movement with selection :( (until Red will let set the caret position) | |
sel: sel + act/2 | |
if sel = (length? face/text) [sel: sel + 1] ; a aesthetic improvement | |
face/selected: to-pair sel | |
system/view/auto-sync?: asy | |
if act/1 <> 'rc [break] | |
] | |
if not system/view/auto-sync? [show face] | |
] | |
; placeholders | |
on-created: func [face [object!] event [event! none!]][] | |
on-key: func [face [object!] event [event! none!]][] | |
on-key-up: func [face [object!] event [event! none!]][] | |
on-change: func [[trace] face [object!] event [event! none!]][] | |
] | |
] | |
init: [ | |
face: self | |
face/actors/on-created: func [face [object!] event [event! none!]] head insert body-of :face/actors/on-created [ | |
face/last-added: copy face/text | |
face/old-text: copy face/text | |
] | |
face/actors/on-key: func [face [object!] event [event! none!] /local selected] head insert body-of :face/actors/on-key [ | |
unless none? event [ | |
if char? event/key [face/key: event/key] | |
either event/key = #"^Z" [ | |
face/ctrlz: face/ctrlz + 1 | |
][ | |
face/ctrlz: 0 | |
] | |
if all [ | |
pair? selected: face/selected ; also store selected range | |
event/key = #"^D" | |
][ | |
face/text: head insert skip face/text face/selected/2 face/actors/selection face/text face/selected | |
face/selected: selected ; restore selected range | |
face/optimize: false ; avoid joining last insertions toghether | |
face/actors/on-change face none | |
face/optimize: true | |
] | |
face/selected-range: face/selected ; also store selected range | |
if all [ | |
pair? selected: face/selected-range ; also store selected range | |
event/key = tab | |
][ | |
either event/shift? [ | |
face/selected-range: face/actors/outdent face/old-text selected | |
face/selected: face/selected-range + 0x1 | |
][ | |
face/selected-range: face/actors/indent face/old-text selected | |
face/selected: face/selected-range - 0x1 | |
] | |
; "manually" add to undos | |
append/only face/undos reduce ['rc face/selected/1 - 1 face/actors/selection face/text selected ] | |
append/only face/undos reduce ['ic face/selected/1 - 1 face/actors/selection face/old-text face/selected-range ] | |
face/last-added: copy face/old-text ; keep this updated | |
face/optimize: false ; avoid joining last insertions together | |
] | |
if all [ | |
event/key = 'up | |
event/ctrl? | |
event/shift? | |
][ | |
; TBD ;probedo ["up"] | |
] | |
if not system/view/auto-sync? [show face] ;@@ force update even if auto-sync? is off | |
] ;unless none? | |
] | |
face/actors/on-key-up: func [face [object!] event [event! none!]] head insert body-of :face/actors/on-key-up [ | |
unless none? event [ | |
if all [event/key = #"Z" event/ctrl? not event/shift?] [ | |
face/actors/undo face | |
] | |
if all [event/key = #"Z" event/ctrl? event/shift?] [ | |
face/actors/redo face | |
] | |
face/key: none | |
] ;unless none? | |
] | |
face/actors/on-change: func [[trace] face [object!] event [event! none!]] head insert body-of :face/actors/on-change [ | |
if odd? face/ctrlz [ | |
face/text: face/old-text ; undo the original undo | |
] | |
either all [ | |
pair? face/selected-range | |
face/key = tab | |
][ | |
face/text: face/old-text ; undo the tab substitution | |
face/selected: face/selected-range ; restore selected range | |
face/optimize: false ; avoid joining last insertions toghether | |
][ | |
face/actors/add-to-undos face face/text | |
] | |
if not system/view/auto-sync? [show face] ;@@ force update even if auto-sync? is off | |
face/optimize: true | |
face/old-text: copy face/text | |
] | |
] | |
] | |
do | |
[ | |
if any [%area-plus.red = find/last/tail system/options/script "/" ; It's really me ? | |
system/script/args = "test"] [ | |
;print "" ; open console for debug | |
system/view/VID/styles/text: [template: [type: 'text size: 0x0]] | |
win: layout compose/deep [ | |
title "Text area with multiple undos" ;@@ I wish I could do : title (system/script/header/title) | |
space 4x4 | |
below | |
text "Try an area with multiple undos" | |
text "Please forgive 1-char selection used to move the caret ;(" bold | |
text "If, when undoing, something disappears, try undoing again !" | |
a+: area-plus 500x400 focus {if 'this-is-a-test [ | |
try any [ | |
Selection + <Tab> | |
Selection + <Shift+Tab> | |
] | |
to indent! (or~ outdent! Selection) | |
] | |
also <Ctrl+D> to duplicate! selection | |
} | |
across | |
button "Undo" [a+/undo set-focus a+] | |
button "Redo" [a+/redo set-focus a+] | |
] | |
view win | |
] ; if | |
] ; do |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;Red [Needs: 'View] | |
Red [ | |
title: "Rich-text Text Area" | |
file: %area-rt.red | |
author: @luce80 | |
License: 'PD | |
gist-view: none | |
date: 22-04-2024 | |
version: 0.9.4 | |
history: [ | |
0.0.0 [20-01-2024 "Started"] | |
0.8.0 [19-02-2024 "Main aspects completed"] | |
0.9.0 [24-02-2024 "Fixes, ADD: read-only"] | |
0.9.2 [07-03-2024 "ADD: clear-text FIX: use integer!s for scrollers calcs, FIX: undo smaller changes, FIX: immediatly show cursor "] | |
0.9.3 [10-04-2024 "FIX: hide cursor in read-only mode FIX: wheel scroll with only vert scroller"] | |
0.9.4 [22-04-2024 "ADD: margins and edge"] | |
] | |
Note: {Needs Red 0.6.5 built 17-Feb-2024 or later} | |
Notes: { | |
Many things are copied WITHOUT permission from %view-edit.r of Rebol2 SDK. Original header with Copyright note is given below. | |
Other things are copied from Didier Cadieu and myself. | |
This style was created because default area does not expose any of its methods! | |
This style was created with a rich-text drawn on another face because rich-text lacks a "scroll" or "origin" parameter! | |
IMPORTANT: Undoing is done only with removes and insertions, there are no changes. This means that | |
a change is made by a remove followed by an insertion, and this means that, sometimes, when undoing a change, some text | |
will first disappear! But you'll just have to undo again! | |
There are a few "public" functions: | |
undo: func ["Undo previous editing action"] | |
redo: func ["Redo last editing action"] | |
update: func ["Reshow all"[ | |
insert-text: func [ | |
"Insert given string at given position (0 means tail)" | |
string [string!] position [integer!] | |
] | |
remove-text: func [ | |
"Remove length characters (or 0 to indicate to tail) at given position" | |
position [integer!] length [integer!] | |
] | |
clear-text: func ["Remove all text"] | |
get-text: func ["Return a copy of the text"] | |
selected?: func ["Return TRUE if some text is highlighted"][ | |
select: func [ | |
"Use a pair! to highlight a part of the text (index x length) or move the caret (index x 0)" | |
value [pair!] | |
] | |
select-all: func ["Highlight all text"] | |
Please avoid modifing area text directly, use the provided `insert-text` and `remove-text` instead. | |
You can also do some "fancy" stuff using face/actors/edit-text which is the main function but only "at your own risk". | |
You can set some parameters by using `options` VID keyword. | |
The currently available parameters are: | |
- flags: a `block!` containing one or more of: | |
- read-only Prevent user input, You can still select and copy text. | |
TBD: cambiare colore on-unfocus | |
paced scrolling | |
options/themes | |
word skipping!=selection | |
word selection on mouse move | |
better undo optimizations ('-+ '+-) | |
overwrite mode (is it worth it?) | |
line-scrolling instead of pixel scrolling | |
} | |
view-edit-R2-header: [ | |
Title: "REBOL/View: Text Edit Core" | |
Version: 2.7.6 | |
Rights: "Copyright REBOL Technologies 2008. All rights reserved." | |
Home: http://www.rebol.com | |
Date: 14-Mar-2008 | |
; You are free to use, modify, and distribute this file as long as the | |
; above header, copyright, and this entire comment remains intact. | |
; This software is provided "as is" without warranties of any kind. | |
; In no event shall REBOL Technologies or source contributors be liable | |
; for any damages of any kind, even if advised of the possibility of such | |
; damage. See license for more information. | |
; Please help us to improve this software by contributing changes and | |
; fixes. See http://www.rebol.com/support.html for details. | |
] | |
] | |
probedo: func [code [block!] /local result][print [mold code mold/only result: reduce code] do :result] | |
if system/build/date < 12-02-2024 [alert "A more recent version of Red is required !" either empty? gui-console-ctx/terminal/lines [quit][halt]] ; to use recent fixes | |
area-rt.red-ctx: context [ | |
use: func [words [block!] body [block!]][body: has words body body] | |
trim-auto: func [ | |
"Auto indents lines relative to first line." | |
series [series!] | |
/local firstline firstchar indent | |
] [ | |
firstline: remove/part series find series complement charset " ^/" | |
firstchar: find firstline complement charset " ^-" | |
indent: head copy/part firstline firstchar | |
replace/all series indent "" | |
head series | |
] | |
trim-auto: func [ | |
"Auto indents lines relative to first line.(4 spaces=1 tab) (modifies)" | |
string [string!] | |
/local space non-spaces line start end | |
] [ | |
space: " " | |
non-spaces: complement charset " ^-" | |
; skip initial empty lines | |
parse string [any newline any [some space some newline] start:] | |
; trim initial empty lines | |
string: remove/part string start | |
; add initial "fake" newline to simplify things | |
string: head insert string "^/" | |
; convert initial spaces to tabs | |
line: [newline start: opt [some [start: [1 3 space tab | 4 space] end: ( | |
change/part start "^-" end | |
)]] opt [some non-spaces]] | |
parse string [some line] | |
; outdent lines | |
while [find/match string "^/^-"] [replace/all string "^/^-" "^/"] | |
; remove initial "fake" newline | |
remove string | |
] | |
away?: func [ ;@@ workaround for #5472 | |
face event | |
][ | |
event/offset <> min max 0x0 event/offset face/size - face/scrollers-size | |
] | |
find-window: function [ | |
"Finds a face's window face." | |
face [object!] | |
][ | |
if p: face/parent [ | |
while [p/type <> 'window][p: p/parent] | |
] | |
p | |
] | |
set-focus: func [ | |
"Sets the focus on the argument face" | |
face [object!] | |
/local p | |
][ | |
p: find-window face | |
if p/selected <> face [ | |
if p/selected [do-actor p/selected none 'unfocus] | |
do-actor p/selected: face none 'focus | |
] | |
] | |
focused?: function [ | |
"TRUE if face is focused." | |
face [object!] | |
][ | |
same? face get in find-window face 'selected | |
] | |
system/view/VID/styles/area-rt: [ | |
default-actor: on-change | |
template: [ | |
type: 'rich-text ; could be also 'base | |
size: 150x150 | |
color: white ;255.255.200 ;55 | |
;text: copy "" | |
flags: [scrollable focusable]; | |
tabs: none line-spacing: 'default handles: none ; mandatory fields for a rich-text | |
menu: [ | |
"Cut Ctrl+X" menu-cut | |
"Copy Ctrl+C" menu-copy | |
"Paste Ctrl+V" menu-paste | |
--- | |
"Undo Ctrl+Z" menu-undo | |
"Redo Ctrl+Shift+Z" menu-redo | |
--- | |
"Select All Ctrl+A" menu-select-all | |
"Duplicate Ctrl+D" menu-duplicate | |
"lowercase Ctrl+U" menu-lowercase | |
"UPPERCASE Ctrl+Shift+U" menu-uppercase | |
"Indent Tab" menu-indent | |
"Outdent Shift+Tab" menu-outdent | |
] | |
selected: 0x0 | |
created: false | |
old-handler: none ; old global tab handler | |
down?: false ;@@ workaround for #5475 | |
scrollers-size: system/view/metrics/misc/scroller ; scrollers thickness | |
us: self | |
margins: 4x4 ;@@ should this be inside `para`? | |
para: make para! [ | |
scroll: (0, 0) | |
wrap?: false | |
on-change*: func [word old new][ | |
; if not function? :new [prin "PARAC " ?? word ?? new] | |
switch word [ | |
wrap? [ | |
if object? us/draw/(rt/text)/para [ | |
set-quiet in us/draw/(rt/text)/para 'wrap? new | |
; set-quiet word old | |
] | |
] | |
scroll [ | |
us/draw/(rt/scroll): new | |
] | |
] | |
] | |
] | |
draw: compose/deep [ | |
line-width 2 pen (black + 0.0.0.230) box 0x0 (size) ; edge | |
clip (margins) (size) ; clip | |
translate (margins) ; margins | |
translate (0, 0) ; scroll offset | |
text (0, 0) (rtd-layout [white bg 0.120.215 " " /bg]) ; main rich.text with colors of highlighted text | |
; caret | |
pen black ; caret color | |
translate (0, 0) ; caret offset | |
scale 1 1 ; caret width and height | |
line-width 1 | |
line (0.5, 0) (0.5, 1) ; caret vertical line | |
] | |
rt: object [ ;@@ Red "strange" get-word! draw indexing makes me prefer this "indirect" numbers :( | |
edge: 7 | |
clip-offset: edge + 2 | |
clip: edge + 3 | |
scroll: clip + 4 | |
text: scroll + 3 | |
caret: text | |
caret-color: caret + 2 | |
caret-offset: caret + 4 ; relative to rich-text offset | |
caret-width: caret + 6 | |
caret-height: caret + 7 | |
face: none | |
data: none | |
font-size: 2 ; index inside rt/face/data | |
font-color: 3 ; index inside rt/face/data | |
selection: 4 ; index inside rt/face/data | |
hi-font-color: 5 ; index inside rt/face/data | |
hi-color: 7 ; index inside rt/face/data | |
] | |
; FIXME: add a "dark" theme? Change depending on OS? | |
colors: object [ | |
background: white | |
text: black | |
highlight: sky ; highlighted background | |
hi-text: white ; highlighted text | |
highlight-unfocused: gray ; highlighted background when unfocused | |
hi-text-unfocused: black ; highlighted text when unfocused | |
cursor: black | |
] | |
values: object [ ; object used to extend VID params and set main control params | |
flags: copy [] ; 'read-only , TBD 'blinking | |
] | |
; public functions | |
undo: func ["Undo previous editing action"][actors/undo self] | |
redo: func ["Redo last editing action"][actors/redo self] | |
update: func [ ;@@ REDEFINED update | |
"Reshow all" | |
][ | |
actors/update-scrollers self size size-text self/rt/face | |
actors/draw-cursor self none ; used also to calc caret/offset | |
actors/scroll-to-caret self | |
actors/draw-hilight self | |
] | |
;edit | |
insert-text: func [ | |
"Insert given string at given position (0 means tail)" | |
string [string! char!] position [integer!] | |
][ | |
if position = 0 [position: 1 + length? text] | |
position: min max 1 position 1 + length? text | |
actors/caret/pos: at text position | |
;actors/caret/pos: | |
actors/insert-chars/no-opt self string | |
update | |
] | |
remove-text: func [ | |
"Remove length characters (or 0 to indicate to tail) at given position" | |
position [integer!] length [integer!] | |
][ | |
position: min max 1 position length? text | |
if length = 0 [length: length? text] | |
length: min max 1 length (1 + length? text) - position | |
actors/caret/pos: at text position + length | |
actors/caret/pos: actors/remove-chars self compose [ctrl? (false)] [] reduce [at text position] | |
update | |
] | |
clear-text: func [ | |
"Remove all text" | |
][ | |
remove-text 1 0 | |
] | |
get-text: func [ | |
"Return a copy of the text" | |
][ | |
copy head text | |
] | |
selected?: func [ | |
"Return TRUE if some text is highlighted" | |
][ | |
to logic! all [pair? selected selected/2 > 0] | |
] | |
select: func [ ;@@ REDEFINED select | |
"Highlight a part of the text using a pair! (index x length)" | |
value [pair!] | |
][ | |
actors/select self value | |
] | |
select-all: func [ | |
"Highlight all text" | |
][ | |
actors/select self as-pair 1 length? text | |
] | |
; | |
actors: [ | |
us: none | |
highlight-start: none | |
highlight-end: none | |
none none ;@@ workaround for #5488 | |
scroller: make map! 2 | |
caret: make object! [ | |
index: 1 ; integer! | |
pos: none ; string! | |
offset: (0, 0) ; relative to rich-text offset | |
width: 1 ; currently unused | |
height: 15 ; line height | |
color: black | |
blinking: 0:0:0.6 | |
visible: 0 | |
; on-change*: func [word old new][ | |
; if word = 'pos [draw-cursor us none] | |
; ] | |
] | |
flag?: func [face flag [word! block!]] [ | |
to logic! find face/values/flags flag | |
] | |
; indent outdent | |
indent: func [string [string!] selected [pair!] /local start beg end] [ | |
start: at string selected/1 | |
start: any [find/tail/reverse start newline head string] | |
beg: index? start | |
end: selected/1 + selected/2 | |
until [ | |
insert start tab | |
end: end + 1 | |
start: find/tail start newline | |
any [ | |
none? start | |
(index? start) >= end | |
] | |
] | |
as-pair beg end - beg | |
] | |
outdent: func [string [string!] selected [pair!] /local start beg end] [ | |
start: at string selected/1 | |
start: any [find/tail/reverse start newline head string] | |
beg: index? start | |
end: selected/1 + selected/2 | |
until [ | |
if start/1 = tab [ | |
remove start | |
end: end - 1 | |
] | |
start: find/tail start newline | |
any [ | |
none? start | |
(index? start) >= end | |
] | |
] | |
as-pair beg end - beg | |
] | |
; | |
; undo redo | |
history: copy [] | |
none ;@@ workaround for #5488 | |
; act is a block in the form: [command where what how-many] (eg. [-- 10 "hi" 2] [++ 10 "HI" 2]) | |
;command is '-- or '++, where is an index, what is a text, how-many is the length | |
add_to_history: func [act [block! none!] /no-opt] [ | |
if none? act [exit] | |
;FIXME: do-actor us none 'change | |
if 10000 < length? history [remove head history] ; if limit exceeded remove 1st | |
clear history ; erase the future | |
;try to optimize by grouping actions | |
either all [ | |
not no-opt | |
(length? head history) >= 1 | |
act/1 = '++ | |
history/-1/1 = act/1 ; same action | |
(history/-1/2 + history/-1/4) = act/2 ; consecutive additions | |
not find word-limits/1 act/3 ; not a separator | |
history/-1/4 <= 4 ; only small changes | |
act/4 <= 2 ; only small changes | |
] | |
[ | |
append history/-1/3 act/3 | |
history/-1/4: history/-1/4 + act/4 | |
][ | |
history: insert/only tail history act | |
] | |
] | |
undo: func [face [object!] /local act index text len][ | |
if head? history [exit] | |
loop 2 [ ; used to do 2 consecutive undos | |
history: back history | |
set [act index text len] first history | |
caret/pos: switch act [ | |
++ [remove/part at face/text index len] ; if something was inserted, now remove it | |
-- [insert at face/text index text] | |
] | |
; optimize simple changes by undoing again | |
if not all [ | |
not head? history | |
act = '++ history/-1/1 = '-- ; a change | |
history/-1/2 = index ; | |
history/-1/4 = len ; | |
][break] | |
] ; loop | |
face/draw/(face/rt/caret-color): caret/color ; momentarily show cursor (also for unfocused area) | |
unlight-text | |
face/update | |
if function? :on-change [any [attempt [on-change face reduce ['type 'key 'key at face/text index]] on-change face none] ] | |
] | |
redo: func [face [object!] /local act index text len asy][ | |
if tail? history [exit] | |
loop 2 [ ; used to do 2 consecutive redos | |
set [act index text len] first history | |
history: next history | |
caret/pos: switch act [ | |
-- [remove/part at face/text index len] | |
++ [insert at face/text index text] | |
] | |
; optimize simple changes by redoing again | |
if not all [ | |
not tail? history | |
act = '-- history/1/1 = '++ ; a change | |
history/1/2 = index ; | |
history/1/4 = len ; | |
][break] | |
] ; loop | |
face/draw/(face/rt/caret-color): caret/color ; momentarily show cursor (also for unfocused area) | |
unlight-text | |
face/update | |
if function? :on-change [any [attempt [on-change face reduce ['type 'key 'key at face/text index]] on-change face none] ] | |
] | |
; | |
;-- Text highlight functions (but, do not reshow the face): | |
hilight-text: func [face begin end][ | |
highlight-start: begin | |
highlight-end: end | |
] | |
hilight-all: func [face][ | |
either empty? face/text [unlight-text][ | |
highlight-start: head face/text | |
highlight-end: caret/pos: tail face/text | |
] | |
] | |
unlight-text: func [] [ | |
highlight-start: highlight-end: none | |
] | |
hilight?: func [] [ | |
all [ | |
;object? focal-face | |
string? highlight-start | |
string? highlight-end | |
not zero? offset? highlight-end highlight-start | |
] | |
] | |
hilight-range?: has [start end] [ | |
start: highlight-start | |
end: highlight-end | |
if negative? offset? start end [start: end end: highlight-start] | |
reduce [start end] | |
] | |
select: func [ ;@@ REDEFINED select | |
face [object!] | |
value [pair!] | |
][ | |
if any [ | |
not string? face/text | |
empty? face/text | |
][ | |
return false | |
] | |
; constrain to valid ranges | |
value/1: min max 1 value/1 1 + (length? face/text) | |
value/2: min max 0 value/2 1 + (length? face/text) - value/1 | |
unlight-text | |
hilight-text face (at face/text value/1) (caret/pos: at face/text value/1 + value/2) | |
face/update | |
value | |
] | |
draw-hilight: function [face [object!]][ | |
start: index? any [highlight-start ""] | |
end: index? any [highlight-end ""] | |
if start > end [tmp: start start: end end: tmp] | |
if start = 0 [start: caret/index] | |
face/rt/data/(face/rt/selection): as-pair start end - start | |
if face/selected <> face/rt/data/(face/rt/selection) [ | |
set-quiet in face 'selected face/rt/data/(face/rt/selection) ;@@ use set-quiet to avoid too many reactions | |
] | |
;if not system/view/auto-sync? [show face] | |
] | |
; | |
;-- Copy and delete functions: | |
copy-selected-text: func [face /local start end][ | |
if all [ | |
hilight? | |
;not flag-face? face hide | |
][ | |
set [start end] hilight-range? | |
attempt [write-clipboard copy/part start end] | |
true | |
] ; else return none | |
] | |
copy-text: func [face] [ | |
if not copy-selected-text face [ ; copy all if none selected (!!! should be line) | |
hilight-all face | |
copy-selected-text face | |
] ; else return none | |
] | |
delete-selected-text: func [/local face start end res][ | |
if hilight? [ | |
set [start end] hilight-range? | |
;if flag-face? face hide [remove/part at face/text index? start offset? start end] | |
add_to_history reduce ['-- index? start copy/part start end offset? start end] | |
remove/part start end | |
caret/pos: start | |
unlight-text | |
true | |
] ; else return none | |
] | |
; | |
;-- Cursor movement: | |
word-limits: use [cs][ | |
cs: charset " ^-^/^m/[](){}^"" | |
reduce [cs complement cs] | |
] | |
; next-word: func [str /local s ns] [ | |
; set [s ns] word-limits | |
; any [all [s: find str s find s ns] tail str] | |
; ] | |
; back-word: func [str /local s ns] [ | |
; set [s ns] word-limits | |
; any [all [ns: find/reverse back str ns ns: find/reverse ns s next ns] head str] | |
; ] | |
words-limits: make bitset! {[]^{^}()-^"^/} ;@@ this is different from word-limits | |
space: make bitset! " ^-" | |
;non-space: complement union words-limits space | |
non-spaces: union words-limits space | |
non-space: complement non-spaces | |
none ;@@ workaround for #5488 | |
skip-non-space: func [str inc] [while [find non-space any [str/:inc #" "]][str: skip str inc] str] | |
skip-space: func [str inc] [while [find space any [str/:inc #"a"]][str: skip str inc] str] | |
skip-words-limits: func [str inc] [while [find words-limits any [str/:inc #"a"]][str: skip str inc] str] | |
next-word: func [str /local pos] [ | |
pos: str | |
if pos = str: skip-space skip-non-space str 1 1 [str: skip-space skip-words-limits str 1 0] | |
str | |
] | |
back-word: func [str /local pos] [ | |
pos: str | |
if pos = str: skip-non-space skip-space str -1 -1 [str: skip-words-limits str -1] | |
str | |
] | |
end-of-line: func [str /local nstr] [ ;returns at newline | |
any [find str newline tail str] | |
] | |
beg-of-line: func [str /local nstr] [ ;returns just after newline | |
any [find/reverse/tail str newline head str] | |
] | |
move: func [event ctrl plain] [ | |
; Deal with cursor movement, including special shift and control cases. | |
either event/shift? [any [highlight-start highlight-start: caret/pos]][unlight-text] | |
caret/pos: either event/ctrl? ctrl plain | |
if event/shift? [either caret/pos = highlight-start [unlight-text][highlight-end: caret/pos]] | |
] | |
move-y: func [face delta /local pos][ | |
; Move up or down a number of lines. | |
; use caret line offset and sub-rich-text offset | |
pos: face/draw/(face/rt/caret-offset) + 0x1 + delta ; | |
caret/index: offset-to-caret face/rt/face pos | |
caret/pos: at face/text caret/index | |
] | |
draw-cursor: func [face event /local pos][ | |
;?? caret/pos | |
caret/index: index? any [caret/pos face/text] | |
;?? caret/index | |
caret/offset: caret-to-offset face/rt/face caret/index | |
;@@ workaround for wrapped text (para/wrap? = true) | |
if all [face/para face/para/wrap? event (caret/offset/y > (event/offset/y - face/para/scroll/y - face/margins/y))] [ | |
pos: offset-to-caret face/rt/face event/offset - face/para/scroll - face/margins | |
caret/offset: as-pair (pick caret-to-offset/lower face/rt/face pos - 1 'x) (pick caret-to-offset face/rt/face pos - 1 'y) | |
] | |
;?? caret/offset | |
;?? face/para/scroll | |
;face/draw/(face/rt/scroll): face/para/scroll ; sync para/scroll , but using reactions | |
face/draw/(face/rt/caret-offset): caret/offset | |
] | |
; | |
;-- Character handling: | |
keys-to-insert: complement charset [#"^A" - #"^(1F)" #"^(DEL)"] | |
keymap: [ ; a small table, so does not benefit from hashing | |
#"^(back)" back-char | |
#"^~" back-char ;@@ Red specific | |
#"^(tab)" tab-char | |
#"^(del)" delete | |
#"^M" enter | |
#"^A" all-text | |
#"^C" copy-text | |
#"^X" cut-text | |
#"^V" paste-text | |
#"^T" clear-tail | |
#"^D" duplicate-text | |
#"^U" case-text | |
#"^Z" undo-redo | |
] | |
none none ;@@ workaround for #5488 | |
remove-chars: func [face event ctrl plain /local tmp start end][ | |
if none? delete-selected-text [ | |
start: either event/ctrl? ctrl plain | |
end: caret/pos | |
if negative? offset? start end [tmp: start start: end end: tmp] | |
add_to_history reduce ['-- index? start copy/part start end offset? start end] | |
if function? :on-change [any [attempt [on-change face reduce ['type 'key 'key start]] on-change face none] ] | |
caret/pos: remove/part start end | |
] | |
update-scrollers face face/size size-text face/rt/face | |
caret/pos | |
] | |
insert-chars: func [face chars /no-opt /local len][ | |
delete-selected-text | |
; For password spoofed text (***), the above may put caret on wrong face. | |
; Check, and restore proper caret, otherwise we lose a character. | |
if not same? head face/text head caret/pos [caret/pos: at face/text index? caret/pos] | |
; The caret may be off the end, so just append if it is. | |
if error? try [caret/pos: insert caret/pos chars][append caret/pos chars] | |
len: either char? chars [1][length? chars] | |
add_to_history/:no-opt reduce ['++ (index? caret/pos) - len to-string chars len] | |
update-scrollers face face/size size-text face/rt/face | |
if function? :on-change [any [attempt [on-change face reduce ['type 'key 'key chars]] on-change face none] ] | |
caret/pos | |
] | |
edit-text: func [ | |
face event | |
/local key liney tmp tmp2 page-up page-down face-size pos | |
][ | |
key: event/key | |
;?? key | |
;probedo [event/shift? event/ctrl? mold key ] | |
;-- Compute edge and face sizes (less the edge): | |
face-size: face/size - face/scrollers-size | |
;-- Fetch the vertical line: | |
liney: face/actors/caret/height | |
;-- Most keys insert into the text, others convert to words: | |
if char? key [ | |
either find keys-to-insert key [ | |
insert-chars face key | |
][ | |
key: system/words/select keymap key | |
] | |
] | |
;-- Key action handling: | |
if word? key [ | |
page-up: [move-y face face-size - liney - liney * 0x-1] | |
page-down: [move-y face face-size - liney * 0x1] | |
; Most frequent keys are first: | |
do system/words/select [ | |
back-char [remove-chars face event [back-word caret/pos] [back caret/pos]] | |
delete [remove-chars face event [next-word caret/pos] [next caret/pos]] | |
left [move event [back-word caret/pos][back caret/pos]] | |
right [move event [next-word caret/pos][next caret/pos]] | |
up [move event page-up [move-y face liney * 0x-1]] | |
down [move event page-down [move-y face liney * 0x1]] | |
page-up [move event [head caret/pos] page-up] | |
page-down [move event [tail caret/pos] page-down] | |
home [move event [head caret/pos][beg-of-line caret/pos]] | |
end [move event [tail caret/pos][end-of-line caret/pos]] | |
enter [insert-chars face newline] | |
copy-text [copy-text face unlight-text] | |
cut-text [copy-text face delete-selected-text] | |
paste-text [ | |
tmp: read-clipboard | |
if string? tmp [insert-chars/no-opt face tmp] | |
] | |
clear-tail [remove-chars face event [end-of-line caret/pos] [end-of-line caret/pos]] | |
all-text [hilight-all face] | |
tab-char [ | |
case [ | |
all [event/ctrl? event/shift?] [ | |
set-focus tmp: get-focusable/back find face/parent/pane face | |
] | |
event/ctrl? [ | |
set-focus tmp: get-focusable next find face/parent/pane face | |
] | |
'else [ | |
either hilight? [ | |
; tab indentation | |
add_to_history reduce ['-- (index? highlight-start) (copy/part highlight-start highlight-end) (offset? highlight-start highlight-end)] | |
face/selected: either event/shift? [ | |
outdent face/text face/selected | |
][ | |
indent face/text face/selected | |
] | |
add_to_history reduce ['++ (face/selected/1) (copy/part at face/text face/selected/1 face/selected/2) (face/selected/2)] | |
][ | |
insert-chars face tab | |
] | |
] | |
] | |
] | |
duplicate-text [ | |
either hilight? [ | |
set [tmp tmp2] hilight-range? | |
unlight-text ; avoid deleting selection | |
insert-chars/no-opt face copy/part tmp tmp2 | |
; re-highlight | |
caret/pos: tmp2 | |
hilight-text face tmp tmp2 | |
][ | |
pos: caret/pos ; store caret position | |
tmp: beg-of-line caret/pos | |
tmp2: caret/pos: next end-of-line caret/pos | |
if (first back tmp2) <> newline [insert-chars face newline] ; if we are at the end add also a newline | |
insert-chars face copy/part tmp tmp2 | |
; reposition caret | |
caret/pos: pos | |
] | |
] | |
case-text [ | |
if hilight? [ | |
tmp: copy/part highlight-start highlight-end | |
insert-chars face either event/shift? [uppercase tmp][lowercase tmp] | |
] | |
] | |
undo-redo [either event/shift? [redo face][undo face]] | |
] key | |
] | |
draw-cursor face none ;@@ used to calc caret/offset for scroll-to-caret | |
;-- Scroll the face to keep caret visible? | |
scroll-to-caret face | |
draw-hilight face | |
] | |
; | |
; scroll, scrollers and resize | |
scroll-to: function [face [object!] pos [integer! string!]][ | |
;TBD | |
] | |
scroll-to-caret: func [face /local face-size liney tmp tmp2 scroll][ | |
face-size: face/rt/face/size | |
;-- Fetch the vertical line: | |
liney: face/actors/caret/height | |
; store old value | |
scroll: face/para/scroll | |
tmp2: caret/offset - absolute scroll | |
tmp: face-size - tmp2 - (caret/width * 1x0) | |
;-- Scroll right if off left side, or left if off right side: | |
if tmp/x < 0 [face/para/scroll/x: to-integer scroll/x + tmp/x] | |
if tmp2/x < 0 [face/para/scroll/x: to-integer scroll/x - tmp2/x] | |
; update scrollers | |
if scroll/x <> face/para/scroll/x [ON-SCROLL face compose [key track picked (to-integer absolute face/para/scroll/x) orientation horizontal] ] | |
;-- Scroll up if off bottom, or down if off top: | |
if (tmp/y - liney) < 0 [face/para/scroll/y: to-integer scroll/y + tmp/y - liney] | |
if tmp2/y < 0 [face/para/scroll/y: to-integer scroll/y - tmp2/y] | |
; update scrollers | |
if scroll/y <> face/para/scroll/y [ON-SCROLL face compose [key track picked (to-integer absolute face/para/scroll/y) orientation vertical] ] | |
] | |
scroll: function [face [object!] pos [integer! ] axis [word!]][ | |
face/para/scroll/(axis): to integer! 1 + negate pos ;@@ "1 +" because this must be 0-based | |
;face/draw/(face/rt/scroll): face/para/scroll ; using reactions | |
] | |
; must adjust both scrollers at the same time | |
update-scrollers: function [face [object!] size [pair! point2D! none!] inner-size [pair! point2D!]][ ; some parts of this function are inspired by one of Anton Rolls | |
asy: system/view/auto-sync? | |
system/view/auto-sync?: no | |
scy: scroller/y | |
scx: scroller/x | |
; scrollers widths, add 2 to make it look better and avoid cursor being hidden | |
y-size: 2 + to integer! system/view/metrics/misc/scroller/y * 96 / system/view/metrics/dpi ;@@ WHAT !!??, really ?? | |
x-size: 2 + to integer! system/view/metrics/misc/scroller/x * 96 / system/view/metrics/dpi | |
; account also for margins | |
; do it here because this function is called all over the place | |
size: size - (face/margins * 2) | |
visible-y: size/y | |
total-y: to integer! inner-size/y | |
visible-x: size/x | |
total-x: to integer! inner-size/x | |
; determine if scrollers are necessary to be shown | |
; subtract here space taken by scrollers, if necessary | |
if scy-visible?: total-y > visible-y [ | |
; adding a vertical scroller changes horizontal visible size | |
visible-x: size/x - y-size | |
] | |
if scx-visible?: total-x > visible-x [ | |
; adding a horizontal scroller changes vertical visible size | |
visible-y: size/y - x-size | |
] | |
; adding the horizontal scroller might make the vertical scroller necessary | |
if all [not scy-visible? scy-visible?: total-y > visible-y ] [ | |
; adding a vertical scroller changes horizontal visible size | |
visible-x: size/x - y-size | |
] | |
;scy/max-size: 0 ;@@ workaround to avoid scroller to become disabled ! | |
scy/page-size: 0 | |
scy/page-size: to integer! visible-y | |
;scy/visible?: none ;@@ workaround | |
scy/visible?: scy-visible? | |
scy/max-size: to integer! total-y ; @@ ...must place this here | |
scy/min-size: to integer! visible-y / 10 ; FIXME hardcoded value | |
; constrain to allowed range | |
scy/position: to integer! min max 1 scy/position (total-y - visible-y) | |
;scx/max-size: 0 ;@@ workaround to avoid scroller to become disbled ! or ... | |
scx/page-size: 0 | |
scx/page-size: to integer! visible-x | |
;scx/visible?: none ;@@ workaround | |
scx/visible?: scx-visible? | |
scx/max-size: to integer! total-x + 1 ;@@... this MUST be placed after "visible?". @@ + 1 only for 144 dpi but beware that /visible? is setted also by Red ! | |
scx/min-size: to integer! visible-x / 10 | |
; constrain to allowed range | |
scx/position: to integer! min max 1 scx/position (total-x - visible-x) | |
; scroll to keep maximum possible visibility | |
face/actors/scroll face to integer! max 1 min scy/position absolute (total-y - visible-y) 'y | |
face/actors/scroll face to integer! max 1 min scx/position absolute (total-x - visible-x) 'x | |
face/scrollers-size: as-pair any [all [scy-visible? y-size] 0] any [all [scx-visible? x-size] 0] ;@@ note REVERSED axes ! | |
face/rt/face/size: size - face/scrollers-size ;@@ margins already subtracted | |
;show face | |
system/view/auto-sync?: asy | |
] | |
resize: function [face [object!] size [pair! point2D! none!]][ | |
;print ["resize" size] | |
update-scrollers face size (size-text face/rt/face) + caret/width | |
; if wrapped text, move cursor to nearest text | |
if face/para/wrap? [ | |
caret/index: offset-to-caret face/rt/face (caret/offset - face/para/scroll - face/margins) | |
caret/pos: at face/text caret/index | |
draw-cursor face none ; used also to calc caret/offset | |
] | |
face/draw/(face/rt/edge): size | |
face/draw/(face/rt/clip): size - face/margins - face/scrollers-size + 2x0 | |
size ; IMPORTANT for reactions ! | |
] | |
; | |
created: false | |
none none ;@@ workaround for #5488 | |
ON-CREATE: func [face][ | |
;print "ON-CREATE" | |
append face/options [cursor: I-beam] | |
;face/draw/(face/rt/text)/para/wrap?: face/para/wrap? | |
face/rt/face/para/wrap?: face/para/wrap? | |
face/draw/(face/rt/caret-height): face/actors/caret/height: rich-text/line-height? face/rt/face 1 | |
face/actors/caret/pos: head face/text | |
scroller/x: get-scroller face 'horizontal | |
scroller/y: get-scroller face 'vertical | |
update-scrollers face face/size size-text face/rt/face | |
;?? face | |
face/actors/created: true | |
if flag? face 'read-only [face/draw/(face/rt/caret-color): caret/color: 'off] | |
face/rate: if not flag? face 'read-only [caret/blinking] | |
] | |
; ON-FOCUS: func [face event] [print "focus" system/view/capturing?: yes] | |
; ON-UNFOCUS: func [face event] [print "UNfocus" system/view/capturing?: no] | |
; ON-DETECT: func [face event] [?? event/type if event/type = 'key [?? event/key 'stop]] ; make this area read-only | |
ON-KEY-DOWN: func [face event][ | |
;prin "Kdown " ?? event/key | |
if flag? face 'read-only [exit] | |
;@@ many workarounds | |
if all[ event/key = #"^(tab)" event/ctrl?] [edit-text face event] ; FIXME: change handler | |
if all[ event/key = #"Z" event/shift? event/ctrl?] [ON-KEY face compose [key #"^Z" shift? (true) ctrl? (true)]] | |
if all[ event/key = #"U" event/shift? event/ctrl?] [ON-KEY face compose [key #"^U" shift? (true) ctrl? (true)]] | |
] | |
ON-KEY: func [face event][ | |
;?? event/key | |
if flag? face 'read-only [ | |
if event/key = #"^C" [edit-text face [key copy-text]] | |
exit | |
] | |
;face/rate: none | |
face/draw/(face/rt/caret-color): caret/color | |
edit-text face event | |
;face/rate: caret/blinking | |
] | |
ON-DOWN: func [face event /local tmp][ | |
;print "DOWN " | |
set-flag face 'all-over | |
;set-flag/clear face 'focusable | |
set-focus face | |
;face/rate: caret/blinking | |
face/down?: true ;@@ workaround for #5475 | |
; show immediatly the cursor | |
face/draw/(face/rt/caret-color): caret/color | |
either event/shift? [ | |
ON-OVER face event | |
][ | |
unlight-text ; FIXME: if not focused | |
] | |
;?? event/offset | |
caret/index: offset-to-caret face/rt/face (event/offset - face/para/scroll - face/margins) | |
caret/pos: at face/text caret/index | |
draw-cursor face event | |
draw-hilight face | |
scroll-to-caret face | |
; face/update | |
;none | |
] | |
ON-OVER: func [face event /local tmp][ | |
;prin "over " ?? event/flags | |
if not event/down? [return 'done] | |
if not face/down? [return 'done] ;@@ workaround for #5475 | |
if away? face event [;@@ workaround for #5472 | |
; handle scrolling of area while selecting text. | |
scroll-to-caret face | |
] | |
; handle selection with mouse | |
tmp: offset-to-caret face/rt/face (event/offset - face/para/scroll - face/margins) | |
if not-equal? caret/index tmp [ | |
if not highlight-start [highlight-start: caret/pos] | |
highlight-end: caret/pos: at face/text caret/index: tmp | |
draw-cursor face event | |
draw-hilight face | |
;show face | |
] | |
] | |
ON-UP: func [face event][ | |
set-flag/clear face 'all-over | |
face/down?: false ;@@ workaround for #5475 | |
; stop scrolling if needed | |
;face/rate: none; show face] | |
] | |
ON-DBL-CLICK: func [face event][ | |
; select word | |
;edit-text face compose [key right shift? (false) ctrl? (false)] | |
edit-text face compose [key left shift? (false) ctrl? (true)] | |
edit-text face compose [key right shift? (true) ctrl? (true)] | |
] | |
ON-SCROLL: func [face [object!] event [event! none! block!] /local axis scr pos][ ; function "layout" inspired by @cosacam1 version | |
;print " scroll" | |
axis: pick [y x] (any [event/orientation 'vertical]) = 'vertical | |
; invert orientation if Ctrl is pressed and ...(...IDK!) , this is wrong FIXME | |
; if all [axis = 'y any [not scroller/y/visible? event/ctrl?]] [axis: 'x] ; FIXME: better use shift ? | |
scr: scroller/(axis) | |
pos: scr/position | |
scr/position: min max 1 switch event/key [ | |
up left [pos - scr/min-size] | |
down right [pos + scr/min-size] | |
page-up page-left [pos - scr/page-size] | |
page-down page-right [pos + scr/page-size] | |
track [event/picked] | |
wheel [pos - (scr/min-size * to integer! event/picked)] ; forwarded event by on-wheel | |
end [pos] | |
] (scr/max-size - scr/page-size + 1) | |
scroll face scr/position axis ; use an overwritable function | |
;if not system/view/auto-sync? [show face] | |
] | |
ON-WHEEL: function [face [object!] event [event! none!]][;May-be switch shift and ctrl ? | |
if scroller/y/visible? [ | |
ON-SCROLL face event ; forward | |
] | |
] | |
ON-MENU: func [face event][ | |
edit-text face compose system/words/select [ | |
menu-copy [key #"^C" shift? (false) ctrl? (true)] | |
menu-cut [key #"^X" shift? (false) ctrl? (true)] | |
menu-paste [key #"^V" shift? (false) ctrl? (true)] | |
menu-undo [key #"^Z" shift? (false) ctrl? (true)] | |
menu-redo [key #"^Z" shift? (true) ctrl? (true)] | |
menu-select-all [key #"^A" shift? (false) ctrl? (true)] | |
menu-duplicate [key #"^D" shift? (false) ctrl? (true)] | |
menu-lowercase [key #"^U" shift? (false) ctrl? (true)] | |
menu-uppercase [key #"^U" shift? (true) ctrl? (true)] | |
menu-indent [key tab-char shift? (false) ctrl? (false)] | |
menu-outdent [key tab-char shift? (true) ctrl? (false)] | |
] event/picked | |
] | |
ON-TIME: func [face event][ | |
either focused? face [ | |
;face/rate: caret/blinking | |
face/draw/(face/rt/caret-color): either 0 = caret/visible: 1 - caret/visible [glass][caret/color] | |
][ | |
face/draw/(face/rt/caret-color): glass | |
;face/rate: none | |
] | |
;if not system/view/auto-sync? [show face] | |
] | |
ON-CHANGE: func [face event][ | |
;placeholder | |
] | |
] | |
old-on-change*: :on-change* | |
on-change*: func [word old new][ | |
old-on-change* word :old :new | |
switch to word! word [ | |
text [ | |
us/draw/(us/rt/text)/text: new | |
;try [actors/on-change us none] | |
if function? :actors/on-change [actors/on-change us none] | |
] | |
size [ | |
;?? new | |
if actors/created [ ;@@ LATER flag | |
;set-quiet word (probe actors/resize us new);- old | |
actors/resize us new | |
] | |
] | |
tabs [set-quiet in us/draw/(rt/text) 'tabs new] | |
; line-spacing [ | |
; set-quiet in us/draw/(rt/text) 'line-spacing new | |
; probedo [ | |
; us/draw/(rt/caret-height): actors/caret/height: rich-text/line-height? us/draw/(rt/text) 1 | |
; ] | |
; ] ; FIXME: uncomment when fixed | |
selected [select new] | |
] | |
] | |
] | |
init: [ | |
;print "INIT" | |
; add this font object "LATER" | |
font: make any [font font!] [ | |
; size: ? 9 ; | |
; name ? W11 area seem to not use Segoe UI. | |
; color: glass ; make "main" base text invisible | |
real-color: black | |
on-change*: func [word old new][ | |
; if not function? :new [prin "FC " ?? word ?? new ?? us/text] | |
if us/text = none [try [us/text: clear us/draw/(us/rt/text)/text]] ;@@ workaround to sync reactions and strings | |
switch word [ | |
size [ | |
if created [ ;@@ LATER flag | |
us/draw/(rt/text)/data/(rt/font-size): new | |
] | |
;try [set-quiet us/draw/(rt/caret-height): rich-text/line-height? us 1] ; FIXME: wrong assignment | |
if object? us/actors/caret [set-quiet in us/actors/caret 'height us/draw/(rt/caret-height)] | |
] | |
color [ | |
;set-quiet us/draw/(rt/color) new | |
;set-quiet us/draw/(rt/text)/font/color new | |
;set-quiet in us/draw/(rt/text) data/(rt/font-color) new | |
;if created [ ;@@ LATER flag | |
;us/draw/(rt/text)/data/(rt/font-color): new | |
set-quiet 'real-color new | |
set-quiet word glass ; keep "main" text invisible | |
;] | |
] | |
name [ | |
if object? us/draw/(rt/text)/font [set-quiet in us/draw/(rt/text)/font 'name new] | |
] | |
] | |
] | |
] | |
font/size: any [font/size 9] ;@@ must give a default value, but give it only if not already given | |
font/real-color: any [font/color black] ; copy and store original color or give a default | |
font/color: any [font/color black] | |
created: flags ; store flags | |
values: make values any [options []] | |
values/flags: to-block values/flags | |
set self values | |
flags: created ; restore flags | |
;options: union trim to block! options [style: area-rt] | |
para/scroll: (0, 0) ;@@ ?? must re-set because in previouse Red versions this is set to none somehow | |
rt/face: draw/(rt/text) | |
rt/data: rt/face/data | |
insert rt/data [1x-1 10 255.0.255] | |
rt/data/(rt/selection): 1x0 | |
draw/(rt/caret-height): actors/caret/height: rich-text/line-height? rt/face 1 | |
draw/(rt/caret-width): actors/caret/width: 1 ; FIXME: hardcoded | |
draw/(rt/clip): size - margins + actors/caret/width | |
draw/(rt/edge): size | |
; add a default para! | |
rt/face/para: make para! [wrap?: false] | |
; add a default font! | |
rt/face/font: make any [font font!] [] | |
; sync font size | |
rt/data/(rt/font-size): rt/face/font/size | |
; possibly re-hide main face's color | |
if font/color <> glass [font/color: glass] | |
rt/data/(rt/font-color): font/real-color | |
;@hiiamboris workaround (kludge ;) ) to make tab key work inside face | |
; insert-event-func 'tab func [face event] also compose/deep [ | |
; if face/type <> 'rich-text [(:system/view/handlers/tab) face event] | |
; ] remove-event-func 'tab | |
old-handler: :system/view/handlers/tab | |
put system/view/handlers 'tab func [face event] [ | |
if attempt [face/options/style <> 'area-rt] [old-handler face event] ;@@ beware this lit-word! does not match for a custom style! | |
] | |
] | |
] | |
] ; ctx | |
do | |
[ | |
if any [%area-rt.red = find/last/tail system/options/script "/" ; It's really me ? | |
;if any [system/script/title = none ; It's really me ? | |
system/script/args = "test"] [ | |
;prin "" ; open console for debug | |
;system/view/auto-sync?: no | |
system/view/VID/styles/text: [template: [type: 'text size: 0x0]] | |
win: layout compose/deep [ | |
title "Text area with multiple undos" ;@@ I wish I could do : title (system/script/header/title) | |
space 4x4 | |
button "Add text" [a+/insert-text "ABCDEFGH" 0] | |
button "Undo" [a+/undo] | |
button "Redo" [a+/redo] | |
return | |
a+: area-rt 400x200 focus ;font-name system/view/fonts/fixed ;yellow red font-size 20 with [tabs: 40 line-spacing: 30] | |
{if 'this-is-a-test [ | |
try any [ | |
Selection + <Tab> | |
Selection + <Shift+Tab> | |
] | |
to indent! (or~ outdent! Selection) | |
] | |
also try [context-menu for many other [shortcuts!]] + all [classic ones] | |
} | |
] | |
react/later compose [ | |
a+/size: win/size - (win/size - a+/size) | |
if not system/view/auto-sync? [show win] | |
] | |
view/flags win 'resize | |
] ; if | |
] ; do |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Red [ | |
title: "Multi column text list" | |
author: @luce80 | |
Rights: "Copyright (C) 2024 Marco Antoniazzi. All rights reserved." | |
License: BSL-1 | |
file: %multi-text-list.red | |
gist-view: https://gist.githubusercontent.com/luce80//433286c66d98997aff6e69fbd6323a35#file-multi-text-list-red | |
date: 02-01-2024 | |
version: 0.0.0 | |
history: [ | |
0.0.0 [22-01-2023 "Started"] | |
0.0.1 [28-01-2023 "Using textmin"] | |
0.0.2 [29-01-2023 "scrolling"] | |
0.0.3 [04-02-2023 "column size and move"] | |
0.0.4 [02-01-2024 "adapted to new point2D! datatype and `init` binding"] | |
] | |
Notes: { | |
This is a VID style to make it easier to enter numbers. | |
To initialize the main value use a string or `data`. | |
You can set some parameters by using `options` VID keyword. | |
The currently available parameters are: | |
- | |
Since this is a `panel` you can not use a `block!` to have a default action, use `on-enter` and/or `on-click` instead. | |
Since this is a `panel` you can not use a `number!` to specify the size, you must use a `pair!` instead. | |
Since this is a `panel` you can not give focus to it, you must use e.g. `set-focus my-spinner/field` instead. | |
See at bottom of script for a usage example. | |
} | |
] | |
{ | |
Se ordino la vista dei dati , devo ordinare anche i dati? | |
Se selezione qualche riga , cosa restituisco? Attualmente non prevedo di rendere possibile la selezione della singola cella. | |
Gli "headers", cioè i titoli delle colonne , fanno parte dei dati? Se sì come li gestisco ? | |
} | |
probedo: func [code [block!] /local result][print [mold code mold/only result: reduce code] do :result] | |
system/view/VID/styles/textmin: [ ; FIXME: multi-text-list-cell | |
default-actor: on-click | |
template: [ | |
type: 'text | |
size: 0x0 | |
color: white | |
para: make para! [wrap?: false] | |
extra: object [row: 0] | |
actors: [ | |
on-over: func [face event] [ | |
;face/color: pick [255.255.255 255.0.0 ] event/away? if not system/view/auto-sync? [show face]] | |
face/parent/parent/set 'highlighted event/away? face/extra/row | |
] | |
] | |
] | |
] | |
system/view/VID/styles/multi-text-list: [ | |
default-actor: on-click | |
template: [ | |
type: 'panel | |
size: 100x100 | |
data: [[""]] | |
flags: [scrollable] | |
children-span: 0x0 ; -- ALL this fields are reacting targets !? | |
columns: 1 | |
rows: 1 | |
row-height: 0 | |
visible-columns: 1 | |
visible-rows: 1 | |
max-visible-rows: 1 | |
widths: copy [0] | |
offsets: copy [] | |
cols: copy [] | |
old-cols: copy [] | |
dest-col-x: 0 | |
headers: none | |
headers-height: 0 | |
space: 4x0 ; space between columns and rows | |
data-copy: none | |
draw-arrow: [ | |
;pen 0.0.0 fill-pen off ;line-wodth 10 | |
;trans: [translate 0x0] scal: scale 1 1 [line -4x2 0x-2 4x2] | |
circle 10x10 10 | |
] | |
;flags: copy [] ; reserved by Red | |
old-text: none | |
this: none ; self | |
field: none ; shortcut | |
old-size: 0x0 ; used to resize incrementally | |
values: object [ ; object used to extend VID params and set main control params | |
flags: copy [] ; | |
colors: object [ | |
back-even: white | |
back-odd: white | |
back-headers: silver | |
text: black | |
select: cyan | |
; highlight: select + back / 2 ; or select + 0.0.0.128 | |
; high-select: select + (255 - back) / 2 | |
] | |
] | |
get: func [attribute [word!] spec][actors/get self attribute spec] | |
set: func [attribute [word!] spec value][actors/set self attribute spec value] | |
actors: [ | |
scroller: make map! 2 | |
; scroller fields | |
; position: 1 ;@@ a better name could be "scroll" or "amount" or "value" or "data" | |
; page-size: none ;@@ a better name could be "page" or "visible-part" | |
; min-size: 1 ; ?? . Used for step scrolling | |
; max-size: 1 ;@@ a better name could be "total" | |
; visible?: true | |
; vertical?: true | |
; parent: none | |
; page: 1 ; ?? | |
span?: func [ | |
"Returns a block of [min-pos max-size] bounds for all faces" | |
root [object!] | |
/part count [integer!] "Limit the number of faces" | |
/local origin margin face | |
][ | |
origin: 100000x100000 | |
margin: 0x0 | |
foreach face root/pane [ | |
if all [count negative? count: count - 1] [break] | |
origin: min origin face/offset | |
margin: max margin face/offset + face/size | |
] | |
reduce [origin margin - origin] | |
] | |
flag?: [ ; will be transformed to a function! | |
to logic! find values/flags flag | |
] | |
get: function [face [object!] attribute [word!] spec][ | |
; columns-order | |
; columns-widths | |
switch/default attribute [ | |
column-at-offset [at-offset-column face spec] | |
column-index [index-column face spec] | |
column-offset [face/pane/(index-column face spec)/offset/x] | |
column-right [right-column face spec] | |
column-width [to integer! face/pane/(index-column face spec)/size/x] | |
] [do make error! append copy "Unknown multi-column-text-list attribute: " attribute] | |
] | |
set: function [face [object!] attribute [word!] spec value][ | |
; columns-order | |
; columns-widths | |
switch/default attribute [ | |
column-positions [re-pos-columns face spec value] | |
column-move [move-column face spec value] | |
column-move-all [probedo [spec value] move-all-column face spec value] | |
column-order [order-column face spec value] | |
column-width [width-column face spec value] | |
column-width-all [width-all-column face spec value] | |
highlighted [highlight face spec value] | |
sort [sorting face spec value] | |
] [do make error! append copy "Unknown multi-column-text-list attribute: " attribute] | |
] | |
; move-column: set at get 'columns-order n at get 'columns-order m | |
; size-column: change at get 'columns-widths n width | |
; hide-column: remove at get 'columns-order n | |
highlight: func [face [object!] state [logic!] row [integer!] /local asy][ | |
asy: system/view/auto-sync? | |
system/view/auto-sync?: no | |
if row > face/rows [exit] | |
row: max 1 row | |
repeat c face/columns [ | |
face/pane/(c)/pane/(row)/color: pick [255.255.255 255.0.0 ] state | |
show face/pane/(c)/pane/(row) | |
] | |
system/view/auto-sync?: asy | |
] | |
index-column: func ["Returns column of given ID" face [object!] which [integer!] /local c][ | |
which: min max 1 which face/columns | |
; find header with given ID | |
repeat c face/columns [ | |
if face/pane/(face/columns + c)/extra/column = which [which: c break] | |
] | |
which | |
] | |
at-offset-column: func ["Returns column at given x offset" face [object!] spec [block!] /local asy pane pos-x min-x offset-x c distance offsets source-x dir][ | |
pane: face/pane | |
idx: spec/1 | |
pos-x: spec/2 | |
min-x: face/children-span/1/x | |
max-x: face/children-span/1/x + face/children-span/2/x | |
;probedo [idx pos-x min-x max-x] | |
res: none | |
case [ | |
;pos-x < min-x [res: 1] | |
;pos-x > max-x [res: face/columns] | |
'else [ | |
repeat c face/columns [ | |
;?? c | |
if all [ | |
pos-x >= (pane/(c)/offset/x); | |
pos-x <= (pane/(c)/offset/x + pane/(c)/size/x + (face/space/x)); | |
idx <> pane/(face/columns + c)/extra/column | |
] | |
[ | |
;probedo ["in" c] | |
res: pane/(face/columns + c)/extra/column | |
break | |
] | |
] | |
] | |
] | |
; probedo [ | |
; face/cols | |
; index? find face/cols idx | |
; res | |
; ] | |
res | |
] | |
re-pos-columns: func ["Repositions columns" face [object!] src [integer!] dst [integer!] /local asy pane pos-x min-x offset-x c distance offsets source-x dir][ | |
;probedo [src dst] | |
face/cols: copy face/old-cols | |
;probedo [src dst index? find face/cols src index? find face/cols dst] | |
move/part find face/cols src find face/cols dst 1 | |
;probedo [src dst face/cols] | |
;exit | |
old-offset: face/children-span/1/x ; min left x position | |
offset: old-offset ; | |
;if offset = 0 [exit] | |
foreach idx face/cols [ | |
c: index-column face idx | |
old-size-x: face/pane/(c)/size/x | |
either src <> face/pane/(face/columns + c)/extra/column [ | |
; place columns | |
face/pane/(c)/offset/x: offset | |
; place col-headers | |
c: c + face/columns | |
face/pane/(c)/offset/x: offset | |
; face/pane/(face/columns + c)/offset/x: face/pane/(face/columns + c)/offset/x - offset | |
; place col-draggers | |
c: c + face/columns | |
face/pane/(c)/offset/x: offset + old-size-x - (face/pane/(c)/size/x / 2) | |
; face/pane/(face/columns + face/columns + c)/offset/x: face/pane/(face/columns + face/columns + c)/offset/x - offset | |
][ | |
;probedo [face/pane/(c)/offset/x offset] | |
face/dest-col-x: offset | |
] | |
offset: offset + old-size-x + face/space/x | |
] | |
;face/old-cols: face/cols | |
res | |
] | |
right-column: func ["Returns column right of given one" face [object!] curr-face [object!] /local asy offset-x c][ | |
offset-x: curr-face/offset/x + curr-face/size/x + face/space/x | |
; find column face right of indicated offset and return its ID stored in header /extra | |
repeat c face/columns [ | |
if face/pane/(c)/offset/x >= offset-x [return face/pane/(face/columns + c)/extra/column] | |
] | |
none | |
] | |
order-column: func ["Changes columns order" face [object!] which [integer!] order [word!]/local c new-which][ | |
new-which: index-column face which | |
; pop to top | |
; pop column | |
move/part at face/pane new-which at face/pane face/columns 1 | |
; pop col-header | |
move/part at face/pane (face/columns + new-which) at face/pane (face/columns + face/columns) 1 | |
; pop col-dragger | |
move/part at face/pane (face/columns + face/columns + new-which) at face/pane (face/columns + face/columns + face/columns) 1 | |
;probedo [new-which which: index-column face which] | |
;probedo [which: face/columns] | |
; pop order | |
;move/part find face/cols which tail face/cols 1 | |
;probedo [face/cols] | |
] | |
move-column: func ["Moves a column and its dragger" face [object!] which [integer!] pos-x [integer! float!] /no-show /local asy col-dragger col c r][ | |
asy: system/view/auto-sync? | |
system/view/auto-sync?: no | |
; find header with given ID | |
which: index-column face which | |
;if not system/view/auto-sync? [show face] | |
;?? pos-x | |
face/pane/(which)/offset/x: pos-x | |
;dump-face face | |
; move col-dragger | |
col-dragger: face/pane/(face/columns + face/columns + which) | |
;col-dragger/offset/x: (face/get 'column-width which) + pos-x - (col-dragger/size/x / 2) | |
col-dragger/offset/x: pos-x + face/pane/(which)/size/x - (col-dragger/size/x / 2) | |
unless no-show [show face] | |
system/view/auto-sync?: asy | |
] | |
move-all-column: func ["Moves a column, its dragger and its header" face [object!] which [integer!] pos-x [integer! float!]/local asy old-width col c r][ | |
;where: first at-offset-column face reduce [face pos-x] | |
;probedo [attempt [where/offset/x]] | |
move-column/no-show face which pos-x | |
which: index-column face which | |
; move col-header | |
face/pane/(face/columns + which)/offset/x: pos-x | |
if not system/view/auto-sync? [show face] | |
] | |
width-column: func [face [object!] which [integer!] value [integer!] /no-show /local asy old-width col c r span][ | |
asy: system/view/auto-sync? | |
system/view/auto-sync?: no | |
which: index-column face which | |
value: to integer! min max 4 value 100000 ; FIXME: better this ? system/view/screens/1/size/x - 20 | |
; resize column's header | |
face/pane/(face/columns + which)/size/x: value | |
col: face/pane/(which) | |
old-width: col/size/x | |
; resize column's panel | |
col/size/x: value | |
; resize rows's panel | |
;col/pane/1/size/x: value | |
; resize column's rows | |
repeat r face/max-visible-rows [ | |
col/pane/(r)/size/x: value | |
] | |
; move others | |
value: old-width - value | |
repeat c face/columns [ | |
if face/pane/(c)/offset/x > face/pane/(which)/offset/x [ | |
; move right columns | |
face/pane/(c)/offset/x: face/pane/(c)/offset/x - value | |
; move col-headers | |
face/pane/(face/columns + c)/offset/x: face/pane/(face/columns + c)/offset/x - value | |
; move col-draggers | |
face/pane/(face/columns + face/columns + c)/offset/x: face/pane/(face/columns + face/columns + c)/offset/x - value | |
] | |
] | |
face/children-span: second span: span? face | |
if span/1/x > 0 [do make error! "MCTL wrong layout, too right"] | |
;probedo [scroller/x/position] | |
if scroller/x [ | |
;update-scrollers face as-pair (face/size/x) (face/size/y - face/headers-height) as-pair (face/children-span/x) (face/row-height * face/rows) ; this makes GUI flicker a lot :( | |
update-scrollers face as-pair (face/size/x) (min (face/size/y - face/headers-height) (face/row-height * face/max-visible-rows - 1)) as-pair (face/children-span/x) (face/row-height * face/rows) | |
] | |
unless no-show [show face] | |
system/view/auto-sync?: asy | |
] | |
width-all-column: func [face [object!] which [integer!] value [integer!] /local asy old-width col c r span][ | |
which: index-column face which | |
old-width: face/pane/(which)/size/x | |
width-column/no-show face which value | |
value: old-width - value | |
; move col-dragger | |
face/pane/(face/columns + face/columns + which)/offset/x: face/pane/(face/columns + face/columns + which)/offset/x - value | |
show face | |
] | |
comp: context [ | |
col: 1 | |
compare-row: func [ | |
a | |
b | |
][ | |
any [ | |
attempt [ | |
a/(col) < b/(col) | |
] | |
(form a/(col)) < (form b/(col)) | |
] | |
] | |
] | |
sorting: func [face [object!] which [integer!] value [integer! word! none!] /local asy old-width col c r span][ | |
;bisecting-sort face/data-copy which | |
comp/col: which | |
sort/stable/compare face/data-copy :comp/compare-row | |
;?? value | |
if any [value = 'z-a value = -1] [reverse face/data-copy] | |
scroll face scroller/y/position 'y ; refresh | |
show face | |
] | |
mini: func [face [object!] start [integer!] r [integer!] c [integer!] /local n minimum][ | |
face/data-copy/(start + r)/(c) | |
; minimum: face/data/(1)/(c) | |
; repeat n start + r - 1 [ | |
; ;probedo [n start r c minimum face/data/(n + 1)/(c) ] | |
; minimum: min minimum face/data/(n + 1)/(c) | |
; ] | |
; minimum | |
] | |
scroll-to: function [face [object!] pos [pair!]][ | |
;TBD | |
] | |
scroll: func [face [object!] value [integer!] axis [word!] /local asy offset old-offset start col c r data][ | |
asy: system/view/auto-sync? | |
system/view/auto-sync?: no | |
;if value = 1 [exit] ; no scrollers movement, but we could still have been moved ! | |
value: max 1 value | |
;probedo ["scroll" value (to-lit-word axis) face/row-height] | |
either axis = 'y [ | |
start: to integer! value - 1 / (face/row-height + 1e-6) + 1e-6 | |
offset: face/headers-height + to integer! negate modulo value - 1 face/row-height | |
repeat col face/columns [ | |
c: index-column face col | |
face/pane/(col)/offset/y: offset | |
;probedo [start face/visible-rows] | |
;probedo [face/max-visible-rows face/visible-rows] | |
repeat r min face/max-visible-rows face/visible-rows [ | |
if (start + r) > face/rows [break] | |
;?? r | |
;?? col | |
;probedo [(start + r) c r] | |
data: form face/data/(start + r)/(col) | |
;data: form mini face start r col | |
face/pane/(c)/pane/(r)/text: either (start + r) <= face/rows [data][" "] | |
] | |
] | |
][ | |
;offset: value - 1 - absolute face/pane/1/offset/x ;@@ "- 1" because this must be 0-based | |
old-offset: first first span? face ; min left x position | |
offset: old-offset + value - 1 ;@@ "- 1" because this must be 0-based | |
if offset = 0 [exit] | |
repeat c face/columns [ | |
; move columns | |
face/pane/(c)/offset/x: face/pane/(c)/offset/x - offset | |
; move col-headers | |
c: c + face/columns | |
face/pane/(c)/offset/x: face/pane/(c)/offset/x - offset | |
; face/pane/(face/columns + c)/offset/x: face/pane/(face/columns + c)/offset/x - offset | |
; move col-draggers | |
c: c + face/columns | |
face/pane/(c)/offset/x: face/pane/(c)/offset/x - offset | |
; face/pane/(face/columns + face/columns + c)/offset/x: face/pane/(face/columns + face/columns + c)/offset/x - offset | |
] | |
] | |
system/view/auto-sync?: asy | |
] | |
on-scroll: func [face [object!] event [event! none!] /local axis][ ; function "layout" inspired by @cosacam1 version | |
axis: pick [y x] (any [event/orientation 'vertical]) = 'vertical | |
if all [axis = 'y any [not scroller/y/visible? event/ctrl?]] [axis: 'x] ; FIXME: better use shift ? | |
scroller/(axis)/position: min max 1 switch event/key [ | |
up left [scroller/(axis)/position - scroller/(axis)/min-size] | |
down right [scroller/(axis)/position + scroller/(axis)/min-size] | |
page-up page-left [scroller/(axis)/position - scroller/(axis)/page-size] | |
page-down page-right [scroller/(axis)/position + scroller/(axis)/page-size] | |
track [event/picked ] | |
wheel [scroller/(axis)/position - (scroller/(axis)/min-size * to integer! event/picked)] ; forwarded event by on-wheel | |
end [scroller/(axis)/position] | |
] (scroller/(axis)/max-size - scroller/(axis)/page-size) | |
scroll face scroller/(axis)/position axis | |
if not system/view/auto-sync? [show face] | |
] | |
on-scroll: func [face [object!] event [event! none!] /local axis scr pos][ ; function "layout" inspired by @cosacam1 version | |
axis: pick [y x] (any [event/orientation 'vertical]) = 'vertical | |
if all [axis = 'y any [not scroller/y/visible? event/ctrl?]] [axis: 'x] ; FIXME: better use shift ? | |
scr: scroller/(axis) | |
pos: scr/position | |
scr/position: min max 1 switch event/key [ | |
up left [pos - scr/min-size] | |
down right [pos + scr/min-size] | |
page-up page-left [pos - scr/page-size] | |
page-down page-right [pos + scr/page-size] | |
track [event/picked] | |
wheel [pos - (scr/min-size * to integer! event/picked)] ; forwarded event by on-wheel | |
end [pos] | |
] (scr/max-size - scr/page-size) | |
scroll face scr/position axis ; use an overwritable function | |
if not system/view/auto-sync? [show face] | |
] | |
on-wheel: function [face [object!] event [event! none!]][;May-be switch shift and ctrl ? | |
if any [scroller/x/visible? scroller/y/visible?] [ | |
on-scroll face event ; forward | |
] | |
] | |
on-created: func [face [object!] event [event! none!] /locl temp][ | |
scroller/x: get-scroller face 'horizontal | |
scroller/y: get-scroller face 'vertical | |
face/children-span: second span? face | |
;face/child/offset: 0x0 ; align to top-left | |
face/data-copy: copy face/data | |
resize face face/size | |
;face/set 'column-width-all 1 50 | |
;face/set 'column-width-all 2 150 | |
face/set 'column-width-all 3 100 | |
] | |
entangle: func ["Activate reactions" face1 face2][ | |
;if flag? 'fixed [face1/values/min-child-size: face1/values/max-child-size: face2/size ] | |
face1/size: face1/actors/resize face1 face1/size | |
;if not system/view/auto-sync? [show [face1]] ; | |
] | |
; must adjust both scrollers at the same time | |
update-scrollers: function [face [object!] size [pair! none!] inner-size [pair!]][ ; some parts of this function are inspired by one of Anton Rolls | |
asy: system/view/auto-sync? | |
system/view/auto-sync?: no | |
scy: scroller/y | |
scx: scroller/x | |
y-size: to integer! system/view/metrics/misc/scroller/y * 96 / system/view/metrics/dpi ;@@ WHAT !!??, really ?? | |
x-size: to integer! system/view/metrics/misc/scroller/x * 96 / system/view/metrics/dpi | |
;face/child/size: min max face/values/min-child-size size face/values/max-child-size | |
visible-y: size/y | |
total-y: inner-size/y | |
visible-x: size/x | |
total-x: inner-size/x | |
; determine if scrollers are necessary to be shown | |
; subtract here space taken by scrollers, if necessary | |
if scy-visible?: total-y > visible-y [ | |
;face/children-span/y: min max face/values/min-child-size/y (size/y - x-size) face/values/max-child-size/y | |
;total-y: face/children-span/y | |
if scy-visible?: total-y > visible-y [; check again | |
; adding a vertical scroller changes horizontal visible size | |
visible-x: size/x - y-size | |
] | |
] | |
if scx-visible?: total-x > visible-x [ | |
;face/children-span/x: min max face/values/min-child-size/x (size/x - y-size) face/values/max-child-size/x | |
;total-x: face/children-span/x | |
if scx-visible?: total-x > visible-x [; check again | |
; adding a horizontal scroller changes vertical visible size | |
visible-y: size/y - x-size | |
] | |
] | |
; adding the horizontal scroller might make the vertical scroller necessary | |
if all [not scy-visible? scy-visible?: total-y > visible-y ] [ | |
;face/children-span/y: min max face/values/min-child-size/y (size/y - x-size) face/values/max-child-size/y | |
;total-y: face/children-span/y | |
if scy-visible?: total-y > visible-y [; check again | |
; adding a vertical scroller changes horizontal visible size | |
visible-x: size/x - y-size | |
] | |
] | |
scy/max-size: 0 ;@@ workaround to avoid scroller to become disbled ! | |
scy/max-size: to integer! total-y | |
scy/page-size: 0 | |
scy/page-size: to integer! visible-y | |
scy/visible?: none | |
scy/visible?: scy-visible? | |
scy/min-size: to integer! visible-y / 10 ; FIXME hardcoded value | |
; constrain to allowed range | |
scy/position: to integer! min max 1 scy/position (total-y - visible-y) | |
scx/max-size: 0 ;@@ workaround to avoid scroller to become disbled ! | |
scx/max-size: to integer! total-x + 1 ;@@ + 1 only for 144 dpi but beware that /visible? is setted also by Red ! | |
scx/page-size: 0 | |
scx/page-size: to integer! visible-x | |
scx/visible?: none | |
scx/visible?: scx-visible? | |
scx/min-size: to integer! visible-x / 10 | |
;probedo ["1" scx/position (total-x - visible-x) ] | |
; constrain to allowed range | |
scx/position: to integer! min max 1 scx/position (total-x - visible-x) | |
span: span? face | |
; scroll to keep maximum possible visibility while keeping top-left alignment | |
face/actors/scroll face to integer! max 1 min scy/position absolute (total-y - visible-y) 'y | |
;face/actors/scroll face to integer! max 1 min scx/position absolute (total-x - visible-x) 'x | |
; right align | |
if (span/2/x + span/1/x) < visible-x [face/actors/scroll face to integer! span/2/x - visible-x 'x] | |
; fall back to left alignment | |
if span/2/x < visible-x [face/actors/scroll face 1 'x] | |
;show face | |
system/view/auto-sync?: asy | |
] | |
resize: function [face [object!] size [pair! point2D! none!]][ | |
;face/visible-rows: 2 + to integer! size/y / (face/row-height + 1e-6) + 1e-6 | |
face/visible-rows: round/ceiling (size/y - face/headers-height) / (face/row-height + 1e-6) + 1e-6 ; FIXME face/space/y | |
; FIXME: recalc also max-visible-rows because user could have changed screen resolution ? | |
;span: span? face | |
face/children-span: second span? face | |
;update-scrollers face as-pair (face/size/x) (face/size/y - face/headers-height) as-pair (face/children-span/x) (face/row-height * face/rows) | |
update-scrollers face as-pair (face/size/x) (min (face/size/y - face/headers-height) (face/row-height * face/max-visible-rows - 1)) as-pair (face/children-span/x) (face/row-height * face/rows) | |
if not system/view/auto-sync? [show face] | |
size ; IMPORTANT for reactions ! | |
] | |
] | |
] | |
init: [ | |
face: self | |
;?? face | |
face/actors/flag?: func [flag [word! block!]] bind face/actors/flag? face ;@@ Do I really have to do this ?? | |
; face/actors/set: func [value [number!] ] bind face/actors/set face | |
; face/actors/resize: func [size [pair!] /local siz arrw-up arrw-dn] bind face/actors/resize face | |
face/options: union trim to block! face/options [style: multi-text-list] | |
;row-height: pick get in make-face/spec 'textmin ["Wfqp"] 'size 2 | |
face/row-height: make-face/spec 'textmin ["Wfqp"] | |
face/row-height: face/row-height/size/y ;+ row-gap | |
face/max-visible-rows: 2 + round/ceiling system/view/screens/1/size/y / face/row-height | |
;?? face/row-height | |
if none? face/data [face/data: [[""]]] | |
if empty? face/data [face/data: [[""]]] | |
if empty? face/data/1 [face/data: [[""]]] | |
face/columns: length? face/data/1 | |
face/rows: (length? face/data) - 1 ; FIXME: - 1 because 1st row is that of headers | |
comment [(; cursor image | |
draw/transparent 21x12 [ | |
anti-alias off | |
translate 11x6 | |
shape [ | |
pen black fill-pen white | |
move -11x0 | |
'line 6x-6 0x5 10x0 0x-4 5x5 -5x5 0x-3 -10x0 0x4 -6x-6 | |
] | |
] | |
)] | |
main-pane: compose [ ;@@ I cannot use face/pane because of a reactor ! and I cannot use pane because it is already bounded ? | |
origin 0x25 | |
space 4x0 ;@@ hardcoded, watch out | |
across | |
style col-dragger: | |
box 8x25 magenta ;255.255.255.254 | |
cursor hand ; placeholder | |
all-over | |
with [ | |
actors: object [ | |
on-click: func [face event] [ | |
print "click" | |
;mtl/actors/update-scrollers mtl mtl/size as-pair (mtl/children-span/x) (mtl/row-height * mtl/rows) | |
] | |
on-down: func [face event] [ | |
face/extra/x: event/offset/x | |
] | |
on-over: func [face event /local delta mtl col-offset] [ | |
if all [event/down? face/extra/column > 0] [ | |
;system/view/debug?: yes | |
offset-x: face/offset/x | |
mtl: face/parent | |
col-offset: mtl/get 'column-offset (face/extra/column) | |
;face/offset/x: face/offset/x + event/offset/x - face/extra/x ; move face | |
face/offset/x: min max (4 + 4 + col-offset) (offset-x + event/offset/x - face/extra/x) (280 + col-offset ) ; move face | |
delta: to integer! face/offset/x - offset-x | |
mtl/set 'column-width face/extra/column ((mtl/get 'column-width face/extra/column) + delta) | |
if not system/view/auto-sync? [show face] | |
system/view/debug?: no | |
] | |
] | |
] | |
] | |
style col-header: | |
text 80x25 cyan " " center middle no-wrap ; headers | |
;base 255.255.255.254 80x25 cyan " " center middle no-wrap ; use this to see the "trick" and the circle when clicked | |
loose | |
;extra object [origin: 0x0 column: 0 old-column: 0 delta: 0 left: none right: none sort: 0] | |
;extra object [origin: 0x0 column: (c) old-column: (c) delta: 0 left: none right: none source-x: 0 dest-x: 0 dragged: none inside: none sort: 0] | |
with [ | |
actors: object [ | |
on-created: func [face event] [ | |
append face/options compose [bounds: (object [min: face/offset - 10000x0 max: face/offset + 10000x0])] | |
] | |
on-drag-start: func [face event /local c offsets mtl] [ | |
face/extra/dragged: false | |
mtl: face/parent | |
mtl/dest-col-x: face/offset/x | |
face/extra/origin: face/offset | |
face/extra/delta: face/offset/x | |
;probedo[face/extra/right: face/parent/get 'column-right face] | |
face/extra/source-x: face/offset/x | |
face/extra/dest-x: face/offset/x | |
mtl/set 'column-order face/extra/column 'top | |
face/extra/inside: none | |
mtl/children-span: mtl/actors/span?/part mtl mtl/columns | |
] | |
on-drag: func [face event /local mtl dest dir temp origin-x op distance left-x min-x max-x] [ | |
face/extra/dragged: true | |
;robedo [face/extra/column] | |
mtl: face/parent | |
min-x: 1e7 | |
max-x: -1e7 | |
mtl/set 'column-move face/extra/column face/offset/x | |
res: mtl/get 'column-at-offset reduce [face/extra/column (face/offset/x + (face/size/x / 2)) ] | |
if none? res [;print "none" | |
mtl/old-cols: mtl/cols | |
exit | |
] | |
mtl/set 'column-positions face/extra/column res | |
] | |
on-drop: func [face event /local mtl] [ | |
mtl: face/parent | |
either face/extra/dragged [ | |
mtl/set 'column-move-all face/extra/column mtl/dest-col-x | |
if not system/view/auto-sync? [show face] | |
][ | |
print "click" | |
mtl: face/parent | |
face/extra/sort: either 0 = order: face/extra/sort [1][negate order] | |
probedo [face/draw] | |
face/draw: mtl/draw-arrow | |
;face/draw/trans/2: 10x10 | |
probedo [face/draw] | |
show face | |
;order: pick [a-z z-a] order = 1 | |
mtl/set 'sort face/extra/column face/extra/sort | |
] | |
] | |
] | |
] | |
] | |
color: white | |
; append columns and cells | |
repeat c face/columns [ | |
append face/old-cols c | |
;append main-pane 'panel | |
; append columns | |
append main-pane compose/only [ | |
panel (as-pair 80 system/view/screens/1/size/y) yellow (copy [origin 0x0 space 0x0 below]) | |
;col-header with (compose [extra/column: (c)]) ; headers | |
;col-header extra object [origin: 0x0 column: (c) delta: 0] | |
;panel ; | |
] | |
;p: copy [origin 0x0 space 0x0 below] | |
; append cells | |
;repeat r min face/max-visible-rows face/rows [ | |
repeat r face/max-visible-rows [ | |
append last main-pane compose/only [ | |
textmin 60 (color - (c * 30 * 1.1.0)) " " ;(form reduce [c r]);(form face/data/(r)/(c)) ;on-over [face/color: pick [255.255.255 255.0.0 ] event/away?] | |
with (compose [extra/row: (r)]) | |
] | |
] | |
;append/only last main-pane p | |
;append main-pane [return at 0x0] | |
;append main-pane [return] | |
] | |
append main-pane [return origin 0x0 across] | |
; append headers | |
repeat c face/columns [ | |
append main-pane compose/deep [ | |
col-header data (c) extra object [origin: 0x0 column: (c) old-column: (c) delta: 0 left: none right: none source-x: 0 dest-x: 0 dragged: none inside: none sort: 0] | |
] | |
] | |
append main-pane [return origin 76x0 space 76x0 across] | |
; append draggers | |
repeat c face/columns [ | |
;append main-pane 'col-dragger ;compose/only [col-dragger with (compose [extra/column: (c - 1)]) ] | |
append main-pane compose/deep [ | |
col-dragger extra object [column: (c - 0) x: 0 ] | |
] | |
] | |
; append main-pane [base 200x15 0.0.255.200] | |
;?? main-pane | |
pane: layout/only/tight main-pane | |
face/headers-height: face/pane/(face/columns + 1)/size/y | |
face/headers: copy face/data/1 ; FIXME: parse ... | |
;@@ remove headers row | |
remove face/data | |
; move headers on top by moving them to tail | |
; repeat c face/columns [ | |
; move face/pane/(c)/pane next face/pane/(c)/pane | |
; ] | |
; set cells' texts | |
repeat c face/columns [ | |
repeat r min face/max-visible-rows face/rows [ ;@@ WARNING: use do because of overwritten `min`! | |
face/pane/(c)/pane/(r)/text: form face/data/(r)/(c) | |
] | |
] | |
; set headers titles | |
repeat c face/columns [ | |
face/pane/(face/columns + c)/text: form face/headers/(c) | |
] | |
;?? face/pane | |
;dump-face face | |
system/view/VID/styles/textmin: none | |
face/size: 300x150 | |
face/cols: copy face/old-cols | |
face/color: blue ; to see if resizing is right | |
react/link/later :face/actors/entangle [face face] | |
] | |
] | |
do | |
[ | |
if any [%multi-text-list.red = find/last/tail system/options/script "/" ; It's really me ? | |
system/script/args = "test"] [ | |
print "" ; open console for debug | |
system/view/auto-sync?: no | |
system/view/VID/styles/text: [template: [type: 'text size: 0x0]] | |
{ | |
view [ | |
button "OK" all-over | |
on-down [x: event/offset/x] | |
on-over [if event/down? [face/offset/x: face/offset/x + round/floor/to event/offset/x 2 - x]] | |
] | |
; options/bounds: object [min: pair! max: pair!] | |
} | |
win: layout [ | |
title "Multi column text list examples" ;@@ I wish I could do : title (system/script/header/title) | |
backdrop brown | |
across middle | |
style text: text font-size 10 | |
;{Please do not blame me for a slow or flickering GUI rendering, and for other "hiccups".} | |
button "insert C" | |
button "move C2 left" [] ;loose | |
extra object [column: 1 x: 0] | |
all-over | |
with [ | |
actors: object [ | |
on-click: func [face event] [ | |
print "click" | |
;mtl/actors/update-scrollers mtl mtl/size as-pair (mtl/children-span/x) (mtl/row-height * mtl/rows) | |
] | |
on-down: func [face event] [ | |
face/extra/x: event/offset/x | |
] | |
on-over: func [face event /local delta] [ | |
if event/down? [ | |
;system/view/debug?: yes | |
delta: face/offset/x | |
face/offset/x: min max (mtl/get 'column-offset (face/extra/column)) face/offset/x + event/offset/x - face/extra/x ((180 + mtl/get 'column-offset (face/extra/column)) ) ; move face | |
delta: face/offset/x - delta | |
mtl/set 'column-width face/extra/column ((mtl/get 'column-width face/extra/column) + delta) | |
if not system/view/auto-sync? [show face] | |
system/view/debug?: no | |
] | |
] | |
] | |
] | |
button "append C" loose ;with [options: compose [bounds: (object [min: 0x0 max: 0x0])]];[mtl/insert-column 4 ["Col 4 1" "Col 4 2"]] | |
extra object [origin: 0x0 column: 3 delta: 0] | |
on-created [ | |
face/extra/origin: face/offset | |
face/extra/delta: face/offset/x | |
append face/options compose [bounds: (object [min: face/offset - 100x0 max: face/offset + 100x0])] | |
] | |
on-drag [ | |
mtl/set 'column-move face/extra/column face/offset/x - face/extra/delta | |
face/extra/delta: face/offset/x | |
] | |
on-drop [ | |
face/offset: face/extra/origin | |
if not system/view/auto-sync? [show face] | |
] | |
return | |
mtl1: multi-text-list data [ | |
;["Name" 80 'A-Z "Column 2" "Column 3"] ;'opt icon <name> [string! image!] <width> [integer! default: 80] <backcolor> [tuple! default: white] <textcolor> [tuple! default: black] align [word! 'left 'center 'right default: 'left] sorting [word! 'A-Z 'Z-A none! default: none] editing [logic! default: true] | |
["Column 1" "Col 2" "Col 3"] | |
["Element 1" 10:25 "Comment 31"] | |
["Element 2" "Column 2" "Comment 32"] | |
["Element 53" "C 2" "Comment 33"] | |
["Element 4" "C 2" "Comment 34"] | |
["Element 5" "C 2" "Comment 35"] | |
["Element 6" "C 2" "Comment 36"] | |
["Element 7" "C 2" "Comment 3"] | |
; ["Element 8" "C 2" "Comment 3"] | |
; ["Element 9" "C 2" "Comment 3"] | |
; ["Element 10" "C 2" "Comment 3"] | |
; ["Element 11" "C 2" "Comment 3"] | |
; ["Element 12" "C 2" "Comment 3"] | |
; ["Element 13" "C 2" "Comment 3"] | |
; ["Element 14" "C 2" "Comment 3"] | |
; ["Element 15" "C 2" "Comment 3"] | |
] | |
] | |
;dump-face win | |
view/flags/options win 'resize [ | |
actors: object [ | |
on-create: func [face][face/data: face/size] ; init value | |
on-focus: func [face event][face/data: face/size] ; store old size | |
on-resize: func [face event][face/actors/on-resizing face event] ; forward | |
on-resizing: func [face event /local siz][ | |
siz: face/size - face/data ; compute size difference | |
face/data: face/size ; store new size | |
mtl1/size: mtl1/size + (siz * 1x1) | |
; t-1/offset: t-1/offset + (siz * 1x0) | |
if not system/view/auto-sync? [show face] | |
] | |
] | |
] | |
] ; if | |
] ; do |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Red [ | |
title: "Numeric spin field" | |
author: @luce80 | |
Rights: "Copyright (C) 2024 Marco Antoniazzi. All rights reserved." | |
License: BSL-1 | |
file: %numeric-spinner.red | |
gist: https://gist.githubusercontent.com/luce80/433286c66d98997aff6e69fbd6323a35/raw/3d8cba0644232748218442467537c8f4e706d361/numeric-spinner.red | |
date: 02-01-2024 | |
version: 0.6.7 | |
history: [ | |
0.0.0 [18-10-2022 "Started"] | |
0.1.0 [24-10-2022 "continued"] | |
0.2.0 [29-10-2022 "Most graphic parts and behaviours"] | |
0.3.0 [30-10-2022 "Integer flag and reactions"] | |
0.4.0 [01-11-2022 "cycle, drag, wheel, keys, fixes"] | |
0.5.1 [05-11-2022 "keep decimal places, docs, changed sizing algorithm, fixes"] | |
0.5.2 [06-11-2022 {system/script/args = "test", focus}] | |
0.6.2 [19-11-2022 "Allow math ops, minor enhancement"] | |
0.6.3 [25-11-2022 "fix (!?) when auto-sync? is off, simplified space-ops"] | |
0.6.4 [22-01-2023 "Added style: spin-number"] | |
0.6.5 [20-02-2023 "refactored code a little"] | |
0.6.6 [29-12-2023 "made compatible with new point2D! datatype"] | |
0.6.7 [02-01-2024 "removed face/min etc."] | |
] | |
Notes: { | |
This is a VID style to make it easier to enter numbers. | |
To initialize the main value use a string or `data`. | |
You can set some parameters by using `options` VID keyword. | |
The currently available parameters are: | |
- min: minimum value (lower limit) | |
- max: maximum value (upper limit) | |
- step: step by which in/de-crement when clicking arrow buttons | |
- precision: a number between 0 and 1 used to define number of decimal places displayed. E.g. 0.01 means display 2 decimals | |
- flags: a `block!` containing one or more of: | |
- integer numbers are displayed and kept as `integer!`s | |
- cycle during GUI interaction if upper limit is exceeded the number will restart from lower limit | |
- read-only field is disabled but arrow buttons will still work | |
Since this is a `panel` you can not use a `block!` to have a default action, use `on-enter` and/or `on-click` instead. | |
Since this is a `panel` you can not use a `number!` to specify the size, you must use a `pair!` instead. | |
Since this is a `panel` you can not give focus to it, you must use e.g. `set-focus my-spinner/field` instead. | |
See at bottom of script for a usage example. | |
} | |
] | |
probedo: func [code [block!] /local result][print [mold code mold/only result: reduce code] do :result] | |
system/view/VID/styles/spin-number: [ | |
default-actor: on-enter | |
template: [ | |
type: 'panel | |
size: 0x0 | |
;flags: copy [] ; reserved by Red | |
old-text: none | |
this: none ; self | |
field: none ; shortcut | |
old-size: 0x0 ; used to resize incrementally | |
values: object [ ; object used to extend VID params and set main control params | |
min: 0 | |
max: 100 | |
step: 1 ; to in/de-crement | |
factor: 1 ; when using <ctrl> or <shift> ; TBD | |
precision: step ;@@ or something else ? | |
flags: copy [] ; 'integer , 'cycle , 'read-only ;TBD 'percent , 'time , 'money , 'alerts(-on-wrong-validation) | |
;TBD unit: "" ;"%", " mm" etc. could use @toomasv's units.red | |
;TBD prefix: "" ; "$" | |
] | |
actors: [ | |
no-num: complement charset "1234567890,.-+*/() ^-" | |
space-ops: func [string [string!]][; add spaces around math ops | |
if find string no-num [return ""] ; force error! | |
parse next string [; use next to avoid sign symbol | |
some [to ["+" | "-" | "*" | "/"] insert " " skip insert " "] | |
] | |
head string | |
] | |
flag?: [ ; will be transformed to a function! | |
to logic! find values/flags flag | |
] | |
set: [ ; will be transformed to a function! | |
;prin "set " | |
;?? value | |
if flag? 'cycle [value: values/min + mod (value - values/min) (values/max - values/min)] | |
value: to float! min max values/min value values/max ; force to float! | |
precision: min max 1e-6 values/precision 1 ; avoid division by 0 and scientific notation | |
; add decimals to remove them later | |
value: (round/to value precision) + (precision * 0.1 * sign? value) | |
text: form value | |
; keep only desired decimal places | |
text: head clear skip text (length? text) - pick [1 2] precision < 1 | |
if flag? 'integer [value: to integer! round value] | |
data: value | |
old-text: copy text ; store old value to be able to restore it if an error occurs | |
text | |
] | |
change: func [face [object!] event [event! none!] delta [number!]][ | |
; FIXME: use a non-linear (exponential ?) function | |
set face/data + (face/values/step * delta * factor face/field event) | |
] | |
resize: [ ; will be transformed to a function! | |
siz: size - old-size ; compute size difference | |
old-size: size ; store new size | |
arrw-up: pane/2 arrw-dn: pane/3 | |
field/size: field/size + (siz * 1x1) | |
field/font/size: to integer! size/y / 2 | |
;arrw-up/size: arrw-up/size * (1, 0) + (field/size * (0, 0.5)) | |
;arrw-dn/size: arrw-dn/size * (1, 0) + (field/size/y - arrw-up/size/y + 1 * (0, 1)) | |
arrw-up/size/y: to integer! field/size/y / 2 | |
arrw-up/offset: arrw-up/offset + (siz * 1x0) | |
arrw-dn/size/y: to integer! field/size/y - arrw-up/size/y + 1 | |
arrw-dn/offset: as-point2D (arrw-up/offset/x) (field/size/y - arrw-dn/size/y + 1) | |
;size: size ; done by entangle | |
size ; IMPORTANT for reactions ! | |
] | |
factor: func [face [object!] event [event! none!]][ | |
; FIXME: hardcoded values | |
case [ | |
all [event/ctrl? event/shift?] [20] | |
event/ctrl? [2] | |
event/shift? [10] | |
face/extra/shift? [10] | |
'else [1] | |
] | |
] | |
entangle: func ["Activate reactions" face1 face2][ | |
face2/text: face1/actors/set face1/data | |
face1/size: face1/actors/resize face1/size | |
if not system/view/auto-sync? [show [face1 face2]] ;@@ force update even if auto-sync? is off (!!??) | |
] | |
on-created: func [face [object!] event [event! none!]][ | |
;?? face | |
face/old-size: face/size | |
face/field: face/pane/1 | |
if not number? load face/field/text [face/field/text: form face/min] ; FIXME: or give error! ? | |
;face/data: load face/field/text | |
face/actors/set load face/field/text | |
] | |
] | |
] | |
init: [ | |
face: self | |
face/actors/flag?: func [flag [word! block!]] bind face/actors/flag? face ;@@ Do I really have to do this ?? | |
face/actors/set: func [value [number!] /local precision] bind face/actors/set face | |
face/actors/resize: func [size [pair! point2D!] /local siz arrw-up arrw-dn] bind face/actors/resize face | |
if face/size/x = 0 [face/size/x: 60] | |
if face/size/y = 0 [face/size/y: 26] | |
face/pane: face/flags ; store flags | |
face/values: make face/values any [face/options []] | |
if not all [face/options face/options/precision] [face/values/precision: face/values/step] | |
face/values/flags: to-block face/values/flags | |
set face face/values | |
face/flags: face/pane ; restore flags | |
face/options: trim union to block! face/options [style: spin-number] | |
face/pane: layout/only/tight compose [ | |
space 0x0 | |
across | |
field (any [all [face/data form face/data] face/text "0"]) (face/size - 13x0) right font-size (any [attempt [face/font/size] 12]) ; | |
extra object [shift?: false offset: none last-offset: none moving?: false] | |
on-down [face/extra/offset: face/extra/last-offset: event/offset] | |
on-up [face/extra/offset: none face/extra/moving?: false] | |
all-over | |
on-over [ | |
if any [face/extra/moving? all [ face/extra/offset (absolute (face/extra/offset/y - event/offset/y)) > 10]] [ | |
face/extra/moving?: true | |
face/parent/actors/change face/parent event either face/extra/offset [face/extra/last-offset/y - event/offset/y] [0] | |
] | |
face/extra/last-offset: event/offset | |
] | |
on-key [ | |
face/parent/actors/change face/parent event switch/default event/key [up [+1] down [-1]] [exit] | |
] | |
on-wheel [ | |
face/parent/actors/change face/parent event event/picked | |
] | |
[; action (on-enter) | |
if none? attempt [face/parent/actors/set do face/parent/actors/space-ops face/text] [face/text: copy face/parent/old-text] | |
] | |
below | |
style arrow: button 13x13 font-size 8 data 1 extra object [shift?: false time: none] | |
on-time [ | |
face/actors/on-click face event | |
if all [time? face/extra/time (now/time - face/extra/time) >= 0:0:2] [ ; wait 2 seconds and then speed up even more | |
face/rate: to-time .02 | |
face/extra/time: 0 | |
] | |
if none? face/extra/time [ | |
face/rate: to-time .1 | |
face/extra/time: now/time | |
] | |
] | |
on-down [ | |
face/extra/shift?: event/shift? face/rate: to-time .5 ;@@ use face/extra/shift? because time events do not have event/shift? | |
if not system/view/auto-sync? [show face] ;@@ force update even if auto-sync? is off (!!??) | |
] | |
on-up [ | |
face/extra/time: none face/rate: none | |
if not system/view/auto-sync? [show face] ;@@ force update even if auto-sync? is off (!!??) | |
] | |
[ ; action (on-click) | |
set-focus face/parent/field | |
face/parent/actors/set face/parent/data + (face/parent/values/step * face/data * face/parent/actors/factor face event) | |
] | |
arrow "▲" data +1 | |
arrow "▼" data -1 | |
] | |
face/pane/1/parent: face | |
face/pane/2/parent: face | |
face/pane/3/parent: face | |
if face/actors/flag? 'read-only [face/pane/1/enabled?: false face/pane/1/flags: [no-border] ]; | |
face/size/x: face/pane/1/size/x + face/pane/2/size/x - 2 ;@@ "-2" because of button's "outline" (is this only Win related?) | |
face/size/y: face/pane/1/size/y | |
;face/color: blue ; to see if resizing is right | |
react/link/later :face/actors/entangle [face face/pane/1] | |
] | |
] | |
do | |
[ | |
if any [%numeric-spinner.red = find/last/tail system/options/script "/" ; It's really me ? | |
system/script/args = "test"] [ | |
;print "" ; open console for debug | |
system/view/VID/styles/text: [template: [type: 'text size: 0x0]] | |
win: layout [ | |
title "Numeric spinner examples" ;@@ I wish I could do : title (system/script/header/title) | |
across middle | |
style text: text font-size 10 | |
; note that spinners can be initialized with a string or with data | |
sp-1: spin-number "50.0" options [min: 0 max: 100 step: 0.5] | |
t-1: text "min 0.0 max 100.0 step 0.5" | |
return | |
sp-2: spin-number data 50.0 options [min: 0 max: 360 precision: 0.1 flags: [cycle]] | |
t-2: text "min 0.0 max 360.0 step 1.0 precision 0.1 cycle" | |
return | |
sp-3: spin-number 80x25 "2.0" options [min: 0 max: 1000 step: 1 flags: [integer]] | |
t-3: text "min 0 max 1000 step 1 integer" | |
return | |
sp-4: spin-number 150x50 "20.0" options [min: -1000 max: 1000 step: .01] | |
t-4: text "min -1000 max 1000 step .01" | |
return | |
below | |
space 0x0 | |
style h: h5 bold | |
h-1: h "Use also (qualified) arrows keys" | |
h-2: h "Use also (qualified) scroll-wheel" | |
h-3: h "Try also (qualified) drag up and down inside field" | |
h-4: h "Also simple math operations are allowed" | |
] | |
view/flags/options win 'resize [ | |
actors: object [ | |
on-create: func [face][face/data: face/size] ; init value | |
on-focus: func [face event][face/data: face/size] ; store old size | |
on-resize: func [face event][face/actors/on-resizing face event] ; forward | |
on-resizing: func [face event /local siz][ | |
siz: face/size - face/data ; compute size difference | |
face/data: face/size ; store new size | |
sp-1/size: sp-1/size + (siz * 1x0) | |
t-1/offset: t-1/offset + (siz * 1x0) | |
] | |
] | |
] | |
] ; if | |
] ; do |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Red [] | |
;%numeric-spinner.red | |
;%area-plus.red | |
;%splitter.red | |
;%scrollable-panel.red | |
;%%multi-text-list.red | |
;%area-rt.red | |
;%spinner-panel.red | |
;%tipped-button.red |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Red [ | |
title: "Scrollable panel" | |
file: %scrollable-panel.red | |
author: @luce80 | |
License: 'PD | |
gist-view: https://gist.github.com/luce80/433286c66d98997aff6e69fbd6323a35#file-scrollable-panel-red | |
date: 27-02-2024 | |
version: 0.6.5 | |
history: [ | |
0.0.0 [10-10-2022 "Started"] | |
0.0.1 [18-12-2022 "minimum working version"] | |
0.6.0 [24-12-2022 "working version"] | |
0.6.1 [06-01-2023 "Better 'fixed checking"] | |
0.6.2 [28-01-2023 "fixed scrollers update workaround and child size change"] | |
0.6.3 [29-01-2023 "externalized scroll function"] | |
0.6.4 [29-12-2023 "fixed for new point2D! datatype"] | |
0.6.5 [27-02-2024 "fixed to avoid sizing before scrollers are available"] | |
] | |
Note: {Needs Red 0.6.4 built 09-Aug-2022 or later} | |
Notes: { | |
This is a VID panel with auto-hiding scrollers. | |
It can have only 1 child face. | |
The child face size will be adapted to that of the panel. | |
You can set some parameters by using `options` VID keyword. | |
The currently available parameters are: | |
- min-child-size: the minimum size [pair!] (in pixels) that the child gadget can have. | |
- max-child-size: the maximum size [pair!] (in pixels) that the child gadget can have. | |
- flags: a `block!` containing one or more of: | |
- fixed Set min-child-size = max-child-size = initial child size | |
Note that by setting min-child-size = max-child-size you will have a fixed size child. | |
Please do not blame me for a slow or flickering GUI rendering, and for other "hiccups". | |
See at bottom of script for a usage example. | |
} | |
] | |
probedo: func [code [block!] /local result][print [mold code mold/only result: reduce code] do :result] | |
if system/build/date < 26-11-2022 [alert "A more recent version of Red is required !" either empty? gui-console-ctx/terminal/lines [quit][halt]] | |
scrollable-panel.red-ctx: context [ | |
system/view/VID/styles/scrollable-panel: [ | |
default-actor: on-down | |
template: [ | |
type: 'panel | |
flags: [scrollable] | |
child: none ; shortcut | |
values: object [ ; object used to extend VID params and set main control params | |
min-child-size: 0x0 | |
max-child-size: 100000x100000 | |
flags: copy [] ; 'fixed | |
] | |
actors: [ | |
scroller: make map! 2 | |
; scroller fields | |
; position: 1 ;@@ a better name could be "scroll" or "amount" or "value" or "data" | |
; page-size: none ;@@ a better name could be "page" or "visible-part" | |
; min-size: 1 ; ?? . Used for step scrolling | |
; max-size: 1 ;@@ a better name could be "total" | |
; visible?: true | |
; vertical?: true ;@@ a better name could be "orientation" with value 'x or 'y | |
; parent: none | |
; page: 1 ; ?? | |
flag?: [ ; will be transformed to a function! | |
to logic! find values/flags flag | |
] | |
scroll-to: function [face [object!] pos [pair!]][ | |
;TBD | |
] | |
scroll: function [face [object!] pos [integer! ] axis [word!]][ | |
face/child/offset/(axis): to integer! 1 + negate pos ;@@ "1 +" because this must be 0-based | |
] | |
on-scroll: func [face [object!] event [event! none!] /local axis scr pos][ ; function "layout" inspired by @cosacam1 version | |
axis: pick [y x] (any [event/orientation 'vertical]) = 'vertical | |
if all [axis = 'y any [not scroller/y/visible? event/ctrl?]] [axis: 'x] ; FIXME: better use shift ? | |
scr: scroller/(axis) | |
pos: scr/position | |
scr/position: min max 1 switch event/key [ | |
up left [pos - scr/min-size] | |
down right [pos + scr/min-size] | |
page-up page-left [pos - scr/page-size] | |
page-down page-right [pos + scr/page-size] | |
track [event/picked] | |
wheel [pos - (scr/min-size * to integer! event/picked)] ; forwarded event by on-wheel | |
end [pos] | |
] (scr/max-size - scr/page-size) | |
scroll face scr/position axis ; use an overwritable function | |
if not system/view/auto-sync? [show face] | |
] | |
on-wheel: function [face [object!] event [event! none!]][;May-be switch shift and ctrl ? | |
if any [scroller/x/visible? scroller/y/visible?] [ | |
on-scroll face event ; forward | |
] | |
] | |
on-created: func [face [object!] event [event! none!] /locl temp][ | |
scroller/x: get-scroller face 'horizontal | |
scroller/y: get-scroller face 'vertical | |
if face/values/min-child-size/x > face/values/max-child-size/x [temp: face/values/min-child-size/x face/values/min-child-size/x: face/values/max-child-size/x face/values/max-child-size/x: temp] | |
if face/values/min-child-size/y > face/values/max-child-size/y [temp: face/values/min-child-size/y face/values/min-child-size/y: face/values/max-child-size/y face/values/max-child-size/y: temp] | |
if face/values/min-child-size = face/values/max-child-size [face/values/flags: union face/values/flags [fixed]] | |
if flag? 'fixed [face/values/min-child-size: face/values/max-child-size: min max face/values/min-child-size face/child/size face/values/max-child-size] | |
face/child/offset: 0x0 ; align to top-left | |
resize face face/size | |
] | |
entangle: func ["Activate reactions" face1 face2][ | |
if flag? 'fixed [face1/values/min-child-size: face1/values/max-child-size: face2/size ] | |
if not all [scroller/x scroller/y] [exit] ; we are not displayed yet | |
face1/size: face1/actors/resize face1 face1/size | |
;if not system/view/auto-sync? [show [face1]] ; | |
] | |
; must adjust both scrollers at the same time | |
resize: function [face [object!] size [pair! point2D! none!]][ ; some parts of this function are inspired by one of Anton Rolls | |
asy: system/view/auto-sync? | |
system/view/auto-sync?: no | |
scy: scroller/y | |
scx: scroller/x | |
y-size: to integer! system/view/metrics/misc/scroller/y * 96 / system/view/metrics/dpi ;@@ WHAT !!??, really ?? | |
x-size: to integer! system/view/metrics/misc/scroller/x * 96 / system/view/metrics/dpi | |
face/child/size: min max face/values/min-child-size size face/values/max-child-size | |
visible-y: size/y | |
total-y: face/child/size/y | |
visible-x: size/x | |
total-x: face/child/size/x | |
; determine if scrollers are necessary to be shown | |
; subtract here space taken by scrollers, if necessary | |
if scy-visible?: total-y > visible-y [ | |
face/child/size/y: min max face/values/min-child-size/y (size/y - x-size) face/values/max-child-size/y | |
total-y: face/child/size/y | |
if scy-visible?: total-y > visible-y [; check again | |
; adding a vertical scroller changes horizontal visible size | |
visible-x: size/x - y-size | |
] | |
] | |
if scx-visible?: total-x > visible-x [ | |
face/child/size/x: min max face/values/min-child-size/x (size/x - y-size) face/values/max-child-size/x | |
total-x: face/child/size/x | |
if scx-visible?: total-x > visible-x [; check again | |
; adding a horizontal scroller changes vertical visible size | |
visible-y: size/y - x-size | |
] | |
] | |
; adding the horizontal scroller might make the vertical scroller necessary | |
if all [not scy-visible? scy-visible?: total-y > visible-y ] [ | |
face/child/size/y: min max face/values/min-child-size/y (size/y - x-size) face/values/max-child-size/y | |
total-y: face/child/size/y | |
if scy-visible?: total-y > visible-y [; check again | |
; adding a vertical scroller changes horizontal visible size | |
visible-x: size/x - y-size | |
] | |
] | |
scy/max-size: 0 ;@@ workaround to avoid scroller to become disbled ! | |
scy/max-size: to integer! total-y | |
scy/page-size: 0 | |
scy/page-size: to integer! visible-y | |
scy/visible?: none | |
scy/visible?: scy-visible? | |
scx/max-size: 0 ;@@ workaround to avoid scroller to become disbled ! | |
scx/max-size: to integer! total-x + 1 ;@@ + 1 only for 144 dpi but beware that /visible? is setted also by Red ! | |
scx/page-size: 0 | |
scx/page-size: to integer! visible-x | |
scx/visible?: none | |
scx/visible?: scx-visible? | |
scy/min-size: to integer! scy/page-size / 10 ; FIXME hardcoded value | |
scx/min-size: to integer! scx/page-size / 10 | |
; constrain to allowed range | |
scy/position: to integer! min max 1 scy/position (scy/max-size - scy/page-size) | |
scx/position: to integer! min max 1 scx/position (scx/max-size - scx/page-size) | |
; scroll to keep maximum possible visibility while keeping top-left alignment | |
face/actors/scroll face max 1 min scy/position to integer! absolute (total-y - visible-y) 'y | |
face/actors/scroll face max 1 min scx/position to integer! absolute (total-x - visible-x) 'x | |
show face | |
system/view/auto-sync?: asy | |
size ; IMPORTANT for reactions ! | |
] | |
] | |
] | |
init: [ | |
face: self | |
face/actors/flag?: func [flag [word! block!]] bind face/actors/flag? face ;@@ Do I really have to do this binding ?? | |
if (length? face/pane) <> 1 [do make error! "Scrollable panel must contain only 1 face"] | |
face/child: face/flags ; store flags | |
face/values: make face/values any [face/options []] | |
face/values/flags: to-block face/values/flags | |
;set face face/values | |
face/flags: face/child ; restore flags | |
face/child: face/pane/1 | |
if any [not pair? face/values/min-child-size not pair? face/values/max-child-size] [do make error! "Min and max scrollable panel child sizes must be pair!"] | |
react/link/later :face/actors/entangle [face face/child] | |
] | |
] | |
] ; ctx | |
do | |
[ | |
if any [%scrollable-panel.red = find/last/tail system/options/script "/" ; It's really me ? | |
system/script/args = "test"] [ | |
;print "" ; open console for debug | |
system/view/VID/styles/textmin: [template: [type: 'text size: 0x0]] | |
win: layout compose/deep [ | |
title "Scrollable panel" ;@@ I'd like to do : title (system/script/header/title) | |
below | |
space 0x4 | |
textmin "Resize the window to see button being resized" | |
textmin "and auto-scrollers appear when necessary" | |
sp: scrollable-panel 350x350 magenta [ | |
button 250x200 wrap "Tanto gentile e tanto onesta pare^/la donna mia quand'ella altrui saluta^/ch'ogne lingua deven tremando muta" | |
] options [min-child-size: 150x130 max-child-size: 300x250] | |
] | |
react compose [ | |
sp/size: win/size - (win/size - sp/size) | |
if not system/view/auto-sync? [show win] | |
] | |
view/flags win 'resize | |
] ; if | |
] ; do |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Red [ | |
title: "A one-row panel with a spinner used instead of a scroller" | |
author: @luce80 | |
Rights: "Copyright (C) 2024 Marco Antoniazzi. All rights reserved." | |
License: BSL-1 | |
file: %spinner-panel.red | |
gist: none | |
date: 18-03-2024 | |
version: 0.9.0 | |
history: [ | |
0.0.0 [16-03-2024 "Started"] | |
0.9.0 [18-03-2024 "main aspects completed"] | |
] | |
Notes: { | |
This is a VID panel meant to contain a row of widgets, it behaves similar to the row of the tabs of a `tab-panel`. | |
If it's size is smaller then its content, a spinner (that is two arrows) will appear to let you scroll. | |
Since this is a `panel` you can not use a `block!` to have a default action, use `on-enter` and/or `on-click` instead. | |
Since this is a `panel` you can not use a `number!` to specify the size, you must use a `pair!` or a `point2D!` instead. | |
Since this is a `panel` you can not give focus to it, you must use e.g. `set-focus my-panel/field` instead. | |
See at bottom of script for a usage example. | |
} | |
] | |
probedo: func [code [block!] /local result][print [mold code mold/only result: reduce code] do :result] | |
spinner-panel.red-ctx: context [ | |
span?: func [ | |
"Returns a block of [min-pos max-size] bounds for all faces in pane" | |
pane [block!] | |
/part count [integer!] "Limit the number of faces" | |
/local origin margin face | |
][ | |
origin: (100000, 100000) | |
margin: (0, 0) | |
foreach face pane [ | |
if all [count negative? count: count - 1] [break] | |
origin: min origin face/offset | |
margin: max margin face/offset + face/size | |
] | |
reduce [origin margin - origin] | |
] | |
system/view/VID/styles/spinner-panel: [ | |
default-actor: on-down | |
template: [ | |
type: 'panel | |
size: 200x200 | |
this: self | |
; shortcuts | |
bar: | |
arrows: | |
arrow-L: | |
arrow-R: | |
left-face: | |
in-size: | |
margin: none | |
actors: [ | |
visible-arrows: func [face flag [logic!]] [ | |
face/arrow-L/visible?: flag | |
face/arrow-R/visible?: flag | |
face/arrows/visible?: flag | |
face/arrow-L/enabled?: not head? face/left-face | |
if not face/arrow-L/enabled? [face/arrow-L/rate: none] | |
face/arrow-R/enabled?: not tail? next face/left-face | |
if not face/arrow-R/enabled? [face/arrow-R/rate: none] | |
] | |
find-non-visible: func [root bar dir /local pane face][ | |
pane: head root/left-face | |
forall pane [ | |
face: first pane | |
either dir = 'left [ | |
if (absolute bar/offset/x) <= (face/offset/x) [return either head? pane [tail pane] [back pane]] | |
][ | |
if (root/arrows/offset/x) < (face/offset/x + face/size/x + bar/offset/x) [return pane] | |
] | |
] | |
tail pane | |
] | |
resize: func [face size [pair! point2D!] /local visibility hid-face][;FIXME: not perfect :( | |
;FIXME: re-calc in-size if something is added or removed | |
face/bar/offset/x: min max | |
negate face/size/x | |
max (size/x - face/bar/size/x + face/margin/x - face/arrows/size/x) face/bar/offset/x | |
0 | |
; keep arrows right aligned | |
face/arrows/offset/x: to-integer size/x - face/arrows/size/x | |
visible-arrows face visibility: size/x < (face/in-size/x + face/margin/x) | |
; keep bar left aligned | |
if not visibility [face/bar/offset/x: 0] | |
; re-enable if risizing clipped something | |
face/arrow-L/enabled?: not tail? find-non-visible face face/bar 'left | |
face/arrow-R/enabled?: not tail? find-non-visible face face/bar 'right | |
] | |
] | |
old-on-change*: :on-change* | |
on-change*: func [word old new][ | |
old-on-change* word :old :new | |
switch to word! word [ | |
size [ | |
if old <> new [actors/resize this new] | |
] | |
] | |
] | |
] | |
init: [ | |
in-size: second span? pane | |
left-face: pane ; store "real" pane | |
margin: left-face/1/offset ;FIXME: size - in-size | |
pane: layout/tight/only compose [ | |
below right | |
panel (size) [] | |
;FIXME: hardcoded numbers | |
style button: button (as-point2D 15 size/y - 2) font-size 8 | |
on-down [face/rate: 0:0:0.2] | |
on-up [face/rate: none] | |
on-time [do-actor face none 'click] | |
pad (as-point2D 0 0 - size/y) ; re-put on top | |
panel [ | |
origin 0x0 space 0x0 | |
button "◀" ; <- | |
on-click [ | |
left-face: actors/find-non-visible this bar 'left | |
bar/offset/x: negate left-face/1/offset/x | |
actors/visible-arrows this true | |
] | |
button "▶" ; -> | |
on-click [ | |
left-face: actors/find-non-visible this bar 'right | |
bar/offset/x: arrows/offset/x - (left-face/1/offset/x + left-face/1/size/x) | |
actors/visible-arrows this true | |
] | |
] | |
] | |
bar: pane/1 | |
; put original pane in newly created pane | |
bar/pane: left-face | |
arrows: pane/2 | |
arrow-L: first arrows/pane | |
arrow-R: second arrows/pane | |
; hide arrows | |
actors/visible-arrows self false | |
] | |
] | |
] ; context | |
do | |
[ | |
if any [%spinner-panel.red = find/last/tail system/options/script "/" ; It's really me ? | |
system/script/args = "test"] [ | |
; | |
;print "" ; open console for debug | |
system/view/VID/styles/text: [template: [type: 'text size: 0x0]] | |
win: layout [ | |
title "Spinner panel example" ;@@ I wish I could do : title (system/script/header/title) | |
across middle | |
panel-s: spinner-panel [ | |
button "Hello World" | |
button "Hello " | |
button " World" | |
button "Hello World!" | |
button "Hi" ;20 | |
check "checker" silver | |
] | |
] | |
view/flags/options win 'resize [ | |
actors: object [ | |
on-create: func [face][face/data: face/size] ; init value | |
on-focus: func [face event][face/data: face/size] ; store old size | |
on-resize: func [face event][face/actors/on-resizing face event] ; forward | |
on-resizing: func [face event /local siz][ | |
siz: face/size - face/data ; compute size difference | |
face/data: face/size ; store new size | |
panel-s/size: panel-s/size + (siz * 1x1) | |
] | |
] | |
] | |
] ; if | |
] ; do |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Red [ | |
title: "Splitter face" | |
author: @luce80 | |
Rights: "Copyright (C) 2022-2024 Marco Antoniazzi. All rights reserved." | |
License: BSL-1 | |
file: %splitter.red | |
gist-view: https://gist.github.com/luce80/433286c66d98997aff6e69fbd6323a35#file-splitter-red | |
date: 16-04-2024 | |
version: 0.7.9 | |
history: [ | |
0.0.0 [27-11-2022 "Started"] | |
0.0.1 [03-12-2022 "minimum working version"] | |
0.1.0 [04-12-2022 "Cursor arrows, 3D bar, reactions"] | |
0.7.0 [08-12-2022 "undraggable, fixed, min-size, view-resize, example"] | |
0.7.1 [11-12-2022 "Added but then removed options/margins"] | |
0.7.2 [13-12-2022 "Adjusted example faces size"] | |
0.7.3 [24-12-2022 "Simplified separator drawing block"] | |
0.7.4 [03-01-2023 "Avoid useless re-showing"] | |
0.7.5 [15-01-2023 "Improved window example resizing"] | |
0.7.6 [22-01-2023 "Added style: splitter and style: separator"] | |
0.7.7 [28-12-2023 "Fixed for new point2d! datatype and `draw that must be outside of init block"] | |
0.7.8 [05-04-2024 "Fixed cursor images using system images and removed ON-CREATE actor"] | |
0.7.9 [16-04-2024 "Fixed by no more overwriting ON-CREATED actor"] | |
] | |
Note: {Needs Red 0.6.4 built 09-Aug-2022 or later} | |
Notes: { | |
This is a VID style to allow two child faces to be resized by the user with a separator bar. | |
There can be only 2 child faces. | |
The separator bar width is given by the space between the two child faces. | |
A good (Windows default ?) width should be 7 pixels. | |
Orientation is provided by VID: default is `across` or use `below` to have a vertical layout | |
You can set some parameters by using `options` VID keyword. | |
The currently available parameters are: | |
- first-min-size: the minimum size (in pixels) that the first gadget can have. | |
- second-min-size: the minimum size (in pixels) that the second gadget can have. | |
- flags: a `block!` containing one or more of: | |
- separator A 3D-looking separator is drawn in the splitter. | |
- first-fixed When the splitter gadget is resized, the first gadget will keep its size. | |
- second-fixed When the splitter gadget is resized, the second gadget will keep its size. | |
- undraggable The splitter can not be moved to resize the gadgets. | |
Please do not blame me for a slow or flickering GUI rendering, and for other "hiccups". | |
See at bottom of script for a usage example. | |
} | |
] | |
probedo: func [code [block!] /local result][print [mold code mold/only result: reduce code] do :result] | |
use: func [words [block!] body [block!]][body: has words body body] ; FIXME: redefined ?, place in a context ? | |
view-resize: function [ | |
"Displays a resizable window view from a layout block or from a window face" | |
spec [block! object!] "Layout block or face object with a root splitter" | |
/tight "Zero offset and origin" | |
/options | |
opts [block!] "Optional features in [name: value] format" | |
/flags | |
flgs [block! word!] "One or more window flags" | |
/with | |
body [block!] "Do custom code instead of resizing root splitter" | |
;/modal "Display a modal window (pop-up)" | |
/no-wait "Return immediately - do not wait" | |
][ | |
if not options [opts: copy []] | |
append opts [ | |
actors: object [ | |
; FIXME I should insert instead of overwrite | |
on-resizing: func [face event /local siz][ | |
siz: event/offset - face/size ; compute size difference (event/offset contains new size) | |
either face/pane/1/options/style = 'splitter [ | |
face/pane/1/size: face/pane/1/size + (siz * 1x1) ; resizing the main splitter will resize all others | |
if not system/view/auto-sync? [show face/pane/1] | |
][ | |
do body ; FIXME do also if root face is a splitter ? | |
] | |
] | |
] | |
] | |
view/flags/options spec 'resize opts | |
] | |
system/view/VID/styles/splitter: [ | |
default-actor: on-down | |
template: [ | |
type: 'panel | |
size: 200x200 | |
; color: 255.255.255.254 ; default transparent | |
;flags: copy [] ; reserved by Red | |
face1: none ; shortcut | |
face2: none ; shortcut | |
bar: none ; shortcut | |
ratio: 0 | |
axis: 1 | |
margin: 0x0 | |
bar-size: 0x0 | |
first-min-size: 0 | |
second-min-size: 0 | |
values: object [ ; object used to extend VID params and set main control params | |
first-min-size: 0 | |
second-min-size: 0 | |
; TBD separator-size, separator-color | |
; TBD first-weight [percent!] | |
flags: copy [] ; 'separator , 'first-fixed, second-fixed, undraggable ; TBD transparent, over | |
] | |
actors: [ | |
ON-CREATED: func [face [object!] event [event! none!]] [] ;placeholder | |
] | |
] | |
init: [ | |
face: self | |
;probedo [ "init" face/options] | |
actors: make actors [ | |
flag?: [ ; will be transformed to a function! | |
to logic! find values/flags flag | |
] | |
resize: func [face [object!] size [pair! point2D!] axis [integer!] /local asy total face1 face2] [ | |
asy: system/view/auto-sync? | |
system/view/auto-sync?: no | |
total: size/(axis) - face/bar/size/(axis) - (2 * face/margin/(axis)) ; total fixed size | |
face1: face/face1 | |
face2: face/face2 | |
case [ | |
flag? 'first-fixed [] | |
flag? 'second-fixed [face1/size/(axis): to integer! max face/first-min-size (total - face2/size/(axis)) ] | |
'proportional [face1/size/(axis): to integer! max face/first-min-size min (total - face/second-min-size) max face/first-min-size (to integer! round total * face/ratio)] | |
] | |
face2/size/(axis): to integer! max face/second-min-size (total - face1/size/(axis)) | |
face/bar/offset/(axis): to integer! face/margin/(axis) + face1/size/(axis) | |
face2/offset/(axis): to integer! face/bar/offset/(axis) + face/bar/size/(axis) | |
if flag? 'first-fixed [ | |
face/bar/offset/(axis): to integer! max (face/first-min-size + face/margin/(axis) ) (total + face/margin/(axis) - face2/size/(axis)) | |
face1/size/(axis): to integer! face/bar/offset/(axis) - face/margin/(axis) | |
] | |
face1/size/(3 - axis): to integer! face2/size/(3 - axis): to integer! size/(3 - axis) - (2 * face/margin/(3 - axis)) | |
face/bar/size/(3 - axis): to integer! size/(3 - axis) | |
if all [face/bar/size/(axis) >= 6 flag? 'separator][ | |
face/bar/draw: compose | |
[ | |
anti-alias off | |
fill-pen off | |
pen 255.255.255.100 | |
line (as-pair 3 face/bar/size/y - 2) 3x3 (as-pair face/bar/size/x - 2 3) | |
pen 0.0.0.200 | |
line (as-pair 2 face/bar/size/y - 2) (as-pair face/bar/size/x - 2 face/bar/size/y - 2) (as-pair face/bar/size/x - 2 3) | |
] | |
] | |
system/view/auto-sync?: asy | |
;if face/parent/type = 'window [show face/parent] | |
size ; IMPORTANT for reactions ! | |
] | |
entangle: func ["Activate reactions" face1 face2][ | |
face1/size: face1/actors/resize face1 face1/size face1/axis | |
;if not system/view/auto-sync? [show [face1]] ; | |
] | |
; add custom actor but avoid overwriting existing one | |
old-on-created: :on-created | |
ON-CREATED: func [face [object!] event [event! none!]] [ | |
face/actors/old-on-created face event | |
face/actors/resize face face/size face/axis | |
] | |
] | |
face/actors/flag?: func [flag [word! block!]] bind face/actors/flag? face ;@@ Do I really have to do this binding ?? | |
;if (length? face/pane) <> 2 [do make error! "Splitter panel must contain only 2 faces"] | |
system/catalog/errors/script: make system/catalog/errors/script [ | |
splitter-invalid: copy ["Splitter panel must contain only 2 faces"] | |
] | |
if (length? face/pane) <> 2 [cause-error 'script 'splitter-invalid ""] | |
if face/size/x = 0 [face/size/x: 200] | |
if face/size/y = 0 [face/size/y: 200] | |
face/ratio: face/flags ; store flags | |
face/values: make face/values any [face/options []] | |
face/values/flags: to-block face/values/flags | |
set face face/values | |
face/flags: face/ratio ; restore flags | |
face/options: union trim to block! face/options [style: splitter] | |
if all [face/actors/flag? 'first-fixed face/actors/flag? 'second-fixed][remove find face/values/flags 'second-fixed] | |
face/first-min-size: max 0 face/first-min-size ; only positive values | |
face/second-min-size: max 0 face/second-min-size | |
use [deltax deltay axis space total] [ | |
deltax: absolute face/pane/2/offset/x - face/pane/1/offset/x | |
deltay: absolute face/pane/2/offset/y - face/pane/1/offset/y | |
axis: face/axis: pick [1 2] deltax > deltay | |
total: face/pane/1/size/(axis) + face/pane/2/size/(axis) | |
face/ratio: face/pane/1/size/(axis) / (total + 1e-6) | |
space: face/pane/2/offset - (face/pane/1/offset + face/pane/1/size) | |
space: pick reduce [space/1 space/2] axis ;FIXME: space: any [face/bar-size... | |
face/bar-size: max 0x0 as-pair space space | |
if (face/first-min-size + face/second-min-size) > total [ | |
face/second-min-size: total - face/first-min-size: to integer! total / 2 ; avoid oversize | |
] | |
] | |
face/margin: face/pane/1/offset | |
append face/pane make-face/offset/spec 'base 0x0 compose [ | |
(face/bar-size) ; | |
;10x10 | |
;255.0.0.100 | |
(any [face/color system/view/metrics/colors/panel 128.128.128]) | |
(all [not face/actors/flag? 'undraggable 'loose]) | |
cursor (pick [resize-we resize-ns] face/axis) | |
extra object [origin: 0] | |
with [ | |
actors: object [ | |
on-drag-start: func [face event] [ | |
face/extra/origin: face/offset/(face/parent/axis) | |
] | |
on-drag: func [face event /local asy parent axis delta face1 face2] [ | |
asy: system/view/auto-sync? | |
system/view/auto-sync?: no | |
parent: face/parent | |
axis: parent/axis | |
face/offset/(3 - axis): 0 | |
face/offset/(axis): to integer! min max (parent/margin/(axis) + parent/first-min-size) face/offset/(axis) (parent/size/(axis) - face/size/(axis) - parent/margin/(axis) - parent/second-min-size) | |
delta: face/offset/(axis) - face/extra/origin | |
face1: parent/face1 | |
face2: parent/face2 | |
face1/size/(axis): to integer! max 0 face1/size/(axis) + delta | |
face1/offset/(axis): to integer! face/offset/(axis) - face1/size/(axis) | |
face2/size/(axis): to integer! max 0 face2/size/(axis) - delta | |
face2/offset/(axis): to integer! face/offset/(axis) + face/size/(axis) | |
face/extra/origin: face/offset/(axis) | |
show parent | |
system/view/auto-sync?: asy | |
] | |
on-drop: func [face event /local parent axis] [ | |
parent: face/parent | |
axis: parent/axis | |
parent/ratio: (parent/face1/size/(axis)) / (parent/face1/size/(axis) + parent/face2/size/(axis) + 1e-6) | |
] | |
] | |
] | |
] | |
face/pane/3/options: trim union to block! face/pane/3/options [style: separator] | |
face/pane/1/parent: face | |
face/pane/2/parent: face | |
face/pane/3/parent: face | |
face/face1: face/pane/1 | |
face/face2: face/pane/2 | |
face/bar: face/pane/3 | |
;face/color: green ; to see if resizing is right | |
react/link/later :face/actors/entangle [face face/bar] | |
] | |
] | |
do | |
[ | |
if any [%splitter.red = find/last/tail system/options/script "/" ; It's really me ? | |
system/script/args = "test"] [ | |
;print "" ; open console for debug | |
system/view/auto-sync?: no ; speed up resizing a bit | |
system/view/VID/styles/text: [template: [type: 'text size: 0x0]] | |
win: layout [ | |
title "Splitter face examples" ;@@ I wish I could do : title (system/script/header/title) | |
below | |
h5 "Try to drag the separators (if you can find them ;) ) and resize the window" | |
across | |
radio "Simple" on | |
radio "More" [move p/pane next p/pane show p] | |
return | |
p: panel red [ | |
origin 0x0 | |
tspl1: splitter 600x400 [ | |
;origin 0x0 | |
space 6x6 | |
below | |
splitter [ | |
origin 0x0 space 6x6 | |
below | |
splitter [ | |
origin 0x0 space 6x6 | |
base 287 sky "face 1^/with proportional width" | |
base 287 gray "face 2^/with proportional width" | |
] | |
splitter [ | |
origin 0x0 space 6x6 | |
base 287 sky "face 1^/with fixed width" | |
base 287 gray "face 2^/with resizing width" | |
] options [flags: [first-fixed] ] | |
] | |
splitter [ | |
origin 0x0 space 6x6 | |
base 287 sky "face 1^/with resizing width" | |
base 287 gray "face 2^/with fixed width" | |
] options [flags: [second-fixed] ] | |
] ;options [flags: [enlarged]] ; TBD ? | |
origin 0x0 | |
tspl2: splitter yellow 600x400 [ | |
;origin 0x0 | |
space 7x7 | |
below | |
splitter [ | |
origin 0x0 space 6x6 across middle | |
button "button 1" | |
splitter [ | |
origin 0x0 space 6x6 across middle | |
button "button 2" | |
splitter [ | |
origin 0x0 space 6x6 across middle | |
button "button 3" | |
button "button 4" | |
] | |
] | |
] options [flags: [ undraggable]] | |
splitter [ | |
origin 0x0 space 7x7 | |
base sky "min-size: 100" | |
splitter [ | |
origin 0x0 space 7x7 | |
area {Please do not blame me for a slow or flickering GUI rendering, and for other "hiccups".} wrap | |
base "min-size: 50" | |
] options [second-min-size: 50 flags: [second-fixed separator] ] | |
] options [first-min-size: 100 flags: [first-fixed separator] ] | |
] options [flags: [first-fixed separator] ] | |
] ; p | |
] | |
move p/pane next p/pane ; swap main panels | |
view/flags/options win 'resize [ | |
actors: object [ | |
on-create: func [face][face/data: face/size] ; init value | |
on-focus: func [face event][face/data: face/size] ; store old size | |
on-resize: func [face event /local siz][ | |
siz: face/size - face/data ; compute size difference | |
face/data: face/size ; store new size | |
tspl1/size: tspl2/size: p/size: p/size + (siz * 1x1) | |
if not system/view/auto-sync? [show win] | |
] | |
] | |
] | |
] ; if | |
] ; do |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment