Compare commits

...

230 Commits
v0.9.0 ... main

Author SHA1 Message Date
Alexei Anoshenko da3d963b40 Optimisation 2025-08-23 22:05:12 +03:00
Alexei Anoshenko 469980428c
Merge pull request #13 from anoshenko/bug-fixes/fix-change-listeners-to-string-conversion
Fixed issue while converting change-listeners property value to a string
2025-08-11 14:53:07 +03:00
Mikalai Turankou cccbb75219 Fixed issue while converting change-listeners property value to a string 2025-08-11 14:39:06 +03:00
Alexei Anoshenko 942d7c45d3 Optimisation 2025-08-06 19:31:17 +03:00
Alexei Anoshenko 049a2b365f Updated Button 2025-08-06 17:17:57 +03:00
Alexei Anoshenko c4dfba2796 Bug fixing 2025-08-06 16:02:39 +03:00
Alexei Anoshenko 5cb3841cf8
Merge pull request #12 from anoshenko/bug-fixes/fix-tabslayout-remove-view
Fixed bug while removing child from TabsLayout
2025-08-05 13:11:41 +03:00
Mikalai Turankou 01a9bfeb93 Fixed bug while removing child from TabsLayout 2025-08-05 12:17:26 +03:00
Alexei Anoshenko cb7d11ed15 Bug fixing 2025-08-02 19:33:40 +03:00
Alexei Anoshenko 8fae202d75 Bug fixing 2025-07-23 18:52:00 +03:00
Alexei Anoshenko 7702221672 Bug fixing 2025-07-13 17:22:55 +03:00
Alexei Anoshenko ff8f6f30f2 Bug fixing 2025-07-10 12:54:04 +03:00
Alexei Anoshenko 7cec6e5736 Bug fixing 2025-07-09 12:57:46 +03:00
Alexei Anoshenko 7cc553176f Bug fixing 2025-07-06 19:11:34 +03:00
Alexei Anoshenko 55a86011cd Bug fixing 2025-07-06 12:54:59 +03:00
Alexei Anoshenko 73cc26b54e Optimisation 2025-07-06 11:20:37 +03:00
Alexei Anoshenko 8066fb92ba Added All() iterator and IsEmpty() method to Properties interface 2025-07-03 17:47:05 +03:00
Alexei Anoshenko 4c76000254 Changed DataValue interface
Renamed `ArrayElements() []DataValue` method of DataNode interface to `Array() []DataValue`
Added `ArrayElements() iter.Seq[DataValue]` iterator to DataNode interface
2025-07-03 17:15:43 +03:00
Alexei Anoshenko 86f3f4c731 Added Properties iterator to DataObject 2025-07-03 12:49:41 +03:00
Alexei Anoshenko 90b1d44597 Updated CHANGELOG.md 2025-07-03 11:28:14 +03:00
Alexei Anoshenko 824e1b01ad Added CreatePopupFromResources function 2025-07-03 11:20:53 +03:00
Alexei Anoshenko 83ec4f0a20 Added fmt.Stringer implementation to Popup 2025-07-03 10:42:04 +03:00
Alexei Anoshenko 0e4068bcfb Update popup.go 2025-07-02 11:58:34 +03:00
Alexei Anoshenko d28ee630b6 Update popup.go 2025-07-01 19:03:39 +03:00
Alexei Anoshenko f36ee4a4a7 Merge branch 'main' into feature/fix-animations 2025-07-01 17:38:50 +03:00
Alexei Anoshenko 00ef0b3624 Added Properties interface implementation to Popup interface 2025-07-01 17:34:31 +03:00
Mikalai Turankou 517809d4a8 Fixed compilation issue 2025-07-01 15:24:02 +03:00
mikalai-turankou 098985b342
Merge branch 'main' into feature/fix-animations 2025-07-01 15:13:33 +03:00
Mikalai Turankou 7969124bcd Added conversion to a string of []AnimationProperty 2025-07-01 14:58:52 +03:00
Alexei Anoshenko c3c8b9e858 Changed ParseDataText function return values 2025-06-25 17:42:32 +03:00
Alexei Anoshenko 3090a0e94f Spell Checking 2025-06-25 14:36:04 +03:00
Alexei Anoshenko b0185726db Added ViewCreateListener interface 2025-06-25 13:53:08 +03:00
Alexei Anoshenko 2dd8d8d256 Updated readme 2025-06-24 19:31:38 +03:00
Alexei Anoshenko 4cec7fef26 Update CHANGELOG.md 2025-06-24 18:46:41 +03:00
Alexei Anoshenko e618377c11 Added binding support for canvas "draw-function" 2025-06-24 18:41:56 +03:00
Alexei Anoshenko 73b14ed78a Adde binding parameter to CreateView functions 2025-06-24 13:53:36 +03:00
Alexei Anoshenko bbbaf28aba Optimisation 2025-06-23 16:59:24 +03:00
Alexei Anoshenko 0433f460e4 Added change listener binding 2025-06-22 21:04:01 +03:00
Alexei Anoshenko d633c80155 Optimisation 2025-06-20 14:56:42 +03:00
Alexei Anoshenko cb4d197bb7 Bug fixing 2025-06-19 18:36:44 +03:00
Alexei Anoshenko 2f07584b37 Added binding to View.String() 2025-06-19 17:31:39 +03:00
Alexei Anoshenko 24aeeb515b Added binding support for MediaPlayer error event 2025-06-19 14:51:40 +03:00
Alexei Anoshenko f2fb948325 Improved binding for no args listeners 2025-06-18 14:19:57 +03:00
Alexei Anoshenko 4b00299878 Improved binding for 2 args listeners 2025-06-18 13:24:53 +03:00
Alexei Anoshenko 3c3c09b043 Added binding support 2025-06-17 21:08:16 +03:00
Alexei Anoshenko ec2c5393f1 Updated Readme 2025-06-11 17:54:06 +03:00
Alexei Anoshenko 4f07435637 Added drag-and-drop of files 2025-06-11 14:20:41 +03:00
Alexei Anoshenko b76e3e56d8 Added "drop-effect-allowed" property 2025-06-09 19:35:14 +03:00
Alexei Anoshenko c9744168ba Added "drag effect" property 2025-06-07 12:19:17 +03:00
Alexei Anoshenko 4f1969975d Added drag-and-drop support 2025-06-07 10:51:01 +03:00
Alexei Anoshenko c06214f4ae Fixed TabsLayout 2025-05-25 17:40:35 +03:00
Alexei Anoshenko 0c4aa9bfc6
Merge pull request #10 from anoshenko/bug-fixes/polygon-clip-shape 2025-05-22 11:46:07 +03:00
Alexei Anoshenko 81e826adf7
Merge pull request #9 from anoshenko/bug-fixes/list-checked-items 2025-05-22 11:45:54 +03:00
Mikalai Turankou e96b9193cc Fixed incorrect type of polygon clip shape while converting it to a string 2025-05-21 14:52:57 +03:00
Alexei Anoshenko 561164bc23 Update app_styles.css 2025-05-08 16:00:49 +03:00
Mikalai Turankou f19a0e5c12 Fixed issue with GetListViewCheckedItems() API(doesn't return checked item if list is in SingleCheckbox mode and exactly one item has been selected) 2025-05-06 11:28:05 +03:00
Alexei Anoshenko fdd70ece27 Updated to go 1.24 2025-04-30 12:15:56 +03:00
Alexei Anoshenko d3b07f35ca Bug fixing 2025-03-13 11:50:57 +03:00
Alexei Anoshenko 31489dbc03 Bug fixing 2025-02-07 22:14:21 +03:00
Alexei Anoshenko f49b73e979 Bug fixing 2025-02-07 21:45:02 +03:00
Alexei Anoshenko b6832dac40 updated go.mod 2025-01-21 10:47:03 -05:00
Alexei Anoshenko 5d50d5b772 Bug fining 2024-12-18 19:11:51 +03:00
Alexei Anoshenko 6c0980c46e Merge branch 'v0.18' 2024-12-17 19:14:33 +03:00
Alexei Anoshenko fa984dcf78 Changed ViewByID functions 2024-12-10 18:23:04 +03:00
Alexei Anoshenko 0e0b73fdb9 Bug fixing 2024-12-08 21:11:46 +03:00
Alexei Anoshenko 86e58ef33a Bug fixing 2024-12-08 20:47:43 +03:00
Alexei Anoshenko 848606a3be Added "hide-summary-marker" DetailsView property 2024-12-07 19:24:54 +03:00
Alexei Anoshenko 0bdfe48f09 Update animation.go 2024-12-06 20:00:53 +03:00
Alexei Anoshenko 5971cd9105 Update animation.go 2024-12-06 19:56:51 +03:00
Alexei Anoshenko 28881bac9a Added NewTransitionAnimation, NewAnimation 2024-12-06 19:52:57 +03:00
Alexei Anoshenko 0c2bea9a75 Renamed Animation interface -> AnimationProperty 2024-12-06 19:15:23 +03:00
Alexei Anoshenko 1a60488537 Renamed ViewFilter interface -> FilterProperty 2024-12-06 18:52:34 +03:00
Alexei Anoshenko ec796b3697 Update README.md 2024-12-06 18:44:28 +03:00
Alexei Anoshenko 5039998cf9 Renamed ClipShape -> ClipShapeProperty 2024-12-06 18:38:43 +03:00
Alexei Anoshenko a8242c99fe Updated doc comments 2024-12-05 20:15:39 +03:00
Alexei Anoshenko 0919376f09 Updated CHANGELOG.md 2024-12-04 19:35:28 +03:00
Alexei Anoshenko cccf1e6ee1 Added conic gradient to canvas 2024-12-04 19:27:33 +03:00
Alexei Anoshenko 7bb90e5b2a Added New...Gradient functions 2024-12-04 18:45:08 +03:00
Alexei Anoshenko 5f55d30443 Added NewRadius functions 2024-12-03 11:20:32 +03:00
Alexei Anoshenko 8e20e80899 Added NewBounds function 2024-12-03 10:45:55 +03:00
Alexei Anoshenko 32141b996a Rename ViewShadow interface 2024-12-03 10:25:55 +03:00
Alexei Anoshenko 5efa2f5ae8 Bug fixing 2024-12-02 15:47:06 +03:00
Alexei Anoshenko 8a353f765e Improved SizeUnit and AngleUnit functions 2024-12-02 15:05:49 +03:00
Alexei Anoshenko f81ffe6bed Refactoring 2024-12-01 12:42:38 +03:00
Alexei Anoshenko bed6c1bf41 Optimisation 2024-12-01 12:30:33 +03:00
Alexei Anoshenko f632104d49 Update backgroundConicGradient.go 2024-11-27 16:28:11 +02:00
Alexei Anoshenko 7ce631b6ce Bug fixing 2024-11-27 16:06:12 +02:00
Alexei Anoshenko 27beb1e679 Added "mask" property 2024-11-27 10:32:13 +02:00
Alexei Anoshenko a5577273e6 Update CHANGELOG.md 2024-11-26 12:15:06 +02:00
Alexei Anoshenko 87735b3a4d Update CHANGELOG.md 2024-11-26 12:11:39 +02:00
Alexei Anoshenko e9937a8f3a Update popup.go 2024-11-26 11:56:52 +02:00
Alexei Anoshenko bc6e0c4db9 Added Popup show/hide animation 2024-11-25 22:13:29 +02:00
Alexei Anoshenko 5cc4475370 * Added "push-perspective", "push-rotate-x", "push-rotate-y", "push-rotate-z", "push-rotate", "push-skew-x", "push-skew-y", "push-scale-x", "push-scale-y", "push-scale-z", "push-translate-x", "push-translate-y", "push-translate-z" properties. 2024-11-25 12:03:08 +02:00
Alexei Anoshenko bdc8472600 Cleanup 2024-11-24 21:15:05 +02:00
Alexei Anoshenko 91093637c7 Update stackLayout.go 2024-11-24 21:14:35 +02:00
Alexei Anoshenko 488368de8c Optimisation 2024-11-24 17:23:24 +02:00
Alexei Anoshenko 6b2a5b4aee Optimisation 2024-11-24 15:43:31 +02:00
Alexei Anoshenko ed639c94c6 Improved StackLayout push/pop animation 2024-11-24 08:52:43 +02:00
Alexei Anoshenko 31c07ced98 Bug fixing 2024-11-22 15:36:08 +02:00
Alexei Anoshenko e8da32fca8 Bug fixing 2024-11-21 17:50:58 +02:00
Alexei Anoshenko 32f0f83ebf Bug fixing 2024-11-21 15:48:21 +03:00
Alexei Anoshenko 84a00af595 Improved DetailsView 2024-11-21 08:25:46 +02:00
Alexei Anoshenko 7d4b90769f Bug fixing 2024-11-20 15:06:59 +02:00
Alexei Anoshenko 857ad69260 Transform interface renamed to TransformProperty. TransformTag constant renamed to Transform. 2024-11-18 16:35:21 +02:00
Alexei Anoshenko 0f2e7e55ea OriginX, OriginY, and OriginZ properties renamed to TransformOriginX, TransformOriginY, and TransformOriginZ. GetOrigin function renamed to GetTransformOrigin 2024-11-18 16:20:25 +02:00
Alexei Anoshenko ff6b7c7e67
Merge pull request #7 from anoshenko/feature/fix-view-radius-setting 2024-11-14 14:20:13 +03:00
Mikalai Turankou bdd722ba09 Fixed issue while setting the view radius corner values from resource file 2024-11-14 12:26:35 +03:00
Alexei Anoshenko e2775d52f2 Added PropertyName type 2024-11-13 12:56:39 +03:00
Alexei Anoshenko 8fcc52de63 Added LineJoin and LineCap type 2024-10-28 13:11:43 +03:00
Alexei Anoshenko b65b7f6df8 Added comments 2024-10-21 18:37:35 +03:00
Alexei Anoshenko daf41dd7e0 Update CHANGELOG.md 2024-10-17 18:08:35 +03:00
Alexei Anoshenko d392d5214b Bug fixing 2024-10-17 18:07:17 +03:00
Alexei Anoshenko 7ac196c549 Bug fixing 2024-09-25 13:45:47 +03:00
Mikalai Turankou f239af2324 Added more comments for the constants which represent UI element's properties and events 2024-09-18 13:50:06 +03:00
Mikalai Turankou f9822a22f2 Fixed typo 2024-09-16 14:11:54 +03:00
Mikalai Turankou 1a21487540 Added missing comments for exported types like constants, variables, functions, structs and interfaces 2024-09-12 14:05:11 +03:00
Alexei Anoshenko 5707ca3088 Added "item-separators" property to DropDownList and GetDropDownItemSeparators function 2024-09-03 19:55:14 +03:00
Alexei Anoshenko 10cf3a69fc Update defaultTheme.rui 2024-09-03 18:53:14 +03:00
Alexei Anoshenko 1110375cb6 Added support of AccentColor to Checkbox, ListView, and TableView 2024-09-03 15:31:11 +03:00
Alexei Anoshenko 6afb518645 Added GetCheckboxChangedListeners function 2024-09-03 14:51:19 +03:00
Alexei Anoshenko e65c04188c Bug fixing 2024-08-24 19:39:18 +03:00
Alexei Anoshenko 87148836c0 Updated Path
* Added NewPath and NewPathFromSvg methods to Canvas interface
* Removed NewPath function
* Removed Reset methods from Path interface
2024-08-20 20:01:26 +03:00
Alexei Anoshenko 2708c7ceb6 Added RemoveClientItem to Session interface 2024-08-14 18:48:10 +03:00
Alexei Anoshenko a6707495cf Bug fixing 2024-08-13 16:27:54 +03:00
Alexei Anoshenko 7f06a9d201 Added "transform" property and Transform interface 2024-08-13 13:52:47 +03:00
Alexei Anoshenko 1d94d14b1e Update animationRun.go 2024-08-01 22:32:31 +03:00
Alexei Anoshenko ea388467b4 Update README.md 2024-08-01 16:58:31 +03:00
Alexei Anoshenko c9954afa7f Added OpenRawResource function 2024-07-17 16:32:05 +03:00
Alexei Anoshenko 95043b0b9a Fixed animation 2024-07-17 16:30:43 +03:00
Alexei Anoshenko 9b27cb4a55 Updated Animation.String method 2024-07-06 13:04:12 +03:00
anoshenko 5e3d37a6a0 Added Start, Stop, Pause, and Resume methods to Animation interface 2024-07-05 16:41:07 +03:00
anoshenko 2f3de8fce3 Optimised animation 2024-07-01 19:17:03 +03:00
anoshenko 09031b9fa0 Bug fixing 2024-06-29 12:05:27 +03:00
Alexei Anoshenko b2b9befa14 Optimisation 2024-06-26 19:01:00 +03:00
Alexei Anoshenko 00d6e2379d Added "mod", "rem", "round", "round-up", "round-down", and "round-to-zero" SizeFunc functions 2024-06-26 18:45:47 +03:00
Alexei Anoshenko d1d8c2af37 Bug fixing 2024-06-19 16:36:50 +03:00
Alexei Anoshenko 27ebaf1bfe Bug fixing 2024-06-13 09:37:48 +03:00
Alexei Anoshenko a471df2471 Added AutoCertDomain param 2024-06-11 15:03:13 +03:00
anoshenko 658142d3f1 Added "text-wrap" property 2024-06-06 17:33:55 +03:00
anoshenko d0d81907eb Added GridAdapter 2024-06-06 16:20:15 +03:00
anoshenko f9d7c77500 Can use ListAdapter as "content" property value of ListLayout 2024-06-03 18:31:25 +03:00
anoshenko a9877d99b8 Bug fixing 2024-05-29 15:31:58 +03:00
Alexei Anoshenko 5edeca2286 Bug fixing 2024-05-20 16:51:30 +03:00
Alexei Anoshenko b4d1e34f21 Added "data-list" property 2024-05-18 18:57:41 +03:00
Alexei Anoshenko fd516af017 Bug fixing 2024-05-16 11:13:45 +03:00
Alexei Anoshenko b1628023f7 Excluding default properties of CustomView from the result of String() 2024-05-02 15:07:57 +03:00
anoshenko 918fbf3473 Update httpHandler.go 2024-04-29 12:32:24 +03:00
Alexei Anoshenko cbca1e7c87
Merge pull request #4 from anoshenko/0.14
0.14
2024-04-29 12:28:54 +03:00
Alexei Anoshenko a9f59e3080
Merge branch 'main' into 0.14 2024-04-29 12:28:39 +03:00
anoshenko 345850b552 Added SocketAutoClose property to AppParams 2024-04-27 16:16:30 +03:00
anoshenko dcc69ad777 Added "cell-vertical-self-align", and "cell-horizontal-self-align" properties 2024-04-24 19:26:57 +03:00
anoshenko 8bfa759230 Bug fixing 2024-04-24 13:49:16 +03:00
anoshenko 9ac68ac0c9 Updated docs 2024-04-23 19:34:36 +03:00
anoshenko b1f085b891 Bug fixing 2024-04-23 18:24:51 +03:00
anoshenko 9e4fdccd79 Upgrade go.mod 2024-04-23 12:02:30 +03:00
Alexei Anoshenko 7fd6a7985e Bug fixing 2024-04-22 20:03:40 +03:00
Alexei Anoshenko 50e5b8d44d Bug fixing 2024-04-22 19:17:04 +03:00
Alexei Anoshenko 856b09b04b Fixed "background" property 2024-04-22 19:14:58 +03:00
Alexei Anoshenko 0740a48346 Fixing "hint" property 2024-04-22 16:35:18 +03:00
anoshenko befb2a7484 Added StartTimer and StopTimer to Session 2024-04-22 13:09:35 +03:00
Alexei Anoshenko 8268d6ba3c Create httpHandler.go 2024-04-12 17:29:33 +03:00
Alexei Anoshenko 18e053693c Bug fixing 2024-04-12 16:10:45 +03:00
Alexei Anoshenko 589b05ce18 Bug fixing 2024-04-12 15:43:09 +03:00
Alexei Anoshenko d7cc08018b Bug fixing 2024-04-12 15:34:42 +03:00
anoshenko 6c49f37f68 Bug fixing 2024-03-13 15:01:02 +03:00
anoshenko ebcba7f9c2 Added NoSocket parameter of the app 2024-03-12 19:32:22 +03:00
Alexei Anoshenko 30c915d73b Bug fixing 2024-02-27 17:08:05 +03:00
Alexei Anoshenko d07b24c953 Added writeMutex to wsBridge 2024-02-10 12:25:01 +03:00
Alexei Anoshenko 5354ec6ea1 Refactoring of js code 2024-01-15 08:13:46 -05:00
Alexei Anoshenko d7f0dd4358 Update README.md 2023-11-12 17:25:49 +02:00
anoshenko d2204a8627 Changed the type of KeyEvent.Code field 2023-05-29 17:19:47 +03:00
Alexei Anoshenko c44093e6f4 Added key codes 2023-05-29 15:28:22 +03:00
anoshenko 600f56d8ea Added support for height and width range in media styles 2023-05-26 20:08:25 +03:00
anoshenko 3d44aa3ba3 Bug fixing 2023-05-18 12:26:54 +03:00
anoshenko dc2ea14cac Added SetHotKey function to Session interface 2023-05-15 16:19:33 +03:00
anoshenko ab421b4c32 Added PopupButtonType 2023-05-15 15:27:37 +03:00
anoshenko 904859ff6e Added ReloadCell and ReloadTableViewCell functions 2023-05-14 17:53:26 +03:00
anoshenko 3bf3b9c2ba Update theme.go 2023-05-10 15:52:56 +03:00
anoshenko 1cf0ee0bae Bug fixing 2023-05-07 20:58:51 +03:00
anoshenko b15305d727 Changed FocusView function 2023-05-07 19:44:28 +03:00
anoshenko 7c0449775c Bug fixing 2023-05-07 19:26:02 +03:00
anoshenko 4ec3fe2ff2 Optimisation 2023-05-04 16:45:03 +03:00
anoshenko b6b5183f21 Bug fixing 2023-05-04 13:37:26 +03:00
anoshenko f75435eb6c Bug fixing 2023-05-03 15:16:03 +03:00
anoshenko b7a0aa9a6d Bug fixing 2023-05-02 18:58:57 +03:00
anoshenko 43a8b9fe58 Bug fixing & optimisation 2023-05-02 17:20:01 +03:00
anoshenko 43cb889ab1 Bug fixing 2023-05-02 14:49:35 +03:00
anoshenko dec7e723ef Bug fixing 2023-04-25 17:33:08 +03:00
anoshenko 677e0e8692 Update CHANGELOG.md 2023-04-25 17:30:21 +03:00
anoshenko a83d634f54 Added "tooltip" property 2023-04-25 17:20:47 +03:00
anoshenko ac8bb47677 Added the ViewIndex function to the ViewsContainer interface 2023-04-24 21:13:45 +03:00
anoshenko f3c9bf7f56 Changed the main TimePicker listener 2023-04-23 18:54:53 +03:00
anoshenko 4e7dd37f6a Changed the main DatePicker listener 2023-04-23 18:47:07 +03:00
anoshenko 05fa725003 Changed the main NumberPicker listener 2023-04-23 18:39:25 +03:00
anoshenko 763de29698 Changed the main ColorPicker listener 2023-04-23 18:27:04 +03:00
anoshenko 2a480cc6ac Changed the main DropDownList and EditView listener 2023-04-23 18:07:01 +03:00
anoshenko eac3379fb1 Added "outline-offset" property 2023-04-23 13:41:26 +03:00
anoshenko 372f5971e8 Fixed SvgImageView 2023-04-23 12:49:32 +03:00
anoshenko d1b30c56da Optimisation 2023-04-23 12:32:03 +03:00
Alexei Anoshenko 62910bf41f Update resources.go 2023-04-20 09:16:16 +03:00
Alexei Anoshenko 644ec6d8c5 Added InlineImageFromResource function 2023-01-24 17:41:56 -05:00
Alexei Anoshenko 42b34ac4df Added SvgImageView 2023-01-24 17:06:36 -05:00
Alexei Anoshenko b99c39f947
Merge pull request #3 from anoshenko/0.11.0
0.11.0
2023-01-03 15:08:09 +03:00
anoshenko 01e2e2e00b Added "column-span-all" property 2023-01-03 14:56:57 +03:00
anoshenko c7a7b3ed1e Added "column-fill" property 2022-12-23 17:27:14 +03:00
anoshenko 3993dbad20 Updated readme 2022-12-20 18:55:54 +03:00
anoshenko 3c3271663d Added "tabindex" property 2022-12-20 18:38:39 +03:00
anoshenko c5485c27c2 Updated readme 2022-12-20 17:48:00 +03:00
anoshenko 60783f2f22 Updated readme 2022-12-19 19:18:35 +03:00
anoshenko e5842180ef Added mix-blend-mode and background-blend-mode 2022-12-19 18:31:35 +03:00
anoshenko 9fe570ec22 Added "order" property 2022-12-18 18:37:36 +03:00
anoshenko c31b2f9d8c PropertyWithTag method of DataObject renamed to PropertyByTag 2022-12-18 18:22:58 +03:00
anoshenko d3002ced0e Bug fixing 2022-11-23 15:10:29 +03:00
anoshenko 7bb4da32bf Added "srcset" property to ImageView 2022-11-11 12:55:58 +03:00
anoshenko ff5c73b7c3 Updated log functions 2022-11-09 13:40:08 +03:00
anoshenko ecbcda7a53 Bug fixing 2022-11-08 16:31:21 +03:00
anoshenko 9f084cc3f2 Optimisation 2022-11-03 14:33:54 +03:00
anoshenko 12df67cfb4 Update wasmBridge.go 2022-11-02 23:16:45 +03:00
anoshenko 8943be8e91 Updated Image for wasm 2022-11-02 21:22:15 +03:00
anoshenko c0f60e7bdd Updated wasmBridge 2022-11-02 21:05:55 +03:00
anoshenko 4e363d03a5 Renamed "runFunc" to "callFunc" 2022-11-02 20:10:19 +03:00
anoshenko 8200f98d0d Canvas refactoring 2022-11-02 20:08:02 +03:00
anoshenko b26525e892 Updated wasm support 2022-11-01 20:13:09 +03:00
anoshenko b4032b31e0 Optimisation 2022-10-31 16:07:59 +03:00
anoshenko f8d797a4c1 Optimisation 2022-10-30 17:22:33 +03:00
anoshenko 76413c931a The Canvas.TextWidth method replaced by Canvas.TextMetrics 2022-10-30 12:35:22 +03:00
anoshenko 8216ce192a Bug fixing 2022-10-29 21:08:51 +03:00
anoshenko 45c798d14c bug fixing 2022-10-29 20:48:03 +03:00
anoshenko d096a35af9 Optimisation 2022-10-29 20:16:40 +03:00
Alexei Anoshenko 4523a9a427 Added wasm support 2022-10-27 16:14:30 +03:00
120 changed files with 30224 additions and 15005 deletions

View File

@ -1,3 +1,153 @@
# v0.20.0
* Added support of binding
* Added "binding" argument to CreateViewFromResources, CreateViewFromText, and CreateViewFromObject functions
* Added CreatePopupFromResources, CreatePopupFromText, and CreatePopupFromObject functions
* Added All() iterator and IsEmpty() method to Properties interface
* Added implementation of Properties interface to Popup
* Changed ParseDataText function return values
* Added `Properties() iter.Seq[DataNode]` iterator to DataObject interface
* Renamed `ArrayElements() []DataValue` method of DataNode interface to `Array() []DataValue`
* Added `ArrayElements() iter.Seq[DataValue]` iterator to DataNode interface
# v0.19.0
* Added support of drag-and-drop
* Added LoadFile method to View interface
# v0.18.2
* fixed typo: GetShadowProperties -> GetShadowProperty
# v0.18.0
* Property name type changed from string to PropertyName.
* Renamed:
Transform interface -> TransformProperty
NewTransform function -> NewTransformProperty
TransformTag constant -> Transform.
"origin-x" property -> "transform-origin-x"
"origin-y" property -> "transform-origin-y"
"origin-z" property -> "transform-origin-z"
GetOrigin function -> GetTransformOrigin.
BorderBoxClip constant -> BorderBox
PaddingBoxClip constant -> PaddingBox
ContentBoxClip constant -> ContentBox.
ViewShadow interface -> ShadowProperty
NewViewShadow function -> NewShadow
NewInsetViewShadow function -> NewInsetShadow
NewShadowWithParams function -> NewShadowProperty
NewColumnSeparator function -> NewColumnSeparatorProperty
ClipShape interface -> ClipShapeProperty
InsetClip function -> NewInsetClip
CircleClip function -> NewCircleClip
EllipseClip function -> NewEllipseClip
PolygonClip function -> NewPolygonClip
PolygonPointsClip function -> NewPolygonPointsClip
ViewFilter interface -> FilterProperty
NewViewFilter function -> NewFilterProperty
Animation interface -> AnimationProperty
AnimationTag constant -> Animation
NewAnimation function -> NewAnimationProperty
* Added functions: NewBounds, NewEllipticRadius, NewRadii, NewLinearGradient, NewCircleRadialGradient,
NewEllipseRadialGradient, GetPushTransform, GetPushDuration, GetPushTiming, IsMoveToFrontAnimation,
GetBackground, GetMask, GetBackgroundClip,GetBackgroundOrigin, GetMaskClip, GetMaskOrigin, NewColumnSeparator,
NewClipShapeProperty, NewTransitionAnimation, NewAnimation, IsSummaryMarkerHidden.
* Changed ViewByID functions
* Added SetConicGradientFillStyle and SetConicGradientStrokeStyle methods to Canvas interface.
* Changed Push, Pop, MoveToFront, and MoveToFrontByID methods of StackLayout interface.
* Removed DefaultAnimation, StartToEndAnimation, EndToStartAnimation, TopDownAnimation, and BottomUpAnimation constants.
* Added StackLayout properties: "push-transform", "push-duration", "push-timing", "move-to-front-animation", "push-perspective",
"push-rotate-x", "push-rotate-y", "push-rotate-z", "push-rotate", "push-skew-x", "push-skew-y",
"push-scale-x", "push-scale-y", "push-scale-z", "push-translate-x", "push-translate-y", "push-translate-z".
* Added "show-opacity", "show-transform", "show-duration", and "show-timing" Popup properties.
* Added "mask", "mask-clip", "mask-origin", and "background-origin" properties.
* Added "hide-summary-marker" DetailsView property.
* Added LineJoin type. Type of constants MiterJoin, RoundJoin, and BevelJoin changed to LineJoin. Type of Canvas.SetLineJoin function argument changed to LineJoin.
* Added LineCap type. Type of constants ButtCap, RoundCap, and SquareCap changed to LineCap. Type of Canvas.SetLineCap function argument changed to LineCap.
# v0.17.3
Added SetParams method to View interface
# v0.17.0
* Added "mod", "rem", "round", "round-up", "round-down", and "round-to-zero" SizeFunc functions
* Added ModSize, RemSize, RoundSize, RoundUpSize, RoundDownSize, and RoundToZeroSize functions
* Added Start, Stop, Pause, and Resume methods to Animation interface
* Added "transform" property and Transform interface
* Added OpenRawResource, GetCheckboxChangedListeners functions
* Added RemoveClientItem method to Session interface
* Added "item-separators" property to DropDownList and GetDropDownItemSeparators function
* Added NewPath and NewPathFromSvg methods to Canvas interface
* Removed NewPath function
* Removed Reset methods from Path interface
# v0.16.0
* Can use ListAdapter as "content" property value of ListLayout
* The IsListItemEnabled method of the ListAdapter interface has been made optional
* Can use GridAdapter as "content" property value of GridLayout
* Added "text-wrap" property and GetGetTextWrap function
* Bug fixing
# v0.15.0
* Added "data-list" property
* Bug fixing
# v0.14.0
* Added the ability to work without creating a WebSocket. Added NoSocket property to AppParams.
* Added SocketAutoClose property to AppParams.
* Added the ability to run a timer on the client side. Added StartTimer and StopTimer methods to Session interface.
* Added "cell-vertical-self-align", and "cell-horizontal-self-align" properties
* Bug fixing
# v0.13.x
* Added NewHandler function
* Bug fixing
# v0.13.0
* Added SetHotKey function to Session interface
* Added ViewIndex function to ViewsContainer interface
* Added ReloadCell function to TableView interface
* Added ReloadTableViewCell function
* Added "tooltip" property and GetTooltip function
* Added "outline-offset" property and GetOutlineOffset function
* Changed the main event listener format for "drop-down-event", "edit-text-changed",
"color-changed", "number-changed", "date-changed", and "time-changed" events.
Old format is "<listener>(<view>, <new value>)", new format is "<listener>(<view>, <new value>, <old value>)"
* Changed FocusView function
* Added support for height and width range in media styles.
Changed MediaStyle, SetMediaStyle, and MediaStyles functions of Theme interface
* Bug fixing
# v0.12.0
* Added SvgImageView
* Added InlineImageFromResource function
# v0.11.0
* Added "tabindex", "order", "column-fill", "column-span-all", "background-blend-mode", and "mix-blend-mode" properties
* Added GetTabIndex, GetOrder, GetColumnFill, IsColumnSpanAll, GetBackgroundBlendMode, and GetMixBlendMode functions
* ClientItem, SetClientItem, and RemoveAllClientItems method added to Session interface
* PropertyWithTag method of DataObject interface renamed to PropertyByTag
# v0.10.0
* The Canvas.TextWidth method replaced by Canvas.TextMetrics
* Added support of WebAssembly
# v0.9.0 # v0.9.0
* Requires go 1.18 or higher * Requires go 1.18 or higher
@ -68,7 +218,7 @@
# v0.2.0 # v0.2.0
* Added "animation" and "transition" properties, Animation interface, animation events * Added "animation" and "transition" properties, Animation interface, animation events
* Renamed ColorPropery constant to ColorTag * Renamed ColorProperty constant to ColorTag
* Updated readme * Updated readme
* Added the Animation example to the demo * Added the Animation example to the demo
* Bug fixing * Bug fixing

File diff suppressed because it is too large Load Diff

997
README.md

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@ package rui
import "strings" import "strings"
// AbsoluteLayout - list-container of View // AbsoluteLayout represent an AbsoluteLayout view where child views can be arbitrary positioned
type AbsoluteLayout interface { type AbsoluteLayout interface {
ViewsContainer ViewsContainer
} }
@ -20,7 +20,8 @@ func NewAbsoluteLayout(session Session, params Params) AbsoluteLayout {
} }
func newAbsoluteLayout(session Session) View { func newAbsoluteLayout(session Session) View {
return NewAbsoluteLayout(session, nil) //return NewAbsoluteLayout(session, nil)
return new(absoluteLayoutData)
} }
// Init initialize fields of ViewsContainer by default values // Init initialize fields of ViewsContainer by default values
@ -34,7 +35,7 @@ func (layout *absoluteLayoutData) htmlSubviews(self View, buffer *strings.Builde
if layout.views != nil { if layout.views != nil {
for _, view := range layout.views { for _, view := range layout.views {
view.addToCSSStyle(map[string]string{`position`: `absolute`}) view.addToCSSStyle(map[string]string{`position`: `absolute`})
viewHTML(view, buffer) viewHTML(view, buffer, "")
} }
} }
} }

View File

@ -1,6 +1,7 @@
package rui package rui
import ( import (
"errors"
"fmt" "fmt"
"math" "math"
"strconv" "strconv"
@ -11,43 +12,51 @@ import (
// Can take the following values: Radian, Degree, Gradian, and Turn // Can take the following values: Radian, Degree, Gradian, and Turn
type AngleUnitType uint8 type AngleUnitType uint8
// Constants which represent values or the [AngleUnitType]
const ( const (
// Radian - angle in radians // Radian - angle in radians
Radian AngleUnitType = 0 Radian AngleUnitType = 0
// Radian - angle in radians * π // Radian - angle in radians * π
PiRadian AngleUnitType = 1 PiRadian AngleUnitType = 1
// Degree - angle in degrees // Degree - angle in degrees
Degree AngleUnitType = 2 Degree AngleUnitType = 2
// Gradian - angle in gradian (1400 of a full circle) // Gradian - angle in gradian (1400 of a full circle)
Gradian AngleUnitType = 3 Gradian AngleUnitType = 3
// Turn - angle in turns (1 turn = 360 degree) // Turn - angle in turns (1 turn = 360 degree)
Turn AngleUnitType = 4 Turn AngleUnitType = 4
) )
// AngleUnit describe a size (Value field) and size unit (Type field). // AngleUnit used to represent an angular values
type AngleUnit struct { type AngleUnit struct {
Type AngleUnitType // Type of the angle value
Type AngleUnitType
// Value of the angle in Type units
Value float64 Value float64
} }
// Deg creates AngleUnit with Degree type // Deg creates AngleUnit with Degree type
func Deg(value float64) AngleUnit { func Deg[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) AngleUnit {
return AngleUnit{Type: Degree, Value: value} return AngleUnit{Type: Degree, Value: float64(value)}
} }
// Rad create AngleUnit with Radian type // Rad create AngleUnit with Radian type
func Rad(value float64) AngleUnit { func Rad[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) AngleUnit {
return AngleUnit{Type: Radian, Value: value} return AngleUnit{Type: Radian, Value: float64(value)}
} }
// PiRad create AngleUnit with PiRadian type // PiRad create AngleUnit with PiRadian type
func PiRad(value float64) AngleUnit { func PiRad[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) AngleUnit {
return AngleUnit{Type: PiRadian, Value: value} return AngleUnit{Type: PiRadian, Value: float64(value)}
} }
// Grad create AngleUnit with Gradian type // Grad create AngleUnit with Gradian type
func Grad(value float64) AngleUnit { func Grad[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) AngleUnit {
return AngleUnit{Type: Gradian, Value: value} return AngleUnit{Type: Gradian, Value: float64(value)}
} }
// Equal compare two AngleUnit. Return true if AngleUnit are equal // Equal compare two AngleUnit. Return true if AngleUnit are equal
@ -78,6 +87,10 @@ func StringToAngleUnit(value string) (AngleUnit, bool) {
func stringToAngleUnit(value string) (AngleUnit, error) { func stringToAngleUnit(value string) (AngleUnit, error) {
value = strings.ToLower(strings.Trim(value, " \t\n\r")) value = strings.ToLower(strings.Trim(value, " \t\n\r"))
if value == "" {
return AngleUnit{}, errors.New(`invalid AngleUnit value: ""`)
}
setValue := func(suffix string, unitType AngleUnitType) (AngleUnit, error) { setValue := func(suffix string, unitType AngleUnitType) (AngleUnit, error) {
val, err := strconv.ParseFloat(value[:len(value)-len(suffix)], 64) val, err := strconv.ParseFloat(value[:len(value)-len(suffix)], 64)
if err != nil { if err != nil {

File diff suppressed because it is too large Load Diff

View File

@ -1,170 +1,181 @@
package rui package rui
import "strings" // Constants which describe values for view's animation events properties
const ( const (
// TransitionRunEvent is the constant for "transition-run-event" property tag. // TransitionRunEvent is the constant for "transition-run-event" property tag.
// The "transition-run-event" is fired when a transition is first created, //
// i.e. before any transition delay has begun. // Used by View:
TransitionRunEvent = "transition-run-event" // Is fired when a transition is first created, i.e. before any transition delay has begun.
//
// General listener format:
// func(view rui.View, propertyName rui.PropertyName).
//
// where:
// - view - Interface of a view which generated this event,
// - propertyName - Name of the property.
//
// Allowed listener formats:
// func(view rui.View),
// func(propertyName rui.PropertyName)
// func().
TransitionRunEvent PropertyName = "transition-run-event"
// TransitionStartEvent is the constant for "transition-end-event" property tag. // TransitionStartEvent is the constant for "transition-start-event" property tag.
// The "transition-start-event" is fired when a transition has actually started, //
// i.e., after "delay" has ended. // Used by View:
TransitionStartEvent = "transition-start-event" // Is fired when a transition has actually started, i.e., after "delay" has ended.
//
// General listener format:
// func(view rui.View, propertyName rui.PropertyName).
//
// where:
// - view - Interface of a view which generated this event,
// - propertyName - Name of the property.
//
// Allowed listener formats:
// func(view rui.View)
// func(propertyName rui.PropertyName)
// func()
TransitionStartEvent PropertyName = "transition-start-event"
// TransitionEndEvent is the constant for "transition-end-event" property tag. // TransitionEndEvent is the constant for "transition-end-event" property tag.
// The "transition-end-event" is fired when a transition has completed. //
TransitionEndEvent = "transition-end-event" // Used by View:
// Is fired when a transition has completed.
//
// General listener format:
// func(view rui.View, propertyName rui.PropertyName).
//
// where:
// - view - Interface of a view which generated this event,
// - propertyName - Name of the property.
//
// Allowed listener formats:
// func(view rui.View)
// func(propertyName rui.PropertyName)
// func()
TransitionEndEvent PropertyName = "transition-end-event"
// TransitionCancelEvent is the constant for "transition-cancel-event" property tag. // TransitionCancelEvent is the constant for "transition-cancel-event" property tag.
// The "transition-cancel-event" is fired when a transition is cancelled. The transition is cancelled when: //
// * A new property transition has begun. // Used by View:
// * The "visibility" property is set to "gone". // Is fired when a transition is cancelled. The transition is cancelled when:
// * The transition is stopped before it has run to completion, e.g. by moving the mouse off a hover-transitioning view. // - A new property transition has begun.
TransitionCancelEvent = "transition-cancel-event" // - The "visibility" property is set to "gone".
// - The transition is stopped before it has run to completion, e.g. by moving the mouse off a hover-transitioning view.
//
// General listener format:
// func(view rui.View, propertyName rui.PropertyName).
//
// where:
// - view - Interface of a view which generated this event,
// - propertyName - Name of the property.
//
// Allowed listener formats:
// func(view rui.View)
// func(propertyName rui.PropertyName)
// func()
TransitionCancelEvent PropertyName = "transition-cancel-event"
// AnimationStartEvent is the constant for "animation-start-event" property tag. // AnimationStartEvent is the constant for "animation-start-event" property tag.
// The "animation-start-event" is fired when an animation has started. //
// If there is an animation-delay, this event will fire once the delay period has expired. // Used by View:
AnimationStartEvent = "animation-start-event" // Fired when an animation has started. If there is an "animation-delay", this event will fire once the delay period has
// expired.
//
// General listener format:
// func(view rui.View, animationId string).
//
// where:
// - view - Interface of a view which generated this event,
// - animationId - Id of the animation.
//
// Allowed listener formats:
// func(view rui.View)
// func(animationId string)
// func()
AnimationStartEvent PropertyName = "animation-start-event"
// AnimationEndEvent is the constant for "animation-end-event" property tag. // AnimationEndEvent is the constant for "animation-end-event" property tag.
// The "animation-end-event" is fired when aт фnimation has completed. //
// If the animation aborts before reaching completion, such as if the element is removed // Used by View:
// or the animation is removed from the element, the "animation-end-event" is not fired. // Fired when an animation has completed. If the animation aborts before reaching completion, such as if the element is
AnimationEndEvent = "animation-end-event" // removed or the animation is removed from the element, the "animation-end-event" is not fired.
//
// General listener format:
// func(view rui.View, animationId string).
//
// where:
// - view - Interface of a view which generated this event,
// - animationId - Id of the animation.
//
// Allowed listener formats:
// func(view rui.View)
// func(animationId string)
// func()
AnimationEndEvent PropertyName = "animation-end-event"
// AnimationCancelEvent is the constant for "animation-cancel-event" property tag. // AnimationCancelEvent is the constant for "animation-cancel-event" property tag.
// The "animation-cancel-event" is fired when an animation unexpectedly aborts. //
// In other words, any time it stops running without sending the "animation-end-event". // Used by View:
// This might happen when the animation-name is changed such that the animation is removed, // Fired when an animation unexpectedly aborts. In other words, any time it stops running without sending the
// or when the animating view is hidden. Therefore, either directly or because any of its // "animation-end-event". This might happen when the animation-name is changed such that the animation is removed, or when
// containing views are hidden. // the animating view is hidden. Therefore, either directly or because any of its containing views are hidden. The event
// The event is not supported by all browsers. // is not supported by all browsers.
AnimationCancelEvent = "animation-cancel-event" //
// General listener format:
// func(view rui.View, animationId string).
//
// where:
// - view - Interface of a view which generated this event,
// - animationId - Id of the animation.
//
// Allowed listener formats:
// func(view rui.View)
// func(animationId string)
// func()
AnimationCancelEvent PropertyName = "animation-cancel-event"
// AnimationIterationEvent is the constant for "animation-iteration-event" property tag. // AnimationIterationEvent is the constant for "animation-iteration-event" property tag.
// The "animation-iteration-event" is fired when an iteration of an animation ends, //
// and another one begins. This event does not occur at the same time as the animationend event, // Used by View:
// and therefore does not occur for animations with an "iteration-count" of one. // Fired when an iteration of an animation ends, and another one begins. This event does not occur at the same time as the
AnimationIterationEvent = "animation-iteration-event" // animation end event, and therefore does not occur for animations with an "iteration-count" of one.
//
// General listener format:
// func(view rui.View, animationId string).
//
// where:
// - view - Interface of a view which generated this event,
// - animationId - Id of the animation.
//
// Allowed listener formats:
// func(view rui.View)
// func(animationId string)
// func()
AnimationIterationEvent PropertyName = "animation-iteration-event"
) )
var transitionEvents = map[string]struct{ jsEvent, jsFunc string }{ func (view *viewData) handleTransitionEvents(tag PropertyName, data DataObject) {
TransitionRunEvent: {jsEvent: "ontransitionrun", jsFunc: "transitionRunEvent"}, if propertyName, ok := data.PropertyValue("property"); ok {
TransitionStartEvent: {jsEvent: "ontransitionstart", jsFunc: "transitionStartEvent"}, property := PropertyName(propertyName)
TransitionEndEvent: {jsEvent: "ontransitionend", jsFunc: "transitionEndEvent"},
TransitionCancelEvent: {jsEvent: "ontransitioncancel", jsFunc: "transitionCancelEvent"},
}
func (view *viewData) setTransitionListener(tag string, value any) bool {
listeners, ok := valueToEventListeners[View, string](value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeTransitionListener(tag)
} else if js, ok := transitionEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
}
} else {
return false
}
return true
}
func (view *viewData) removeTransitionListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := transitionEvents[tag]; ok {
removeProperty(view.htmlID(), js.jsEvent, view.Session())
}
}
}
func transitionEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range transitionEvents {
if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View, string)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
}
}
}
}
func (view *viewData) handleTransitionEvents(tag string, data DataObject) {
if property, ok := data.PropertyValue("property"); ok {
if tag == TransitionEndEvent || tag == TransitionCancelEvent { if tag == TransitionEndEvent || tag == TransitionCancelEvent {
if animation, ok := view.singleTransition[property]; ok { if animation, ok := view.singleTransition[property]; ok {
delete(view.singleTransition, property) delete(view.singleTransition, property)
if animation != nil { setTransition(view, property, animation)
view.transitions[property] = animation session := view.session
} else { session.updateCSSProperty(view.htmlID(), "transition", transitionCSS(view, session))
delete(view.transitions, property)
}
view.updateTransitionCSS()
} }
} }
for _, listener := range getEventListeners[View, string](view, nil, tag) { for _, listener := range getOneArgEventListeners[View, PropertyName](view, nil, tag) {
listener(view, property) listener.Run(view, property)
} }
} }
} }
var animationEvents = map[string]struct{ jsEvent, jsFunc string }{ func (view *viewData) handleAnimationEvents(tag PropertyName, data DataObject) {
AnimationStartEvent: {jsEvent: "onanimationstart", jsFunc: "animationStartEvent"}, if listeners := getOneArgEventListeners[View, string](view, nil, tag); len(listeners) > 0 {
AnimationEndEvent: {jsEvent: "onanimationend", jsFunc: "animationEndEvent"},
AnimationIterationEvent: {jsEvent: "onanimationiteration", jsFunc: "animationIterationEvent"},
AnimationCancelEvent: {jsEvent: "onanimationcancel", jsFunc: "animationCancelEvent"},
}
func (view *viewData) setAnimationListener(tag string, value any) bool {
listeners, ok := valueToEventListeners[View, string](value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeAnimationListener(tag)
} else if js, ok := animationEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
}
} else {
return false
}
return true
}
func (view *viewData) removeAnimationListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := animationEvents[tag]; ok {
removeProperty(view.htmlID(), js.jsEvent, view.Session())
}
}
}
func animationEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range animationEvents {
if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
}
}
}
}
func (view *viewData) handleAnimationEvents(tag string, data DataObject) {
if listeners := getEventListeners[View, string](view, nil, tag); len(listeners) > 0 {
id := "" id := ""
if name, ok := data.PropertyValue("name"); ok { if name, ok := data.PropertyValue("name"); ok {
for _, animation := range GetAnimation(view) { for _, animation := range GetAnimation(view) {
@ -174,63 +185,135 @@ func (view *viewData) handleAnimationEvents(tag string, data DataObject) {
} }
} }
for _, listener := range listeners { for _, listener := range listeners {
listener(view, id) listener.Run(view, id)
} }
} }
} }
// GetTransitionRunListeners returns the "transition-run-event" listener list. // GetTransitionRunListeners returns the "transition-run-event" listener list.
// If there are no listeners then the empty list is returned. // If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetTransitionRunListeners(view View, subviewID ...string) []func(View, string) { // Result elements can be of the following types:
return getEventListeners[View, string](view, subviewID, TransitionRunEvent) // - func(rui.View, string),
// - func(rui.View),
// - func(string),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTransitionRunListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, string](view, subviewID, TransitionRunEvent)
} }
// GetTransitionStartListeners returns the "transition-start-event" listener list. // GetTransitionStartListeners returns the "transition-start-event" listener list.
// If there are no listeners then the empty list is returned. // If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetTransitionStartListeners(view View, subviewID ...string) []func(View, string) { // Result elements can be of the following types:
return getEventListeners[View, string](view, subviewID, TransitionStartEvent) // - func(rui.View, string),
// - func(rui.View),
// - func(string),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTransitionStartListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, string](view, subviewID, TransitionStartEvent)
} }
// GetTransitionEndListeners returns the "transition-end-event" listener list. // GetTransitionEndListeners returns the "transition-end-event" listener list.
// If there are no listeners then the empty list is returned. // If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetTransitionEndListeners(view View, subviewID ...string) []func(View, string) { // Result elements can be of the following types:
return getEventListeners[View, string](view, subviewID, TransitionEndEvent) // - func(rui.View, string),
// - func(rui.View),
// - func(string),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTransitionEndListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, string](view, subviewID, TransitionEndEvent)
} }
// GetTransitionCancelListeners returns the "transition-cancel-event" listener list. // GetTransitionCancelListeners returns the "transition-cancel-event" listener list.
// If there are no listeners then the empty list is returned. // If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetTransitionCancelListeners(view View, subviewID ...string) []func(View, string) { // Result elements can be of the following types:
return getEventListeners[View, string](view, subviewID, TransitionCancelEvent) // - func(rui.View, string),
// - func(rui.View),
// - func(string),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTransitionCancelListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, string](view, subviewID, TransitionCancelEvent)
} }
// GetAnimationStartListeners returns the "animation-start-event" listener list. // GetAnimationStartListeners returns the "animation-start-event" listener list.
// If there are no listeners then the empty list is returned. // If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetAnimationStartListeners(view View, subviewID ...string) []func(View, string) { // Result elements can be of the following types:
return getEventListeners[View, string](view, subviewID, AnimationStartEvent) // - func(rui.View, string),
// - func(rui.View),
// - func(string),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetAnimationStartListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, string](view, subviewID, AnimationStartEvent)
} }
// GetAnimationEndListeners returns the "animation-end-event" listener list. // GetAnimationEndListeners returns the "animation-end-event" listener list.
// If there are no listeners then the empty list is returned. // If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetAnimationEndListeners(view View, subviewID ...string) []func(View, string) { // Result elements can be of the following types:
return getEventListeners[View, string](view, subviewID, AnimationEndEvent) // - func(rui.View, string),
// - func(rui.View),
// - func(string),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetAnimationEndListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, string](view, subviewID, AnimationEndEvent)
} }
// GetAnimationCancelListeners returns the "animation-cancel-event" listener list. // GetAnimationCancelListeners returns the "animation-cancel-event" listener list.
// If there are no listeners then the empty list is returned. // If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetAnimationCancelListeners(view View, subviewID ...string) []func(View, string) { // Result elements can be of the following types:
return getEventListeners[View, string](view, subviewID, AnimationCancelEvent) // - func(rui.View, string),
// - func(rui.View),
// - func(string),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetAnimationCancelListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, string](view, subviewID, AnimationCancelEvent)
} }
// GetAnimationIterationListeners returns the "animation-iteration-event" listener list. // GetAnimationIterationListeners returns the "animation-iteration-event" listener list.
// If there are no listeners then the empty list is returned. // If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetAnimationIterationListeners(view View, subviewID ...string) []func(View, string) { // Result elements can be of the following types:
return getEventListeners[View, string](view, subviewID, AnimationIterationEvent) // - func(rui.View, string),
// - func(rui.View),
// - func(string),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetAnimationIterationListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, string](view, subviewID, AnimationIterationEvent)
} }

135
animationRun.go Normal file
View File

@ -0,0 +1,135 @@
package rui
import "slices"
func (animation *animationData) Start(view View, listener func(view View, animation AnimationProperty, event PropertyName)) bool {
if view == nil {
ErrorLog("nil View in animation.Start() function")
return false
}
if !animation.hasAnimatedProperty() {
return false
}
animation.view = view
animation.listener = listener
animation.oldAnimation = nil
//if getOneArgEventListeners[View, PropertyName](view, nil, Animation)
if value := view.Get(Animation); value != nil {
if oldAnimation, ok := value.([]AnimationProperty); ok && len(oldAnimation) > 0 {
animation.oldAnimation = oldAnimation
}
}
animation.oldListeners = map[PropertyName][]oneArgListener[View, PropertyName]{}
setListeners := func(event PropertyName, listener func(View, PropertyName)) {
listeners := getOneArgEventListeners[View, PropertyName](view, nil, event)
if len(listeners) > 0 {
animation.oldListeners[event] = slices.Clone(listeners)
}
view.Set(event, append(listeners, newOneArgListenerVE(listener)))
}
setListeners(AnimationStartEvent, animation.onAnimationStart)
setListeners(AnimationEndEvent, animation.onAnimationEnd)
setListeners(AnimationCancelEvent, animation.onAnimationCancel)
setListeners(AnimationIterationEvent, animation.onAnimationIteration)
view.Set(Animation, animation)
return true
}
func (animation *animationData) finish() {
if animation.view != nil {
for _, event := range []PropertyName{AnimationStartEvent, AnimationEndEvent, AnimationCancelEvent, AnimationIterationEvent} {
if listeners, ok := animation.oldListeners[event]; ok && len(listeners) > 0 {
animation.view.Set(event, listeners)
} else {
animation.view.Remove(event)
}
}
if animation.oldAnimation != nil {
animation.view.Set(Animation, animation.oldAnimation)
animation.oldAnimation = nil
} else {
animation.view.Set(Animation, "")
}
animation.oldListeners = map[PropertyName][]oneArgListener[View, PropertyName]{}
animation.view = nil
animation.listener = nil
}
}
func (animation *animationData) Stop() {
animation.onAnimationCancel(animation.view, "")
}
func (animation *animationData) Pause() {
if animation.view != nil {
animation.view.Set(AnimationPaused, true)
}
}
func (animation *animationData) Resume() {
if animation.view != nil {
animation.view.Remove(AnimationPaused)
}
}
func (animation *animationData) onAnimationStart(view View, _ PropertyName) {
if animation.view != nil && animation.listener != nil {
animation.listener(animation.view, animation, AnimationStartEvent)
}
}
func (animation *animationData) onAnimationEnd(view View, _ PropertyName) {
if animation.view != nil {
animationView := animation.view
listener := animation.listener
if value, ok := animation.properties[PropertyTag]; ok {
if props, ok := value.([]AnimatedProperty); ok {
for _, prop := range props {
animationView.setRaw(prop.Tag, prop.To)
}
}
}
animation.finish()
if listener != nil {
listener(animationView, animation, AnimationEndEvent)
}
}
}
func (animation *animationData) onAnimationIteration(view View, _ PropertyName) {
if animation.view != nil && animation.listener != nil {
animation.listener(animation.view, animation, AnimationIterationEvent)
}
}
func (animation *animationData) onAnimationCancel(view View, _ PropertyName) {
if animation.view != nil {
animationView := animation.view
listener := animation.listener
if value, ok := animation.properties[PropertyTag]; ok {
if props, ok := value.([]AnimatedProperty); ok {
for _, prop := range props {
animationView.Set(prop.Tag, prop.To)
}
}
}
animation.finish()
if listener != nil {
listener(animationView, animation, AnimationCancelEvent)
}
}
}

View File

@ -2,7 +2,6 @@ package rui
import ( import (
"fmt" "fmt"
"log"
"runtime" "runtime"
) )
@ -10,14 +9,8 @@ import (
// clients and the server is displayed in the debug log // clients and the server is displayed in the debug log
var ProtocolInDebugLog = false var ProtocolInDebugLog = false
var debugLogFunc func(string) = func(text string) { var debugLogFunc func(string) = debugLog
log.Println("\033[34m" + text) var errorLogFunc func(string) = errorLog
}
var errorLogFunc = func(text string) {
log.Println("\033[31m" + text)
//println(text)
}
// SetDebugLog sets a function for outputting debug info. // SetDebugLog sets a function for outputting debug info.
// The default value is nil (debug info is ignored) // The default value is nil (debug info is ignored)

459
appServer.go Normal file
View File

@ -0,0 +1,459 @@
//go:build !wasm
package rui
import (
"context"
_ "embed"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os/exec"
"runtime"
"strconv"
"strings"
"time"
"golang.org/x/crypto/acme/autocert"
)
//go:embed app_socket.js
var socketScripts string
//go:embed app_post.js
var httpPostScripts string
func debugLog(text string) {
log.Println("\033[34m" + text)
}
func errorLog(text string) {
log.Println("\033[31m" + text)
}
type sessionInfo struct {
session Session
response chan string
}
type application struct {
server *http.Server
params AppParams
createContentFunc func(Session) SessionContent
sessions map[int]sessionInfo
}
func (app *application) getStartPage() string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString("<!DOCTYPE html>\n<html>\n")
getStartPage(buffer, app.params)
buffer.WriteString("\n</html>")
return buffer.String()
}
func (app *application) Params() AppParams {
params := app.params
if params.NoSocket {
params.SocketAutoClose = 0
}
return params
}
func (app *application) Finish() {
for _, session := range app.sessions {
session.session.close()
if session.response != nil {
close(session.response)
session.response = nil
}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := app.server.Shutdown(ctx); err != nil {
log.Println(err.Error())
}
}
func (app *application) nextSessionID() int {
n := rand.Intn(0x7FFFFFFE) + 1
_, ok := app.sessions[n]
for ok {
n = rand.Intn(0x7FFFFFFE) + 1
_, ok = app.sessions[n]
}
return n
}
func (app *application) removeSession(id int) {
if info, ok := app.sessions[id]; ok {
if info.response != nil {
close(info.response)
}
delete(app.sessions, id)
}
}
func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if ProtocolInDebugLog {
DebugLogF("%s %s", req.Method, req.URL.Path)
}
switch req.Method {
case http.MethodPost:
if req.URL.Path == "/" {
app.postHandler(w, req)
}
case http.MethodGet:
switch req.URL.Path {
case "/":
w.WriteHeader(http.StatusOK)
io.WriteString(w, app.getStartPage())
case "/ws":
if bridge := createSocketBridge(w, req); bridge != nil {
go app.socketReader(bridge)
}
case "/script.js":
w.WriteHeader(http.StatusOK)
if app.params.NoSocket {
io.WriteString(w, httpPostScripts)
} else {
io.WriteString(w, socketScripts)
}
io.WriteString(w, "\n")
io.WriteString(w, defaultScripts)
default:
filename := req.URL.Path[1:]
if size := len(filename); size > 0 && filename[size-1] == '/' {
filename = filename[:size-1]
}
if !serveResourceFile(filename, w, req) &&
!serveDownloadFile(filename, w, req) {
w.WriteHeader(http.StatusNotFound)
}
}
}
}
func setSessionIDCookie(w http.ResponseWriter, sessionID int) {
cookie := http.Cookie{
Name: "session",
Value: strconv.Itoa(sessionID),
HttpOnly: true,
}
http.SetCookie(w, &cookie)
}
func (app *application) postHandler(w http.ResponseWriter, req *http.Request) {
if reqBody, err := io.ReadAll(req.Body); err == nil {
message := string(reqBody)
if ProtocolInDebugLog {
DebugLog(message)
}
obj, err := ParseDataText(message)
if err != nil {
ErrorLog(err.Error())
return
}
var session Session = nil
var response chan string = nil
if cookie, err := req.Cookie("session"); err == nil {
sessionID, err := strconv.Atoi(cookie.Value)
if err != nil {
ErrorLog(err.Error())
} else if info, ok := app.sessions[sessionID]; ok && info.response != nil {
response = info.response
session = info.session
}
}
command := obj.Tag()
startSession := false
if session == nil || command == "startSession" {
events := make(chan DataObject, 1024)
bridge := createHttpBridge(req)
response = bridge.response
answer := ""
session, answer = app.startSession(obj, events, bridge, response)
bridge.writeMessage(answer)
session.onStart()
if command == "session-resume" {
session.onResume()
}
bridge.sendResponse()
setSessionIDCookie(w, session.ID())
startSession = true
go sessionEventHandler(session, events, bridge)
}
if !startSession {
switch command {
case "nop":
session.sendResponse()
case "session-close":
session.onFinish()
session.App().removeSession(session.ID())
return
default:
if !session.handleAnswer(command, obj) {
session.addToEventsQueue(obj)
}
}
}
io.WriteString(w, <-response)
for len(response) > 0 {
io.WriteString(w, <-response)
}
}
}
func (app *application) socketReader(bridge *wsBridge) {
var session Session
events := make(chan DataObject, 1024)
for {
message, ok := bridge.readMessage()
if !ok {
events <- NewDataObject("disconnect")
return
}
if ProtocolInDebugLog {
DebugLog("🖥️ -> " + message)
}
obj, err := ParseDataText(message)
if err != nil {
ErrorLog(err.Error())
return
}
switch command := obj.Tag(); command {
case "startSession":
answer := ""
if session, answer = app.startSession(obj, events, bridge, nil); session != nil {
if !bridge.writeMessage(answer) {
return
}
session.onStart()
go sessionEventHandler(session, events, bridge)
}
case "reconnect":
session = nil
if sessionText, ok := obj.PropertyValue("session"); ok {
if sessionID, err := strconv.Atoi(sessionText); err == nil {
if info, ok := app.sessions[sessionID]; ok {
session = info.session
session.setBridge(events, bridge)
go sessionEventHandler(session, events, bridge)
session.onReconnect()
} else {
DebugLogF("Session #%d not exists", sessionID)
}
} else {
ErrorLog(`strconv.Atoi(sessionText) error: ` + err.Error())
}
} else {
ErrorLog(`"session" key not found`)
}
if session == nil {
/* answer := ""
if session, answer = app.startSession(obj, events, bridge, nil); session != nil {
if !bridge.writeMessage(answer) {
return
}
session.onStart()
go sessionEventHandler(session, events, bridge)
bridge.writeMessage("restartSession();")
}
*/
bridge.writeMessage("reloadPage();")
return
}
default:
if !session.handleAnswer(command, obj) {
events <- obj
}
}
}
}
func sessionEventHandler(session Session, events chan DataObject, bridge bridge) {
for {
data := <-events
switch command := data.Tag(); command {
case "disconnect":
session.onDisconnect()
return
case "session-close":
session.onFinish()
session.App().removeSession(session.ID())
bridge.close()
default:
session.handleEvent(command, data)
}
}
}
func (app *application) startSession(params DataObject, events chan DataObject,
bridge bridge, response chan string) (Session, string) {
if app.createContentFunc == nil {
return nil, ""
}
session := newSession(app, app.nextSessionID(), "", params)
session.setBridge(events, bridge)
if !session.setContent(app.createContentFunc(session)) {
return nil, ""
}
app.sessions[session.ID()] = sessionInfo{
session: session,
response: response,
}
answer := allocStringBuilder()
defer freeStringBuilder(answer)
answer.WriteString("sessionID = '")
answer.WriteString(strconv.Itoa(session.ID()))
answer.WriteString("';\n")
session.writeInitScript(answer)
answerText := answer.String()
if ProtocolInDebugLog {
DebugLog("Start session:")
DebugLog(answerText)
}
return session, answerText
}
var apps = []*application{}
// StartApp - create the new application and start it
func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) {
resources.scanDefaultResourcePath()
app := new(application)
app.params = params
app.sessions = map[int]sessionInfo{}
app.createContentFunc = createContentFunc
apps = append(apps, app)
redirectAddr := ""
https := params.AutoCertDomain != "" || (params.CertFile != "" && params.KeyFile != "")
if index := strings.IndexRune(addr, ':'); index >= 0 {
redirectAddr = addr[:index] + ":80"
} else {
redirectAddr = addr + ":80"
if https {
addr += ":443"
} else {
addr += ":80"
}
}
serverRun := func(err error) {
if err != nil {
if err == http.ErrServerClosed {
log.Println(err)
} else {
log.Fatal(err)
}
}
}
if https {
if params.Redirect80 {
redirectTLS := func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://"+addr+r.RequestURI, http.StatusMovedPermanently)
}
go func() {
serverRun(http.ListenAndServe(redirectAddr, http.HandlerFunc(redirectTLS)))
}()
}
if params.AutoCertDomain != "" {
mux := http.NewServeMux()
mux.Handle("/", app)
serverRun(http.Serve(autocert.NewListener(params.AutoCertDomain), mux))
} else {
app.server = &http.Server{Addr: addr}
http.Handle("/", app)
serverRun(app.server.ListenAndServeTLS(params.CertFile, params.KeyFile))
}
} else {
app.server = &http.Server{Addr: addr}
http.Handle("/", app)
serverRun(app.server.ListenAndServe())
}
}
// FinishApp finishes application
func FinishApp() {
for _, app := range apps {
app.Finish()
}
apps = []*application{}
}
// OpenBrowser open browser with specific URL locally. Useful for applications which run on local machine
// or for debug purposes.
func OpenBrowser(url string) bool {
var err error
switch runtime.GOOS {
case "linux":
for _, provider := range []string{"xdg-open", "x-www-browser", "www-browser"} {
if _, err = exec.LookPath(provider); err == nil {
if err = exec.Command(provider, url).Start(); err == nil {
return true
}
}
}
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
return err != nil
}

186
appWasm.go Normal file
View File

@ -0,0 +1,186 @@
//go:build wasm
package rui
import (
_ "embed"
"encoding/base64"
"path/filepath"
"strings"
"syscall/js"
)
//go:embed app_wasm.js
var wasmScripts string
type wasmApp struct {
params AppParams
createContentFunc func(Session) SessionContent
session Session
bridge bridge
close chan DataObject
}
func (app *wasmApp) Finish() {
app.session.close()
}
func (app *wasmApp) Params() AppParams {
params := app.params
params.SocketAutoClose = 0
return params
}
func debugLog(text string) {
js.Global().Get("console").Call("log", text)
}
func errorLog(text string) {
js.Global().Get("console").Call("log", "%c"+text, "color: #F00;")
}
func (app *wasmApp) handleMessage(this js.Value, args []js.Value) any {
if len(args) > 0 {
text := args[0].String()
if ProtocolInDebugLog {
DebugLog(text)
}
if obj := ParseDataText(text); obj != nil {
switch command := obj.Tag(); command {
case "session-close":
app.close <- obj
default:
if !app.session.handleAnswer(command, obj) {
app.session.handleEvent(command, obj)
}
}
}
}
return nil
}
func (app *wasmApp) removeSession(id int) {
}
func (app *wasmApp) createSession() Session {
session := newSession(app, 0, "", ParseDataText(js.Global().Call("sessionInfo", "").String()))
session.setBridge(app.close, app.bridge)
session.setContent(app.createContentFunc(session))
return session
}
func (app *wasmApp) init(params AppParams) {
app.params = params
document := js.Global().Get("document")
body := document.Call("querySelector", "body")
head := document.Call("querySelector", "head")
meta := document.Call("createElement", "meta")
meta.Set("name", "viewport")
meta.Set("content", "width=device-width")
head.Call("appendChild", meta)
meta = document.Call("createElement", "base")
meta.Set("target", "_blank")
meta.Set("rel", "noopener")
head.Call("appendChild", meta)
if params.Icon != "" {
url := params.Icon
if image, ok := resources.images[params.Icon]; ok && image.fs != nil {
dataType := map[string]string{
".svg": "data:image/svg+xml",
".png": "data:image/png",
".jpg": "data:image/jpg",
".jpeg": "data:image/jpg",
".gif": "data:image/gif",
}
ext := strings.ToLower(filepath.Ext(params.Icon))
if prefix, ok := dataType[ext]; ok {
if data, err := image.fs.ReadFile(image.path); err == nil {
url = prefix + ";base64," + base64.StdEncoding.EncodeToString(data)
} else {
DebugLog(err.Error())
}
}
}
meta = document.Call("createElement", "link")
meta.Set("rel", "icon")
meta.Set("href", url)
head.Call("appendChild", meta)
}
script := document.Call("createElement", "script")
script.Set("type", "text/javascript")
script.Set("textContent", defaultScripts+wasmScripts)
body.Call("appendChild", script)
js.Global().Set("sendMessage", js.FuncOf(app.handleMessage))
app.close = make(chan DataObject)
app.session = app.createSession()
style := document.Call("createElement", "style")
css := appStyles + app.session.getCurrentTheme().cssText(app.session)
css = strings.ReplaceAll(css, `\n`, "\n")
css = strings.ReplaceAll(css, `\t`, "\t")
style.Set("textContent", css)
document.Call("querySelector", "head").Call("appendChild", style)
style = document.Call("createElement", "style")
style.Set("id", "ruiAnimations")
document.Call("querySelector", "head").Call("appendChild", style)
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
div := document.Call("createElement", "div")
div.Set("className", "ruiRoot")
div.Set("id", "ruiRootView")
viewHTML(app.session.RootView(), buffer, "")
div.Set("innerHTML", buffer.String())
body.Call("appendChild", div)
div = document.Call("createElement", "div")
div.Set("className", "ruiPopupLayer")
div.Set("id", "ruiPopupLayer")
div.Set("style", "visibility: hidden;")
body.Call("appendChild", div)
div = document.Call("createElement", "a")
div.Set("id", "ruiDownloader")
div.Set("download", "")
div.Set("style", "display: none;")
body.Call("appendChild", div)
if params.TitleColor != 0 {
app.bridge.callFunc("setTitleColor", params.TitleColor.cssString())
}
}
// StartApp - create the new wasmApp and start it
func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) {
if createContentFunc == nil {
return
}
app := new(wasmApp)
app.createContentFunc = createContentFunc
app.close = make(chan DataObject)
app.bridge = createWasmBridge(app.close)
app.init(params)
<-app.close
}
func FinishApp() {
}
func OpenBrowser(url string) bool {
return false
}

25
app_post.js Normal file
View File

@ -0,0 +1,25 @@
async function sendMessage(message) {
const response = await fetch('/', {
method : 'POST',
body : message,
"Content-Type" : "text/plain",
});
const text = await response.text();
if (text != "") {
window.eval(text)
}
}
window.onload = function() {
sendMessage( sessionInfo() );
}
window.onfocus = function() {
windowFocus = true
sendMessage( "session-resume{}" );
}
function closeSocket() {
}

File diff suppressed because it is too large Load Diff

78
app_socket.js Normal file
View File

@ -0,0 +1,78 @@
let socket
function sendMessage(message) {
if (!socket) {
createSocket(function() {
sendMessage( "reconnect{session=" + sessionID + "}" );
if (!windowFocus) {
windowFocus = true;
sendMessage( "session-resume{session=" + sessionID +"}" );
}
socket.send(message);
});
} else {
socket.send(message);
}
}
function createSocket(onopen) {
let socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://"
socketUrl += document.location.hostname
const port = document.location.port
if (port) {
socketUrl += ":" + port
}
socketUrl += window.location.pathname + "ws"
socket = new WebSocket(socketUrl);
socket.onopen = onopen;
socket.onclose = onSocketClose;
socket.onerror = onSocketError;
socket.onmessage = function(event) {
window.execScript ? window.execScript(event.data) : window.eval(event.data);
};
}
function closeSocket() {
if (socket) {
socket.close()
}
}
window.onload = createSocket(function() {
sendMessage( sessionInfo() );
});
window.onfocus = function() {
windowFocus = true
if (!socket) {
createSocket(function() {
sendMessage( "reconnect{session=" + sessionID + "}" );
sendMessage( "session-resume{session=" + sessionID +"}" );
});
} else {
sendMessage( "session-resume{session=" + sessionID +"}" );
}
}
function onSocketReopen() {
sendMessage( "reconnect{session=" + sessionID + "}" );
}
function socketReconnect() {
if (!socket) {
createSocket(onSocketReopen);
}
}
function onSocketClose(event) {
console.log("socket closed")
socket = null;
if (!event.wasClean && windowFocus) {
window.setTimeout(socketReconnect, 10000);
}
}
function onSocketError(error) {
console.log(error);
}

View File

@ -8,6 +8,13 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
:root {
--tooltip-arrow-size: 6px;
--tooltip-background: white;
--tooltip-text-color: black;
--tooltip-shadow-color: gray;
}
body { body {
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;
@ -15,6 +22,7 @@ body {
margin: 0 auto; margin: 0 auto;
width: 100%; width: 100%;
height: 100vh; height: 100vh;
font-family: system-ui;
} }
div { div {
@ -34,12 +42,14 @@ div:focus {
*/ */
input { input {
box-sizing: border-box;
margin: 2px; margin: 2px;
padding: 1px; padding: 1px;
font-size: inherit; font-size: inherit;
} }
select { select {
box-sizing: border-box;
margin: 2px; margin: 2px;
font-size: inherit; font-size: inherit;
} }
@ -49,10 +59,12 @@ button {
} }
textarea { textarea {
box-sizing: border-box;
margin: 2px; margin: 2px;
padding: 1px; padding: 4px;
overflow: auto; overflow: auto;
font-size: inherit; font-size: inherit;
resize: none;
} }
ul:focus { ul:focus {
@ -68,7 +80,7 @@ ul:focus {
} }
.ruiPopupLayer { .ruiPopupLayer {
background-color: rgba(128,128,128,0.1); /*background-color: rgba(128,128,128,0.1);*/
position: absolute; position: absolute;
top: 0px; top: 0px;
bottom: 0px; bottom: 0px;
@ -76,7 +88,54 @@ ul:focus {
left: 0px; left: 0px;
} }
.ruiTooltipLayer {
display: grid;
grid-template-rows: 1fr auto 1fr;
justify-items: center;
align-items: center;
position: absolute;
top: 0px;
bottom: 0px;
right: 0px;
left: 0px;
transition: opacity 0.5s ease-out;
filter: drop-shadow(0px 0px 2px var(--tooltip-shadow-color));
}
.ruiTooltipTopArrow {
grid-row-start: 1;
grid-row-end: 2;
border-width: var(--tooltip-arrow-size);
border-style: solid;
border-color: transparent transparent var(--tooltip-background) transparent;
margin-left: 12px;
margin-right: 12px;
}
.ruiTooltipBottomArrow {
grid-row-start: 3;
grid-row-end: 4;
border-width: var(--tooltip-arrow-size);
border-style: solid;
border-color: var(--tooltip-background) transparent transparent transparent;
margin-left: 12px;
margin-right: 12px;
}
.ruiTooltipText {
grid-row-start: 2;
grid-row-end: 3;
padding: 4px 8px 4px 8px;
margin-left: 8px;
margin-right: 8px;
background-color: var(--tooltip-background);
color: var(--tooltip-text-color);
/*box-shadow: 0px 0px 4px 2px #8888;*/
border-radius: 4px;
}
.ruiView { .ruiView {
box-sizing: border-box;
} }
.ruiAbsoluteLayout { .ruiAbsoluteLayout {
@ -89,6 +148,19 @@ ul:focus {
.ruiListLayout { .ruiListLayout {
display: flex; display: flex;
overflow: auto;
}
.ruiButton {
display: flex;
overflow: auto;
justify-content: center;
align-items: center;
flex-flow: row;
}
.ruiColumnLayout {
overflow: auto;
} }
.ruiStackLayout { .ruiStackLayout {
@ -112,16 +184,25 @@ ul:focus {
} }
.ruiImageView { .ruiImageView {
display: block;
}
.ruiSvgImageView {
display: grid; display: grid;
} }
.ruiListView { .ruiListView {
overflow: auto; overflow: auto;
/*
display: flex;
align-content: stretch;
*/
} }
.hiddenMarker {
list-style: none;
}
.hiddenMarker::-webkit-details-marker {
display: none;
}
/* /*
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
body { body {

8
app_wasm.js Normal file
View File

@ -0,0 +1,8 @@
window.onfocus = function() {
windowFocus = true
sendMessage( "session-resume{session=" + sessionID +"}" );
}
function closeSocket() {
}

View File

@ -1,21 +1,8 @@
package rui package rui
import ( import (
"bytes"
"context"
_ "embed" _ "embed"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings" "strings"
"time"
) )
//go:embed app_scripts.js //go:embed app_scripts.js
@ -27,62 +14,70 @@ var appStyles string
//go:embed defaultTheme.rui //go:embed defaultTheme.rui
var defaultThemeText string var defaultThemeText string
// Application - app interface // Application represent generic application interface, see also [Session]
type Application interface { type Application interface {
// Finish finishes the application
Finish() Finish()
nextSessionID() int
removeSession(id int)
}
type application struct { // Params returns application parameters set by StartApp function
server *http.Server Params() AppParams
params AppParams
createContentFunc func(Session) SessionContent removeSession(id int)
sessions map[int]Session
} }
// AppParams defines parameters of the app // AppParams defines parameters of the app
type AppParams struct { type AppParams struct {
// Title - title of the app window/tab // Title - title of the app window/tab
Title string Title string
// TitleColor - background color of the app window/tab (applied only for Safari and Chrome for Android) // TitleColor - background color of the app window/tab (applied only for Safari and Chrome for Android)
TitleColor Color TitleColor Color
// Icon - the icon file name // Icon - the icon file name
Icon string Icon string
// CertFile - path of a certificate for the server must be provided // CertFile - path of a certificate for the server must be provided
// if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated. // if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated.
// If the certificate is signed by a certificate authority, the certFile should be the concatenation // If the certificate is signed by a certificate authority, the certFile should be the concatenation
// of the server's certificate, any intermediates, and the CA's certificate. // of the server's certificate, any intermediates, and the CA's certificate.
CertFile string CertFile string
AutoCertDomain string
// KeyFile - path of a private key for the server must be provided // KeyFile - path of a private key for the server must be provided
// if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated. // if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated.
KeyFile string KeyFile string
// Redirect80 - if true then the function of redirect from port 80 to 443 is created // Redirect80 - if true then the function of redirect from port 80 to 443 is created
Redirect80 bool Redirect80 bool
// NoSocket - if true then WebSockets will not be used and information exchange
// between the client and the server will be carried out only via http.
NoSocket bool
// SocketAutoClose - time in seconds after which the socket is automatically closed for an inactive session.
// The countdown begins after the OnPause event arrives.
// If the value of this property is less than or equal to 0 then the socket is not closed.
SocketAutoClose int
} }
func (app *application) getStartPage() string { func getStartPage(buffer *strings.Builder, params AppParams) {
buffer := allocStringBuilder() buffer.WriteString(`<head>
defer freeStringBuilder(buffer)
buffer.WriteString(`<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>`) <title>`)
buffer.WriteString(app.params.Title) buffer.WriteString(params.Title)
buffer.WriteString("</title>") buffer.WriteString("</title>")
if app.params.Icon != "" { if params.Icon != "" {
buffer.WriteString(` buffer.WriteString(`
<link rel="icon" href="`) <link rel="icon" href="`)
buffer.WriteString(app.params.Icon) buffer.WriteString(params.Icon)
buffer.WriteString(`">`) buffer.WriteString(`">`)
} }
if app.params.TitleColor != 0 { if params.TitleColor != 0 {
buffer.WriteString(` buffer.WriteString(`
<meta name="theme-color" content="`) <meta name="theme-color" content="`)
buffer.WriteString(app.params.TitleColor.cssString()) buffer.WriteString(params.TitleColor.cssString())
buffer.WriteString(`">`) buffer.WriteString(`">`)
} }
@ -92,356 +87,18 @@ func (app *application) getStartPage() string {
<style>`) <style>`)
buffer.WriteString(appStyles) buffer.WriteString(appStyles)
buffer.WriteString(`</style> buffer.WriteString(`</style>
<script>`) <style id="ruiAnimations"></style>
buffer.WriteString(defaultScripts) <script src="/script.js"></script>
buffer.WriteString(`</script>
</head> </head>
<body> <body id="body" onkeydown="keyDownEvent(this, event)">
<div class="ruiRoot" id="ruiRootView"></div> <div class="ruiRoot" id="ruiRootView"></div>
<div class="ruiPopupLayer" id="ruiPopupLayer" style="visibility: hidden;" onclick="clickOutsidePopup(event)"></div> <div class="ruiPopupLayer" id="ruiPopupLayer" style="visibility: hidden; isolation: isolate;"></div>
<div class="ruiTooltipLayer" id="ruiTooltipLayer" style="visibility: hidden; opacity: 0;">
<div id="ruiTooltipText" class="ruiTooltipText"></div>
<div id="ruiTooltipTopArrow" class="ruiTooltipTopArrow"></div>
<div id="ruiTooltipBottomArrow" class="ruiTooltipBottomArrow"></div>
</div>
<a id="ruiDownloader" download style="display: none;"></a> <a id="ruiDownloader" download style="display: none;"></a>
</body> </body>`)
</html>`)
return buffer.String()
}
func (app *application) Finish() {
for _, session := range app.sessions {
session.close()
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := app.server.Shutdown(ctx); err != nil {
log.Println(err.Error())
}
}
func (app *application) nextSessionID() int {
n := rand.Intn(0x7FFFFFFE) + 1
_, ok := app.sessions[n]
for ok {
n = rand.Intn(0x7FFFFFFE) + 1
_, ok = app.sessions[n]
}
return n
}
func (app *application) removeSession(id int) {
delete(app.sessions, id)
}
func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if ProtocolInDebugLog {
DebugLogF("%s %s", req.Method, req.URL.Path)
}
switch req.Method {
case "GET":
switch req.URL.Path {
case "/":
w.WriteHeader(http.StatusOK)
io.WriteString(w, app.getStartPage())
case "/ws":
if brige := CreateSocketBrige(w, req); brige != nil {
go app.socketReader(brige)
}
default:
filename := req.URL.Path[1:]
if size := len(filename); size > 0 && filename[size-1] == '/' {
filename = filename[:size-1]
}
if !serveResourceFile(filename, w, req) &&
!serveDownloadFile(filename, w, req) {
w.WriteHeader(http.StatusNotFound)
}
}
}
}
func (app *application) socketReader(brige WebBrige) {
var session Session
events := make(chan DataObject, 1024)
for {
message, ok := brige.ReadMessage()
if !ok {
events <- NewDataObject("disconnect")
return
}
if ProtocolInDebugLog {
DebugLog(message)
}
if obj := ParseDataText(message); obj != nil {
command := obj.Tag()
switch command {
case "startSession":
answer := ""
if session, answer = app.startSession(obj, events, brige); session != nil {
if !brige.WriteMessage(answer) {
return
}
session.onStart()
go sessionEventHandler(session, events, brige)
}
case "reconnect":
if sessionText, ok := obj.PropertyValue("session"); ok {
if sessionID, err := strconv.Atoi(sessionText); err == nil {
if session = app.sessions[sessionID]; session != nil {
session.setBrige(events, brige)
answer := allocStringBuilder()
defer freeStringBuilder(answer)
session.writeInitScript(answer)
if !brige.WriteMessage(answer.String()) {
return
}
session.onReconnect()
go sessionEventHandler(session, events, brige)
return
}
DebugLogF("Session #%d not exists", sessionID)
} else {
ErrorLog(`strconv.Atoi(sessionText) error: ` + err.Error())
}
} else {
ErrorLog(`"session" key not found`)
}
answer := ""
if session, answer = app.startSession(obj, events, brige); session != nil {
if !brige.WriteMessage(answer) {
return
}
session.onStart()
go sessionEventHandler(session, events, brige)
}
case "answer":
session.handleAnswer(obj)
case "imageLoaded":
session.imageManager().imageLoaded(obj, session)
case "imageError":
session.imageManager().imageLoadError(obj, session)
default:
events <- obj
}
}
}
}
func sessionEventHandler(session Session, events chan DataObject, brige WebBrige) {
for {
data := <-events
switch command := data.Tag(); command {
case "disconnect":
session.onDisconnect()
return
case "session-close":
session.onFinish()
session.App().removeSession(session.ID())
brige.Close()
case "session-pause":
session.onPause()
case "session-resume":
session.onResume()
case "root-size":
session.handleRootSize(data)
case "resize":
session.handleResize(data)
default:
session.handleViewEvent(command, data)
}
}
}
func (app *application) startSession(params DataObject, events chan DataObject, brige WebBrige) (Session, string) {
if app.createContentFunc == nil {
return nil, ""
}
session := newSession(app, app.nextSessionID(), "", params)
session.setBrige(events, brige)
if !session.setContent(app.createContentFunc(session), session) {
return nil, ""
}
app.sessions[session.ID()] = session
answer := allocStringBuilder()
defer freeStringBuilder(answer)
answer.WriteString("sessionID = '")
answer.WriteString(strconv.Itoa(session.ID()))
answer.WriteString("';\n")
session.writeInitScript(answer)
answerText := answer.String()
if ProtocolInDebugLog {
DebugLog("Start session:")
DebugLog(answerText)
}
return session, answerText
}
var apps = []*application{}
// StartApp - create the new application and start it
func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) {
app := new(application)
app.params = params
app.sessions = map[int]Session{}
app.createContentFunc = createContentFunc
apps = append(apps, app)
redirectAddr := ""
if index := strings.IndexRune(addr, ':'); index >= 0 {
redirectAddr = addr[:index] + ":80"
} else {
redirectAddr = addr + ":80"
if params.CertFile != "" && params.KeyFile != "" {
addr += ":443"
} else {
addr += ":80"
}
}
app.server = &http.Server{Addr: addr}
http.Handle("/", app)
serverRun := func(err error) {
if err != nil {
if err == http.ErrServerClosed {
log.Println(err)
} else {
log.Fatal(err)
}
}
}
if params.CertFile != "" && params.KeyFile != "" {
if params.Redirect80 {
redirectTLS := func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://"+addr+r.RequestURI, http.StatusMovedPermanently)
}
go func() {
serverRun(http.ListenAndServe(redirectAddr, http.HandlerFunc(redirectTLS)))
}()
}
serverRun(app.server.ListenAndServeTLS(params.CertFile, params.KeyFile))
} else {
serverRun(app.server.ListenAndServe())
}
}
func FinishApp() {
for _, app := range apps {
app.Finish()
}
apps = []*application{}
}
func OpenBrowser(url string) bool {
var err error
switch runtime.GOOS {
case "linux":
for _, provider := range []string{"xdg-open", "x-www-browser", "www-browser"} {
if _, err = exec.LookPath(provider); err == nil {
if exec.Command(provider, url).Start(); err == nil {
return true
}
}
}
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
return err != nil
}
type downloadFile struct {
filename string
path string
data []byte
}
var currentDownloadId = int(rand.Int31())
var downloadFiles = map[string]downloadFile{}
func (session *sessionData) startDownload(file downloadFile) {
currentDownloadId++
id := strconv.Itoa(currentDownloadId)
downloadFiles[id] = file
session.runScript(fmt.Sprintf(`startDowndload("%s", "%s")`, id, file.filename))
}
func serveDownloadFile(id string, w http.ResponseWriter, r *http.Request) bool {
if file, ok := downloadFiles[id]; ok {
delete(downloadFiles, id)
if file.data != nil {
http.ServeContent(w, r, file.filename, time.Now(), bytes.NewReader(file.data))
return true
} else if _, err := os.Stat(file.path); err == nil {
http.ServeFile(w, r, file.path)
return true
}
}
return false
}
// DownloadFile starts downloading the file on the client side.
func (session *sessionData) DownloadFile(path string) {
if _, err := os.Stat(path); err != nil {
ErrorLog(err.Error())
return
}
_, filename := filepath.Split(path)
session.startDownload(downloadFile{
filename: filename,
path: path,
data: nil,
})
}
// DownloadFileData starts downloading the file on the client side. Arguments specify the name of the downloaded file and its contents
func (session *sessionData) DownloadFileData(filename string, data []byte) {
if data == nil {
ErrorLog("Invalid download data. Must be not nil.")
return
}
session.startDownload(downloadFile{
filename: filename,
path: "",
data: data,
})
} }

View File

@ -1,5 +1,6 @@
package rui package rui
// AudioPlayer is a type of a [View] which can play audio files
type AudioPlayer interface { type AudioPlayer interface {
MediaPlayer MediaPlayer
} }
@ -12,13 +13,12 @@ type audioPlayerData struct {
func NewAudioPlayer(session Session, params Params) AudioPlayer { func NewAudioPlayer(session Session, params Params) AudioPlayer {
view := new(audioPlayerData) view := new(audioPlayerData)
view.init(session) view.init(session)
view.tag = "AudioPlayer"
setInitParams(view, params) setInitParams(view, params)
return view return view
} }
func newAudioPlayer(session Session) View { func newAudioPlayer(session Session) View {
return NewAudioPlayer(session, nil) return new(audioPlayerData) // NewAudioPlayer(session, nil)
} }
func (player *audioPlayerData) init(session Session) { func (player *audioPlayerData) init(session Session) {
@ -26,10 +26,6 @@ func (player *audioPlayerData) init(session Session) {
player.tag = "AudioPlayer" player.tag = "AudioPlayer"
} }
func (player *audioPlayerData) String() string {
return getViewString(player)
}
func (player *audioPlayerData) htmlTag() string { func (player *audioPlayerData) htmlTag() string {
return "audio" return "audio"
} }

View File

@ -1,113 +1,75 @@
package rui package rui
import "strings" import (
"fmt"
const (
// NoRepeat is value of the Repeat property of an background image:
// The image is not repeated (and hence the background image painting area
// will not necessarily be entirely covered). The position of the non-repeated
// background image is defined by the background-position CSS property.
NoRepeat = 0
// RepeatXY is value of the Repeat property of an background image:
// The image is repeated as much as needed to cover the whole background
// image painting area. The last image will be clipped if it doesn't fit.
RepeatXY = 1
// RepeatX is value of the Repeat property of an background image:
// The image is repeated horizontally as much as needed to cover
// the whole width background image painting area. The image is not repeated vertically.
// The last image will be clipped if it doesn't fit.
RepeatX = 2
// RepeatY is value of the Repeat property of an background image:
// The image is repeated vertically as much as needed to cover
// the whole height background image painting area. The image is not repeated horizontally.
// The last image will be clipped if it doesn't fit.
RepeatY = 3
// RepeatRound is value of the Repeat property of an background image:
// As the allowed space increases in size, the repeated images will stretch (leaving no gaps)
// until there is room (space left >= half of the image width) for another one to be added.
// When the next image is added, all of the current ones compress to allow room.
RepeatRound = 4
// RepeatSpace is value of the Repeat property of an background image:
// The image is repeated as much as possible without clipping. The first and last images
// are pinned to either side of the element, and whitespace is distributed evenly between the images.
RepeatSpace = 5
// ScrollAttachment is value of the Attachment property of an background image:
// The background is fixed relative to the element itself and does not scroll with its contents.
// (It is effectively attached to the element's border.)
ScrollAttachment = 0
// FixedAttachment is value of the Attachment property of an background image:
// The background is fixed relative to the viewport. Even if an element has
// a scrolling mechanism, the background doesn't move with the element.
FixedAttachment = 1
// LocalAttachment is value of the Attachment property of an background image:
// The background is fixed relative to the element's contents. If the element has a scrolling mechanism,
// the background scrolls with the element's contents, and the background painting area
// and background positioning area are relative to the scrollable area of the element
// rather than to the border framing them.
LocalAttachment = 2
// BorderBoxClip is value of the BackgroundClip property:
// The background extends to the outside edge of the border (but underneath the border in z-ordering).
BorderBoxClip = 0
// PaddingBoxClip is value of the BackgroundClip property:
// The background extends to the outside edge of the padding. No background is drawn beneath the border.
PaddingBoxClip = 1
// ContentBoxClip is value of the BackgroundClip property:
// The background is painted within (clipped to) the content box.
ContentBoxClip = 2
) )
// BackgroundElement describes the background element. const (
// BorderBox is the value of the following properties:
// - BackgroundClip - The background extends to the outside edge of the border (but underneath the border in z-ordering).
// - BackgroundOrigin - The background is positioned relative to the border box.
// - MaskClip - The painted content is clipped to the border box.
// - MaskOrigin - The mask is positioned relative to the border box.
BorderBox = 0
// PaddingBox is value of the BackgroundClip and MaskClip property:
// - BackgroundClip - The background extends to the outside edge of the padding. No background is drawn beneath the border.
// - BackgroundOrigin - The background is positioned relative to the padding box.
// - MaskClip - The painted content is clipped to the padding box.
// - MaskOrigin - The mask is positioned relative to the padding box.
PaddingBox = 1
// ContentBox is value of the BackgroundClip and MaskClip property:
// - BackgroundClip - The background is painted within (clipped to) the content box.
// - BackgroundOrigin - The background is positioned relative to the content box.
// - MaskClip - The painted content is clipped to the content box.
// - MaskOrigin - The mask is positioned relative to the content box.
ContentBox = 2
)
// BackgroundElement describes the background element
type BackgroundElement interface { type BackgroundElement interface {
Properties Properties
fmt.Stringer
stringWriter
cssStyle(session Session) string cssStyle(session Session) string
// Tag returns type of the background element.
// Possible values are: "image", "conic-gradient", "linear-gradient" and "radial-gradient"
Tag() string Tag() string
// Clone creates a new copy of BackgroundElement
Clone() BackgroundElement Clone() BackgroundElement
} }
type backgroundElement struct { type backgroundElement struct {
propertyList dataProperty
} }
type backgroundImage struct {
backgroundElement
}
// NewBackgroundImage creates the new background image
func createBackground(obj DataObject) BackgroundElement { func createBackground(obj DataObject) BackgroundElement {
var result BackgroundElement = nil var result BackgroundElement = nil
switch obj.Tag() { switch obj.Tag() {
case "image": case "image":
image := new(backgroundImage) result = NewBackgroundImage(nil)
image.properties = map[string]any{}
result = image
case "linear-gradient": case "linear-gradient":
gradient := new(backgroundLinearGradient) result = NewBackgroundLinearGradient(nil)
gradient.properties = map[string]any{}
result = gradient
case "radial-gradient": case "radial-gradient":
gradient := new(backgroundRadialGradient) result = NewBackgroundRadialGradient(nil)
gradient.properties = map[string]any{}
result = gradient
case "conic-gradient": case "conic-gradient":
gradient := new(backgroundConicGradient) result = NewBackgroundConicGradient(nil)
gradient.properties = map[string]any{}
result = gradient
default: default:
return nil return nil
} }
count := obj.PropertyCount() for node := range obj.Properties() {
for i := 0; i < count; i++ { if node.Type() == TextNode {
if node := obj.Property(i); node.Type() == TextNode {
if value := node.Text(); value != "" { if value := node.Text(); value != "" {
result.Set(node.Tag(), value) result.Set(PropertyName(node.Tag()), value)
} }
} }
} }
@ -115,127 +77,242 @@ func createBackground(obj DataObject) BackgroundElement {
return result return result
} }
// NewBackgroundImage creates the new background image func parseBackgroundText(text string) BackgroundElement {
func NewBackgroundImage(params Params) BackgroundElement { obj, err := ParseDataText(text)
result := new(backgroundImage) if err != nil {
result.properties = map[string]any{} ErrorLog(err.Error())
for tag, value := range params { return nil
result.Set(tag, value)
}
return result
}
func (image *backgroundImage) Tag() string {
return "image"
}
func (image *backgroundImage) Clone() BackgroundElement {
result := NewBackgroundImage(nil)
for tag, value := range image.properties {
result.setRaw(tag, value)
}
return result
}
func (image *backgroundImage) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag {
case "source":
tag = Source
case Fit:
tag = backgroundFit
case HorizontalAlign:
tag = ImageHorizontalAlign
case VerticalAlign:
tag = ImageVerticalAlign
} }
return tag return createBackground(obj)
} }
func (image *backgroundImage) Set(tag string, value any) bool { func parseBackgroundValue(value any) []BackgroundElement {
tag = image.normalizeTag(tag)
switch tag {
case Attachment, Width, Height, Repeat, ImageHorizontalAlign, ImageVerticalAlign,
backgroundFit, Source:
return image.backgroundElement.Set(tag, value)
}
return false switch value := value.(type) {
} case BackgroundElement:
return []BackgroundElement{value}
func (image *backgroundImage) Get(tag string) any { case []BackgroundElement:
return image.backgroundElement.Get(image.normalizeTag(tag)) return value
}
func (image *backgroundImage) cssStyle(session Session) string { case []DataValue:
if src, ok := imageProperty(image, Source, session); ok && src != "" { background := []BackgroundElement{}
buffer := allocStringBuilder() for _, el := range value {
defer freeStringBuilder(buffer) if el.IsObject() {
if element := createBackground(el.Object()); element != nil {
buffer.WriteString(`url(`) background = append(background, element)
buffer.WriteString(src) } else {
buffer.WriteRune(')') return nil
}
attachment, _ := enumProperty(image, Attachment, session, NoRepeat) } else if element := parseBackgroundText(el.Value()); element != nil {
values := enumProperties[Attachment].values background = append(background, element)
if attachment > 0 && attachment < len(values) { } else {
buffer.WriteRune(' ') return nil
buffer.WriteString(values[attachment])
}
align, _ := enumProperty(image, ImageHorizontalAlign, session, LeftAlign)
values = enumProperties[ImageHorizontalAlign].values
if align >= 0 && align < len(values) {
buffer.WriteRune(' ')
buffer.WriteString(values[align])
} else {
buffer.WriteString(` left`)
}
align, _ = enumProperty(image, ImageVerticalAlign, session, TopAlign)
values = enumProperties[ImageVerticalAlign].values
if align >= 0 && align < len(values) {
buffer.WriteRune(' ')
buffer.WriteString(values[align])
} else {
buffer.WriteString(` top`)
}
fit, _ := enumProperty(image, backgroundFit, session, NoneFit)
values = enumProperties[backgroundFit].values
if fit > 0 && fit < len(values) {
buffer.WriteString(` / `)
buffer.WriteString(values[fit])
} else {
width, _ := sizeProperty(image, Width, session)
height, _ := sizeProperty(image, Height, session)
if width.Type != Auto || height.Type != Auto {
buffer.WriteString(` / `)
buffer.WriteString(width.cssString("auto", session))
buffer.WriteRune(' ')
buffer.WriteString(height.cssString("auto", session))
} }
} }
return background
repeat, _ := enumProperty(image, Repeat, session, NoRepeat) case DataObject:
values = enumProperties[Repeat].values if element := createBackground(value); element != nil {
if repeat >= 0 && repeat < len(values) { return []BackgroundElement{element}
buffer.WriteRune(' ')
buffer.WriteString(values[repeat])
} else {
buffer.WriteString(` no-repeat`)
} }
return buffer.String() case []DataObject:
background := []BackgroundElement{}
for _, obj := range value {
if element := createBackground(obj); element != nil {
background = append(background, element)
} else {
return nil
}
}
return background
case string:
if element := parseBackgroundText(value); element != nil {
return []BackgroundElement{element}
}
case []string:
elements := make([]BackgroundElement, 0, len(value))
for _, text := range value {
if element := parseBackgroundText(text); element != nil {
elements = append(elements, element)
} else {
return nil
}
}
return elements
case []any:
elements := make([]BackgroundElement, 0, len(value))
for _, val := range value {
switch val := val.(type) {
case BackgroundElement:
elements = append(elements, val)
case string:
if element := parseBackgroundText(val); element != nil {
elements = append(elements, element)
} else {
return nil
}
default:
return nil
}
}
return elements
} }
return nil
}
func setBackgroundProperty(properties Properties, tag PropertyName, value any) []PropertyName {
background := parseBackgroundValue(value)
if background == nil {
notCompatibleType(tag, value)
return nil
}
if len(background) > 0 {
properties.setRaw(tag, background)
} else if properties.getRaw(tag) != nil {
properties.setRaw(tag, nil)
} else {
return []PropertyName{}
}
return []PropertyName{tag}
}
func backgroundCSS(properties Properties, session Session) string {
if value := properties.getRaw(Background); value != nil {
if backgrounds, ok := value.([]BackgroundElement); ok && len(backgrounds) > 0 {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for _, background := range backgrounds {
if value := background.cssStyle(session); value != "" {
if buffer.Len() > 0 {
buffer.WriteString(", ")
}
buffer.WriteString(value)
}
}
if buffer.Len() > 0 {
backgroundColor, _ := colorProperty(properties, BackgroundColor, session)
if backgroundColor != 0 {
buffer.WriteRune(' ')
buffer.WriteString(backgroundColor.cssString())
}
return buffer.String()
}
}
}
return "" return ""
} }
func maskCSS(properties Properties, session Session) string {
if value := properties.getRaw(Mask); value != nil {
if backgrounds, ok := value.([]BackgroundElement); ok && len(backgrounds) > 0 {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for _, background := range backgrounds {
if value := background.cssStyle(session); value != "" {
if buffer.Len() > 0 {
buffer.WriteString(", ")
}
buffer.WriteString(value)
}
}
return buffer.String()
}
}
return ""
}
func backgroundStyledPropery(view View, subviewID []string, tag PropertyName) []BackgroundElement {
var background []BackgroundElement = nil
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(tag); value != nil {
if backgrounds, ok := value.([]BackgroundElement); ok {
background = backgrounds
}
} else if value := valueFromStyle(view, tag); value != nil {
background = parseBackgroundValue(value)
}
}
if count := len(background); count > 0 {
result := make([]BackgroundElement, count)
copy(result, background)
return result
}
return []BackgroundElement{}
}
// GetBackground returns the view background.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetBackground(view View, subviewID ...string) []BackgroundElement {
return backgroundStyledPropery(view, subviewID, Background)
}
// GetMask returns the view mask.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetMask(view View, subviewID ...string) []BackgroundElement {
return backgroundStyledPropery(view, subviewID, Mask)
}
// GetBackgroundClip returns a "background-clip" of the subview. Returns one of next values:
//
// BorderBox (0), PaddingBox (1), ContentBox (2)
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetBackgroundClip(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, BackgroundClip, 0, false)
}
// GetBackgroundOrigin returns a "background-origin" of the subview. Returns one of next values:
//
// BorderBox (0), PaddingBox (1), ContentBox (2)
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetBackgroundOrigin(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, BackgroundOrigin, 0, false)
}
// GetMaskClip returns a "mask-clip" of the subview. Returns one of next values:
//
// BorderBox (0), PaddingBox (1), ContentBox (2)
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetMaskClip(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, MaskClip, 0, false)
}
// GetMaskOrigin returns a "mask-origin" of the subview. Returns one of next values:
//
// BorderBox (0), PaddingBox (1), ContentBox (2)
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetMaskOrigin(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, MaskOrigin, 0, false)
}

View File

@ -19,15 +19,23 @@ type BackgroundGradientAngle struct {
} }
// NewBackgroundConicGradient creates the new background conic gradient // NewBackgroundConicGradient creates the new background conic gradient
//
// The following properties can be used:
// - "gradient" [Gradient] - Describes gradient stop points. This is a mandatory property while describing background gradients.
// - "center-x" [CenterX] - center X point of the gradient.
// - "center-y" [CenterY] - center Y point of the gradient.
// - "from" [From] - start angle position of the gradient.
// - "repeating" [Repeating] - Defines whether stop points needs to be repeated after the last one.
func NewBackgroundConicGradient(params Params) BackgroundElement { func NewBackgroundConicGradient(params Params) BackgroundElement {
result := new(backgroundConicGradient) result := new(backgroundConicGradient)
result.properties = map[string]any{} result.init()
for tag, value := range params { for tag, value := range params {
result.Set(tag, value) result.Set(tag, value)
} }
return result return result
} }
// String convert internal representation of [BackgroundGradientAngle] into a string.
func (point *BackgroundGradientAngle) String() string { func (point *BackgroundGradientAngle) String() string {
result := "black" result := "black"
if point.Color != nil { if point.Color != nil {
@ -47,7 +55,6 @@ func (point *BackgroundGradientAngle) String() string {
case AngleUnit: case AngleUnit:
result += " " + value.String() result += " " + value.String()
} }
} }
@ -58,20 +65,21 @@ func (point *BackgroundGradientAngle) color(session Session) (Color, bool) {
if point.Color != nil { if point.Color != nil {
switch color := point.Color.(type) { switch color := point.Color.(type) {
case string: case string:
if color != "" { if ok, constName := isConstantName(color); ok {
if color[0] == '@' { if clr, ok := session.Color(constName); ok {
if clr, ok := session.Color(color[1:]); ok { return clr, true
return clr, true
}
} else {
if clr, ok := StringToColor(color); ok {
return clr, true
}
} }
} else if clr, ok := StringToColor(color); ok {
return clr, true
} }
case Color: case Color:
return color, true return color, true
default:
if n, ok := isInt(color); ok {
return Color(n), true
}
} }
} }
return 0, false return 0, false
@ -92,19 +100,15 @@ func (point *BackgroundGradientAngle) cssString(session Session, buffer *strings
if point.Angle != nil { if point.Angle != nil {
switch value := point.Angle.(type) { switch value := point.Angle.(type) {
case string: case string:
if value != "" { if ok, constName := isConstantName(value); ok {
if value[0] == '@' { if value, ok = session.Constant(constName); !ok {
if val, ok := session.Constant(value[1:]); ok { return
value = val
} else {
return
}
} }
}
if angle, ok := StringToAngleUnit(value); ok { if angle, ok := StringToAngleUnit(value); ok {
buffer.WriteRune(' ') buffer.WriteRune(' ')
buffer.WriteString(angle.cssString()) buffer.WriteString(angle.cssString())
}
} }
case AngleUnit: case AngleUnit:
@ -114,6 +118,15 @@ func (point *BackgroundGradientAngle) cssString(session Session, buffer *strings
} }
} }
func (gradient *backgroundConicGradient) init() {
gradient.backgroundElement.init()
gradient.normalize = normalizeConicGradientTag
gradient.set = backgroundConicGradientSet
gradient.supportedProperties = []PropertyName{
CenterX, CenterY, Repeating, From, Gradient,
}
}
func (gradient *backgroundConicGradient) Tag() string { func (gradient *backgroundConicGradient) Tag() string {
return "conic-gradient" return "conic-gradient"
} }
@ -126,8 +139,8 @@ func (image *backgroundConicGradient) Clone() BackgroundElement {
return result return result
} }
func (gradient *backgroundConicGradient) normalizeTag(tag string) string { func normalizeConicGradientTag(tag PropertyName) PropertyName {
tag = strings.ToLower(tag) tag = defaultNormalize(tag)
switch tag { switch tag {
case "x-center": case "x-center":
tag = CenterX tag = CenterX
@ -139,27 +152,50 @@ func (gradient *backgroundConicGradient) normalizeTag(tag string) string {
return tag return tag
} }
func (gradient *backgroundConicGradient) Set(tag string, value any) bool { func backgroundConicGradientSet(properties Properties, tag PropertyName, value any) []PropertyName {
tag = gradient.normalizeTag(tag)
switch tag { switch tag {
case CenterX, CenterY, Repeating, From:
return gradient.propertyList.Set(tag, value)
case Gradient: case Gradient:
return gradient.setGradient(value) switch value := value.(type) {
case string:
if value == "" {
return propertiesRemove(properties, tag)
}
if strings.ContainsAny(value, ", ") {
if vector := parseGradientText(value); vector != nil {
properties.setRaw(Gradient, vector)
return []PropertyName{tag}
}
} else if ok, _ := isConstantName(value); ok {
properties.setRaw(Gradient, value)
return []PropertyName{tag}
}
ErrorLogF(`Invalid conic gradient: "%s"`, value)
case []BackgroundGradientAngle:
count := len(value)
if count < 2 {
ErrorLog("The gradient must contain at least 2 points")
return nil
}
for i, point := range value {
if point.Color == nil {
ErrorLogF("Invalid %d element of the conic gradient: Color is nil", i)
return nil
}
}
properties.setRaw(Gradient, value)
return []PropertyName{tag}
default:
notCompatibleType(tag, value)
}
return nil
} }
ErrorLogF(`"%s" property is not supported by BackgroundConicGradient`, tag) return propertiesSet(properties, tag, value)
return false
}
func (gradient *backgroundConicGradient) stringToAngle(text string) (any, bool) {
if text == "" {
return nil, false
} else if text[0] == '@' {
return text, true
}
return StringToAngleUnit(text)
} }
func (gradient *backgroundConicGradient) stringToGradientPoint(text string) (BackgroundGradientAngle, bool) { func (gradient *backgroundConicGradient) stringToGradientPoint(text string) (BackgroundGradientAngle, bool) {
@ -178,7 +214,7 @@ func (gradient *backgroundConicGradient) stringToGradientPoint(text string) (Bac
return result, false return result, false
} }
if colorText[0] == '@' { if ok, _ := isConstantName(colorText); ok {
result.Color = colorText result.Color = colorText
} else if color, ok := StringToColor(colorText); ok { } else if color, ok := StringToColor(colorText); ok {
result.Color = color result.Color = color
@ -187,7 +223,9 @@ func (gradient *backgroundConicGradient) stringToGradientPoint(text string) (Bac
} }
if pointText != "" { if pointText != "" {
if angle, ok := gradient.stringToAngle(pointText); ok { if ok, _ := isConstantName(pointText); ok {
result.Angle = pointText
} else if angle, ok := StringToAngleUnit(text); ok {
result.Angle = angle result.Angle = angle
} else { } else {
return result, false return result, false
@ -209,63 +247,12 @@ func (gradient *backgroundConicGradient) parseGradientText(value string) []Backg
for i, element := range elements { for i, element := range elements {
var ok bool var ok bool
if vector[i], ok = gradient.stringToGradientPoint(strings.Trim(element, " ")); !ok { if vector[i], ok = gradient.stringToGradientPoint(strings.Trim(element, " ")); !ok {
ErrorLogF(`Ivalid %d element of the conic gradient: "%s"`, i, element) ErrorLogF(`Invalid %d element of the conic gradient: "%s"`, i, element)
return nil return nil
} }
} }
return vector return vector
} }
func (gradient *backgroundConicGradient) setGradient(value any) bool {
if value == nil {
delete(gradient.properties, Gradient)
return true
}
switch value := value.(type) {
case string:
if value == "" {
delete(gradient.properties, Gradient)
return true
}
if strings.Contains(value, ",") || strings.Contains(value, " ") {
if vector := gradient.parseGradientText(value); vector != nil {
gradient.properties[Gradient] = vector
return true
}
return false
} else if value[0] == '@' {
gradient.properties[Gradient] = value
return true
}
ErrorLogF(`Ivalid conic gradient: "%s"`, value)
return false
case []BackgroundGradientAngle:
count := len(value)
if count < 2 {
ErrorLog("The gradient must contain at least 2 points")
return false
}
for i, point := range value {
if point.Color == nil {
ErrorLogF("Ivalid %d element of the conic gradient: Color is nil", i)
return false
}
}
gradient.properties[Gradient] = value
return true
}
return false
}
func (gradient *backgroundConicGradient) Get(tag string) any {
return gradient.backgroundElement.Get(gradient.normalizeTag(tag))
}
func (gradient *backgroundConicGradient) cssStyle(session Session) string { func (gradient *backgroundConicGradient) cssStyle(session Session) string {
points := []BackgroundGradientAngle{} points := []BackgroundGradientAngle{}
@ -336,3 +323,16 @@ func (gradient *backgroundConicGradient) cssStyle(session Session) string {
return buffer.String() return buffer.String()
} }
func (gradient *backgroundConicGradient) writeString(buffer *strings.Builder, indent string) {
gradient.writeToBuffer(buffer, indent, gradient.Tag(), []PropertyName{
Gradient,
CenterX,
CenterY,
Repeating,
})
}
func (gradient *backgroundConicGradient) String() string {
return runStringWriter(gradient)
}

View File

@ -1,612 +0,0 @@
package rui
import "strings"
const (
// ToTopGradient is value of the Direction property of a linear gradient. The value is equivalent to the 0deg angle
ToTopGradient = 0
// ToRightTopGradient is value of the Direction property of a linear gradient.
ToRightTopGradient = 1
// ToRightGradient is value of the Direction property of a linear gradient. The value is equivalent to the 90deg angle
ToRightGradient = 2
// ToRightBottomGradient is value of the Direction property of a linear gradient.
ToRightBottomGradient = 3
// ToBottomGradient is value of the Direction property of a linear gradient. The value is equivalent to the 180deg angle
ToBottomGradient = 4
// ToLeftBottomGradient is value of the Direction property of a linear gradient.
ToLeftBottomGradient = 5
// ToLeftGradient is value of the Direction property of a linear gradient. The value is equivalent to the 270deg angle
ToLeftGradient = 6
// ToLeftTopGradient is value of the Direction property of a linear gradient.
ToLeftTopGradient = 7
// EllipseGradient is value of the Shape property of a radial gradient background:
// the shape is an axis-aligned ellipse
EllipseGradient = 0
// CircleGradient is value of the Shape property of a radial gradient background:
// the gradient's shape is a circle with constant radius
CircleGradient = 1
// ClosestSideGradient is value of the Radius property of a radial gradient background:
// The gradient's ending shape meets the side of the box closest to its center (for circles)
// or meets both the vertical and horizontal sides closest to the center (for ellipses).
ClosestSideGradient = 0
// ClosestCornerGradient is value of the Radius property of a radial gradient background:
// The gradient's ending shape is sized so that it exactly meets the closest corner
// of the box from its center.
ClosestCornerGradient = 1
// FarthestSideGradient is value of the Radius property of a radial gradient background:
// Similar to closest-side, except the ending shape is sized to meet the side of the box
// farthest from its center (or vertical and horizontal sides).
FarthestSideGradient = 2
// FarthestCornerGradient is value of the Radius property of a radial gradient background:
// The default value, the gradient's ending shape is sized so that it exactly meets
// the farthest corner of the box from its center.
FarthestCornerGradient = 3
)
// BackgroundGradientPoint define point on gradient straight line
type BackgroundGradientPoint struct {
// Color - the color of the point. Must not be nil.
// Can take a value of Color type or string (color constant or textual description of the color)
Color any
// Pos - the distance from the start of the gradient straight line. Optional (may be nil).
// Can take a value of SizeUnit type or string (angle constant or textual description of the SizeUnit)
Pos any
}
type backgroundGradient struct {
backgroundElement
}
type backgroundLinearGradient struct {
backgroundGradient
}
type backgroundRadialGradient struct {
backgroundGradient
}
// NewBackgroundLinearGradient creates the new background linear gradient
func NewBackgroundLinearGradient(params Params) BackgroundElement {
result := new(backgroundLinearGradient)
result.properties = map[string]any{}
for tag, value := range params {
result.Set(tag, value)
}
return result
}
// NewBackgroundRadialGradient creates the new background radial gradient
func NewBackgroundRadialGradient(params Params) BackgroundElement {
result := new(backgroundRadialGradient)
result.properties = map[string]any{}
for tag, value := range params {
result.Set(tag, value)
}
return result
}
func (gradient *backgroundGradient) parseGradientText(value string) []BackgroundGradientPoint {
elements := strings.Split(value, ",")
count := len(elements)
if count < 2 {
ErrorLog("The gradient must contain at least 2 points")
return nil
}
points := make([]BackgroundGradientPoint, count)
for i, element := range elements {
if !points[i].setValue(element) {
ErrorLogF(`Ivalid %d element of the conic gradient: "%s"`, i, element)
return nil
}
}
return points
}
func (gradient *backgroundGradient) Set(tag string, value any) bool {
switch tag = strings.ToLower(tag); tag {
case Repeating:
return gradient.setBoolProperty(tag, value)
case Gradient:
switch value := value.(type) {
case string:
if value != "" {
if strings.Contains(value, " ") || strings.Contains(value, ",") {
if points := gradient.parseGradientText(value); len(points) >= 2 {
gradient.properties[Gradient] = points
return true
}
} else if value[0] == '@' {
gradient.properties[Gradient] = value
return true
}
}
case []BackgroundGradientPoint:
if len(value) >= 2 {
gradient.properties[Gradient] = value
return true
}
case []Color:
count := len(value)
if count >= 2 {
points := make([]BackgroundGradientPoint, count)
for i, color := range value {
points[i].Color = color
}
gradient.properties[Gradient] = points
return true
}
case []GradientPoint:
count := len(value)
if count >= 2 {
points := make([]BackgroundGradientPoint, count)
for i, point := range value {
points[i].Color = point.Color
points[i].Pos = Percent(point.Offset * 100)
}
gradient.properties[Gradient] = points
return true
}
}
ErrorLogF("Invalid gradient %v", value)
return false
}
ErrorLogF("Property %s is not supported by a background gradient", tag)
return false
}
func (point *BackgroundGradientPoint) setValue(text string) bool {
text = strings.Trim(text, " ")
colorText := text
pointText := ""
if index := strings.Index(text, " "); index > 0 {
colorText = text[:index]
pointText = strings.Trim(text[index+1:], " ")
}
if colorText == "" {
return false
}
if colorText[0] == '@' {
point.Color = colorText
} else if color, ok := StringToColor(colorText); ok {
point.Color = color
} else {
return false
}
if pointText == "" {
point.Pos = nil
} else if pointText[0] == '@' {
point.Pos = pointText
} else if pos, ok := StringToSizeUnit(pointText); ok {
point.Pos = pos
} else {
return false
}
return true
}
func (point *BackgroundGradientPoint) color(session Session) (Color, bool) {
if point.Color != nil {
switch color := point.Color.(type) {
case string:
if color != "" {
if color[0] == '@' {
if clr, ok := session.Color(color[1:]); ok {
return clr, true
}
} else {
if clr, ok := StringToColor(color); ok {
return clr, true
}
}
}
case Color:
return color, true
}
}
return 0, false
}
func (gradient *backgroundGradient) writeGradient(session Session, buffer *strings.Builder) bool {
value, ok := gradient.properties[Gradient]
if !ok {
return false
}
var points []BackgroundGradientPoint = nil
switch value := value.(type) {
case string:
if value != "" && value[0] == '@' {
if text, ok := session.Constant(value[1:]); ok {
points = gradient.parseGradientText(text)
}
}
case []BackgroundGradientPoint:
points = value
}
if len(points) > 0 {
for i, point := range points {
if i > 0 {
buffer.WriteString(`, `)
}
if color, ok := point.color(session); ok {
buffer.WriteString(color.cssString())
} else {
return false
}
if point.Pos != nil {
switch value := point.Pos.(type) {
case string:
if value != "" {
if value, ok := session.resolveConstants(value); ok {
if pos, ok := StringToSizeUnit(value); ok && pos.Type != Auto {
buffer.WriteRune(' ')
buffer.WriteString(pos.cssString("", session))
}
}
}
case SizeUnit:
if value.Type != Auto {
buffer.WriteRune(' ')
buffer.WriteString(value.cssString("", session))
}
}
}
}
return true
}
return false
}
func (gradient *backgroundLinearGradient) Tag() string {
return "linear-gradient"
}
func (image *backgroundLinearGradient) Clone() BackgroundElement {
result := NewBackgroundLinearGradient(nil)
for tag, value := range image.properties {
result.setRaw(tag, value)
}
return result
}
func (gradient *backgroundLinearGradient) Set(tag string, value any) bool {
if strings.ToLower(tag) == Direction {
switch value := value.(type) {
case AngleUnit:
gradient.properties[Direction] = value
return true
case string:
if gradient.setSimpleProperty(tag, value) {
return true
}
if angle, ok := StringToAngleUnit(value); ok {
gradient.properties[Direction] = angle
return true
}
}
return gradient.setEnumProperty(tag, value, enumProperties[Direction].values)
}
return gradient.backgroundGradient.Set(tag, value)
}
func (gradient *backgroundLinearGradient) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if repeating, _ := boolProperty(gradient, Repeating, session); repeating {
buffer.WriteString(`repeating-linear-gradient(`)
} else {
buffer.WriteString(`linear-gradient(`)
}
if value, ok := gradient.properties[Direction]; ok {
switch value := value.(type) {
case string:
if text, ok := session.resolveConstants(value); ok {
direction := enumProperties[Direction]
if n, ok := enumStringToInt(text, direction.values, false); ok {
buffer.WriteString(direction.cssValues[n])
buffer.WriteString(", ")
} else {
if angle, ok := StringToAngleUnit(text); ok {
buffer.WriteString(angle.cssString())
buffer.WriteString(", ")
} else {
ErrorLog(`Invalid linear gradient direction: ` + text)
}
}
} else {
ErrorLog(`Invalid linear gradient direction: ` + value)
}
case int:
values := enumProperties[Direction].cssValues
if value >= 0 && value < len(values) {
buffer.WriteString(values[value])
buffer.WriteString(", ")
} else {
ErrorLogF(`Invalid linear gradient direction: %d`, value)
}
case AngleUnit:
buffer.WriteString(value.cssString())
buffer.WriteString(", ")
}
}
if !gradient.writeGradient(session, buffer) {
return ""
}
buffer.WriteString(") ")
return buffer.String()
}
func (gradient *backgroundRadialGradient) Tag() string {
return "radial-gradient"
}
func (image *backgroundRadialGradient) Clone() BackgroundElement {
result := NewBackgroundRadialGradient(nil)
for tag, value := range image.properties {
result.setRaw(tag, value)
}
return result
}
func (gradient *backgroundRadialGradient) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag {
case Radius:
tag = RadialGradientRadius
case Shape:
tag = RadialGradientShape
case "x-center":
tag = CenterX
case "y-center":
tag = CenterY
}
return tag
}
func (gradient *backgroundRadialGradient) Set(tag string, value any) bool {
tag = gradient.normalizeTag(tag)
switch tag {
case RadialGradientRadius:
switch value := value.(type) {
case []SizeUnit:
switch len(value) {
case 0:
delete(gradient.properties, RadialGradientRadius)
return true
case 1:
if value[0].Type == Auto {
delete(gradient.properties, RadialGradientRadius)
} else {
gradient.properties[RadialGradientRadius] = value[0]
}
return true
default:
gradient.properties[RadialGradientRadius] = value
return true
}
case []any:
switch len(value) {
case 0:
delete(gradient.properties, RadialGradientRadius)
return true
case 1:
return gradient.Set(RadialGradientRadius, value[0])
default:
gradient.properties[RadialGradientRadius] = value
return true
}
case string:
if gradient.setSimpleProperty(RadialGradientRadius, value) {
return true
}
if size, err := stringToSizeUnit(value); err == nil {
if size.Type == Auto {
delete(gradient.properties, RadialGradientRadius)
} else {
gradient.properties[RadialGradientRadius] = size
}
return true
}
return gradient.setEnumProperty(RadialGradientRadius, value, enumProperties[RadialGradientRadius].values)
case SizeUnit:
if value.Type == Auto {
delete(gradient.properties, RadialGradientRadius)
} else {
gradient.properties[RadialGradientRadius] = value
}
return true
case int:
n := value
if n >= 0 && n < len(enumProperties[RadialGradientRadius].values) {
return gradient.propertyList.Set(RadialGradientRadius, value)
}
}
ErrorLogF(`Invalid value of "%s" property: %v`, tag, value)
case RadialGradientShape, CenterX, CenterY:
return gradient.propertyList.Set(tag, value)
}
return gradient.backgroundGradient.Set(tag, value)
}
func (gradient *backgroundRadialGradient) Get(tag string) any {
return gradient.backgroundGradient.Get(gradient.normalizeTag(tag))
}
func (gradient *backgroundRadialGradient) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if repeating, _ := boolProperty(gradient, Repeating, session); repeating {
buffer.WriteString(`repeating-radial-gradient(`)
} else {
buffer.WriteString(`radial-gradient(`)
}
var shapeText string
if shape, ok := enumProperty(gradient, RadialGradientShape, session, EllipseGradient); ok && shape == CircleGradient {
shapeText = `circle `
} else {
shapeText = `ellipse `
}
if value, ok := gradient.properties[RadialGradientRadius]; ok {
switch value := value.(type) {
case string:
if text, ok := session.resolveConstants(value); ok {
values := enumProperties[RadialGradientRadius]
if n, ok := enumStringToInt(text, values.values, false); ok {
buffer.WriteString(shapeText)
shapeText = ""
buffer.WriteString(values.cssValues[n])
buffer.WriteString(" ")
} else {
if r, ok := StringToSizeUnit(text); ok && r.Type != Auto {
buffer.WriteString("ellipse ")
shapeText = ""
buffer.WriteString(r.cssString("", session))
buffer.WriteString(" ")
buffer.WriteString(r.cssString("", session))
buffer.WriteString(" ")
} else {
ErrorLog(`Invalid radial gradient radius: ` + text)
}
}
} else {
ErrorLog(`Invalid radial gradient radius: ` + value)
}
case int:
values := enumProperties[RadialGradientRadius].cssValues
if value >= 0 && value < len(values) {
buffer.WriteString(shapeText)
shapeText = ""
buffer.WriteString(values[value])
buffer.WriteString(" ")
} else {
ErrorLogF(`Invalid radial gradient radius: %d`, value)
}
case SizeUnit:
if value.Type != Auto {
buffer.WriteString("ellipse ")
shapeText = ""
buffer.WriteString(value.cssString("", session))
buffer.WriteString(" ")
buffer.WriteString(value.cssString("", session))
buffer.WriteString(" ")
}
case []SizeUnit:
count := len(value)
if count > 2 {
count = 2
}
buffer.WriteString("ellipse ")
shapeText = ""
for i := 0; i < count; i++ {
buffer.WriteString(value[i].cssString("50%", session))
buffer.WriteString(" ")
}
case []any:
count := len(value)
if count > 2 {
count = 2
}
buffer.WriteString("ellipse ")
shapeText = ""
for i := 0; i < count; i++ {
if value[i] != nil {
switch value := value[i].(type) {
case SizeUnit:
buffer.WriteString(value.cssString("50%", session))
buffer.WriteString(" ")
case string:
if text, ok := session.resolveConstants(value); ok {
if size, err := stringToSizeUnit(text); err == nil {
buffer.WriteString(size.cssString("50%", session))
buffer.WriteString(" ")
} else {
buffer.WriteString("50% ")
}
} else {
buffer.WriteString("50% ")
}
}
} else {
buffer.WriteString("50% ")
}
}
}
}
x, _ := sizeProperty(gradient, CenterX, session)
y, _ := sizeProperty(gradient, CenterX, session)
if x.Type != Auto || y.Type != Auto {
if shapeText != "" {
buffer.WriteString(shapeText)
}
buffer.WriteString("at ")
buffer.WriteString(x.cssString("50%", session))
buffer.WriteString(" ")
buffer.WriteString(y.cssString("50%", session))
}
buffer.WriteString(", ")
if !gradient.writeGradient(session, buffer) {
return ""
}
buffer.WriteString(") ")
return buffer.String()
}

217
backgroundImage.go Normal file
View File

@ -0,0 +1,217 @@
package rui
import (
"strings"
)
// Constants related to view's background description
const (
// NoRepeat is value of the Repeat property of an background image:
//
// The image is not repeated (and hence the background image painting area
// will not necessarily be entirely covered). The position of the non-repeated
// background image is defined by the background-position CSS property.
NoRepeat = 0
// RepeatXY is value of the Repeat property of an background image:
//
// The image is repeated as much as needed to cover the whole background
// image painting area. The last image will be clipped if it doesn't fit.
RepeatXY = 1
// RepeatX is value of the Repeat property of an background image:
//
// The image is repeated horizontally as much as needed to cover
// the whole width background image painting area. The image is not repeated vertically.
// The last image will be clipped if it doesn't fit.
RepeatX = 2
// RepeatY is value of the Repeat property of an background image:
//
// The image is repeated vertically as much as needed to cover
// the whole height background image painting area. The image is not repeated horizontally.
// The last image will be clipped if it doesn't fit.
RepeatY = 3
// RepeatRound is value of the Repeat property of an background image:
//
// As the allowed space increases in size, the repeated images will stretch (leaving no gaps)
// until there is room (space left >= half of the image width) for another one to be added.
// When the next image is added, all of the current ones compress to allow room.
RepeatRound = 4
// RepeatSpace is value of the Repeat property of an background image:
//
// The image is repeated as much as possible without clipping. The first and last images
// are pinned to either side of the element, and whitespace is distributed evenly between the images.
RepeatSpace = 5
// ScrollAttachment is value of the Attachment property of an background image:
//
// The background is fixed relative to the element itself and does not scroll with its contents.
// (It is effectively attached to the element's border.)
ScrollAttachment = 0
// FixedAttachment is value of the Attachment property of an background image:
//
// The background is fixed relative to the viewport. Even if an element has
// a scrolling mechanism, the background doesn't move with the element.
FixedAttachment = 1
// LocalAttachment is value of the Attachment property of an background image:
//
// The background is fixed relative to the element's contents. If the element has a scrolling mechanism,
// the background scrolls with the element's contents, and the background painting area
// and background positioning area are relative to the scrollable area of the element
// rather than to the border framing them.
LocalAttachment = 2
)
type backgroundImage struct {
backgroundElement
}
// NewBackgroundImage creates the new background image
//
// The following properties can be used:
// - "src" [Source] - the name of the image in the "images" folder of the resources, or the URL of the image or inline-image.
// - "width" [Width] - the width of the image.
// - "height" [Height] - the height of the image.
// - "image-horizontal-align" [ImageHorizontalAlign] - the horizontal alignment of the image relative to view's bounds.
// - "image-vertical-align" [ImageVerticalAlign] - the vertical alignment of the image relative to view's bounds.
// - "repeat" [Repeat] - the repetition of the image.
// - "fit" [Fit] - the image scaling parameters.
// - "attachment" [Attachment] - defines whether a background image's position is fixed within the viewport or scrolls with its containing block.
func NewBackgroundImage(params Params) BackgroundElement {
result := new(backgroundImage)
result.init()
for tag, value := range params {
result.Set(tag, value)
}
return result
}
func (image *backgroundImage) init() {
image.backgroundElement.init()
image.normalize = normalizeBackgroundImageTag
image.supportedProperties = []PropertyName{
Attachment, Width, Height, Repeat, ImageHorizontalAlign, ImageVerticalAlign, backgroundFit, Source,
}
}
func (image *backgroundImage) Tag() string {
return "image"
}
func (image *backgroundImage) Clone() BackgroundElement {
result := NewBackgroundImage(nil)
for tag, value := range image.properties {
result.setRaw(tag, value)
}
return result
}
func normalizeBackgroundImageTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case "source":
tag = Source
case Fit:
tag = backgroundFit
case HorizontalAlign:
tag = ImageHorizontalAlign
case VerticalAlign:
tag = ImageVerticalAlign
}
return tag
}
func (image *backgroundImage) cssStyle(session Session) string {
if src, ok := imageProperty(image, Source, session); ok && src != "" {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(`url(`)
buffer.WriteString(src)
buffer.WriteRune(')')
attachment, _ := enumProperty(image, Attachment, session, NoRepeat)
values := enumProperties[Attachment].values
if attachment > 0 && attachment < len(values) {
buffer.WriteRune(' ')
buffer.WriteString(values[attachment])
}
align, _ := enumProperty(image, ImageHorizontalAlign, session, LeftAlign)
values = enumProperties[ImageHorizontalAlign].values
if align >= 0 && align < len(values) {
buffer.WriteRune(' ')
buffer.WriteString(values[align])
} else {
buffer.WriteString(` left`)
}
align, _ = enumProperty(image, ImageVerticalAlign, session, TopAlign)
values = enumProperties[ImageVerticalAlign].values
if align >= 0 && align < len(values) {
buffer.WriteRune(' ')
buffer.WriteString(values[align])
} else {
buffer.WriteString(` top`)
}
fit, _ := enumProperty(image, backgroundFit, session, NoneFit)
values = enumProperties[backgroundFit].values
if fit > 0 && fit < len(values) {
buffer.WriteString(` / `)
buffer.WriteString(values[fit])
} else {
width, _ := sizeProperty(image, Width, session)
height, _ := sizeProperty(image, Height, session)
if width.Type != Auto || height.Type != Auto {
buffer.WriteString(` / `)
buffer.WriteString(width.cssString("auto", session))
buffer.WriteRune(' ')
buffer.WriteString(height.cssString("auto", session))
}
}
repeat, _ := enumProperty(image, Repeat, session, NoRepeat)
values = enumProperties[Repeat].values
if repeat >= 0 && repeat < len(values) {
buffer.WriteRune(' ')
buffer.WriteString(values[repeat])
} else {
buffer.WriteString(` no-repeat`)
}
return buffer.String()
}
return ""
}
func (image *backgroundImage) writeString(buffer *strings.Builder, indent string) {
image.writeToBuffer(buffer, indent, image.Tag(), []PropertyName{
Source,
Width,
Height,
ImageHorizontalAlign,
ImageVerticalAlign,
backgroundFit,
Repeat,
Attachment,
})
}
func (image *backgroundImage) String() string {
return runStringWriter(image)
}

406
backgroundLinearGradient.go Normal file
View File

@ -0,0 +1,406 @@
package rui
import "strings"
type LinearGradientDirectionType int
// Constants related to view's background gradient description
const (
// ToTopGradient is value of the Direction property of a linear gradient. The value is equivalent to the 0deg angle
ToTopGradient LinearGradientDirectionType = 0
// ToRightTopGradient is value of the Direction property of a linear gradient.
ToRightTopGradient LinearGradientDirectionType = 1
// ToRightGradient is value of the Direction property of a linear gradient. The value is equivalent to the 90deg angle
ToRightGradient LinearGradientDirectionType = 2
// ToRightBottomGradient is value of the Direction property of a linear gradient.
ToRightBottomGradient LinearGradientDirectionType = 3
// ToBottomGradient is value of the Direction property of a linear gradient. The value is equivalent to the 180deg angle
ToBottomGradient LinearGradientDirectionType = 4
// ToLeftBottomGradient is value of the Direction property of a linear gradient.
ToLeftBottomGradient LinearGradientDirectionType = 5
// ToLeftGradient is value of the Direction property of a linear gradient. The value is equivalent to the 270deg angle
ToLeftGradient LinearGradientDirectionType = 6
// ToLeftTopGradient is value of the Direction property of a linear gradient.
ToLeftTopGradient LinearGradientDirectionType = 7
)
// BackgroundGradientPoint define point on gradient straight line
type BackgroundGradientPoint struct {
// Color - the color of the point. Must not be nil.
// Can take a value of Color type or string (color constant or textual description of the color)
Color any
// Pos - the distance from the start of the gradient straight line. Optional (may be nil).
// Can take a value of SizeUnit type or string (size constant or textual description of the SizeUnit)
Pos any
}
type backgroundGradient struct {
backgroundElement
}
type backgroundLinearGradient struct {
backgroundGradient
}
// NewBackgroundLinearGradient creates the new background linear gradient.
//
// The following properties can be used:
// - "gradient" [Gradient] - Describes gradient stop points. This is a mandatory property while describing background gradients.
// - "direction" [Direction] - Defines the direction of the gradient line.
// - "repeating" [Repeating] - Defines whether stop points needs to be repeated after the last one.
func NewBackgroundLinearGradient(params Params) BackgroundElement {
result := new(backgroundLinearGradient)
result.init()
for tag, value := range params {
result.Set(tag, value)
}
return result
}
// NewLinearGradient creates the new background linear gradient.
func NewLinearGradient[DirectionType LinearGradientDirectionType | AngleUnit](direction DirectionType, repeating bool, point1 GradientPoint, point2 GradientPoint, points ...GradientPoint) BackgroundElement {
params := Params{
Direction: direction,
Gradient: append([]GradientPoint{point1, point2}, points...),
}
if repeating {
params[Repeating] = true
}
return NewBackgroundLinearGradient(params)
}
func parseGradientText(value string) []BackgroundGradientPoint {
elements := strings.Split(value, ",")
count := len(elements)
if count < 2 {
ErrorLog("The gradient must contain at least 2 points")
return nil
}
points := make([]BackgroundGradientPoint, count)
for i, element := range elements {
if !points[i].setValue(element) {
ErrorLogF(`Invalid %d element of the conic gradient: "%s"`, i, element)
return nil
}
}
return points
}
func backgroundGradientSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case Repeating:
return setBoolProperty(properties, tag, value)
case Gradient:
switch value := value.(type) {
case string:
if ok, _ := isConstantName(value); ok {
properties.setRaw(Gradient, value)
return []PropertyName{tag}
}
if strings.ContainsAny(value, " ,") {
if points := parseGradientText(value); len(points) >= 2 {
properties.setRaw(Gradient, points)
return []PropertyName{tag}
}
}
case []BackgroundGradientPoint:
if len(value) >= 2 {
properties.setRaw(Gradient, value)
return []PropertyName{tag}
}
case []Color:
count := len(value)
if count >= 2 {
points := make([]BackgroundGradientPoint, count)
for i, color := range value {
points[i].Color = color
}
properties.setRaw(Gradient, points)
return []PropertyName{tag}
}
case []GradientPoint:
count := len(value)
if count >= 2 {
points := make([]BackgroundGradientPoint, count)
for i, point := range value {
points[i].Color = point.Color
points[i].Pos = Percent(point.Offset * 100)
}
properties.setRaw(Gradient, points)
return []PropertyName{tag}
}
}
ErrorLogF("Invalid gradient %v", value)
return nil
}
ErrorLogF("Property %s is not supported by a background gradient", tag)
return nil
}
func (point *BackgroundGradientPoint) setValue(text string) bool {
text = strings.Trim(text, " ")
colorText := text
pointText := ""
if index := strings.Index(text, " "); index > 0 {
colorText = text[:index]
pointText = strings.Trim(text[index+1:], " ")
}
if colorText == "" {
return false
}
if ok, _ := isConstantName(colorText); ok {
point.Color = colorText
} else if color, ok := StringToColor(colorText); ok {
point.Color = color
} else {
return false
}
if pointText == "" {
point.Pos = nil
} else if ok, _ := isConstantName(pointText); ok {
point.Pos = pointText
} else if pos, ok := StringToSizeUnit(pointText); ok {
point.Pos = pos
} else {
return false
}
return true
}
func (point *BackgroundGradientPoint) color(session Session) (Color, bool) {
if point.Color != nil {
switch color := point.Color.(type) {
case string:
if ok, constName := isConstantName(color); ok {
return session.Color(constName)
}
return StringToColor(color)
case Color:
return color, true
default:
if n, ok := isInt(point.Color); ok {
return Color(n), true
}
}
}
return 0, false
}
// String convert internal representation of [BackgroundGradientPoint] into a string.
func (point *BackgroundGradientPoint) String() string {
result := "black"
if point.Color != nil {
switch color := point.Color.(type) {
case string:
result = color
case Color:
result = color.String()
}
}
if point.Pos != nil {
switch value := point.Pos.(type) {
case string:
result += " " + value
case SizeUnit:
if value.Type != Auto {
result += " " + value.String()
}
}
}
return result
}
func (gradient *backgroundGradient) writeGradient(session Session, buffer *strings.Builder) bool {
value, ok := gradient.properties[Gradient]
if !ok {
return false
}
var points []BackgroundGradientPoint = nil
switch value := value.(type) {
case string:
if ok, constName := isConstantName(value); ok {
if text, ok := session.Constant(constName); ok {
points = parseGradientText(text)
}
}
case []BackgroundGradientPoint:
points = value
}
if len(points) > 0 {
for i, point := range points {
if i > 0 {
buffer.WriteString(`, `)
}
if color, ok := point.color(session); ok {
buffer.WriteString(color.cssString())
} else {
return false
}
if point.Pos != nil {
switch value := point.Pos.(type) {
case string:
if value != "" {
if value, ok := session.resolveConstants(value); ok {
if pos, ok := StringToSizeUnit(value); ok && pos.Type != Auto {
buffer.WriteRune(' ')
buffer.WriteString(pos.cssString("", session))
}
}
}
case SizeUnit:
if value.Type != Auto {
buffer.WriteRune(' ')
buffer.WriteString(value.cssString("", session))
}
}
}
}
return true
}
return false
}
func (gradient *backgroundLinearGradient) init() {
gradient.backgroundElement.init()
gradient.set = backgroundLinearGradientSet
gradient.supportedProperties = []PropertyName{Direction, Repeating, Gradient}
}
func (gradient *backgroundLinearGradient) Tag() string {
return "linear-gradient"
}
func (image *backgroundLinearGradient) Clone() BackgroundElement {
result := NewBackgroundLinearGradient(nil)
for tag, value := range image.properties {
result.setRaw(tag, value)
}
return result
}
func backgroundLinearGradientSet(properties Properties, tag PropertyName, value any) []PropertyName {
if tag == Direction {
switch value := value.(type) {
case AngleUnit:
properties.setRaw(Direction, value)
return []PropertyName{tag}
case string:
if setSimpleProperty(properties, tag, value) {
return []PropertyName{tag}
}
if angle, ok := StringToAngleUnit(value); ok {
properties.setRaw(Direction, angle)
return []PropertyName{tag}
}
case LinearGradientDirectionType:
return setEnumProperty(properties, tag, int(value), enumProperties[Direction].values)
}
return setEnumProperty(properties, tag, value, enumProperties[Direction].values)
}
return backgroundGradientSet(properties, tag, value)
}
func (gradient *backgroundLinearGradient) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if repeating, _ := boolProperty(gradient, Repeating, session); repeating {
buffer.WriteString(`repeating-linear-gradient(`)
} else {
buffer.WriteString(`linear-gradient(`)
}
if value, ok := gradient.properties[Direction]; ok {
switch value := value.(type) {
case string:
if text, ok := session.resolveConstants(value); ok {
direction := enumProperties[Direction]
if n, ok := enumStringToInt(text, direction.values, false); ok {
buffer.WriteString(direction.cssValues[n])
buffer.WriteString(", ")
} else {
if angle, ok := StringToAngleUnit(text); ok {
buffer.WriteString(angle.cssString())
buffer.WriteString(", ")
} else {
ErrorLog(`Invalid linear gradient direction: ` + text)
}
}
} else {
ErrorLog(`Invalid linear gradient direction: ` + value)
}
case int:
values := enumProperties[Direction].cssValues
if value >= 0 && value < len(values) {
buffer.WriteString(values[value])
buffer.WriteString(", ")
} else {
ErrorLogF(`Invalid linear gradient direction: %d`, value)
}
case AngleUnit:
buffer.WriteString(value.cssString())
buffer.WriteString(", ")
}
}
if !gradient.writeGradient(session, buffer) {
return ""
}
buffer.WriteString(") ")
return buffer.String()
}
func (gradient *backgroundLinearGradient) writeString(buffer *strings.Builder, indent string) {
gradient.writeToBuffer(buffer, indent, gradient.Tag(), []PropertyName{
Gradient,
Repeating,
Direction,
})
}
func (gradient *backgroundLinearGradient) String() string {
return runStringWriter(gradient)
}

357
backgroundRadialGradient.go Normal file
View File

@ -0,0 +1,357 @@
package rui
import "strings"
type RadialGradientRadiusType int
// Constants related to view's background gradient description
const (
// EllipseGradient is value of the Shape property of a radial gradient background:
// the shape is an axis-aligned ellipse
EllipseGradient = 0
// CircleGradient is value of the Shape property of a radial gradient background:
// the gradient's shape is a circle with constant radius
CircleGradient = 1
// ClosestSideGradient is value of the Radius property of a radial gradient background:
// The gradient's ending shape meets the side of the box closest to its center (for circles)
// or meets both the vertical and horizontal sides closest to the center (for ellipses).
ClosestSideGradient RadialGradientRadiusType = 0
// ClosestCornerGradient is value of the Radius property of a radial gradient background:
// The gradient's ending shape is sized so that it exactly meets the closest corner
// of the box from its center.
ClosestCornerGradient RadialGradientRadiusType = 1
// FarthestSideGradient is value of the Radius property of a radial gradient background:
// Similar to closest-side, except the ending shape is sized to meet the side of the box
// farthest from its center (or vertical and horizontal sides).
FarthestSideGradient RadialGradientRadiusType = 2
// FarthestCornerGradient is value of the Radius property of a radial gradient background:
// The default value, the gradient's ending shape is sized so that it exactly meets
// the farthest corner of the box from its center.
FarthestCornerGradient RadialGradientRadiusType = 3
)
type backgroundRadialGradient struct {
backgroundGradient
}
// NewBackgroundRadialGradient creates the new background radial gradient.
//
// The following properties can be used:
// - "gradient" (Gradient) - Describes gradient stop points. This is a mandatory property while describing background gradients.
// - "center-x" (CenterX), "center-y" (CenterY) - Defines the gradient center point coordinates.
// - "radial-gradient-radius" (RadialGradientRadius) - Defines radius of the radial gradient.
// - "radial-gradient-shape" (RadialGradientShape) - Defines shape of the radial gradient.
// - "repeating" (Repeating) - Defines whether stop points needs to be repeated after the last one.
func NewBackgroundRadialGradient(params Params) BackgroundElement {
result := new(backgroundRadialGradient)
result.init()
for tag, value := range params {
result.Set(tag, value)
}
return result
}
// NewCircleRadialGradient creates the new background circle radial gradient.
func NewCircleRadialGradient[radiusType SizeUnit | RadialGradientRadiusType](xCenter, yCenter SizeUnit, radius radiusType, repeating bool, point1 GradientPoint, point2 GradientPoint, points ...GradientPoint) BackgroundElement {
params := Params{
RadialGradientShape: CircleGradient,
Gradient: append([]GradientPoint{point1, point2}, points...),
RadialGradientRadius: radius,
}
if xCenter.Type != Auto {
params[CenterX] = xCenter
}
if yCenter.Type != Auto {
params[CenterY] = yCenter
}
if repeating {
params[Repeating] = true
}
return NewBackgroundRadialGradient(params)
}
// NewEllipseRadialGradient creates the new background ellipse radial gradient.
func NewEllipseRadialGradient[radiusType []SizeUnit | RadialGradientRadiusType](xCenter, yCenter SizeUnit, radius radiusType, repeating bool, point1 GradientPoint, point2 GradientPoint, points ...GradientPoint) BackgroundElement {
params := Params{
RadialGradientShape: EllipseGradient,
Gradient: append([]GradientPoint{point1, point2}, points...),
RadialGradientRadius: radius,
}
if xCenter.Type != Auto {
params[CenterX] = xCenter
}
if yCenter.Type != Auto {
params[CenterY] = yCenter
}
if repeating {
params[Repeating] = true
}
return NewBackgroundRadialGradient(params)
}
func (gradient *backgroundRadialGradient) init() {
gradient.backgroundElement.init()
gradient.normalize = normalizeRadialGradientTag
gradient.set = backgroundRadialGradientSet
gradient.supportedProperties = []PropertyName{
RadialGradientRadius, RadialGradientShape, CenterX, CenterY, Gradient, Repeating,
}
}
func (gradient *backgroundRadialGradient) Tag() string {
return "radial-gradient"
}
func (image *backgroundRadialGradient) Clone() BackgroundElement {
result := NewBackgroundRadialGradient(nil)
for tag, value := range image.properties {
result.setRaw(tag, value)
}
return result
}
func normalizeRadialGradientTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Radius:
tag = RadialGradientRadius
case Shape:
tag = RadialGradientShape
case "x-center":
tag = CenterX
case "y-center":
tag = CenterY
}
return tag
}
func backgroundRadialGradientSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case RadialGradientRadius:
switch value := value.(type) {
case []SizeUnit:
switch len(value) {
case 0:
properties.setRaw(RadialGradientRadius, nil)
case 1:
if value[0].Type == Auto {
properties.setRaw(RadialGradientRadius, nil)
} else {
properties.setRaw(RadialGradientRadius, value[0])
}
default:
properties.setRaw(RadialGradientRadius, value)
}
return []PropertyName{tag}
case []any:
switch len(value) {
case 0:
properties.setRaw(RadialGradientRadius, nil)
return []PropertyName{tag}
case 1:
return backgroundRadialGradientSet(properties, RadialGradientRadius, value[0])
default:
properties.setRaw(RadialGradientRadius, value)
return []PropertyName{tag}
}
case string:
if setSimpleProperty(properties, RadialGradientRadius, value) {
return []PropertyName{tag}
}
if size, err := stringToSizeUnit(value); err == nil {
if size.Type == Auto {
properties.setRaw(RadialGradientRadius, nil)
} else {
properties.setRaw(RadialGradientRadius, size)
}
return []PropertyName{tag}
}
return setEnumProperty(properties, RadialGradientRadius, value, enumProperties[RadialGradientRadius].values)
case SizeUnit:
if value.Type == Auto {
properties.setRaw(RadialGradientRadius, nil)
} else {
properties.setRaw(RadialGradientRadius, value)
}
return []PropertyName{tag}
case RadialGradientRadiusType:
return setEnumProperty(properties, RadialGradientRadius, int(value), enumProperties[RadialGradientRadius].values)
case int:
return setEnumProperty(properties, RadialGradientRadius, value, enumProperties[RadialGradientRadius].values)
}
ErrorLogF(`Invalid value of "%s" property: %v`, tag, value)
return nil
case RadialGradientShape, CenterX, CenterY:
return propertiesSet(properties, tag, value)
}
return backgroundGradientSet(properties, tag, value)
}
func (gradient *backgroundRadialGradient) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if repeating, _ := boolProperty(gradient, Repeating, session); repeating {
buffer.WriteString(`repeating-radial-gradient(`)
} else {
buffer.WriteString(`radial-gradient(`)
}
var shapeText string
if shape, ok := enumProperty(gradient, RadialGradientShape, session, EllipseGradient); ok && shape == CircleGradient {
shapeText = `circle `
} else {
shapeText = `ellipse `
}
if value, ok := gradient.properties[RadialGradientRadius]; ok {
switch value := value.(type) {
case string:
if text, ok := session.resolveConstants(value); ok {
values := enumProperties[RadialGradientRadius]
if n, ok := enumStringToInt(text, values.values, false); ok {
buffer.WriteString(shapeText)
shapeText = ""
buffer.WriteString(values.cssValues[n])
buffer.WriteString(" ")
} else {
if r, ok := StringToSizeUnit(text); ok && r.Type != Auto {
buffer.WriteString("ellipse ")
shapeText = ""
buffer.WriteString(r.cssString("", session))
buffer.WriteString(" ")
buffer.WriteString(r.cssString("", session))
buffer.WriteString(" ")
} else {
ErrorLog(`Invalid radial gradient radius: ` + text)
}
}
} else {
ErrorLog(`Invalid radial gradient radius: ` + value)
}
case int:
values := enumProperties[RadialGradientRadius].cssValues
if value >= 0 && value < len(values) {
buffer.WriteString(shapeText)
shapeText = ""
buffer.WriteString(values[value])
buffer.WriteString(" ")
} else {
ErrorLogF(`Invalid radial gradient radius: %d`, value)
}
case SizeUnit:
if value.Type != Auto {
buffer.WriteString("ellipse ")
shapeText = ""
buffer.WriteString(value.cssString("", session))
buffer.WriteString(" ")
buffer.WriteString(value.cssString("", session))
buffer.WriteString(" ")
}
case []SizeUnit:
count := len(value)
if count > 2 {
count = 2
}
buffer.WriteString("ellipse ")
shapeText = ""
for i := range count {
buffer.WriteString(value[i].cssString("50%", session))
buffer.WriteString(" ")
}
case []any:
count := len(value)
if count > 2 {
count = 2
}
buffer.WriteString("ellipse ")
shapeText = ""
for i := range count {
if value[i] != nil {
switch value := value[i].(type) {
case SizeUnit:
buffer.WriteString(value.cssString("50%", session))
buffer.WriteString(" ")
case string:
if text, ok := session.resolveConstants(value); ok {
if size, err := stringToSizeUnit(text); err == nil {
buffer.WriteString(size.cssString("50%", session))
buffer.WriteString(" ")
} else {
buffer.WriteString("50% ")
}
} else {
buffer.WriteString("50% ")
}
}
} else {
buffer.WriteString("50% ")
}
}
}
}
x, _ := sizeProperty(gradient, CenterX, session)
y, _ := sizeProperty(gradient, CenterX, session)
if x.Type != Auto || y.Type != Auto {
if shapeText != "" {
buffer.WriteString(shapeText)
}
buffer.WriteString("at ")
buffer.WriteString(x.cssString("50%", session))
buffer.WriteString(" ")
buffer.WriteString(y.cssString("50%", session))
} else if shapeText != "" {
buffer.WriteString(shapeText)
}
buffer.WriteString(", ")
if !gradient.writeGradient(session, buffer) {
return ""
}
buffer.WriteString(") ")
return buffer.String()
}
func (gradient *backgroundRadialGradient) writeString(buffer *strings.Builder, indent string) {
gradient.writeToBuffer(buffer, indent, gradient.Tag(), []PropertyName{
Gradient,
CenterX,
CenterY,
Repeating,
RadialGradientShape,
RadialGradientRadius,
})
}
func (gradient *backgroundRadialGradient) String() string {
return runStringWriter(gradient)
}

622
border.go
View File

@ -5,44 +5,173 @@ import (
"strings" "strings"
) )
// Constants related to view's border description
const ( const (
// NoneLine constant specifies that there is no border // NoneLine constant specifies that there is no border
NoneLine = 0 NoneLine = 0
// SolidLine constant specifies the border/line as a solid line // SolidLine constant specifies the border/line as a solid line
SolidLine = 1 SolidLine = 1
// DashedLine constant specifies the border/line as a dashed line // DashedLine constant specifies the border/line as a dashed line
DashedLine = 2 DashedLine = 2
// DottedLine constant specifies the border/line as a dotted line // DottedLine constant specifies the border/line as a dotted line
DottedLine = 3 DottedLine = 3
// DoubleLine constant specifies the border/line as a double solid line // DoubleLine constant specifies the border/line as a double solid line
DoubleLine = 4 DoubleLine = 4
// DoubleLine constant specifies the border/line as a double solid line // DoubleLine constant specifies the border/line as a double solid line
WavyLine = 5 WavyLine = 5
// LeftStyle is the constant for "left-style" property tag. // LeftStyle is the constant for "left-style" property tag.
LeftStyle = "left-style" //
// RightStyle is the constant for "-right-style" property tag. // Used by BorderProperty.
RightStyle = "right-style" // Left border line style.
//
// Supported types: int, string.
//
// Values:
// - 0 (NoneLine) or "none" - The border will not be drawn.
// - 1 (SolidLine) or "solid" - Solid line as a border.
// - 2 (DashedLine) or "dashed" - Dashed line as a border.
// - 3 (DottedLine) or "dotted" - Dotted line as a border.
// - 4 (DoubleLine) or "double" - Double line as a border.
LeftStyle PropertyName = "left-style"
// RightStyle is the constant for "right-style" property tag.
//
// Used by BorderProperty.
// Right border line style.
//
// Supported types: int, string.
//
// Values:
// - 0 (NoneLine) or "none" - The border will not be drawn.
// - 1 (SolidLine) or "solid" - Solid line as a border.
// - 2 (DashedLine) or "dashed" - Dashed line as a border.
// - 3 (DottedLine) or "dotted" - Dotted line as a border.
// - 4 (DoubleLine) or "double" - Double line as a border.
RightStyle PropertyName = "right-style"
// TopStyle is the constant for "top-style" property tag. // TopStyle is the constant for "top-style" property tag.
TopStyle = "top-style" //
// Used by BorderProperty.
// Top border line style.
//
// Supported types: int, string.
//
// Values:
// - 0 (NoneLine) or "none" - The border will not be drawn.
// - 1 (SolidLine) or "solid" - Solid line as a border.
// - 2 (DashedLine) or "dashed" - Dashed line as a border.
// - 3 (DottedLine) or "dotted" - Dotted line as a border.
// - 4 (DoubleLine) or "double" - Double line as a border.
TopStyle PropertyName = "top-style"
// BottomStyle is the constant for "bottom-style" property tag. // BottomStyle is the constant for "bottom-style" property tag.
BottomStyle = "bottom-style" //
// Used by BorderProperty.
// Bottom border line style.
//
// Supported types: int, string.
//
// Values:
// - 0 (NoneLine) or "none" - The border will not be drawn.
// - 1 (SolidLine) or "solid" - Solid line as a border.
// - 2 (DashedLine) or "dashed" - Dashed line as a border.
// - 3 (DottedLine) or "dotted" - Dotted line as a border.
// - 4 (DoubleLine) or "double" - Double line as a border.
BottomStyle PropertyName = "bottom-style"
// LeftWidth is the constant for "left-width" property tag. // LeftWidth is the constant for "left-width" property tag.
LeftWidth = "left-width" //
// RightWidth is the constant for "-right-width" property tag. // Used by BorderProperty.
RightWidth = "right-width" // Left border line width.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See [SizeUnit] description for more details.
LeftWidth PropertyName = "left-width"
// RightWidth is the constant for "right-width" property tag.
//
// Used by BorderProperty.
// Right border line width.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See [SizeUnit] description for more details.
RightWidth PropertyName = "right-width"
// TopWidth is the constant for "top-width" property tag. // TopWidth is the constant for "top-width" property tag.
TopWidth = "top-width" //
// Used by BorderProperty.
// Top border line width.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See [SizeUnit] description for more details.
TopWidth PropertyName = "top-width"
// BottomWidth is the constant for "bottom-width" property tag. // BottomWidth is the constant for "bottom-width" property tag.
BottomWidth = "bottom-width" //
// Used by BorderProperty.
// Bottom border line width.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See [SizeUnit] description for more details.
BottomWidth PropertyName = "bottom-width"
// LeftColor is the constant for "left-color" property tag. // LeftColor is the constant for "left-color" property tag.
LeftColor = "left-color" //
// RightColor is the constant for "-right-color" property tag. // Used by BorderProperty.
RightColor = "right-color" // Left border line color.
//
// Supported types: Color, string.
//
// Internal type is Color, other types converted to it during assignment.
// See [Color] description for more details.
LeftColor PropertyName = "left-color"
// RightColor is the constant for "right-color" property tag.
//
// Used by BorderProperty.
// Right border line color.
//
// Supported types: Color, string.
//
// Internal type is Color, other types converted to it during assignment.
// See [Color] description for more details.
RightColor PropertyName = "right-color"
// TopColor is the constant for "top-color" property tag. // TopColor is the constant for "top-color" property tag.
TopColor = "top-color" //
// Used by BorderProperty.
// Top border line color.
//
// Supported types: Color, string.
//
// Internal type is Color, other types converted to it during assignment.
// See [Color] description for more details.
TopColor PropertyName = "top-color"
// BottomColor is the constant for "bottom-color" property tag. // BottomColor is the constant for "bottom-color" property tag.
BottomColor = "bottom-color" //
// Used by BorderProperty.
// Bottom border line color.
//
// Supported types: Color, string.
//
// Internal type is Color, other types converted to it during assignment.
// See [Color] description for more details.
BottomColor PropertyName = "bottom-color"
) )
// BorderProperty is the interface of a view border data // BorderProperty is the interface of a view border data
@ -50,8 +179,11 @@ type BorderProperty interface {
Properties Properties
fmt.Stringer fmt.Stringer
stringWriter stringWriter
// ViewBorders returns top, right, bottom and left borders information all together
ViewBorders(session Session) ViewBorders ViewBorders(session Session) ViewBorders
delete(tag string)
deleteTag(tag PropertyName) bool
cssStyle(builder cssBuilder, session Session) cssStyle(builder cssBuilder, session Session)
cssWidth(builder cssBuilder, session Session) cssWidth(builder cssBuilder, session Session)
cssColor(builder cssBuilder, session Session) cssColor(builder cssBuilder, session Session)
@ -61,12 +193,12 @@ type BorderProperty interface {
} }
type borderProperty struct { type borderProperty struct {
propertyList dataProperty
} }
func newBorderProperty(value any) BorderProperty { func newBorderProperty(value any) BorderProperty {
border := new(borderProperty) border := new(borderProperty)
border.properties = map[string]any{} border.init()
if value != nil { if value != nil {
switch value := value.(type) { switch value := value.(type) {
@ -128,12 +260,20 @@ func newBorderProperty(value any) BorderProperty {
return border return border
} }
// NewBorder creates the new BorderProperty // NewBorder creates the new BorderProperty.
// The following properties can be used:
//
// "style" (Style). Determines the line style (int). Valid values: 0 (NoneLine), 1 (SolidLine), 2 (DashedLine), 3 (DottedLine), or 4 (DoubleLine);
//
// "color" (ColorTag). Determines the line color (Color);
//
// "width" (Width). Determines the line thickness (SizeUnit).
func NewBorder(params Params) BorderProperty { func NewBorder(params Params) BorderProperty {
border := new(borderProperty) border := new(borderProperty)
border.properties = map[string]any{} border.init()
if params != nil { if params != nil {
for _, tag := range []string{Style, Width, ColorTag, Left, Right, Top, Bottom, for _, tag := range []PropertyName{Style, Width, ColorTag, Left, Right, Top, Bottom,
LeftStyle, RightStyle, TopStyle, BottomStyle, LeftStyle, RightStyle, TopStyle, BottomStyle,
LeftWidth, RightWidth, TopWidth, BottomWidth, LeftWidth, RightWidth, TopWidth, BottomWidth,
LeftColor, RightColor, TopColor, BottomColor} { LeftColor, RightColor, TopColor, BottomColor} {
@ -145,8 +285,37 @@ func NewBorder(params Params) BorderProperty {
return border return border
} }
func (border *borderProperty) normalizeTag(tag string) string { func (border *borderProperty) init() {
tag = strings.ToLower(tag) border.dataProperty.init()
border.normalize = normalizeBorderTag
border.get = borderGet
border.set = borderSet
border.remove = borderRemove
border.supportedProperties = []PropertyName{
Left,
Right,
Top,
Bottom,
Style,
LeftStyle,
RightStyle,
TopStyle,
BottomStyle,
Width,
LeftWidth,
RightWidth,
TopWidth,
BottomWidth,
ColorTag,
LeftColor,
RightColor,
TopColor,
BottomColor,
}
}
func normalizeBorderTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag { switch tag {
case BorderLeft, CellBorderLeft: case BorderLeft, CellBorderLeft:
return Left return Left
@ -213,23 +382,26 @@ func (border *borderProperty) writeString(buffer *strings.Builder, indent string
buffer.WriteString("_{ ") buffer.WriteString("_{ ")
comma := false comma := false
write := func(tag string, value any) { write := func(tag PropertyName, value any) {
if comma { text := propertyValueToString(tag, value, indent)
buffer.WriteString(", ") if text != "" {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
buffer.WriteString(text)
comma = true
} }
buffer.WriteString(tag)
buffer.WriteString(" = ")
writePropertyValue(buffer, BorderStyle, value, indent)
comma = true
} }
for _, tag := range []string{Style, Width, ColorTag} { for _, tag := range []PropertyName{Style, Width, ColorTag} {
if value, ok := border.properties[tag]; ok { if value, ok := border.properties[tag]; ok {
write(tag, value) write(tag, value)
} }
} }
for _, side := range []string{Top, Right, Bottom, Left} { for _, side := range []PropertyName{Top, Right, Bottom, Left} {
style, okStyle := border.properties[side+"-"+Style] style, okStyle := border.properties[side+"-"+Style]
width, okWidth := border.properties[side+"-"+Width] width, okWidth := border.properties[side+"-"+Width]
color, okColor := border.properties[side+"-"+ColorTag] color, okColor := border.properties[side+"-"+ColorTag]
@ -239,7 +411,7 @@ func (border *borderProperty) writeString(buffer *strings.Builder, indent string
comma = false comma = false
} }
buffer.WriteString(side) buffer.WriteString(string(side))
buffer.WriteString(" = _{ ") buffer.WriteString(" = _{ ")
if okStyle { if okStyle {
write(Style, style) write(Style, style)
@ -262,164 +434,92 @@ func (border *borderProperty) String() string {
return runStringWriter(border) return runStringWriter(border)
} }
func (border *borderProperty) setSingleBorderObject(prefix string, obj DataObject) bool {
result := true
if text, ok := obj.PropertyValue(Style); ok {
if !border.setEnumProperty(prefix+"-style", text, enumProperties[BorderStyle].values) {
result = false
}
}
if text, ok := obj.PropertyValue(ColorTag); ok {
if !border.setColorProperty(prefix+"-color", text) {
result = false
}
}
if text, ok := obj.PropertyValue("width"); ok {
if !border.setSizeProperty(prefix+"-width", text) {
result = false
}
}
return result
}
func (border *borderProperty) setBorderObject(obj DataObject) bool { func (border *borderProperty) setBorderObject(obj DataObject) bool {
result := true result := true
for node := range obj.Properties() {
for _, side := range []string{Top, Right, Bottom, Left} { tag := PropertyName(node.Tag())
if node := obj.PropertyWithTag(side); node != nil { switch node.Type() {
if node.Type() == ObjectNode { case TextNode:
if !border.setSingleBorderObject(side, node.Object()) { if borderSet(border, tag, node.Text()) == nil {
result = false
}
} else {
notCompatibleType(side, node)
result = false
}
}
}
if text, ok := obj.PropertyValue(Style); ok {
values := split4Values(text)
styles := enumProperties[BorderStyle].values
switch len(values) {
case 1:
if !border.setEnumProperty(Style, values[0], styles) {
result = false result = false
} }
case 4: case ObjectNode:
for n, tag := range [4]string{TopStyle, RightStyle, BottomStyle, LeftStyle} { if borderSet(border, tag, node.Object()) == nil {
if !border.setEnumProperty(tag, values[n], styles) {
result = false
}
}
default:
notCompatibleType(Style, text)
result = false
}
}
if text, ok := obj.PropertyValue(ColorTag); ok {
values := split4Values(text)
switch len(values) {
case 1:
if !border.setColorProperty(ColorTag, values[0]) {
return false
}
case 4:
for n, tag := range [4]string{TopColor, RightColor, BottomColor, LeftColor} {
if !border.setColorProperty(tag, values[n]) {
return false
}
}
default:
notCompatibleType(ColorTag, text)
result = false
}
}
if text, ok := obj.PropertyValue(Width); ok {
values := split4Values(text)
switch len(values) {
case 1:
if !border.setSizeProperty(Width, values[0]) {
result = false result = false
} }
case 4:
for n, tag := range [4]string{TopWidth, RightWidth, BottomWidth, LeftWidth} {
if !border.setSizeProperty(tag, values[n]) {
result = false
}
}
default: default:
notCompatibleType(Width, text)
result = false result = false
} }
} }
return result return result
} }
func (border *borderProperty) Remove(tag string) { func borderRemove(properties Properties, tag PropertyName) []PropertyName {
tag = border.normalizeTag(tag) result := []PropertyName{}
removeTag := func(t PropertyName) {
if properties.getRaw(t) != nil {
properties.setRaw(t, nil)
result = append(result, t)
}
}
switch tag { switch tag {
case Style: case Style:
for _, t := range []string{tag, TopStyle, RightStyle, BottomStyle, LeftStyle} { for _, t := range []PropertyName{tag, TopStyle, RightStyle, BottomStyle, LeftStyle} {
delete(border.properties, t) removeTag(t)
} }
case Width: case Width:
for _, t := range []string{tag, TopWidth, RightWidth, BottomWidth, LeftWidth} { for _, t := range []PropertyName{tag, TopWidth, RightWidth, BottomWidth, LeftWidth} {
delete(border.properties, t) removeTag(t)
} }
case ColorTag: case ColorTag:
for _, t := range []string{tag, TopColor, RightColor, BottomColor, LeftColor} { for _, t := range []PropertyName{tag, TopColor, RightColor, BottomColor, LeftColor} {
delete(border.properties, t) removeTag(t)
} }
case Left, Right, Top, Bottom: case Left, Right, Top, Bottom:
border.Remove(tag + "-style") removeTag(tag + "-style")
border.Remove(tag + "-width") removeTag(tag + "-width")
border.Remove(tag + "-color") removeTag(tag + "-color")
case LeftStyle, RightStyle, TopStyle, BottomStyle: case LeftStyle, RightStyle, TopStyle, BottomStyle:
delete(border.properties, tag) removeTag(tag)
if style, ok := border.properties[Style]; ok && style != nil { if style := properties.getRaw(Style); style != nil {
for _, t := range []string{TopStyle, RightStyle, BottomStyle, LeftStyle} { for _, t := range []PropertyName{TopStyle, RightStyle, BottomStyle, LeftStyle} {
if t != tag { if t != tag {
if _, ok := border.properties[t]; !ok { if properties.getRaw(t) == nil {
border.properties[t] = style properties.setRaw(t, style)
result = append(result, t)
} }
} }
} }
} }
case LeftWidth, RightWidth, TopWidth, BottomWidth: case LeftWidth, RightWidth, TopWidth, BottomWidth:
delete(border.properties, tag) removeTag(tag)
if width, ok := border.properties[Width]; ok && width != nil { if width := properties.getRaw(Width); width != nil {
for _, t := range []string{TopWidth, RightWidth, BottomWidth, LeftWidth} { for _, t := range []PropertyName{TopWidth, RightWidth, BottomWidth, LeftWidth} {
if t != tag { if t != tag {
if _, ok := border.properties[t]; !ok { if properties.getRaw(t) == nil {
border.properties[t] = width properties.setRaw(t, width)
result = append(result, t)
} }
} }
} }
} }
case LeftColor, RightColor, TopColor, BottomColor: case LeftColor, RightColor, TopColor, BottomColor:
delete(border.properties, tag) removeTag(tag)
if color, ok := border.properties[ColorTag]; ok && color != nil { if color := properties.getRaw(ColorTag); color != nil {
for _, t := range []string{TopColor, RightColor, BottomColor, LeftColor} { for _, t := range []PropertyName{TopColor, RightColor, BottomColor, LeftColor} {
if t != tag { if t != tag {
if _, ok := border.properties[t]; !ok { if properties.getRaw(t) == nil {
border.properties[t] = color properties.setRaw(t, color)
result = append(result, t)
} }
} }
} }
@ -428,80 +528,121 @@ func (border *borderProperty) Remove(tag string) {
default: default:
ErrorLogF(`"%s" property is not compatible with the BorderProperty`, tag) ErrorLogF(`"%s" property is not compatible with the BorderProperty`, tag)
} }
return result
} }
func (border *borderProperty) Set(tag string, value any) bool { func borderSet(properties Properties, tag PropertyName, value any) []PropertyName {
if value == nil {
border.Remove(tag)
return true
}
tag = border.normalizeTag(tag) setSingleBorderObject := func(prefix PropertyName, obj DataObject) []PropertyName {
result := []PropertyName{}
if text, ok := obj.PropertyValue(string(Style)); ok {
props := setEnumProperty(properties, prefix+"-style", text, enumProperties[BorderStyle].values)
if props == nil {
return nil
}
result = append(result, props...)
}
if text, ok := obj.PropertyValue(string(ColorTag)); ok {
props := setColorProperty(properties, prefix+"-color", text)
if props == nil && len(result) == 0 {
return nil
}
result = append(result, props...)
}
if text, ok := obj.PropertyValue("width"); ok {
props := setSizeProperty(properties, prefix+"-width", text)
if props == nil && len(result) == 0 {
return nil
}
result = append(result, props...)
}
if len(result) > 0 {
result = append(result, prefix)
}
return result
}
switch tag { switch tag {
case Style: case Style:
if border.setEnumProperty(Style, value, enumProperties[BorderStyle].values) { if result := setEnumProperty(properties, Style, value, enumProperties[BorderStyle].values); result != nil {
for _, side := range []string{TopStyle, RightStyle, BottomStyle, LeftStyle} { for _, side := range []PropertyName{TopStyle, RightStyle, BottomStyle, LeftStyle} {
delete(border.properties, side) if value := properties.getRaw(side); value != nil {
properties.setRaw(side, nil)
result = append(result, side)
}
} }
return true return result
} }
case Width: case Width:
if border.setSizeProperty(Width, value) { if result := setSizeProperty(properties, Width, value); result != nil {
for _, side := range []string{TopWidth, RightWidth, BottomWidth, LeftWidth} { for _, side := range []PropertyName{TopWidth, RightWidth, BottomWidth, LeftWidth} {
delete(border.properties, side) if value := properties.getRaw(side); value != nil {
properties.setRaw(side, nil)
result = append(result, side)
}
} }
return true return result
} }
case ColorTag: case ColorTag:
if border.setColorProperty(ColorTag, value) { if result := setColorProperty(properties, ColorTag, value); result != nil {
for _, side := range []string{TopColor, RightColor, BottomColor, LeftColor} { for _, side := range []PropertyName{TopColor, RightColor, BottomColor, LeftColor} {
delete(border.properties, side) if value := properties.getRaw(side); value != nil {
properties.setRaw(side, nil)
result = append(result, side)
}
} }
return true return result
} }
case LeftStyle, RightStyle, TopStyle, BottomStyle: case LeftStyle, RightStyle, TopStyle, BottomStyle:
return border.setEnumProperty(tag, value, enumProperties[BorderStyle].values) return setEnumProperty(properties, tag, value, enumProperties[BorderStyle].values)
case LeftWidth, RightWidth, TopWidth, BottomWidth: case LeftWidth, RightWidth, TopWidth, BottomWidth:
return border.setSizeProperty(tag, value) return setSizeProperty(properties, tag, value)
case LeftColor, RightColor, TopColor, BottomColor: case LeftColor, RightColor, TopColor, BottomColor:
return border.setColorProperty(tag, value) return setColorProperty(properties, tag, value)
case Left, Right, Top, Bottom: case Left, Right, Top, Bottom:
switch value := value.(type) { switch value := value.(type) {
case string: case string:
if obj := ParseDataText(value); obj != nil { obj, err := ParseDataText(value)
return border.setSingleBorderObject(tag, obj) if err != nil {
ErrorLog(err.Error())
} else {
return setSingleBorderObject(tag, obj)
} }
case DataObject: case DataObject:
return border.setSingleBorderObject(tag, value) return setSingleBorderObject(tag, value)
case BorderProperty: case BorderProperty:
result := []PropertyName{}
styleTag := tag + "-" + Style styleTag := tag + "-" + Style
if style := value.Get(styleTag); value != nil { if style := value.Get(styleTag); value != nil {
border.properties[styleTag] = style properties.setRaw(styleTag, style)
result = append(result, styleTag)
} }
colorTag := tag + "-" + ColorTag colorTag := tag + "-" + ColorTag
if color := value.Get(colorTag); value != nil { if color := value.Get(colorTag); value != nil {
border.properties[colorTag] = color properties.setRaw(colorTag, color)
result = append(result, colorTag)
} }
widthTag := tag + "-" + Width widthTag := tag + "-" + Width
if width := value.Get(widthTag); value != nil { if width := value.Get(widthTag); value != nil {
border.properties[widthTag] = width properties.setRaw(widthTag, width)
result = append(result, widthTag)
} }
return true return result
case ViewBorder: case ViewBorder:
border.properties[tag+"-"+Style] = value.Style properties.setRaw(tag+"-"+Style, value.Style)
border.properties[tag+"-"+Width] = value.Width properties.setRaw(tag+"-"+Width, value.Width)
border.properties[tag+"-"+ColorTag] = value.Color properties.setRaw(tag+"-"+ColorTag, value.Color)
return true return []PropertyName{tag + "-" + Style, tag + "-" + Width, tag + "-" + ColorTag}
} }
fallthrough fallthrough
@ -509,105 +650,119 @@ func (border *borderProperty) Set(tag string, value any) bool {
ErrorLogF(`"%s" property is not compatible with the BorderProperty`, tag) ErrorLogF(`"%s" property is not compatible with the BorderProperty`, tag)
} }
return false return nil
} }
func (border *borderProperty) Get(tag string) any { func borderGet(properties Properties, tag PropertyName) any {
tag = border.normalizeTag(tag) if result := properties.getRaw(tag); result != nil {
if result, ok := border.properties[tag]; ok {
return result return result
} }
switch tag { switch tag {
case Left, Right, Top, Bottom: case Left, Right, Top, Bottom:
result := newBorderProperty(nil) result := newBorderProperty(nil)
if style, ok := border.properties[tag+"-"+Style]; ok { if style := properties.getRaw(tag + "-" + Style); style != nil {
result.Set(Style, style) result.Set(Style, style)
} else if style, ok := border.properties[Style]; ok { } else if style := properties.getRaw(Style); style != nil {
result.Set(Style, style) result.Set(Style, style)
} }
if width, ok := border.properties[tag+"-"+Width]; ok { if width := properties.getRaw(tag + "-" + Width); width != nil {
result.Set(Width, width) result.Set(Width, width)
} else if width, ok := border.properties[Width]; ok { } else if width := properties.getRaw(Width); width != nil {
result.Set(Width, width) result.Set(Width, width)
} }
if color, ok := border.properties[tag+"-"+ColorTag]; ok { if color := properties.getRaw(tag + "-" + ColorTag); color != nil {
result.Set(ColorTag, color) result.Set(ColorTag, color)
} else if color, ok := border.properties[ColorTag]; ok { } else if color := properties.getRaw(ColorTag); color != nil {
result.Set(ColorTag, color) result.Set(ColorTag, color)
} }
return result return result
case LeftStyle, RightStyle, TopStyle, BottomStyle: case LeftStyle, RightStyle, TopStyle, BottomStyle:
if style, ok := border.properties[tag]; ok { if style := properties.getRaw(tag); style != nil {
return style return style
} }
return border.properties[Style] return properties.getRaw(Style)
case LeftWidth, RightWidth, TopWidth, BottomWidth: case LeftWidth, RightWidth, TopWidth, BottomWidth:
if width, ok := border.properties[tag]; ok { if width := properties.getRaw(tag); width != nil {
return width return width
} }
return border.properties[Width] return properties.getRaw(Width)
case LeftColor, RightColor, TopColor, BottomColor: case LeftColor, RightColor, TopColor, BottomColor:
if color, ok := border.properties[tag]; ok { if color := properties.getRaw(tag); color != nil {
return color return color
} }
return border.properties[ColorTag] return properties.getRaw(ColorTag)
} }
return nil return nil
} }
func (border *borderProperty) delete(tag string) { func (border *borderProperty) deleteTag(tag PropertyName) bool {
tag = border.normalizeTag(tag)
remove := []string{} result := false
removeTags := func(tags []PropertyName) {
for _, tag := range tags {
if border.getRaw(tag) != nil {
border.setRaw(tag, nil)
result = true
}
}
}
switch tag { switch tag {
case Style: case Style:
remove = []string{Style, LeftStyle, RightStyle, TopStyle, BottomStyle} removeTags([]PropertyName{Style, LeftStyle, RightStyle, TopStyle, BottomStyle})
case Width: case Width:
remove = []string{Width, LeftWidth, RightWidth, TopWidth, BottomWidth} removeTags([]PropertyName{Width, LeftWidth, RightWidth, TopWidth, BottomWidth})
case ColorTag: case ColorTag:
remove = []string{ColorTag, LeftColor, RightColor, TopColor, BottomColor} removeTags([]PropertyName{ColorTag, LeftColor, RightColor, TopColor, BottomColor})
case Left, Right, Top, Bottom: case Left, Right, Top, Bottom:
if border.Get(Style) != nil { if border.Get(Style) != nil {
border.properties[tag+"-"+Style] = 0 border.properties[tag+"-"+Style] = 0
remove = []string{tag + "-" + ColorTag, tag + "-" + Width} result = true
removeTags([]PropertyName{tag + "-" + ColorTag, tag + "-" + Width})
} else { } else {
remove = []string{tag + "-" + Style, tag + "-" + ColorTag, tag + "-" + Width} removeTags([]PropertyName{tag + "-" + Style, tag + "-" + ColorTag, tag + "-" + Width})
} }
case LeftStyle, RightStyle, TopStyle, BottomStyle: case LeftStyle, RightStyle, TopStyle, BottomStyle:
if border.Get(Style) != nil { if border.getRaw(tag) != nil {
border.properties[tag] = 0 if border.Get(Style) != nil {
} else { border.properties[tag] = 0
remove = []string{tag} result = true
} else {
removeTags([]PropertyName{tag})
}
} }
case LeftWidth, RightWidth, TopWidth, BottomWidth: case LeftWidth, RightWidth, TopWidth, BottomWidth:
if border.Get(Width) != nil { if border.getRaw(tag) != nil {
border.properties[tag] = AutoSize() if border.Get(Width) != nil {
} else { border.properties[tag] = AutoSize()
remove = []string{tag} result = true
} else {
removeTags([]PropertyName{tag})
}
} }
case LeftColor, RightColor, TopColor, BottomColor: case LeftColor, RightColor, TopColor, BottomColor:
if border.Get(ColorTag) != nil { if border.getRaw(tag) != nil {
border.properties[tag] = 0 if border.Get(ColorTag) != nil {
} else { border.properties[tag] = 0
remove = []string{tag} result = true
} else {
removeTags([]PropertyName{tag})
}
} }
} }
for _, tag := range remove { return result
delete(border.properties, tag)
}
} }
func (border *borderProperty) ViewBorders(session Session) ViewBorders { func (border *borderProperty) ViewBorders(session Session) ViewBorders {
@ -616,7 +771,7 @@ func (border *borderProperty) ViewBorders(session Session) ViewBorders {
defWidth, _ := sizeProperty(border, Width, session) defWidth, _ := sizeProperty(border, Width, session)
defColor, _ := colorProperty(border, ColorTag, session) defColor, _ := colorProperty(border, ColorTag, session)
getBorder := func(prefix string) ViewBorder { getBorder := func(prefix PropertyName) ViewBorder {
var result ViewBorder var result ViewBorder
var ok bool var ok bool
if result.Style, ok = valueToEnum(border.getRaw(prefix+Style), BorderStyle, session, NoneLine); !ok { if result.Style, ok = valueToEnum(border.getRaw(prefix+Style), BorderStyle, session, NoneLine); !ok {
@ -645,9 +800,9 @@ func (border *borderProperty) cssStyle(builder cssBuilder, session Session) {
if borders.Top.Style == borders.Right.Style && if borders.Top.Style == borders.Right.Style &&
borders.Top.Style == borders.Left.Style && borders.Top.Style == borders.Left.Style &&
borders.Top.Style == borders.Bottom.Style { borders.Top.Style == borders.Bottom.Style {
builder.add(BorderStyle, values[borders.Top.Style]) builder.add(string(BorderStyle), values[borders.Top.Style])
} else { } else {
builder.addValues(BorderStyle, " ", values[borders.Top.Style], builder.addValues(string(BorderStyle), " ", values[borders.Top.Style],
values[borders.Right.Style], values[borders.Bottom.Style], values[borders.Left.Style]) values[borders.Right.Style], values[borders.Bottom.Style], values[borders.Left.Style])
} }
} }
@ -703,8 +858,13 @@ func (border *borderProperty) cssColorValue(session Session) string {
// ViewBorder describes parameters of a view border // ViewBorder describes parameters of a view border
type ViewBorder struct { type ViewBorder struct {
// Style of the border line
Style int Style int
// Color of the border line
Color Color Color Color
// Width of the border line
Width SizeUnit Width SizeUnit
} }
@ -726,11 +886,25 @@ func (border *ViewBorders) AllTheSame() bool {
border.Top.Width.Equal(border.Bottom.Width) border.Top.Width.Equal(border.Bottom.Width)
} }
func getBorder(style Properties, tag string) BorderProperty { func getBorderProperty(properties Properties, tag PropertyName) BorderProperty {
if value := style.Get(tag); value != nil { if value := properties.getRaw(tag); value != nil {
if border, ok := value.(BorderProperty); ok { if border, ok := value.(BorderProperty); ok {
return border return border
} }
} }
return nil return nil
} }
func setBorderPropertyElement(properties Properties, mainTag, tag PropertyName, value any) []PropertyName {
border := getBorderProperty(properties, mainTag)
if border == nil {
border = NewBorder(nil)
if border.Set(tag, value) {
properties.setRaw(mainTag, border)
return []PropertyName{mainTag, tag}
}
} else if border.Set(tag, value) {
return []PropertyName{mainTag, tag}
}
return nil
}

245
bounds.go
View File

@ -5,34 +5,60 @@ import (
"strings" "strings"
) )
// BorderProperty is the interface of a bounds property data // BorderProperty is an interface of a bounds property data
type BoundsProperty interface { type BoundsProperty interface {
Properties Properties
fmt.Stringer fmt.Stringer
stringWriter stringWriter
// Bounds returns top, right, bottom and left size of the bounds
Bounds(session Session) Bounds Bounds(session Session) Bounds
} }
type boundsPropertyData struct { type boundsPropertyData struct {
propertyList dataProperty
} }
// NewBoundsProperty creates the new BoundsProperty object // NewBoundsProperty creates the new BoundsProperty object.
//
// The following SizeUnit properties can be used: "left" (Left), "right" (Right), "top" (Top), and "bottom" (Bottom).
func NewBoundsProperty(params Params) BoundsProperty { func NewBoundsProperty(params Params) BoundsProperty {
bounds := new(boundsPropertyData) bounds := new(boundsPropertyData)
bounds.properties = map[string]any{} bounds.init()
if params != nil { if params != nil {
for _, tag := range []string{Top, Right, Bottom, Left} { for _, tag := range bounds.supportedProperties {
if value, ok := params[tag]; ok { if value, ok := params[tag]; ok && value != nil {
bounds.Set(tag, value) bounds.set(bounds, tag, value)
} }
} }
} }
return bounds return bounds
} }
func (bounds *boundsPropertyData) normalizeTag(tag string) string { // NewBounds creates the new BoundsProperty object.
tag = strings.ToLower(tag) //
// The arguments specify the boundaries in a clockwise direction: "top", "right", "bottom", and "left".
//
// If the argument is specified as int or float64, the value is considered to be in pixels.
func NewBounds[topType SizeUnit | int | float64, rightType SizeUnit | int | float64, bottomType SizeUnit | int | float64, leftType SizeUnit | int | float64](
top topType, right rightType, bottom bottomType, left leftType) BoundsProperty {
return NewBoundsProperty(Params{
Top: top,
Right: right,
Bottom: bottom,
Left: left,
})
}
func (bounds *boundsPropertyData) init() {
bounds.dataProperty.init()
bounds.normalize = normalizeBoundsTag
bounds.supportedProperties = []PropertyName{Top, Right, Bottom, Left}
}
func normalizeBoundsTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag { switch tag {
case MarginTop, PaddingTop, CellPaddingTop, case MarginTop, PaddingTop, CellPaddingTop,
"top-margin", "top-padding", "top-cell-padding": "top-margin", "top-padding", "top-cell-padding":
@ -58,55 +84,6 @@ func (bounds *boundsPropertyData) String() string {
return runStringWriter(bounds) return runStringWriter(bounds)
} }
func (bounds *boundsPropertyData) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("_{ ")
comma := false
for _, tag := range []string{Top, Right, Bottom, Left} {
if value, ok := bounds.properties[tag]; ok {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(" = ")
writePropertyValue(buffer, tag, value, indent)
comma = true
}
}
buffer.WriteString(" }")
}
func (bounds *boundsPropertyData) Remove(tag string) {
bounds.propertyList.Remove(bounds.normalizeTag(tag))
}
func (bounds *boundsPropertyData) Set(tag string, value any) bool {
if value == nil {
bounds.Remove(tag)
return true
}
tag = bounds.normalizeTag(tag)
switch tag {
case Top, Right, Bottom, Left:
return bounds.setSizeProperty(tag, value)
default:
ErrorLogF(`"%s" property is not compatible with the BoundsProperty`, tag)
}
return false
}
func (bounds *boundsPropertyData) Get(tag string) any {
tag = bounds.normalizeTag(tag)
if value, ok := bounds.properties[tag]; ok {
return value
}
return nil
}
func (bounds *boundsPropertyData) Bounds(session Session) Bounds { func (bounds *boundsPropertyData) Bounds(session Session) Bounds {
top, _ := sizeProperty(bounds, Top, session) top, _ := sizeProperty(bounds, Top, session)
right, _ := sizeProperty(bounds, Right, session) right, _ := sizeProperty(bounds, Right, session)
@ -138,7 +115,7 @@ func (bounds *Bounds) SetAll(value SizeUnit) {
bounds.Left = value bounds.Left = value
} }
func (bounds *Bounds) setFromProperties(tag, topTag, rightTag, bottomTag, leftTag string, properties Properties, session Session) { func (bounds *Bounds) setFromProperties(tag, topTag, rightTag, bottomTag, leftTag PropertyName, properties Properties, session Session) {
bounds.Top = AutoSize() bounds.Top = AutoSize()
if size, ok := sizeProperty(properties, tag, session); ok { if size, ok := sizeProperty(properties, tag, session); ok {
bounds.Top = size bounds.Top = size
@ -161,22 +138,6 @@ func (bounds *Bounds) setFromProperties(tag, topTag, rightTag, bottomTag, leftTa
} }
} }
/*
func (bounds *Bounds) allFieldsAuto() bool {
return bounds.Left.Type == Auto &&
bounds.Top.Type == Auto &&
bounds.Right.Type == Auto &&
bounds.Bottom.Type == Auto
}
func (bounds *Bounds) allFieldsZero() bool {
return (bounds.Left.Type == Auto || bounds.Left.Value == 0) &&
(bounds.Top.Type == Auto || bounds.Top.Value == 0) &&
(bounds.Right.Type == Auto || bounds.Right.Value == 0) &&
(bounds.Bottom.Type == Auto || bounds.Bottom.Value == 0)
}
*/
func (bounds *Bounds) allFieldsEqual() bool { func (bounds *Bounds) allFieldsEqual() bool {
if bounds.Left.Type == bounds.Top.Type && if bounds.Left.Type == bounds.Top.Type &&
bounds.Left.Type == bounds.Right.Type && bounds.Left.Type == bounds.Right.Type &&
@ -190,20 +151,6 @@ func (bounds *Bounds) allFieldsEqual() bool {
return false return false
} }
/*
func (bounds Bounds) writeCSSString(buffer *strings.Builder, textForAuto string) {
buffer.WriteString(bounds.Top.cssString(textForAuto))
if !bounds.allFieldsEqual() {
buffer.WriteRune(' ')
buffer.WriteString(bounds.Right.cssString(textForAuto))
buffer.WriteRune(' ')
buffer.WriteString(bounds.Bottom.cssString(textForAuto))
buffer.WriteRune(' ')
buffer.WriteString(bounds.Left.cssString(textForAuto))
}
}
*/
// String convert Bounds to string // String convert Bounds to string
func (bounds *Bounds) String() string { func (bounds *Bounds) String() string {
if bounds.allFieldsEqual() { if bounds.allFieldsEqual() {
@ -213,11 +160,11 @@ func (bounds *Bounds) String() string {
bounds.Bottom.String() + "," + bounds.Left.String() bounds.Bottom.String() + "," + bounds.Left.String()
} }
func (bounds *Bounds) cssValue(tag string, builder cssBuilder, session Session) { func (bounds *Bounds) cssValue(tag PropertyName, builder cssBuilder, session Session) {
if bounds.allFieldsEqual() { if bounds.allFieldsEqual() {
builder.add(tag, bounds.Top.cssString("0", session)) builder.add(string(tag), bounds.Top.cssString("0", session))
} else { } else {
builder.addValues(tag, " ", builder.addValues(string(tag), " ",
bounds.Top.cssString("0", session), bounds.Top.cssString("0", session),
bounds.Right.cssString("0", session), bounds.Right.cssString("0", session),
bounds.Bottom.cssString("0", session), bounds.Bottom.cssString("0", session),
@ -231,11 +178,11 @@ func (bounds *Bounds) cssString(session Session) string {
return builder.finish() return builder.finish()
} }
func (properties *propertyList) setBounds(tag string, value any) bool { func setBoundsProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !properties.setSimpleProperty(tag, value) { if !setSimpleProperty(properties, tag, value) {
switch value := value.(type) { switch value := value.(type) {
case string: case string:
if strings.Contains(value, ",") { if strings.ContainsRune(value, ',') {
values := split4Values(value) values := split4Values(value)
count := len(values) count := len(values)
switch count { switch count {
@ -244,88 +191,119 @@ func (properties *propertyList) setBounds(tag string, value any) bool {
case 4: case 4:
bounds := NewBoundsProperty(nil) bounds := NewBoundsProperty(nil)
for i, tag := range []string{Top, Right, Bottom, Left} { for i, tag := range []PropertyName{Top, Right, Bottom, Left} {
if !bounds.Set(tag, values[i]) { if !bounds.Set(tag, values[i]) {
notCompatibleType(tag, value) return nil
return false
} }
} }
properties.properties[tag] = bounds properties.setRaw(tag, bounds)
return true return []PropertyName{tag}
default: default:
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return nil
} }
} }
return properties.setSizeProperty(tag, value) return setSizeProperty(properties, tag, value)
case SizeUnit: case SizeUnit:
properties.properties[tag] = value properties.setRaw(tag, value)
case float32: case float32:
properties.properties[tag] = Px(float64(value)) properties.setRaw(tag, Px(float64(value)))
case float64: case float64:
properties.properties[tag] = Px(value) properties.setRaw(tag, Px(value))
case Bounds: case Bounds:
bounds := NewBoundsProperty(nil) bounds := NewBoundsProperty(nil)
if value.Top.Type != Auto { if value.Top.Type != Auto {
bounds.Set(Top, value.Top) bounds.setRaw(Top, value.Top)
} }
if value.Right.Type != Auto { if value.Right.Type != Auto {
bounds.Set(Right, value.Right) bounds.setRaw(Right, value.Right)
} }
if value.Bottom.Type != Auto { if value.Bottom.Type != Auto {
bounds.Set(Bottom, value.Bottom) bounds.setRaw(Bottom, value.Bottom)
} }
if value.Left.Type != Auto { if value.Left.Type != Auto {
bounds.Set(Left, value.Left) bounds.setRaw(Left, value.Left)
} }
properties.properties[tag] = bounds properties.setRaw(tag, bounds)
case BoundsProperty: case BoundsProperty:
properties.properties[tag] = value properties.setRaw(tag, value)
case DataObject: case DataObject:
bounds := NewBoundsProperty(nil) bounds := NewBoundsProperty(nil)
for _, tag := range []string{Top, Right, Bottom, Left} { for _, tag := range []PropertyName{Top, Right, Bottom, Left} {
if text, ok := value.PropertyValue(tag); ok { if text, ok := value.PropertyValue(string(tag)); ok {
if !bounds.Set(tag, text) { if !bounds.Set(tag, text) {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return nil
} }
} }
} }
properties.properties[tag] = bounds properties.setRaw(tag, bounds)
default: default:
if n, ok := isInt(value); ok { if n, ok := isInt(value); ok {
properties.properties[tag] = Px(float64(n)) properties.setRaw(tag, Px(float64(n)))
} else { } else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return nil
} }
} }
} }
return true return []PropertyName{tag}
} }
func (properties *propertyList) boundsProperty(tag string) BoundsProperty { func removeBoundsPropertySide(properties Properties, mainTag, sideTag PropertyName) []PropertyName {
if value, ok := properties.properties[tag]; ok { if bounds := getBoundsProperty(properties, mainTag); bounds != nil {
if bounds.getRaw(sideTag) != nil {
bounds.Remove(sideTag)
if bounds.IsEmpty() {
bounds = nil
}
properties.setRaw(mainTag, bounds)
return []PropertyName{mainTag, sideTag}
}
}
return []PropertyName{}
}
func setBoundsPropertySide(properties Properties, mainTag, sideTag PropertyName, value any) []PropertyName {
if value == nil {
return removeBoundsPropertySide(properties, mainTag, sideTag)
}
bounds := getBoundsProperty(properties, mainTag)
if bounds == nil {
bounds = NewBoundsProperty(nil)
}
if bounds.Set(sideTag, value) {
properties.setRaw(mainTag, bounds)
return []PropertyName{mainTag, sideTag}
}
notCompatibleType(sideTag, value)
return nil
}
func getBoundsProperty(properties Properties, tag PropertyName) BoundsProperty {
if value := properties.getRaw(tag); value != nil {
switch value := value.(type) { switch value := value.(type) {
case string: case string:
bounds := NewBoundsProperty(nil) bounds := NewBoundsProperty(nil)
for _, t := range []string{Top, Right, Bottom, Left} { for _, t := range []PropertyName{Top, Right, Bottom, Left} {
bounds.Set(t, value) bounds.Set(t, value)
} }
return bounds return bounds
case SizeUnit: case SizeUnit:
bounds := NewBoundsProperty(nil) bounds := NewBoundsProperty(nil)
for _, t := range []string{Top, Right, Bottom, Left} { for _, t := range []PropertyName{Top, Right, Bottom, Left} {
bounds.Set(t, value) bounds.Set(t, value)
} }
return bounds return bounds
@ -342,29 +320,10 @@ func (properties *propertyList) boundsProperty(tag string) BoundsProperty {
} }
} }
return NewBoundsProperty(nil) return nil
} }
func (properties *propertyList) removeBoundsSide(mainTag, sideTag string) { func getBounds(properties Properties, tag PropertyName, session Session) (Bounds, bool) {
bounds := properties.boundsProperty(mainTag)
if bounds.Get(sideTag) != nil {
bounds.Remove(sideTag)
properties.properties[mainTag] = bounds
}
}
func (properties *propertyList) setBoundsSide(mainTag, sideTag string, value any) bool {
bounds := properties.boundsProperty(mainTag)
if bounds.Set(sideTag, value) {
properties.properties[mainTag] = bounds
return true
}
notCompatibleType(sideTag, value)
return false
}
func boundsProperty(properties Properties, tag string, session Session) (Bounds, bool) {
if value := properties.Get(tag); value != nil { if value := properties.Get(tag); value != nil {
switch value := value.(type) { switch value := value.(type) {
case string: case string:

View File

@ -1,36 +1,86 @@
package rui package rui
// Button - button view import "strings"
// Button represent a Button view
type Button interface { type Button interface {
CustomView ListLayout
} }
type buttonData struct { type buttonData struct {
CustomViewData listLayoutData
} }
// NewButton create new Button object and return it // NewButton create new Button object and return it
func NewButton(session Session, params Params) Button { func NewButton(session Session, params Params) Button {
button := new(buttonData) button := new(buttonData)
InitCustomView(button, "Button", session, params) button.init(session)
setInitParams(button, params)
return button return button
} }
func newButton(session Session) View { func newButton(session Session) View {
return NewButton(session, nil) return new(buttonData)
} }
func (button *buttonData) CreateSuperView(session Session) View { func (button *buttonData) init(session Session) {
return NewListLayout(session, Params{ button.listLayoutData.init(session)
Semantics: ButtonSemantics, button.tag = "Button"
Style: "ruiButton", button.systemClass = "ruiButton"
StyleDisabled: "ruiDisabledButton", button.setRaw(Style, "ruiEnabledButton")
HorizontalAlign: CenterAlign, button.setRaw(StyleDisabled, "ruiDisabledButton")
VerticalAlign: CenterAlign, button.setRaw(Semantics, ButtonSemantics)
Orientation: StartToEndOrientation, button.setRaw(TabIndex, 0)
})
} }
func (button *buttonData) Focusable() bool { func (button *buttonData) Focusable() bool {
return true return true
} }
func (button *buttonData) htmlSubviews(self View, buffer *strings.Builder) {
if button.views != nil {
for _, view := range button.views {
view.addToCSSStyle(map[string]string{`flex`: `0 0 auto`})
viewHTML(view, buffer, "")
}
}
}
// GetButtonVerticalAlign returns the vertical align of a Button subview:
// TopAlign (0), BottomAlign (1), CenterAlign (2), or StretchAlign (3)
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetButtonVerticalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, VerticalAlign, CenterAlign, false)
}
// GetButtonHorizontalAlign returns the vertical align of a Button subview:
// LeftAlign (0), RightAlign (1), CenterAlign (2), or StretchAlign (3)
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetButtonHorizontalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, HorizontalAlign, CenterAlign, false)
}
// GetButtonOrientation returns the orientation of a Button subview:
// TopDownOrientation (0), StartToEndOrientation (1), BottomUpOrientation (2), or EndToStartOrientation (3)
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetButtonOrientation(view View, subviewID ...string) int {
if view = getSubview(view, subviewID); view != nil {
if orientation, ok := valueToOrientation(view.Get(Orientation), view.Session()); ok {
return orientation
}
if value := valueFromStyle(view, Orientation); value != nil {
if orientation, ok := valueToOrientation(value, view.Session()); ok {
return orientation
}
}
}
return StartToEndOrientation
}

831
canvas.go

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,25 @@
package rui package rui
import "strings" import "reflect"
// DrawFunction is the constant for the "draw-function" property tag. // DrawFunction is the constant for "draw-function" property tag.
// The "draw-function" property sets the draw function of CanvasView. //
// The function should have the following format: func(Canvas) // Used by `CanvasView`.
const DrawFunction = "draw-function" // Property sets the draw function of `CanvasView`.
//
// Supported types: `func(Canvas)`.
const DrawFunction PropertyName = "draw-function"
// CanvasView interface of a custom draw view // CanvasView interface of a custom draw view
type CanvasView interface { type CanvasView interface {
View View
// Redraw forces CanvasView to redraw its content
Redraw() Redraw()
} }
type canvasViewData struct { type canvasViewData struct {
viewData viewData
drawer func(Canvas)
} }
// NewCanvasView creates the new custom draw view // NewCanvasView creates the new custom draw view
@ -27,21 +31,21 @@ func NewCanvasView(session Session, params Params) CanvasView {
} }
func newCanvasView(session Session) View { func newCanvasView(session Session) View {
return NewCanvasView(session, nil) return new(canvasViewData)
} }
// Init initialize fields of ViewsContainer by default values // Init initialize fields of ViewsContainer by default values
func (canvasView *canvasViewData) init(session Session) { func (canvasView *canvasViewData) init(session Session) {
canvasView.viewData.init(session) canvasView.viewData.init(session)
canvasView.tag = "CanvasView" canvasView.tag = "CanvasView"
canvasView.normalize = normalizeCanvasViewTag
canvasView.set = canvasView.setFunc
canvasView.remove = canvasView.removeFunc
canvasView.changed = canvasView.propertyChanged
} }
func (canvasView *canvasViewData) String() string { func normalizeCanvasViewTag(tag PropertyName) PropertyName {
return getViewString(canvasView) tag = defaultNormalize(tag)
}
func (canvasView *canvasViewData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag { switch tag {
case "draw-func": case "draw-func":
tag = DrawFunction tag = DrawFunction
@ -49,51 +53,44 @@ func (canvasView *canvasViewData) normalizeTag(tag string) string {
return tag return tag
} }
func (canvasView *canvasViewData) Remove(tag string) { func (canvasView *canvasViewData) removeFunc(tag PropertyName) []PropertyName {
canvasView.remove(canvasView.normalizeTag(tag))
}
func (canvasView *canvasViewData) remove(tag string) {
if tag == DrawFunction { if tag == DrawFunction {
canvasView.drawer = nil if canvasView.getRaw(DrawFunction) != nil {
canvasView.Redraw() canvasView.setRaw(DrawFunction, nil)
canvasView.propertyChangedEvent(tag) //canvasView.Redraw()
} else { return []PropertyName{DrawFunction}
canvasView.viewData.remove(tag)
}
}
func (canvasView *canvasViewData) Set(tag string, value any) bool {
return canvasView.set(canvasView.normalizeTag(tag), value)
}
func (canvasView *canvasViewData) set(tag string, value any) bool {
if tag == DrawFunction {
if value == nil {
canvasView.drawer = nil
} else if fn, ok := value.(func(Canvas)); ok {
canvasView.drawer = fn
} else {
notCompatibleType(tag, value)
return false
} }
canvasView.Redraw() return []PropertyName{}
canvasView.propertyChangedEvent(tag)
return true
} }
return canvasView.viewData.set(tag, value) return canvasView.viewData.removeFunc(tag)
} }
func (canvasView *canvasViewData) Get(tag string) any { func (canvasView *canvasViewData) setFunc(tag PropertyName, value any) []PropertyName {
return canvasView.get(canvasView.normalizeTag(tag))
}
func (canvasView *canvasViewData) get(tag string) any {
if tag == DrawFunction { if tag == DrawFunction {
return canvasView.drawer switch value := value.(type) {
case func(Canvas):
canvasView.setRaw(DrawFunction, value)
case string:
canvasView.setRaw(DrawFunction, value)
default:
notCompatibleType(tag, value)
return nil
}
return []PropertyName{DrawFunction}
}
return canvasView.viewData.setFunc(tag, value)
}
func (canvasView *canvasViewData) propertyChanged(tag PropertyName) {
if tag == DrawFunction {
canvasView.Redraw()
} else {
canvasView.viewData.propertyChanged(tag)
} }
return canvasView.viewData.get(tag)
} }
func (canvasView *canvasViewData) htmlTag() string { func (canvasView *canvasViewData) htmlTag() string {
@ -101,14 +98,36 @@ func (canvasView *canvasViewData) htmlTag() string {
} }
func (canvasView *canvasViewData) Redraw() { func (canvasView *canvasViewData) Redraw() {
if canvasView.drawer != nil { canvas := newCanvas(canvasView)
canvas := newCanvas(canvasView) canvas.ClearRect(0, 0, canvasView.frame.Width, canvasView.frame.Height)
canvas.ClearRect(0, 0, canvasView.frame.Width, canvasView.frame.Height) if value := canvasView.getRaw(DrawFunction); value != nil {
if canvasView.drawer != nil { switch drawer := value.(type) {
canvasView.drawer(canvas) case func(Canvas):
drawer(canvas)
case string:
bind := canvasView.binding()
if bind == nil {
ErrorLogF(`There is no a binding object for call "%s"`, drawer)
break
}
val := reflect.ValueOf(bind)
method := val.MethodByName(drawer)
if !method.IsValid() {
ErrorLogF(`The "%s" method is not valid`, drawer)
break
}
methodType := method.Type()
if methodType.NumIn() == 1 && equalType(methodType.In(0), reflect.TypeOf(canvas)) {
method.Call([]reflect.Value{reflect.ValueOf(canvas)})
} else {
ErrorLogF(`Unsupported prototype of "%s" method`, drawer)
}
} }
canvasView.session.runScript(canvas.finishDraw())
} }
canvas.finishDraw()
} }
func (canvasView *canvasViewData) onResize(self View, x, y, width, height float64) { func (canvasView *canvasViewData) onResize(self View, x, y, width, height float64) {

View File

@ -1,199 +1,163 @@
package rui package rui
import ( import (
"fmt"
"strings" "strings"
) )
// CheckboxChangedEvent is the constant for "checkbox-event" property tag. // CheckboxChangedEvent is the constant for "checkbox-event" property tag.
// The "checkbox-event" event occurs when the checkbox becomes checked/unchecked. //
// The main listener format: func(Checkbox, bool), where the second argument is the checkbox state. // Used by `Checkbox`.
const CheckboxChangedEvent = "checkbox-event" // Event occurs when the checkbox becomes checked/unchecked.
//
// General listener format:
//
// func(checkbox rui.Checkbox, checked bool)
//
// where:
// - checkbox - Interface of a checkbox which generated this event,
// - checked - Checkbox state.
//
// Allowed listener formats:
//
// func(checkbox rui.Checkbox)
// func(checked bool)
// func()
const CheckboxChangedEvent PropertyName = "checkbox-event"
// Checkbox - checkbox view // Checkbox represent a Checkbox view
type Checkbox interface { type Checkbox interface {
ViewsContainer ViewsContainer
} }
type checkboxData struct { type checkboxData struct {
viewsContainerData viewsContainerData
checkedListeners []func(Checkbox, bool)
} }
// NewCheckbox create new Checkbox object and return it // NewCheckbox create new Checkbox object and return it
func NewCheckbox(session Session, params Params) Checkbox { func NewCheckbox(session Session, params Params) Checkbox {
view := new(checkboxData) view := new(checkboxData)
view.init(session) view.init(session)
setInitParams(view, Params{
ClickEvent: checkboxClickListener,
KeyDownEvent: checkboxKeyListener,
})
setInitParams(view, params) setInitParams(view, params)
return view return view
} }
func newCheckbox(session Session) View { func newCheckbox(session Session) View {
return NewCheckbox(session, nil) return new(checkboxData)
} }
func (button *checkboxData) init(session Session) { func (button *checkboxData) init(session Session) {
button.viewsContainerData.init(session) button.viewsContainerData.init(session)
button.tag = "Checkbox" button.tag = "Checkbox"
button.systemClass = "ruiGridLayout ruiCheckbox" button.systemClass = "ruiGridLayout ruiCheckbox"
button.checkedListeners = []func(Checkbox, bool){} button.get = button.getFunc
} button.set = button.setFunc
button.remove = button.removeFunc
button.changed = button.propertyChanged
func (button *checkboxData) String() string { button.setRaw(ClickEvent, []oneArgListener[View, MouseEvent]{newOneArgListenerVE(checkboxClickListener)})
return getViewString(button) button.setRaw(KeyDownEvent, []oneArgListener[View, KeyEvent]{newOneArgListenerVE(checkboxKeyListener)})
} }
func (button *checkboxData) Focusable() bool { func (button *checkboxData) Focusable() bool {
return true return true
} }
func (button *checkboxData) Get(tag string) any { func (button *checkboxData) propertyChanged(tag PropertyName) {
switch strings.ToLower(tag) {
case CheckboxChangedEvent:
return button.checkedListeners
}
return button.viewsContainerData.Get(tag)
}
func (button *checkboxData) Set(tag string, value any) bool {
return button.set(tag, value)
}
func (button *checkboxData) set(tag string, value any) bool {
switch tag { switch tag {
case CheckboxChangedEvent:
if !button.setChangedListener(value) {
notCompatibleType(tag, value)
return false
}
case Checked: case Checked:
oldChecked := button.checked() session := button.Session()
if !button.setBoolProperty(Checked, value) { checked := IsCheckboxChecked(button)
return false if listeners := getOneArgEventListeners[Checkbox, bool](button, nil, CheckboxChangedEvent); len(listeners) > 0 {
} for _, listener := range listeners {
if button.created { listener.Run(button, checked)
checked := button.checked()
if checked != oldChecked {
button.changedCheckboxState(checked)
} }
} }
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
checkboxHtml(button, buffer, checked)
session.updateInnerHTML(button.htmlID()+"checkbox", buffer.String())
case CheckboxHorizontalAlign, CheckboxVerticalAlign: case CheckboxHorizontalAlign, CheckboxVerticalAlign:
if !button.setEnumProperty(tag, value, enumProperties[tag].values) { htmlID := button.htmlID()
return false session := button.Session()
} updateCSSStyle(htmlID, session)
if button.created { updateInnerHTML(htmlID, session)
htmlID := button.htmlID()
updateCSSStyle(htmlID, button.session)
updateInnerHTML(htmlID, button.session)
}
case VerticalAlign: case VerticalAlign:
if !button.setEnumProperty(tag, value, enumProperties[tag].values) { button.Session().updateCSSProperty(button.htmlID()+"content", "align-items", checkboxVerticalAlignCSS(button))
return false
}
if button.created {
updateCSSProperty(button.htmlID()+"content", "align-items", button.cssVerticalAlign(), button.session)
}
case HorizontalAlign: case HorizontalAlign:
if !button.setEnumProperty(tag, value, enumProperties[tag].values) { button.Session().updateCSSProperty(button.htmlID()+"content", "justify-items", checkboxHorizontalAlignCSS(button))
return false
}
if button.created {
updateCSSProperty(button.htmlID()+"content", "justify-items", button.cssHorizontalAlign(), button.session)
}
case CellVerticalAlign, CellHorizontalAlign, CellWidth, CellHeight: case AccentColor:
return false updateInnerHTML(button.htmlID(), button.Session())
default: default:
return button.viewsContainerData.set(tag, value) button.viewsContainerData.propertyChanged(tag)
} }
button.propertyChangedEvent(tag)
return true
} }
func (button *checkboxData) Remove(tag string) { func (button *checkboxData) getFunc(tag PropertyName) any {
button.remove(strings.ToLower(tag)) switch tag {
case CheckboxChangedEvent:
if listeners := getOneArgEventRawListeners[Checkbox, bool](button, nil, tag); len(listeners) > 0 {
return listeners
}
return nil
}
return button.viewData.getFunc(tag)
} }
func (button *checkboxData) remove(tag string) { func (button *checkboxData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag { switch tag {
case ClickEvent: case ClickEvent:
if !button.viewsContainerData.set(ClickEvent, checkboxClickListener) { if listeners, ok := valueToOneArgEventListeners[View, MouseEvent](value); ok && listeners != nil {
delete(button.properties, tag) listeners = append(listeners, newOneArgListenerVE(checkboxClickListener))
button.setRaw(tag, listeners)
return []PropertyName{tag}
} }
return nil
case KeyDownEvent: case KeyDownEvent:
if !button.viewsContainerData.set(KeyDownEvent, checkboxKeyListener) { if listeners, ok := valueToOneArgEventListeners[View, KeyEvent](value); ok && listeners != nil {
delete(button.properties, tag) listeners = append(listeners, newOneArgListenerVE(checkboxKeyListener))
button.setRaw(tag, listeners)
return []PropertyName{tag}
} }
return nil
case CheckboxChangedEvent: case CheckboxChangedEvent:
if len(button.checkedListeners) > 0 { return setOneArgEventListener[Checkbox, bool](button, tag, value)
button.checkedListeners = []func(Checkbox, bool){}
}
case Checked: case Checked:
oldChecked := button.checked() return setBoolProperty(button, Checked, value)
delete(button.properties, tag)
if button.created && oldChecked {
button.changedCheckboxState(false)
}
case CheckboxHorizontalAlign, CheckboxVerticalAlign: case CellVerticalAlign, CellHorizontalAlign, CellWidth, CellHeight:
delete(button.properties, tag) ErrorLogF(`"%s" property is not compatible with the BoundsProperty`, string(tag))
if button.created { return nil
htmlID := button.htmlID()
updateCSSStyle(htmlID, button.session)
updateInnerHTML(htmlID, button.session)
}
case VerticalAlign:
delete(button.properties, tag)
if button.created {
updateCSSProperty(button.htmlID()+"content", "align-items", button.cssVerticalAlign(), button.session)
}
case HorizontalAlign:
delete(button.properties, tag)
if button.created {
updateCSSProperty(button.htmlID()+"content", "justify-items", button.cssHorizontalAlign(), button.session)
}
default:
button.viewsContainerData.remove(tag)
return
}
button.propertyChangedEvent(tag)
}
func (button *checkboxData) checked() bool {
checked, _ := boolProperty(button, Checked, button.Session())
return checked
}
func (button *checkboxData) changedCheckboxState(state bool) {
for _, listener := range button.checkedListeners {
listener(button, state)
} }
buffer := allocStringBuilder() return button.viewsContainerData.setFunc(tag, value)
defer freeStringBuilder(buffer)
button.htmlCheckbox(buffer, state)
button.Session().runScript(fmt.Sprintf(`updateInnerHTML('%v', '%v');`, button.htmlID()+"checkbox", buffer.String()))
} }
func checkboxClickListener(view View) { func (button *checkboxData) removeFunc(tag PropertyName) []PropertyName {
switch tag {
case ClickEvent:
button.setRaw(ClickEvent, []oneArgListener[View, MouseEvent]{newOneArgListenerVE(checkboxClickListener)})
return []PropertyName{ClickEvent}
case KeyDownEvent:
button.setRaw(KeyDownEvent, []oneArgListener[View, KeyEvent]{newOneArgListenerVE(checkboxKeyListener)})
return []PropertyName{ClickEvent}
}
return button.viewsContainerData.removeFunc(tag)
}
func checkboxClickListener(view View, _ MouseEvent) {
view.Set(Checked, !IsCheckboxChecked(view)) view.Set(Checked, !IsCheckboxChecked(view))
BlurView(view) BlurView(view)
} }
@ -205,17 +169,6 @@ func checkboxKeyListener(view View, event KeyEvent) {
} }
} }
func (button *checkboxData) setChangedListener(value any) bool {
listeners, ok := valueToEventListeners[Checkbox, bool](value)
if !ok {
return false
} else if listeners == nil {
listeners = []func(Checkbox, bool){}
}
button.checkedListeners = listeners
return true
}
func (button *checkboxData) cssStyle(self View, builder cssBuilder) { func (button *checkboxData) cssStyle(self View, builder cssBuilder) {
session := button.Session() session := button.Session()
vAlign := GetCheckboxVerticalAlign(button) vAlign := GetCheckboxVerticalAlign(button)
@ -242,10 +195,11 @@ func (button *checkboxData) cssStyle(self View, builder cssBuilder) {
builder.add("align-items", "stretch") builder.add("align-items", "stretch")
builder.add("justify-items", "stretch") builder.add("justify-items", "stretch")
button.viewsContainerData.cssStyle(self, builder) button.viewData.cssStyle(self, builder)
} }
func (button *checkboxData) htmlCheckbox(buffer *strings.Builder, checked bool) (int, int) { func checkboxHtml(button View, buffer *strings.Builder, checked bool) (int, int) {
//func (button *checkboxData) htmlCheckbox(buffer *strings.Builder, checked bool) (int, int) {
vAlign := GetCheckboxVerticalAlign(button) vAlign := GetCheckboxVerticalAlign(button)
hAlign := GetCheckboxHorizontalAlign(button) hAlign := GetCheckboxHorizontalAlign(button)
@ -279,10 +233,16 @@ func (button *checkboxData) htmlCheckbox(buffer *strings.Builder, checked bool)
} }
buffer.WriteString(`">`) buffer.WriteString(`">`)
accentColor := Color(0)
if color := GetAccentColor(button, ""); color != 0 {
accentColor = color
}
if checked { if checked {
buffer.WriteString(button.Session().checkboxOnImage()) buffer.WriteString(button.Session().checkboxOnImage(accentColor))
} else { } else {
buffer.WriteString(button.Session().checkboxOffImage()) buffer.WriteString(button.Session().checkboxOffImage(accentColor))
} }
buffer.WriteString(`</div>`) buffer.WriteString(`</div>`)
@ -291,7 +251,7 @@ func (button *checkboxData) htmlCheckbox(buffer *strings.Builder, checked bool)
func (button *checkboxData) htmlSubviews(self View, buffer *strings.Builder) { func (button *checkboxData) htmlSubviews(self View, buffer *strings.Builder) {
vCheckboxAlign, hCheckboxAlign := button.htmlCheckbox(buffer, IsCheckboxChecked(button)) vCheckboxAlign, hCheckboxAlign := checkboxHtml(button, buffer, IsCheckboxChecked(button))
buffer.WriteString(`<div id="`) buffer.WriteString(`<div id="`)
buffer.WriteString(button.htmlID()) buffer.WriteString(button.htmlID())
@ -309,11 +269,11 @@ func (button *checkboxData) htmlSubviews(self View, buffer *strings.Builder) {
} }
buffer.WriteString(" align-items: ") buffer.WriteString(" align-items: ")
buffer.WriteString(button.cssVerticalAlign()) buffer.WriteString(checkboxVerticalAlignCSS(button))
buffer.WriteRune(';') buffer.WriteRune(';')
buffer.WriteString(" justify-items: ") buffer.WriteString(" justify-items: ")
buffer.WriteString(button.cssHorizontalAlign()) buffer.WriteString(checkboxHorizontalAlignCSS(button))
buffer.WriteRune(';') buffer.WriteRune(';')
buffer.WriteString(`">`) buffer.WriteString(`">`)
@ -321,8 +281,8 @@ func (button *checkboxData) htmlSubviews(self View, buffer *strings.Builder) {
buffer.WriteString(`</div>`) buffer.WriteString(`</div>`)
} }
func (button *checkboxData) cssHorizontalAlign() string { func checkboxHorizontalAlignCSS(view View) string {
align := GetHorizontalAlign(button) align := GetHorizontalAlign(view)
values := enumProperties[CellHorizontalAlign].cssValues values := enumProperties[CellHorizontalAlign].cssValues
if align >= 0 && align < len(values) { if align >= 0 && align < len(values) {
return values[align] return values[align]
@ -330,8 +290,8 @@ func (button *checkboxData) cssHorizontalAlign() string {
return values[0] return values[0]
} }
func (button *checkboxData) cssVerticalAlign() string { func checkboxVerticalAlignCSS(view View) string {
align := GetVerticalAlign(button) align := GetVerticalAlign(view)
values := enumProperties[CellVerticalAlign].cssValues values := enumProperties[CellVerticalAlign].cssValues
if align >= 0 && align < len(values) { if align >= 0 && align < len(values) {
return values[align] return values[align]
@ -340,19 +300,41 @@ func (button *checkboxData) cssVerticalAlign() string {
} }
// IsCheckboxChecked returns true if the Checkbox is checked, false otherwise. // IsCheckboxChecked returns true if the Checkbox is checked, false otherwise.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func IsCheckboxChecked(view View, subviewID ...string) bool { func IsCheckboxChecked(view View, subviewID ...string) bool {
return boolStyledProperty(view, subviewID, Checked, false) return boolStyledProperty(view, subviewID, Checked, false)
} }
// GetCheckboxVerticalAlign return the vertical align of a Checkbox subview: TopAlign (0), BottomAlign (1), CenterAlign (2) // GetCheckboxVerticalAlign return the vertical align of a Checkbox subview: TopAlign (0), BottomAlign (1), CenterAlign (2)
// If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetCheckboxVerticalAlign(view View, subviewID ...string) int { func GetCheckboxVerticalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, CheckboxVerticalAlign, LeftAlign, false) return enumStyledProperty(view, subviewID, CheckboxVerticalAlign, LeftAlign, false)
} }
// GetCheckboxHorizontalAlign return the vertical align of a Checkbox subview: LeftAlign (0), RightAlign (1), CenterAlign (2) // GetCheckboxHorizontalAlign return the vertical align of a Checkbox subview: LeftAlign (0), RightAlign (1), CenterAlign (2)
// If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetCheckboxHorizontalAlign(view View, subviewID ...string) int { func GetCheckboxHorizontalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, CheckboxHorizontalAlign, TopAlign, false) return enumStyledProperty(view, subviewID, CheckboxHorizontalAlign, TopAlign, false)
} }
// GetCheckboxChangedListeners returns the CheckboxChangedListener list of an Checkbox subview.
// If there are no listeners then the empty list is returned.
//
// Result elements can be of the following types:
// - func(Checkbox, bool),
// - func(Checkbox),
// - func(bool),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetCheckboxChangedListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[Checkbox, bool](view, subviewID, CheckboxChangedEvent)
}

704
clipShape.go Normal file
View File

@ -0,0 +1,704 @@
package rui
import (
"fmt"
"strings"
)
type ClipShape string
const (
InsetClip ClipShape = "inset"
CircleClip ClipShape = "circle"
EllipseClip ClipShape = "ellipse"
PolygonClip ClipShape = "polygon"
)
// ClipShapeProperty defines a View clipping area
type ClipShapeProperty interface {
Properties
fmt.Stringer
stringWriter
// Shape returns the clip shape type
Shape() ClipShape
cssStyle(session Session) string
valid(session Session) bool
}
type insetClipData struct {
dataProperty
}
type ellipseClipData struct {
dataProperty
}
type circleClipData struct {
dataProperty
}
type polygonClipData struct {
dataProperty
}
// NewClipShapeProperty creates ClipShapeProperty.
//
// The following properties can be used for shapes:
//
// InsetClip:
// - "top" (Top) - offset (SizeUnit) from the top border of a View;
// - "right" (Right) - offset (SizeUnit) from the right border of a View;
// - "bottom" (Bottom) - offset (SizeUnit) from the bottom border of a View;
// - "left" (Left) - offset (SizeUnit) from the left border of a View;
// - "radius" (Radius) - corner radius (RadiusProperty).
//
// CircleClip:
// - "x" (X) - x-axis position (SizeUnit) of the circle clip center;
// - "y" (Y) - y-axis position (SizeUnit) of the circle clip center;
// - "radius" (Radius) - radius (SizeUnit) of the circle clip center.
//
// EllipseClip:
// - "x" (X) - x-axis position (SizeUnit) of the ellipse clip center;
// - "y" (Y) - y-axis position (SizeUnit) of the ellipse clip center;
// - "radius-x" (RadiusX) - x-axis radius (SizeUnit) of the ellipse clip center;
// - "radius-y" (RadiusY) - y-axis radius (SizeUnit) of the ellipse clip center.
//
// PolygonClip:
// - "points" (Points) - an array ([]SizeUnit) of corner points of the polygon in the following order: x1, y1, x2, y2, ….
//
// The function will return nil if no properties are specified, unsupported properties are specified, or at least one property has an invalid value.
func NewClipShapeProperty(shape ClipShape, params Params) ClipShapeProperty {
if len(params) == 0 {
ErrorLog("No ClipShapeProperty params")
return nil
}
var result ClipShapeProperty
switch shape {
case InsetClip:
clip := new(insetClipData)
clip.init()
result = clip
case CircleClip:
clip := new(circleClipData)
clip.init()
result = clip
case EllipseClip:
clip := new(ellipseClipData)
clip.init()
result = clip
case PolygonClip:
clip := new(polygonClipData)
clip.init()
result = clip
default:
ErrorLog("Unknown ClipShape: " + string(shape))
return nil
}
for tag, value := range params {
if !result.Set(tag, value) {
return nil
}
}
return result
}
// NewInsetClip creates a rectangle View clipping area.
// - top - offset from the top border of a View;
// - right - offset from the right border of a View;
// - bottom - offset from the bottom border of a View;
// - left - offset from the left border of a View;
// - radius - corner radius, pass nil if you don't need to round corners
func NewInsetClip(top, right, bottom, left SizeUnit, radius RadiusProperty) ClipShapeProperty {
clip := new(insetClipData)
clip.init()
clip.setRaw(Top, top)
clip.setRaw(Right, right)
clip.setRaw(Bottom, bottom)
clip.setRaw(Left, left)
if radius != nil {
clip.setRaw(Radius, radius)
}
return clip
}
// NewCircleClip creates a circle View clipping area.
// - x - x-axis position of the circle clip center;
// - y - y-axis position of the circle clip center;
// - radius - radius of the circle clip center.
func NewCircleClip(x, y, radius SizeUnit) ClipShapeProperty {
clip := new(circleClipData)
clip.init()
clip.setRaw(X, x)
clip.setRaw(Y, y)
clip.setRaw(Radius, radius)
return clip
}
// NewEllipseClip creates a ellipse View clipping area.
// - x - x-axis position of the ellipse clip center;
// - y - y-axis position of the ellipse clip center;
// - rx - x-axis radius of the ellipse clip center;
// - ry - y-axis radius of the ellipse clip center.
func NewEllipseClip(x, y, rx, ry SizeUnit) ClipShapeProperty {
clip := new(ellipseClipData)
clip.init()
clip.setRaw(X, x)
clip.setRaw(Y, y)
clip.setRaw(RadiusX, rx)
clip.setRaw(RadiusY, ry)
return clip
}
// NewPolygonClip creates a polygon View clipping area.
// - points - an array of corner points of the polygon in the following order: x1, y1, x2, y2, …
//
// The elements of the function argument can be or text constants,
// or the text representation of SizeUnit, or elements of SizeUnit type.
func NewPolygonClip(points []any) ClipShapeProperty {
clip := new(polygonClipData)
clip.init()
if polygonClipDataSet(clip, Points, points) != nil {
return clip
}
return nil
}
// NewPolygonPointsClip creates a polygon View clipping area.
// - points - an array of corner points of the polygon in the following order: x1, y1, x2, y2, …
func NewPolygonPointsClip(points []SizeUnit) ClipShapeProperty {
clip := new(polygonClipData)
clip.init()
if polygonClipDataSet(clip, Points, points) != nil {
return clip
}
return nil
}
func (clip *insetClipData) init() {
clip.dataProperty.init()
clip.set = insetClipDataSet
clip.supportedProperties = []PropertyName{
Top, Right, Bottom, Left, Radius,
RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY,
RadiusTopRight, RadiusTopRightX, RadiusTopRightY,
RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY,
RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY,
}
}
func (clip *insetClipData) Shape() ClipShape {
return InsetClip
}
func insetClipDataSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case Top, Right, Bottom, Left:
return setSizeProperty(properties, tag, value)
case Radius:
return setRadiusProperty(properties, value)
case RadiusX, RadiusY, RadiusTopLeft, RadiusTopLeftX, RadiusTopLeftY,
RadiusTopRight, RadiusTopRightX, RadiusTopRightY,
RadiusBottomLeft, RadiusBottomLeftX, RadiusBottomLeftY,
RadiusBottomRight, RadiusBottomRightX, RadiusBottomRightY:
if setRadiusPropertyElement(properties, tag, value) {
return []PropertyName{tag, Radius}
}
return nil
}
ErrorLogF(`"%s" property is not supported by the inset clip shape`, tag)
return nil
}
func (clip *insetClipData) String() string {
return runStringWriter(clip)
}
func (clip *insetClipData) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("inset { ")
comma := false
for _, tag := range []PropertyName{Top, Right, Bottom, Left, Radius} {
if value, ok := clip.properties[tag]; ok {
text := propertyValueToString(tag, value, indent)
if text != "" {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
buffer.WriteString(text)
comma = true
}
}
}
buffer.WriteString(" }")
}
func (clip *insetClipData) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
leadText := "inset("
for _, tag := range []PropertyName{Top, Right, Bottom, Left} {
value, _ := sizeProperty(clip, tag, session)
buffer.WriteString(leadText)
buffer.WriteString(value.cssString("0px", session))
leadText = " "
}
if radius := getRadiusProperty(clip); radius != nil {
buffer.WriteString(" round ")
buffer.WriteString(radius.BoxRadius(session).cssString(session))
}
buffer.WriteRune(')')
return buffer.String()
}
func (clip *insetClipData) valid(session Session) bool {
for _, tag := range []PropertyName{Top, Right, Bottom, Left, Radius, RadiusX, RadiusY} {
if value, ok := sizeProperty(clip, tag, session); ok && value.Type != Auto && value.Value != 0 {
return true
}
}
return false
}
func (clip *circleClipData) init() {
clip.dataProperty.init()
clip.set = circleClipDataSet
clip.supportedProperties = []PropertyName{X, Y, Radius}
}
func (clip *circleClipData) Shape() ClipShape {
return CircleClip
}
func circleClipDataSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case X, Y, Radius:
return setSizeProperty(properties, tag, value)
}
ErrorLogF(`"%s" property is not supported by the circle clip shape`, tag)
return nil
}
func (clip *circleClipData) String() string {
return runStringWriter(clip)
}
func (clip *circleClipData) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("circle { ")
comma := false
for _, tag := range []PropertyName{Radius, X, Y} {
if value, ok := clip.properties[tag]; ok {
text := propertyValueToString(tag, value, indent)
if text != "" {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
buffer.WriteString(text)
comma = true
}
}
}
buffer.WriteString(" }")
}
func (clip *circleClipData) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString("circle(")
r, _ := sizeProperty(clip, Radius, session)
buffer.WriteString(r.cssString("50%", session))
buffer.WriteString(" at ")
x, _ := sizeProperty(clip, X, session)
buffer.WriteString(x.cssString("50%", session))
buffer.WriteRune(' ')
y, _ := sizeProperty(clip, Y, session)
buffer.WriteString(y.cssString("50%", session))
buffer.WriteRune(')')
return buffer.String()
}
func (clip *circleClipData) valid(session Session) bool {
if value, ok := sizeProperty(clip, Radius, session); ok && value.Value == 0 {
return false
}
return true
}
func (clip *ellipseClipData) init() {
clip.dataProperty.init()
clip.set = ellipseClipDataSet
clip.supportedProperties = []PropertyName{X, Y, Radius, RadiusX, RadiusY}
}
func (clip *ellipseClipData) Shape() ClipShape {
return EllipseClip
}
func ellipseClipDataSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case X, Y, RadiusX, RadiusY:
return setSizeProperty(properties, tag, value)
case Radius:
if result := setSizeProperty(properties, RadiusX, value); result != nil {
properties.setRaw(RadiusY, properties.getRaw(RadiusX))
return append(result, RadiusY)
}
return nil
}
ErrorLogF(`"%s" property is not supported by the ellipse clip shape`, tag)
return nil
}
func (clip *ellipseClipData) String() string {
return runStringWriter(clip)
}
func (clip *ellipseClipData) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("ellipse { ")
comma := false
for _, tag := range []PropertyName{RadiusX, RadiusY, X, Y} {
if value, ok := clip.properties[tag]; ok {
text := propertyValueToString(tag, value, indent)
if text != "" {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
buffer.WriteString(text)
comma = true
}
}
}
buffer.WriteString(" }")
}
func (clip *ellipseClipData) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
rx, _ := sizeProperty(clip, RadiusX, session)
ry, _ := sizeProperty(clip, RadiusX, session)
buffer.WriteString("ellipse(")
buffer.WriteString(rx.cssString("50%", session))
buffer.WriteRune(' ')
buffer.WriteString(ry.cssString("50%", session))
buffer.WriteString(" at ")
x, _ := sizeProperty(clip, X, session)
buffer.WriteString(x.cssString("50%", session))
buffer.WriteRune(' ')
y, _ := sizeProperty(clip, Y, session)
buffer.WriteString(y.cssString("50%", session))
buffer.WriteRune(')')
return buffer.String()
}
func (clip *ellipseClipData) valid(session Session) bool {
rx, _ := sizeProperty(clip, RadiusX, session)
ry, _ := sizeProperty(clip, RadiusY, session)
return rx.Value != 0 && ry.Value != 0
}
func (clip *polygonClipData) init() {
clip.dataProperty.init()
clip.set = polygonClipDataSet
clip.supportedProperties = []PropertyName{Points}
}
func (clip *polygonClipData) Shape() ClipShape {
return PolygonClip
}
func polygonClipDataSet(properties Properties, tag PropertyName, value any) []PropertyName {
if Points == tag {
switch value := value.(type) {
case []any:
points := make([]any, len(value))
for i, val := range value {
switch val := val.(type) {
case string:
if ok, _ := isConstantName(val); ok {
points[i] = val
} else if size, ok := StringToSizeUnit(val); ok {
points[i] = size
} else {
notCompatibleType(tag, val)
return nil
}
case SizeUnit:
points[i] = val
default:
notCompatibleType(tag, val)
points[i] = AutoSize()
return nil
}
}
properties.setRaw(Points, points)
return []PropertyName{tag}
case []SizeUnit:
points := make([]any, len(value))
for i, point := range value {
points[i] = point
}
properties.setRaw(Points, points)
return []PropertyName{tag}
case string:
values := strings.Split(value, ",")
points := make([]any, len(values))
for i, val := range values {
val = strings.Trim(val, " \t\n\r")
if ok, _ := isConstantName(val); ok {
points[i] = val
} else if size, ok := StringToSizeUnit(val); ok {
points[i] = size
} else {
notCompatibleType(tag, val)
return nil
}
}
properties.setRaw(Points, points)
return []PropertyName{tag}
}
}
return nil
}
func (clip *polygonClipData) String() string {
return runStringWriter(clip)
}
func (clip *polygonClipData) points() []any {
if value := clip.getRaw(Points); value != nil {
if points, ok := value.([]any); ok {
return points
}
}
return nil
}
func (clip *polygonClipData) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("polygon { ")
if points := clip.points(); points != nil {
buffer.WriteString(string(Points))
buffer.WriteString(` = "`)
comma := false
for _, value := range points {
text := propertyValueToString("", value, indent)
if text != "" {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(text)
comma = true
}
}
buffer.WriteString(`" `)
}
buffer.WriteRune('}')
}
func (clip *polygonClipData) cssStyle(session Session) string {
points := clip.points()
count := len(points)
if count < 2 {
return ""
}
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
writePoint := func(value any) {
switch value := value.(type) {
case string:
if val, ok := session.resolveConstants(value); ok {
if size, ok := StringToSizeUnit(val); ok {
buffer.WriteString(size.cssString("0px", session))
return
}
}
case SizeUnit:
buffer.WriteString(value.cssString("0px", session))
return
}
buffer.WriteString("0px")
}
leadText := "polygon("
for i := 1; i < count; i += 2 {
buffer.WriteString(leadText)
writePoint(points[i-1])
buffer.WriteRune(' ')
writePoint(points[i])
leadText = ", "
}
buffer.WriteRune(')')
return buffer.String()
}
func (clip *polygonClipData) valid(session Session) bool {
return len(clip.points()) > 0
}
func parseClipShapeProperty(obj DataObject) ClipShapeProperty {
switch obj.Tag() {
case "inset":
clip := new(insetClipData)
clip.init()
for _, tag := range []PropertyName{Top, Right, Bottom, Left, Radius, RadiusX, RadiusY} {
if value, ok := obj.PropertyValue(string(tag)); ok {
insetClipDataSet(clip, tag, value)
}
}
return clip
case "circle":
clip := new(circleClipData)
clip.init()
for _, tag := range []PropertyName{X, Y, Radius} {
if value, ok := obj.PropertyValue(string(tag)); ok {
circleClipDataSet(clip, tag, value)
}
}
return clip
case "ellipse":
clip := new(ellipseClipData)
clip.init()
for _, tag := range []PropertyName{X, Y, RadiusX, RadiusY} {
if value, ok := obj.PropertyValue(string(tag)); ok {
ellipseClipDataSet(clip, tag, value)
}
}
return clip
case "polygon":
clip := new(polygonClipData)
clip.init()
if value, ok := obj.PropertyValue(string(Points)); ok {
polygonClipDataSet(clip, Points, value)
}
return clip
}
return nil
}
func setClipShapePropertyProperty(properties Properties, tag PropertyName, value any) []PropertyName {
switch value := value.(type) {
case ClipShapeProperty:
properties.setRaw(tag, value)
return []PropertyName{tag}
case string:
if ok, _ := isConstantName(value); ok {
properties.setRaw(tag, value)
return []PropertyName{tag}
}
if obj := NewDataObject(value); obj == nil {
if clip := parseClipShapeProperty(obj); clip != nil {
properties.setRaw(tag, clip)
return []PropertyName{tag}
}
}
case DataObject:
if clip := parseClipShapeProperty(value); clip != nil {
properties.setRaw(tag, clip)
return []PropertyName{tag}
}
case DataValue:
if value.IsObject() {
if clip := parseClipShapeProperty(value.Object()); clip != nil {
properties.setRaw(tag, clip)
return []PropertyName{tag}
}
}
}
notCompatibleType(tag, value)
return nil
}
func getClipShapeProperty(prop Properties, tag PropertyName, session Session) ClipShapeProperty {
if value := prop.getRaw(tag); value != nil {
switch value := value.(type) {
case ClipShapeProperty:
return value
case string:
if text, ok := session.resolveConstants(value); ok {
if obj := NewDataObject(text); obj == nil {
return parseClipShapeProperty(obj)
}
}
}
}
return nil
}
// GetClip returns a View clipping area.
// If the second argument (subviewID) is not specified or it is "" then a top position of the first argument (view) is returned
func GetClip(view View, subviewID ...string) ClipShapeProperty {
if view = getSubview(view, subviewID); view != nil {
return getClipShapeProperty(view, Clip, view.Session())
}
return nil
}
// GetShapeOutside returns a shape around which adjacent inline content.
// If the second argument (subviewID) is not specified or it is "" then a top position of the first argument (view) is returned
func GetShapeOutside(view View, subviewID ...string) ClipShapeProperty {
if view = getSubview(view, subviewID); view != nil {
return getClipShapeProperty(view, ShapeOutside, view.Session())
}
return nil
}

View File

@ -11,6 +11,22 @@ import (
// Color - represent color in argb format // Color - represent color in argb format
type Color uint32 type Color uint32
// ARGB creates Color using alpha, red, green and blue components
func ARGB[T int | uint | int8 | uint8](alpha, red, green, blue T) Color {
return ((Color(alpha) & 0xFF) << 24) +
((Color(red) & 0xFF) << 16) +
((Color(green) & 0xFF) << 8) +
(Color(blue) & 0xFF)
}
// RGB creates Color using red, green and blue components
func RGB[T int | uint | int8 | uint8](red, green, blue T) Color {
return (Color(0xFF) << 24) +
((Color(red) & 0xFF) << 16) +
((Color(green) & 0xFF) << 8) +
(Color(blue) & 0xFF)
}
// ARGB - return alpha, red, green and blue components of the color // ARGB - return alpha, red, green and blue components of the color
func (color Color) ARGB() (uint8, uint8, uint8, uint8) { func (color Color) ARGB() (uint8, uint8, uint8, uint8) {
return uint8(color >> 24), return uint8(color >> 24),

View File

@ -2,6 +2,7 @@ package rui
import "sort" import "sort"
// A set of predefined colors used in the library
const ( const (
// Black color constant // Black color constant
Black Color = 0xff000000 Black Color = 0xff000000
@ -55,8 +56,8 @@ const (
BlueViolet Color = 0xff8a2be2 BlueViolet Color = 0xff8a2be2
// Brown color constant // Brown color constant
Brown Color = 0xffa52a2a Brown Color = 0xffa52a2a
// Burlywood color constant // BurlyWood color constant
Burlywood Color = 0xffdeb887 BurlyWood Color = 0xffdeb887
// CadetBlue color constant // CadetBlue color constant
CadetBlue Color = 0xff5f9ea0 CadetBlue Color = 0xff5f9ea0
// Chartreuse color constant // Chartreuse color constant
@ -67,8 +68,8 @@ const (
Coral Color = 0xffff7f50 Coral Color = 0xffff7f50
// CornflowerBlue color constant // CornflowerBlue color constant
CornflowerBlue Color = 0xff6495ed CornflowerBlue Color = 0xff6495ed
// Cornsilk color constant // CornSilk color constant
Cornsilk Color = 0xfffff8dc CornSilk Color = 0xfffff8dc
// Crimson color constant // Crimson color constant
Crimson Color = 0xffdc143c Crimson Color = 0xffdc143c
// Cyan color constant // Cyan color constant
@ -105,8 +106,8 @@ const (
DarkSlateBlue Color = 0xff483d8b DarkSlateBlue Color = 0xff483d8b
// DarkSlateGray color constant // DarkSlateGray color constant
DarkSlateGray Color = 0xff2f4f4f DarkSlateGray Color = 0xff2f4f4f
// Darkslategrey color constant // DarkSlateGrey color constant
Darkslategrey Color = 0xff2f4f4f DarkSlateGrey Color = 0xff2f4f4f
// DarkTurquoise color constant // DarkTurquoise color constant
DarkTurquoise Color = 0xff00ced1 DarkTurquoise Color = 0xff00ced1
// DarkViolet color constant // DarkViolet color constant
@ -135,8 +136,8 @@ const (
Gold Color = 0xffffd700 Gold Color = 0xffffd700
// GoldenRod color constant // GoldenRod color constant
GoldenRod Color = 0xffdaa520 GoldenRod Color = 0xffdaa520
// GreenyEllow color constant // GreenYellow color constant
GreenyEllow Color = 0xffadff2f GreenYellow Color = 0xffadff2f
// Grey color constant // Grey color constant
Grey Color = 0xff808080 Grey Color = 0xff808080
// Honeydew color constant // Honeydew color constant
@ -293,8 +294,8 @@ const (
Violet Color = 0xffee82ee Violet Color = 0xffee82ee
// Wheat color constant // Wheat color constant
Wheat Color = 0xfff5deb3 Wheat Color = 0xfff5deb3
// Whitesmoke color constant // WhiteSmoke color constant
Whitesmoke Color = 0xfff5f5f5 WhiteSmoke Color = 0xfff5f5f5
// YellowGreen color constant // YellowGreen color constant
YellowGreen Color = 0xff9acd32 YellowGreen Color = 0xff9acd32
) )
@ -449,8 +450,12 @@ var colorConstants = map[string]Color{
"yellowgreen": 0xff9acd32, "yellowgreen": 0xff9acd32,
} }
// NamedColor make a relation between color and its name
type NamedColor struct { type NamedColor struct {
Name string // Name of a color
Name string
// Color value
Color Color Color Color
} }

View File

@ -1,23 +1,51 @@
package rui package rui
import ( import (
"fmt"
"strings" "strings"
) )
// Constants for [ColorPicker] specific properties and events.
const ( const (
ColorChangedEvent = "color-changed" // ColorChangedEvent is the constant for "color-changed" property tag.
ColorPickerValue = "color-picker-value" //
// Used by `ColorPicker`.
// Event generated when color picker value has been changed.
//
// General listener format:
// func(picker rui.ColorPicker, newColor, oldColor rui.Color)
//
// where:
// - picker - Interface of a color picker which generated this event,
// - newColor - New color value,
// - oldColor - Old color value.
//
// Allowed listener formats:
// func(picker rui.ColorPicker, newColor rui.Color)
// func(newColor, oldColor rui.Color)
// func(newColor rui.Color)
// func(picker rui.ColorPicker)
// func()
ColorChangedEvent PropertyName = "color-changed"
// ColorPickerValue is the constant for "color-picker-value" property tag.
//
// Used by `ColorPicker`.
// Define current color picker value.
//
// Supported types: `Color`, `string`.
//
// Internal type is `Color`, other types converted to it during assignment.
// See `Color` description for more details.
ColorPickerValue PropertyName = "color-picker-value"
) )
// ColorPicker - ColorPicker view // ColorPicker represent a ColorPicker view
type ColorPicker interface { type ColorPicker interface {
View View
} }
type colorPickerData struct { type colorPickerData struct {
viewData viewData
colorChangedListeners []func(ColorPicker, Color)
} }
// NewColorPicker create new ColorPicker object and return it // NewColorPicker create new ColorPicker object and return it
@ -29,118 +57,94 @@ func NewColorPicker(session Session, params Params) ColorPicker {
} }
func newColorPicker(session Session) View { func newColorPicker(session Session) View {
return NewColorPicker(session, nil) return new(colorPickerData)
} }
func (picker *colorPickerData) init(session Session) { func (picker *colorPickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "ColorPicker" picker.tag = "ColorPicker"
picker.colorChangedListeners = []func(ColorPicker, Color){} picker.hasHtmlDisabled = true
picker.properties[Padding] = Px(0) picker.properties[Padding] = Px(0)
picker.normalize = normalizeColorPickerTag
picker.get = picker.getFunc
picker.set = picker.setFunc
picker.changed = picker.propertyChanged
} }
func (picker *colorPickerData) String() string { func normalizeColorPickerTag(tag PropertyName) PropertyName {
return getViewString(picker) tag = defaultNormalize(tag)
}
func (picker *colorPickerData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag { switch tag {
case Value, ColorTag: case Value, ColorTag:
return ColorPickerValue return ColorPickerValue
} }
return tag return normalizeDataListTag(tag)
} }
func (picker *colorPickerData) Remove(tag string) { func (picker *colorPickerData) getFunc(tag PropertyName) any {
picker.remove(picker.normalizeTag(tag))
}
func (picker *colorPickerData) remove(tag string) {
switch tag { switch tag {
case ColorChangedEvent: case ColorChangedEvent:
if len(picker.colorChangedListeners) > 0 { if listeners := getTwoArgEventRawListeners[ColorPicker, Color](picker, nil, tag); len(listeners) > 0 {
picker.colorChangedListeners = []func(ColorPicker, Color){} return listeners
picker.propertyChangedEvent(tag)
} }
return nil
}
return picker.viewData.getFunc(tag)
}
func (picker *colorPickerData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case ColorChangedEvent:
return setTwoArgEventListener[ColorPicker, Color](picker, tag, value)
case ColorPickerValue: case ColorPickerValue:
oldColor := GetColorPickerValue(picker) oldColor := GetColorPickerValue(picker)
delete(picker.properties, ColorPickerValue) result := setColorProperty(picker, ColorPickerValue, value)
picker.colorChanged(oldColor) if result != nil {
picker.setRaw("old-color", oldColor)
default:
picker.viewData.remove(tag)
}
}
func (picker *colorPickerData) Set(tag string, value any) bool {
return picker.set(picker.normalizeTag(tag), value)
}
func (picker *colorPickerData) set(tag string, value any) bool {
if value == nil {
picker.remove(tag)
return true
}
switch tag {
case ColorChangedEvent:
listeners, ok := valueToEventListeners[ColorPicker, Color](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(ColorPicker, Color){}
} }
picker.colorChangedListeners = listeners return result
picker.propertyChangedEvent(tag)
return true
case DataList:
return setDataList(picker, value, "")
}
return picker.viewData.setFunc(tag, value)
}
func (picker *colorPickerData) propertyChanged(tag PropertyName) {
switch tag {
case ColorPickerValue: case ColorPickerValue:
oldColor := GetColorPickerValue(picker) color := GetColorPickerValue(picker)
if picker.setColorProperty(ColorPickerValue, value) { picker.Session().callFunc("setInputValue", picker.htmlID(), color.rgbString())
picker.colorChanged(oldColor)
return true if listeners := getTwoArgEventListeners[ColorPicker, Color](picker, nil, ColorChangedEvent); len(listeners) > 0 {
oldColor := Color(0)
if value := picker.getRaw("old-color"); value != nil {
oldColor = value.(Color)
}
for _, listener := range listeners {
listener.Run(picker, color, oldColor)
}
} }
default: default:
return picker.viewData.set(tag, value) picker.viewData.propertyChanged(tag)
} }
return false
}
func (picker *colorPickerData) colorChanged(oldColor Color) {
if newColor := GetColorPickerValue(picker); oldColor != newColor {
if picker.created {
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, picker.htmlID(), newColor.rgbString()))
}
for _, listener := range picker.colorChangedListeners {
listener(picker, newColor)
}
picker.propertyChangedEvent(ColorTag)
}
}
func (picker *colorPickerData) Get(tag string) any {
return picker.get(picker.normalizeTag(tag))
}
func (picker *colorPickerData) get(tag string) any {
switch tag {
case ColorChangedEvent:
return picker.colorChangedListeners
default:
return picker.viewData.get(tag)
}
} }
func (picker *colorPickerData) htmlTag() string { func (picker *colorPickerData) htmlTag() string {
return "input" return "input"
} }
func (picker *colorPickerData) htmlSubviews(self View, buffer *strings.Builder) {
dataListHtmlSubviews(self, buffer, func(text string, session Session) string {
text, _ = session.resolveConstants(text)
return text
})
}
func (picker *colorPickerData) htmlProperties(self View, buffer *strings.Builder) { func (picker *colorPickerData) htmlProperties(self View, buffer *strings.Builder) {
picker.viewData.htmlProperties(self, buffer) picker.viewData.htmlProperties(self, buffer)
@ -152,26 +156,22 @@ func (picker *colorPickerData) htmlProperties(self View, buffer *strings.Builder
if picker.getRaw(ClickEvent) == nil { if picker.getRaw(ClickEvent) == nil {
buffer.WriteString(` onclick="stopEventPropagation(this, event)"`) buffer.WriteString(` onclick="stopEventPropagation(this, event)"`)
} }
dataListHtmlProperties(picker, buffer)
} }
func (picker *colorPickerData) htmlDisabledProperties(self View, buffer *strings.Builder) { func (picker *colorPickerData) handleCommand(self View, command PropertyName, data DataObject) bool {
if IsDisabled(self) {
buffer.WriteString(` disabled`)
}
picker.viewData.htmlDisabledProperties(self, buffer)
}
func (picker *colorPickerData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "textChanged": case "textChanged":
if text, ok := data.PropertyValue("text"); ok { if text, ok := data.PropertyValue("text"); ok {
oldColor := GetColorPickerValue(picker)
if color, ok := StringToColor(text); ok { if color, ok := StringToColor(text); ok {
oldColor := GetColorPickerValue(picker)
picker.properties[ColorPickerValue] = color picker.properties[ColorPickerValue] = color
if color != oldColor { if color != oldColor {
for _, listener := range picker.colorChangedListeners { for _, listener := range getTwoArgEventListeners[ColorPicker, Color](picker, nil, ColorChangedEvent) {
listener(picker, color) listener.Run(picker, color, oldColor)
} }
picker.runChangeListener(ColorPickerValue)
} }
} }
} }
@ -182,16 +182,15 @@ func (picker *colorPickerData) handleCommand(self View, command string, data Dat
} }
// GetColorPickerValue returns the value of ColorPicker subview. // GetColorPickerValue returns the value of ColorPicker subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetColorPickerValue(view View, subviewID ...string) Color { func GetColorPickerValue(view View, subviewID ...string) Color {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value, ok := colorProperty(view, ColorPickerValue, view.Session()); ok { if value, ok := colorProperty(view, ColorPickerValue, view.Session()); ok {
return value return value
} }
for _, tag := range []string{ColorPickerValue, Value, ColorTag} { for _, tag := range []PropertyName{ColorPickerValue, Value, ColorTag} {
if value := valueFromStyle(view, tag); value != nil { if value := valueFromStyle(view, tag); value != nil {
if result, ok := valueToColor(value, view.Session()); ok { if result, ok := valueToColor(value, view.Session()); ok {
return result return result
@ -204,7 +203,18 @@ func GetColorPickerValue(view View, subviewID ...string) Color {
// GetColorChangedListeners returns the ColorChangedListener list of an ColorPicker subview. // GetColorChangedListeners returns the ColorChangedListener list of an ColorPicker subview.
// If there are no listeners then the empty list is returned // If there are no listeners then the empty list is returned
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetColorChangedListeners(view View, subviewID ...string) []func(ColorPicker, Color) { // Result elements can be of the following types:
return getEventListeners[ColorPicker, Color](view, subviewID, ColorChangedEvent) // - func(rui.ColorPicker, rui.Color, rui.Color),
// - func(rui.ColorPicker, rui.Color),
// - func(rui.ColorPicker),
// - func(rui.Color, rui.Color),
// - func(rui.Color),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetColorChangedListeners(view View, subviewID ...string) []any {
return getTwoArgEventRawListeners[ColorPicker, Color](view, subviewID, ColorChangedEvent)
} }

View File

@ -2,40 +2,120 @@ package rui
import ( import (
"strconv" "strconv"
"strings"
) )
// Constants for [ColumnLayout] specific properties and events
const ( const (
// ColumnCount is the constant for the "column-count" property tag. // ColumnCount is the constant for "column-count" property tag.
// The "column-count" int property specifies number of columns into which the content is break //
// Values less than zero are not valid. if the "column-count" property value is 0 then // Used by ColumnLayout.
// the number of columns is calculated based on the "column-width" property // Specifies number of columns into which the content is break. Values less than zero are not valid. If this property
ColumnCount = "column-count" // value is 0 then the number of columns is calculated based on the "column-width" property.
// ColumnWidth is the constant for the "column-width" property tag. //
// The "column-width" SizeUnit property specifies the width of each column. // Supported types: int, string.
ColumnWidth = "column-width" //
// ColumnGap is the constant for the "column-gap" property tag. // Values:
// The "column-width" SizeUnit property sets the size of the gap (gutter) between columns. // - 0 or "0" - Use "column-width" to control how many columns will be created.
ColumnGap = "column-gap" // - positive value - Тhe number of columns into which the content is divided.
// ColumnSeparator is the constant for the "column-separator" property tag. ColumnCount PropertyName = "column-count"
// The "column-separator" property specifies the line drawn between columns in a multi-column layout.
ColumnSeparator = "column-separator" // ColumnWidth is the constant for "column-width" property tag.
// ColumnSeparatorStyle is the constant for the "column-separator-style" property tag. //
// The "column-separator-style" int property sets the style of the line drawn between // Used by ColumnLayout.
// columns in a multi-column layout. // Specifies the width of each column.
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4). //
ColumnSeparatorStyle = "column-separator-style" // Supported types: SizeUnit, SizeFunc, string, float, int.
// ColumnSeparatorWidth is the constant for the "column-separator-width" property tag. //
// The "column-separator-width" SizeUnit property sets the width of the line drawn between // Internal type is SizeUnit, other types converted to it during assignment.
// columns in a multi-column layout. // See [SizeUnit] description for more details.
ColumnSeparatorWidth = "column-separator-width" ColumnWidth PropertyName = "column-width"
// ColumnSeparatorColor is the constant for the "column-separator-color" property tag.
// The "column-separator-color" Color property sets the color of the line drawn between // ColumnGap is the constant for "column-gap" property tag.
// columns in a multi-column layout. //
ColumnSeparatorColor = "column-separator-color" // Used by ColumnLayout.
// Set the size of the gap (gutter) between columns.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See [SizeUnit] description for more details.
ColumnGap PropertyName = "column-gap"
// ColumnSeparator is the constant for "column-separator" property tag.
//
// Used by ColumnLayout.
// Specifies the line drawn between columns in a multi-column layout.
//
// Supported types: ColumnSeparatorProperty, ViewBorder.
//
// Internal type is ColumnSeparatorProperty, other types converted to it during assignment.
// See [ColumnSeparatorProperty] and [ViewBorder] description for more details.
ColumnSeparator PropertyName = "column-separator"
// ColumnSeparatorStyle is the constant for "column-separator-style" property tag.
//
// Used by ColumnLayout.
// Controls the style of the line drawn between columns in a multi-column layout.
//
// Supported types: int, string.
//
// Values:
// - 0 (NoneLine) or "none" - The separator will not be drawn.
// - 1 (SolidLine) or "solid" - Solid line as a separator.
// - 2 (DashedLine) or "dashed" - Dashed line as a separator.
// - 3 (DottedLine) or "dotted" - Dotted line as a separator.
// - 4 (DoubleLine) or "double" - Double line as a separator.
ColumnSeparatorStyle PropertyName = "column-separator-style"
// ColumnSeparatorWidth is the constant for "column-separator-width" property tag.
//
// Used by ColumnLayout.
// Set the width of the line drawn between columns in a multi-column layout.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
ColumnSeparatorWidth PropertyName = "column-separator-width"
// ColumnSeparatorColor is the constant for "column-separator-color" property tag.
//
// Used by ColumnLayout.
// Set the color of the line drawn between columns in a multi-column layout.
//
// Supported types: Color, string.
//
// Internal type is Color, other types converted to it during assignment.
// See Color description for more details.
ColumnSeparatorColor PropertyName = "column-separator-color"
// ColumnFill is the constant for "column-fill" property tag.
//
// Used by ColumnLayout.
// Controls how a ColumnLayout's content is balanced when broken into columns. Default value is "balance".
//
// Supported types: int, string.
//
// Values:
// - 0 (ColumnFillBalance) or "balance" - Content is equally divided between columns.
// - 1 (ColumnFillAuto) or "auto" - Columns are filled sequentially. Content takes up only the room it needs, possibly resulting in some columns remaining empty.
ColumnFill PropertyName = "column-fill"
// ColumnSpanAll is the constant for "column-span-all" property tag.
//
// Used by ColumnLayout.
// Property used in views placed inside the column layout container. Makes it possible for a view to span across all
// columns. Default value is false.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", or "1" - View will span across all columns.
// - false, 0, "false", "no", "off", or "0" - View will be a part of a column.
ColumnSpanAll PropertyName = "column-span-all"
) )
// ColumnLayout - grid-container of View // ColumnLayout represent a ColumnLayout view
type ColumnLayout interface { type ColumnLayout interface {
ViewsContainer ViewsContainer
} }
@ -53,22 +133,20 @@ func NewColumnLayout(session Session, params Params) ColumnLayout {
} }
func newColumnLayout(session Session) View { func newColumnLayout(session Session) View {
return NewColumnLayout(session, nil) return new(columnLayoutData)
} }
// Init initialize fields of ColumnLayout by default values // Init initialize fields of ColumnLayout by default values
func (ColumnLayout *columnLayoutData) init(session Session) { func (columnLayout *columnLayoutData) init(session Session) {
ColumnLayout.viewsContainerData.init(session) columnLayout.viewsContainerData.init(session)
ColumnLayout.tag = "ColumnLayout" columnLayout.tag = "ColumnLayout"
//ColumnLayout.systemClass = "ruiColumnLayout" columnLayout.systemClass = "ruiColumnLayout"
columnLayout.normalize = normalizeColumnLayoutTag
columnLayout.changed = columnLayout.propertyChanged
} }
func (columnLayout *columnLayoutData) String() string { func normalizeColumnLayoutTag(tag PropertyName) PropertyName {
return getViewString(columnLayout) tag = defaultNormalize(tag)
}
func (columnLayout *columnLayoutData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag { switch tag {
case Gap: case Gap:
return ColumnGap return ColumnGap
@ -76,62 +154,28 @@ func (columnLayout *columnLayoutData) normalizeTag(tag string) string {
return tag return tag
} }
func (columnLayout *columnLayoutData) Get(tag string) any { func (columnLayout *columnLayoutData) propertyChanged(tag PropertyName) {
return columnLayout.get(columnLayout.normalizeTag(tag)) switch tag {
} case ColumnSeparator:
css := ""
func (columnLayout *columnLayoutData) Remove(tag string) { session := columnLayout.Session()
columnLayout.remove(columnLayout.normalizeTag(tag)) if value := columnLayout.getRaw(ColumnSeparator); value != nil {
} separator := value.(ColumnSeparatorProperty)
css = separator.cssValue(session)
func (columnLayout *columnLayoutData) remove(tag string) {
columnLayout.viewsContainerData.remove(tag)
if columnLayout.created {
switch tag {
case ColumnCount, ColumnWidth, ColumnGap:
updateCSSProperty(columnLayout.htmlID(), tag, "", columnLayout.Session())
case ColumnSeparator:
updateCSSProperty(columnLayout.htmlID(), "column-rule", "", columnLayout.Session())
} }
} session.updateCSSProperty(columnLayout.htmlID(), "column-rule", css)
}
func (columnLayout *columnLayoutData) Set(tag string, value any) bool { case ColumnCount:
return columnLayout.set(columnLayout.normalizeTag(tag), value) session := columnLayout.Session()
} if count := GetColumnCount(columnLayout); count > 0 {
session.updateCSSProperty(columnLayout.htmlID(), string(ColumnCount), strconv.Itoa(count))
func (columnLayout *columnLayoutData) set(tag string, value any) bool { } else {
if value == nil { session.updateCSSProperty(columnLayout.htmlID(), string(ColumnCount), "auto")
columnLayout.remove(tag)
return true
}
if !columnLayout.viewsContainerData.set(tag, value) {
return false
}
if columnLayout.created {
switch tag {
case ColumnSeparator:
css := ""
session := columnLayout.Session()
if val, ok := columnLayout.properties[ColumnSeparator]; ok {
separator := val.(ColumnSeparatorProperty)
css = separator.cssValue(columnLayout.Session())
}
updateCSSProperty(columnLayout.htmlID(), "column-rule", css, session)
case ColumnCount:
session := columnLayout.Session()
if count, ok := intProperty(columnLayout, tag, session, 0); ok && count > 0 {
updateCSSProperty(columnLayout.htmlID(), tag, strconv.Itoa(count), session)
} else {
updateCSSProperty(columnLayout.htmlID(), tag, "auto", session)
}
} }
default:
columnLayout.viewsContainerData.propertyChanged(tag)
} }
return true
} }
// GetColumnCount returns int value which specifies number of columns into which the content of // GetColumnCount returns int value which specifies number of columns into which the content of
@ -155,11 +199,7 @@ func GetColumnGap(view View, subviewID ...string) SizeUnit {
} }
func getColumnSeparator(view View, subviewID []string) ViewBorder { func getColumnSeparator(view View, subviewID []string) ViewBorder {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0])
}
if view != nil {
value := view.Get(ColumnSeparator) value := view.Get(ColumnSeparator)
if value == nil { if value == nil {
value = valueFromStyle(view, ColumnSeparator) value = valueFromStyle(view, ColumnSeparator)
@ -206,3 +246,20 @@ func GetColumnSeparatorColor(view View, subviewID ...string) Color {
border := getColumnSeparator(view, subviewID) border := getColumnSeparator(view, subviewID)
return border.Color return border.Color
} }
// GetColumnFill returns a "column-fill" property value of the subview.
// Returns one of next values: ColumnFillBalance (0) or ColumnFillAuto (1)
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetColumnFill(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, ColumnFill, ColumnFillBalance, true)
}
// IsColumnSpanAll returns a "column-span-all" property value of the subview.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func IsColumnSpanAll(view View, subviewID ...string) bool {
return boolStyledProperty(view, subviewID, ColumnSpanAll, false)
}

View File

@ -2,7 +2,6 @@ package rui
import ( import (
"fmt" "fmt"
"strings"
) )
// ColumnSeparatorProperty is the interface of a view separator data // ColumnSeparatorProperty is the interface of a view separator data
@ -10,19 +9,22 @@ type ColumnSeparatorProperty interface {
Properties Properties
fmt.Stringer fmt.Stringer
stringWriter stringWriter
// ViewBorder returns column separator description in a form of ViewBorder
ViewBorder(session Session) ViewBorder ViewBorder(session Session) ViewBorder
cssValue(session Session) string cssValue(session Session) string
} }
type columnSeparatorProperty struct { type columnSeparatorProperty struct {
propertyList dataProperty
} }
func newColumnSeparatorProperty(value any) ColumnSeparatorProperty { func newColumnSeparatorProperty(value any) ColumnSeparatorProperty {
if value == nil { if value == nil {
separator := new(columnSeparatorProperty) separator := new(columnSeparatorProperty)
separator.properties = map[string]any{} separator.init()
return separator return separator
} }
@ -32,17 +34,18 @@ func newColumnSeparatorProperty(value any) ColumnSeparatorProperty {
case DataObject: case DataObject:
separator := new(columnSeparatorProperty) separator := new(columnSeparatorProperty)
separator.properties = map[string]any{} separator.init()
for _, tag := range []string{Style, Width, ColorTag} { for _, tag := range []PropertyName{Style, Width, ColorTag} {
if val, ok := value.PropertyValue(tag); ok && val != "" { if val, ok := value.PropertyValue(string(tag)); ok && val != "" {
separator.set(tag, value) propertiesSet(separator, tag, value)
} }
} }
return separator return separator
case ViewBorder: case ViewBorder:
separator := new(columnSeparatorProperty) separator := new(columnSeparatorProperty)
separator.properties = map[string]any{ separator.init()
separator.properties = map[PropertyName]any{
Style: value.Style, Style: value.Style,
Width: value.Width, Width: value.Width,
ColorTag: value.Color, ColorTag: value.Color,
@ -54,12 +57,17 @@ func newColumnSeparatorProperty(value any) ColumnSeparatorProperty {
return nil return nil
} }
// NewColumnSeparator creates the new ColumnSeparatorProperty // NewColumnSeparatorProperty creates the new ColumnSeparatorProperty.
func NewColumnSeparator(params Params) ColumnSeparatorProperty { //
// The following properties can be used:
// - "style" (Style) - Determines the line style (type is int). Valid values: 0 (NoneLine), 1 (SolidLine), 2 (DashedLine), 3 (DottedLine), or 4 (DoubleLine);
// - "color" (ColorTag) - Determines the line color (type is [Color]);
// - "width" (Width) - Determines the line thickness (type is [SizeUnit]).
func NewColumnSeparatorProperty(params Params) ColumnSeparatorProperty {
separator := new(columnSeparatorProperty) separator := new(columnSeparatorProperty)
separator.properties = map[string]any{} separator.init()
if params != nil { if params != nil {
for _, tag := range []string{Style, Width, ColorTag} { for _, tag := range []PropertyName{Style, Width, ColorTag} {
if value, ok := params[tag]; ok && value != nil { if value, ok := params[tag]; ok && value != nil {
separator.Set(tag, value) separator.Set(tag, value)
} }
@ -68,8 +76,29 @@ func NewColumnSeparator(params Params) ColumnSeparatorProperty {
return separator return separator
} }
func (separator *columnSeparatorProperty) normalizeTag(tag string) string { // NewColumnSeparator creates the new ColumnSeparatorProperty.
tag = strings.ToLower(tag) //
// Arguments:
// - style - determines the line style. Valid values: 0 [NoneLine], 1 [SolidLine], 2 [DashedLine], 3 [DottedLine], or 4 [DoubleLine];
// - color - determines the line color;
// - width - determines the line thickness.
func NewColumnSeparator(style int, color Color, width SizeUnit) ColumnSeparatorProperty {
return NewColumnSeparatorProperty(Params{
Width: width,
Style: style,
ColorTag: color,
})
}
func (separator *columnSeparatorProperty) init() {
separator.dataProperty.init()
separator.normalize = normalizeColumnSeparatorTag
separator.set = columnSeparatorSet
separator.supportedProperties = []PropertyName{Style, Width, ColorTag}
}
func normalizeColumnSeparatorTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag { switch tag {
case ColumnSeparatorStyle, "separator-style": case ColumnSeparatorStyle, "separator-style":
return Style return Style
@ -84,69 +113,16 @@ func (separator *columnSeparatorProperty) normalizeTag(tag string) string {
return tag return tag
} }
func (separator *columnSeparatorProperty) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("_{ ")
comma := false
for _, tag := range []string{Style, Width, ColorTag} {
if value, ok := separator.properties[tag]; ok {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(" = ")
writePropertyValue(buffer, BorderStyle, value, indent)
comma = true
}
}
buffer.WriteString(" }")
}
func (separator *columnSeparatorProperty) String() string { func (separator *columnSeparatorProperty) String() string {
return runStringWriter(separator) return runStringWriter(separator)
} }
func (separator *columnSeparatorProperty) Remove(tag string) { func getColumnSeparatorProperty(properties Properties) ColumnSeparatorProperty {
if val := properties.getRaw(ColumnSeparator); val != nil {
switch tag = separator.normalizeTag(tag); tag { if separator, ok := val.(ColumnSeparatorProperty); ok {
case Style, Width, ColorTag: return separator
delete(separator.properties, tag) }
default:
ErrorLogF(`"%s" property is not compatible with the ColumnSeparatorProperty`, tag)
} }
}
func (separator *columnSeparatorProperty) Set(tag string, value any) bool {
tag = separator.normalizeTag(tag)
if value == nil {
separator.remove(tag)
return true
}
switch tag {
case Style:
return separator.setEnumProperty(Style, value, enumProperties[BorderStyle].values)
case Width:
return separator.setSizeProperty(Width, value)
case ColorTag:
return separator.setColorProperty(ColorTag, value)
}
ErrorLogF(`"%s" property is not compatible with the ColumnSeparatorProperty`, tag)
return false
}
func (separator *columnSeparatorProperty) Get(tag string) any {
tag = separator.normalizeTag(tag)
if result, ok := separator.properties[tag]; ok {
return result
}
return nil return nil
} }
@ -189,3 +165,10 @@ func (separator *columnSeparatorProperty) cssValue(session Session) string {
return buffer.String() return buffer.String()
} }
func columnSeparatorSet(properties Properties, tag PropertyName, value any) []PropertyName {
if tag == Style {
return setEnumProperty(properties, Style, value, enumProperties[BorderStyle].values)
}
return propertiesSet(properties, tag, value)
}

View File

@ -150,9 +150,11 @@ func (builder *cssValueBuilder) addValues(key, separator string, values ...strin
} }
} }
func (builder *cssStyleBuilder) init() { func (builder *cssStyleBuilder) init(kbSize int) {
builder.buffer = allocStringBuilder() builder.buffer = allocStringBuilder()
builder.buffer.Grow(16 * 1024) if kbSize > 0 {
builder.buffer.Grow(kbSize * 1024)
}
} }
func (builder *cssStyleBuilder) finish() string { func (builder *cssStyleBuilder) finish() string {
@ -168,7 +170,7 @@ func (builder *cssStyleBuilder) finish() string {
func (builder *cssStyleBuilder) startMedia(rule string) { func (builder *cssStyleBuilder) startMedia(rule string) {
if builder.buffer == nil { if builder.buffer == nil {
builder.init() builder.init(0)
} }
builder.buffer.WriteString(`@media screen`) builder.buffer.WriteString(`@media screen`)
builder.buffer.WriteString(rule) builder.buffer.WriteString(rule)
@ -178,7 +180,7 @@ func (builder *cssStyleBuilder) startMedia(rule string) {
func (builder *cssStyleBuilder) endMedia() { func (builder *cssStyleBuilder) endMedia() {
if builder.buffer == nil { if builder.buffer == nil {
builder.init() builder.init(0)
} }
builder.buffer.WriteString(`}\n`) builder.buffer.WriteString(`}\n`)
builder.media = false builder.media = false
@ -192,7 +194,7 @@ func (builder *cssStyleBuilder) startStyle(name string) {
} }
if builder.buffer == nil { if builder.buffer == nil {
builder.init() builder.init(0)
} }
if builder.media { if builder.media {
builder.buffer.WriteString(`\t`) builder.buffer.WriteString(`\t`)
@ -210,7 +212,7 @@ func (builder *cssStyleBuilder) startStyle(name string) {
func (builder *cssStyleBuilder) endStyle() { func (builder *cssStyleBuilder) endStyle() {
if builder.buffer == nil { if builder.buffer == nil {
builder.init() builder.init(0)
} }
if builder.media { if builder.media {
builder.buffer.WriteString(`\t`) builder.buffer.WriteString(`\t`)
@ -220,7 +222,7 @@ func (builder *cssStyleBuilder) endStyle() {
func (builder *cssStyleBuilder) startAnimation(name string) { func (builder *cssStyleBuilder) startAnimation(name string) {
if builder.buffer == nil { if builder.buffer == nil {
builder.init() builder.init(0)
} }
builder.media = true builder.media = true
@ -231,7 +233,7 @@ func (builder *cssStyleBuilder) startAnimation(name string) {
func (builder *cssStyleBuilder) endAnimation() { func (builder *cssStyleBuilder) endAnimation() {
if builder.buffer == nil { if builder.buffer == nil {
builder.init() builder.init(0)
} }
builder.buffer.WriteString(`}\n`) builder.buffer.WriteString(`}\n`)
builder.media = false builder.media = false
@ -239,7 +241,7 @@ func (builder *cssStyleBuilder) endAnimation() {
func (builder *cssStyleBuilder) startAnimationFrame(name string) { func (builder *cssStyleBuilder) startAnimationFrame(name string) {
if builder.buffer == nil { if builder.buffer == nil {
builder.init() builder.init(0)
} }
builder.buffer.WriteString(`\t`) builder.buffer.WriteString(`\t`)
@ -249,7 +251,7 @@ func (builder *cssStyleBuilder) startAnimationFrame(name string) {
func (builder *cssStyleBuilder) endAnimationFrame() { func (builder *cssStyleBuilder) endAnimationFrame() {
if builder.buffer == nil { if builder.buffer == nil {
builder.init() builder.init(0)
} }
builder.buffer.WriteString(`\t}\n`) builder.buffer.WriteString(`\t}\n`)
} }
@ -257,7 +259,7 @@ func (builder *cssStyleBuilder) endAnimationFrame() {
func (builder *cssStyleBuilder) add(key, value string) { func (builder *cssStyleBuilder) add(key, value string) {
if value != "" { if value != "" {
if builder.buffer == nil { if builder.buffer == nil {
builder.init() builder.init(0)
} }
if builder.media { if builder.media {
builder.buffer.WriteString(`\t`) builder.buffer.WriteString(`\t`)
@ -276,7 +278,7 @@ func (builder *cssStyleBuilder) addValues(key, separator string, values ...strin
} }
if builder.buffer == nil { if builder.buffer == nil {
builder.init() builder.init(0)
} }
if builder.media { if builder.media {
builder.buffer.WriteString(`\t`) builder.buffer.WriteString(`\t`)

View File

@ -1,20 +1,29 @@
package rui package rui
import "strings" import (
"iter"
"strings"
)
// CustomView defines a custom view interface // CustomView defines a custom view interface
type CustomView interface { type CustomView interface {
ViewsContainer ViewsContainer
// CreateSuperView must be implemented to create a base view from which custom control has been built
CreateSuperView(session Session) View CreateSuperView(session Session) View
// SuperView must be implemented to return a base view from which custom control has been built
SuperView() View SuperView() View
setSuperView(view View) setSuperView(view View)
setTag(tag string) setTag(tag string)
} }
// CustomViewData defines a data of a basic custom view // CustomViewData defines a data of a basic custom view
type CustomViewData struct { type CustomViewData struct {
tag string tag string
superView View superView View
defaultParams Params
} }
// InitCustomView initializes fields of CustomView by default values // InitCustomView initializes fields of CustomView by default values
@ -30,6 +39,9 @@ func InitCustomView(customView CustomView, tag string, session Session, params P
return true return true
} }
func (customView *CustomViewData) init(session Session) {
}
// SuperView returns a super view // SuperView returns a super view
func (customView *CustomViewData) SuperView() View { func (customView *CustomViewData) SuperView() View {
return customView.superView return customView.superView
@ -37,6 +49,12 @@ func (customView *CustomViewData) SuperView() View {
func (customView *CustomViewData) setSuperView(view View) { func (customView *CustomViewData) setSuperView(view View) {
customView.superView = view customView.superView = view
customView.defaultParams = Params{}
for tag, value := range view.All() {
if value != nil {
customView.defaultParams[tag] = value
}
}
} }
func (customView *CustomViewData) setTag(tag string) { func (customView *CustomViewData) setTag(tag string) {
@ -45,52 +63,71 @@ func (customView *CustomViewData) setTag(tag string) {
// Get returns a value of the property with name defined by the argument. // Get returns a value of the property with name defined by the argument.
// The type of return value depends on the property. If the property is not set then nil is returned. // The type of return value depends on the property. If the property is not set then nil is returned.
func (customView *CustomViewData) Get(tag string) any { func (customView *CustomViewData) Get(tag PropertyName) any {
return customView.superView.Get(tag) return customView.superView.Get(tag)
} }
func (customView *CustomViewData) getRaw(tag string) any { func (customView *CustomViewData) getRaw(tag PropertyName) any {
return customView.superView.getRaw(tag) return customView.superView.getRaw(tag)
} }
func (customView *CustomViewData) setRaw(tag string, value any) { func (customView *CustomViewData) setRaw(tag PropertyName, value any) {
customView.superView.setRaw(tag, value) customView.superView.setRaw(tag, value)
} }
func (customView *CustomViewData) setContent(value any) bool {
if container, ok := customView.superView.(ViewsContainer); ok {
return container.setContent(value)
}
return false
}
// Set sets the value (second argument) of the property with name defined by the first argument. // Set sets the value (second argument) of the property with name defined by the first argument.
// Return "true" if the value has been set, in the opposite case "false" are returned and // Return "true" if the value has been set, in the opposite case "false" are returned and
// a description of the error is written to the log // a description of the error is written to the log
func (customView *CustomViewData) Set(tag string, value any) bool { func (customView *CustomViewData) Set(tag PropertyName, value any) bool {
return customView.superView.Set(tag, value) return customView.superView.Set(tag, value)
} }
func (customView *CustomViewData) SetAnimated(tag string, value any, animation Animation) bool { // SetAnimated sets the value (second argument) of the property with name defined by the first argument.
// Return "true" if the value has been set, in the opposite case "false" are returned and
// a description of the error is written to the log
func (customView *CustomViewData) SetAnimated(tag PropertyName, value any, animation AnimationProperty) bool {
return customView.superView.SetAnimated(tag, value, animation) return customView.superView.SetAnimated(tag, value, animation)
} }
func (customView *CustomViewData) SetChangeListener(tag string, listener func(View, string)) { func (customView *CustomViewData) SetParams(params Params) bool {
customView.superView.SetChangeListener(tag, listener) return customView.superView.SetParams(params)
}
// SetChangeListener set the function to track the change of the View property
func (customView *CustomViewData) SetChangeListener(tag PropertyName, listener any) bool {
return customView.superView.SetChangeListener(tag, listener)
} }
// Remove removes the property with name defined by the argument // Remove removes the property with name defined by the argument
func (customView *CustomViewData) Remove(tag string) { func (customView *CustomViewData) Remove(tag PropertyName) {
customView.superView.Remove(tag) customView.superView.Remove(tag)
} }
// AllTags returns an array of the set properties func (customView *CustomViewData) AllTags() []PropertyName {
func (customView *CustomViewData) AllTags() []string {
return customView.superView.AllTags() return customView.superView.AllTags()
} }
// AllTags returns an array of the set properties
func (customView *CustomViewData) All() iter.Seq2[PropertyName, any] {
return customView.superView.All()
}
func (customView *CustomViewData) IsEmpty() bool {
return customView.superView.IsEmpty()
}
// Clear removes all properties // Clear removes all properties
func (customView *CustomViewData) Clear() { func (customView *CustomViewData) Clear() {
customView.superView.Clear() customView.superView.Clear()
} }
func (customView *CustomViewData) cssViewStyle(buffer cssBuilder, session Session) {
customView.superView.cssViewStyle(buffer, session)
}
// Session returns a current Session interface // Session returns a current Session interface
func (customView *CustomViewData) Session() Session { func (customView *CustomViewData) Session() Session {
return customView.superView.Session() return customView.superView.Session()
@ -144,10 +181,12 @@ func (customView *CustomViewData) Frame() Frame {
return customView.superView.Frame() return customView.superView.Frame()
} }
// Scroll returns a location and size of a scrollable view in pixels
func (customView *CustomViewData) Scroll() Frame { func (customView *CustomViewData) Scroll() Frame {
return customView.superView.Scroll() return customView.superView.Scroll()
} }
// HasFocus returns "true" if the view has focus
func (customView *CustomViewData) HasFocus() bool { func (customView *CustomViewData) HasFocus() bool {
return customView.superView.HasFocus() return customView.superView.HasFocus()
} }
@ -160,7 +199,7 @@ func (customView *CustomViewData) onItemResize(self View, index string, x, y, wi
customView.superView.onItemResize(customView.superView, index, x, y, width, height) customView.superView.onItemResize(customView.superView, index, x, y, width, height)
} }
func (customView *CustomViewData) handleCommand(self View, command string, data DataObject) bool { func (customView *CustomViewData) handleCommand(self View, command PropertyName, data DataObject) bool {
return customView.superView.handleCommand(customView.superView, command, data) return customView.superView.handleCommand(customView.superView, command, data)
} }
@ -188,8 +227,8 @@ func (customView *CustomViewData) htmlProperties(self View, buffer *strings.Buil
customView.superView.htmlProperties(customView.superView, buffer) customView.superView.htmlProperties(customView.superView, buffer)
} }
func (customView *CustomViewData) htmlDisabledProperties(self View, buffer *strings.Builder) { func (customView *CustomViewData) htmlDisabledProperty() bool {
customView.superView.htmlDisabledProperties(customView.superView, buffer) return customView.superView.htmlDisabledProperty()
} }
func (customView *CustomViewData) cssStyle(self View, builder cssBuilder) { func (customView *CustomViewData) cssStyle(self View, builder cssBuilder) {
@ -243,13 +282,48 @@ func (customView *CustomViewData) RemoveView(index int) View {
return container.RemoveView(index) return container.RemoveView(index)
} }
} }
return nil return nil
} }
func (customView *CustomViewData) RemoveViewByID(id string) View {
if customView.superView != nil {
if container, ok := customView.superView.(ViewsContainer); ok {
return container.RemoveViewByID(id)
}
}
return nil
}
// Remove removes a view from the list of a view children and return it
func (customView *CustomViewData) ViewIndex(view View) int {
if customView.superView != nil {
if container, ok := customView.superView.(ViewsContainer); ok {
return container.ViewIndex(view)
}
}
return -1
}
func (customView *CustomViewData) excludeTags() []PropertyName {
if customView.superView != nil {
exclude := []PropertyName{}
for tag, value := range customView.defaultParams {
if value == customView.superView.getRaw(tag) {
exclude = append(exclude, tag)
}
}
return exclude
}
return nil
}
// String convert internal representation of a [CustomViewData] into a string.
func (customView *CustomViewData) String() string { func (customView *CustomViewData) String() string {
if customView.superView != nil { if customView.superView != nil {
return getViewString(customView) buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
writeViewStyle(customView.tag, customView, buffer, "", customView.excludeTags())
return buffer.String()
} }
return customView.tag + " { }" return customView.tag + " { }"
} }
@ -260,22 +334,40 @@ func (customView *CustomViewData) setScroll(x, y, width, height float64) {
} }
} }
func (customView *CustomViewData) Transition(tag string) Animation { // Transition returns the transition animation of the property(tag). Returns nil is there is no transition animation.
func (customView *CustomViewData) Transition(tag PropertyName) AnimationProperty {
if customView.superView != nil { if customView.superView != nil {
return customView.superView.Transition(tag) return customView.superView.Transition(tag)
} }
return nil return nil
} }
func (customView *CustomViewData) Transitions() map[string]Animation { // Transitions returns a map of transition animations. The result is always non-nil.
func (customView *CustomViewData) Transitions() map[PropertyName]AnimationProperty {
if customView.superView != nil { if customView.superView != nil {
return customView.superView.Transitions() return customView.superView.Transitions()
} }
return map[string]Animation{} return map[PropertyName]AnimationProperty{}
} }
func (customView *CustomViewData) SetTransition(tag string, animation Animation) { // SetTransition sets the transition animation for the property if "animation" argument is not nil, and
// removes the transition animation of the property if "animation" argument is nil.
// The "tag" argument is the property name.
func (customView *CustomViewData) SetTransition(tag PropertyName, animation AnimationProperty) {
if customView.superView != nil { if customView.superView != nil {
customView.superView.SetTransition(tag, animation) customView.superView.SetTransition(tag, animation)
} }
} }
func (customView *CustomViewData) LoadFile(file FileInfo, result func(FileInfo, []byte)) {
if customView.superView != nil {
customView.superView.LoadFile(file, result)
}
}
func (customView *CustomViewData) binding() any {
if customView.superView != nil {
return customView.superView.binding()
}
return nil
}

984
data.go

File diff suppressed because it is too large Load Diff

324
dataList.go Normal file
View File

@ -0,0 +1,324 @@
package rui
import (
"fmt"
"strconv"
"strings"
"time"
)
const (
// DataList is the constant for "data-list" property tag.
//
// Used by ColorPicker, DatePicker, EditView, NumberPicker, TimePicker.
//
// # Usage in ColorPicker
//
// List of pre-defined colors.
//
// Supported types: []string, string, []fmt.Stringer, []Color, []any containing
// elements of string, fmt.Stringer, int, int8…int64, uint, uint8…uint64.
//
// Internal type is []string, other types converted to it during assignment.
//
// Conversion rules:
// - string - contain single item.
// - []string - an array of items.
// - []fmt.Stringer - an array of objects convertible to a string.
// - []Color - An array of color values which will be converted to a string array.
// - []any - this array must contain only types which were listed in Types section.
//
// # Usage in DatePicker
//
// List of predefined dates. If we set this property, date picker may have a drop-down menu with a list of these values.
// Some browsers may ignore this property, such as Safari for macOS. The value of this property must be an array of
// strings in the format "YYYY-MM-DD".
//
// Supported types: []string, string, []fmt.Stringer, []time.Time, []any containing elements of string, fmt.Stringer, time.Time.
//
// Internal type is []string, other types converted to it during assignment.
//
// Conversion rules:
// - string - contain single item.
// - []string - an array of items.
// - []fmt.Stringer - an array of objects convertible to a string.
// - []time.Time - an array of Time values which will be converted to a string array.
// - []any - this array must contain only types which were listed in Types section.
//
// # Usage in EditView
//
// Array of recommended values.
//
// Supported types: []string, string, []fmt.Stringer, and []any containing
// elements of string, fmt.Stringer, bool, rune, float32, float64, int, int8…int64, uint, uint8…uint64.
//
// Internal type is []string, other types converted to it during assignment.
//
// Conversion rules:
// - string - contain single item.
// - []string - an array of items.
// - []fmt.Stringer - an array of objects convertible to a string.
// - []any - this array must contain only types which were listed in Types section.
//
// # Usage in NumberPicker
//
// Specify an array of recommended values.
//
// Supported types: []string, string, []fmt.Stringer, []float, []int, []bool, []any containing elements
// of string, fmt.Stringer, rune, float32, float64, int, int8…int64, uint, uint8…uint64.
//
// Internal type is []string, other types converted to it during assignment.
//
// Conversion rules:
// - string - must contain integer or floating point number, converted to []string.
// - []string - an array of strings which must contain integer or floating point numbers, stored as is.
// - []fmt.Stringer - object which implement this interface must contain integer or floating point numbers, converted to a []string.
// - []float - converted to []string.
// - []int - converted to []string.
// - []any - an array which may contain types listed in Types section above, each value will be converted to a string and wrapped to array.
//
// # Usage in TimePicker
//
// An array of recommended values. The value of this property must be an array of strings in the format "HH:MM:SS" or
// "HH:MM".
//
// Supported types: []string, string, []fmt.Stringer, []time.Time, []any containing elements of string, fmt.Stringer, time.Time.
//
// Internal type is []string, other types converted to it during assignment.
//
// Conversion rules:
// - string - contain single item.
// - []string - an array of items.
// - []fmt.Stringer - an array of objects convertible to a string.
// - []time.Time - An array of Time values which will be converted to a string array.
// - []any - this array must contain only types which were listed in Types section.
DataList PropertyName = "data-list"
)
func dataListID(view View) string {
return view.htmlID() + "-datalist"
}
func normalizeDataListTag(tag PropertyName) PropertyName {
switch tag {
case "datalist":
return DataList
}
return tag
}
func setDataList(properties Properties, value any, dateTimeFormat string) []PropertyName {
if items, ok := anyToStringArray(value, dateTimeFormat); ok {
properties.setRaw(DataList, items)
return []PropertyName{DataList}
}
notCompatibleType(DataList, value)
return nil
}
func anyToStringArray(value any, dateTimeFormat string) ([]string, bool) {
switch value := value.(type) {
case string:
return []string{value}, true
case []string:
return value, true
case []DataValue:
items := make([]string, 0, len(value))
for _, val := range value {
if !val.IsObject() {
items = append(items, val.Value())
}
}
return items, true
case []fmt.Stringer:
items := make([]string, len(value))
for i, str := range value {
items[i] = str.String()
}
return items, true
case []Color:
items := make([]string, len(value))
for i, str := range value {
items[i] = str.String()
}
return items, true
case []SizeUnit:
items := make([]string, len(value))
for i, str := range value {
items[i] = str.String()
}
return items, true
case []AngleUnit:
items := make([]string, len(value))
for i, str := range value {
items[i] = str.String()
}
return items, true
case []float32:
items := make([]string, len(value))
for i, val := range value {
items[i] = fmt.Sprintf("%g", float64(val))
}
return items, true
case []float64:
items := make([]string, len(value))
for i, val := range value {
items[i] = fmt.Sprintf("%g", val)
}
return items, true
case []int:
return intArrayToStringArray(value), true
case []uint:
return intArrayToStringArray(value), true
case []int8:
return intArrayToStringArray(value), true
case []uint8:
return intArrayToStringArray(value), true
case []int16:
return intArrayToStringArray(value), true
case []uint16:
return intArrayToStringArray(value), true
case []int32:
return intArrayToStringArray(value), true
case []uint32:
return intArrayToStringArray(value), true
case []int64:
return intArrayToStringArray(value), true
case []uint64:
return intArrayToStringArray(value), true
case []bool:
items := make([]string, len(value))
for i, val := range value {
if val {
items[i] = "true"
} else {
items[i] = "false"
}
}
return items, true
case []time.Time:
if dateTimeFormat == "" {
dateTimeFormat = dateFormat + " " + timeFormat
}
items := make([]string, len(value))
for i, val := range value {
items[i] = val.Format(dateTimeFormat)
}
return items, true
case []any:
items := make([]string, 0, len(value))
for _, v := range value {
switch val := v.(type) {
case string:
items = append(items, val)
case fmt.Stringer:
items = append(items, val.String())
case bool:
if val {
items = append(items, "true")
} else {
items = append(items, "false")
}
case float32:
items = append(items, fmt.Sprintf("%g", float64(val)))
case float64:
items = append(items, fmt.Sprintf("%g", val))
case rune:
items = append(items, string(val))
default:
if n, ok := isInt(v); ok {
items = append(items, strconv.Itoa(n))
} else {
return []string{}, false
}
}
}
return items, true
}
return []string{}, false
}
func getDataListProperty(properties Properties) []string {
if value := properties.getRaw(DataList); value != nil {
if items, ok := value.([]string); ok {
return items
}
}
return nil
}
func dataListHtmlSubviews(view View, buffer *strings.Builder, normalizeItem func(text string, session Session) string) {
if items := getDataListProperty(view); len(items) > 0 {
session := view.Session()
buffer.WriteString(`<datalist id="`)
buffer.WriteString(dataListID(view))
buffer.WriteString(`">`)
for _, text := range items {
text = normalizeItem(text, session)
if strings.ContainsRune(text, '"') {
text = strings.ReplaceAll(text, `"`, `&#34;`)
}
if strings.ContainsRune(text, '\n') {
text = strings.ReplaceAll(text, "\n", `\n`)
}
buffer.WriteString(`<option value="`)
buffer.WriteString(text)
buffer.WriteString(`"></option>`)
}
buffer.WriteString(`</datalist>`)
}
}
func dataListHtmlProperties(view View, buffer *strings.Builder) {
if len(getDataListProperty(view)) > 0 {
buffer.WriteString(` list="`)
buffer.WriteString(dataListID(view))
buffer.WriteString(`"`)
}
}
// GetDataList returns the data list of an editor.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDataList(view View, subviewID ...string) []string {
if view = getSubview(view, subviewID); view != nil {
return getDataListProperty(view)
}
return []string{}
}

View File

@ -6,10 +6,6 @@ import (
func TestParseDataText(t *testing.T) { func TestParseDataText(t *testing.T) {
SetErrorLog(func(text string) {
t.Error(text)
})
text := `obj1 { text := `obj1 {
key1 = val1, key1 = val1,
key2=obj2{ key2=obj2{
@ -27,8 +23,10 @@ func TestParseDataText(t *testing.T) {
key3 = "\n \t \\ \r \" ' \X4F\x4e \U01Ea",` + key3 = "\n \t \\ \r \" ' \X4F\x4e \U01Ea",` +
"key4=`" + `\n \t \\ \r \" ' \x8F \UF80a` + "`\r}" "key4=`" + `\n \t \\ \r \" ' \x8F \UF80a` + "`\r}"
obj := ParseDataText(text) obj, err := ParseDataText(text)
if obj != nil { if err != nil {
t.Error(err)
} else {
if obj.Tag() != "obj1" { if obj.Tag() != "obj1" {
t.Error(`obj.Tag() != "obj1"`) t.Error(`obj.Tag() != "obj1"`)
} }
@ -75,7 +73,7 @@ func TestParseDataText(t *testing.T) {
t.Errorf(`obj.PropertyValue("key5") result: ("%s",%v)`, val, ok) t.Errorf(`obj.PropertyValue("key5") result: ("%s",%v)`, val, ok)
} }
testKey := func(obj DataObject, index int, tag string, nodeType int) DataNode { testKey := func(obj DataObject, index int, tag string, nodeType DataNodeType) DataNode {
key := obj.Property(index) key := obj.Property(index)
if key == nil { if key == nil {
t.Errorf(`%s.Property(%d) == nil`, obj.Tag(), index) t.Errorf(`%s.Property(%d) == nil`, obj.Tag(), index)
@ -118,7 +116,7 @@ func TestParseDataText(t *testing.T) {
type testKeyData struct { type testKeyData struct {
tag string tag string
nodeType int nodeType DataNodeType
} }
data := []testKeyData{ data := []testKeyData{
@ -173,9 +171,6 @@ func TestParseDataText(t *testing.T) {
} }
} }
SetErrorLog(func(text string) {
})
failText := []string{ failText := []string{
" ", " ",
"obj[]", "obj[]",
@ -204,7 +199,7 @@ func TestParseDataText(t *testing.T) {
} }
for _, txt := range failText { for _, txt := range failText {
if obj := ParseDataText(txt); obj != nil { if _, err := ParseDataText(txt); err == nil {
t.Errorf("result ParseDataText(\"%s\") must be fail", txt) t.Errorf("result ParseDataText(\"%s\") must be fail", txt)
} }
} }

View File

@ -1,29 +1,124 @@
package rui package rui
import ( import (
"fmt"
"strconv" "strconv"
"strings" "strings"
"time" "time"
) )
// Constants for [DatePicker] specific properties and events.
const ( const (
DateChangedEvent = "date-changed" // DateChangedEvent is the constant for "date-changed" property tag.
DatePickerMin = "date-picker-min" //
DatePickerMax = "date-picker-max" // Used by DatePicker.
DatePickerStep = "date-picker-step" // Occur when date picker value has been changed.
DatePickerValue = "date-picker-value" //
dateFormat = "2006-01-02" // General listener format:
// func(picker rui.DatePicker, newDate time.Time, oldDate time.Time)
//
// where:
// - picker - Interface of a date picker which generated this event,
// - newDate - New date value,
// - oldDate - Old date value.
//
// Allowed listener formats:
// func(picker rui.DatePicker, newDate time.Time)
// func(newDate time.Time, oldDate time.Time)
// func(newDate time.Time)
// func(picker rui.DatePicker)
// func()
DateChangedEvent PropertyName = "date-changed"
// DatePickerMin is the constant for "date-picker-min" property tag.
//
// Used by DatePicker.
// Minimum date value.
//
// Supported types: time.Time, string.
//
// Internal type is time.Time, other types converted to it during assignment.
//
// Conversion rules:
// string - values of this type parsed and converted to time.Time. The following formats are supported:
// - "YYYYMMDD" - "20240102".
// - "Mon-DD-YYYY" - "Jan-02-24".
// - "Mon-DD-YY" - "Jan-02-2024".
// - "DD-Mon-YYYY" - "02-Jan-2024".
// - "YYYY-MM-DD" - "2024-01-02".
// - "Month DD, YYYY" - "January 02, 2024".
// - "DD Month YYYY" - "02 January 2024".
// - "MM/DD/YYYY" - "01/02/2024".
// - "MM/DD/YY" - "01/02/24".
// - "MMDDYY" - "010224".
DatePickerMin PropertyName = "date-picker-min"
// DatePickerMax is the constant for "date-picker-max" property tag.
//
// Used by DatePicker.
// Maximum date value.
//
// Supported types: time.Time, string.
//
// Internal type is time.Time, other types converted to it during assignment.
//
// Conversion rules:
// string - values of this type parsed and converted to time.Time. The following formats are supported:
// - "YYYYMMDD" - "20240102".
// - "Mon-DD-YYYY" - "Jan-02-24".
// - "Mon-DD-YY" - "Jan-02-2024".
// - "DD-Mon-YYYY" - "02-Jan-2024".
// - "YYYY-MM-DD" - "2024-01-02".
// - "Month DD, YYYY" - "January 02, 2024".
// - "DD Month YYYY" - "02 January 2024".
// - "MM/DD/YYYY" - "01/02/2024".
// - "MM/DD/YY" - "01/02/24".
// - "MMDDYY" - "010224".
DatePickerMax PropertyName = "date-picker-max"
// DatePickerStep is the constant for "date-picker-step" property tag.
//
// Used by DatePicker.
// Date change step in days.
//
// Supported types: int, string.
//
// Values:
// positive value - Step value in days used to increment or decrement date.
DatePickerStep PropertyName = "date-picker-step"
// DatePickerValue is the constant for "date-picker-value" property tag.
//
// Used by DatePicker.
// Current value.
//
// Supported types: time.Time, string.
//
// Internal type is time.Time, other types converted to it during assignment.
//
// Conversion rules:
// string - values of this type parsed and converted to time.Time. The following formats are supported:
// - "YYYYMMDD" - "20240102".
// - "Mon-DD-YYYY" - "Jan-02-24".
// - "Mon-DD-YY" - "Jan-02-2024".
// - "DD-Mon-YYYY" - "02-Jan-2024".
// - "YYYY-MM-DD" - "2024-01-02".
// - "Month DD, YYYY" - "January 02, 2024".
// - "DD Month YYYY" - "02 January 2024".
// - "MM/DD/YYYY" - "01/02/2024".
// - "MM/DD/YY" - "01/02/24".
// - "MMDDYY" - "010224".
DatePickerValue PropertyName = "date-picker-value"
dateFormat = "2006-01-02"
) )
// DatePicker - DatePicker view // DatePicker represent a DatePicker view
type DatePicker interface { type DatePicker interface {
View View
} }
type datePickerData struct { type datePickerData struct {
viewData viewData
dateChangedListeners []func(DatePicker, time.Time)
} }
// NewDatePicker create new DatePicker object and return it // NewDatePicker create new DatePicker object and return it
@ -35,234 +130,176 @@ func NewDatePicker(session Session, params Params) DatePicker {
} }
func newDatePicker(session Session) View { func newDatePicker(session Session) View {
return NewDatePicker(session, nil) return new(datePickerData) // NewDatePicker(session, nil)
} }
func (picker *datePickerData) init(session Session) { func (picker *datePickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "DatePicker" picker.tag = "DatePicker"
picker.dateChangedListeners = []func(DatePicker, time.Time){} picker.hasHtmlDisabled = true
} picker.normalize = normalizeDatePickerTag
picker.set = picker.setFunc
func (picker *datePickerData) String() string { picker.get = picker.getFunc
return getViewString(picker) picker.changed = picker.propertyChanged
} }
func (picker *datePickerData) Focusable() bool { func (picker *datePickerData) Focusable() bool {
return true return true
} }
func (picker *datePickerData) normalizeTag(tag string) string { func normalizeDatePickerTag(tag PropertyName) PropertyName {
tag = strings.ToLower(tag) tag = defaultNormalize(tag)
switch tag { switch tag {
case Type, Min, Max, Step, Value: case Type, Min, Max, Step, Value:
return "date-picker-" + tag return "date-picker-" + tag
} }
return tag return normalizeDataListTag(tag)
} }
func (picker *datePickerData) Remove(tag string) { func stringToDate(value string) (time.Time, bool) {
picker.remove(picker.normalizeTag(tag)) format := "20060102"
if strings.ContainsRune(value, '-') {
if part := strings.Split(value, "-"); len(part) == 3 {
if part[0] != "" && part[0][0] > '9' {
if len(part[2]) == 2 {
format = "Jan-02-06"
} else {
format = "Jan-02-2006"
}
} else if part[1] != "" && part[1][0] > '9' {
format = "02-Jan-2006"
} else {
format = "2006-01-02"
}
}
} else if strings.ContainsRune(value, ' ') {
if part := strings.Split(value, " "); len(part) == 3 {
if part[0] != "" && part[0][0] > '9' {
format = "January 02, 2006"
} else {
format = "02 January 2006"
}
}
} else if strings.ContainsRune(value, '/') {
if part := strings.Split(value, "/"); len(part) == 3 {
if len(part[2]) == 2 {
format = "01/02/06"
} else {
format = "01/02/2006"
}
}
} else if len(value) == 6 {
format = "010206"
}
if date, err := time.Parse(format, value); err == nil {
return date, true
}
return time.Now(), false
} }
func (picker *datePickerData) remove(tag string) { func (picker *datePickerData) getFunc(tag PropertyName) any {
switch tag { switch tag {
case DateChangedEvent: case DateChangedEvent:
if len(picker.dateChangedListeners) > 0 { if listeners := getTwoArgEventRawListeners[DatePicker, time.Time](picker, nil, tag); len(listeners) > 0 {
picker.dateChangedListeners = []func(DatePicker, time.Time){} return listeners
picker.propertyChangedEvent(tag)
} }
return return nil
case DatePickerMin:
delete(picker.properties, DatePickerMin)
if picker.created {
removeProperty(picker.htmlID(), Min, picker.session)
}
case DatePickerMax:
delete(picker.properties, DatePickerMax)
if picker.created {
removeProperty(picker.htmlID(), Max, picker.session)
}
case DatePickerStep:
delete(picker.properties, DatePickerStep)
if picker.created {
removeProperty(picker.htmlID(), Step, picker.session)
}
case DatePickerValue:
if _, ok := picker.properties[DatePickerValue]; ok {
delete(picker.properties, DatePickerValue)
date := GetDatePickerValue(picker)
if picker.created {
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, picker.htmlID(), date.Format(dateFormat)))
}
for _, listener := range picker.dateChangedListeners {
listener(picker, date)
}
} else {
return
}
default:
picker.viewData.remove(tag)
return
} }
picker.propertyChangedEvent(tag) return picker.viewData.getFunc(tag)
} }
func (picker *datePickerData) Set(tag string, value any) bool { func (picker *datePickerData) setFunc(tag PropertyName, value any) []PropertyName {
return picker.set(picker.normalizeTag(tag), value)
}
func (picker *datePickerData) set(tag string, value any) bool { setDateValue := func(tag PropertyName) []PropertyName {
if value == nil {
picker.remove(tag)
return true
}
setTimeValue := func(tag string) (time.Time, bool) {
switch value := value.(type) { switch value := value.(type) {
case time.Time: case time.Time:
picker.properties[tag] = value picker.setRaw(tag, value)
return value, true return []PropertyName{tag}
case string: case string:
if text, ok := picker.Session().resolveConstants(value); ok { if ok, _ := isConstantName(value); ok {
format := "20060102" picker.setRaw(tag, value)
if strings.ContainsRune(text, '-') { return []PropertyName{tag}
if part := strings.Split(text, "-"); len(part) == 3 { }
if part[0] != "" && part[0][0] > '9' {
if len(part[2]) == 2 {
format = "Jan-02-06"
} else {
format = "Jan-02-2006"
}
} else if part[1] != "" && part[1][0] > '9' {
format = "02-Jan-2006"
} else {
format = "2006-01-02"
}
}
} else if strings.ContainsRune(text, ' ') {
if part := strings.Split(text, " "); len(part) == 3 {
if part[0] != "" && part[0][0] > '9' {
format = "January 02, 2006"
} else {
format = "02 January 2006"
}
}
} else if strings.ContainsRune(text, '/') {
if part := strings.Split(text, "/"); len(part) == 3 {
if len(part[2]) == 2 {
format = "01/02/06"
} else {
format = "01/02/2006"
}
}
} else if len(text) == 6 {
format = "010206"
}
if date, err := time.Parse(format, text); err == nil { if date, ok := stringToDate(value); ok {
picker.properties[tag] = value picker.setRaw(tag, date)
return date, true return []PropertyName{tag}
}
} }
} }
notCompatibleType(tag, value) notCompatibleType(tag, value)
return time.Now(), false return nil
} }
switch tag { switch tag {
case DatePickerMin, DatePickerMax:
return setDateValue(tag)
case DatePickerStep:
return setIntProperty(picker, DatePickerStep, value)
case DatePickerValue:
picker.setRaw("old-date", GetDatePickerValue(picker))
return setDateValue(tag)
case DateChangedEvent:
return setTwoArgEventListener[DatePicker, time.Time](picker, tag, value)
case DataList:
return setDataList(picker, value, dateFormat)
}
return picker.viewData.setFunc(tag, value)
}
func (picker *datePickerData) propertyChanged(tag PropertyName) {
session := picker.Session()
switch tag {
case DatePickerMin: case DatePickerMin:
old, oldOK := getDateProperty(picker, DatePickerMin, Min) if date, ok := GetDatePickerMin(picker); ok {
if date, ok := setTimeValue(DatePickerMin); ok { session.updateProperty(picker.htmlID(), "min", date.Format(dateFormat))
if !oldOK || date != old { } else {
if picker.created { session.removeProperty(picker.htmlID(), "min")
updateProperty(picker.htmlID(), Min, date.Format(dateFormat), picker.session)
}
picker.propertyChangedEvent(tag)
}
return true
} }
case DatePickerMax: case DatePickerMax:
old, oldOK := getDateProperty(picker, DatePickerMax, Max) if date, ok := GetDatePickerMax(picker); ok {
if date, ok := setTimeValue(DatePickerMax); ok { session.updateProperty(picker.htmlID(), "max", date.Format(dateFormat))
if !oldOK || date != old { } else {
if picker.created { session.removeProperty(picker.htmlID(), "max")
updateProperty(picker.htmlID(), Max, date.Format(dateFormat), picker.session)
}
picker.propertyChangedEvent(tag)
}
return true
} }
case DatePickerStep: case DatePickerStep:
oldStep := GetDatePickerStep(picker) if step := GetDatePickerStep(picker); step > 0 {
if picker.setIntProperty(DatePickerStep, value) { session.updateProperty(picker.htmlID(), "step", strconv.Itoa(step))
if step := GetDatePickerStep(picker); oldStep != step { } else {
if picker.created { session.removeProperty(picker.htmlID(), "step")
if step > 0 {
updateProperty(picker.htmlID(), Step, strconv.Itoa(step), picker.session)
} else {
removeProperty(picker.htmlID(), Step, picker.session)
}
}
picker.propertyChangedEvent(tag)
}
return true
} }
case DatePickerValue: case DatePickerValue:
oldDate := GetDatePickerValue(picker) date := GetDatePickerValue(picker)
if date, ok := setTimeValue(DatePickerValue); ok { session.callFunc("setInputValue", picker.htmlID(), date.Format(dateFormat))
if date != oldDate {
if picker.created { if listeners := getTwoArgEventListeners[DatePicker, time.Time](picker, nil, DateChangedEvent); len(listeners) > 0 {
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, picker.htmlID(), date.Format(dateFormat))) oldDate := time.Now()
if value := picker.getRaw("old-date"); value != nil {
if date, ok := value.(time.Time); ok {
oldDate = date
} }
for _, listener := range picker.dateChangedListeners {
listener(picker, date)
}
picker.propertyChangedEvent(tag)
} }
return true for _, listener := range listeners {
listener.Run(picker, date, oldDate)
}
} }
case DateChangedEvent:
listeners, ok := valueToEventListeners[DatePicker, time.Time](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(DatePicker, time.Time){}
}
picker.dateChangedListeners = listeners
picker.propertyChangedEvent(tag)
return true
default: default:
return picker.viewData.set(tag, value) picker.viewData.propertyChanged(tag)
}
return false
}
func (picker *datePickerData) Get(tag string) any {
return picker.get(picker.normalizeTag(tag))
}
func (picker *datePickerData) get(tag string) any {
switch tag {
case DateChangedEvent:
return picker.dateChangedListeners
default:
return picker.viewData.get(tag)
} }
} }
@ -270,6 +307,16 @@ func (picker *datePickerData) htmlTag() string {
return "input" return "input"
} }
func (picker *datePickerData) htmlSubviews(self View, buffer *strings.Builder) {
dataListHtmlSubviews(self, buffer, func(text string, session Session) string {
text, _ = session.resolveConstants(text)
if date, ok := stringToDate(text); ok {
return date.Format(dateFormat)
}
return text
})
}
func (picker *datePickerData) htmlProperties(self View, buffer *strings.Builder) { func (picker *datePickerData) htmlProperties(self View, buffer *strings.Builder) {
picker.viewData.htmlProperties(self, buffer) picker.viewData.htmlProperties(self, buffer)
@ -301,16 +348,11 @@ func (picker *datePickerData) htmlProperties(self View, buffer *strings.Builder)
if picker.getRaw(ClickEvent) == nil { if picker.getRaw(ClickEvent) == nil {
buffer.WriteString(` onclick="stopEventPropagation(this, event)"`) buffer.WriteString(` onclick="stopEventPropagation(this, event)"`)
} }
dataListHtmlProperties(picker, buffer)
} }
func (picker *datePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) { func (picker *datePickerData) handleCommand(self View, command PropertyName, data DataObject) bool {
if IsDisabled(self) {
buffer.WriteString(` disabled`)
}
picker.viewData.htmlDisabledProperties(self, buffer)
}
func (picker *datePickerData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "textChanged": case "textChanged":
if text, ok := data.PropertyValue("text"); ok { if text, ok := data.PropertyValue("text"); ok {
@ -318,9 +360,10 @@ func (picker *datePickerData) handleCommand(self View, command string, data Data
oldValue := GetDatePickerValue(picker) oldValue := GetDatePickerValue(picker)
picker.properties[DatePickerValue] = value picker.properties[DatePickerValue] = value
if value != oldValue { if value != oldValue {
for _, listener := range picker.dateChangedListeners { for _, listener := range getTwoArgEventListeners[DatePicker, time.Time](picker, nil, DateChangedEvent) {
listener(picker, value) listener.Run(picker, value, oldValue)
} }
picker.runChangeListener(DatePickerValue)
} }
} }
} }
@ -330,7 +373,7 @@ func (picker *datePickerData) handleCommand(self View, command string, data Data
return picker.viewData.handleCommand(self, command, data) return picker.viewData.handleCommand(self, command, data)
} }
func getDateProperty(view View, mainTag, shortTag string) (time.Time, bool) { func getDateProperty(view View, mainTag, shortTag PropertyName) (time.Time, bool) {
valueToTime := func(value any) (time.Time, bool) { valueToTime := func(value any) (time.Time, bool) {
if value != nil { if value != nil {
switch value := value.(type) { switch value := value.(type) {
@ -339,7 +382,7 @@ func getDateProperty(view View, mainTag, shortTag string) (time.Time, bool) {
case string: case string:
if text, ok := view.Session().resolveConstants(value); ok { if text, ok := view.Session().resolveConstants(value); ok {
if result, err := time.Parse(dateFormat, text); err == nil { if result, ok := stringToDate(text); ok {
return result, true return result, true
} }
} }
@ -353,9 +396,11 @@ func getDateProperty(view View, mainTag, shortTag string) (time.Time, bool) {
return result, true return result, true
} }
if value := valueFromStyle(view, shortTag); value != nil { for _, tag := range []PropertyName{mainTag, shortTag} {
if result, ok := valueToTime(value); ok { if value := valueFromStyle(view, tag); value != nil {
return result, true if result, ok := valueToTime(value); ok {
return result, true
}
} }
} }
} }
@ -365,12 +410,11 @@ func getDateProperty(view View, mainTag, shortTag string) (time.Time, bool) {
// GetDatePickerMin returns the min date of DatePicker subview and "true" as the second value if the min date is set, // GetDatePickerMin returns the min date of DatePicker subview and "true" as the second value if the min date is set,
// "false" as the second value otherwise. // "false" as the second value otherwise.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDatePickerMin(view View, subviewID ...string) (time.Time, bool) { func GetDatePickerMin(view View, subviewID ...string) (time.Time, bool) {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0])
}
if view != nil {
return getDateProperty(view, DatePickerMin, Min) return getDateProperty(view, DatePickerMin, Min)
} }
return time.Now(), false return time.Now(), false
@ -378,30 +422,30 @@ func GetDatePickerMin(view View, subviewID ...string) (time.Time, bool) {
// GetDatePickerMax returns the max date of DatePicker subview and "true" as the second value if the min date is set, // GetDatePickerMax returns the max date of DatePicker subview and "true" as the second value if the min date is set,
// "false" as the second value otherwise. // "false" as the second value otherwise.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDatePickerMax(view View, subviewID ...string) (time.Time, bool) { func GetDatePickerMax(view View, subviewID ...string) (time.Time, bool) {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0])
}
if view != nil {
return getDateProperty(view, DatePickerMax, Max) return getDateProperty(view, DatePickerMax, Max)
} }
return time.Now(), false return time.Now(), false
} }
// GetDatePickerStep returns the date changing step in days of DatePicker subview. // GetDatePickerStep returns the date changing step in days of DatePicker subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDatePickerStep(view View, subviewID ...string) int { func GetDatePickerStep(view View, subviewID ...string) int {
return intStyledProperty(view, subviewID, DatePickerStep, 0) return intStyledProperty(view, subviewID, DatePickerStep, 0)
} }
// GetDatePickerValue returns the date of DatePicker subview. // GetDatePickerValue returns the date of DatePicker subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDatePickerValue(view View, subviewID ...string) time.Time { func GetDatePickerValue(view View, subviewID ...string) time.Time {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view == nil {
view = ViewByID(view, subviewID[0])
}
if view == nil {
return time.Now() return time.Now()
} }
date, _ := getDateProperty(view, DatePickerValue, Value) date, _ := getDateProperty(view, DatePickerValue, Value)
@ -410,7 +454,18 @@ func GetDatePickerValue(view View, subviewID ...string) time.Time {
// GetDateChangedListeners returns the DateChangedListener list of an DatePicker subview. // GetDateChangedListeners returns the DateChangedListener list of an DatePicker subview.
// If there are no listeners then the empty list is returned // If there are no listeners then the empty list is returned
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetDateChangedListeners(view View, subviewID ...string) []func(DatePicker, time.Time) { // Result elements can be of the following types:
return getEventListeners[DatePicker, time.Time](view, subviewID, DateChangedEvent) // - func(rui.DatePicker, time.Time, time.Time),
// - func(rui.DatePicker, time.Time),
// - func(rui.DatePicker),
// - func(time.Time, time.Time),
// - func(time.Time),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDateChangedListeners(view View, subviewID ...string) []any {
return getTwoArgEventRawListeners[DatePicker, time.Time](view, subviewID, DateChangedEvent)
} }

View File

@ -23,6 +23,9 @@ theme {
ruiTabTextColor = #FF404040, ruiTabTextColor = #FF404040,
ruiCurrentTabColor = #FFFFFFFF, ruiCurrentTabColor = #FFFFFFFF,
ruiCurrentTabTextColor = #FF000000, ruiCurrentTabTextColor = #FF000000,
ruiTooltipBackground = #FFFFFFFF,
ruiTooltipTextColor = #FF000000,
ruiTooltipShadowColor = #FF808080,
}, },
colors:dark = _{ colors:dark = _{
ruiTextColor = #FFE0E0E0, ruiTextColor = #FFE0E0E0,
@ -43,6 +46,9 @@ theme {
ruiTabTextColor = #FFE0E0E0, ruiTabTextColor = #FFE0E0E0,
ruiCurrentTabColor = #FF000000, ruiCurrentTabColor = #FF000000,
ruiCurrentTabTextColor = #FFFFFFFF, ruiCurrentTabTextColor = #FFFFFFFF,
ruiTooltipBackground = #FF303030,
ruiTooltipTextColor = #FFDDDDDD,
ruiTooltipShadowColor = #FFDDDDDD,
}, },
constants = _{ constants = _{
ruiButtonHorizontalPadding = 16px, ruiButtonHorizontalPadding = 16px,
@ -76,8 +82,7 @@ theme {
background-color = @ruiBackgroundColor, background-color = @ruiBackgroundColor,
accent-color = @ruiHighlightColor, accent-color = @ruiHighlightColor,
}, },
ruiButton { ruiEnabledButton {
align = center,
padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding", padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding",
margin = @ruiButtonMargin, margin = @ruiButtonMargin,
radius = @ruiButtonRadius, radius = @ruiButtonRadius,
@ -86,7 +91,6 @@ theme {
border = _{width = 1px, style = solid, color = @ruiButtonTextColor} border = _{width = 1px, style = solid, color = @ruiButtonTextColor}
}, },
ruiDisabledButton { ruiDisabledButton {
align = center,
padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding", padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding",
margin = @ruiButtonMargin, margin = @ruiButtonMargin,
radius = @ruiButtonRadius, radius = @ruiButtonRadius,
@ -94,14 +98,34 @@ theme {
text-color = @ruiButtonDisabledTextColor, text-color = @ruiButtonDisabledTextColor,
border = _{width = 1px, style = solid, color = @ruiButtonDisabledTextColor} border = _{width = 1px, style = solid, color = @ruiButtonDisabledTextColor}
}, },
ruiButton:hover { ruiEnabledButton:hover {
text-color = @ruiTextColor, text-color = @ruiTextColor,
background-color = @ruiBackgroundColor, background-color = @ruiBackgroundColor,
}, },
ruiButton:focus { ruiEnabledButton:focus {
shadow = _{spread-radius = @ruiButtonHighlightDilation, blur = @ruiButtonHighlightBlur, color = @ruiHighlightColor }, shadow = _{spread-radius = @ruiButtonHighlightDilation, blur = @ruiButtonHighlightBlur, color = @ruiHighlightColor },
}, },
ruiButton:active { ruiEnabledButton:active {
background-color = @ruiButtonActiveColor
},
ruiDefaultButton {
align = center,
padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding",
margin = @ruiButtonMargin,
radius = @ruiButtonRadius,
background-color = @ruiButtonColor,
text-color = @ruiButtonTextColor,
text-weight = bold,
border = _{width = 1px, style = solid, color = @ruiButtonTextColor}
},
ruiDefaultButton:hover {
text-color = @ruiTextColor,
background-color = @ruiBackgroundColor,
},
ruiDefaultButton:focus {
shadow = _{spread-radius = @ruiButtonHighlightDilation, blur = @ruiButtonHighlightBlur, color = @ruiHighlightColor },
},
ruiDefaultButton:active {
background-color = @ruiButtonActiveColor background-color = @ruiButtonActiveColor
}, },
ruiCheckbox { ruiCheckbox {
@ -110,8 +134,8 @@ theme {
margin = 2px, margin = 2px,
}, },
ruiCheckbox:focus { ruiCheckbox:focus {
margin = 0, outline = _{style = solid, color = @ruiHighlightColor, width = 2px },
border = _{style = solid, color = @ruiHighlightColor, width = 2px }, outline-offset = -1px,
}, },
ruiListItem { ruiListItem {
radius = 4px, radius = 4px,

View File

@ -2,17 +2,44 @@ package rui
import "strings" import "strings"
// Constants for [DetailsView] specific properties and events
const ( const (
// Summary is the constant for the "summary" property tag. // Summary is the constant for "summary" property tag.
// The contents of the "summary" property are used as the label for the disclosure widget. //
Summary = "summary" // Used by DetailsView.
// Expanded is the constant for the "expanded" property tag. // The content of this property is used as the label for the disclosure widget.
// If the "expanded" boolean property is "true", then the content of view is visible. //
// If the value is "false" then the content is collapsed. // Supported types:
Expanded = "expanded" // - string - Summary as a text.
// - View - Summary as a view, in this case it can be quite complex if needed.
Summary PropertyName = "summary"
// Expanded is the constant for "expanded" property tag.
//
// Used by DetailsView.
// Controls the content expanded state of the details view. Default value is false.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", or "1" - Content is visible.
// - false, 0, "false", "no", "off", or "0" - Content is collapsed (hidden).
Expanded PropertyName = "expanded"
// HideSummaryMarker is the constant for "hide-summary-marker" property tag.
//
// Used by DetailsView.
// Allows you to hide the summary marker (▶︎). Default value is false.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", or "1" - The summary marker is hidden.
// - false, 0, "false", "no", "off", or "0" - The summary marker is displayed (default value).
HideSummaryMarker PropertyName = "hide-summary-marker"
) )
// DetailsView - collapsible container of View // DetailsView represent a DetailsView view, which is a collapsible container of views
type DetailsView interface { type DetailsView interface {
ViewsContainer ViewsContainer
} }
@ -30,19 +57,21 @@ func NewDetailsView(session Session, params Params) DetailsView {
} }
func newDetailsView(session Session) View { func newDetailsView(session Session) View {
return NewDetailsView(session, nil) return new(detailsViewData)
} }
// Init initialize fields of DetailsView by default values // Init initialize fields of DetailsView by default values
func (detailsView *detailsViewData) init(session Session) { func (detailsView *detailsViewData) init(session Session) {
detailsView.viewsContainerData.init(session) detailsView.viewsContainerData.init(session)
detailsView.tag = "DetailsView" detailsView.tag = "DetailsView"
detailsView.set = detailsView.setFunc
detailsView.changed = detailsView.propertyChanged
//detailsView.systemClass = "ruiDetailsView" //detailsView.systemClass = "ruiDetailsView"
} }
func (detailsView *detailsViewData) Views() []View { func (detailsView *detailsViewData) Views() []View {
views := detailsView.viewsContainerData.Views() views := detailsView.viewsContainerData.Views()
if summary := detailsView.get(Summary); summary != nil { if summary := detailsView.Get(Summary); summary != nil {
switch summary := summary.(type) { switch summary := summary.(type) {
case View: case View:
return append([]View{summary}, views...) return append([]View{summary}, views...)
@ -51,94 +80,53 @@ func (detailsView *detailsViewData) Views() []View {
return views return views
} }
func (detailsView *detailsViewData) Remove(tag string) { func (detailsView *detailsViewData) setFunc(tag PropertyName, value any) []PropertyName {
detailsView.remove(strings.ToLower(tag))
}
func (detailsView *detailsViewData) remove(tag string) {
detailsView.viewsContainerData.remove(tag)
if detailsView.created {
switch tag {
case Summary:
updateInnerHTML(detailsView.htmlID(), detailsView.Session())
case Expanded:
removeProperty(detailsView.htmlID(), "open", detailsView.Session())
}
}
}
func (detailsView *detailsViewData) Set(tag string, value any) bool {
return detailsView.set(strings.ToLower(tag), value)
}
func (detailsView *detailsViewData) set(tag string, value any) bool {
if value == nil {
detailsView.remove(tag)
return true
}
switch tag { switch tag {
case Summary: case Summary:
switch value := value.(type) { switch value := value.(type) {
case string: case string:
detailsView.properties[Summary] = value detailsView.setRaw(Summary, value)
case View: case View:
detailsView.properties[Summary] = value detailsView.setRaw(Summary, value)
value.setParentID(detailsView.htmlID()) value.setParentID(detailsView.htmlID())
case DataObject: case DataObject:
if view := CreateViewFromObject(detailsView.Session(), value); view != nil { if view := CreateViewFromObject(detailsView.Session(), value, nil); view != nil {
detailsView.properties[Summary] = view detailsView.setRaw(Summary, view)
view.setParentID(detailsView.htmlID()) view.setParentID(detailsView.htmlID())
} else { } else {
return false return nil
} }
default: default:
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return nil
}
if detailsView.created {
updateInnerHTML(detailsView.htmlID(), detailsView.Session())
} }
return []PropertyName{tag}
}
return detailsView.viewsContainerData.setFunc(tag, value)
}
func (detailsView *detailsViewData) propertyChanged(tag PropertyName) {
switch tag {
case Summary, HideSummaryMarker:
updateInnerHTML(detailsView.htmlID(), detailsView.Session())
case Expanded: case Expanded:
if !detailsView.setBoolProperty(tag, value) { if IsDetailsExpanded(detailsView) {
notCompatibleType(tag, value) detailsView.Session().updateProperty(detailsView.htmlID(), "open", "")
return false } else {
} detailsView.Session().removeProperty(detailsView.htmlID(), "open")
if detailsView.created {
if IsDetailsExpanded(detailsView) {
updateProperty(detailsView.htmlID(), "open", "", detailsView.Session())
} else {
removeProperty(detailsView.htmlID(), "open", detailsView.Session())
}
} }
case NotTranslate: case NotTranslate:
if !detailsView.viewData.set(tag, value) { updateInnerHTML(detailsView.htmlID(), detailsView.Session())
return false
}
if detailsView.created {
updateInnerHTML(detailsView.htmlID(), detailsView.Session())
}
default: default:
return detailsView.viewsContainerData.Set(tag, value) detailsView.viewsContainerData.propertyChanged(tag)
} }
detailsView.propertyChangedEvent(tag)
return true
}
func (detailsView *detailsViewData) Get(tag string) any {
return detailsView.get(strings.ToLower(tag))
}
func (detailsView *detailsViewData) get(tag string) any {
return detailsView.viewsContainerData.get(tag)
} }
func (detailsView *detailsViewData) htmlTag() string { func (detailsView *detailsViewData) htmlTag() string {
@ -154,31 +142,57 @@ func (detailsView *detailsViewData) htmlProperties(self View, buffer *strings.Bu
} }
func (detailsView *detailsViewData) htmlSubviews(self View, buffer *strings.Builder) { func (detailsView *detailsViewData) htmlSubviews(self View, buffer *strings.Builder) {
summary := false
hidden := IsSummaryMarkerHidden(detailsView)
if value, ok := detailsView.properties[Summary]; ok { if value, ok := detailsView.properties[Summary]; ok {
switch value := value.(type) { switch value := value.(type) {
case string: case string:
if !GetNotTranslate(detailsView) { if !GetNotTranslate(detailsView) {
value, _ = detailsView.session.GetString(value) value, _ = detailsView.session.GetString(value)
} }
buffer.WriteString("<summary>") if hidden {
buffer.WriteString(`<summary class="hiddenMarker">`)
} else {
buffer.WriteString("<summary>")
}
buffer.WriteString(value) buffer.WriteString(value)
buffer.WriteString("</summary>") buffer.WriteString("</summary>")
summary = true
case View: case View:
buffer.WriteString("<summary>") if hidden {
viewHTML(value, buffer) buffer.WriteString(`<summary class="hiddenMarker">`)
buffer.WriteString("</summary>") viewHTML(value, buffer, "")
buffer.WriteString("</summary>")
} else if value.htmlTag() == "div" {
viewHTML(value, buffer, "summary")
} else {
buffer.WriteString(`<summary><div style="display: inline-block;">`)
viewHTML(value, buffer, "")
buffer.WriteString("</div></summary>")
}
summary = true
}
}
if !summary {
if hidden {
buffer.WriteString(`<summary class="hiddenMarker"></summary>`)
} else {
buffer.WriteString("<summary></summary>")
} }
} }
detailsView.viewsContainerData.htmlSubviews(self, buffer) detailsView.viewsContainerData.htmlSubviews(self, buffer)
} }
func (detailsView *detailsViewData) handleCommand(self View, command string, data DataObject) bool { func (detailsView *detailsViewData) handleCommand(self View, command PropertyName, data DataObject) bool {
if command == "details-open" { if command == "details-open" {
if n, ok := dataIntProperty(data, "open"); ok { if n, ok := dataIntProperty(data, "open"); ok {
detailsView.properties[Expanded] = (n != 0) detailsView.properties[Expanded] = (n != 0)
detailsView.propertyChangedEvent(Expanded) detailsView.runChangeListener(Expanded)
} }
return true return true
} }
@ -186,12 +200,11 @@ func (detailsView *detailsViewData) handleCommand(self View, command string, dat
} }
// GetDetailsSummary returns a value of the Summary property of DetailsView. // GetDetailsSummary returns a value of the Summary property of DetailsView.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDetailsSummary(view View, subviewID ...string) View { func GetDetailsSummary(view View, subviewID ...string) View {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.Get(Summary); value != nil { if value := view.Get(Summary); value != nil {
switch value := value.(type) { switch value := value.(type) {
case string: case string:
@ -206,7 +219,17 @@ func GetDetailsSummary(view View, subviewID ...string) View {
} }
// IsDetailsExpanded returns a value of the Expanded property of DetailsView. // IsDetailsExpanded returns a value of the Expanded property of DetailsView.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func IsDetailsExpanded(view View, subviewID ...string) bool { func IsDetailsExpanded(view View, subviewID ...string) bool {
return boolStyledProperty(view, subviewID, Expanded, false) return boolStyledProperty(view, subviewID, Expanded, false)
} }
// IsDetailsExpanded returns a value of the HideSummaryMarker property of DetailsView.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func IsSummaryMarkerHidden(view View, subviewID ...string) bool {
return boolStyledProperty(view, subviewID, HideSummaryMarker, false)
}

70
downloadFile.go Normal file
View File

@ -0,0 +1,70 @@
package rui
import (
"bytes"
"math/rand"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
)
type downloadFile struct {
filename string
path string
data []byte
}
var currentDownloadId = int(rand.Int31())
var downloadFiles = map[string]downloadFile{}
func (session *sessionData) startDownload(file downloadFile) {
currentDownloadId++
id := strconv.Itoa(currentDownloadId)
downloadFiles[id] = file
session.callFunc("startDownload", id, file.filename)
}
func serveDownloadFile(id string, w http.ResponseWriter, r *http.Request) bool {
if file, ok := downloadFiles[id]; ok {
delete(downloadFiles, id)
if file.data != nil {
http.ServeContent(w, r, file.filename, time.Now(), bytes.NewReader(file.data))
return true
} else if _, err := os.Stat(file.path); err == nil {
http.ServeFile(w, r, file.path)
return true
}
}
return false
}
// DownloadFile starts downloading the file on the client side.
func (session *sessionData) DownloadFile(path string) {
if _, err := os.Stat(path); err != nil {
ErrorLog(err.Error())
return
}
_, filename := filepath.Split(path)
session.startDownload(downloadFile{
filename: filename,
path: path,
data: nil,
})
}
// DownloadFileData starts downloading the file on the client side. Arguments specify the name of the downloaded file and its contents
func (session *sessionData) DownloadFileData(filename string, data []byte) {
if data == nil {
ErrorLog("Invalid download data. Must be not nil.")
return
}
session.startDownload(downloadFile{
filename: filename,
path: "",
data: data,
})
}

722
dragAndDrop.go Normal file
View File

@ -0,0 +1,722 @@
package rui
import (
"encoding/base64"
"fmt"
"maps"
"strings"
)
const (
// DragData is the constant for "drag-data" property tag.
//
// Used by View:
//
// Supported types: map[string]string.
DragData PropertyName = "drag-data"
// DragImage is the constant for "drag-image" property tag.
//
// Used by View:
// An url of image to use for the drag feedback image.
//
// Supported type: string.
DragImage PropertyName = "drag-image"
// DragImageXOffset is the constant for "drag-image-x-offset" property tag.
//
// Used by View:
// The horizontal offset in pixels within the drag feedback image.
//
// Supported types: float, int, string.
DragImageXOffset PropertyName = "drag-image-x-offset"
// DragImageYOffset is the constant for "drag-image-y-offset" property tag.
//
// Used by View.
// The vertical offset in pixels within the drag feedback image.
//
// Supported types: float, int, string.
DragImageYOffset PropertyName = "drag-image-y-offset"
// DropEffect is the constant for "drag-effect" property tag.
//
// Used by View.
// Controls the feedback (typically visual) the user is given during a drag and drop operation.
// It will affect which cursor is displayed while dragging. For example, when the user hovers over a target drop element,
// the browser's cursor may indicate which type of operation will occur.
//
// Supported types: int, string.
//
// Values:
// - 0 (DropEffectUndefined) or "undefined" - The property value is not defined (default value).
// - 1 (DropEffectCopy) or "copy" - A copy of the source item may be made at the new location.
// - 2 (DropEffectMove) or "move" - An item may be moved to a new location.
// - 4 (DropEffectLink) or "link" - A link may be established to the source at the new location.
DropEffect PropertyName = "drag-effect"
// DropEffectAllowed is the constant for "drop-effect-allowed" property tag.
//
// Used by View.
// Specifies the effect that is allowed for a drag operation.
// The copy operation is used to indicate that the data being dragged will be copied
// from its present location to the drop location.
// The move operation is used to indicate that the data being dragged will be moved,
// and the link operation is used to indicate that some form of relationship
// or connection will be created between the source and drop locations.
//
// Supported types: int, string.
//
// Values:
// - 0 (DropEffectUndefined) or "undefined" - The property value is not defined (default value). Equivalent to DropEffectAll
// - 1 (DropEffectCopy) or "copy" - A copy of the source item may be made at the new location.
// - 2 (DropEffectMove) or "move" - An item may be moved to a new location.
// - 3 (DropEffectLink) or "link" - A link may be established to the source at the new location.
// - 4 (DropEffectCopyMove) or "copy|move" - A copy or move operation is permitted.
// - 5 (DropEffectCopyLink) or "copy|link" - A copy or link operation is permitted.
// - 6 (DropEffectLinkMove) or "link|move" - A link or move operation is permitted.
// - 7 (DropEffectAll) or "all" or "copy|move|link" - All operations are permitted.
DropEffectAllowed PropertyName = "drag-effect-allowed"
// DragStartEvent is the constant for "drag-start-event" property tag.
//
// Used by View.
// Fired when the user starts dragging an element or text selection.
//
// General listener format:
// func(view rui.View, event rui.DragAndDropEvent).
//
// where:
// - view - Interface of a view which generated this event,
// - event - event parameters.
//
// Allowed listener formats:
// func(view rui.View)
// func(rui.DragAndDropEvent)
// func()
DragStartEvent PropertyName = "drag-start-event"
// DragEndEvent is the constant for "drag-end-event" property tag.
//
// Used by View.
// Fired when a drag operation ends (by releasing a mouse button or hitting the escape key).
//
// General listener format:
// func(view rui.View, event rui.DragAndDropEvent).
//
// where:
// - view - Interface of a view which generated this event,
// - event - event parameters.
//
// Allowed listener formats:
// func(view rui.View)
// func(rui.DragAndDropEvent)
// func()
DragEndEvent PropertyName = "drag-end-event"
// DragEnterEvent is the constant for "drag-enter-event" property tag.
//
// Used by View.
// Fired when a dragged element or text selection enters a valid drop target.
//
// General listener format:
// func(view rui.View, event rui.DragAndDropEvent).
//
// where:
// - view - Interface of a view which generated this event,
// - event - event parameters.
//
// Allowed listener formats:
// func(view rui.View)
// func(rui.DragAndDropEvent)
// func()
DragEnterEvent PropertyName = "drag-enter-event"
// DragLeaveEvent is the constant for "drag-leave-event" property tag.
//
// Used by View.
// Fired when a dragged element or text selection leaves a valid drop target.
//
// General listener format:
// func(view rui.View, event rui.DragAndDropEvent).
//
// where:
// - view - Interface of a view which generated this event,
// - event - event parameters.
//
// Allowed listener formats:
// func(view rui.View)
// func(rui.DragAndDropEvent)
// func()
DragLeaveEvent PropertyName = "drag-leave-event"
// DragOverEvent is the constant for "drag-over-event" property tag.
//
// Used by View.
// Fired when an element or text selection is being dragged over a valid drop target (every few hundred milliseconds).
//
// General listener format:
// func(view rui.View, event rui.DragAndDropEvent).
//
// where:
// - view - Interface of a view which generated this event,
// - event - event parameters.
//
// Allowed listener formats:
// func(view rui.View)
// func(rui.DragAndDropEvent)
// func()
DragOverEvent PropertyName = "drag-over-event"
// DropEvent is the constant for "drop-event" property tag.
//
// Used by View.
// Fired when an element or text selection is dropped on a valid drop target.
//
// General listener format:
// func(view rui.View, event rui.DragAndDropEvent).
//
// where:
// - view - Interface of a view which generated this event,
// - event - event parameters.
//
// Allowed listener formats:
// func(view rui.View)
// func(rui.DragAndDropEvent)
// func()
DropEvent PropertyName = "drop-event"
// DropEffectUndefined - the value of the "drop-effect" and "drop-effect-allowed" properties: the value is not defined (default value).
DropEffectUndefined = 0
// DropEffectNone - the value of the DropEffect field of the DragEvent struct: the item may not be dropped.
DropEffectNone = 0
// DropEffectCopy - the value of the "drop-effect" and "drop-effect-allowed" properties: a copy of the source item may be made at the new location.
DropEffectCopy = 1
// DropEffectMove - the value of the "drop-effect" and "drop-effect-allowed" properties: an item may be moved to a new location.
DropEffectMove = 2
// DropEffectLink - the value of the "drop-effect" and "drop-effect-allowed" properties: a link may be established to the source at the new location.
DropEffectLink = 4
// DropEffectCopyMove - the value of the "drop-effect-allowed" property: a copy or move operation is permitted.
DropEffectCopyMove = DropEffectCopy + DropEffectMove
// DropEffectCopyLink - the value of the "drop-effect-allowed" property: a copy or link operation is permitted.
DropEffectCopyLink = DropEffectCopy + DropEffectLink
// DropEffectLinkMove - the value of the "drop-effect-allowed" property: a link or move operation is permitted.
DropEffectLinkMove = DropEffectLink + DropEffectMove
// DropEffectAll - the value of the "drop-effect-allowed" property: all operations (copy, move, and link) are permitted (default value).
DropEffectAll = DropEffectCopy + DropEffectMove + DropEffectLink
)
// MouseEvent represent a mouse event
type DragAndDropEvent struct {
MouseEvent
Data map[string]string
Files []FileInfo
Target View
EffectAllowed int
DropEffect int
}
func (event *DragAndDropEvent) init(session Session, data DataObject) {
event.MouseEvent.init(data)
event.Data = map[string]string{}
if value, ok := data.PropertyValue("data"); ok {
data := strings.Split(value, ";")
for _, line := range data {
pair := strings.Split(line, ":")
if len(pair) == 2 {
mime, err := base64.StdEncoding.DecodeString(pair[0])
if err != nil {
ErrorLog(err.Error())
} else {
val, err := base64.StdEncoding.DecodeString(pair[1])
if err == nil {
event.Data[string(mime)] = string(val)
} else {
ErrorLog(err.Error())
}
}
}
}
}
if targetId, ok := data.PropertyValue("target"); ok {
event.Target = session.viewByHTMLID(targetId)
}
if effect, ok := data.PropertyValue("effect-allowed"); ok {
for i, value := range []string{"undefined", "copy", "move", "copyMove", "link", "copyLink", "linkMove", "all"} {
if value == effect {
event.EffectAllowed = i
break
}
}
}
if effect, ok := data.PropertyValue("drop-effect"); ok && effect != "" {
for i, value := range []string{"none", "copy", "move", "", "link"} {
if value == effect {
event.DropEffect = i
break
}
}
}
event.Files = parseFilesTag(data)
}
func stringToDropEffect(text string) (int, bool) {
text = strings.Trim(text, " \t\n")
if n, ok := enumStringToInt(text, []string{"", "copy", "move", "", "link"}, false); ok {
switch n {
case DropEffectUndefined, DropEffectCopy, DropEffectMove, DropEffectLink:
return n, true
}
}
return 0, false
}
func (view *viewData) setDropEffect(value any) []PropertyName {
if !setSimpleProperty(view, DropEffect, value) {
if text, ok := value.(string); ok {
if n, ok := stringToDropEffect(text); ok {
if n == DropEffectUndefined {
view.setRaw(DropEffect, nil)
} else {
view.setRaw(DropEffect, n)
}
} else {
invalidPropertyValue(DropEffect, value)
return nil
}
} else if i, ok := isInt(value); ok {
switch i {
case DropEffectUndefined:
view.setRaw(DropEffect, nil)
case DropEffectCopy, DropEffectMove, DropEffectLink:
view.setRaw(DropEffect, i)
default:
invalidPropertyValue(DropEffect, value)
return nil
}
} else {
notCompatibleType(DropEffect, value)
return nil
}
}
return []PropertyName{DropEffect}
}
func stringToDropEffectAllowed(text string) (int, bool) {
if strings.ContainsRune(text, '|') {
elements := strings.Split(text, "|")
result := 0
for _, element := range elements {
if n, ok := stringToDropEffect(element); ok && n != DropEffectUndefined {
result |= n
} else {
return 0, false
}
}
return result, true
}
text = strings.Trim(text, " \t\n")
if text != "" {
if n, ok := enumStringToInt(text, []string{"undefined", "copy", "move", "", "link", "", "", "all"}, false); ok {
return n, true
}
}
return 0, false
}
func (view *viewData) setDropEffectAllowed(value any) []PropertyName {
if !setSimpleProperty(view, DropEffectAllowed, value) {
if text, ok := value.(string); ok {
if n, ok := stringToDropEffectAllowed(text); ok {
if n == DropEffectUndefined {
view.setRaw(DropEffectAllowed, nil)
} else {
view.setRaw(DropEffectAllowed, n)
}
} else {
invalidPropertyValue(DropEffectAllowed, value)
return nil
}
} else {
n, ok := isInt(value)
if !ok {
notCompatibleType(DropEffectAllowed, value)
return nil
}
if n == DropEffectUndefined {
view.setRaw(DropEffectAllowed, nil)
} else if n > DropEffectUndefined && n <= DropEffectAll {
view.setRaw(DropEffectAllowed, n)
} else {
notCompatibleType(DropEffectAllowed, value)
return nil
}
}
}
return []PropertyName{DropEffectAllowed}
}
func handleDragAndDropEvents(view View, tag PropertyName, data DataObject) {
listeners := getOneArgEventListeners[View, DragAndDropEvent](view, nil, tag)
if len(listeners) > 0 {
var event DragAndDropEvent
event.init(view.Session(), data)
for _, listener := range listeners {
listener.Run(view, event)
}
}
}
func base64DragData(view View) string {
if value := view.getRaw(DragData); value != nil {
if data, ok := value.(map[string]string); ok && len(data) > 0 {
buf := allocStringBuilder()
defer freeStringBuilder(buf)
for mime, value := range data {
if buf.Len() > 0 {
buf.WriteRune(';')
}
buf.WriteString(base64.StdEncoding.EncodeToString([]byte(mime)))
buf.WriteRune(':')
buf.WriteString(base64.StdEncoding.EncodeToString([]byte(value)))
}
return buf.String()
}
}
return ""
}
func dragAndDropHtml(view View, buffer *strings.Builder) {
if len(getOneArgEventListeners[View, DragAndDropEvent](view, nil, DropEvent)) > 0 {
buffer.WriteString(`ondragover="dragOverEvent(this, event)" ondrop="dropEvent(this, event)" `)
if len(getOneArgEventListeners[View, DragAndDropEvent](view, nil, DragOverEvent)) > 0 {
buffer.WriteString(`data-drag-over="1" `)
}
}
if dragData := base64DragData(view); dragData != "" {
buffer.WriteString(`draggable="true" data-drag="`)
buffer.WriteString(dragData)
buffer.WriteString(`" ondragstart="dragStartEvent(this, event)" `)
} else if len(getOneArgEventListeners[View, DragAndDropEvent](view, nil, DragStartEvent)) > 0 {
buffer.WriteString(` ondragstart="dragStartEvent(this, event)" `)
}
enterEvent := false
switch GetDropEffect(view) {
case DropEffectCopy:
buffer.WriteString(` data-drop-effect="copy" ondragenter="dragEnterEvent(this, event)"`)
enterEvent = true
case DropEffectMove:
buffer.WriteString(` data-drop-effect="move" ondragenter="dragEnterEvent(this, event)"`)
enterEvent = true
case DropEffectLink:
buffer.WriteString(` data-drop-effect="link" ondragenter="dragEnterEvent(this, event)"`)
enterEvent = true
}
if enterEvent {
viewEventsHtml[DragAndDropEvent](view, []PropertyName{DragEndEvent, DragLeaveEvent}, buffer)
} else {
viewEventsHtml[DragAndDropEvent](view, []PropertyName{DragEndEvent, DragEnterEvent, DragLeaveEvent}, buffer)
}
if img := GetDragImage(view); img != "" {
buffer.WriteString(` data-drag-image="`)
buffer.WriteString(img)
buffer.WriteString(`" `)
}
if f := GetDragImageXOffset(view); f != 0 {
buffer.WriteString(` data-drag-image-x="`)
fmt.Fprintf(buffer, "%g", f)
buffer.WriteString(`" `)
}
if f := GetDragImageYOffset(view); f != 0 {
buffer.WriteString(` data-drag-image-y="`)
fmt.Fprintf(buffer, "%g", f)
buffer.WriteString(`" `)
}
effects := []string{"undefined", "copy", "move", "copyMove", "link", "copyLink", "linkMove", "all"}
if n := GetDropEffectAllowed(view); n > 0 && n < len(effects) {
buffer.WriteString(` data-drop-effect-allowed="`)
buffer.WriteString(effects[n])
buffer.WriteString(`" `)
}
}
func (view *viewData) LoadFile(file FileInfo, result func(FileInfo, []byte)) {
if result != nil {
view.fileLoader[file.key()] = result
view.Session().callFunc("loadDropFile", view.htmlID(), file.Name, file.Size)
}
}
// GetDragStartEventListeners returns the "drag-start-event" listener list. If there are no listeners then the empty list is returned.
//
// Result elements can be of the following types:
// - func(rui.View, rui.DragAndDropEvent),
// - func(rui.View),
// - func(rui.DragAndDropEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragStartEventListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, DragAndDropEvent](view, subviewID, DragStartEvent)
}
// GetDragEndEventListeners returns the "drag-end-event" listener list. If there are no listeners then the empty list is returned.
//
// Result elements can be of the following types:
// - func(rui.View, rui.DragAndDropEvent),
// - func(rui.View),
// - func(rui.DragAndDropEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragEndEventListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, DragAndDropEvent](view, subviewID, DragEndEvent)
}
// GetDragEnterEventListeners returns the "drag-enter-event" listener list. If there are no listeners then the empty list is returned.
//
// Result elements can be of the following types:
// - func(rui.View, rui.DragAndDropEvent),
// - func(rui.View),
// - func(rui.DragAndDropEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragEnterEventListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, DragAndDropEvent](view, subviewID, DragEnterEvent)
}
// GetDragLeaveEventListeners returns the "drag-leave-event" listener list. If there are no listeners then the empty list is returned.
//
// Result elements can be of the following types:
// - func(rui.View, rui.DragAndDropEvent),
// - func(rui.View),
// - func(rui.DragAndDropEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragLeaveEventListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, DragAndDropEvent](view, subviewID, DragLeaveEvent)
}
// GetDragOverEventListeners returns the "drag-over-event" listener list. If there are no listeners then the empty list is returned.
//
// Result elements can be of the following types:
// - func(rui.View, rui.DragAndDropEvent),
// - func(rui.View),
// - func(rui.DragAndDropEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragOverEventListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, DragAndDropEvent](view, subviewID, DragOverEvent)
}
// GetDropEventListeners returns the "drag-start-event" listener list. If there are no listeners then the empty list is returned.
//
// Result elements can be of the following types:
// - func(rui.View, rui.DragAndDropEvent),
// - func(rui.View),
// - func(rui.DragAndDropEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDropEventListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, DragAndDropEvent](view, subviewID, DropEvent)
}
// GetDropEventListeners returns the "drag-data" data.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragData(view View, subviewID ...string) map[string]string {
result := map[string]string{}
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(DragData); value != nil {
if data, ok := value.(map[string]string); ok {
maps.Copy(result, data)
}
}
}
return result
}
// GetDragImage returns the drag feedback image.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragImage(view View, subviewID ...string) string {
if view = getSubview(view, subviewID); view != nil {
value := view.getRaw(DragImage)
if value == nil {
value = valueFromStyle(view, DragImage)
}
if value != nil {
if img, ok := value.(string); ok {
img = strings.Trim(img, " \t")
if ok, constName := isConstantName(img); ok {
if img, ok = view.Session().ImageConstant(constName); ok {
return img
}
} else {
return img
}
}
}
}
return ""
}
// GetDragImageXOffset returns the horizontal offset in pixels within the drag feedback image.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragImageXOffset(view View, subviewID ...string) float64 {
return floatStyledProperty(view, subviewID, DragImageXOffset, 0)
}
// GetDragImageYOffset returns the vertical offset in pixels within the drag feedback image.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDragImageYOffset(view View, subviewID ...string) float64 {
return floatStyledProperty(view, subviewID, DragImageYOffset, 0)
}
// GetDropEffect returns the effect that is allowed for a drag operation.
// Controls the feedback (typically visual) the user is given during a drag and drop operation.
// It will affect which cursor is displayed while dragging.
//
// Returns one of next values:
// - 0 (DropEffectUndefined) - The value is not defined (all operations are permitted).
// - 1 (DropEffectCopy) - A copy of the source item may be made at the new location.
// - 2 (DropEffectMove) - An item may be moved to a new location.
// - 4 (DropEffectLink) - A link may be established to the source at the new location.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDropEffect(view View, subviewID ...string) int {
if view = getSubview(view, subviewID); view != nil {
value := view.getRaw(DropEffect)
if value == nil {
value = valueFromStyle(view, DropEffect)
}
if value != nil {
switch value := value.(type) {
case int:
return value
case string:
if value, ok := view.Session().resolveConstants(value); ok {
if n, ok := stringToDropEffect(value); ok {
return n
}
}
default:
return DropEffectUndefined
}
}
}
return DropEffectUndefined
}
// GetDropEffectAllowed returns the effect that is allowed for a drag operation.
// The copy operation is used to indicate that the data being dragged will be copied from its present location to the drop location.
// The move operation is used to indicate that the data being dragged will be moved,
// and the link operation is used to indicate that some form of relationship
// or connection will be created between the source and drop locations.
//
// Returns one of next values:
// - 0 (DropEffectUndefined) - The value is not defined (all operations are permitted).
// - 1 (DropEffectCopy) - A copy of the source item may be made at the new location.
// - 2 (DropEffectMove) - An item may be moved to a new location.
// - 4 (DropEffectLink) - A link may be established to the source at the new location.
// - 3 (DropEffectCopyMove) - A copy or move operation is permitted.
// - 5 (DropEffectCopyLink) - A copy or link operation is permitted.
// - 6 (DropEffectLinkMove) - A link or move operation is permitted.
// - 7 (DropEffectAll) - All operations are permitted.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDropEffectAllowed(view View, subviewID ...string) int {
if view = getSubview(view, subviewID); view != nil {
value := view.getRaw(DropEffectAllowed)
if value == nil {
value = valueFromStyle(view, DropEffectAllowed)
}
if value != nil {
switch value := value.(type) {
case int:
return value
case string:
if value, ok := view.Session().resolveConstants(value); ok {
if n, ok := stringToDropEffectAllowed(value); ok {
return n
}
}
default:
return DropEffectUndefined
}
}
}
return DropEffectUndefined
}

View File

@ -1,27 +1,37 @@
package rui package rui
import ( import (
"fmt"
"strconv" "strconv"
"strings" "strings"
) )
// DropDownEvent is the constant for "drop-down-event" property tag. // DropDownEvent is the constant for "drop-down-event" property tag.
// The "drop-down-event" event occurs when a list item becomes selected. //
// The main listener format: func(DropDownList, int), where the second argument is the item index. // Used by DropDownList.
const DropDownEvent = "drop-down-event" // Occur when a list item becomes selected.
//
// General listener format:
//
// func(list rui.DropDownList, index int)
//
// where:
// - list - Interface of a drop down list which generated this event,
// - index - Index of a newly selected item.
//
// Allowed listener formats:
//
// func(index int)
// func(list rui.DropDownList)
// func()
const DropDownEvent PropertyName = "drop-down-event"
// DropDownList - the interface of a drop-down list view // DropDownList represent a DropDownList view
type DropDownList interface { type DropDownList interface {
View View
getItems() []string
} }
type dropDownListData struct { type dropDownListData struct {
viewData viewData
items []string
disabledItems []any
dropDownListener []func(DropDownList, int)
} }
// NewDropDownList create new DropDownList object and return it // NewDropDownList create new DropDownList object and return it
@ -33,301 +43,162 @@ func NewDropDownList(session Session, params Params) DropDownList {
} }
func newDropDownList(session Session) View { func newDropDownList(session Session) View {
return NewDropDownList(session, nil) return new(dropDownListData)
} }
func (list *dropDownListData) init(session Session) { func (list *dropDownListData) init(session Session) {
list.viewData.init(session) list.viewData.init(session)
list.tag = "DropDownList" list.tag = "DropDownList"
list.items = []string{} list.hasHtmlDisabled = true
list.disabledItems = []any{} list.normalize = normalizeDropDownListTag
list.dropDownListener = []func(DropDownList, int){} list.get = list.getFunc
} list.set = list.setFunc
list.changed = list.propertyChanged
func (list *dropDownListData) String() string {
return getViewString(list)
} }
func (list *dropDownListData) Focusable() bool { func (list *dropDownListData) Focusable() bool {
return true return true
} }
func (list *dropDownListData) Remove(tag string) { func normalizeDropDownListTag(tag PropertyName) PropertyName {
list.remove(strings.ToLower(tag)) tag = defaultNormalize(tag)
if tag == "separators" {
return ItemSeparators
}
return tag
} }
func (list *dropDownListData) remove(tag string) { func (list *dropDownListData) getFunc(tag PropertyName) any {
switch tag {
case DropDownEvent:
if listeners := getTwoArgEventRawListeners[DropDownList, int](list, nil, tag); len(listeners) > 0 {
return listeners
}
return nil
}
return list.viewData.getFunc(tag)
}
func (list *dropDownListData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag { switch tag {
case Items: case Items:
if len(list.items) > 0 { if items, ok := anyToStringArray(value, ""); ok {
list.items = []string{} return setArrayPropertyValue(list, tag, items)
if list.created {
updateInnerHTML(list.htmlID(), list.session)
}
list.propertyChangedEvent(tag)
} }
notCompatibleType(Items, value)
return nil
case DisabledItems: case DisabledItems, ItemSeparators:
if len(list.disabledItems) > 0 { if items, ok := parseIndicesArray(value); ok {
list.disabledItems = []any{} return setArrayPropertyValue(list, tag, items)
if list.created {
updateInnerHTML(list.htmlID(), list.session)
}
list.propertyChangedEvent(tag)
} }
notCompatibleType(tag, value)
return nil
case DropDownEvent: case DropDownEvent:
if len(list.dropDownListener) > 0 { return setTwoArgEventListener[DropDownList, int](list, tag, value)
list.dropDownListener = []func(DropDownList, int){}
list.propertyChangedEvent(tag)
}
case Current: case Current:
oldCurrent := GetCurrent(list) list.setRaw("old-current", GetCurrent(list))
delete(list.properties, Current) return setIntProperty(list, Current, value)
if oldCurrent != 0 { }
if list.created {
list.session.runScript(fmt.Sprintf(`selectDropDownListItem('%s', %d)`, list.htmlID(), 0)) return list.viewData.setFunc(tag, value)
} }
list.onSelectedItemChanged(0)
func (list *dropDownListData) propertyChanged(tag PropertyName) {
switch tag {
case Items, DisabledItems, ItemSeparators:
updateInnerHTML(list.htmlID(), list.Session())
case Current:
current := GetCurrent(list)
list.Session().callFunc("selectDropDownListItem", list.htmlID(), current)
oldCurrent, _ := intProperty(list, "old-current", list.Session(), -1)
for _, listener := range getTwoArgEventListeners[DropDownList, int](list, nil, DropDownEvent) {
listener.Run(list, current, oldCurrent)
} }
default: default:
list.viewData.remove(tag) list.viewData.propertyChanged(tag)
return
} }
} }
func (list *dropDownListData) Set(tag string, value any) bool { func intArrayToStringArray[T int | uint | int8 | uint8 | int16 | uint16 | int32 | uint32 | int64 | uint64](array []T) []string {
return list.set(strings.ToLower(tag), value) items := make([]string, len(array))
for i, val := range array {
items[i] = strconv.Itoa(int(val))
}
return items
} }
func (list *dropDownListData) set(tag string, value any) bool { func parseIndicesArray(value any) ([]any, bool) {
if value == nil {
list.remove(tag)
return true
}
switch tag {
case Items:
return list.setItems(value)
case DisabledItems:
return list.setDisabledItems(value)
case DropDownEvent:
listeners, ok := valueToEventListeners[DropDownList, int](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(DropDownList, int){}
}
list.dropDownListener = listeners
list.propertyChangedEvent(tag)
return true
case Current:
oldCurrent := GetCurrent(list)
if !list.setIntProperty(Current, value) {
return false
}
if current := GetCurrent(list); oldCurrent != current {
if list.created {
list.session.runScript(fmt.Sprintf(`selectDropDownListItem('%s', %d)`, list.htmlID(), current))
}
list.onSelectedItemChanged(current)
}
return true
}
return list.viewData.set(tag, value)
}
func (list *dropDownListData) setItems(value any) bool {
switch value := value.(type) { switch value := value.(type) {
case string: case int:
list.items = []string{value} return []any{value}, true
case []int:
items := make([]any, len(value))
for i, n := range value {
items[i] = n
}
return items, true
case []any:
items := make([]any, 0, len(value))
for _, val := range value {
if val != nil {
switch val := val.(type) {
case string:
if ok, _ := isConstantName(val); ok {
items = append(items, val)
} else if n, err := strconv.Atoi(val); err == nil {
items = append(items, n)
} else {
return nil, false
}
default:
if n, ok := isInt(val); ok {
items = append(items, n)
} else {
return nil, false
}
}
}
}
return items, true
case []string: case []string:
list.items = value items := make([]any, 0, len(value))
for _, str := range value {
case []DataValue: if str = strings.Trim(str, " \t"); str != "" {
list.items = make([]string, 0, len(value)) if ok, _ := isConstantName(str); ok {
for _, val := range value { items = append(items, str)
if !val.IsObject() { } else if n, err := strconv.Atoi(str); err == nil {
list.items = append(list.items, val.Value()) items = append(items, n)
}
}
case []fmt.Stringer:
list.items = make([]string, len(value))
for i, str := range value {
list.items[i] = str.String()
}
case []any:
items := make([]string, 0, len(value))
for _, v := range value {
switch val := v.(type) {
case string:
items = append(items, val)
case fmt.Stringer:
items = append(items, val.String())
case bool:
if val {
items = append(items, "true")
} else { } else {
items = append(items, "false") return nil, false
}
case float32:
items = append(items, fmt.Sprintf("%g", float64(val)))
case float64:
items = append(items, fmt.Sprintf("%g", val))
case rune:
items = append(items, string(val))
default:
if n, ok := isInt(v); ok {
items = append(items, strconv.Itoa(n))
} else {
notCompatibleType(Items, value)
return false
} }
} }
} }
return items, true
list.items = items
default:
notCompatibleType(Items, value)
return false
}
if list.created {
updateInnerHTML(list.htmlID(), list.session)
}
list.propertyChangedEvent(Items)
return true
}
func (list *dropDownListData) setDisabledItems(value any) bool {
switch value := value.(type) {
case []int:
list.disabledItems = make([]any, len(value))
for i, n := range value {
list.disabledItems[i] = n
}
case []any:
disabledItems := make([]any, len(value))
for i, val := range value {
if val == nil {
notCompatibleType(DisabledItems, value)
return false
}
switch val := val.(type) {
case string:
if isConstantName(val) {
disabledItems[i] = val
} else {
n, err := strconv.Atoi(val)
if err != nil {
notCompatibleType(DisabledItems, value)
return false
}
disabledItems[i] = n
}
default:
if n, ok := isInt(val); ok {
disabledItems[i] = n
} else {
notCompatibleType(DisabledItems, value)
return false
}
}
}
list.disabledItems = disabledItems
case string: case string:
values := strings.Split(value, ",") return parseIndicesArray(strings.Split(value, ","))
disabledItems := make([]any, len(values))
for i, str := range values {
str = strings.Trim(str, " ")
if str == "" {
notCompatibleType(DisabledItems, value)
return false
}
if isConstantName(str) {
disabledItems[i] = str
} else {
n, err := strconv.Atoi(str)
if err != nil {
notCompatibleType(DisabledItems, value)
return false
}
disabledItems[i] = n
}
}
list.disabledItems = disabledItems
case []DataValue: case []DataValue:
disabledItems := make([]string, 0, len(value)) items := make([]string, 0, len(value))
for _, val := range value { for _, val := range value {
if !val.IsObject() { if !val.IsObject() {
disabledItems = append(disabledItems, val.Value()) items = append(items, val.Value())
} }
} }
return list.setDisabledItems(disabledItems) return parseIndicesArray(items)
default:
notCompatibleType(DisabledItems, value)
return false
} }
if list.created { return nil, false
updateInnerHTML(list.htmlID(), list.session)
}
list.propertyChangedEvent(Items)
return true
}
func (list *dropDownListData) Get(tag string) any {
return list.get(strings.ToLower(tag))
}
func (list *dropDownListData) get(tag string) any {
switch tag {
case Items:
return list.items
case DisabledItems:
return list.disabledItems
case Current:
result, _ := intProperty(list, Current, list.session, 0)
return result
case DropDownEvent:
return list.dropDownListener
}
return list.viewData.get(tag)
}
func (list *dropDownListData) getItems() []string {
return list.items
} }
func (list *dropDownListData) htmlTag() string { func (list *dropDownListData) htmlTag() string {
@ -335,11 +206,12 @@ func (list *dropDownListData) htmlTag() string {
} }
func (list *dropDownListData) htmlSubviews(self View, buffer *strings.Builder) { func (list *dropDownListData) htmlSubviews(self View, buffer *strings.Builder) {
if list.items != nil { if items := GetDropDownItems(list); len(items) > 0 {
current := GetCurrent(list) current := GetCurrent(list)
notTranslate := GetNotTranslate(list) notTranslate := GetNotTranslate(list)
disabledItems := GetDropDownDisabledItems(list) disabledItems := GetDropDownDisabledItems(list)
for i, item := range list.items { separators := GetDropDownItemSeparators(list)
for i, item := range items {
disabled := false disabled := false
for _, index := range disabledItems { for _, index := range disabledItems {
if i == index { if i == index {
@ -361,6 +233,12 @@ func (list *dropDownListData) htmlSubviews(self View, buffer *strings.Builder) {
buffer.WriteString(item) buffer.WriteString(item)
buffer.WriteString("</option>") buffer.WriteString("</option>")
for _, index := range separators {
if i == index {
buffer.WriteString("<hr>")
break
}
}
} }
} }
} }
@ -370,28 +248,19 @@ func (list *dropDownListData) htmlProperties(self View, buffer *strings.Builder)
buffer.WriteString(` size="1" onchange="dropDownListEvent(this, event)"`) buffer.WriteString(` size="1" onchange="dropDownListEvent(this, event)"`)
} }
func (list *dropDownListData) htmlDisabledProperties(self View, buffer *strings.Builder) { func (list *dropDownListData) handleCommand(self View, command PropertyName, data DataObject) bool {
list.viewData.htmlDisabledProperties(self, buffer)
if IsDisabled(list) {
buffer.WriteString(`disabled`)
}
}
func (list *dropDownListData) onSelectedItemChanged(number int) {
for _, listener := range list.dropDownListener {
listener(list, number)
}
list.propertyChangedEvent(Current)
}
func (list *dropDownListData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "itemSelected": case "itemSelected":
if text, ok := data.PropertyValue("number"); ok { if text, ok := data.PropertyValue("number"); ok {
if number, err := strconv.Atoi(text); err == nil { if number, err := strconv.Atoi(text); err == nil {
if GetCurrent(list) != number && number >= 0 && number < len(list.items) { items := GetDropDownItems(list)
if GetCurrent(list) != number && number >= 0 && number < len(items) {
old := GetCurrent(list)
list.properties[Current] = number list.properties[Current] = number
list.onSelectedItemChanged(number) for _, listener := range getTwoArgEventListeners[DropDownList, int](list, nil, DropDownEvent) {
listener.Run(list, number, old)
}
list.runChangeListener(Current)
} }
} else { } else {
ErrorLog(err.Error()) ErrorLog(err.Error())
@ -405,34 +274,40 @@ func (list *dropDownListData) handleCommand(self View, command string, data Data
} }
// GetDropDownListeners returns the "drop-down-event" listener list. If there are no listeners then the empty list is returned. // GetDropDownListeners returns the "drop-down-event" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetDropDownListeners(view View, subviewID ...string) []func(DropDownList, int) { // Result elements can be of the following types:
return getEventListeners[DropDownList, int](view, subviewID, DropDownEvent) // - func(rui.DropDownList, int, int),
// - func(rui.DropDownList, int),
// - func(rui.DropDownList),
// - func(int, int),
// - func(int),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDropDownListeners(view View, subviewID ...string) []any {
return getTwoArgEventRawListeners[DropDownList, int](view, subviewID, DropDownEvent)
} }
// GetDropDownItems return the DropDownList items list. // GetDropDownItems return the DropDownList items list.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDropDownItems(view View, subviewID ...string) []string { func GetDropDownItems(view View, subviewID ...string) []string {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0]) if value := view.Get(Items); value != nil {
} if items, ok := value.([]string); ok {
if view != nil { return items
if list, ok := view.(DropDownList); ok { }
return list.getItems()
} }
} }
return []string{} return []string{}
} }
// GetDropDownDisabledItems return the list of DropDownList disabled item indexes. func getIndicesArray(view View, tag PropertyName) []int {
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
func GetDropDownDisabledItems(view View, subviewID ...string) []int {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil { if view != nil {
if value := view.Get(DisabledItems); value != nil { if value := view.Get(tag); value != nil {
if values, ok := value.([]any); ok { if values, ok := value.([]any); ok {
count := len(values) count := len(values)
if count > 0 { if count > 0 {
@ -443,8 +318,8 @@ func GetDropDownDisabledItems(view View, subviewID ...string) []int {
result = append(result, value) result = append(result, value)
case string: case string:
if value != "" && value[0] == '@' { if ok, constName := isConstantName(value); ok {
if val, ok := view.Session().Constant(value[1:]); ok { if val, ok := view.Session().Constant(constName); ok {
if n, err := strconv.Atoi(val); err == nil { if n, err := strconv.Atoi(val); err == nil {
result = append(result, n) result = append(result, n)
} }
@ -459,3 +334,21 @@ func GetDropDownDisabledItems(view View, subviewID ...string) []int {
} }
return []int{} return []int{}
} }
// GetDropDownDisabledItems return an array of disabled(non selectable) items indices of DropDownList.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDropDownDisabledItems(view View, subviewID ...string) []int {
view = getSubview(view, subviewID)
return getIndicesArray(view, DisabledItems)
}
// GetDropDownItemSeparators return an array of indices of DropDownList items after which a separator should be added.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDropDownItemSeparators(view View, subviewID ...string) []int {
view = getSubview(view, subviewID)
return getIndicesArray(view, ItemSeparators)
}

View File

@ -1,48 +1,107 @@
package rui package rui
import ( import (
"fmt"
"strconv" "strconv"
"strings" "strings"
) )
// Constants for [EditView] specific properties and events
const ( const (
// EditTextChangedEvent is the constant for the "edit-text-changed" property tag. // EditTextChangedEvent is the constant for "edit-text-changed" property tag.
EditTextChangedEvent = "edit-text-changed" //
// EditViewType is the constant for the "edit-view-type" property tag. // Used by EditView.
EditViewType = "edit-view-type" // Occur when edit view text has been changed.
// EditViewPattern is the constant for the "edit-view-pattern" property tag. //
EditViewPattern = "edit-view-pattern" // General listener format:
// Spellcheck is the constant for the "spellcheck" property tag. // func(editView rui.EditView, newText string, oldText string).
Spellcheck = "spellcheck" //
// where:
// - editView - Interface of an edit view which generated this event,
// - newText - New edit view text,
// - oldText - Previous edit view text.
//
// Allowed listener formats:
// - func(editView rui.EditView, newText string)
// - func(newText string, oldText string)
// - func(newText string)
// - func(editView rui.EditView)
// - func()
EditTextChangedEvent PropertyName = "edit-text-changed"
// EditViewType is the constant for "edit-view-type" property tag.
//
// Used by EditView.
// Type of the text input. Default value is "text".
//
// Supported types: int, string.
//
// Values:
// - 0 (SingleLineText) or "text" - One-line text editor.
// - 1 (PasswordText) or "password" - Password editor. The text is hidden by asterisks.
// - 2 (EmailText) or "email" - Single e-mail editor.
// - 3 (EmailsText) or "emails" - Multiple e-mail editor.
// - 4 (URLText) or "url" - Internet address input editor.
// - 5 (PhoneText) or "phone" - Phone number editor.
// - 6 (MultiLineText) or "multiline" - Multi-line text editor.
EditViewType PropertyName = "edit-view-type"
// EditViewPattern is the constant for "edit-view-pattern" property tag.
//
// Used by EditView.
// Regular expression to limit editing of a text.
//
// Supported types: string.
EditViewPattern PropertyName = "edit-view-pattern"
// Spellcheck is the constant for "spellcheck" property tag.
//
// Used by EditView.
// Enable or disable spell checker. Available in SingleLineText and MultiLineText types of edit view. Default value is
// false.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", "1" - Enable spell checker for text.
// - false, 0, "false", "no", "off", "0" - Disable spell checker for text.
Spellcheck PropertyName = "spellcheck"
) )
// Constants for the values of an [EditView] "edit-view-type" property
const ( const (
// SingleLineText - single-line text type of EditView // SingleLineText - single-line text type of EditView
SingleLineText = 0 SingleLineText = 0
// PasswordText - password type of EditView // PasswordText - password type of EditView
PasswordText = 1 PasswordText = 1
// EmailText - e-mail type of EditView. Allows to enter one email // EmailText - e-mail type of EditView. Allows to enter one email
EmailText = 2 EmailText = 2
// EmailsText - e-mail type of EditView. Allows to enter multiple emails separeted by comma
// EmailsText - e-mail type of EditView. Allows to enter multiple emails separated by comma
EmailsText = 3 EmailsText = 3
// URLText - url type of EditView. Allows to enter one url // URLText - url type of EditView. Allows to enter one url
URLText = 4 URLText = 4
// PhoneText - telephone type of EditView. Allows to enter one phone number // PhoneText - telephone type of EditView. Allows to enter one phone number
PhoneText = 5 PhoneText = 5
// MultiLineText - multi-line text type of EditView // MultiLineText - multi-line text type of EditView
MultiLineText = 6 MultiLineText = 6
) )
// EditView - grid-container of View // EditView represent an EditView view
type EditView interface { type EditView interface {
View View
// AppendText appends text to the current text of an EditView view
AppendText(text string) AppendText(text string)
textChanged(newText, oldText string)
} }
type editViewData struct { type editViewData struct {
viewData viewData
textChangeListeners []func(EditView, string)
} }
// NewEditView create new EditView object and return it // NewEditView create new EditView object and return it
@ -54,25 +113,25 @@ func NewEditView(session Session, params Params) EditView {
} }
func newEditView(session Session) View { func newEditView(session Session) View {
return NewEditView(session, nil) return new(editViewData) // NewEditView(session, nil)
} }
func (edit *editViewData) init(session Session) { func (edit *editViewData) init(session Session) {
edit.viewData.init(session) edit.viewData.init(session)
edit.textChangeListeners = []func(EditView, string){} edit.hasHtmlDisabled = true
edit.tag = "EditView" edit.tag = "EditView"
} edit.normalize = normalizeEditViewTag
edit.get = edit.getFunc
func (edit *editViewData) String() string { edit.set = edit.setFunc
return getViewString(edit) edit.changed = edit.propertyChanged
} }
func (edit *editViewData) Focusable() bool { func (edit *editViewData) Focusable() bool {
return true return true
} }
func (edit *editViewData) normalizeTag(tag string) string { func normalizeEditViewTag(tag PropertyName) PropertyName {
tag = strings.ToLower(tag) tag = defaultNormalize(tag)
switch tag { switch tag {
case Type, "edit-type": case Type, "edit-type":
return EditViewType return EditViewType
@ -87,307 +146,144 @@ func (edit *editViewData) normalizeTag(tag string) string {
return EditWrap return EditWrap
} }
return tag return normalizeDataListTag(tag)
} }
func (edit *editViewData) Remove(tag string) { func (edit *editViewData) getFunc(tag PropertyName) any {
edit.remove(edit.normalizeTag(tag))
}
func (edit *editViewData) remove(tag string) {
_, exists := edit.properties[tag]
switch tag { switch tag {
case EditTextChangedEvent:
if listeners := getTwoArgEventRawListeners[EditView, string](edit, nil, tag); len(listeners) > 0 {
return listeners
}
return nil
}
return edit.viewData.getFunc(tag)
}
func (edit *editViewData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case Text:
if text, ok := value.(string); ok {
old := ""
if val := edit.getRaw(Text); val != nil {
if txt, ok := val.(string); ok {
old = txt
}
}
edit.setRaw("old-text", old)
edit.setRaw(tag, text)
return []PropertyName{tag}
}
notCompatibleType(tag, value)
return nil
case Hint: case Hint:
if exists { if text, ok := value.(string); ok {
delete(edit.properties, Hint) return setStringPropertyValue(edit, tag, strings.Trim(text, " \t\n"))
if edit.created {
removeProperty(edit.htmlID(), "placeholder", edit.session)
}
edit.propertyChangedEvent(tag)
} }
notCompatibleType(tag, value)
return nil
case MaxLength: case DataList:
if exists { setDataList(edit, value, "")
delete(edit.properties, MaxLength)
if edit.created {
removeProperty(edit.htmlID(), "maxlength", edit.session)
}
edit.propertyChangedEvent(tag)
}
case ReadOnly, Spellcheck:
if exists {
delete(edit.properties, tag)
if edit.created {
updateBoolProperty(edit.htmlID(), tag, false, edit.session)
}
edit.propertyChangedEvent(tag)
}
case EditTextChangedEvent: case EditTextChangedEvent:
if len(edit.textChangeListeners) > 0 { return setTwoArgEventListener[EditView, string](edit, tag, value)
edit.textChangeListeners = []func(EditView, string){}
edit.propertyChangedEvent(tag)
}
case Text:
if exists {
oldText := GetText(edit)
delete(edit.properties, tag)
if oldText != "" {
edit.textChanged("")
if edit.created {
edit.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, edit.htmlID(), ""))
}
}
}
case EditViewPattern:
if exists {
oldText := GetEditViewPattern(edit)
delete(edit.properties, tag)
if oldText != "" {
if edit.created {
removeProperty(edit.htmlID(), Pattern, edit.session)
}
edit.propertyChangedEvent(tag)
}
}
case EditViewType:
if exists {
oldType := GetEditViewType(edit)
delete(edit.properties, tag)
if oldType != 0 {
if edit.created {
updateInnerHTML(edit.parentHTMLID(), edit.session)
}
edit.propertyChangedEvent(tag)
}
}
case EditWrap:
if exists {
oldWrap := IsEditViewWrap(edit)
delete(edit.properties, tag)
if GetEditViewType(edit) == MultiLineText {
if wrap := IsEditViewWrap(edit); wrap != oldWrap {
if edit.created {
if wrap {
updateProperty(edit.htmlID(), "wrap", "soft", edit.session)
} else {
updateProperty(edit.htmlID(), "wrap", "off", edit.session)
}
}
edit.propertyChangedEvent(tag)
}
}
}
default:
edit.viewData.remove(tag)
return
} }
return edit.viewData.setFunc(tag, value)
} }
func (edit *editViewData) Set(tag string, value any) bool { func (edit *editViewData) propertyChanged(tag PropertyName) {
return edit.set(edit.normalizeTag(tag), value) session := edit.Session()
}
func (edit *editViewData) set(tag string, value any) bool {
if value == nil {
edit.remove(tag)
return true
}
switch tag { switch tag {
case Text: case Text:
oldText := GetText(edit) text := GetText(edit)
if text, ok := value.(string); ok { session.callFunc("setInputValue", edit.htmlID(), text)
edit.properties[Text] = text
if text = GetText(edit); oldText != text { old := ""
edit.textChanged(text) if val := edit.getRaw("old-text"); val != nil {
if edit.created { if txt, ok := val.(string); ok {
if GetEditViewType(edit) == MultiLineText { old = txt
updateInnerHTML(edit.htmlID(), edit.Session())
} else {
text = strings.ReplaceAll(text, `"`, `\"`)
text = strings.ReplaceAll(text, `'`, `\'`)
text = strings.ReplaceAll(text, "\n", `\n`)
text = strings.ReplaceAll(text, "\r", `\r`)
edit.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, edit.htmlID(), text))
}
}
} }
return true
} }
return false edit.textChanged(text, old)
case Hint: case Hint:
oldText := GetHint(edit) if text := GetHint(edit); text != "" {
if text, ok := value.(string); ok { session.updateProperty(edit.htmlID(), "placeholder", text)
edit.properties[Hint] = text } else {
if text = GetHint(edit); oldText != text { session.removeProperty(edit.htmlID(), "placeholder")
if edit.created {
if text != "" {
updateProperty(edit.htmlID(), "placeholder", text, edit.session)
} else {
removeProperty(edit.htmlID(), "placeholder", edit.session)
}
}
edit.propertyChangedEvent(tag)
}
return true
} }
return false
case MaxLength: case MaxLength:
oldMaxLength := GetMaxLength(edit) if maxLength := GetMaxLength(edit); maxLength > 0 {
if edit.setIntProperty(MaxLength, value) { session.updateProperty(edit.htmlID(), "maxlength", strconv.Itoa(maxLength))
if maxLength := GetMaxLength(edit); maxLength != oldMaxLength { } else {
if edit.created { session.removeProperty(edit.htmlID(), "maxlength")
if maxLength > 0 {
updateProperty(edit.htmlID(), "maxlength", strconv.Itoa(maxLength), edit.session)
} else {
removeProperty(edit.htmlID(), "maxlength", edit.session)
}
}
edit.propertyChangedEvent(tag)
}
return true
} }
return false
case ReadOnly: case ReadOnly:
if edit.setBoolProperty(ReadOnly, value) { if IsReadOnly(edit) {
if edit.created { session.updateProperty(edit.htmlID(), "readonly", "")
if IsReadOnly(edit) { } else {
updateProperty(edit.htmlID(), ReadOnly, "", edit.session) session.removeProperty(edit.htmlID(), "readonly")
} else {
removeProperty(edit.htmlID(), ReadOnly, edit.session)
}
}
edit.propertyChangedEvent(tag)
return true
} }
return false
case Spellcheck: case Spellcheck:
if edit.setBoolProperty(Spellcheck, value) { session.updateProperty(edit.htmlID(), "spellcheck", IsSpellcheck(edit))
if edit.created {
updateBoolProperty(edit.htmlID(), Spellcheck, IsSpellcheck(edit), edit.session)
}
edit.propertyChangedEvent(tag)
return true
}
return false
case EditViewPattern: case EditViewPattern:
oldText := GetEditViewPattern(edit) if text := GetEditViewPattern(edit); text != "" {
if text, ok := value.(string); ok { session.updateProperty(edit.htmlID(), "pattern", text)
edit.properties[EditViewPattern] = text } else {
if text = GetEditViewPattern(edit); oldText != text { session.removeProperty(edit.htmlID(), "pattern")
if edit.created {
if text != "" {
updateProperty(edit.htmlID(), Pattern, text, edit.session)
} else {
removeProperty(edit.htmlID(), Pattern, edit.session)
}
}
edit.propertyChangedEvent(tag)
}
return true
} }
return false
case EditViewType: case EditViewType:
oldType := GetEditViewType(edit) updateInnerHTML(edit.parentHTMLID(), session)
if edit.setEnumProperty(EditViewType, value, enumProperties[EditViewType].values) {
if GetEditViewType(edit) != oldType {
if edit.created {
updateInnerHTML(edit.parentHTMLID(), edit.session)
}
edit.propertyChangedEvent(tag)
}
return true
}
return false
case EditWrap: case EditWrap:
oldWrap := IsEditViewWrap(edit) if wrap := IsEditViewWrap(edit); wrap {
if edit.setBoolProperty(EditWrap, value) { session.updateProperty(edit.htmlID(), "wrap", "soft")
if GetEditViewType(edit) == MultiLineText { } else {
if wrap := IsEditViewWrap(edit); wrap != oldWrap { session.updateProperty(edit.htmlID(), "wrap", "off")
if edit.created {
if wrap {
updateProperty(edit.htmlID(), "wrap", "soft", edit.session)
} else {
updateProperty(edit.htmlID(), "wrap", "off", edit.session)
}
}
edit.propertyChangedEvent(tag)
}
}
return true
} }
return false
case EditTextChangedEvent: case DataList:
listeners, ok := valueToEventListeners[EditView, string](value) updateInnerHTML(edit.htmlID(), session)
if !ok {
notCompatibleType(tag, value) default:
return false edit.viewData.propertyChanged(tag)
} else if listeners == nil {
listeners = []func(EditView, string){}
}
edit.textChangeListeners = listeners
edit.propertyChangedEvent(tag)
return true
} }
return edit.viewData.set(tag, value)
}
func (edit *editViewData) Get(tag string) any {
return edit.get(edit.normalizeTag(tag))
}
func (edit *editViewData) get(tag string) any {
if tag == EditTextChangedEvent {
return edit.textChangeListeners
}
return edit.viewData.get(tag)
} }
func (edit *editViewData) AppendText(text string) { func (edit *editViewData) AppendText(text string) {
if GetEditViewType(edit) == MultiLineText { if GetEditViewType(edit) == MultiLineText {
if value := edit.getRaw(Text); value != nil { if value := edit.getRaw(Text); value != nil {
if textValue, ok := value.(string); ok { if textValue, ok := value.(string); ok {
oldText := textValue
textValue += text textValue += text
edit.properties[Text] = textValue edit.properties[Text] = textValue
edit.session.callFunc("appendToInnerHTML", edit.htmlID(), text)
text := strings.ReplaceAll(text, `"`, `\"`) edit.session.callFunc("appendToInputValue", edit.htmlID(), text)
text = strings.ReplaceAll(text, `'`, `\'`) edit.textChanged(textValue, oldText)
text = strings.ReplaceAll(text, "\n", `\n`)
text = strings.ReplaceAll(text, "\r", `\r`)
edit.session.runScript(`appendToInnerHTML("` + edit.htmlID() + `", "` + text + `")`)
edit.textChanged(textValue)
return return
} }
} }
edit.set(Text, text) edit.Set(Text, text)
} else { } else {
edit.set(Text, GetText(edit)+text) edit.Set(Text, GetText(edit)+text)
} }
} }
func (edit *editViewData) textChanged(newText string) { func (edit *editViewData) textChanged(newText, oldText string) {
for _, listener := range edit.textChangeListeners { for _, listener := range getTwoArgEventListeners[EditView, string](edit, nil, EditTextChangedEvent) {
listener(edit, newText) listener.Run(edit, newText, oldText)
} }
edit.propertyChangedEvent(Text) edit.runChangeListener(Text)
} }
func (edit *editViewData) htmlTag() string { func (edit *editViewData) htmlTag() string {
@ -397,6 +293,17 @@ func (edit *editViewData) htmlTag() string {
return "input" return "input"
} }
func (edit *editViewData) htmlSubviews(self View, buffer *strings.Builder) {
if GetEditViewType(edit) == MultiLineText {
if text := GetText(edit); text != "" {
buffer.WriteString(text)
}
}
dataListHtmlSubviews(self, buffer, func(text string, session Session) string {
return text
})
}
func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) { func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) {
edit.viewData.htmlProperties(self, buffer) edit.viewData.htmlProperties(self, buffer)
@ -452,7 +359,10 @@ func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) {
if strings.ContainsRune(text, '"') { if strings.ContainsRune(text, '"') {
text = strings.ReplaceAll(text, `"`, `&#34;`) text = strings.ReplaceAll(text, `"`, `&#34;`)
} }
return textToJS(text) if strings.ContainsRune(text, '\n') {
text = strings.ReplaceAll(text, "\n", `\n`)
}
return text
} }
if hint := GetHint(edit); hint != "" { if hint := GetHint(edit); hint != "" {
@ -475,29 +385,18 @@ func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) {
buffer.WriteByte('"') buffer.WriteByte('"')
} }
} }
dataListHtmlProperties(edit, buffer)
} }
func (edit *editViewData) htmlDisabledProperties(self View, buffer *strings.Builder) { func (edit *editViewData) handleCommand(self View, command PropertyName, data DataObject) bool {
if IsDisabled(self) {
buffer.WriteString(` disabled`)
}
edit.viewData.htmlDisabledProperties(self, buffer)
}
func (edit *editViewData) htmlSubviews(self View, buffer *strings.Builder) {
if GetEditViewType(edit) == MultiLineText {
buffer.WriteString(textToJS(GetText(edit)))
}
}
func (edit *editViewData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "textChanged": case "textChanged":
oldText := GetText(edit) oldText := GetText(edit)
if text, ok := data.PropertyValue("text"); ok { if text, ok := data.PropertyValue("text"); ok {
edit.properties[Text] = text edit.setRaw(Text, text)
if text := GetText(edit); text != oldText { if text != oldText {
edit.textChanged(text) edit.textChanged(text, oldText)
} }
} }
return true return true
@ -509,10 +408,7 @@ func (edit *editViewData) handleCommand(self View, command string, data DataObje
// GetText returns a text of the EditView subview. // GetText returns a text of the EditView subview.
// If the second argument (subviewID) is not specified or it is "" then a text of the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then a text of the first argument (view) is returned.
func GetText(view View, subviewID ...string) string { func GetText(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.getRaw(Text); value != nil { if value := view.getRaw(Text); value != nil {
if text, ok := value.(string); ok { if text, ok := value.(string); ok {
return text return text
@ -525,25 +421,34 @@ func GetText(view View, subviewID ...string) string {
// GetHint returns a hint text of the subview. // GetHint returns a hint text of the subview.
// If the second argument (subviewID) is not specified or it is "" then a text of the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then a text of the first argument (view) is returned.
func GetHint(view View, subviewID ...string) string { func GetHint(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" { view = getSubview(view, subviewID)
view = ViewByID(view, subviewID[0])
} session := view.Session()
text := ""
if view != nil { if view != nil {
if text, ok := stringProperty(view, Hint, view.Session()); ok { var ok bool
return text text, ok = stringProperty(view, Hint, view.Session())
} if !ok {
if value := valueFromStyle(view, Hint); value != nil { if value := valueFromStyle(view, Hint); value != nil {
if text, ok := value.(string); ok { if text, ok = value.(string); ok {
if text, ok = view.Session().resolveConstants(text); ok { if text, ok = session.resolveConstants(text); !ok {
return text text = ""
}
} else {
text = ""
} }
} }
} }
} }
return ""
if text != "" && !GetNotTranslate(view) {
text, _ = session.GetString(text)
}
return text
} }
// GetMaxLength returns a maximal lenght of EditView. If a maximal lenght is not limited then 0 is returned // GetMaxLength returns a maximal length of EditView. If a maximal length is not limited then 0 is returned
// If the second argument (subviewID) is not specified or it is "" then a value of the first argument (view) is returned. // If the second argument (subviewID) is not specified or it is "" then a value of the first argument (view) is returned.
func GetMaxLength(view View, subviewID ...string) int { func GetMaxLength(view View, subviewID ...string) int {
return intStyledProperty(view, subviewID, MaxLength, 0) return intStyledProperty(view, subviewID, MaxLength, 0)
@ -556,31 +461,45 @@ func IsReadOnly(view View, subviewID ...string) bool {
} }
// IsSpellcheck returns a value of the Spellcheck property of EditView. // IsSpellcheck returns a value of the Spellcheck property of EditView.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func IsSpellcheck(view View, subviewID ...string) bool { func IsSpellcheck(view View, subviewID ...string) bool {
return boolStyledProperty(view, subviewID, Spellcheck, false) return boolStyledProperty(view, subviewID, Spellcheck, false)
} }
// GetTextChangedListeners returns the TextChangedListener list of an EditView or MultiLineEditView subview. // GetTextChangedListeners returns the TextChangedListener list of an EditView or MultiLineEditView subview.
// If there are no listeners then the empty list is returned // If there are no listeners then the empty list is returned
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetTextChangedListeners(view View, subviewID ...string) []func(EditView, string) { // Result elements can be of the following types:
return getEventListeners[EditView, string](view, subviewID, EditTextChangedEvent) // - func(rui.EditView, string, string),
// - func(rui.EditView, string),
// - func(rui.EditView),
// - func(string, string),
// - func(string),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTextChangedListeners(view View, subviewID ...string) []any {
return getTwoArgEventRawListeners[EditView, string](view, subviewID, EditTextChangedEvent)
} }
// GetEditViewType returns a value of the Type property of EditView. // GetEditViewType returns a value of the Type property of EditView.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetEditViewType(view View, subviewID ...string) int { func GetEditViewType(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, EditViewType, SingleLineText, false) return enumStyledProperty(view, subviewID, EditViewType, SingleLineText, false)
} }
// GetEditViewPattern returns a value of the Pattern property of EditView. // GetEditViewPattern returns a value of the Pattern property of EditView.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetEditViewPattern(view View, subviewID ...string) string { func GetEditViewPattern(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if pattern, ok := stringProperty(view, EditViewPattern, view.Session()); ok { if pattern, ok := stringProperty(view, EditViewPattern, view.Session()); ok {
return pattern return pattern
} }
@ -596,13 +515,17 @@ func GetEditViewPattern(view View, subviewID ...string) string {
} }
// IsEditViewWrap returns a value of the EditWrap property of MultiLineEditView. // IsEditViewWrap returns a value of the EditWrap property of MultiLineEditView.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func IsEditViewWrap(view View, subviewID ...string) bool { func IsEditViewWrap(view View, subviewID ...string) bool {
return boolStyledProperty(view, subviewID, EditWrap, false) return boolStyledProperty(view, subviewID, EditWrap, false)
} }
// AppendEditText appends the text to the EditView content. // AppendEditText appends the text to the EditView content.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func AppendEditText(view View, subviewID string, text string) { func AppendEditText(view View, subviewID string, text string) {
if subviewID != "" { if subviewID != "" {
if edit := EditViewByID(view, subviewID); edit != nil { if edit := EditViewByID(view, subviewID); edit != nil {
@ -616,8 +539,10 @@ func AppendEditText(view View, subviewID string, text string) {
} }
} }
// GetCaretColor returns the color of the text input carret. // GetCaretColor returns the color of the text input caret.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetCaretColor(view View, subviewID ...string) Color { func GetCaretColor(view View, subviewID ...string) Color {
return colorStyledProperty(view, subviewID, CaretColor, false) return colorStyledProperty(view, subviewID, CaretColor, false)
} }

262
events.go Normal file
View File

@ -0,0 +1,262 @@
package rui
import (
"reflect"
"strings"
)
var eventJsFunc = map[PropertyName]struct{ jsEvent, jsFunc string }{
FocusEvent: {jsEvent: "onfocus", jsFunc: "focusEvent"},
LostFocusEvent: {jsEvent: "onblur", jsFunc: "blurEvent"},
KeyDownEvent: {jsEvent: "onkeydown", jsFunc: "keyDownEvent"},
KeyUpEvent: {jsEvent: "onkeyup", jsFunc: "keyUpEvent"},
ClickEvent: {jsEvent: "onclick", jsFunc: "clickEvent"},
DoubleClickEvent: {jsEvent: "ondblclick", jsFunc: "doubleClickEvent"},
MouseDown: {jsEvent: "onmousedown", jsFunc: "mouseDownEvent"},
MouseUp: {jsEvent: "onmouseup", jsFunc: "mouseUpEvent"},
MouseMove: {jsEvent: "onmousemove", jsFunc: "mouseMoveEvent"},
MouseOut: {jsEvent: "onmouseout", jsFunc: "mouseOutEvent"},
MouseOver: {jsEvent: "onmouseover", jsFunc: "mouseOverEvent"},
ContextMenuEvent: {jsEvent: "oncontextmenu", jsFunc: "contextMenuEvent"},
PointerDown: {jsEvent: "onpointerdown", jsFunc: "pointerDownEvent"},
PointerUp: {jsEvent: "onpointerup", jsFunc: "pointerUpEvent"},
PointerMove: {jsEvent: "onpointermove", jsFunc: "pointerMoveEvent"},
PointerCancel: {jsEvent: "onpointercancel", jsFunc: "pointerCancelEvent"},
PointerOut: {jsEvent: "onpointerout", jsFunc: "pointerOutEvent"},
PointerOver: {jsEvent: "onpointerover", jsFunc: "pointerOverEvent"},
TouchStart: {jsEvent: "ontouchstart", jsFunc: "touchStartEvent"},
TouchEnd: {jsEvent: "ontouchend", jsFunc: "touchEndEvent"},
TouchMove: {jsEvent: "ontouchmove", jsFunc: "touchMoveEvent"},
TouchCancel: {jsEvent: "ontouchcancel", jsFunc: "touchCancelEvent"},
TransitionRunEvent: {jsEvent: "ontransitionrun", jsFunc: "transitionRunEvent"},
TransitionStartEvent: {jsEvent: "ontransitionstart", jsFunc: "transitionStartEvent"},
TransitionEndEvent: {jsEvent: "ontransitionend", jsFunc: "transitionEndEvent"},
TransitionCancelEvent: {jsEvent: "ontransitioncancel", jsFunc: "transitionCancelEvent"},
AnimationStartEvent: {jsEvent: "onanimationstart", jsFunc: "animationStartEvent"},
AnimationEndEvent: {jsEvent: "onanimationend", jsFunc: "animationEndEvent"},
AnimationIterationEvent: {jsEvent: "onanimationiteration", jsFunc: "animationIterationEvent"},
AnimationCancelEvent: {jsEvent: "onanimationcancel", jsFunc: "animationCancelEvent"},
DragEndEvent: {jsEvent: "ondragend", jsFunc: "dragEndEvent"},
DragEnterEvent: {jsEvent: "ondragenter", jsFunc: "dragEnterEvent"},
DragLeaveEvent: {jsEvent: "ondragleave", jsFunc: "dragLeaveEvent"},
}
func viewEventsHtml[T any](view View, events []PropertyName, buffer *strings.Builder) {
for _, tag := range events {
if js, ok := eventJsFunc[tag]; ok {
if value := getOneArgEventListeners[View, T](view, nil, tag); len(value) > 0 {
buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
}
}
}
}
func updateEventListenerHtml(view View, tag PropertyName) {
if js, ok := eventJsFunc[tag]; ok {
value := view.getRaw(tag)
session := view.Session()
htmlID := view.htmlID()
if value == nil {
session.removeProperty(view.htmlID(), js.jsEvent)
} else {
session.updateProperty(htmlID, js.jsEvent, js.jsFunc+"(this, event)")
}
}
}
type noArgListener[V View] interface {
Run(V)
rawListener() any
}
type noArgListener0[V View] struct {
fn func()
}
type noArgListenerV[V View] struct {
fn func(V)
}
type noArgListenerBinding[V View] struct {
name string
}
func newNoArgListener0[V View](fn func()) noArgListener[V] {
obj := new(noArgListener0[V])
obj.fn = fn
return obj
}
func (data *noArgListener0[V]) Run(_ V) {
data.fn()
}
func (data *noArgListener0[V]) rawListener() any {
return data.fn
}
func newNoArgListenerV[V View](fn func(V)) noArgListener[V] {
obj := new(noArgListenerV[V])
obj.fn = fn
return obj
}
func (data *noArgListenerV[V]) Run(view V) {
data.fn(view)
}
func (data *noArgListenerV[V]) rawListener() any {
return data.fn
}
func newNoArgListenerBinding[V View](name string) noArgListener[V] {
obj := new(noArgListenerBinding[V])
obj.name = name
return obj
}
func (data *noArgListenerBinding[V]) Run(view V) {
bind := view.binding()
if bind == nil {
ErrorLogF(`There is no a binding object for call "%s"`, data.name)
return
}
val := reflect.ValueOf(bind)
method := val.MethodByName(data.name)
if !method.IsValid() {
ErrorLogF(`The "%s" method is not valid`, data.name)
return
}
methodType := method.Type()
var args []reflect.Value = nil
switch methodType.NumIn() {
case 0:
args = []reflect.Value{}
case 1:
if equalType(methodType.In(0), reflect.TypeOf(view)) {
args = []reflect.Value{reflect.ValueOf(view)}
}
}
if args != nil {
method.Call(args)
} else {
ErrorLogF(`Unsupported prototype of "%s" method`, data.name)
}
}
func equalType(inType reflect.Type, argType reflect.Type) bool {
return inType == argType || (inType.Kind() == reflect.Interface && argType.Implements(inType))
}
func (data *noArgListenerBinding[V]) rawListener() any {
return data.name
}
func valueToNoArgEventListeners[V View](value any) ([]noArgListener[V], bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case []noArgListener[V]:
return value, true
case noArgListener[V]:
return []noArgListener[V]{value}, true
case string:
return []noArgListener[V]{newNoArgListenerBinding[V](value)}, true
case func(V):
return []noArgListener[V]{newNoArgListenerV(value)}, true
case func():
return []noArgListener[V]{newNoArgListener0[V](value)}, true
case []func(V):
result := make([]noArgListener[V], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newNoArgListenerV(fn))
}
}
return result, len(result) > 0
case []func():
result := make([]noArgListener[V], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newNoArgListener0[V](fn))
}
}
return result, len(result) > 0
case []any:
result := make([]noArgListener[V], 0, len(value))
for _, v := range value {
if v != nil {
switch v := v.(type) {
case func(V):
result = append(result, newNoArgListenerV(v))
case func():
result = append(result, newNoArgListener0[V](v))
case string:
result = append(result, newNoArgListenerBinding[V](v))
default:
return nil, false
}
}
}
return result, len(result) > 0
}
return nil, false
}
func setNoArgEventListener[V View](view View, tag PropertyName, value any) []PropertyName {
if listeners, ok := valueToNoArgEventListeners[V](value); ok {
return setArrayPropertyValue(view, tag, listeners)
}
notCompatibleType(tag, value)
return nil
}
func getNoArgEventListeners[V View](view View, subviewID []string, tag PropertyName) []noArgListener[V] {
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(tag); value != nil {
if result, ok := value.([]noArgListener[V]); ok {
return result
}
}
}
return []noArgListener[V]{}
}
func getNoArgEventRawListeners[V View](view View, subviewID []string, tag PropertyName) []any {
listeners := getNoArgEventListeners[V](view, subviewID, tag)
result := make([]any, len(listeners))
for i, l := range listeners {
result[i] = l.rawListener()
}
return result
}
func getNoArgBinding[V View](listeners []noArgListener[V]) string {
for _, listener := range listeners {
raw := listener.rawListener()
if text, ok := raw.(string); ok && text != "" {
return text
}
}
return ""
}

271
events1arg.go Normal file
View File

@ -0,0 +1,271 @@
package rui
import (
"reflect"
)
type oneArgListener[V View, E any] interface {
Run(V, E)
rawListener() any
}
type oneArgListener0[V View, E any] struct {
fn func()
}
type oneArgListenerV[V View, E any] struct {
fn func(V)
}
type oneArgListenerE[V View, E any] struct {
fn func(E)
}
type oneArgListenerVE[V View, E any] struct {
fn func(V, E)
}
type oneArgListenerBinding[V View, E any] struct {
name string
}
func newOneArgListener0[V View, E any](fn func()) oneArgListener[V, E] {
obj := new(oneArgListener0[V, E])
obj.fn = fn
return obj
}
func (data *oneArgListener0[V, E]) Run(_ V, _ E) {
data.fn()
}
func (data *oneArgListener0[V, E]) rawListener() any {
return data.fn
}
func newOneArgListenerV[V View, E any](fn func(V)) oneArgListener[V, E] {
obj := new(oneArgListenerV[V, E])
obj.fn = fn
return obj
}
func (data *oneArgListenerV[V, E]) Run(view V, _ E) {
data.fn(view)
}
func (data *oneArgListenerV[V, E]) rawListener() any {
return data.fn
}
func newOneArgListenerE[V View, E any](fn func(E)) oneArgListener[V, E] {
obj := new(oneArgListenerE[V, E])
obj.fn = fn
return obj
}
func (data *oneArgListenerE[V, E]) Run(_ V, event E) {
data.fn(event)
}
func (data *oneArgListenerE[V, E]) rawListener() any {
return data.fn
}
func newOneArgListenerVE[V View, E any](fn func(V, E)) oneArgListener[V, E] {
obj := new(oneArgListenerVE[V, E])
obj.fn = fn
return obj
}
func (data *oneArgListenerVE[V, E]) Run(view V, arg E) {
data.fn(view, arg)
}
func (data *oneArgListenerVE[V, E]) rawListener() any {
return data.fn
}
func newOneArgListenerBinding[V View, E any](name string) oneArgListener[V, E] {
obj := new(oneArgListenerBinding[V, E])
obj.name = name
return obj
}
func (data *oneArgListenerBinding[V, E]) Run(view V, event E) {
bind := view.binding()
if bind == nil {
ErrorLogF(`There is no a binding object for call "%s"`, data.name)
return
}
val := reflect.ValueOf(bind)
method := val.MethodByName(data.name)
if !method.IsValid() {
ErrorLogF(`The "%s" method is not valid`, data.name)
return
}
methodType := method.Type()
var args []reflect.Value = nil
switch methodType.NumIn() {
case 0:
args = []reflect.Value{}
case 1:
inType := methodType.In(0)
if equalType(inType, reflect.TypeOf(event)) {
args = []reflect.Value{reflect.ValueOf(event)}
} else if equalType(inType, reflect.TypeOf(view)) {
args = []reflect.Value{reflect.ValueOf(view)}
}
case 2:
if equalType(methodType.In(0), reflect.TypeOf(view)) &&
equalType(methodType.In(1), reflect.TypeOf(event)) {
args = []reflect.Value{reflect.ValueOf(view), reflect.ValueOf(event)}
}
}
if args != nil {
method.Call(args)
} else {
ErrorLogF(`Unsupported prototype of "%s" method`, data.name)
}
}
func (data *oneArgListenerBinding[V, E]) rawListener() any {
return data.name
}
func valueToOneArgEventListeners[V View, E any](value any) ([]oneArgListener[V, E], bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case []oneArgListener[V, E]:
return value, true
case oneArgListener[V, E]:
return []oneArgListener[V, E]{value}, true
case string:
return []oneArgListener[V, E]{newOneArgListenerBinding[V, E](value)}, true
case func(V, E):
return []oneArgListener[V, E]{newOneArgListenerVE(value)}, true
case func(V):
return []oneArgListener[V, E]{newOneArgListenerV[V, E](value)}, true
case func(E):
return []oneArgListener[V, E]{newOneArgListenerE[V](value)}, true
case func():
return []oneArgListener[V, E]{newOneArgListener0[V, E](value)}, true
case []func(V, E):
result := make([]oneArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newOneArgListenerVE(fn))
}
}
return result, len(result) > 0
case []func(E):
result := make([]oneArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newOneArgListenerE[V](fn))
}
}
return result, len(result) > 0
case []func(V):
result := make([]oneArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newOneArgListenerV[V, E](fn))
}
}
return result, len(result) > 0
case []func():
result := make([]oneArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newOneArgListener0[V, E](fn))
}
}
return result, len(result) > 0
case []any:
result := make([]oneArgListener[V, E], 0, len(value))
for _, v := range value {
if v != nil {
switch v := v.(type) {
case func(V, E):
result = append(result, newOneArgListenerVE(v))
case func(E):
result = append(result, newOneArgListenerE[V](v))
case func(V):
result = append(result, newOneArgListenerV[V, E](v))
case func():
result = append(result, newOneArgListener0[V, E](v))
case string:
result = append(result, newOneArgListenerBinding[V, E](v))
default:
return nil, false
}
}
}
return result, len(result) > 0
}
return nil, false
}
func setOneArgEventListener[V View, T any](view View, tag PropertyName, value any) []PropertyName {
if listeners, ok := valueToOneArgEventListeners[V, T](value); ok {
return setArrayPropertyValue(view, tag, listeners)
}
notCompatibleType(tag, value)
return nil
}
func getOneArgEventListeners[V View, E any](view View, subviewID []string, tag PropertyName) []oneArgListener[V, E] {
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(tag); value != nil {
if result, ok := value.([]oneArgListener[V, E]); ok {
return result
}
}
}
return []oneArgListener[V, E]{}
}
func getOneArgEventRawListeners[V View, E any](view View, subviewID []string, tag PropertyName) []any {
listeners := getOneArgEventListeners[V, E](view, subviewID, tag)
result := make([]any, len(listeners))
for i, l := range listeners {
result[i] = l.rawListener()
}
return result
}
func getOneArgBinding[V View, E any](listeners []oneArgListener[V, E]) string {
for _, listener := range listeners {
raw := listener.rawListener()
if text, ok := raw.(string); ok && text != "" {
return text
}
}
return ""
}

345
events2arg.go Normal file
View File

@ -0,0 +1,345 @@
package rui
import "reflect"
type twoArgListener[V View, E any] interface {
Run(V, E, E)
rawListener() any
}
type twoArgListener0[V View, E any] struct {
fn func()
}
type twoArgListenerV[V View, E any] struct {
fn func(V)
}
type twoArgListenerE[V View, E any] struct {
fn func(E)
}
type twoArgListenerVE[V View, E any] struct {
fn func(V, E)
}
type twoArgListenerEE[V View, E any] struct {
fn func(E, E)
}
type twoArgListenerVEE[V View, E any] struct {
fn func(V, E, E)
}
type twoArgListenerBinding[V View, E any] struct {
name string
}
func newTwoArgListener0[V View, E any](fn func()) twoArgListener[V, E] {
obj := new(twoArgListener0[V, E])
obj.fn = fn
return obj
}
func (data *twoArgListener0[V, E]) Run(_ V, _ E, _ E) {
data.fn()
}
func (data *twoArgListener0[V, E]) rawListener() any {
return data.fn
}
func newTwoArgListenerV[V View, E any](fn func(V)) twoArgListener[V, E] {
obj := new(twoArgListenerV[V, E])
obj.fn = fn
return obj
}
func (data *twoArgListenerV[V, E]) Run(view V, _ E, _ E) {
data.fn(view)
}
func (data *twoArgListenerV[V, E]) rawListener() any {
return data.fn
}
func newTwoArgListenerE[V View, E any](fn func(E)) twoArgListener[V, E] {
obj := new(twoArgListenerE[V, E])
obj.fn = fn
return obj
}
func (data *twoArgListenerE[V, E]) Run(_ V, arg E, _ E) {
data.fn(arg)
}
func (data *twoArgListenerE[V, E]) rawListener() any {
return data.fn
}
func newTwoArgListenerVE[V View, E any](fn func(V, E)) twoArgListener[V, E] {
obj := new(twoArgListenerVE[V, E])
obj.fn = fn
return obj
}
func (data *twoArgListenerVE[V, E]) Run(view V, arg E, _ E) {
data.fn(view, arg)
}
func (data *twoArgListenerVE[V, E]) rawListener() any {
return data.fn
}
func newTwoArgListenerEE[V View, E any](fn func(E, E)) twoArgListener[V, E] {
obj := new(twoArgListenerEE[V, E])
obj.fn = fn
return obj
}
func (data *twoArgListenerEE[V, E]) Run(_ V, arg1 E, arg2 E) {
data.fn(arg1, arg2)
}
func (data *twoArgListenerEE[V, E]) rawListener() any {
return data.fn
}
func newTwoArgListenerVEE[V View, E any](fn func(V, E, E)) twoArgListener[V, E] {
obj := new(twoArgListenerVEE[V, E])
obj.fn = fn
return obj
}
func (data *twoArgListenerVEE[V, E]) Run(view V, arg1 E, arg2 E) {
data.fn(view, arg1, arg2)
}
func (data *twoArgListenerVEE[V, E]) rawListener() any {
return data.fn
}
func newTwoArgListenerBinding[V View, E any](name string) twoArgListener[V, E] {
obj := new(twoArgListenerBinding[V, E])
obj.name = name
return obj
}
func (data *twoArgListenerBinding[V, E]) Run(view V, arg1 E, arg2 E) {
bind := view.binding()
if bind == nil {
ErrorLogF(`There is no a binding object for call "%s"`, data.name)
return
}
val := reflect.ValueOf(bind)
method := val.MethodByName(data.name)
if !method.IsValid() {
ErrorLogF(`The "%s" method is not valid`, data.name)
return
}
methodType := method.Type()
var args []reflect.Value = nil
switch methodType.NumIn() {
case 0:
args = []reflect.Value{}
case 1:
inType := methodType.In(0)
if equalType(inType, reflect.TypeOf(arg1)) {
args = []reflect.Value{reflect.ValueOf(arg1)}
} else if equalType(inType, reflect.TypeOf(view)) {
args = []reflect.Value{reflect.ValueOf(view)}
}
case 2:
inType0 := methodType.In(0)
inType1 := methodType.In(1)
if equalType(inType0, reflect.TypeOf(view)) && equalType(inType1, reflect.TypeOf(arg1)) {
args = []reflect.Value{reflect.ValueOf(view), reflect.ValueOf(arg1)}
} else if equalType(inType0, reflect.TypeOf(arg1)) && equalType(inType1, reflect.TypeOf(arg2)) {
args = []reflect.Value{reflect.ValueOf(arg1), reflect.ValueOf(arg2)}
}
case 3:
if equalType(methodType.In(0), reflect.TypeOf(view)) &&
equalType(methodType.In(1), reflect.TypeOf(arg1)) &&
equalType(methodType.In(2), reflect.TypeOf(arg2)) {
args = []reflect.Value{reflect.ValueOf(view), reflect.ValueOf(arg1), reflect.ValueOf(arg2)}
}
}
if args != nil {
method.Call(args)
} else {
ErrorLogF(`Unsupported prototype of "%s" method`, data.name)
}
}
func (data *twoArgListenerBinding[V, E]) rawListener() any {
return data.name
}
func valueToTwoArgEventListeners[V View, E any](value any) ([]twoArgListener[V, E], bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case []twoArgListener[V, E]:
return value, true
case twoArgListener[V, E]:
return []twoArgListener[V, E]{value}, true
case string:
return []twoArgListener[V, E]{newTwoArgListenerBinding[V, E](value)}, true
case func(V, E):
return []twoArgListener[V, E]{newTwoArgListenerVE(value)}, true
case func(V):
return []twoArgListener[V, E]{newTwoArgListenerV[V, E](value)}, true
case func(E):
return []twoArgListener[V, E]{newTwoArgListenerE[V](value)}, true
case func():
return []twoArgListener[V, E]{newTwoArgListener0[V, E](value)}, true
case func(E, E):
return []twoArgListener[V, E]{newTwoArgListenerEE[V](value)}, true
case func(V, E, E):
return []twoArgListener[V, E]{newTwoArgListenerVEE(value)}, true
case []func(V, E):
result := make([]twoArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newTwoArgListenerVE(fn))
}
}
return result, len(result) > 0
case []func(E):
result := make([]twoArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newTwoArgListenerE[V](fn))
}
}
return result, len(result) > 0
case []func(V):
result := make([]twoArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newTwoArgListenerV[V, E](fn))
}
}
return result, len(result) > 0
case []func():
result := make([]twoArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newTwoArgListener0[V, E](fn))
}
}
return result, len(result) > 0
case []func(E, E):
result := make([]twoArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newTwoArgListenerEE[V](fn))
}
}
return result, len(result) > 0
case []func(V, E, E):
result := make([]twoArgListener[V, E], 0, len(value))
for _, fn := range value {
if fn != nil {
result = append(result, newTwoArgListenerVEE(fn))
}
}
return result, len(result) > 0
case []any:
result := make([]twoArgListener[V, E], 0, len(value))
for _, v := range value {
if v != nil {
switch v := v.(type) {
case func(V, E):
result = append(result, newTwoArgListenerVE(v))
case func(E):
result = append(result, newTwoArgListenerE[V](v))
case func(V):
result = append(result, newTwoArgListenerV[V, E](v))
case func():
result = append(result, newTwoArgListener0[V, E](v))
case func(E, E):
result = append(result, newTwoArgListenerEE[V](v))
case func(V, E, E):
result = append(result, newTwoArgListenerVEE(v))
case string:
result = append(result, newTwoArgListenerBinding[V, E](v))
default:
return nil, false
}
}
}
return result, len(result) > 0
}
return nil, false
}
func setTwoArgEventListener[V View, T any](view View, tag PropertyName, value any) []PropertyName {
if listeners, ok := valueToTwoArgEventListeners[V, T](value); ok {
return setArrayPropertyValue(view, tag, listeners)
}
notCompatibleType(tag, value)
return nil
}
func getTwoArgEventListeners[V View, E any](view View, subviewID []string, tag PropertyName) []twoArgListener[V, E] {
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(tag); value != nil {
if result, ok := value.([]twoArgListener[V, E]); ok {
return result
}
}
}
return []twoArgListener[V, E]{}
}
func getTwoArgEventRawListeners[V View, E any](view View, subviewID []string, tag PropertyName) []any {
listeners := getTwoArgEventListeners[V, E](view, subviewID, tag)
result := make([]any, len(listeners))
for i, l := range listeners {
result[i] = l.rawListener()
}
return result
}
func getTwoArgBinding[V View, E any](listeners []twoArgListener[V, E]) string {
for _, listener := range listeners {
raw := listener.rawListener()
if text, ok := raw.(string); ok && text != "" {
return text
}
}
return ""
}

View File

@ -1,38 +1,77 @@
package rui package rui
import ( import (
"encoding/base64"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"time" "time"
) )
// Constants for [FilePicker] specific properties and events
const ( const (
// FileSelectedEvent is the constant for "file-selected-event" property tag. // FileSelectedEvent is the constant for "file-selected-event" property tag.
// The "file-selected-event" is fired when user selects file(s) in the FilePicker. //
FileSelectedEvent = "file-selected-event" // Used by FilePicker.
// Fired when user selects file(s).
//
// General listener format:
// func(picker rui.FilePicker, files []rui.FileInfo).
//
// where:
// picker - Interface of a file picker which generated this event,
// files - Array of description of selected files.
//
// Allowed listener formats:
// func(picker rui.FilePicker)
// func(files []rui.FileInfo)
// func()
FileSelectedEvent PropertyName = "file-selected-event"
// Accept is the constant for "accept" property tag. // Accept is the constant for "accept" property tag.
// The "accept" property of the FilePicker sets the list of allowed file extensions or MIME types. //
Accept = "accept" // Used by FilePicker.
// Set the list of allowed file extensions or MIME types.
//
// Supported types: string, []string.
//
// Internal type is string, other types converted to it during assignment.
//
// Conversion rules:
// - string - may contain single value of multiple separated by comma(,).
// - []string - an array of acceptable file extensions or MIME types.
Accept PropertyName = "accept"
// Multiple is the constant for "multiple" property tag. // Multiple is the constant for "multiple" property tag.
// The "multiple" bool property of the FilePicker sets whether multiple files can be selected //
Multiple = "multiple" // Used by FilePicker.
// Controls whether multiple files can be selected.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", "1" - Several files can be selected.
// - false, 0, "false", "no", "off", "0" - Only one file can be selected.
Multiple PropertyName = "multiple"
) )
// FileInfo describes a file which selected in the FilePicker view // FileInfo describes a file which selected in the FilePicker view
type FileInfo struct { type FileInfo struct {
// Name - the file's name. // Name - the file's name.
Name string Name string
// LastModified specifying the date and time at which the file was last modified // LastModified specifying the date and time at which the file was last modified
LastModified time.Time LastModified time.Time
// Size - the size of the file in bytes. // Size - the size of the file in bytes.
Size int64 Size int64
// MimeType - the file's MIME type. // MimeType - the file's MIME type.
MimeType string MimeType string
data []byte
} }
// FilePicker - the control view for the files selecting // FilePicker represents the FilePicker view
type FilePicker interface { type FilePicker interface {
View View
// Files returns the list of selected files. // Files returns the list of selected files.
@ -45,12 +84,12 @@ type FilePicker interface {
type filePickerData struct { type filePickerData struct {
viewData viewData
files []FileInfo files []FileInfo
fileSelectedListeners []func(FilePicker, []FileInfo) //loader map[int]func(FileInfo, []byte)
loader map[int]func(FileInfo, []byte)
} }
func (file *FileInfo) initBy(node DataValue) { func dataToFileInfo(node DataValue) FileInfo {
var file FileInfo
if obj := node.Object(); obj != nil { if obj := node.Object(); obj != nil {
file.Name, _ = obj.PropertyValue("name") file.Name, _ = obj.PropertyValue("name")
file.MimeType, _ = obj.PropertyValue("mime-type") file.MimeType, _ = obj.PropertyValue("mime-type")
@ -67,6 +106,11 @@ func (file *FileInfo) initBy(node DataValue) {
} }
} }
} }
return file
}
func (file FileInfo) key() string {
return fmt.Sprintf("%s:%d", file.Name, int(file.Size))
} }
// NewFilePicker create new FilePicker object and return it // NewFilePicker create new FilePicker object and return it
@ -78,19 +122,19 @@ func NewFilePicker(session Session, params Params) FilePicker {
} }
func newFilePicker(session Session) View { func newFilePicker(session Session) View {
return NewFilePicker(session, nil) return new(filePickerData) // NewFilePicker(session, nil)
} }
func (picker *filePickerData) init(session Session) { func (picker *filePickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "FilePicker" picker.tag = "FilePicker"
picker.hasHtmlDisabled = true
picker.files = []FileInfo{} picker.files = []FileInfo{}
picker.loader = map[int]func(FileInfo, []byte){} //picker.loader = map[int]func(FileInfo, []byte){}
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){} picker.get = picker.getFunc
} picker.set = picker.setFunc
picker.changed = picker.propertyChanged
func (picker *filePickerData) String() string {
return getViewString(picker)
} }
func (picker *filePickerData) Focusable() bool { func (picker *filePickerData) Focusable() bool {
@ -102,75 +146,47 @@ func (picker *filePickerData) Files() []FileInfo {
} }
func (picker *filePickerData) LoadFile(file FileInfo, result func(FileInfo, []byte)) { func (picker *filePickerData) LoadFile(file FileInfo, result func(FileInfo, []byte)) {
if result == nil { if result != nil {
return for i, info := range picker.files {
} if info.Name == file.Name && info.Size == file.Size && info.LastModified.Equal(file.LastModified) {
if info.data != nil {
for i, info := range picker.files { result(info, info.data)
if info.Name == file.Name && info.Size == file.Size && info.LastModified == file.LastModified { } else {
picker.loader[i] = result picker.fileLoader[info.key()] = func(file FileInfo, data []byte) {
picker.Session().runScript(fmt.Sprintf(`loadSelectedFile("%s", %d)`, picker.htmlID(), i)) picker.files[i].data = data
return result(file, data)
}
picker.Session().callFunc("loadSelectedFile", picker.htmlID(), i)
}
return
}
} }
picker.viewData.LoadFile(file, result)
} }
} }
func (picker *filePickerData) Remove(tag string) { func (picker *filePickerData) getFunc(tag PropertyName) any {
picker.remove(strings.ToLower(tag))
}
func (picker *filePickerData) remove(tag string) {
switch tag { switch tag {
case FileSelectedEvent: case FileSelectedEvent:
if len(picker.fileSelectedListeners) > 0 { if listeners := getOneArgEventRawListeners[FilePicker, []FileInfo](picker, nil, tag); len(listeners) > 0 {
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){} return listeners
picker.propertyChangedEvent(tag)
} }
return nil
case Accept:
delete(picker.properties, tag)
if picker.created {
removeProperty(picker.htmlID(), "accept", picker.Session())
}
picker.propertyChangedEvent(tag)
default:
picker.viewData.remove(tag)
} }
return picker.viewData.getFunc(tag)
} }
func (picker *filePickerData) Set(tag string, value any) bool { func (picker *filePickerData) setFunc(tag PropertyName, value any) []PropertyName {
return picker.set(strings.ToLower(tag), value)
}
func (picker *filePickerData) set(tag string, value any) bool {
if value == nil {
picker.remove(tag)
return true
}
switch tag { switch tag {
case FileSelectedEvent: case FileSelectedEvent:
listeners, ok := valueToEventListeners[FilePicker, []FileInfo](value) return setOneArgEventListener[FilePicker, []FileInfo](picker, tag, value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(FilePicker, []FileInfo){}
}
picker.fileSelectedListeners = listeners
picker.propertyChangedEvent(tag)
return true
case Accept: case Accept:
switch value := value.(type) { switch value := value.(type) {
case string: case string:
value = strings.Trim(value, " \t\n") return setStringPropertyValue(picker, Accept, strings.Trim(value, " \t\n"))
if value == "" {
picker.remove(Accept)
} else {
picker.properties[Accept] = value
}
case []string: case []string:
buffer := allocStringBuilder() buffer := allocStringBuilder()
@ -184,29 +200,27 @@ func (picker *filePickerData) set(tag string, value any) bool {
buffer.WriteString(val) buffer.WriteString(val)
} }
} }
if buffer.Len() == 0 { return setStringPropertyValue(picker, Accept, buffer.String())
picker.remove(Accept)
} else {
picker.properties[Accept] = buffer.String()
}
default:
notCompatibleType(tag, value)
return false
} }
notCompatibleType(tag, value)
return nil
}
if picker.created { return picker.viewData.setFunc(tag, value)
if css := picker.acceptCSS(); css != "" { }
updateProperty(picker.htmlID(), "accept", css, picker.Session())
} else { func (picker *filePickerData) propertyChanged(tag PropertyName) {
removeProperty(picker.htmlID(), "accept", picker.Session()) switch tag {
} case Accept:
session := picker.Session()
if css := acceptPropertyCSS(picker); css != "" {
session.updateProperty(picker.htmlID(), "accept", css)
} else {
session.removeProperty(picker.htmlID(), "accept")
} }
picker.propertyChangedEvent(tag)
return true
default: default:
return picker.viewData.set(tag, value) picker.viewData.propertyChanged(tag)
} }
} }
@ -214,10 +228,10 @@ func (picker *filePickerData) htmlTag() string {
return "input" return "input"
} }
func (picker *filePickerData) acceptCSS() string { func acceptPropertyCSS(view View) string {
accept, ok := stringProperty(picker, Accept, picker.Session()) accept, ok := stringProperty(view, Accept, view.Session())
if !ok { if !ok {
if value := valueFromStyle(picker, Accept); value != nil { if value := valueFromStyle(view, Accept); value != nil {
accept, ok = value.(string) accept, ok = value.(string)
} }
} }
@ -230,7 +244,7 @@ func (picker *filePickerData) acceptCSS() string {
if buffer.Len() > 0 { if buffer.Len() > 0 {
buffer.WriteString(", ") buffer.WriteString(", ")
} }
if value[0] != '.' && !strings.Contains(value, "/") { if value[0] != '.' && !strings.ContainsRune(value, '/') {
buffer.WriteRune('.') buffer.WriteRune('.')
} }
buffer.WriteString(value) buffer.WriteString(value)
@ -244,7 +258,7 @@ func (picker *filePickerData) acceptCSS() string {
func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder) { func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder) {
picker.viewData.htmlProperties(self, buffer) picker.viewData.htmlProperties(self, buffer)
if accept := picker.acceptCSS(); accept != "" { if accept := acceptPropertyCSS(picker); accept != "" {
buffer.WriteString(` accept="`) buffer.WriteString(` accept="`)
buffer.WriteString(accept) buffer.WriteString(accept)
buffer.WriteRune('"') buffer.WriteRune('"')
@ -261,69 +275,27 @@ func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder)
} }
} }
func (picker *filePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) { func parseFilesTag(data DataObject) []FileInfo {
if IsDisabled(self) { if node := data.PropertyByTag("files"); node != nil && node.Type() == ArrayNode {
buffer.WriteString(` disabled`) count := node.ArraySize()
files := make([]FileInfo, count)
for i := range count {
if value := node.ArrayElement(i); value != nil {
files[i] = dataToFileInfo(value)
}
}
return files
} }
picker.viewData.htmlDisabledProperties(self, buffer) return nil
} }
func (picker *filePickerData) handleCommand(self View, command string, data DataObject) bool { func (picker *filePickerData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command { switch command {
case "fileSelected": case "fileSelected":
if node := data.PropertyWithTag("files"); node != nil && node.Type() == ArrayNode { if files := parseFilesTag(data); files != nil {
count := node.ArraySize()
files := make([]FileInfo, count)
for i := 0; i < count; i++ {
if value := node.ArrayElement(i); value != nil {
files[i].initBy(value)
}
}
picker.files = files picker.files = files
for _, listener := range getOneArgEventListeners[FilePicker, []FileInfo](picker, nil, FileSelectedEvent) {
for _, listener := range picker.fileSelectedListeners { listener.Run(picker, files)
listener(picker, files)
}
}
return true
case "fileLoaded":
if index, ok := dataIntProperty(data, "index"); ok {
if result, ok := picker.loader[index]; ok {
var file FileInfo
file.initBy(data)
var fileData []byte = nil
if base64Data, ok := data.PropertyValue("data"); ok {
if index := strings.LastIndex(base64Data, ","); index >= 0 {
base64Data = base64Data[index+1:]
}
decode, err := base64.StdEncoding.DecodeString(base64Data)
if err == nil {
fileData = decode
} else {
ErrorLog(err.Error())
}
}
result(file, fileData)
delete(picker.loader, index)
}
}
return true
case "fileLoadingError":
if error, ok := data.PropertyValue("error"); ok {
ErrorLog(error)
}
if index, ok := dataIntProperty(data, "index"); ok {
if result, ok := picker.loader[index]; ok {
if index >= 0 && index < len(picker.files) {
result(picker.files[index], nil)
} else {
result(FileInfo{}, nil)
}
delete(picker.loader, index)
} }
} }
return true return true
@ -357,18 +329,19 @@ func LoadFilePickerFile(view View, subviewID string, file FileInfo, result func(
} }
// IsMultipleFilePicker returns "true" if multiple files can be selected in the FilePicker, "false" otherwise. // IsMultipleFilePicker returns "true" if multiple files can be selected in the FilePicker, "false" otherwise.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func IsMultipleFilePicker(view View, subviewID ...string) bool { func IsMultipleFilePicker(view View, subviewID ...string) bool {
return boolStyledProperty(view, subviewID, Multiple, false) return boolStyledProperty(view, subviewID, Multiple, false)
} }
// GetFilePickerAccept returns sets the list of allowed file extensions or MIME types. // GetFilePickerAccept returns sets the list of allowed file extensions or MIME types.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetFilePickerAccept(view View, subviewID ...string) []string { func GetFilePickerAccept(view View, subviewID ...string) []string {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0])
}
if view != nil {
accept, ok := stringProperty(view, Accept, view.Session()) accept, ok := stringProperty(view, Accept, view.Session())
if !ok { if !ok {
if value := valueFromStyle(view, Accept); value != nil { if value := valueFromStyle(view, Accept); value != nil {
@ -377,7 +350,7 @@ func GetFilePickerAccept(view View, subviewID ...string) []string {
} }
if ok { if ok {
result := strings.Split(accept, ",") result := strings.Split(accept, ",")
for i := 0; i < len(result); i++ { for i := range len(result) {
result[i] = strings.Trim(result[i], " \t\n") result[i] = strings.Trim(result[i], " \t\n")
} }
return result return result
@ -388,7 +361,16 @@ func GetFilePickerAccept(view View, subviewID ...string) []string {
// GetFileSelectedListeners returns the "file-selected-event" listener list. // GetFileSelectedListeners returns the "file-selected-event" listener list.
// If there are no listeners then the empty list is returned. // If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetFileSelectedListeners(view View, subviewID ...string) []func(FilePicker, []FileInfo) { // Result elements can be of the following types:
return getEventListeners[FilePicker, []FileInfo](view, subviewID, FileSelectedEvent) // - func(rui.View, []rui.FileInfo),
// - func(rui.View),
// - func([]rui.FileInfo),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetFileSelectedListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[FilePicker, []FileInfo](view, subviewID, FileSelectedEvent)
} }

341
filter.go Normal file
View File

@ -0,0 +1,341 @@
package rui
import (
"fmt"
"strings"
)
// Constants for [FilterProperty] specific properties and events
const (
// Blur is the constant for "blur" property tag.
//
// Used by FilterProperty.
// Applies a Gaussian blur. The value of radius defines the value of the standard deviation to the Gaussian function, or
// how many pixels on the screen blend into each other, so a larger value will create more blur. The lacuna value for
// interpolation is 0. The parameter is specified as a length in pixels.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
Blur PropertyName = "blur"
// Brightness is the constant for "brightness" property tag.
//
// Used by FilterProperty.
// Applies a linear multiplier to input image, making it appear more or less bright. A value of 0% will create an image
// that is completely black. A value of 100% leaves the input unchanged. Other values are linear multipliers on the
// effect. Values of an amount over 100% are allowed, providing brighter results.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
Brightness PropertyName = "brightness"
// Contrast is the constant for "contrast" property tag.
//
// Used by FilterProperty.
// Adjusts the contrast of the input. A value of 0% will create an image that is completely black. A value of 100% leaves
// the input unchanged. Values of amount over 100% are allowed, providing results with less contrast.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
Contrast PropertyName = "contrast"
// DropShadow is the constant for "drop-shadow" property tag.
//
// Used by FilterProperty.
// Applies a drop shadow effect to the input image. A drop shadow is effectively a blurred, offset version of the input
// image's alpha mask drawn in a particular color, composited below the image. Shadow parameters are set using the
// ShadowProperty interface.
//
// Supported types: []ShadowProperty, ShadowProperty, string.
//
// Internal type is []ShadowProperty, other types converted to it during assignment.
// See ShadowProperty description for more details.
//
// Conversion rules:
// - []ShadowProperty - stored as is, no conversion performed.
// - ShadowProperty - converted to []ShadowProperty.
// - string - string representation of ShadowProperty. Example: "_{blur = 1em, color = black, spread-radius = 0.5em}".
DropShadow PropertyName = "drop-shadow"
// Grayscale is the constant for "grayscale" property tag.
//
// Used by FilterProperty.
// Converts the input image to grayscale. The value of amount defines the proportion of the conversion. A value of 100%
// is completely grayscale. A value of 0% leaves the input unchanged. Values between 0% and 100% are linear multipliers on
// the effect.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
Grayscale PropertyName = "grayscale"
// HueRotate is the constant for "hue-rotate" property tag.
//
// Used by FilterProperty.
// Applies a hue rotation on the input image. The value of angle defines the number of degrees around the color circle
// the input samples will be adjusted. A value of 0deg leaves the input unchanged. If the angle parameter is missing, a
// value of 0deg is used. Though there is no maximum value, the effect of values above 360deg wraps around.
//
// Supported types: AngleUnit, string, float, int.
//
// Internal type is AngleUnit, other types will be converted to it during assignment.
// See AngleUnit description for more details.
//
// Conversion rules:
// - AngleUnit - stored as is, no conversion performed.
// - string - must contain string representation of AngleUnit. If numeric value will be provided without any suffix then AngleUnit with value and Radian value type will be created.
// - float - a new AngleUnit value will be created with Radian as a type.
// - int - a new AngleUnit value will be created with Radian as a type.
HueRotate PropertyName = "hue-rotate"
// Invert is the constant for "invert" property tag.
//
// Used by FilterProperty.
// Inverts the samples in the input image. The value of amount defines the proportion of the conversion. A value of 100%
// is completely inverted. A value of 0% leaves the input unchanged. Values between 0% and 100% are linear multipliers on
// the effect.
//
// Supported types: float64, int, string.
//
// Internal type is float, other types converted to it during assignment.
Invert PropertyName = "invert"
// Saturate is the constant for "saturate" property tag.
//
// Used by FilterProperty.
// Saturates the input image. The value of amount defines the proportion of the conversion. A value of 0% is completely
// un-saturated. A value of 100% leaves the input unchanged. Other values are linear multipliers on the effect. Values of
// amount over 100% are allowed, providing super-saturated results.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
Saturate PropertyName = "saturate"
// Sepia is the constant for "sepia" property tag.
//
// Used by FilterProperty.
// Converts the input image to sepia. The value of amount defines the proportion of the conversion. A value of 100% is
// completely sepia. A value of 0% leaves the input unchanged. Values between 0% and 100% are linear multipliers on the
// effect.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
Sepia PropertyName = "sepia"
)
// FilterProperty defines an applied to a View a graphical effects like blur or color shift.
// Allowable properties are Blur, Brightness, Contrast, DropShadow, Grayscale, HueRotate, Invert, Opacity, Saturate, and Sepia
type FilterProperty interface {
Properties
fmt.Stringer
stringWriter
cssStyle(session Session) string
}
type filterData struct {
dataProperty
}
// NewFilterProperty creates the new FilterProperty
func NewFilterProperty(params Params) FilterProperty {
if len(params) > 0 {
filter := new(filterData)
filter.init()
for tag, value := range params {
if !filter.Set(tag, value) {
return nil
}
}
return filter
}
return nil
}
func newFilterProperty(obj DataObject) FilterProperty {
filter := new(filterData)
filter.init()
for node := range obj.Properties() {
tag := node.Tag()
switch node.Type() {
case TextNode:
filter.Set(PropertyName(tag), node.Text())
case ObjectNode:
if tag == string(HueRotate) {
// TODO
} else {
ErrorLog(`Invalid value of "` + tag + `"`)
}
default:
ErrorLog(`Invalid value of "` + tag + `"`)
}
}
if len(filter.properties) > 0 {
return filter
}
ErrorLog("Empty view filter")
return nil
}
func (filter *filterData) init() {
filter.dataProperty.init()
filter.set = filterDataSet
filter.supportedProperties = []PropertyName{Blur, Brightness, Contrast, Saturate, Grayscale, Invert, Opacity, Sepia, HueRotate, DropShadow}
}
func filterDataSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case Blur, Brightness, Contrast, Saturate:
return setFloatProperty(properties, tag, value, 0, 10000)
case Grayscale, Invert, Opacity, Sepia:
return setFloatProperty(properties, tag, value, 0, 100)
case HueRotate:
return setAngleProperty(properties, tag, value)
case DropShadow:
if setShadowProperty(properties, tag, value) {
return []PropertyName{tag}
}
}
ErrorLogF(`"%s" property is not supported by the view filter`, tag)
return nil
}
func (filter *filterData) String() string {
return runStringWriter(filter)
}
func (filter *filterData) writeString(buffer *strings.Builder, indent string) {
filter.writeToBuffer(buffer, indent, "filter", filter.AllTags())
}
func (filter *filterData) cssStyle(session Session) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if value, ok := floatTextProperty(filter, Blur, session, 0); ok {
buffer.WriteString(string(Blur))
buffer.WriteRune('(')
buffer.WriteString(value)
buffer.WriteString("px)")
}
for _, tag := range []PropertyName{Brightness, Contrast, Saturate, Grayscale, Invert, Opacity, Sepia} {
if value, ok := floatTextProperty(filter, tag, session, 0); ok {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString(string(tag))
buffer.WriteRune('(')
buffer.WriteString(value)
buffer.WriteString("%)")
}
}
if value, ok := angleProperty(filter, HueRotate, session); ok {
if buffer.Len() > 0 {
buffer.WriteRune(' ')
}
buffer.WriteString(string(HueRotate))
buffer.WriteRune('(')
buffer.WriteString(value.cssString())
buffer.WriteRune(')')
}
var lead string
if buffer.Len() > 0 {
lead = " drop-shadow("
} else {
lead = "drop-shadow("
}
for _, shadow := range getShadows(filter, DropShadow) {
if shadow.cssTextStyle(buffer, session, lead) {
buffer.WriteRune(')')
lead = " drop-shadow("
}
}
return buffer.String()
}
func setFilterProperty(properties Properties, tag PropertyName, value any) []PropertyName {
switch value := value.(type) {
case FilterProperty:
properties.setRaw(tag, value)
return []PropertyName{tag}
case string:
if obj := NewDataObject(value); obj == nil {
if filter := newFilterProperty(obj); filter != nil {
properties.setRaw(tag, filter)
return []PropertyName{tag}
}
}
case DataObject:
if filter := newFilterProperty(value); filter != nil {
properties.setRaw(tag, filter)
return []PropertyName{tag}
}
case DataValue:
if value.IsObject() {
if filter := newFilterProperty(value.Object()); filter != nil {
properties.setRaw(tag, filter)
return []PropertyName{tag}
}
}
}
notCompatibleType(tag, value)
return nil
}
// GetFilter returns a View graphical effects like blur or color shift.
// If the second argument (subviewID) is not specified or it is "" then a top position of the first argument (view) is returned
func GetFilter(view View, subviewID ...string) FilterProperty {
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(Filter); value != nil {
if filter, ok := value.(FilterProperty); ok {
return filter
}
}
if value := valueFromStyle(view, Filter); value != nil {
if filter, ok := value.(FilterProperty); ok {
return filter
}
}
}
return nil
}
// GetBackdropFilter returns the area behind a View graphical effects like blur or color shift.
// If the second argument (subviewID) is not specified or it is "" then a top position of the first argument (view) is returned
func GetBackdropFilter(view View, subviewID ...string) FilterProperty {
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(BackdropFilter); value != nil {
if filter, ok := value.(FilterProperty); ok {
return filter
}
}
if value := valueFromStyle(view, BackdropFilter); value != nil {
if filter, ok := value.(FilterProperty); ok {
return filter
}
}
}
return nil
}

View File

@ -2,160 +2,74 @@ package rui
import "strings" import "strings"
// Constants which represent [View] specific focus events properties
const ( const (
// FocusEvent is the constant for "focus-event" property tag. // FocusEvent is the constant for "focus-event" property tag.
// The "focus-event" event occurs when the View takes input focus. //
// The main listener format: // Used by View.
// func(View). // Occur when the view takes input focus.
// The additional listener format: //
// func(). // General listener format:
FocusEvent = "focus-event" // func(rui.View).
//
// where:
// view - Interface of a view which generated this event.
//
// Allowed listener formats:
// func().
FocusEvent PropertyName = "focus-event"
// LostFocusEvent is the constant for "lost-focus-event" property tag. // LostFocusEvent is the constant for "lost-focus-event" property tag.
// The "lost-focus-event" event occurs when the View lost input focus. //
// The main listener format: // Used by View.
// func(View). // Occur when the View lost input focus.
// The additional listener format: //
// func(). // General listener format:
LostFocusEvent = "lost-focus-event" // func(view rui.View).
//
// where:
// view - Interface of a view which generated this event.
//
// Allowed listener formats:
// func()
LostFocusEvent PropertyName = "lost-focus-event"
) )
func valueToNoParamListeners[V any](value any) ([]func(V), bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case func(V):
return []func(V){value}, true
case func():
fn := func(V) {
value()
}
return []func(V){fn}, true
case []func(V):
if len(value) == 0 {
return nil, true
}
for _, fn := range value {
if fn == nil {
return nil, false
}
}
return value, true
case []func():
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(V) {
v()
}
}
return listeners, true
case []any:
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V), count)
for i, v := range value {
if v == nil {
return nil, false
}
switch v := v.(type) {
case func(V):
listeners[i] = v
case func():
listeners[i] = func(V) {
v()
}
default:
return nil, false
}
}
return listeners, true
}
return nil, false
}
var focusEvents = map[string]struct{ jsEvent, jsFunc string }{
FocusEvent: {jsEvent: "onfocus", jsFunc: "focusEvent"},
LostFocusEvent: {jsEvent: "onblur", jsFunc: "blurEvent"},
}
func (view *viewData) setFocusListener(tag string, value any) bool {
listeners, ok := valueToNoParamListeners[View](value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeFocusListener(tag)
} else if js, ok := focusEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
}
} else {
return false
}
return true
}
func (view *viewData) removeFocusListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := focusEvents[tag]; ok {
removeProperty(view.htmlID(), js.jsEvent, view.Session())
}
}
}
func getFocusListeners(view View, subviewID []string, tag string) []func(View) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.Get(tag); value != nil {
if result, ok := value.([]func(View)); ok {
return result
}
}
}
return []func(View){}
}
func focusEventsHtml(view View, buffer *strings.Builder) { func focusEventsHtml(view View, buffer *strings.Builder) {
if view.Focusable() { if view.Focusable() {
for _, js := range focusEvents { for _, tag := range []PropertyName{FocusEvent, LostFocusEvent} {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `) if js, ok := eventJsFunc[tag]; ok {
buffer.WriteString(js.jsEvent)
buffer.WriteString(`="`)
buffer.WriteString(js.jsFunc)
buffer.WriteString(`(this, event)" `)
}
} }
} }
} }
// GetFocusListeners returns a FocusListener list. If there are no listeners then the empty list is returned // GetFocusListeners returns a FocusListener list. If there are no listeners then the empty list is returned
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetFocusListeners(view View, subviewID ...string) []func(View) { // Result elements can be of the following types:
return getFocusListeners(view, subviewID, FocusEvent) // - func(rui.View),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetFocusListeners(view View, subviewID ...string) []any {
return getNoArgEventRawListeners[View](view, subviewID, FocusEvent)
} }
// GetLostFocusListeners returns a LostFocusListener list. If there are no listeners then the empty list is returned // GetLostFocusListeners returns a LostFocusListener list. If there are no listeners then the empty list is returned
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetLostFocusListeners(view View, subviewID ...string) []func(View) { // Result elements can be of the following types:
return getFocusListeners(view, subviewID, LostFocusEvent) // - func(rui.View),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetLostFocusListeners(view View, subviewID ...string) []any {
return getNoArgEventRawListeners[View](view, subviewID, LostFocusEvent)
} }

12
go.mod
View File

@ -1,5 +1,13 @@
module github.com/anoshenko/rui module github.com/anoshenko/rui
go 1.18 go 1.24
require github.com/gorilla/websocket v1.5.0 require (
github.com/gorilla/websocket v1.5.3
golang.org/x/crypto v0.37.0
)
require (
golang.org/x/net v0.39.0 // indirect
golang.org/x/text v0.24.0 // indirect
)

18
go.sum
View File

@ -1,2 +1,16 @@
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=

View File

@ -5,13 +5,116 @@ import (
"strings" "strings"
) )
// GridLayout - grid-container of View // Constants related to [GridLayout] specific properties and events
const (
// CellVerticalAlign is the constant for "cell-vertical-align" property tag.
//
// Used by GridLayout, SvgImageView.
//
// Usage in GridLayout:
// Sets the default vertical alignment of GridLayout children within the cell they are occupying.
//
// Supported types: int, string.
//
// Values:
// - 0 (TopAlign) or "top" - Top alignment.
// - 1 (BottomAlign) or "bottom" - Bottom alignment.
// - 2 (CenterAlign) or "center" - Center alignment.
// - 3 (StretchAlign) or "stretch" - Full height stretch.
//
// Usage in SvgImageView:
// Same as "vertical-align".
CellVerticalAlign PropertyName = "cell-vertical-align"
// CellHorizontalAlign is the constant for "cell-horizontal-align" property tag.
//
// Used by GridLayout, SvgImageView.
//
// Usage in GridLayout:
// Sets the default horizontal alignment of GridLayout children within the occupied cell.
//
// Supported types: int, string.
//
// Values:
// - 0 (LeftAlign) or "left" - Left alignment.
// - 1 (RightAlign) or "right" - Right alignment.
// - 2 (CenterAlign) or "center" - Center alignment.
// - 3 (StretchAlign) or "stretch" - Full width stretch.
//
// Usage in SvgImageView:
// Same as "horizontal-align".
CellHorizontalAlign PropertyName = "cell-horizontal-align"
// CellVerticalSelfAlign is the constant for "cell-vertical-self-align" property tag.
//
// Used by GridLayout.
// Sets the vertical alignment of GridLayout children within the cell they are occupying. The property is set for the
// child view of GridLayout.
//
// Supported types: int, string.
//
// Values:
// - 0 (TopAlign) or "top" - Top alignment.
// - 1 (BottomAlign) or "bottom" - Bottom alignment.
// - 2 (CenterAlign) or "center" - Center alignment.
// - 3 (StretchAlign) or "stretch" - Full height stretch.
CellVerticalSelfAlign PropertyName = "cell-vertical-self-align"
// CellHorizontalSelfAlign is the constant for "cell-horizontal-self-align" property tag.
//
// Used by GridLayout.
// Sets the horizontal alignment of GridLayout children within the occupied cell. The property is set for the child view
// of GridLayout.
//
// Supported types: int, string.
//
// Values:
// - 0 (LeftAlign) or "left" - Left alignment.
// - 1 (RightAlign) or "right" - Right alignment.
// - 2 (CenterAlign) or "center" - Center alignment.
// - 3 (StretchAlign) or "stretch" - Full width stretch.
CellHorizontalSelfAlign PropertyName = "cell-horizontal-self-align"
)
// GridAdapter is an interface to define [GridLayout] content. [GridLayout] will query interface functions to populate
// its content
type GridAdapter interface {
// GridColumnCount returns the number of columns in the grid
GridColumnCount() int
// GridRowCount returns the number of rows in the grid
GridRowCount() int
// GridCellContent creates a View at the given cell
GridCellContent(row, column int, session Session) View
}
// GridCellColumnSpanAdapter implements the optional method of the [GridAdapter] interface
type GridCellColumnSpanAdapter interface {
// GridCellColumnSpan returns the number of columns that a cell spans.
// Values less than 1 are ignored.
GridCellColumnSpan(row, column int) int
}
// GridCellColumnSpanAdapter implements the optional method of the [GridAdapter] interface
type GridCellRowSpanAdapter interface {
// GridCellRowSpan returns the number of rows that a cell spans
// Values less than 1 are ignored.
GridCellRowSpan(row, column int) int
}
// GridLayout represents a GridLayout view
type GridLayout interface { type GridLayout interface {
ViewsContainer ViewsContainer
// UpdateContent updates child Views if the "content" property value is set to GridAdapter,
// otherwise does nothing
UpdateGridContent()
} }
type gridLayoutData struct { type gridLayoutData struct {
viewsContainerData viewsContainerData
adapter GridAdapter
} }
// NewGridLayout create new GridLayout object and return it // NewGridLayout create new GridLayout object and return it
@ -23,7 +126,8 @@ func NewGridLayout(session Session, params Params) GridLayout {
} }
func newGridLayout(session Session) View { func newGridLayout(session Session) View {
return NewGridLayout(session, nil) //return NewGridLayout(session, nil)
return new(gridLayoutData)
} }
// Init initialize fields of GridLayout by default values // Init initialize fields of GridLayout by default values
@ -31,20 +135,22 @@ func (gridLayout *gridLayoutData) init(session Session) {
gridLayout.viewsContainerData.init(session) gridLayout.viewsContainerData.init(session)
gridLayout.tag = "GridLayout" gridLayout.tag = "GridLayout"
gridLayout.systemClass = "ruiGridLayout" gridLayout.systemClass = "ruiGridLayout"
gridLayout.adapter = nil
gridLayout.normalize = normalizeGridLayoutTag
gridLayout.get = gridLayout.getFunc
gridLayout.set = gridLayout.setFunc
gridLayout.remove = gridLayout.removeFunc
gridLayout.changed = gridLayout.propertyChanged
} }
func (gridLayout *gridLayoutData) String() string { func setGridCellSize(properties Properties, tag PropertyName, value any) []PropertyName {
return getViewString(gridLayout)
}
func (style *viewStyle) setGridCellSize(tag string, value any) bool {
setValues := func(values []string) bool { setValues := func(values []string) bool {
count := len(values) count := len(values)
if count > 1 { if count > 1 {
sizes := make([]any, count) sizes := make([]any, count)
for i, val := range values { for i, val := range values {
val = strings.Trim(val, " \t\n\r") val = strings.Trim(val, " \t\n\r")
if isConstantName(val) { if ok, _ := isConstantName(val); ok {
sizes[i] = val sizes[i] = val
} else if fn := parseSizeFunc(val); fn != nil { } else if fn := parseSizeFunc(val); fn != nil {
sizes[i] = SizeUnit{Type: SizeFunction, Function: fn} sizes[i] = SizeUnit{Type: SizeFunction, Function: fn}
@ -55,11 +161,11 @@ func (style *viewStyle) setGridCellSize(tag string, value any) bool {
return false return false
} }
} }
style.properties[tag] = sizes properties.setRaw(tag, sizes)
} else if isConstantName(values[0]) { } else if ok, _ := isConstantName(values[0]); ok {
style.properties[tag] = values[0] properties.setRaw(tag, values[0])
} else if size, err := stringToSizeUnit(values[0]); err == nil { } else if size, err := stringToSizeUnit(values[0]); err == nil {
style.properties[tag] = size properties.setRaw(tag, size)
} else { } else {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return false return false
@ -71,41 +177,41 @@ func (style *viewStyle) setGridCellSize(tag string, value any) bool {
case CellWidth, CellHeight: case CellWidth, CellHeight:
switch value := value.(type) { switch value := value.(type) {
case SizeUnit, []SizeUnit: case SizeUnit, []SizeUnit:
style.properties[tag] = value properties.setRaw(tag, value)
case string: case string:
if !setValues(strings.Split(value, ",")) { if !setValues(strings.Split(value, ",")) {
return false return nil
} }
case []string: case []string:
if !setValues(value) { if !setValues(value) {
return false return nil
} }
case []DataValue: case []DataValue:
count := len(value) count := len(value)
if count == 0 { if count == 0 {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return false return nil
} }
values := make([]string, count) values := make([]string, count)
for i, val := range value { for i, val := range value {
if val.IsObject() { if val.IsObject() {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return false return nil
} }
values[i] = val.Value() values[i] = val.Value()
} }
if !setValues(values) { if !setValues(values) {
return false return nil
} }
case []any: case []any:
count := len(value) count := len(value)
if count == 0 { if count == 0 {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return false return nil
} }
sizes := make([]any, count) sizes := make([]any, count)
for i, val := range value { for i, val := range value {
@ -114,35 +220,35 @@ func (style *viewStyle) setGridCellSize(tag string, value any) bool {
sizes[i] = val sizes[i] = val
case string: case string:
if isConstantName(val) { if ok, _ := isConstantName(val); ok {
sizes[i] = val sizes[i] = val
} else if size, err := stringToSizeUnit(val); err == nil { } else if size, err := stringToSizeUnit(val); err == nil {
sizes[i] = size sizes[i] = size
} else { } else {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return false return nil
} }
default: default:
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return false return nil
} }
} }
style.properties[tag] = sizes properties.setRaw(tag, sizes)
default: default:
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return nil
} }
return true return []PropertyName{tag}
} }
return false return nil
} }
func (style *viewStyle) gridCellSizesCSS(tag string, session Session) string { func gridCellSizesCSS(properties Properties, tag PropertyName, session Session) string {
switch cellSize := gridCellSizes(style, tag, session); len(cellSize) { switch cellSize := gridCellSizes(properties, tag, session); len(cellSize) {
case 0: case 0:
case 1: case 1:
@ -179,8 +285,8 @@ func (style *viewStyle) gridCellSizesCSS(tag string, session Session) string {
return "" return ""
} }
func (gridLayout *gridLayoutData) normalizeTag(tag string) string { func normalizeGridLayoutTag(tag PropertyName) PropertyName {
tag = strings.ToLower(tag) tag = defaultNormalize(tag)
switch tag { switch tag {
case VerticalAlign: case VerticalAlign:
return CellVerticalAlign return CellVerticalAlign
@ -197,84 +303,166 @@ func (gridLayout *gridLayoutData) normalizeTag(tag string) string {
return tag return tag
} }
func (gridLayout *gridLayoutData) Get(tag string) any { func (gridLayout *gridLayoutData) getFunc(tag PropertyName) any {
return gridLayout.get(gridLayout.normalizeTag(tag)) switch tag {
} case Gap:
func (gridLayout *gridLayoutData) get(tag string) any {
if tag == Gap {
rowGap := GetGridRowGap(gridLayout) rowGap := GetGridRowGap(gridLayout)
columnGap := GetGridColumnGap(gridLayout) columnGap := GetGridColumnGap(gridLayout)
if rowGap.Equal(columnGap) { if rowGap.Equal(columnGap) {
return rowGap return rowGap
} }
return AutoSize() return AutoSize()
}
return gridLayout.viewsContainerData.get(tag)
}
func (gridLayout *gridLayoutData) Remove(tag string) {
gridLayout.remove(gridLayout.normalizeTag(tag))
}
func (gridLayout *gridLayoutData) remove(tag string) {
if tag == Gap {
gridLayout.remove(GridRowGap)
gridLayout.remove(GridColumnGap)
gridLayout.propertyChangedEvent(Gap)
return
}
gridLayout.viewsContainerData.remove(tag)
if gridLayout.created {
switch tag {
case CellWidth:
updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`,
gridLayout.gridCellSizesCSS(CellWidth, gridLayout.session), gridLayout.session)
case CellHeight:
updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`,
gridLayout.gridCellSizesCSS(CellHeight, gridLayout.session), gridLayout.session)
case Content:
if gridLayout.adapter != nil {
return gridLayout.adapter
} }
} }
return gridLayout.viewsContainerData.getFunc(tag)
} }
func (gridLayout *gridLayoutData) Set(tag string, value any) bool { func (gridLayout *gridLayoutData) removeFunc(tag PropertyName) []PropertyName {
return gridLayout.set(gridLayout.normalizeTag(tag), value) switch tag {
} case Gap:
result := []PropertyName{}
func (gridLayout *gridLayoutData) set(tag string, value any) bool { for _, tag := range []PropertyName{GridRowGap, GridColumnGap} {
if value == nil { if gridLayout.getRaw(tag) != nil {
gridLayout.remove(tag) gridLayout.setRaw(tag, nil)
return true result = append(result, tag)
}
if tag == Gap {
return gridLayout.set(GridRowGap, value) && gridLayout.set(GridColumnGap, value)
}
if gridLayout.viewsContainerData.set(tag, value) {
if gridLayout.created {
switch tag {
case CellWidth:
updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`,
gridLayout.gridCellSizesCSS(CellWidth, gridLayout.session), gridLayout.session)
case CellHeight:
updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`,
gridLayout.gridCellSizesCSS(CellHeight, gridLayout.session), gridLayout.session)
} }
} }
return true return result
case Content:
if len(gridLayout.views) > 0 || gridLayout.adapter != nil {
gridLayout.views = []View{}
gridLayout.adapter = nil
return []PropertyName{Content}
}
return []PropertyName{}
} }
return false return gridLayout.viewsContainerData.removeFunc(tag)
} }
func gridCellSizes(properties Properties, tag string, session Session) []SizeUnit { func (gridLayout *gridLayoutData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case Gap:
result := gridLayout.setFunc(GridRowGap, value)
if result != nil {
if gap := gridLayout.getRaw(GridRowGap); gap != nil {
gridLayout.setRaw(GridColumnGap, gap)
result = append(result, GridColumnGap)
}
}
return result
case Content:
if adapter, ok := value.(GridAdapter); ok {
gridLayout.adapter = adapter
gridLayout.createGridContent()
} else if gridLayout.setContent(value) {
gridLayout.adapter = nil
} else {
return nil
}
return []PropertyName{Content}
}
return gridLayout.viewsContainerData.setFunc(tag, value)
}
func (gridLayout *gridLayoutData) propertyChanged(tag PropertyName) {
switch tag {
case CellWidth:
session := gridLayout.Session()
session.updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`,
gridCellSizesCSS(gridLayout, CellWidth, session))
case CellHeight:
session := gridLayout.Session()
session.updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`,
gridCellSizesCSS(gridLayout, CellHeight, session))
default:
gridLayout.viewsContainerData.propertyChanged(tag)
}
}
func (gridLayout *gridLayoutData) createGridContent() bool {
if gridLayout.adapter == nil {
return false
}
adapter := gridLayout.adapter
gridLayout.views = []View{}
session := gridLayout.session
htmlID := gridLayout.htmlID()
isDisabled := IsDisabled(gridLayout)
var columnSpan GridCellColumnSpanAdapter = nil
if span, ok := adapter.(GridCellColumnSpanAdapter); ok {
columnSpan = span
}
var rowSpan GridCellRowSpanAdapter = nil
if span, ok := adapter.(GridCellRowSpanAdapter); ok {
rowSpan = span
}
width := adapter.GridColumnCount()
height := adapter.GridRowCount()
for column := 0; column < width; column++ {
for row := 0; row < height; row++ {
if view := adapter.GridCellContent(row, column, session); view != nil {
view.setParentID(htmlID)
columnCount := 1
if columnSpan != nil {
columnCount = columnSpan.GridCellColumnSpan(row, column)
}
if columnCount > 1 {
view.Set(Column, Range{First: column, Last: column + columnCount - 1})
} else {
view.Set(Column, column)
}
rowCount := 1
if rowSpan != nil {
rowCount = rowSpan.GridCellRowSpan(row, column)
}
if rowCount > 1 {
view.Set(Row, Range{First: row, Last: row + rowCount - 1})
} else {
view.Set(Row, row)
}
if isDisabled {
view.Set(Disabled, true)
}
gridLayout.views = append(gridLayout.views, view)
}
}
}
return true
}
func (gridLayout *gridLayoutData) UpdateGridContent() {
if gridLayout.createGridContent() {
if gridLayout.created {
updateInnerHTML(gridLayout.htmlID(), gridLayout.session)
}
gridLayout.runChangeListener(Content)
}
}
func gridCellSizes(properties Properties, tag PropertyName, session Session) []SizeUnit {
if value := properties.Get(tag); value != nil { if value := properties.Get(tag); value != nil {
switch value := value.(type) { switch value := value.(type) {
case []SizeUnit: case []SizeUnit:
@ -314,38 +502,37 @@ func gridCellSizes(properties Properties, tag string, session Session) []SizeUni
return []SizeUnit{} return []SizeUnit{}
} }
/*
func (gridLayout *gridLayoutData) cssStyle(self View, builder cssBuilder) {
gridLayout.viewsContainerData.cssStyle(self, builder)
}
*/
// GetCellVerticalAlign returns the vertical align of a GridLayout cell content: TopAlign (0), BottomAlign (1), CenterAlign (2), StretchAlign (3) // GetCellVerticalAlign returns the vertical align of a GridLayout cell content: TopAlign (0), BottomAlign (1), CenterAlign (2), StretchAlign (3)
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetCellVerticalAlign(view View, subviewID ...string) int { func GetCellVerticalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, CellVerticalAlign, StretchAlign, false) return enumStyledProperty(view, subviewID, CellVerticalAlign, StretchAlign, false)
} }
// GetCellHorizontalAlign returns the vertical align of a GridLayout cell content: LeftAlign (0), RightAlign (1), CenterAlign (2), StretchAlign (3) // GetCellHorizontalAlign returns the vertical align of a GridLayout cell content: LeftAlign (0), RightAlign (1), CenterAlign (2), StretchAlign (3)
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetCellHorizontalAlign(view View, subviewID ...string) int { func GetCellHorizontalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, CellHorizontalAlign, StretchAlign, false) return enumStyledProperty(view, subviewID, CellHorizontalAlign, StretchAlign, false)
} }
// GetGridAutoFlow returns the value of the "grid-auto-flow" property // GetGridAutoFlow returns the value of the "grid-auto-flow" property
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetGridAutoFlow(view View, subviewID ...string) int { func GetGridAutoFlow(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, GridAutoFlow, 0, false) return enumStyledProperty(view, subviewID, GridAutoFlow, 0, false)
} }
// GetCellWidth returns the width of a GridLayout cell. If the result is an empty array, then the width is not set. // GetCellWidth returns the width of a GridLayout cell. If the result is an empty array, then the width is not set.
// If the result is a single value array, then the width of all cell is equal. // If the result is a single value array, then the width of all cell is equal.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetCellWidth(view View, subviewID ...string) []SizeUnit { func GetCellWidth(view View, subviewID ...string) []SizeUnit {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0])
}
if view != nil {
return gridCellSizes(view, CellWidth, view.Session()) return gridCellSizes(view, CellWidth, view.Session())
} }
return []SizeUnit{} return []SizeUnit{}
@ -353,25 +540,28 @@ func GetCellWidth(view View, subviewID ...string) []SizeUnit {
// GetCellHeight returns the height of a GridLayout cell. If the result is an empty array, then the height is not set. // GetCellHeight returns the height of a GridLayout cell. If the result is an empty array, then the height is not set.
// If the result is a single value array, then the height of all cell is equal. // If the result is a single value array, then the height of all cell is equal.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetCellHeight(view View, subviewID ...string) []SizeUnit { func GetCellHeight(view View, subviewID ...string) []SizeUnit {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0])
}
if view != nil {
return gridCellSizes(view, CellHeight, view.Session()) return gridCellSizes(view, CellHeight, view.Session())
} }
return []SizeUnit{} return []SizeUnit{}
} }
// GetGridRowGap returns the gap between GridLayout rows. // GetGridRowGap returns the gap between GridLayout rows.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetGridRowGap(view View, subviewID ...string) SizeUnit { func GetGridRowGap(view View, subviewID ...string) SizeUnit {
return sizeStyledProperty(view, subviewID, GridRowGap, false) return sizeStyledProperty(view, subviewID, GridRowGap, false)
} }
// GetGridColumnGap returns the gap between GridLayout columns. // GetGridColumnGap returns the gap between GridLayout columns.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetGridColumnGap(view View, subviewID ...string) SizeUnit { func GetGridColumnGap(view View, subviewID ...string) SizeUnit {
return sizeStyledProperty(view, subviewID, GridColumnGap, false) return sizeStyledProperty(view, subviewID, GridColumnGap, false)
} }

56
httpHandler.go Normal file
View File

@ -0,0 +1,56 @@
//go:build !wasm
package rui
import (
"net/http"
"strings"
)
type httpHandler struct {
app *application
prefix string
}
func (h *httpHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
path := `/` + strings.TrimPrefix(req.URL.Path, `/`)
req.URL.Path = `/` + strings.TrimPrefix(strings.TrimPrefix(path, h.prefix), `/`)
h.app.ServeHTTP(w, req)
}
}
// NewHandler is used to embed the rui application in third-party web frameworks (net/http, gin, echo...).
//
// Example for echo:
//
// e := echo.New()
// e.Any(`/ui/*`, func()echo.HandlerFunc{
// rui.AddEmbedResources(&resources)
//
// h := rui.NewHandler("/ui", CreateSessionContent, rui.AppParams{
// Title: `Awesome app`,
// Icon: `favicon.png`,
// })
//
// return func(c echo.Context) error {
// h.ServeHTTP(c.Response(), c.Request())
// return nil
// }
// })
func NewHandler(urlPrefix string, createContentFunc func(Session) SessionContent, params AppParams) *httpHandler {
app := new(application)
app.params = params
app.sessions = map[int]sessionInfo{}
app.createContentFunc = createContentFunc
apps = append(apps, app)
h := &httpHandler{
app: app,
prefix: `/` + strings.Trim(urlPrefix, `/`),
}
return h
}

View File

@ -1,34 +1,47 @@
package rui package rui
import "strconv" import (
"strconv"
)
// ImageLoadingStatus defines type of status of the image loading
type ImageLoadingStatus int
// Constants which represent return values of the LoadingStatus function of an [Image] view
const ( const (
// ImageLoading is the image loading status: in the process of loading // ImageLoading is the image loading status: in the process of loading
ImageLoading = 0 ImageLoading ImageLoadingStatus = 0
// ImageReady is the image loading status: the image is loaded successfully // ImageReady is the image loading status: the image is loaded successfully
ImageReady = 1 ImageReady ImageLoadingStatus = 1
// ImageLoadingError is the image loading status: an error occurred while loading // ImageLoadingError is the image loading status: an error occurred while loading
ImageLoadingError = 2 ImageLoadingError ImageLoadingStatus = 2
) )
// Image defines the image that is used for drawing operations on the Canvas. // Image defines the image that is used for drawing operations on the Canvas.
type Image interface { type Image interface {
// URL returns the url of the image // URL returns the url of the image
URL() string URL() string
// LoadingStatus returns the status of the image loading: ImageLoading (0), ImageReady (1), ImageLoadingError (2)
LoadingStatus() int // LoadingStatus returns the status of the image loading:
// - ImageLoading (0) - in the process of loading;
// - ImageReady (1) - the image is loaded successfully;
// - ImageLoadingError (2) - an error occurred while loading.
LoadingStatus() ImageLoadingStatus
// LoadingError: if LoadingStatus() == ImageLoadingError then returns the error text, "" otherwise // LoadingError: if LoadingStatus() == ImageLoadingError then returns the error text, "" otherwise
LoadingError() string LoadingError() string
setLoadingError(err string) setLoadingError(err string)
// Width returns the width of the image in pixels. While LoadingStatus() != ImageReady returns 0 // Width returns the width of the image in pixels. While LoadingStatus() != ImageReady returns 0
Width() float64 Width() float64
// Height returns the height of the image in pixels. While LoadingStatus() != ImageReady returns 0 // Height returns the height of the image in pixels. While LoadingStatus() != ImageReady returns 0
Height() float64 Height() float64
} }
type imageData struct { type imageData struct {
url string url string
loadingStatus int loadingStatus ImageLoadingStatus
loadingError string loadingError string
width, height float64 width, height float64
listener func(Image) listener func(Image)
@ -42,7 +55,7 @@ func (image *imageData) URL() string {
return image.url return image.url
} }
func (image *imageData) LoadingStatus() int { func (image *imageData) LoadingStatus() ImageLoadingStatus {
return image.loadingStatus return image.loadingStatus
} }
@ -76,11 +89,13 @@ func (manager *imageManager) loadImage(url string, onLoaded func(Image), session
image.listener = onLoaded image.listener = onLoaded
image.loadingStatus = ImageLoading image.loadingStatus = ImageLoading
manager.images[url] = image manager.images[url] = image
session.runScript("loadImage('" + url + "');")
session.callFunc("loadImage", url)
session.sendResponse()
return image return image
} }
func (manager *imageManager) imageLoaded(obj DataObject, session Session) { func (manager *imageManager) imageLoaded(obj DataObject) {
if manager.images == nil { if manager.images == nil {
manager.images = make(map[string]*imageData) manager.images = make(map[string]*imageData)
return return
@ -106,7 +121,7 @@ func (manager *imageManager) imageLoaded(obj DataObject, session Session) {
} }
} }
func (manager *imageManager) imageLoadError(obj DataObject, session Session) { func (manager *imageManager) imageLoadError(obj DataObject) {
if manager.images == nil { if manager.images == nil {
manager.images = make(map[string]*imageData) manager.images = make(map[string]*imageData)
return return
@ -128,8 +143,8 @@ func (manager *imageManager) imageLoadError(obj DataObject, session Session) {
// LoadImage starts the async image loading by url // LoadImage starts the async image loading by url
func LoadImage(url string, onLoaded func(Image), session Session) Image { func LoadImage(url string, onLoaded func(Image), session Session) Image {
if url != "" && url[0] == '@' { if ok, constName := isConstantName(url); ok {
if image, ok := session.ImageConstant(url[1:]); ok { if image, ok := session.ImageConstant(constName); ok {
url = image url = image
} }
} }

View File

@ -5,35 +5,63 @@ import (
"strings" "strings"
) )
// Constants which represent [ImageView] specific properties and events
const ( const (
// LoadedEvent is the constant for the "loaded-event" property tag. // LoadedEvent is the constant for "loaded-event" property tag.
// The "loaded-event" event occurs event occurs when the image has been loaded. //
LoadedEvent = "loaded-event" // Used by ImageView.
// ErrorEvent is the constant for the "error-event" property tag. // Occur when the image has been loaded.
// The "error-event" event occurs event occurs when the image loading failed. //
ErrorEvent = "error-event" // General listener format:
// func(image rui.ImageView)
//
// where:
// image - Interface of an image view which generated this event.
//
// Allowed listener formats:
// func()
LoadedEvent PropertyName = "loaded-event"
// ErrorEvent is the constant for "error-event" property tag.
//
// Used by ImageView.
// Occur when the image loading has been failed.
//
// General listener format:
// func(image rui.ImageView)
//
// where:
// image - Interface of an image view which generated this event.
//
// Allowed listener formats:
// func()
ErrorEvent PropertyName = "error-event"
// NoneFit - value of the "object-fit" property of an ImageView. The replaced content is not resized // NoneFit - value of the "object-fit" property of an ImageView. The replaced content is not resized
NoneFit = 0 NoneFit = 0
// ContainFit - value of the "object-fit" property of an ImageView. The replaced content // ContainFit - value of the "object-fit" property of an ImageView. The replaced content
// is scaled to maintain its aspect ratio while fitting within the elements content box. // is scaled to maintain its aspect ratio while fitting within the elements content box.
// The entire object is made to fill the box, while preserving its aspect ratio, so the object // The entire object is made to fill the box, while preserving its aspect ratio, so the object
// will be "letterboxed" if its aspect ratio does not match the aspect ratio of the box. // will be "letterboxed" if its aspect ratio does not match the aspect ratio of the box.
ContainFit = 1 ContainFit = 1
// CoverFit - value of the "object-fit" property of an ImageView. The replaced content // CoverFit - value of the "object-fit" property of an ImageView. The replaced content
// is sized to maintain its aspect ratio while filling the elements entire content box. // is sized to maintain its aspect ratio while filling the elements entire content box.
// If the object's aspect ratio does not match the aspect ratio of its box, then the object will be clipped to fit. // If the object's aspect ratio does not match the aspect ratio of its box, then the object will be clipped to fit.
CoverFit = 2 CoverFit = 2
// FillFit - value of the "object-fit" property of an ImageView. The replaced content is sized // FillFit - value of the "object-fit" property of an ImageView. The replaced content is sized
// to fill the elements content box. The entire object will completely fill the box. // to fill the elements content box. The entire object will completely fill the box.
// If the object's aspect ratio does not match the aspect ratio of its box, then the object will be stretched to fit. // If the object's aspect ratio does not match the aspect ratio of its box, then the object will be stretched to fit.
FillFit = 3 FillFit = 3
// ScaleDownFit - value of the "object-fit" property of an ImageView. The content is sized as // ScaleDownFit - value of the "object-fit" property of an ImageView. The content is sized as
// if NoneFit or ContainFit were specified, whichever would result in a smaller concrete object size. // if NoneFit or ContainFit were specified, whichever would result in a smaller concrete object size.
ScaleDownFit = 4 ScaleDownFit = 4
) )
// ImageView - image View // ImageView represents an ImageView view
type ImageView interface { type ImageView interface {
View View
// NaturalSize returns the intrinsic, density-corrected size (width, height) of the image in pixels. // NaturalSize returns the intrinsic, density-corrected size (width, height) of the image in pixels.
@ -60,27 +88,29 @@ func NewImageView(session Session, params Params) ImageView {
} }
func newImageView(session Session) View { func newImageView(session Session) View {
return NewImageView(session, nil) return new(imageViewData)
} }
// Init initialize fields of imageView by default values // Init initialize fields of imageView by default values
func (imageView *imageViewData) init(session Session) { func (imageView *imageViewData) init(session Session) {
imageView.viewData.init(session) imageView.viewData.init(session)
imageView.tag = "ImageView" imageView.tag = "ImageView"
//imageView.systemClass = "ruiImageView" imageView.systemClass = "ruiImageView"
imageView.normalize = normalizeImageViewTag
imageView.get = imageView.getFunc
imageView.set = imageView.setFunc
imageView.changed = imageView.propertyChanged
} }
func (imageView *imageViewData) String() string { func normalizeImageViewTag(tag PropertyName) PropertyName {
return getViewString(imageView) tag = defaultNormalize(tag)
}
func (imageView *imageViewData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag { switch tag {
case "source": case "source":
tag = Source tag = Source
case "src-set", "source-set":
tag = SrcSet
case VerticalAlign: case VerticalAlign:
tag = ImageVerticalAlign tag = ImageVerticalAlign
@ -93,108 +123,92 @@ func (imageView *imageViewData) normalizeTag(tag string) string {
return tag return tag
} }
func (imageView *imageViewData) Remove(tag string) { func (imageView *imageViewData) getFunc(tag PropertyName) any {
imageView.remove(imageView.normalizeTag(tag)) switch tag {
} case LoadedEvent, ErrorEvent:
if listeners := getNoArgEventRawListeners[ImageView](imageView, nil, tag); len(listeners) > 0 {
func (imageView *imageViewData) remove(tag string) { return listeners
imageView.viewData.remove(tag)
if imageView.created {
switch tag {
case Source:
updateProperty(imageView.htmlID(), "src", "", imageView.session)
removeProperty(imageView.htmlID(), "srcset", imageView.session)
case AltText:
updateInnerHTML(imageView.htmlID(), imageView.session)
case ImageVerticalAlign, ImageHorizontalAlign:
updateCSSStyle(imageView.htmlID(), imageView.session)
} }
return nil
} }
return imageView.viewData.getFunc(tag)
} }
func (imageView *imageViewData) Set(tag string, value any) bool { func (imageView *imageViewData) setFunc(tag PropertyName, value any) []PropertyName {
return imageView.set(imageView.normalizeTag(tag), value)
switch tag {
case Source, SrcSet, AltText:
if text, ok := value.(string); ok {
return setStringPropertyValue(imageView, tag, text)
}
notCompatibleType(tag, value)
return nil
case LoadedEvent, ErrorEvent:
return setNoArgEventListener[ImageView](imageView, tag, value)
}
return imageView.viewData.setFunc(tag, value)
} }
func (imageView *imageViewData) set(tag string, value any) bool { func (imageView *imageViewData) propertyChanged(tag PropertyName) {
if value == nil { session := imageView.Session()
imageView.remove(tag) htmlID := imageView.htmlID()
return true
}
switch tag { switch tag {
case Source: case Source:
if text, ok := value.(string); ok { src, srcset := imageViewSrc(imageView, GetImageViewSource(imageView))
imageView.properties[Source] = text session.updateProperty(htmlID, "src", src)
if imageView.created { if srcset != "" {
src := text session.updateProperty(htmlID, "srcset", srcset)
if src != "" && src[0] == '@' { } else {
src, _ = imageProperty(imageView, Source, imageView.session) session.removeProperty(htmlID, "srcset")
} }
updateProperty(imageView.htmlID(), "src", src, imageView.session)
if srcset := imageView.srcSet(src); srcset != "" { case SrcSet:
updateProperty(imageView.htmlID(), "srcset", srcset, imageView.session) _, srcset := imageViewSrc(imageView, GetImageViewSource(imageView))
} else { if srcset != "" {
removeProperty(imageView.htmlID(), "srcset", imageView.session) session.updateProperty(htmlID, "srcset", srcset)
} } else {
} session.removeProperty(htmlID, "srcset")
imageView.propertyChangedEvent(Source)
return true
} }
notCompatibleType(Source, value)
case AltText: case AltText:
if text, ok := value.(string); ok { updateInnerHTML(htmlID, session)
imageView.properties[AltText] = text
if imageView.created {
updateInnerHTML(imageView.htmlID(), imageView.session)
}
imageView.propertyChangedEvent(Source)
return true
}
notCompatibleType(tag, value)
case LoadedEvent, ErrorEvent: case ImageVerticalAlign, ImageHorizontalAlign:
if listeners, ok := valueToNoParamListeners[ImageView](value); ok { updateCSSStyle(htmlID, session)
if listeners == nil {
delete(imageView.properties, tag)
} else {
imageView.properties[tag] = listeners
}
return true
}
default: default:
if imageView.viewData.set(tag, value) { imageView.viewData.propertyChanged(tag)
if imageView.created { }
switch tag { }
case ImageVerticalAlign, ImageHorizontalAlign:
updateCSSStyle(imageView.htmlID(), imageView.session) func imageViewSrcSet(view View, path string) string {
if value := view.getRaw(SrcSet); value != nil {
if text, ok := value.(string); ok {
srcset := strings.Split(text, ",")
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
for i, src := range srcset {
if i > 0 {
buffer.WriteString(", ")
}
src = strings.Trim(src, " \t\n")
buffer.WriteString(src)
if index := strings.LastIndex(src, "@"); index > 0 {
if ext := strings.LastIndex(src, "."); ext > index {
buffer.WriteRune(' ')
buffer.WriteString(src[index+1 : ext])
}
} else {
buffer.WriteString(" 1x")
} }
} }
return true return buffer.String()
} }
} }
return false
}
func (imageView *imageViewData) Get(tag string) any {
return imageView.viewData.get(imageView.normalizeTag(tag))
}
func (imageView *imageViewData) imageListeners(tag string) []func(ImageView) {
if value := imageView.getRaw(tag); value != nil {
if listeners, ok := value.([]func(ImageView)); ok {
return listeners
}
}
return []func(ImageView){}
}
func (imageView *imageViewData) srcSet(path string) string {
if srcset, ok := resources.imageSrcSets[path]; ok { if srcset, ok := resources.imageSrcSets[path]; ok {
buffer := allocStringBuilder() buffer := allocStringBuilder()
defer freeStringBuilder(buffer) defer freeStringBuilder(buffer)
@ -203,7 +217,7 @@ func (imageView *imageViewData) srcSet(path string) string {
buffer.WriteString(", ") buffer.WriteString(", ")
} }
buffer.WriteString(src.path) buffer.WriteString(src.path)
buffer.WriteString(fmt.Sprintf(" %gx", src.scale)) fmt.Fprintf(buffer, " %gx", src.scale)
} }
return buffer.String() return buffer.String()
} }
@ -214,29 +228,31 @@ func (imageView *imageViewData) htmlTag() string {
return "img" return "img"
} }
/* func imageViewSrc(view View, src string) (string, string) {
func (imageView *imageViewData) closeHTMLTag() bool { if ok, constName := isConstantName(src); ok {
return false if image, ok := view.Session().ImageConstant(constName); ok {
src = image
} else {
return "", ""
}
}
if src != "" {
return src, imageViewSrcSet(view, src)
}
return "", ""
} }
*/
func (imageView *imageViewData) htmlProperties(self View, buffer *strings.Builder) { func (imageView *imageViewData) htmlProperties(self View, buffer *strings.Builder) {
imageView.viewData.htmlProperties(self, buffer) imageView.viewData.htmlProperties(self, buffer)
if imageResource, ok := imageProperty(imageView, Source, imageView.Session()); ok && imageResource != "" { if imageResource, ok := imageProperty(imageView, Source, imageView.Session()); ok && imageResource != "" {
if imageResource[0] == '@' { if src, srcset := imageViewSrc(imageView, imageResource); src != "" {
if image, ok := imageView.Session().ImageConstant(imageResource[1:]); ok {
imageResource = image
} else {
imageResource = ""
}
}
if imageResource != "" {
buffer.WriteString(` src="`) buffer.WriteString(` src="`)
buffer.WriteString(imageResource) buffer.WriteString(src)
buffer.WriteString(`"`) buffer.WriteString(`"`)
if srcset := imageView.srcSet(imageResource); srcset != "" { if srcset != "" {
buffer.WriteString(` srcset="`) buffer.WriteString(` srcset="`)
buffer.WriteString(srcset) buffer.WriteString(srcset)
buffer.WriteString(`"`) buffer.WriteString(`"`)
@ -246,13 +262,13 @@ func (imageView *imageViewData) htmlProperties(self View, buffer *strings.Builde
if text := GetImageViewAltText(imageView); text != "" { if text := GetImageViewAltText(imageView); text != "" {
buffer.WriteString(` alt="`) buffer.WriteString(` alt="`)
buffer.WriteString(textToJS(text)) buffer.WriteString(text)
buffer.WriteString(`"`) buffer.WriteString(`"`)
} }
buffer.WriteString(` onload="imageLoaded(this, event)"`) buffer.WriteString(` onload="imageLoaded(this, event)"`)
if len(imageView.imageListeners(ErrorEvent)) > 0 { if len(getNoArgEventListeners[ImageView](imageView, nil, ErrorEvent)) > 0 {
buffer.WriteString(` onerror="imageError(this, event)"`) buffer.WriteString(` onerror="imageError(this, event)"`)
} }
} }
@ -292,11 +308,11 @@ func (imageView *imageViewData) cssStyle(self View, builder cssBuilder) {
} }
} }
func (imageView *imageViewData) handleCommand(self View, command string, data DataObject) bool { func (imageView *imageViewData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command { switch command {
case "imageViewError": case "imageViewError":
for _, listener := range imageView.imageListeners(ErrorEvent) { for _, listener := range getNoArgEventListeners[ImageView](imageView, nil, ErrorEvent) {
listener(imageView) listener.Run(imageView)
} }
case "imageViewLoaded": case "imageViewLoaded":
@ -304,8 +320,8 @@ func (imageView *imageViewData) handleCommand(self View, command string, data Da
imageView.naturalHeight = dataFloatProperty(data, "natural-height") imageView.naturalHeight = dataFloatProperty(data, "natural-height")
imageView.currentSrc, _ = data.PropertyValue("current-src") imageView.currentSrc, _ = data.PropertyValue("current-src")
for _, listener := range imageView.imageListeners(LoadedEvent) { for _, listener := range getNoArgEventListeners[ImageView](imageView, nil, LoadedEvent) {
listener(imageView) listener.Run(imageView)
} }
default: default:
@ -325,11 +341,7 @@ func (imageView *imageViewData) CurrentSource() string {
// GetImageViewSource returns the image URL of an ImageView subview. // GetImageViewSource returns the image URL of an ImageView subview.
// If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned // If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned
func GetImageViewSource(view View, subviewID ...string) string { func GetImageViewSource(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if image, ok := imageProperty(view, Source, view.Session()); ok { if image, ok := imageProperty(view, Source, view.Session()); ok {
return image return image
} }
@ -341,11 +353,7 @@ func GetImageViewSource(view View, subviewID ...string) string {
// GetImageViewAltText returns an alternative text description of an ImageView subview. // GetImageViewAltText returns an alternative text description of an ImageView subview.
// If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned // If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned
func GetImageViewAltText(view View, subviewID ...string) string { func GetImageViewAltText(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.getRaw(AltText); value != nil { if value := view.getRaw(AltText); value != nil {
if text, ok := value.(string); ok { if text, ok := value.(string); ok {
text, _ = view.Session().GetString(text) text, _ = view.Session().GetString(text)
@ -374,3 +382,31 @@ func GetImageViewVerticalAlign(view View, subviewID ...string) int {
func GetImageViewHorizontalAlign(view View, subviewID ...string) int { func GetImageViewHorizontalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, ImageHorizontalAlign, LeftAlign, false) return enumStyledProperty(view, subviewID, ImageHorizontalAlign, LeftAlign, false)
} }
// GetImageViewErrorEventListeners returns the list of "error-event" event listeners.
// If there are no listeners then the empty list is returned
//
// Result elements can be of the following types:
// - func(rui.ImageView)
// - func()
// - string
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetImageViewErrorEventListeners(view View, subviewID ...string) []any {
return getNoArgEventRawListeners[View](view, subviewID, ErrorEvent)
}
// GetImageViewLoadedEventListeners returns the list of "loaded-event" event listeners.
// If there are no listeners then the empty list is returned
//
// Result elements can be of the following types:
// - func(rui.ImageView)
// - func()
// - string
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetImageViewLoadedEventListeners(view View, subviewID ...string) []any {
return getNoArgEventRawListeners[View](view, subviewID, LoadedEvent)
}

View File

@ -2,24 +2,388 @@ package rui
import "strings" import "strings"
// Constants which represent [View] specific keyboard events properties
const ( const (
// KeyDown is the constant for "key-down-event" property tag. // KeyDownEvent is the constant for "key-down-event" property tag.
// The "key-down-event" event is fired when a key is pressed. //
// The main listener format: // Used by View.
// func(View, KeyEvent). // Is fired when a key is pressed.
// The additional listener formats: //
// func(KeyEvent), func(View), and func(). // General listener format:
KeyDownEvent = "key-down-event" //
// func(view rui.View, event rui.KeyEvent).
//
// where:
// - view - Interface of a view which generated this event,
// - event - Key event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.KeyEvent)
// func()
KeyDownEvent PropertyName = "key-down-event"
// KeyPp is the constant for "key-up-event" property tag. // KeyUpEvent is the constant for "key-up-event" property tag.
// The "key-up-event" event is fired when a key is released. //
// The main listener format: // Used by View.
// func(View, KeyEvent). // Is fired when a key is released.
// The additional listener formats: //
// func(KeyEvent), func(View), and func(). // General listener format:
KeyUpEvent = "key-up-event" //
// func(view rui.View, event rui.KeyEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Key event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.KeyEvent)
// func()
KeyUpEvent PropertyName = "key-up-event"
) )
// ControlKeyMask represent ORed state of keyboard's control keys like [AltKey], [CtrlKey], [ShiftKey] and [MetaKey]
type ControlKeyMask int
// KeyCode is a string representation the a physical key being pressed.
// The value is not affected by the current keyboard layout or modifier state,
// so a particular key will always have the same value.
type KeyCode string
// Constants for specific keyboard keys.
const (
// AltKey is the mask of the "alt" key
AltKey ControlKeyMask = 1
// CtrlKey is the mask of the "ctrl" key
CtrlKey ControlKeyMask = 2
// ShiftKey is the mask of the "shift" key
ShiftKey ControlKeyMask = 4
// MetaKey is the mask of the "meta" key
MetaKey ControlKeyMask = 8
// KeyA represent "A" key on the keyboard
KeyA KeyCode = "KeyA"
// KeyB represent "B" key on the keyboard
KeyB KeyCode = "KeyB"
// KeyC represent "C" key on the keyboard
KeyC KeyCode = "KeyC"
// KeyD represent "D" key on the keyboard
KeyD KeyCode = "KeyD"
// KeyE represent "E" key on the keyboard
KeyE KeyCode = "KeyE"
// KeyF represent "F" key on the keyboard
KeyF KeyCode = "KeyF"
// KeyG represent "G" key on the keyboard
KeyG KeyCode = "KeyG"
// KeyH represent "H" key on the keyboard
KeyH KeyCode = "KeyH"
// KeyI represent "I" key on the keyboard
KeyI KeyCode = "KeyI"
// KeyJ represent "J" key on the keyboard
KeyJ KeyCode = "KeyJ"
// KeyK represent "K" key on the keyboard
KeyK KeyCode = "KeyK"
// KeyL represent "L" key on the keyboard
KeyL KeyCode = "KeyL"
// KeyM represent "M" key on the keyboard
KeyM KeyCode = "KeyM"
// KeyN represent "N" key on the keyboard
KeyN KeyCode = "KeyN"
// KeyO represent "O" key on the keyboard
KeyO KeyCode = "KeyO"
// KeyP represent "P" key on the keyboard
KeyP KeyCode = "KeyP"
// KeyQ represent "Q" key on the keyboard
KeyQ KeyCode = "KeyQ"
// KeyR represent "R" key on the keyboard
KeyR KeyCode = "KeyR"
// KeyS represent "S" key on the keyboard
KeyS KeyCode = "KeyS"
// KeyT represent "T" key on the keyboard
KeyT KeyCode = "KeyT"
// KeyU represent "U" key on the keyboard
KeyU KeyCode = "KeyU"
// KeyV represent "V" key on the keyboard
KeyV KeyCode = "KeyV"
// KeyW represent "W" key on the keyboard
KeyW KeyCode = "KeyW"
// KeyX represent "X" key on the keyboard
KeyX KeyCode = "KeyX"
// KeyY represent "Y" key on the keyboard
KeyY KeyCode = "KeyY"
// KeyZ represent "Z" key on the keyboard
KeyZ KeyCode = "KeyZ"
// Digit0Key represent "Digit0" key on the keyboard
Digit0Key KeyCode = "Digit0"
// Digit1Key represent "Digit1" key on the keyboard
Digit1Key KeyCode = "Digit1"
// Digit2Key represent "Digit2" key on the keyboard
Digit2Key KeyCode = "Digit2"
// Digit3Key represent "Digit3" key on the keyboard
Digit3Key KeyCode = "Digit3"
// Digit4Key represent "Digit4" key on the keyboard
Digit4Key KeyCode = "Digit4"
// Digit5Key represent "Digit5" key on the keyboard
Digit5Key KeyCode = "Digit5"
// Digit6Key represent "Digit6" key on the keyboard
Digit6Key KeyCode = "Digit6"
// Digit7Key represent "Digit7" key on the keyboard
Digit7Key KeyCode = "Digit7"
// Digit8Key represent "Digit8" key on the keyboard
Digit8Key KeyCode = "Digit8"
// Digit9Key represent "Digit9" key on the keyboard
Digit9Key KeyCode = "Digit9"
// SpaceKey represent "Space" key on the keyboard
SpaceKey KeyCode = "Space"
// MinusKey represent "Minus" key on the keyboard
MinusKey KeyCode = "Minus"
// EqualKey represent "Equal" key on the keyboard
EqualKey KeyCode = "Equal"
// IntlBackslashKey represent "IntlBackslash" key on the keyboard
IntlBackslashKey KeyCode = "IntlBackslash"
// BracketLeftKey represent "BracketLeft" key on the keyboard
BracketLeftKey KeyCode = "BracketLeft"
// BracketRightKey represent "BracketRight" key on the keyboard
BracketRightKey KeyCode = "BracketRight"
// SemicolonKey represent "Semicolon" key on the keyboard
SemicolonKey KeyCode = "Semicolon"
// CommaKey represent "Comma" key on the keyboard
CommaKey KeyCode = "Comma"
// PeriodKey represent "Period" key on the keyboard
PeriodKey KeyCode = "Period"
// QuoteKey represent "Quote" key on the keyboard
QuoteKey KeyCode = "Quote"
// BackquoteKey represent "Backquote" key on the keyboard
BackquoteKey KeyCode = "Backquote"
// SlashKey represent "Slash" key on the keyboard
SlashKey KeyCode = "Slash"
// EscapeKey represent "Escape" key on the keyboard
EscapeKey KeyCode = "Escape"
// EnterKey represent "Enter" key on the keyboard
EnterKey KeyCode = "Enter"
// TabKey represent "Tab" key on the keyboard
TabKey KeyCode = "Tab"
// CapsLockKey represent "CapsLock" key on the keyboard
CapsLockKey KeyCode = "CapsLock"
// DeleteKey represent "Delete" key on the keyboard
DeleteKey KeyCode = "Delete"
// InsertKey represent "Insert" key on the keyboard
InsertKey KeyCode = "Insert"
// HelpKey represent "Help" key on the keyboard
HelpKey KeyCode = "Help"
// BackspaceKey represent "Backspace" key on the keyboard
BackspaceKey KeyCode = "Backspace"
// PrintScreenKey represent "PrintScreen" key on the keyboard
PrintScreenKey KeyCode = "PrintScreen"
// ScrollLockKey represent "ScrollLock" key on the keyboard
ScrollLockKey KeyCode = "ScrollLock"
// PauseKey represent "Pause" key on the keyboard
PauseKey KeyCode = "Pause"
// ContextMenuKey represent "ContextMenu" key on the keyboard
ContextMenuKey KeyCode = "ContextMenu"
// ArrowLeftKey represent "ArrowLeft" key on the keyboard
ArrowLeftKey KeyCode = "ArrowLeft"
// ArrowRightKey represent "ArrowRight" key on the keyboard
ArrowRightKey KeyCode = "ArrowRight"
// ArrowUpKey represent "ArrowUp" key on the keyboard
ArrowUpKey KeyCode = "ArrowUp"
// ArrowDownKey represent "ArrowDown" key on the keyboard
ArrowDownKey KeyCode = "ArrowDown"
// HomeKey represent "Home" key on the keyboard
HomeKey KeyCode = "Home"
// EndKey represent "End" key on the keyboard
EndKey KeyCode = "End"
// PageUpKey represent "PageUp" key on the keyboard
PageUpKey KeyCode = "PageUp"
// PageDownKey represent "PageDown" key on the keyboard
PageDownKey KeyCode = "PageDown"
// F1Key represent "F1" key on the keyboard
F1Key KeyCode = "F1"
// F2Key represent "F2" key on the keyboard
F2Key KeyCode = "F2"
// F3Key represent "F3" key on the keyboard
F3Key KeyCode = "F3"
// F4Key represent "F4" key on the keyboard
F4Key KeyCode = "F4"
// F5Key represent "F5" key on the keyboard
F5Key KeyCode = "F5"
// F6Key represent "F6" key on the keyboard
F6Key KeyCode = "F6"
// F7Key represent "F7" key on the keyboard
F7Key KeyCode = "F7"
// F8Key represent "F8" key on the keyboard
F8Key KeyCode = "F8"
// F9Key represent "F9" key on the keyboard
F9Key KeyCode = "F9"
// F10Key represent "F10" key on the keyboard
F10Key KeyCode = "F10"
// F11Key represent "F11" key on the keyboard
F11Key KeyCode = "F11"
// F12Key represent "F12" key on the keyboard
F12Key KeyCode = "F12"
// F13Key represent "F13" key on the keyboard
F13Key KeyCode = "F13"
// NumLockKey represent "NumLock" key on the keyboard
NumLockKey KeyCode = "NumLock"
// NumpadKey0 represent "Numpad0" key on the keyboard
NumpadKey0 KeyCode = "Numpad0"
// NumpadKey1 represent "Numpad1" key on the keyboard
NumpadKey1 KeyCode = "Numpad1"
// NumpadKey2 represent "Numpad2" key on the keyboard
NumpadKey2 KeyCode = "Numpad2"
// NumpadKey3 represent "Numpad3" key on the keyboard
NumpadKey3 KeyCode = "Numpad3"
// NumpadKey4 represent "Numpad4" key on the keyboard
NumpadKey4 KeyCode = "Numpad4"
// NumpadKey5 represent "Numpad5" key on the keyboard
NumpadKey5 KeyCode = "Numpad5"
// NumpadKey6 represent "Numpad6" key on the keyboard
NumpadKey6 KeyCode = "Numpad6"
// NumpadKey7 represent "Numpad7" key on the keyboard
NumpadKey7 KeyCode = "Numpad7"
// NumpadKey8 represent "Numpad8" key on the keyboard
NumpadKey8 KeyCode = "Numpad8"
// NumpadKey9 represent "Numpad9" key on the keyboard
NumpadKey9 KeyCode = "Numpad9"
// NumpadDecimalKey represent "NumpadDecimal" key on the keyboard
NumpadDecimalKey KeyCode = "NumpadDecimal"
// NumpadEnterKey represent "NumpadEnter" key on the keyboard
NumpadEnterKey KeyCode = "NumpadEnter"
// NumpadAddKey represent "NumpadAdd" key on the keyboard
NumpadAddKey KeyCode = "NumpadAdd"
// NumpadSubtractKey represent "NumpadSubtract" key on the keyboard
NumpadSubtractKey KeyCode = "NumpadSubtract"
// NumpadMultiplyKey represent "NumpadMultiply" key on the keyboard
NumpadMultiplyKey KeyCode = "NumpadMultiply"
// NumpadDivideKey represent "NumpadDivide" key on the keyboard
NumpadDivideKey KeyCode = "NumpadDivide"
// ShiftLeftKey represent "ShiftLeft" key on the keyboard
ShiftLeftKey KeyCode = "ShiftLeft"
// ShiftRightKey represent "ShiftRight" key on the keyboard
ShiftRightKey KeyCode = "ShiftRight"
// ControlLeftKey represent "ControlLeft" key on the keyboard
ControlLeftKey KeyCode = "ControlLeft"
// ControlRightKey represent "ControlRight" key on the keyboard
ControlRightKey KeyCode = "ControlRight"
// AltLeftKey represent "AltLeft" key on the keyboard
AltLeftKey KeyCode = "AltLeft"
// AltRightKey represent "AltRight" key on the keyboard
AltRightKey KeyCode = "AltRight"
// MetaLeftKey represent "MetaLeft" key on the keyboard
MetaLeftKey KeyCode = "MetaLeft"
// MetaRightKey represent "MetaRight" key on the keyboard
MetaRightKey KeyCode = "MetaRight"
)
// KeyEvent represent a keyboard event
type KeyEvent struct { type KeyEvent struct {
// TimeStamp is the time at which the event was created (in milliseconds). // TimeStamp is the time at which the event was created (in milliseconds).
// This value is time since epoch—but in reality, browsers' definitions vary. // This value is time since epoch—but in reality, browsers' definitions vary.
@ -32,7 +396,7 @@ type KeyEvent struct {
// Code holds a string that identifies the physical key being pressed. The value is not affected // Code holds a string that identifies the physical key being pressed. The value is not affected
// by the current keyboard layout or modifier state, so a particular key will always return the same value. // by the current keyboard layout or modifier state, so a particular key will always return the same value.
Code string Code KeyCode
// Repeat == true if a key has been depressed long enough to trigger key repetition, otherwise false. // Repeat == true if a key has been depressed long enough to trigger key repetition, otherwise false.
Repeat bool Repeat bool
@ -59,7 +423,8 @@ func (event *KeyEvent) init(data DataObject) {
} }
event.Key, _ = data.PropertyValue("key") event.Key, _ = data.PropertyValue("key")
event.Code, _ = data.PropertyValue("code") code, _ := data.PropertyValue("code")
event.Code = KeyCode(code)
event.TimeStamp = getTimeStamp(data) event.TimeStamp = getTimeStamp(data)
event.Repeat = getBool("repeat") event.Repeat = getBool("repeat")
event.CtrlKey = getBool("ctrlKey") event.CtrlKey = getBool("ctrlKey")
@ -68,207 +433,87 @@ func (event *KeyEvent) init(data DataObject) {
event.MetaKey = getBool("metaKey") event.MetaKey = getBool("metaKey")
} }
func valueToEventListeners[V View, E any](value any) ([]func(V, E), bool) {
if value == nil {
return nil, true
}
switch value := value.(type) {
case func(V, E):
return []func(V, E){value}, true
case func(E):
fn := func(_ V, event E) {
value(event)
}
return []func(V, E){fn}, true
case func(V):
fn := func(view V, _ E) {
value(view)
}
return []func(V, E){fn}, true
case func():
fn := func(V, E) {
value()
}
return []func(V, E){fn}, true
case []func(V, E):
if len(value) == 0 {
return nil, true
}
for _, fn := range value {
if fn == nil {
return nil, false
}
}
return value, true
case []func(E):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(_ V, event E) {
v(event)
}
}
return listeners, true
case []func(V):
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(view V, _ E) {
v(view)
}
}
return listeners, true
case []func():
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
listeners[i] = func(V, E) {
v()
}
}
return listeners, true
case []any:
count := len(value)
if count == 0 {
return nil, true
}
listeners := make([]func(V, E), count)
for i, v := range value {
if v == nil {
return nil, false
}
switch v := v.(type) {
case func(V, E):
listeners[i] = v
case func(E):
listeners[i] = func(_ V, event E) {
v(event)
}
case func(V):
listeners[i] = func(view V, _ E) {
v(view)
}
case func():
listeners[i] = func(V, E) {
v()
}
default:
return nil, false
}
}
return listeners, true
}
return nil, false
}
var keyEvents = map[string]struct{ jsEvent, jsFunc string }{
KeyDownEvent: {jsEvent: "onkeydown", jsFunc: "keyDownEvent"},
KeyUpEvent: {jsEvent: "onkeyup", jsFunc: "keyUpEvent"},
}
func (view *viewData) setKeyListener(tag string, value any) bool {
listeners, ok := valueToEventListeners[View, KeyEvent](value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeKeyListener(tag)
} else if js, ok := keyEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
}
} else {
return false
}
return true
}
func (view *viewData) removeKeyListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := keyEvents[tag]; ok {
removeProperty(view.htmlID(), js.jsEvent, view.Session())
}
}
}
func getEventListeners[V View, E any](view View, subviewID []string, tag string) []func(V, E) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.Get(tag); value != nil {
if result, ok := value.([]func(V, E)); ok {
return result
}
}
}
return []func(V, E){}
}
func keyEventsHtml(view View, buffer *strings.Builder) { func keyEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range keyEvents { if len(getOneArgEventListeners[View, KeyEvent](view, nil, KeyDownEvent)) > 0 ||
if listeners := getEventListeners[View, KeyEvent](view, nil, tag); len(listeners) > 0 { (view.Focusable() && len(getOneArgEventListeners[View, MouseEvent](view, nil, ClickEvent)) > 0) {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
} buffer.WriteString(`onkeydown="keyDownEvent(this, event)" `)
}
if len(getOneArgEventListeners[View, KeyEvent](view, nil, KeyUpEvent)) > 0 {
buffer.WriteString(`onkeyup="keyUpEvent(this, event)" `)
} }
} }
func handleKeyEvents(view View, tag string, data DataObject) { func handleKeyEvents(view View, tag PropertyName, data DataObject) {
listeners := getEventListeners[View, KeyEvent](view, nil, tag) var event KeyEvent
if len(listeners) > 0 { event.init(data)
var event KeyEvent listeners := getOneArgEventListeners[View, KeyEvent](view, nil, tag)
event.init(data)
if len(listeners) > 0 {
for _, listener := range listeners { for _, listener := range listeners {
listener(view, event) listener.Run(view, event)
}
return
}
if tag == KeyDownEvent && view.Focusable() && (event.Key == " " || event.Key == "Enter") &&
!IsDisabled(view) && GetSemantics(view) != ButtonSemantics {
switch view.Tag() {
case "EditView", "ListView", "TableView", "TabsLayout", "TimePicker", "DatePicker", "AudioPlayer", "VideoPlayer":
return
}
if listeners := getOneArgEventListeners[View, MouseEvent](view, nil, ClickEvent); len(listeners) > 0 {
clickEvent := MouseEvent{
TimeStamp: event.TimeStamp,
Button: PrimaryMouseButton,
Buttons: PrimaryMouseMask,
CtrlKey: event.CtrlKey,
AltKey: event.AltKey,
ShiftKey: event.ShiftKey,
MetaKey: event.MetaKey,
ClientX: view.Frame().Width / 2,
ClientY: view.Frame().Height / 2,
X: view.Frame().Width / 2,
Y: view.Frame().Height / 2,
ScreenX: view.Frame().Left + view.Frame().Width/2,
ScreenY: view.Frame().Top + view.Frame().Height/2,
}
for _, listener := range listeners {
listener.Run(view, clickEvent)
}
} }
} }
} }
// GetKeyDownListeners returns the "key-down-event" listener list. If there are no listeners then the empty list is returned. // GetKeyDownListeners returns the "key-down-event" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetKeyDownListeners(view View, subviewID ...string) []func(View, KeyEvent) { // Result elements can be of the following types:
return getEventListeners[View, KeyEvent](view, subviewID, KeyDownEvent) // - func(rui.View, rui.KeyEvent),
// - func(rui.View),
// - func(rui.KeyEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetKeyDownListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, KeyEvent](view, subviewID, KeyDownEvent)
} }
// GetKeyUpListeners returns the "key-up-event" listener list. If there are no listeners then the empty list is returned. // GetKeyUpListeners returns the "key-up-event" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetKeyUpListeners(view View, subviewID ...string) []func(View, KeyEvent) { // Result elements can be of the following types:
return getEventListeners[View, KeyEvent](view, subviewID, KeyUpEvent) // - func(rui.View, rui.KeyEvent),
// - func(rui.View),
// - func(rui.KeyEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetKeyUpListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, KeyEvent](view, subviewID, KeyUpEvent)
} }

View File

@ -2,8 +2,16 @@ package rui
// ListAdapter - the list data source // ListAdapter - the list data source
type ListAdapter interface { type ListAdapter interface {
// ListSize returns the number of elements in the list
ListSize() int ListSize() int
// ListItem creates a View of a list item at the given index
ListItem(index int, session Session) View ListItem(index int, session Session) View
}
// ListItemEnabled implements the optional method of ListAdapter interface
type ListItemEnabled interface {
// IsListItemEnabled returns the status (enabled/disabled) of a list item at the given index
IsListItemEnabled(index int) bool IsListItemEnabled(index int) bool
} }

View File

@ -4,30 +4,44 @@ import (
"strings" "strings"
) )
// Constants which represent values of the "orientation" property of the [ListLayout]
const ( const (
// TopDownOrientation - subviews are arranged from top to bottom. Synonym of VerticalOrientation // TopDownOrientation - subviews are arranged from top to bottom. Synonym of VerticalOrientation
TopDownOrientation = 0 TopDownOrientation = 0
// StartToEndOrientation - subviews are arranged from left to right. Synonym of HorizontalOrientation // StartToEndOrientation - subviews are arranged from left to right. Synonym of HorizontalOrientation
StartToEndOrientation = 1 StartToEndOrientation = 1
// BottomUpOrientation - subviews are arranged from bottom to top // BottomUpOrientation - subviews are arranged from bottom to top
BottomUpOrientation = 2 BottomUpOrientation = 2
// EndToStartOrientation - subviews are arranged from right to left // EndToStartOrientation - subviews are arranged from right to left
EndToStartOrientation = 3 EndToStartOrientation = 3
)
// Constants which represent values of the "list-wrap" property of the [ListLayout]
const (
// ListWrapOff - subviews are scrolled and "true" if a new row/column starts // ListWrapOff - subviews are scrolled and "true" if a new row/column starts
ListWrapOff = 0 ListWrapOff = 0
// ListWrapOn - the new row/column starts at bottom/right // ListWrapOn - the new row/column starts at bottom/right
ListWrapOn = 1 ListWrapOn = 1
// ListWrapReverse - the new row/column starts at top/left // ListWrapReverse - the new row/column starts at top/left
ListWrapReverse = 2 ListWrapReverse = 2
) )
// ListLayout - list-container of View // ListLayout represents a ListLayout view
type ListLayout interface { type ListLayout interface {
ViewsContainer ViewsContainer
// UpdateContent updates child Views if the "content" property value is set to ListAdapter,
// otherwise does nothing
UpdateContent()
} }
type listLayoutData struct { type listLayoutData struct {
viewsContainerData viewsContainerData
adapter ListAdapter
} }
// NewListLayout create new ListLayout object and return it // NewListLayout create new ListLayout object and return it
@ -39,7 +53,8 @@ func NewListLayout(session Session, params Params) ListLayout {
} }
func newListLayout(session Session) View { func newListLayout(session Session) View {
return NewListLayout(session, nil) //return NewListLayout(session, nil)
return new(listLayoutData)
} }
// Init initialize fields of ViewsAlignContainer by default values // Init initialize fields of ViewsAlignContainer by default values
@ -47,14 +62,15 @@ func (listLayout *listLayoutData) init(session Session) {
listLayout.viewsContainerData.init(session) listLayout.viewsContainerData.init(session)
listLayout.tag = "ListLayout" listLayout.tag = "ListLayout"
listLayout.systemClass = "ruiListLayout" listLayout.systemClass = "ruiListLayout"
listLayout.normalize = normalizeListLayoutTag
listLayout.get = listLayout.getFunc
listLayout.set = listLayout.setFunc
listLayout.remove = listLayout.removeFunc
listLayout.changed = listLayout.propertyChanged
} }
func (listLayout *listLayoutData) String() string { func normalizeListLayoutTag(tag PropertyName) PropertyName {
return getViewString(listLayout) tag = defaultNormalize(tag)
}
func (listLayout *listLayoutData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag { switch tag {
case "wrap": case "wrap":
tag = ListWrap tag = ListWrap
@ -68,98 +84,149 @@ func (listLayout *listLayoutData) normalizeTag(tag string) string {
return tag return tag
} }
func (listLayout *listLayoutData) Get(tag string) any { func (listLayout *listLayoutData) getFunc(tag PropertyName) any {
return listLayout.get(listLayout.normalizeTag(tag)) switch tag {
} case Gap:
func (listLayout *listLayoutData) get(tag string) any {
if tag == Gap {
if rowGap := GetListRowGap(listLayout); rowGap.Equal(GetListColumnGap(listLayout)) { if rowGap := GetListRowGap(listLayout); rowGap.Equal(GetListColumnGap(listLayout)) {
return rowGap return rowGap
} }
return AutoSize() return AutoSize()
}
return listLayout.viewsContainerData.get(tag) case Content:
} if listLayout.adapter != nil {
return listLayout.adapter
func (listLayout *listLayoutData) Remove(tag string) {
listLayout.remove(listLayout.normalizeTag(tag))
}
func (listLayout *listLayoutData) remove(tag string) {
if tag == Gap {
listLayout.remove(ListRowGap)
listLayout.remove(ListColumnGap)
return
}
listLayout.viewsContainerData.remove(tag)
if listLayout.created {
switch tag {
case Orientation, ListWrap, HorizontalAlign, VerticalAlign:
updateCSSStyle(listLayout.htmlID(), listLayout.session)
} }
} }
return listLayout.viewsContainerData.getFunc(tag)
} }
func (listLayout *listLayoutData) Set(tag string, value any) bool { func (listLayout *listLayoutData) removeFunc(tag PropertyName) []PropertyName {
return listLayout.set(listLayout.normalizeTag(tag), value) switch tag {
} case Gap:
result := []PropertyName{}
func (listLayout *listLayoutData) set(tag string, value any) bool { for _, tag := range []PropertyName{ListRowGap, ListColumnGap} {
if value == nil { if listLayout.getRaw(tag) != nil {
listLayout.remove(tag) listLayout.setRaw(tag, nil)
return true result = append(result, tag)
}
if tag == Gap {
return listLayout.set(ListRowGap, value) && listLayout.set(ListColumnGap, value)
}
if listLayout.viewsContainerData.set(tag, value) {
if listLayout.created {
switch tag {
case Orientation, ListWrap, HorizontalAlign, VerticalAlign:
updateCSSStyle(listLayout.htmlID(), listLayout.session)
} }
} }
return true return result
case Content:
result := listLayout.viewsContainerData.removeFunc(Content)
if listLayout.adapter != nil {
listLayout.adapter = nil
return []PropertyName{Content}
}
return result
}
return listLayout.viewsContainerData.removeFunc(tag)
}
func (listLayout *listLayoutData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case Gap:
result := listLayout.setFunc(ListRowGap, value)
if result != nil {
if gap := listLayout.getRaw(ListRowGap); gap != nil {
listLayout.setRaw(ListColumnGap, gap)
result = append(result, ListColumnGap)
}
}
return result
case Content:
if adapter, ok := value.(ListAdapter); ok {
listLayout.adapter = adapter
listLayout.createContent()
} else if listLayout.setContent(value) {
listLayout.adapter = nil
} else {
return nil
}
return []PropertyName{Content}
}
return listLayout.viewsContainerData.setFunc(tag, value)
}
func (listLayout *listLayoutData) propertyChanged(tag PropertyName) {
switch tag {
case Orientation, ListWrap, HorizontalAlign, VerticalAlign:
updateCSSStyle(listLayout.htmlID(), listLayout.Session())
default:
listLayout.viewsContainerData.propertyChanged(tag)
} }
return false
} }
func (listLayout *listLayoutData) htmlSubviews(self View, buffer *strings.Builder) { func (listLayout *listLayoutData) htmlSubviews(self View, buffer *strings.Builder) {
if listLayout.views != nil { if listLayout.views != nil {
for _, view := range listLayout.views { for _, view := range listLayout.views {
view.addToCSSStyle(map[string]string{`flex`: `0 0 auto`}) view.addToCSSStyle(map[string]string{`flex`: `0 0 auto`})
viewHTML(view, buffer) viewHTML(view, buffer, "")
} }
} }
} }
// GetListVerticalAlign returns the vertical align of a ListLayout or ListView sibview: func (listLayout *listLayoutData) createContent() bool {
if adapter := listLayout.adapter; adapter != nil {
listLayout.views = []View{}
session := listLayout.session
htmlID := listLayout.htmlID()
isDisabled := IsDisabled(listLayout)
for i := range adapter.ListSize() {
if view := adapter.ListItem(i, session); view != nil {
view.setParentID(htmlID)
if isDisabled {
view.Set(Disabled, true)
}
listLayout.views = append(listLayout.views, view)
}
}
return true
}
return false
}
func (listLayout *listLayoutData) UpdateContent() {
if listLayout.createContent() {
if listLayout.created {
updateInnerHTML(listLayout.htmlID(), listLayout.session)
}
listLayout.runChangeListener(Content)
}
}
// GetListVerticalAlign returns the vertical align of a ListLayout or ListView subview:
// TopAlign (0), BottomAlign (1), CenterAlign (2), or StretchAlign (3) // TopAlign (0), BottomAlign (1), CenterAlign (2), or StretchAlign (3)
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetListVerticalAlign(view View, subviewID ...string) int { func GetListVerticalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, VerticalAlign, TopAlign, false) return enumStyledProperty(view, subviewID, VerticalAlign, TopAlign, false)
} }
// GetListHorizontalAlign returns the vertical align of a ListLayout or ListView subview: // GetListHorizontalAlign returns the vertical align of a ListLayout or ListView subview:
// LeftAlign (0), RightAlign (1), CenterAlign (2), or StretchAlign (3) // LeftAlign (0), RightAlign (1), CenterAlign (2), or StretchAlign (3)
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetListHorizontalAlign(view View, subviewID ...string) int { func GetListHorizontalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, HorizontalAlign, LeftAlign, false) return enumStyledProperty(view, subviewID, HorizontalAlign, LeftAlign, false)
} }
// GetListOrientation returns the orientation of a ListLayout or ListView subview: // GetListOrientation returns the orientation of a ListLayout or ListView subview:
// TopDownOrientation (0), StartToEndOrientation (1), BottomUpOrientation (2), or EndToStartOrientation (3) // TopDownOrientation (0), StartToEndOrientation (1), BottomUpOrientation (2), or EndToStartOrientation (3)
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetListOrientation(view View, subviewID ...string) int { func GetListOrientation(view View, subviewID ...string) int {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if orientation, ok := valueToOrientation(view.Get(Orientation), view.Session()); ok { if orientation, ok := valueToOrientation(view.Get(Orientation), view.Session()); ok {
return orientation return orientation
} }
@ -171,24 +238,51 @@ func GetListOrientation(view View, subviewID ...string) int {
} }
} }
return 0 return TopDownOrientation
} }
// GetListWrap returns the wrap type of a ListLayout or ListView subview: // GetListWrap returns the wrap type of a ListLayout or ListView subview:
// ListWrapOff (0), ListWrapOn (1), or ListWrapReverse (2) // ListWrapOff (0), ListWrapOn (1), or ListWrapReverse (2)
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetListWrap(view View, subviewID ...string) int { func GetListWrap(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, ListWrap, ListWrapOff, false) return enumStyledProperty(view, subviewID, ListWrap, ListWrapOff, false)
} }
// GetListRowGap returns the gap between ListLayout or ListView rows. // GetListRowGap returns the gap between ListLayout or ListView rows.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetListRowGap(view View, subviewID ...string) SizeUnit { func GetListRowGap(view View, subviewID ...string) SizeUnit {
return sizeStyledProperty(view, subviewID, ListRowGap, false) return sizeStyledProperty(view, subviewID, ListRowGap, false)
} }
// GetListColumnGap returns the gap between ListLayout or ListView columns. // GetListColumnGap returns the gap between ListLayout or ListView columns.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetListColumnGap(view View, subviewID ...string) SizeUnit { func GetListColumnGap(view View, subviewID ...string) SizeUnit {
return sizeStyledProperty(view, subviewID, ListColumnGap, false) return sizeStyledProperty(view, subviewID, ListColumnGap, false)
} }
// UpdateContent updates child Views of ListLayout/GridLayout subview if the "content" property value is set to ListAdapter/GridAdapter,
// otherwise does nothing.
// If the second argument (subviewID) is not specified or it is "" then the first argument (view) updates.
func UpdateContent(view View, subviewID ...string) {
if view = getSubview(view, subviewID); view != nil {
switch view := view.(type) {
case GridLayout:
view.UpdateGridContent()
case ListLayout:
view.UpdateContent()
case ListView:
view.ReloadListViewData()
case TableView:
view.ReloadTableData()
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -5,105 +5,205 @@ import (
"strings" "strings"
) )
// Constants related to [View] mouse events properties
const ( const (
// ClickEvent is the constant for "click-event" property tag. // ClickEvent is the constant for "click-event" property tag.
// The "click-event" event occurs when the user clicks on the View. //
// The main listener format: // Used by View.
// func(View, MouseEvent). // Occur when the user clicks on the view.
// The additional listener formats: //
// func(MouseEvent), func(View), and func(). // General listener format:
ClickEvent = "click-event" //
// func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
ClickEvent PropertyName = "click-event"
// DoubleClickEvent is the constant for "double-click-event" property tag. // DoubleClickEvent is the constant for "double-click-event" property tag.
// The "double-click-event" event occurs when the user double clicks on the View. //
// The main listener format: // Used by View.
// func(View, MouseEvent). // Occur when the user double clicks on the view.
// The additional listener formats: //
// func(MouseEvent), func(View), and func(). // General listener format:
DoubleClickEvent = "double-click-event" //
// func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
DoubleClickEvent PropertyName = "double-click-event"
// MouseDown is the constant for "mouse-down" property tag. // MouseDown is the constant for "mouse-down" property tag.
// The "mouse-down" event is fired at a View when a pointing device button is pressed //
// while the pointer is inside the view. // Used by View.
// The main listener format: // Is fired at a View when a pointing device button is pressed while the pointer is inside the view.
// func(View, MouseEvent). //
// The additional listener formats: // General listener format:
// func(MouseEvent), func(View), and func(). //
MouseDown = "mouse-down" // func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
MouseDown PropertyName = "mouse-down"
// MouseUp is the constant for "mouse-up" property tag. // MouseUp is the constant for "mouse-up" property tag.
// The "mouse-up" event is fired at a View when a button on a pointing device (such as a mouse //
// or trackpad) is released while the pointer is located inside it. // Used by View.
// "mouse-up" events are the counterpoint to "mouse-down" events. // Is fired at a View when a button on a pointing device (such as a mouse or trackpad) is released while the pointer is
// The main listener format: // located inside it. "mouse-up" events are the counterpoint to "mouse-down" events.
// func(View, MouseEvent). //
// The additional listener formats: // General listener format:
// func(MouseEvent), func(View), and func(). //
MouseUp = "mouse-up" // func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
MouseUp PropertyName = "mouse-up"
// MouseMove is the constant for "mouse-move" property tag. // MouseMove is the constant for "mouse-move" property tag.
// The "mouse-move" event is fired at a view when a pointing device (usually a mouse) is moved //
// while the cursor's hotspot is inside it. // Used by View.
// The main listener format: // Is fired at a view when a pointing device(usually a mouse) is moved while the cursor's hotspot is inside it.
// func(View, MouseEvent). //
// The additional listener formats: // General listener format:
// func(MouseEvent), func(View), and func(). //
MouseMove = "mouse-move" // func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
MouseMove PropertyName = "mouse-move"
// MouseOut is the constant for "mouse-out" property tag. // MouseOut is the constant for "mouse-out" property tag.
// The "mouse-out" event is fired at a View when a pointing device (usually a mouse) is used to move //
// the cursor so that it is no longer contained within the view or one of its children. // Used by View.
// "mouse-out" is also delivered to a view if the cursor enters a child view, // Is fired at a View when a pointing device (usually a mouse) is used to move the cursor so that it is no longer
// because the child view obscures the visible area of the view. // contained within the view or one of its children. "mouse-out" is also delivered to a view if the cursor enters a child
// The main listener format: // view, because the child view obscures the visible area of the view.
// func(View, MouseEvent). //
// The additional listener formats: // General listener format:
// func(MouseEvent), func(View), and func(). //
// The additional listener formats: // func(view rui.View, event rui.MouseEvent)
// func(MouseEvent), func(View), and func(). //
MouseOut = "mouse-out" // where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
MouseOut PropertyName = "mouse-out"
// MouseOver is the constant for "mouse-over" property tag. // MouseOver is the constant for "mouse-over" property tag.
// The "mouse-over" event is fired at a View when a pointing device (such as a mouse or trackpad) //
// is used to move the cursor onto the view or one of its child views. // Used by View.
// The main listener formats: // Is fired at a View when a pointing device (such as a mouse or trackpad) is used to move the cursor onto the view or one
// func(View, MouseEvent). // of its child views.
// The additional listener formats: //
// func(MouseEvent), func(View), and func(). // General listener format:
MouseOver = "mouse-over" //
// func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
MouseOver PropertyName = "mouse-over"
// ContextMenuEvent is the constant for "context-menu-event" property tag. // ContextMenuEvent is the constant for "context-menu-event" property tag.
// The "context-menu-event" event occurs when the user calls the context menu by the right mouse clicking. //
// The main listener format: // Used by View.
// func(View, MouseEvent). // Occur when the user calls the context menu by the right mouse clicking.
// The additional listener formats: //
// func(MouseEvent), func(View), and func(). // General listener format:
ContextMenuEvent = "context-menu-event" //
// func(view rui.View, event rui.MouseEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Mouse event.
//
// Allowed listener formats:
//
// func(view rui.View)
// func(event rui.MouseEvent)
// func()
ContextMenuEvent PropertyName = "context-menu-event"
// PrimaryMouseButton is a number of the main pressed button, usually the left button or the un-initialized state // PrimaryMouseButton is a number of the main pressed button, usually the left button or the un-initialized state
PrimaryMouseButton = 0 PrimaryMouseButton = 0
// AuxiliaryMouseButton is a number of the auxiliary pressed button, usually the wheel button // AuxiliaryMouseButton is a number of the auxiliary pressed button, usually the wheel button
// or the middle button (if present) // or the middle button (if present)
AuxiliaryMouseButton = 1 AuxiliaryMouseButton = 1
// SecondaryMouseButton is a number of the secondary pressed button, usually the right button // SecondaryMouseButton is a number of the secondary pressed button, usually the right button
SecondaryMouseButton = 2 SecondaryMouseButton = 2
// MouseButton4 is a number of the fourth button, typically the Browser Back button // MouseButton4 is a number of the fourth button, typically the Browser Back button
MouseButton4 = 3 MouseButton4 = 3
// MouseButton5 is a number of the fifth button, typically the Browser Forward button // MouseButton5 is a number of the fifth button, typically the Browser Forward button
MouseButton5 = 4 MouseButton5 = 4
// PrimaryMouseMask is the mask of the primary button (usually the left button) // PrimaryMouseMask is the mask of the primary button (usually the left button)
PrimaryMouseMask = 1 PrimaryMouseMask = 1
// SecondaryMouseMask is the mask of the secondary button (usually the right button) // SecondaryMouseMask is the mask of the secondary button (usually the right button)
SecondaryMouseMask = 2 SecondaryMouseMask = 2
// AuxiliaryMouseMask is the mask of the auxiliary button (usually the mouse wheel button or middle button) // AuxiliaryMouseMask is the mask of the auxiliary button (usually the mouse wheel button or middle button)
AuxiliaryMouseMask = 4 AuxiliaryMouseMask = 4
// MouseMask4 is the mask of the 4th button (typically the "Browser Back" button) // MouseMask4 is the mask of the 4th button (typically the "Browser Back" button)
MouseMask4 = 8 MouseMask4 = 8
//MouseMask5 is the mask of the 5th button (typically the "Browser Forward" button) //MouseMask5 is the mask of the 5th button (typically the "Browser Forward" button)
MouseMask5 = 16 MouseMask5 = 16
) )
// MouseEvent represent a mouse event
type MouseEvent struct { type MouseEvent struct {
// TimeStamp is the time at which the event was created (in milliseconds). // TimeStamp is the time at which the event was created (in milliseconds).
// This value is time since epoch—but in reality, browsers' definitions vary. // This value is time since epoch—but in reality, browsers' definitions vary.
@ -144,56 +244,6 @@ type MouseEvent struct {
MetaKey bool MetaKey bool
} }
var mouseEvents = map[string]struct{ jsEvent, jsFunc string }{
ClickEvent: {jsEvent: "onclick", jsFunc: "clickEvent"},
DoubleClickEvent: {jsEvent: "ondblclick", jsFunc: "doubleClickEvent"},
MouseDown: {jsEvent: "onmousedown", jsFunc: "mouseDownEvent"},
MouseUp: {jsEvent: "onmouseup", jsFunc: "mouseUpEvent"},
MouseMove: {jsEvent: "onmousemove", jsFunc: "mouseMoveEvent"},
MouseOut: {jsEvent: "onmouseout", jsFunc: "mouseOutEvent"},
MouseOver: {jsEvent: "onmouseover", jsFunc: "mouseOverEvent"},
ContextMenuEvent: {jsEvent: "oncontextmenu", jsFunc: "contextMenuEvent"},
}
func (view *viewData) setMouseListener(tag string, value any) bool {
listeners, ok := valueToEventListeners[View, MouseEvent](value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removeMouseListener(tag)
} else if js, ok := mouseEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
}
} else {
return false
}
return true
}
func (view *viewData) removeMouseListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := mouseEvents[tag]; ok {
removeProperty(view.htmlID(), js.jsEvent, view.Session())
}
}
}
func mouseEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range mouseEvents {
if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View, MouseEvent)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
}
}
}
}
func getTimeStamp(data DataObject) uint64 { func getTimeStamp(data DataObject) uint64 {
if value, ok := data.PropertyValue("timeStamp"); ok { if value, ok := data.PropertyValue("timeStamp"); ok {
if index := strings.Index(value, "."); index > 0 { if index := strings.Index(value, "."); index > 0 {
@ -223,63 +273,135 @@ func (event *MouseEvent) init(data DataObject) {
event.MetaKey = dataBoolProperty(data, "metaKey") event.MetaKey = dataBoolProperty(data, "metaKey")
} }
func handleMouseEvents(view View, tag string, data DataObject) { func handleMouseEvents(view View, tag PropertyName, data DataObject) {
listeners := getEventListeners[View, MouseEvent](view, nil, tag) listeners := getOneArgEventListeners[View, MouseEvent](view, nil, tag)
if len(listeners) > 0 { if len(listeners) > 0 {
var event MouseEvent var event MouseEvent
event.init(data) event.init(data)
for _, listener := range listeners { for _, listener := range listeners {
listener(view, event) listener.Run(view, event)
} }
} }
} }
// GetClickListeners returns the "click-event" listener list. If there are no listeners then the empty list is returned. // GetClickListeners returns the "click-event" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetClickListeners(view View, subviewID ...string) []func(View, MouseEvent) { // Result elements can be of the following types:
return getEventListeners[View, MouseEvent](view, subviewID, ClickEvent) // - func(View, MouseEvent),
// - func(View),
// - func(MouseEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetClickListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, MouseEvent](view, subviewID, ClickEvent)
} }
// GetDoubleClickListeners returns the "double-click-event" listener list. If there are no listeners then the empty list is returned. // GetDoubleClickListeners returns the "double-click-event" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetDoubleClickListeners(view View, subviewID ...string) []func(View, MouseEvent) { // Result elements can be of the following types:
return getEventListeners[View, MouseEvent](view, subviewID, DoubleClickEvent) // - func(View, MouseEvent),
// - func(View),
// - func(MouseEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetDoubleClickListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, MouseEvent](view, subviewID, DoubleClickEvent)
} }
// GetContextMenuListeners returns the "context-menu" listener list. // GetContextMenuListeners returns the "context-menu" listener list.
// If there are no listeners then the empty list is returned. // If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetContextMenuListeners(view View, subviewID ...string) []func(View, MouseEvent) { // Result elements can be of the following types:
return getEventListeners[View, MouseEvent](view, subviewID, ContextMenuEvent) // - func(View, MouseEvent),
// - func(View),
// - func(MouseEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetContextMenuListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, MouseEvent](view, subviewID, ContextMenuEvent)
} }
// GetMouseDownListeners returns the "mouse-down" listener list. If there are no listeners then the empty list is returned. // GetMouseDownListeners returns the "mouse-down" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetMouseDownListeners(view View, subviewID ...string) []func(View, MouseEvent) { // Result elements can be of the following types:
return getEventListeners[View, MouseEvent](view, subviewID, MouseDown) // - func(View, MouseEvent),
// - func(View),
// - func(MouseEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetMouseDownListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, MouseEvent](view, subviewID, MouseDown)
} }
// GetMouseUpListeners returns the "mouse-up" listener list. If there are no listeners then the empty list is returned. // GetMouseUpListeners returns the "mouse-up" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetMouseUpListeners(view View, subviewID ...string) []func(View, MouseEvent) { // Result elements can be of the following types:
return getEventListeners[View, MouseEvent](view, subviewID, MouseUp) // - func(View, MouseEvent),
// - func(View),
// - func(MouseEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetMouseUpListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, MouseEvent](view, subviewID, MouseUp)
} }
// GetMouseMoveListeners returns the "mouse-move" listener list. If there are no listeners then the empty list is returned. // GetMouseMoveListeners returns the "mouse-move" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetMouseMoveListeners(view View, subviewID ...string) []func(View, MouseEvent) { // Result elements can be of the following types:
return getEventListeners[View, MouseEvent](view, subviewID, MouseMove) // - func(View, MouseEvent),
// - func(View),
// - func(MouseEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetMouseMoveListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, MouseEvent](view, subviewID, MouseMove)
} }
// GetMouseOverListeners returns the "mouse-over" listener list. If there are no listeners then the empty list is returned. // GetMouseOverListeners returns the "mouse-over" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetMouseOverListeners(view View, subviewID ...string) []func(View, MouseEvent) { // Result elements can be of the following types:
return getEventListeners[View, MouseEvent](view, subviewID, MouseOver) // - func(View, MouseEvent),
// - func(View),
// - func(MouseEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetMouseOverListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, MouseEvent](view, subviewID, MouseOver)
} }
// GetMouseOutListeners returns the "mouse-out" listener list. If there are no listeners then the empty list is returned. // GetMouseOutListeners returns the "mouse-out" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetMouseOutListeners(view View, subviewID ...string) []func(View, MouseEvent) { // Result elements can be of the following types:
return getEventListeners[View, MouseEvent](view, subviewID, MouseOut) // - func(View, MouseEvent),
// - func(View),
// - func(MouseEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetMouseOutListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, MouseEvent](view, subviewID, MouseOut)
} }

View File

@ -7,30 +7,109 @@ import (
"strings" "strings"
) )
// Constants related to [NumberPicker] specific properties and events
const ( const (
NumberChangedEvent = "number-changed" // NumberChangedEvent is the constant for "number-changed" property tag.
NumberPickerType = "number-picker-type" //
NumberPickerMin = "number-picker-min" // Used by NumberPicker.
NumberPickerMax = "number-picker-max" // Set listener(s) that track the change in the entered value.
NumberPickerStep = "number-picker-step" //
NumberPickerValue = "number-picker-value" // General listener format:
//
// func(picker rui.NumberPicker, newValue float64, oldValue float64)
//
// where:
// - picker - Interface of a number picker which generated this event,
// - newValue - New value,
// - oldValue - Old Value.
//
// Allowed listener formats:
//
// func(picker rui.NumberPicker, newValue float64)
// func(newValue float64, oldValue float64)
// func(newValue float64)
// func()
NumberChangedEvent PropertyName = "number-changed"
// NumberPickerType is the constant for "number-picker-type" property tag.
//
// Used by NumberPicker.
// Sets the visual representation.
//
// Supported types: int, string.
//
// Values:
// - 0 (NumberEditor) or "editor" - Displayed as an editor.
// - 1 (NumberSlider) or "slider" - Displayed as a slider.
NumberPickerType PropertyName = "number-picker-type"
// NumberPickerMin is the constant for "number-picker-min" property tag.
//
// Used by NumberPicker.
// Set the minimum value. The default value is 0.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
NumberPickerMin PropertyName = "number-picker-min"
// NumberPickerMax is the constant for "number-picker-max" property tag.
//
// Used by NumberPicker.
// Set the maximum value. The default value is 1.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
NumberPickerMax PropertyName = "number-picker-max"
// NumberPickerStep is the constant for "number-picker-step" property tag.
//
// Used by NumberPicker.
// Set the value change step.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
NumberPickerStep PropertyName = "number-picker-step"
// NumberPickerValue is the constant for "number-picker-value" property tag.
//
// Used by NumberPicker.
// Current value. The default value is 0.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
NumberPickerValue PropertyName = "number-picker-value"
// NumberPickerValue is the constant for "number-picker-value" property tag.
//
// Used by NumberPicker.
// Precision of displaying fractional part in editor. The default value is 0 (not used).
//
// Supported types: int, int8...int64, uint, uint8...uint64, string.
//
// Internal type is float, other types converted to it during assignment.
NumberPickerPrecision PropertyName = "number-picker-precision"
) )
// Constants which describe values of the "number-picker-type" property of a [NumberPicker]
const ( const (
// NumberEditor - type of NumberPicker. NumberPicker is presented by editor // NumberEditor - type of NumberPicker. NumberPicker is presented by editor
NumberEditor = 0 NumberEditor = 0
// NumberSlider - type of NumberPicker. NumberPicker is presented by slider // NumberSlider - type of NumberPicker. NumberPicker is presented by slider
NumberSlider = 1 NumberSlider = 1
) )
// NumberPicker - NumberPicker view // NumberPicker represents a NumberPicker view
type NumberPicker interface { type NumberPicker interface {
View View
} }
type numberPickerData struct { type numberPickerData struct {
viewData viewData
numberChangedListeners []func(NumberPicker, float64)
} }
// NewNumberPicker create new NumberPicker object and return it // NewNumberPicker create new NumberPicker object and return it
@ -42,146 +121,114 @@ func NewNumberPicker(session Session, params Params) NumberPicker {
} }
func newNumberPicker(session Session) View { func newNumberPicker(session Session) View {
return NewNumberPicker(session, nil) return new(numberPickerData)
} }
func (picker *numberPickerData) init(session Session) { func (picker *numberPickerData) init(session Session) {
picker.viewData.init(session) picker.viewData.init(session)
picker.tag = "NumberPicker" picker.tag = "NumberPicker"
picker.numberChangedListeners = []func(NumberPicker, float64){} picker.hasHtmlDisabled = true
} picker.normalize = normalizeNumberPickerTag
picker.get = picker.getFunc
func (picker *numberPickerData) String() string { picker.set = picker.setFunc
return getViewString(picker) picker.changed = picker.propertyChanged
} }
func (picker *numberPickerData) Focusable() bool { func (picker *numberPickerData) Focusable() bool {
return true return true
} }
func (picker *numberPickerData) normalizeTag(tag string) string { func normalizeNumberPickerTag(tag PropertyName) PropertyName {
tag = strings.ToLower(tag) tag = defaultNormalize(tag)
switch tag { switch tag {
case Type, Min, Max, Step, Value: case Type, Min, Max, Step, Value, "precision":
return "number-picker-" + tag return "number-picker-" + tag
} }
return tag return normalizeDataListTag(tag)
} }
func (picker *numberPickerData) Remove(tag string) { func (picker *numberPickerData) getFunc(tag PropertyName) any {
picker.remove(picker.normalizeTag(tag))
}
func (picker *numberPickerData) remove(tag string) {
switch tag { switch tag {
case NumberChangedEvent: case NumberChangedEvent:
if len(picker.numberChangedListeners) > 0 { if listeners := getTwoArgEventRawListeners[NumberPicker, float64](picker, nil, tag); len(listeners) > 0 {
picker.numberChangedListeners = []func(NumberPicker, float64){} return listeners
picker.propertyChangedEvent(tag)
} }
return nil
default:
picker.viewData.remove(tag)
picker.propertyChanged(tag)
} }
return picker.viewData.getFunc(tag)
} }
func (picker *numberPickerData) Set(tag string, value any) bool { func (picker *numberPickerData) setFunc(tag PropertyName, value any) []PropertyName {
return picker.set(picker.normalizeTag(tag), value)
}
func (picker *numberPickerData) set(tag string, value any) bool {
if value == nil {
picker.remove(tag)
return true
}
switch tag { switch tag {
case NumberChangedEvent: case NumberChangedEvent:
listeners, ok := valueToEventListeners[NumberPicker, float64](value) return setTwoArgEventListener[NumberPicker, float64](picker, tag, value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(NumberPicker, float64){}
}
picker.numberChangedListeners = listeners
picker.propertyChangedEvent(tag)
return true
case NumberPickerValue: case NumberPickerValue:
oldValue := GetNumberPickerValue(picker) picker.setRaw("old-number", GetNumberPickerValue(picker))
min, max := GetNumberPickerMinMax(picker) min, max := GetNumberPickerMinMax(picker)
if picker.setFloatProperty(NumberPickerValue, value, min, max) {
if f, ok := floatProperty(picker, NumberPickerValue, picker.Session(), min); ok && f != oldValue {
newValue, _ := floatTextProperty(picker, NumberPickerValue, picker.Session(), min)
if picker.created {
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, picker.htmlID(), newValue))
}
for _, listener := range picker.numberChangedListeners {
listener(picker, f)
}
picker.propertyChangedEvent(tag)
}
return true
}
default: return setFloatProperty(picker, NumberPickerValue, value, min, max)
if picker.viewData.set(tag, value) {
picker.propertyChanged(tag) case DataList:
return true return setDataList(picker, value, "")
}
} }
return false
return picker.viewData.setFunc(tag, value)
} }
func (picker *numberPickerData) propertyChanged(tag string) { func (picker *numberPickerData) numberFormat() string {
if picker.created { if precision := GetNumberPickerPrecision(picker); precision > 0 {
switch tag { return fmt.Sprintf("%%.%df", precision)
case NumberPickerType:
if GetNumberPickerType(picker) == NumberSlider {
updateProperty(picker.htmlID(), "type", "range", picker.session)
} else {
updateProperty(picker.htmlID(), "type", "number", picker.session)
}
case NumberPickerMin:
min, _ := GetNumberPickerMinMax(picker)
updateProperty(picker.htmlID(), Min, strconv.FormatFloat(min, 'f', -1, 32), picker.session)
case NumberPickerMax:
_, max := GetNumberPickerMinMax(picker)
updateProperty(picker.htmlID(), Max, strconv.FormatFloat(max, 'f', -1, 32), picker.session)
case NumberPickerStep:
if step := GetNumberPickerStep(picker); step > 0 {
updateProperty(picker.htmlID(), Step, strconv.FormatFloat(step, 'f', -1, 32), picker.session)
} else {
updateProperty(picker.htmlID(), Step, "any", picker.session)
}
case NumberPickerValue:
value := GetNumberPickerValue(picker)
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%f')`, picker.htmlID(), value))
for _, listener := range picker.numberChangedListeners {
listener(picker, value)
}
}
} }
return "%g"
} }
func (picker *numberPickerData) Get(tag string) any { func (picker *numberPickerData) propertyChanged(tag PropertyName) {
return picker.get(picker.normalizeTag(tag))
}
func (picker *numberPickerData) get(tag string) any {
switch tag { switch tag {
case NumberChangedEvent: case NumberPickerType:
return picker.numberChangedListeners if GetNumberPickerType(picker) == NumberSlider {
picker.Session().updateProperty(picker.htmlID(), "type", "range")
} else {
picker.Session().updateProperty(picker.htmlID(), "type", "number")
}
case NumberPickerMin:
min, _ := GetNumberPickerMinMax(picker)
picker.Session().updateProperty(picker.htmlID(), "min", fmt.Sprintf(picker.numberFormat(), min))
case NumberPickerMax:
_, max := GetNumberPickerMinMax(picker)
picker.Session().updateProperty(picker.htmlID(), "max", fmt.Sprintf(picker.numberFormat(), max))
case NumberPickerStep:
if step := GetNumberPickerStep(picker); step > 0 {
picker.Session().updateProperty(picker.htmlID(), "step", fmt.Sprintf(picker.numberFormat(), step))
} else {
picker.Session().updateProperty(picker.htmlID(), "step", "any")
}
case NumberPickerValue:
value := GetNumberPickerValue(picker)
format := picker.numberFormat()
picker.Session().callFunc("setInputValue", picker.htmlID(), fmt.Sprintf(format, value))
if listeners := getTwoArgEventListeners[NumberPicker, float64](picker, nil, NumberChangedEvent); len(listeners) > 0 {
old := 0.0
if val := picker.getRaw("old-number"); val != nil {
if n, ok := val.(float64); ok {
old = n
}
}
if old != value {
for _, listener := range listeners {
listener.Run(picker, value, old)
}
}
}
default: default:
return picker.viewData.get(tag) picker.viewData.propertyChanged(tag)
} }
} }
@ -189,6 +236,13 @@ func (picker *numberPickerData) htmlTag() string {
return "input" return "input"
} }
func (picker *numberPickerData) htmlSubviews(self View, buffer *strings.Builder) {
dataListHtmlSubviews(self, buffer, func(text string, session Session) string {
text, _ = session.resolveConstants(text)
return text
})
}
func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builder) { func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builder) {
picker.viewData.htmlProperties(self, buffer) picker.viewData.htmlProperties(self, buffer)
@ -198,43 +252,39 @@ func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builde
buffer.WriteString(` type="number"`) buffer.WriteString(` type="number"`)
} }
format := picker.numberFormat()
min, max := GetNumberPickerMinMax(picker) min, max := GetNumberPickerMinMax(picker)
if min != math.Inf(-1) { if min != math.Inf(-1) {
buffer.WriteString(` min="`) buffer.WriteString(` min="`)
buffer.WriteString(strconv.FormatFloat(min, 'f', -1, 64)) fmt.Fprintf(buffer, format, min)
buffer.WriteByte('"') buffer.WriteByte('"')
} }
if max != math.Inf(1) { if max != math.Inf(1) {
buffer.WriteString(` max="`) buffer.WriteString(` max="`)
buffer.WriteString(strconv.FormatFloat(max, 'f', -1, 64)) fmt.Fprintf(buffer, format, max)
buffer.WriteByte('"') buffer.WriteByte('"')
} }
step := GetNumberPickerStep(picker) step := GetNumberPickerStep(picker)
if step != 0 { if step != 0 {
buffer.WriteString(` step="`) buffer.WriteString(` step="`)
buffer.WriteString(strconv.FormatFloat(step, 'f', -1, 64)) fmt.Fprintf(buffer, format, step)
buffer.WriteByte('"') buffer.WriteByte('"')
} else { } else {
buffer.WriteString(` step="any"`) buffer.WriteString(` step="any"`)
} }
buffer.WriteString(` value="`) buffer.WriteString(` value="`)
buffer.WriteString(strconv.FormatFloat(GetNumberPickerValue(picker), 'f', -1, 64)) fmt.Fprintf(buffer, format, GetNumberPickerValue(picker))
buffer.WriteByte('"') buffer.WriteByte('"')
buffer.WriteString(` oninput="editViewInputEvent(this)"`) buffer.WriteString(` oninput="editViewInputEvent(this)"`)
dataListHtmlProperties(picker, buffer)
} }
func (picker *numberPickerData) htmlDisabledProperties(self View, buffer *strings.Builder) { func (picker *numberPickerData) handleCommand(self View, command PropertyName, data DataObject) bool {
if IsDisabled(self) {
buffer.WriteString(` disabled`)
}
picker.viewData.htmlDisabledProperties(self, buffer)
}
func (picker *numberPickerData) handleCommand(self View, command string, data DataObject) bool {
switch command { switch command {
case "textChanged": case "textChanged":
if text, ok := data.PropertyValue("text"); ok { if text, ok := data.PropertyValue("text"); ok {
@ -242,9 +292,10 @@ func (picker *numberPickerData) handleCommand(self View, command string, data Da
oldValue := GetNumberPickerValue(picker) oldValue := GetNumberPickerValue(picker)
picker.properties[NumberPickerValue] = text picker.properties[NumberPickerValue] = text
if value != oldValue { if value != oldValue {
for _, listener := range picker.numberChangedListeners { for _, listener := range getTwoArgEventListeners[NumberPicker, float64](picker, nil, NumberChangedEvent) {
listener(picker, value) listener.Run(picker, value, oldValue)
} }
picker.runChangeListener(NumberPickerValue)
} }
} }
} }
@ -257,20 +308,20 @@ func (picker *numberPickerData) handleCommand(self View, command string, data Da
// GetNumberPickerType returns the type of NumberPicker subview. Valid values: // GetNumberPickerType returns the type of NumberPicker subview. Valid values:
// NumberEditor (0) - NumberPicker is presented by editor (default type); // NumberEditor (0) - NumberPicker is presented by editor (default type);
// NumberSlider (1) - NumberPicker is presented by slider. // NumberSlider (1) - NumberPicker is presented by slider.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetNumberPickerType(view View, subviewID ...string) int { func GetNumberPickerType(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, NumberPickerType, NumberEditor, false) return enumStyledProperty(view, subviewID, NumberPickerType, NumberEditor, false)
} }
// GetNumberPickerMinMax returns the min and max value of NumberPicker subview. // GetNumberPickerMinMax returns the min and max value of NumberPicker subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetNumberPickerMinMax(view View, subviewID ...string) (float64, float64) { func GetNumberPickerMinMax(view View, subviewID ...string) (float64, float64) {
var pickerType int view = getSubview(view, subviewID)
if len(subviewID) > 0 && subviewID[0] != "" { pickerType := GetNumberPickerType(view)
pickerType = GetNumberPickerType(view, subviewID[0])
} else {
pickerType = GetNumberPickerType(view)
}
var defMin, defMax float64 var defMin, defMax float64
if pickerType == NumberSlider { if pickerType == NumberSlider {
@ -281,8 +332,8 @@ func GetNumberPickerMinMax(view View, subviewID ...string) (float64, float64) {
defMax = math.Inf(1) defMax = math.Inf(1)
} }
min := floatStyledProperty(view, subviewID, NumberPickerMin, defMin) min := floatStyledProperty(view, nil, NumberPickerMin, defMin)
max := floatStyledProperty(view, subviewID, NumberPickerMax, defMax) max := floatStyledProperty(view, nil, NumberPickerMax, defMax)
if min > max { if min > max {
return max, min return max, min
@ -291,16 +342,14 @@ func GetNumberPickerMinMax(view View, subviewID ...string) (float64, float64) {
} }
// GetNumberPickerStep returns the value changing step of NumberPicker subview. // GetNumberPickerStep returns the value changing step of NumberPicker subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetNumberPickerStep(view View, subviewID ...string) float64 { func GetNumberPickerStep(view View, subviewID ...string) float64 {
var max float64 view = getSubview(view, subviewID)
if len(subviewID) > 0 && subviewID[0] != "" { _, max := GetNumberPickerMinMax(view)
_, max = GetNumberPickerMinMax(view, subviewID[0])
} else {
_, max = GetNumberPickerMinMax(view)
}
result := floatStyledProperty(view, subviewID, NumberPickerStep, 0) result := floatStyledProperty(view, nil, NumberPickerStep, 0)
if result > max { if result > max {
return max return max
} }
@ -308,22 +357,37 @@ func GetNumberPickerStep(view View, subviewID ...string) float64 {
} }
// GetNumberPickerValue returns the value of NumberPicker subview. // GetNumberPickerValue returns the value of NumberPicker subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetNumberPickerValue(view View, subviewID ...string) float64 { func GetNumberPickerValue(view View, subviewID ...string) float64 {
var min float64 view = getSubview(view, subviewID)
if len(subviewID) > 0 && subviewID[0] != "" { min, _ := GetNumberPickerMinMax(view)
min, _ = GetNumberPickerMinMax(view, subviewID[0]) return floatStyledProperty(view, nil, NumberPickerValue, min)
} else {
min, _ = GetNumberPickerMinMax(view)
}
result := floatStyledProperty(view, subviewID, NumberPickerValue, min)
return result
} }
// GetNumberChangedListeners returns the NumberChangedListener list of an NumberPicker subview. // GetNumberChangedListeners returns the NumberChangedListener list of an NumberPicker subview.
// If there are no listeners then the empty list is returned // If there are no listeners then the empty list is returned
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetNumberChangedListeners(view View, subviewID ...string) []func(NumberPicker, float64) { // Result elements can be of the following types:
return getEventListeners[NumberPicker, float64](view, subviewID, NumberChangedEvent) // - func(rui.NumberPicker, float64, float64),
// - func(rui.NumberPicker, float64),
// - func(rui.NumberPicker),
// - func(float64, float64),
// - func(float64),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetNumberChangedListeners(view View, subviewID ...string) []any {
return getTwoArgEventRawListeners[NumberPicker, float64](view, subviewID, NumberChangedEvent)
}
// GetNumberPickerPrecision returns the precision of displaying fractional part in editor of NumberPicker subview.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetNumberPickerPrecision(view View, subviewID ...string) int {
return intStyledProperty(view, subviewID, NumberPickerPrecision, 0)
} }

View File

@ -5,88 +5,72 @@ import (
"strings" "strings"
) )
// OutlineProperty defines a view's outside border
type OutlineProperty interface { type OutlineProperty interface {
Properties Properties
stringWriter stringWriter
fmt.Stringer fmt.Stringer
// ViewOutline returns style color and line width of an outline
ViewOutline(session Session) ViewOutline ViewOutline(session Session) ViewOutline
} }
type outlinePropertyData struct { type outlinePropertyData struct {
propertyList dataProperty
} }
// NewOutlineProperty creates the new OutlineProperty.
//
// The following properties can be used:
// - "color" (ColorTag) - Determines the line color (Color);
// - "width" (Width) - Determines the line thickness (SizeUnit).
func NewOutlineProperty(params Params) OutlineProperty { func NewOutlineProperty(params Params) OutlineProperty {
outline := new(outlinePropertyData) outline := new(outlinePropertyData)
outline.properties = map[string]any{} outline.init()
for tag, value := range params { for tag, value := range params {
outline.Set(tag, value) outline.Set(tag, value)
} }
return outline return outline
} }
func (outline *outlinePropertyData) writeString(buffer *strings.Builder, indent string) { func (outline *outlinePropertyData) init() {
buffer.WriteString("_{ ") outline.dataProperty.init()
comma := false outline.normalize = normalizeOutlineTag
for _, tag := range []string{Style, Width, ColorTag} { outline.set = outlineSet
if value, ok := outline.properties[tag]; ok { outline.supportedProperties = []PropertyName{Style, Width, ColorTag}
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(" = ")
writePropertyValue(buffer, BorderStyle, value, indent)
comma = true
}
}
buffer.WriteString(" }")
} }
func (outline *outlinePropertyData) String() string { func (outline *outlinePropertyData) String() string {
return runStringWriter(outline) return runStringWriter(outline)
} }
func (outline *outlinePropertyData) normalizeTag(tag string) string { func normalizeOutlineTag(tag PropertyName) PropertyName {
return strings.TrimPrefix(strings.ToLower(tag), "outline-") tag = defaultNormalize(tag)
return PropertyName(strings.TrimPrefix(string(tag), "outline-"))
} }
func (outline *outlinePropertyData) Remove(tag string) { func outlineSet(properties Properties, tag PropertyName, value any) []PropertyName {
delete(outline.properties, outline.normalizeTag(tag))
}
func (outline *outlinePropertyData) Set(tag string, value any) bool {
if value == nil {
outline.Remove(tag)
return true
}
tag = outline.normalizeTag(tag)
switch tag { switch tag {
case Style: case Style:
return outline.setEnumProperty(Style, value, enumProperties[BorderStyle].values) return setEnumProperty(properties, Style, value, enumProperties[BorderStyle].values)
case Width: case Width:
if width, ok := value.(SizeUnit); ok { if width, ok := value.(SizeUnit); ok {
switch width.Type { switch width.Type {
case SizeInFraction, SizeInPercent: case SizeInFraction, SizeInPercent:
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return nil
} }
} }
return outline.setSizeProperty(Width, value) return setSizeProperty(properties, Width, value)
case ColorTag: case ColorTag:
return outline.setColorProperty(ColorTag, value) return setColorProperty(properties, ColorTag, value)
default: default:
ErrorLogF(`"%s" property is not compatible with the OutlineProperty`, tag) ErrorLogF(`"%s" property is not compatible with the OutlineProperty`, tag)
} }
return false return nil
}
func (outline *outlinePropertyData) Get(tag string) any {
return outline.propertyList.Get(outline.normalizeTag(tag))
} }
func (outline *outlinePropertyData) ViewOutline(session Session) ViewOutline { func (outline *outlinePropertyData) ViewOutline(session Session) ViewOutline {
@ -98,8 +82,13 @@ func (outline *outlinePropertyData) ViewOutline(session Session) ViewOutline {
// ViewOutline describes parameters of a view border // ViewOutline describes parameters of a view border
type ViewOutline struct { type ViewOutline struct {
// Style of the outline line
Style int Style int
// Color of the outline line
Color Color Color Color
// Width of the outline line
Width SizeUnit Width SizeUnit
} }
@ -118,7 +107,7 @@ func (outline ViewOutline) cssString(session Session) string {
return builder.finish() return builder.finish()
} }
func getOutline(properties Properties) OutlineProperty { func getOutlineProperty(properties Properties) OutlineProperty {
if value := properties.Get(Outline); value != nil { if value := properties.Get(Outline); value != nil {
if outline, ok := value.(OutlineProperty); ok { if outline, ok := value.(OutlineProperty); ok {
return outline return outline
@ -128,30 +117,30 @@ func getOutline(properties Properties) OutlineProperty {
return nil return nil
} }
func (style *viewStyle) setOutline(value any) bool { func setOutlineProperty(properties Properties, value any) []PropertyName {
switch value := value.(type) { switch value := value.(type) {
case OutlineProperty: case OutlineProperty:
style.properties[Outline] = value properties.setRaw(Outline, value)
case ViewOutline: case ViewOutline:
style.properties[Outline] = NewOutlineProperty(Params{Style: value.Style, Width: value.Width, ColorTag: value.Color}) properties.setRaw(Outline, NewOutlineProperty(Params{Style: value.Style, Width: value.Width, ColorTag: value.Color}))
case ViewBorder: case ViewBorder:
style.properties[Outline] = NewOutlineProperty(Params{Style: value.Style, Width: value.Width, ColorTag: value.Color}) properties.setRaw(Outline, NewOutlineProperty(Params{Style: value.Style, Width: value.Width, ColorTag: value.Color}))
case DataObject: case DataObject:
outline := NewOutlineProperty(nil) outline := NewOutlineProperty(nil)
for _, tag := range []string{Style, Width, ColorTag} { for _, tag := range []PropertyName{Style, Width, ColorTag} {
if text, ok := value.PropertyValue(tag); ok && text != "" { if text, ok := value.PropertyValue(string(tag)); ok && text != "" {
outline.Set(tag, text) outline.Set(tag, text)
} }
} }
style.properties[Outline] = outline properties.setRaw(Outline, outline)
default: default:
notCompatibleType(Outline, value) notCompatibleType(Outline, value)
return false return nil
} }
return true return []PropertyName{Outline}
} }

View File

@ -1,27 +1,34 @@
package rui package rui
import "sort" import (
"iter"
"slices"
)
// Params defines a type of a parameters list // Params defines a type of a parameters list
type Params map[string]any type Params map[PropertyName]any
func (params Params) Get(tag string) any { // Get returns a value of the property with name defined by the argument. The type of return value depends
// on the property. If the property is not set then nil is returned.
func (params Params) Get(tag PropertyName) any {
return params.getRaw(tag) return params.getRaw(tag)
} }
func (params Params) getRaw(tag string) any { func (params Params) getRaw(tag PropertyName) any {
if value, ok := params[tag]; ok { if value, ok := params[tag]; ok {
return value return value
} }
return nil return nil
} }
func (params Params) Set(tag string, value any) bool { // Set sets the value (second argument) of the property with name defined by the first argument.
// Return "true" if the value has been set, in the opposite case "false" is returned and a description of an error is written to the log
func (params Params) Set(tag PropertyName, value any) bool {
params.setRaw(tag, value) params.setRaw(tag, value)
return true return true
} }
func (params Params) setRaw(tag string, value any) { func (params Params) setRaw(tag PropertyName, value any) {
if value != nil { if value != nil {
params[tag] = value params[tag] = value
} else { } else {
@ -29,21 +36,38 @@ func (params Params) setRaw(tag string, value any) {
} }
} }
func (params Params) Remove(tag string) { // Remove removes the property with name defined by the argument from a map.
func (params Params) Remove(tag PropertyName) {
delete(params, tag) delete(params, tag)
} }
// Clear removes all properties from a map.
func (params Params) Clear() { func (params Params) Clear() {
for tag := range params { for tag := range params {
delete(params, tag) delete(params, tag)
} }
} }
func (params Params) AllTags() []string { func (params Params) All() iter.Seq2[PropertyName, any] {
tags := make([]string, 0, len(params)) return func(yield func(PropertyName, any) bool) {
for tag, value := range params {
if !yield(tag, value) {
return
}
}
}
}
// AllTags returns a sorted slice of all properties.
func (params Params) AllTags() []PropertyName {
tags := make([]PropertyName, 0, len(params))
for t := range params { for t := range params {
tags = append(tags, t) tags = append(tags, t)
} }
sort.Strings(tags) slices.Sort(tags)
return tags return tags
} }
func (params Params) IsEmpty() bool {
return len(params) == 0
}

153
path.go
View File

@ -1,15 +1,7 @@
package rui package rui
import (
"strconv"
"strings"
)
// Path is a path interface // Path is a path interface
type Path interface { type Path interface {
// Reset erases the Path
Reset()
// MoveTo begins a new sub-path at the point specified by the given (x, y) coordinates // MoveTo begins a new sub-path at the point specified by the given (x, y) coordinates
MoveTo(x, y float64) MoveTo(x, y float64)
@ -19,178 +11,119 @@ type Path interface {
// ArcTo adds a circular arc to the current sub-path, using the given control points and radius. // ArcTo adds a circular arc to the current sub-path, using the given control points and radius.
// The arc is automatically connected to the path's latest point with a straight line, if necessary. // The arc is automatically connected to the path's latest point with a straight line, if necessary.
// x0, y0 - coordinates of the first control point; // - x0, y0 - coordinates of the first control point;
// x1, y1 - coordinates of the second control point; // - x1, y1 - coordinates of the second control point;
// radius - the arc's radius. Must be non-negative. // - radius - the arc's radius. Must be non-negative.
ArcTo(x0, y0, x1, y1, radius float64) ArcTo(x0, y0, x1, y1, radius float64)
// Arc adds a circular arc to the current sub-path. // Arc adds a circular arc to the current sub-path.
// x, y - coordinates of the arc's center; // - x, y - coordinates of the arc's center;
// radius - the arc's radius. Must be non-negative; // - radius - the arc's radius. Must be non-negative;
// startAngle - the angle at which the arc starts, measured clockwise from the positive // - startAngle - the angle at which the arc starts, measured clockwise from the positive
// x-axis and expressed in radians. // x-axis and expressed in radians.
// endAngle - the angle at which the arc ends, measured clockwise from the positive // - endAngle - the angle at which the arc ends, measured clockwise from the positive
// x-axis and expressed in radians. // x-axis and expressed in radians.
// clockwise - if true, causes the arc to be drawn clockwise between the start and end angles, // - clockwise - if true, causes the arc to be drawn clockwise between the start and end angles,
// otherwise - counter-clockwise // otherwise - counter-clockwise
Arc(x, y, radius, startAngle, endAngle float64, clockwise bool) Arc(x, y, radius, startAngle, endAngle float64, clockwise bool)
// BezierCurveTo adds a cubic Bézier curve to the current sub-path. The starting point is // BezierCurveTo adds a cubic Bézier curve to the current sub-path. The starting point is
// the latest point in the current path. // the latest point in the current path.
// cp0x, cp0y - coordinates of the first control point; // - cp0x, cp0y - coordinates of the first control point;
// cp1x, cp1y - coordinates of the second control point; // - cp1x, cp1y - coordinates of the second control point;
// x, y - coordinates of the end point. // - x, y - coordinates of the end point.
BezierCurveTo(cp0x, cp0y, cp1x, cp1y, x, y float64) BezierCurveTo(cp0x, cp0y, cp1x, cp1y, x, y float64)
// QuadraticCurveTo adds a quadratic Bézier curve to the current sub-path. // QuadraticCurveTo adds a quadratic Bézier curve to the current sub-path.
// cpx, cpy - coordinates of the control point; // - cpx, cpy - coordinates of the control point;
// x, y - coordinates of the end point. // - x, y - coordinates of the end point.
QuadraticCurveTo(cpx, cpy, x, y float64) QuadraticCurveTo(cpx, cpy, x, y float64)
// Ellipse adds an elliptical arc to the current sub-path // Ellipse adds an elliptical arc to the current sub-path
// x, y - coordinates of the ellipse's center; // - x, y - coordinates of the ellipse's center;
// radiusX - the ellipse's major-axis radius. Must be non-negative; // - radiusX - the ellipse's major-axis radius. Must be non-negative;
// radiusY - the ellipse's minor-axis radius. Must be non-negative; // - radiusY - the ellipse's minor-axis radius. Must be non-negative;
// rotation - the rotation of the ellipse, expressed in radians; // - rotation - the rotation of the ellipse, expressed in radians;
// startAngle - the angle at which the ellipse starts, measured clockwise // - startAngle - the angle at which the ellipse starts, measured clockwise
// from the positive x-axis and expressed in radians; // from the positive x-axis and expressed in radians;
// endAngle - the angle at which the ellipse ends, measured clockwise // - endAngle - the angle at which the ellipse ends, measured clockwise
// from the positive x-axis and expressed in radians. // from the positive x-axis and expressed in radians.
// clockwise - if true, draws the ellipse clockwise, otherwise draws counter-clockwise // - clockwise - if true, draws the ellipse clockwise, otherwise draws counter-clockwise
Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, clockwise bool) Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, clockwise bool)
// Close adds a straight line from the current point to the start of the current sub-path. // Close adds a straight line from the current point to the start of the current sub-path.
// If the shape has already been closed or has only one point, this function does nothing. // If the shape has already been closed or has only one point, this function does nothing.
Close() Close()
scriptText() string obj() any
} }
type pathData struct { type pathData struct {
script strings.Builder session Session
varName any
} }
// NewPath creates a new empty Path // NewPath creates a new empty Path
func NewPath() Path { func (canvas *canvasData) NewPath() Path {
path := new(pathData) path := new(pathData)
path.script.Grow(4096) path.session = canvas.session
path.script.WriteString("\nctx.beginPath();") path.varName = canvas.session.createPath("")
return path return path
} }
func (path *pathData) Reset() { func (canvas *canvasData) NewPathFromSvg(data string) Path {
path.script.Reset() path := new(pathData)
path.script.WriteString("\nctx.beginPath();") path.session = canvas.session
path.varName = canvas.session.createPath(data)
return path
} }
func (path *pathData) MoveTo(x, y float64) { func (path *pathData) MoveTo(x, y float64) {
path.script.WriteString("\nctx.moveTo(") path.session.callCanvasVarFunc(path.varName, "moveTo", x, y)
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
path.script.WriteString(");")
} }
func (path *pathData) LineTo(x, y float64) { func (path *pathData) LineTo(x, y float64) {
path.script.WriteString("\nctx.lineTo(") path.session.callCanvasVarFunc(path.varName, "lineTo", x, y)
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
path.script.WriteString(");")
} }
func (path *pathData) ArcTo(x0, y0, x1, y1, radius float64) { func (path *pathData) ArcTo(x0, y0, x1, y1, radius float64) {
if radius > 0 { if radius > 0 {
path.script.WriteString("\nctx.arcTo(") path.session.callCanvasVarFunc(path.varName, "arcTo", x0, y0, x1, y1, radius)
path.script.WriteString(strconv.FormatFloat(x0, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y0, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(x1, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y1, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(radius, 'g', -1, 64))
path.script.WriteString(");")
} }
} }
func (path *pathData) Arc(x, y, radius, startAngle, endAngle float64, clockwise bool) { func (path *pathData) Arc(x, y, radius, startAngle, endAngle float64, clockwise bool) {
if radius > 0 { if radius > 0 {
path.script.WriteString("\nctx.arc(")
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(radius, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(startAngle, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(endAngle, 'g', -1, 64))
if !clockwise { if !clockwise {
path.script.WriteString(",true);") path.session.callCanvasVarFunc(path.varName, "arc", x, y, radius, startAngle, endAngle, true)
} else { } else {
path.script.WriteString(");") path.session.callCanvasVarFunc(path.varName, "arc", x, y, radius, startAngle, endAngle)
} }
} }
} }
func (path *pathData) BezierCurveTo(cp0x, cp0y, cp1x, cp1y, x, y float64) { func (path *pathData) BezierCurveTo(cp0x, cp0y, cp1x, cp1y, x, y float64) {
path.script.WriteString("\nctx.bezierCurveTo(") path.session.callCanvasVarFunc(path.varName, "bezierCurveTo", cp0x, cp0y, cp1x, cp1y, x, y)
path.script.WriteString(strconv.FormatFloat(cp0x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(cp0y, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(cp1x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(cp1y, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
path.script.WriteString(");")
} }
func (path *pathData) QuadraticCurveTo(cpx, cpy, x, y float64) { func (path *pathData) QuadraticCurveTo(cpx, cpy, x, y float64) {
path.script.WriteString("\nctx.quadraticCurveTo(") path.session.callCanvasVarFunc(path.varName, "quadraticCurveTo", cpx, cpy, x, y)
path.script.WriteString(strconv.FormatFloat(cpx, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(cpy, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
path.script.WriteString(");")
} }
func (path *pathData) Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, clockwise bool) { func (path *pathData) Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, clockwise bool) {
if radiusX > 0 && radiusY > 0 { if radiusX > 0 && radiusY > 0 {
path.script.WriteString("\nctx.ellipse(")
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(radiusX, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(radiusY, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(rotation, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(startAngle, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(endAngle, 'g', -1, 64))
if !clockwise { if !clockwise {
path.script.WriteString(",true);") path.session.callCanvasVarFunc(path.varName, "ellipse", x, y, radiusX, radiusY, rotation, startAngle, endAngle, true)
} else { } else {
path.script.WriteString(");") path.session.callCanvasVarFunc(path.varName, "ellipse", x, y, radiusX, radiusY, rotation, startAngle, endAngle)
} }
} }
} }
func (path *pathData) Close() { func (path *pathData) Close() {
path.script.WriteString("\nctx.close();") path.session.callCanvasVarFunc(path.varName, "closePath")
} }
func (path *pathData) scriptText() string { func (path *pathData) obj() any {
return path.script.String() return path.varName
} }

View File

@ -1,54 +1,133 @@
package rui package rui
import ( // Constants for [View] specific pointer events properties
"strings"
)
const ( const (
// PointerDown is the constant for "pointer-down" property tag. // PointerDown is the constant for "pointer-down" property tag.
// The "pointer-down" event is fired when a pointer becomes active. For mouse, it is fired when //
// the device transitions from no buttons depressed to at least one button depressed. // Used by View.
// For touch, it is fired when physical contact is made with the digitizer. // Fired when a pointer becomes active. For mouse, it is fired when the device transitions from no buttons depressed to at
// For pen, it is fired when the stylus makes physical contact with the digitizer. // least one button depressed. For touch, it is fired when physical contact is made with the digitizer. For pen, it is
// The main listener format: func(View, PointerEvent). // fired when the stylus makes physical contact with the digitizer.
// The additional listener formats: func(PointerEvent), func(View), and func(). //
PointerDown = "pointer-down" // General listener format:
//
// func(view rui.View, event rui.PointerEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Pointer event.
//
// Allowed listener formats:
//
// func(event rui.PointerEvent)
// func(view rui.View)
// func()
PointerDown PropertyName = "pointer-down"
// PointerUp is the constant for "pointer-up" property tag. // PointerUp is the constant for "pointer-up" property tag.
// The "pointer-up" event is fired when a pointer is no longer active. //
// The main listener format: func(View, PointerEvent). // Used by View.
// The additional listener formats: func(PointerEvent), func(View), and func(). // Is fired when a pointer is no longer active.
PointerUp = "pointer-up" //
// General listener format:
//
// func(view rui.View, event rui.PointerEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Pointer event.
//
// Allowed listener formats:
//
// func(event rui.PointerEvent)
// func(view rui.View)
// func()
PointerUp PropertyName = "pointer-up"
// PointerMove is the constant for "pointer-move" property tag. // PointerMove is the constant for "pointer-move" property tag.
// The "pointer-move" event is fired when a pointer changes coordinates. //
// The main listener format: func(View, PointerEvent). // Used by View.
// The additional listener formats: func(PointerEvent), func(View), and func(). // Is fired when a pointer changes coordinates.
PointerMove = "pointer-move" //
// General listener format:
//
// func(view rui.View, event rui.PointerEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Pointer event.
//
// Allowed listener formats:
//
// func(event rui.PointerEvent)
// func(view rui.View)
// func()
PointerMove PropertyName = "pointer-move"
// PointerCancel is the constant for "pointer-cancel" property tag. // PointerCancel is the constant for "pointer-cancel" property tag.
// The "pointer-cancel" event is fired if the pointer will no longer be able to generate events //
// (for example the related device is deactivated). // Used by View.
// The main listener format: func(View, PointerEvent). // Is fired if the pointer will no longer be able to generate events (for example the related device is deactivated).
// The additional listener formats: func(PointerEvent), func(View), and func(). //
PointerCancel = "pointer-cancel" // General listener format:
//
// func(view rui.View, event rui.PointerEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Pointer event.
//
// Allowed listener formats:
//
// func(event rui.PointerEvent)
// func(view rui.View)
// func()
PointerCancel PropertyName = "pointer-cancel"
// PointerOut is the constant for "pointer-out" property tag. // PointerOut is the constant for "pointer-out" property tag.
// The "pointer-out" event is fired for several reasons including: pointing device is moved out //
// of the hit test boundaries of an element; firing the pointerup event for a device // Used by View.
// that does not support hover (see "pointer-up"); after firing the pointercancel event (see "pointer-cancel"); // Is fired for several reasons including: pointing device is moved out of the hit test boundaries of an element; firing
// when a pen stylus leaves the hover range detectable by the digitizer. // the "pointer-up" event for a device that does not support hover (see "pointer-up"); after firing the "pointer-cancel"
// The main listener format: func(View, PointerEvent). // event (see "pointer-cancel"); when a pen stylus leaves the hover range detectable by the digitizer.
// The additional listener formats: func(PointerEvent), func(View), and func(). //
PointerOut = "pointer-out" // General listener format:
//
// func(view rui.View, event rui.PointerEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Pointer event.
//
// Allowed listener formats:
//
// func(event rui.PointerEvent)
// func(view rui.View)
// func()
PointerOut PropertyName = "pointer-out"
// PointerOver is the constant for "pointer-over" property tag. // PointerOver is the constant for "pointer-over" property tag.
// The "pointer-over" event is fired when a pointing device is moved into an view's hit test boundaries. //
// The main listener format: func(View, PointerEvent). // Used by View.
// The additional listener formats: func(PointerEvent), func(View), and func(). // Is fired when a pointing device is moved into an view's hit test boundaries.
PointerOver = "pointer-over" //
// General listener format:
//
// func(view rui.View, event rui.PointerEvent)
//
// where:
// - view - Interface of a view which generated this event,
// - event - Pointer event.
//
// Allowed listener formats:
//
// func(event rui.PointerEvent)
// func(view rui.View)
// func()
PointerOver PropertyName = "pointer-over"
) )
// PointerEvent represent a stylus events. Also inherit [MouseEvent] attributes
type PointerEvent struct { type PointerEvent struct {
MouseEvent MouseEvent
@ -87,54 +166,6 @@ type PointerEvent struct {
IsPrimary bool IsPrimary bool
} }
var pointerEvents = map[string]struct{ jsEvent, jsFunc string }{
PointerDown: {jsEvent: "onpointerdown", jsFunc: "pointerDownEvent"},
PointerUp: {jsEvent: "onpointerup", jsFunc: "pointerUpEvent"},
PointerMove: {jsEvent: "onpointermove", jsFunc: "pointerMoveEvent"},
PointerCancel: {jsEvent: "onpointercancel", jsFunc: "pointerCancelEvent"},
PointerOut: {jsEvent: "onpointerout", jsFunc: "pointerOutEvent"},
PointerOver: {jsEvent: "onpointerover", jsFunc: "pointerOverEvent"},
}
func (view *viewData) setPointerListener(tag string, value any) bool {
listeners, ok := valueToEventListeners[View, PointerEvent](value)
if !ok {
notCompatibleType(tag, value)
return false
}
if listeners == nil {
view.removePointerListener(tag)
} else if js, ok := pointerEvents[tag]; ok {
view.properties[tag] = listeners
if view.created {
updateProperty(view.htmlID(), js.jsEvent, js.jsFunc+"(this, event)", view.Session())
}
} else {
return false
}
return true
}
func (view *viewData) removePointerListener(tag string) {
delete(view.properties, tag)
if view.created {
if js, ok := pointerEvents[tag]; ok {
removeProperty(view.htmlID(), js.jsEvent, view.Session())
}
}
}
func pointerEventsHtml(view View, buffer *strings.Builder) {
for tag, js := range pointerEvents {
if value := view.getRaw(tag); value != nil {
if listeners, ok := value.([]func(View, PointerEvent)); ok && len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
}
}
}
}
func (event *PointerEvent) init(data DataObject) { func (event *PointerEvent) init(data DataObject) {
event.MouseEvent.init(data) event.MouseEvent.init(data)
@ -151,8 +182,8 @@ func (event *PointerEvent) init(data DataObject) {
event.IsPrimary = dataBoolProperty(data, "isPrimary") event.IsPrimary = dataBoolProperty(data, "isPrimary")
} }
func handlePointerEvents(view View, tag string, data DataObject) { func handlePointerEvents(view View, tag PropertyName, data DataObject) {
listeners := getEventListeners[View, PointerEvent](view, nil, tag) listeners := getOneArgEventListeners[View, PointerEvent](view, nil, tag)
if len(listeners) == 0 { if len(listeners) == 0 {
return return
} }
@ -161,42 +192,96 @@ func handlePointerEvents(view View, tag string, data DataObject) {
event.init(data) event.init(data)
for _, listener := range listeners { for _, listener := range listeners {
listener(view, event) listener.Run(view, event)
} }
} }
// GetPointerDownListeners returns the "pointer-down" listener list. If there are no listeners then the empty list is returned. // GetPointerDownListeners returns the "pointer-down" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetPointerDownListeners(view View, subviewID ...string) []func(View, PointerEvent) { // Result elements can be of the following types:
return getEventListeners[View, PointerEvent](view, subviewID, PointerDown) // - func(View, PointerEvent),
// - func(View),
// - func(PointerEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetPointerDownListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, PointerEvent](view, subviewID, PointerDown)
} }
// GetPointerUpListeners returns the "pointer-up" listener list. If there are no listeners then the empty list is returned. // GetPointerUpListeners returns the "pointer-up" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetPointerUpListeners(view View, subviewID ...string) []func(View, PointerEvent) { // Result elements can be of the following types:
return getEventListeners[View, PointerEvent](view, subviewID, PointerUp) // - func(View, PointerEvent),
// - func(View),
// - func(PointerEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetPointerUpListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, PointerEvent](view, subviewID, PointerUp)
} }
// GetPointerMoveListeners returns the "pointer-move" listener list. If there are no listeners then the empty list is returned. // GetPointerMoveListeners returns the "pointer-move" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetPointerMoveListeners(view View, subviewID ...string) []func(View, PointerEvent) { // Result elements can be of the following types:
return getEventListeners[View, PointerEvent](view, subviewID, PointerMove) // - func(View, PointerEvent),
// - func(View),
// - func(PointerEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetPointerMoveListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, PointerEvent](view, subviewID, PointerMove)
} }
// GetPointerCancelListeners returns the "pointer-cancel" listener list. If there are no listeners then the empty list is returned. // GetPointerCancelListeners returns the "pointer-cancel" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetPointerCancelListeners(view View, subviewID ...string) []func(View, PointerEvent) { // Result elements can be of the following types:
return getEventListeners[View, PointerEvent](view, subviewID, PointerCancel) // - func(View, PointerEvent),
// - func(View),
// - func(PointerEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetPointerCancelListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, PointerEvent](view, subviewID, PointerCancel)
} }
// GetPointerOverListeners returns the "pointer-over" listener list. If there are no listeners then the empty list is returned. // GetPointerOverListeners returns the "pointer-over" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetPointerOverListeners(view View, subviewID ...string) []func(View, PointerEvent) { // Result elements can be of the following types:
return getEventListeners[View, PointerEvent](view, subviewID, PointerOver) // - func(View, PointerEvent),
// - func(View),
// - func(PointerEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetPointerOverListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, PointerEvent](view, subviewID, PointerOver)
} }
// GetPointerOutListeners returns the "pointer-out" listener list. If there are no listeners then the empty list is returned. // GetPointerOutListeners returns the "pointer-out" listener list. If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetPointerOutListeners(view View, subviewID ...string) []func(View, PointerEvent) { // Result elements can be of the following types:
return getEventListeners[View, PointerEvent](view, subviewID, PointerOut) // - func(View, PointerEvent),
// - func(View),
// - func(PointerEvent),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetPointerOutListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, PointerEvent](view, subviewID, PointerOut)
} }

1808
popup.go

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,9 @@ func ShowMessage(title, text string, session Session) {
} }
// ShowQuestion displays a message with the given title and text and two buttons "Yes" and "No". // ShowQuestion displays a message with the given title and text and two buttons "Yes" and "No".
//
// When the "Yes" button is clicked, the message is closed and the onYes function is called (if it is not nil). // When the "Yes" button is clicked, the message is closed and the onYes function is called (if it is not nil).
//
// When the "No" button is pressed, the message is closed and the onNo function is called (if it is not nil). // When the "No" button is pressed, the message is closed and the onNo function is called (if it is not nil).
func ShowQuestion(title, text string, session Session, onYes func(), onNo func()) { func ShowQuestion(title, text string, session Session, onYes func(), onNo func()) {
textView := NewTextView(session, Params{ textView := NewTextView(session, Params{
@ -28,17 +30,9 @@ func ShowQuestion(title, text string, session Session, onYes func(), onNo func()
CloseButton: false, CloseButton: false,
OutsideClose: false, OutsideClose: false,
Buttons: []PopupButton{ Buttons: []PopupButton{
{
Title: "No",
OnClick: func(popup Popup) {
popup.Dismiss()
if onNo != nil {
onNo()
}
},
},
{ {
Title: "Yes", Title: "Yes",
Type: DefaultButton,
OnClick: func(popup Popup) { OnClick: func(popup Popup) {
popup.Dismiss() popup.Dismiss()
if onYes != nil { if onYes != nil {
@ -46,6 +40,16 @@ func ShowQuestion(title, text string, session Session, onYes func(), onNo func()
} }
}, },
}, },
{
Title: "No",
Type: CancelButton,
OnClick: func(popup Popup) {
popup.Dismiss()
if onNo != nil {
onNo()
}
},
},
}, },
} }
if title != "" { if title != "" {
@ -55,6 +59,7 @@ func ShowQuestion(title, text string, session Session, onYes func(), onNo func()
} }
// ShowCancellableQuestion displays a message with the given title and text and three buttons "Yes", "No" and "Cancel". // ShowCancellableQuestion displays a message with the given title and text and three buttons "Yes", "No" and "Cancel".
//
// When the "Yes", "No" or "Cancel" button is pressed, the message is closed and the onYes, onNo or onCancel function // When the "Yes", "No" or "Cancel" button is pressed, the message is closed and the onYes, onNo or onCancel function
// (if it is not nil) is called, respectively. // (if it is not nil) is called, respectively.
func ShowCancellableQuestion(title, text string, session Session, onYes func(), onNo func(), onCancel func()) { func ShowCancellableQuestion(title, text string, session Session, onYes func(), onNo func(), onCancel func()) {
@ -68,11 +73,12 @@ func ShowCancellableQuestion(title, text string, session Session, onYes func(),
OutsideClose: false, OutsideClose: false,
Buttons: []PopupButton{ Buttons: []PopupButton{
{ {
Title: "Cancel", Title: "Yes",
Type: DefaultButton,
OnClick: func(popup Popup) { OnClick: func(popup Popup) {
popup.Dismiss() popup.Dismiss()
if onCancel != nil { if onYes != nil {
onCancel() onYes()
} }
}, },
}, },
@ -86,11 +92,12 @@ func ShowCancellableQuestion(title, text string, session Session, onYes func(),
}, },
}, },
{ {
Title: "Yes", Title: "Cancel",
Type: CancelButton,
OnClick: func(popup Popup) { OnClick: func(popup Popup) {
popup.Dismiss() popup.Dismiss()
if onYes != nil { if onCancel != nil {
onYes() onCancel()
} }
}, },
}, },
@ -127,10 +134,14 @@ func (popup *popupMenuData) ListSize() int {
} }
func (popup *popupMenuData) ListItem(index int, session Session) View { func (popup *popupMenuData) ListItem(index int, session Session) View {
return NewTextView(popup.session, Params{ view := NewTextView(popup.session, Params{
Text: popup.items[index], Text: popup.items[index],
Style: "ruiPopupMenuItem", Style: "ruiPopupMenuItem",
}) })
if !popup.IsListItemEnabled(index) {
view.Set(TextColor, "@ruiDisabledTextColor")
}
return view
} }
func (popup *popupMenuData) IsListItemEnabled(index int) bool { func (popup *popupMenuData) IsListItemEnabled(index int) bool {
@ -144,9 +155,12 @@ func (popup *popupMenuData) IsListItemEnabled(index int) bool {
return true return true
} }
// PopupMenuResult is the constant for the "popup-menu-result" property tag. // PopupMenuResult is the constant for "popup-menu-result" property tag.
// The "popup-menu-result" property sets the function (format: func(int)) to be called when //
// a menu item of popup menu is selected. // Used by `Popup`.
// Set the function to be called when the menu item of popup menu is selected.
//
// Supported types: `func(index int)`.
const PopupMenuResult = "popup-menu-result" const PopupMenuResult = "popup-menu-result"
// ShowMenu displays the menu. Menu items are set using the Items property. // ShowMenu displays the menu. Menu items are set using the Items property.

View File

@ -5,12 +5,30 @@ import (
"strings" "strings"
) )
// Constants for [ProgressBar] specific properties and events
const ( const (
ProgressBarMax = "progress-max" // ProgressBarMax is the constant for "progress-max" property tag.
ProgressBarValue = "progress-value" //
// Used by ProgressBar.
// Maximum value, default is 1.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
ProgressBarMax PropertyName = "progress-max"
// ProgressBarValue is the constant for "progress-value" property tag.
//
// Used by ProgressBar.
// Current value, default is 0.
//
// Supported types: float, int, string.
//
// Internal type is float, other types converted to it during assignment.
ProgressBarValue PropertyName = "progress-value"
) )
// ProgressBar - ProgressBar view // ProgressBar represents a ProgressBar view
type ProgressBar interface { type ProgressBar interface {
View View
} }
@ -28,20 +46,18 @@ func NewProgressBar(session Session, params Params) ProgressBar {
} }
func newProgressBar(session Session) View { func newProgressBar(session Session) View {
return NewProgressBar(session, nil) return new(progressBarData)
} }
func (progress *progressBarData) init(session Session) { func (progress *progressBarData) init(session Session) {
progress.viewData.init(session) progress.viewData.init(session)
progress.tag = "ProgressBar" progress.tag = "ProgressBar"
progress.normalize = normalizeProgressBarTag
progress.changed = progress.propertyChanged
} }
func (progress *progressBarData) String() string { func normalizeProgressBarTag(tag PropertyName) PropertyName {
return getViewString(progress) tag = defaultNormalize(tag)
}
func (progress *progressBarData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
switch tag { switch tag {
case Max, "progress-bar-max", "progressbar-max": case Max, "progress-bar-max", "progressbar-max":
return ProgressBarMax return ProgressBarMax
@ -52,43 +68,22 @@ func (progress *progressBarData) normalizeTag(tag string) string {
return tag return tag
} }
func (progress *progressBarData) Remove(tag string) { func (progress *progressBarData) propertyChanged(tag PropertyName) {
progress.remove(progress.normalizeTag(tag))
}
func (progress *progressBarData) remove(tag string) { switch tag {
progress.viewData.remove(tag) case ProgressBarMax:
progress.propertyChanged(tag) progress.Session().updateProperty(progress.htmlID(), "max",
} strconv.FormatFloat(GetProgressBarMax(progress), 'f', -1, 32))
func (progress *progressBarData) propertyChanged(tag string) { case ProgressBarValue:
if progress.created { progress.Session().updateProperty(progress.htmlID(), "value",
switch tag { strconv.FormatFloat(GetProgressBarValue(progress), 'f', -1, 32))
case ProgressBarMax:
updateProperty(progress.htmlID(), Max, strconv.FormatFloat(GetProgressBarMax(progress), 'f', -1, 32), progress.session)
case ProgressBarValue: default:
updateProperty(progress.htmlID(), Value, strconv.FormatFloat(GetProgressBarValue(progress), 'f', -1, 32), progress.session) progress.viewData.propertyChanged(tag)
}
} }
} }
func (progress *progressBarData) Set(tag string, value any) bool {
return progress.set(progress.normalizeTag(tag), value)
}
func (progress *progressBarData) set(tag string, value any) bool {
if progress.viewData.set(tag, value) {
progress.propertyChanged(tag)
return true
}
return false
}
func (progress *progressBarData) Get(tag string) any {
return progress.get(progress.normalizeTag(tag))
}
func (progress *progressBarData) htmlTag() string { func (progress *progressBarData) htmlTag() string {
return "progress" return "progress"
} }
@ -106,13 +101,17 @@ func (progress *progressBarData) htmlProperties(self View, buffer *strings.Build
} }
// GetProgressBarMax returns the max value of ProgressBar subview. // GetProgressBarMax returns the max value of ProgressBar subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetProgressBarMax(view View, subviewID ...string) float64 { func GetProgressBarMax(view View, subviewID ...string) float64 {
return floatStyledProperty(view, subviewID, ProgressBarMax, 1) return floatStyledProperty(view, subviewID, ProgressBarMax, 1)
} }
// GetProgressBarValue returns the value of ProgressBar subview. // GetProgressBarValue returns the value of ProgressBar subview.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetProgressBarValue(view View, subviewID ...string) float64 { func GetProgressBarValue(view View, subviewID ...string) float64 {
return floatStyledProperty(view, subviewID, ProgressBarValue, 0) return floatStyledProperty(view, subviewID, ProgressBarValue, 0)
} }

View File

@ -1,7 +1,8 @@
package rui package rui
import ( import (
"sort" "iter"
"slices"
"strings" "strings"
) )
@ -9,79 +10,205 @@ import (
type Properties interface { type Properties interface {
// Get returns a value of the property with name defined by the argument. // Get returns a value of the property with name defined by the argument.
// The type of return value depends on the property. If the property is not set then nil is returned. // The type of return value depends on the property. If the property is not set then nil is returned.
Get(tag string) any Get(tag PropertyName) any
getRaw(tag string) any getRaw(tag PropertyName) any
// Set sets the value (second argument) of the property with name defined by the first argument. // Set sets the value (second argument) of the property with name defined by the first argument.
// Return "true" if the value has been set, in the opposite case "false" are returned and // Return "true" if the value has been set, in the opposite case "false" are returned and
// a description of the error is written to the log // a description of the error is written to the log
Set(tag string, value any) bool Set(tag PropertyName, value any) bool
setRaw(tag string, value any) setRaw(tag PropertyName, value any)
// Remove removes the property with name defined by the argument // Remove removes the property with name defined by the argument
Remove(tag string) Remove(tag PropertyName)
// Clear removes all properties // Clear removes all properties
Clear() Clear()
// All returns an iterator to access the properties
All() iter.Seq2[PropertyName, any]
// AllTags returns an array of the set properties // AllTags returns an array of the set properties
AllTags() []string AllTags() []PropertyName
IsEmpty() bool
} }
type propertyList struct { type propertyList struct {
properties map[string]any properties map[PropertyName]any
normalize func(PropertyName) PropertyName
}
type dataProperty struct {
propertyList
supportedProperties []PropertyName
get func(Properties, PropertyName) any
set func(Properties, PropertyName, any) []PropertyName
remove func(Properties, PropertyName) []PropertyName
}
func defaultNormalize(tag PropertyName) PropertyName {
return PropertyName(strings.ToLower(strings.Trim(string(tag), " \t")))
} }
func (properties *propertyList) init() { func (properties *propertyList) init() {
properties.properties = map[string]any{} properties.properties = map[PropertyName]any{}
properties.normalize = defaultNormalize
//properties.getFunc = properties.getRaw
//properties.set = propertiesSet
//properties.remove = propertiesRemove
} }
func (properties *propertyList) Get(tag string) any { func (properties *propertyList) IsEmpty() bool {
return properties.getRaw(strings.ToLower(tag)) return len(properties.properties) == 0
} }
func (properties *propertyList) getRaw(tag string) any { func (properties *propertyList) getRaw(tag PropertyName) any {
if value, ok := properties.properties[tag]; ok { if value, ok := properties.properties[tag]; ok {
return value return value
} }
return nil return nil
} }
func (properties *propertyList) setRaw(tag string, value any) { func (properties *propertyList) setRaw(tag PropertyName, value any) {
properties.properties[tag] = value if value == nil {
} delete(properties.properties, tag)
} else {
func (properties *propertyList) Remove(tag string) { properties.properties[tag] = value
delete(properties.properties, strings.ToLower(tag))
}
func (properties *propertyList) remove(tag string) {
delete(properties.properties, tag)
}
func (properties *propertyList) Clear() {
properties.properties = map[string]any{}
}
func (properties *propertyList) AllTags() []string {
tags := make([]string, 0, len(properties.properties))
for t := range properties.properties {
tags = append(tags, t)
} }
sort.Strings(tags)
return tags
} }
func parseProperties(properties Properties, object DataObject) { /*
count := object.PropertyCount() func (properties *propertyList) Remove(tag PropertyName) {
for i := 0; i < count; i++ { properties.remove(properties, properties.normalize(tag))
if node := object.Property(i); node != nil { }
switch node.Type() { */
case TextNode: func (properties *propertyList) Clear() {
properties.Set(node.Tag(), node.Text()) properties.properties = map[PropertyName]any{}
}
case ObjectNode: func (properties *propertyList) All() iter.Seq2[PropertyName, any] {
properties.Set(node.Tag(), node.Object()) return func(yield func(PropertyName, any) bool) {
for tag, value := range properties.properties {
case ArrayNode: if !yield(tag, value) {
properties.Set(node.Tag(), node.ArrayElements()) return
} }
} }
} }
} }
func (properties *propertyList) AllTags() []PropertyName {
tags := make([]PropertyName, 0, len(properties.properties))
for tag := range properties.properties {
tags = append(tags, tag)
}
slices.Sort(tags)
return tags
}
/*
func (properties *propertyList) writeToBuffer(buffer *strings.Builder,
indent string, objectTag string, tags []PropertyName) {
buffer.WriteString(objectTag)
buffer.WriteString(" {\n")
indent2 := indent + "\t"
for _, tag := range tags {
if value, ok := properties.properties[tag]; ok {
text := propertyValueToString(tag, value, indent2)
if text != "" {
buffer.WriteString(indent2)
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
buffer.WriteString(text)
buffer.WriteString(",\n")
}
}
}
buffer.WriteString(indent)
buffer.WriteString("}")
}
*/
func parseProperties(properties Properties, object DataObject) {
for node := range object.Properties() {
switch node.Type() {
case TextNode:
properties.Set(PropertyName(node.Tag()), node.Text())
case ObjectNode:
properties.Set(PropertyName(node.Tag()), node.Object())
case ArrayNode:
switch node.ArraySize() {
case 0:
// do nothing
case 1:
if v := node.ArrayElement(0); v.IsObject() {
properties.Set(PropertyName(node.Tag()), v.Object())
} else {
properties.Set(PropertyName(node.Tag()), v.Value())
}
default:
properties.Set(PropertyName(node.Tag()), node.Array())
}
}
}
}
func propertiesGet(properties Properties, tag PropertyName) any {
return properties.getRaw(tag)
}
func propertiesRemove(properties Properties, tag PropertyName) []PropertyName {
if properties.getRaw(tag) == nil {
return []PropertyName{}
}
properties.setRaw(tag, nil)
return []PropertyName{tag}
}
func (data *dataProperty) init() {
data.propertyList.init()
data.get = propertiesGet
data.set = propertiesSet
data.remove = propertiesRemove
}
func (data *dataProperty) Get(tag PropertyName) any {
return data.get(data, data.normalize(tag))
}
func (data *dataProperty) Remove(tag PropertyName) {
data.remove(data, data.normalize(tag))
}
func (data *dataProperty) writeToBuffer(buffer *strings.Builder, indent string, objectName string, tags []PropertyName) {
buffer.WriteString(objectName)
buffer.WriteString("{ ")
comma := false
for _, tag := range tags {
if value, ok := data.properties[tag]; ok {
text := propertyValueToString(tag, value, indent)
if text != "" {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
buffer.WriteString(text)
comma = true
}
}
}
buffer.WriteString(" }")
}
func (data *dataProperty) writeString(buffer *strings.Builder, indent string) {
data.writeToBuffer(buffer, indent, "_", data.AllTags())
}

View File

@ -6,7 +6,7 @@ import (
"strings" "strings"
) )
func stringProperty(properties Properties, tag string, session Session) (string, bool) { func stringProperty(properties Properties, tag PropertyName, session Session) (string, bool) {
if value := properties.getRaw(tag); value != nil { if value := properties.getRaw(tag); value != nil {
if text, ok := value.(string); ok { if text, ok := value.(string); ok {
return session.resolveConstants(text) return session.resolveConstants(text)
@ -15,11 +15,11 @@ func stringProperty(properties Properties, tag string, session Session) (string,
return "", false return "", false
} }
func imageProperty(properties Properties, tag string, session Session) (string, bool) { func imageProperty(properties Properties, tag PropertyName, session Session) (string, bool) {
if value := properties.getRaw(tag); value != nil { if value := properties.getRaw(tag); value != nil {
if text, ok := value.(string); ok { if text, ok := value.(string); ok {
if text != "" && text[0] == '@' { if ok, constName := isConstantName(text); ok {
if image, ok := session.ImageConstant(text[1:]); ok { if image, ok := session.ImageConstant(constName); ok {
return image, true return image, true
} else { } else {
return "", false return "", false
@ -61,11 +61,11 @@ func valueToSizeUnit(value any, session Session) (SizeUnit, bool) {
return AutoSize(), false return AutoSize(), false
} }
func sizeProperty(properties Properties, tag string, session Session) (SizeUnit, bool) { func sizeProperty(properties Properties, tag PropertyName, session Session) (SizeUnit, bool) {
return valueToSizeUnit(properties.getRaw(tag), session) return valueToSizeUnit(properties.getRaw(tag), session)
} }
func angleProperty(properties Properties, tag string, session Session) (AngleUnit, bool) { func angleProperty(properties Properties, tag PropertyName, session Session) (AngleUnit, bool) {
if value := properties.getRaw(tag); value != nil { if value := properties.getRaw(tag); value != nil {
switch value := value.(type) { switch value := value.(type) {
case AngleUnit: case AngleUnit:
@ -88,8 +88,8 @@ func valueToColor(value any, session Session) (Color, bool) {
return value, true return value, true
case string: case string:
if len(value) > 1 && value[0] == '@' { if ok, constName := isConstantName(value); ok {
return session.Color(value[1:]) return session.Color(constName)
} }
return StringToColor(value) return StringToColor(value)
} }
@ -98,11 +98,11 @@ func valueToColor(value any, session Session) (Color, bool) {
return Color(0), false return Color(0), false
} }
func colorProperty(properties Properties, tag string, session Session) (Color, bool) { func colorProperty(properties Properties, tag PropertyName, session Session) (Color, bool) {
return valueToColor(properties.getRaw(tag), session) return valueToColor(properties.getRaw(tag), session)
} }
func valueToEnum(value any, tag string, session Session, defaultValue int) (int, bool) { func valueToEnum(value any, tag PropertyName, session Session, defaultValue int) (int, bool) {
if value != nil { if value != nil {
values := enumProperties[tag].values values := enumProperties[tag].values
switch value := value.(type) { switch value := value.(type) {
@ -165,7 +165,7 @@ func enumStringToInt(value string, enumValues []string, logError bool) (int, boo
return 0, false return 0, false
} }
func enumProperty(properties Properties, tag string, session Session, defaultValue int) (int, bool) { func enumProperty(properties Properties, tag PropertyName, session Session, defaultValue int) (int, bool) {
return valueToEnum(properties.getRaw(tag), tag, session, defaultValue) return valueToEnum(properties.getRaw(tag), tag, session, defaultValue)
} }
@ -194,7 +194,7 @@ func valueToBool(value any, session Session) (bool, bool) {
return false, false return false, false
} }
func boolProperty(properties Properties, tag string, session Session) (bool, bool) { func boolProperty(properties Properties, tag PropertyName, session Session) (bool, bool) {
return valueToBool(properties.getRaw(tag), session) return valueToBool(properties.getRaw(tag), session)
} }
@ -224,7 +224,7 @@ func valueToInt(value any, session Session, defaultValue int) (int, bool) {
return defaultValue, false return defaultValue, false
} }
func intProperty(properties Properties, tag string, session Session, defaultValue int) (int, bool) { func intProperty(properties Properties, tag PropertyName, session Session, defaultValue int) (int, bool) {
return valueToInt(properties.getRaw(tag), session, defaultValue) return valueToInt(properties.getRaw(tag), session, defaultValue)
} }
@ -248,7 +248,7 @@ func valueToFloat(value any, session Session, defaultValue float64) (float64, bo
return defaultValue, false return defaultValue, false
} }
func floatProperty(properties Properties, tag string, session Session, defaultValue float64) (float64, bool) { func floatProperty(properties Properties, tag PropertyName, session Session, defaultValue float64) (float64, bool) {
return valueToFloat(properties.getRaw(tag), session, defaultValue) return valueToFloat(properties.getRaw(tag), session, defaultValue)
} }
@ -272,7 +272,7 @@ func valueToFloatText(value any, session Session, defaultValue float64) (string,
return fmt.Sprintf("%g", defaultValue), false return fmt.Sprintf("%g", defaultValue), false
} }
func floatTextProperty(properties Properties, tag string, session Session, defaultValue float64) (string, bool) { func floatTextProperty(properties Properties, tag PropertyName, session Session, defaultValue float64) (string, bool) {
return valueToFloatText(properties.getRaw(tag), session, defaultValue) return valueToFloatText(properties.getRaw(tag), session, defaultValue)
} }
@ -297,6 +297,6 @@ func valueToRange(value any, session Session) (Range, bool) {
return Range{}, false return Range{}, false
} }
func rangeProperty(properties Properties, tag string, session Session) (Range, bool) { func rangeProperty(properties Properties, tag PropertyName, session Session) (Range, bool) {
return valueToRange(properties.getRaw(tag), session) return valueToRange(properties.getRaw(tag), session)
} }

File diff suppressed because it is too large Load Diff

View File

@ -2,11 +2,12 @@ package rui
import ( import (
"math" "math"
"slices"
"strconv" "strconv"
"strings" "strings"
) )
var colorProperties = []string{ var colorProperties = []PropertyName{
ColorTag, ColorTag,
BackgroundColor, BackgroundColor,
TextColor, TextColor,
@ -19,25 +20,24 @@ var colorProperties = []string{
OutlineColor, OutlineColor,
TextLineColor, TextLineColor,
ColorPickerValue, ColorPickerValue,
AccentColor,
} }
func isPropertyInList(tag string, list []string) bool { /*
for _, prop := range list { func isPropertyInList(tag PropertyName, list []PropertyName) bool {
if prop == tag { for _, prop := range list {
return true if prop == tag {
return true
}
} }
return false
} }
return false */
} var angleProperties = []PropertyName{
var angleProperties = []string{
Rotate,
SkewX,
SkewY,
From, From,
} }
var boolProperties = []string{ var boolProperties = []PropertyName{
Disabled, Disabled,
Focusable, Focusable,
Inset, Inset,
@ -63,9 +63,12 @@ var boolProperties = []string{
TabCloseButton, TabCloseButton,
Repeating, Repeating,
UserSelect, UserSelect,
ColumnSpanAll,
MoveToFrontAnimation,
HideSummaryMarker,
} }
var intProperties = []string{ var intProperties = []PropertyName{
ZIndex, ZIndex,
TabSize, TabSize,
HeadHeight, HeadHeight,
@ -73,16 +76,15 @@ var intProperties = []string{
RowSpan, RowSpan,
ColumnSpan, ColumnSpan,
ColumnCount, ColumnCount,
Order,
TabIndex,
MaxLength,
NumberPickerPrecision,
} }
var floatProperties = map[string]struct{ min, max float64 }{ var floatProperties = map[PropertyName]struct{ min, max float64 }{
Opacity: {min: 0, max: 1}, Opacity: {min: 0, max: 1},
ScaleX: {min: -math.MaxFloat64, max: math.MaxFloat64}, ShowOpacity: {min: 0, max: 1},
ScaleY: {min: -math.MaxFloat64, max: math.MaxFloat64},
ScaleZ: {min: -math.MaxFloat64, max: math.MaxFloat64},
RotateX: {min: 0, max: 1},
RotateY: {min: 0, max: 1},
RotateZ: {min: 0, max: 1},
NumberPickerMax: {min: -math.MaxFloat64, max: math.MaxFloat64}, NumberPickerMax: {min: -math.MaxFloat64, max: math.MaxFloat64},
NumberPickerMin: {min: -math.MaxFloat64, max: math.MaxFloat64}, NumberPickerMin: {min: -math.MaxFloat64, max: math.MaxFloat64},
NumberPickerStep: {min: -math.MaxFloat64, max: math.MaxFloat64}, NumberPickerStep: {min: -math.MaxFloat64, max: math.MaxFloat64},
@ -91,87 +93,94 @@ var floatProperties = map[string]struct{ min, max float64 }{
ProgressBarValue: {min: 0, max: math.MaxFloat64}, ProgressBarValue: {min: 0, max: math.MaxFloat64},
VideoWidth: {min: 0, max: 10000}, VideoWidth: {min: 0, max: 10000},
VideoHeight: {min: 0, max: 10000}, VideoHeight: {min: 0, max: 10000},
PushDuration: {min: 0, max: math.MaxFloat64},
ShowDuration: {min: 0, max: math.MaxFloat64},
DragImageXOffset: {min: -math.MaxFloat64, max: math.MaxFloat64},
DragImageYOffset: {min: -math.MaxFloat64, max: math.MaxFloat64},
} }
var sizeProperties = map[string]string{ var sizeProperties = map[PropertyName]string{
Width: Width, Width: string(Width),
Height: Height, Height: string(Height),
MinWidth: MinWidth, MinWidth: string(MinWidth),
MinHeight: MinHeight, MinHeight: string(MinHeight),
MaxWidth: MaxWidth, MaxWidth: string(MaxWidth),
MaxHeight: MaxHeight, MaxHeight: string(MaxHeight),
Left: Left, Left: string(Left),
Right: Right, Right: string(Right),
Top: Top, Top: string(Top),
Bottom: Bottom, Bottom: string(Bottom),
TextSize: "font-size", TextSize: "font-size",
TextIndent: TextIndent, TextIndent: string(TextIndent),
LetterSpacing: LetterSpacing, LetterSpacing: string(LetterSpacing),
WordSpacing: WordSpacing, WordSpacing: string(WordSpacing),
LineHeight: LineHeight, LineHeight: string(LineHeight),
TextLineThickness: "text-decoration-thickness", TextLineThickness: "text-decoration-thickness",
ListRowGap: "row-gap", ListRowGap: "row-gap",
ListColumnGap: "column-gap", ListColumnGap: "column-gap",
GridRowGap: GridRowGap, GridRowGap: string(GridRowGap),
GridColumnGap: GridColumnGap, GridColumnGap: string(GridColumnGap),
ColumnWidth: ColumnWidth, ColumnWidth: string(ColumnWidth),
ColumnGap: ColumnGap, ColumnGap: string(ColumnGap),
Gap: Gap, Gap: string(Gap),
Margin: Margin, Margin: string(Margin),
MarginLeft: MarginLeft, MarginLeft: string(MarginLeft),
MarginRight: MarginRight, MarginRight: string(MarginRight),
MarginTop: MarginTop, MarginTop: string(MarginTop),
MarginBottom: MarginBottom, MarginBottom: string(MarginBottom),
Padding: Padding, Padding: string(Padding),
PaddingLeft: PaddingLeft, PaddingLeft: string(PaddingLeft),
PaddingRight: PaddingRight, PaddingRight: string(PaddingRight),
PaddingTop: PaddingTop, PaddingTop: string(PaddingTop),
PaddingBottom: PaddingBottom, PaddingBottom: string(PaddingBottom),
BorderWidth: BorderWidth, BorderWidth: string(BorderWidth),
BorderLeftWidth: BorderLeftWidth, BorderLeftWidth: string(BorderLeftWidth),
BorderRightWidth: BorderRightWidth, BorderRightWidth: string(BorderRightWidth),
BorderTopWidth: BorderTopWidth, BorderTopWidth: string(BorderTopWidth),
BorderBottomWidth: BorderBottomWidth, BorderBottomWidth: string(BorderBottomWidth),
OutlineWidth: OutlineWidth, OutlineWidth: string(OutlineWidth),
XOffset: XOffset, OutlineOffset: string(OutlineOffset),
YOffset: YOffset, XOffset: string(XOffset),
BlurRadius: BlurRadius, YOffset: string(YOffset),
SpreadRadius: SpreadRadius, BlurRadius: string(BlurRadius),
Perspective: Perspective, SpreadRadius: string(SpreadRadius),
PerspectiveOriginX: PerspectiveOriginX, Perspective: string(Perspective),
PerspectiveOriginY: PerspectiveOriginY, PerspectiveOriginX: string(PerspectiveOriginX),
OriginX: OriginX, PerspectiveOriginY: string(PerspectiveOriginY),
OriginY: OriginY, TransformOriginX: string(TransformOriginX),
OriginZ: OriginZ, TransformOriginY: string(TransformOriginY),
TranslateX: TranslateX, TransformOriginZ: string(TransformOriginZ),
TranslateY: TranslateY, Radius: string(Radius),
TranslateZ: TranslateZ, RadiusX: string(RadiusX),
Radius: Radius, RadiusY: string(RadiusY),
RadiusX: RadiusX, RadiusTopLeft: string(RadiusTopLeft),
RadiusY: RadiusY, RadiusTopLeftX: string(RadiusTopLeftX),
RadiusTopLeft: RadiusTopLeft, RadiusTopLeftY: string(RadiusTopLeftY),
RadiusTopLeftX: RadiusTopLeftX, RadiusTopRight: string(RadiusTopRight),
RadiusTopLeftY: RadiusTopLeftY, RadiusTopRightX: string(RadiusTopRightX),
RadiusTopRight: RadiusTopRight, RadiusTopRightY: string(RadiusTopRightY),
RadiusTopRightX: RadiusTopRightX, RadiusBottomLeft: string(RadiusBottomLeft),
RadiusTopRightY: RadiusTopRightY, RadiusBottomLeftX: string(RadiusBottomLeftX),
RadiusBottomLeft: RadiusBottomLeft, RadiusBottomLeftY: string(RadiusBottomLeftY),
RadiusBottomLeftX: RadiusBottomLeftX, RadiusBottomRight: string(RadiusBottomRight),
RadiusBottomLeftY: RadiusBottomLeftY, RadiusBottomRightX: string(RadiusBottomRightX),
RadiusBottomRight: RadiusBottomRight, RadiusBottomRightY: string(RadiusBottomRightY),
RadiusBottomRightX: RadiusBottomRightX, ItemWidth: string(ItemWidth),
RadiusBottomRightY: RadiusBottomRightY, ItemHeight: string(ItemHeight),
ItemWidth: ItemWidth, CenterX: string(CenterX),
ItemHeight: ItemHeight, CenterY: string(CenterX),
CenterX: CenterX, ArrowSize: "",
CenterY: CenterX, ArrowWidth: "",
ArrowOffset: "",
} }
var enumProperties = map[string]struct { type enumPropertyData struct {
values []string values []string
cssTag string cssTag string
cssValues []string cssValues []string
}{ }
var enumProperties = map[PropertyName]enumPropertyData{
Semantics: { Semantics: {
[]string{"default", "article", "section", "aside", "header", "main", "footer", "navigation", "figure", "figure-caption", "button", "p", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "code"}, []string{"default", "article", "section", "aside", "header", "main", "footer", "navigation", "figure", "figure-caption", "button", "p", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "code"},
"", "",
@ -184,17 +193,17 @@ var enumProperties = map[string]struct {
}, },
Overflow: { Overflow: {
[]string{"hidden", "visible", "scroll", "auto"}, []string{"hidden", "visible", "scroll", "auto"},
Overflow, string(Overflow),
[]string{"hidden", "visible", "scroll", "auto"}, []string{"hidden", "visible", "scroll", "auto"},
}, },
TextAlign: { TextAlign: {
[]string{"left", "right", "center", "justify"}, []string{"left", "right", "center", "justify"},
TextAlign, string(TextAlign),
[]string{"left", "right", "center", "justify"}, []string{"left", "right", "center", "justify"},
}, },
TextTransform: { TextTransform: {
[]string{"none", "capitalize", "lowercase", "uppercase"}, []string{"none", "capitalize", "lowercase", "uppercase"},
TextTransform, string(TextTransform),
[]string{"none", "capitalize", "lowercase", "uppercase"}, []string{"none", "capitalize", "lowercase", "uppercase"},
}, },
TextWeight: { TextWeight: {
@ -204,22 +213,27 @@ var enumProperties = map[string]struct {
}, },
WhiteSpace: { WhiteSpace: {
[]string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"}, []string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"},
WhiteSpace, string(WhiteSpace),
[]string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"}, []string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"},
}, },
WordBreak: { WordBreak: {
[]string{"normal", "break-all", "keep-all", "break-word"}, []string{"normal", "break-all", "keep-all", "break-word"},
WordBreak, string(WordBreak),
[]string{"normal", "break-all", "keep-all", "break-word"}, []string{"normal", "break-all", "keep-all", "break-word"},
}, },
TextOverflow: { TextOverflow: {
[]string{"clip", "ellipsis"}, []string{"clip", "ellipsis"},
TextOverflow, string(TextOverflow),
[]string{"clip", "ellipsis"}, []string{"clip", "ellipsis"},
}, },
TextWrap: {
[]string{"wrap", "nowrap", "balance"},
string(TextWrap),
[]string{"wrap", "nowrap", "balance"},
},
WritingMode: { WritingMode: {
[]string{"horizontal-top-to-bottom", "horizontal-bottom-to-top", "vertical-right-to-left", "vertical-left-to-right"}, []string{"horizontal-top-to-bottom", "horizontal-bottom-to-top", "vertical-right-to-left", "vertical-left-to-right"},
WritingMode, string(WritingMode),
[]string{"horizontal-tb", "horizontal-bt", "vertical-rl", "vertical-lr"}, []string{"horizontal-tb", "horizontal-bt", "vertical-rl", "vertical-lr"},
}, },
TextDirection: { TextDirection: {
@ -239,7 +253,7 @@ var enumProperties = map[string]struct {
}, },
BorderStyle: { BorderStyle: {
[]string{"none", "solid", "dashed", "dotted", "double"}, []string{"none", "solid", "dashed", "dotted", "double"},
BorderStyle, string(BorderStyle),
[]string{"none", "solid", "dashed", "dotted", "double"}, []string{"none", "solid", "dashed", "dotted", "double"},
}, },
TopStyle: { TopStyle: {
@ -264,7 +278,7 @@ var enumProperties = map[string]struct {
}, },
OutlineStyle: { OutlineStyle: {
[]string{"none", "solid", "dashed", "dotted", "double"}, []string{"none", "solid", "dashed", "dotted", "double"},
OutlineStyle, string(OutlineStyle),
[]string{"none", "solid", "dashed", "dotted", "double"}, []string{"none", "solid", "dashed", "dotted", "double"},
}, },
Tabs: { Tabs: {
@ -327,9 +341,19 @@ var enumProperties = map[string]struct {
"justify-items", "justify-items",
[]string{"start", "end", "center", "stretch"}, []string{"start", "end", "center", "stretch"},
}, },
CellVerticalSelfAlign: {
[]string{"top", "bottom", "center", "stretch"},
"align-self",
[]string{"start", "end", "center", "stretch"},
},
CellHorizontalSelfAlign: {
[]string{"left", "right", "center", "stretch"},
"justify-self",
[]string{"start", "end", "center", "stretch"},
},
GridAutoFlow: { GridAutoFlow: {
[]string{"row", "column", "row-dense", "column-dense"}, []string{"row", "column", "row-dense", "column-dense"},
GridAutoFlow, string(GridAutoFlow),
[]string{"row", "column", "row dense", "column dense"}, []string{"row", "column", "row dense", "column dense"},
}, },
ImageVerticalAlign: { ImageVerticalAlign: {
@ -369,7 +393,7 @@ var enumProperties = map[string]struct {
}, },
Cursor: { Cursor: {
[]string{"auto", "default", "none", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", "text", "vertical-text", "alias", "copy", "move", "no-drop", "not-allowed", "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out", "grab", "grabbing"}, []string{"auto", "default", "none", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", "text", "vertical-text", "alias", "copy", "move", "no-drop", "not-allowed", "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out", "grab", "grabbing"},
Cursor, string(Cursor),
[]string{"auto", "default", "none", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", "text", "vertical-text", "alias", "copy", "move", "no-drop", "not-allowed", "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out", "grab", "grabbing"}, []string{"auto", "default", "none", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", "text", "vertical-text", "alias", "copy", "move", "no-drop", "not-allowed", "e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out", "grab", "grabbing"},
}, },
Fit: { Fit: {
@ -397,6 +421,21 @@ var enumProperties = map[string]struct {
"background-clip", "background-clip",
[]string{"border-box", "padding-box", "content-box"}, // "text"}, []string{"border-box", "padding-box", "content-box"}, // "text"},
}, },
BackgroundOrigin: {
[]string{"border-box", "padding-box", "content-box"},
"background-origin",
[]string{"border-box", "padding-box", "content-box"},
},
MaskClip: {
[]string{"border-box", "padding-box", "content-box"},
"mask-clip",
[]string{"border-box", "padding-box", "content-box"},
},
MaskOrigin: {
[]string{"border-box", "padding-box", "content-box"},
"background-origin",
[]string{"border-box", "padding-box", "content-box"},
},
Direction: { Direction: {
[]string{"to-top", "to-right-top", "to-right", "to-right-bottom", "to-bottom", "to-left-bottom", "to-left", "to-left-top"}, []string{"to-top", "to-right-top", "to-right", "to-right-bottom", "to-bottom", "to-left-bottom", "to-left", "to-left-top"},
"", "",
@ -447,32 +486,39 @@ var enumProperties = map[string]struct {
"", "",
[]string{"none", "top", "right", "bottom", "left"}, []string{"none", "top", "right", "bottom", "left"},
}, },
MixBlendMode: {
[]string{"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"},
string(MixBlendMode),
[]string{"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"},
},
BackgroundBlendMode: {
[]string{"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"},
string(BackgroundBlendMode),
[]string{"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"},
},
ColumnFill: {
[]string{"balance", "auto"},
string(ColumnFill),
[]string{"balance", "auto"},
},
} }
func notCompatibleType(tag string, value any) { func notCompatibleType(tag PropertyName, value any) {
ErrorLogF(`"%T" type not compatible with "%s" property`, value, tag) ErrorLogF(`"%T" type not compatible with "%s" property`, value, string(tag))
} }
func invalidPropertyValue(tag string, value any) { func invalidPropertyValue(tag PropertyName, value any) {
ErrorLogF(`Invalid value "%v" of "%s" property`, value, tag) ErrorLogF(`Invalid value "%v" of "%s" property`, value, string(tag))
} }
func isConstantName(text string) bool { func isConstantName(text string) (bool, string) {
len := len(text) len := len(text)
if len <= 1 || text[0] != '@' { if len <= 1 || text[0] != '@' ||
return false strings.ContainsAny(text, ",;|\"'`+(){}[]<>/\\*&%! \t\n\r") {
return false, ""
} }
if len > 2 { return true, text[1:]
last := len - 1
if (text[1] == '`' && text[last] == '`') ||
(text[1] == '"' && text[last] == '"') ||
(text[1] == '\'' && text[last] == '\'') {
return true
}
}
return !strings.ContainsAny(text, ",;|\"'`+(){}[]<>/\\*&%! \t\n\r")
} }
func isInt(value any) (int, bool) { func isInt(value any) (int, bool) {
@ -515,26 +561,48 @@ func isInt(value any) (int, bool) {
return n, true return n, true
} }
func (properties *propertyList) setSimpleProperty(tag string, value any) bool { func setSimpleProperty(properties Properties, tag PropertyName, value any) bool {
if value == nil { if value == nil {
delete(properties.properties, tag) properties.setRaw(tag, nil)
return true return true
} else if text, ok := value.(string); ok { } else if text, ok := value.(string); ok {
text = strings.Trim(text, " \t\n\r") text = strings.Trim(text, " \t\n\r")
if text == "" { if text == "" {
delete(properties.properties, tag) properties.setRaw(tag, nil)
return true return true
} }
if isConstantName(text) { if ok, _ := isConstantName(text); ok {
properties.properties[tag] = text properties.setRaw(tag, text)
return true return true
} }
} }
return false return false
} }
func (properties *propertyList) setSizeProperty(tag string, value any) bool { func setStringPropertyValue(properties Properties, tag PropertyName, text any) []PropertyName {
if !properties.setSimpleProperty(tag, value) { if text != "" {
properties.setRaw(tag, text)
} else if properties.getRaw(tag) != nil {
properties.setRaw(tag, nil)
} else {
return []PropertyName{}
}
return []PropertyName{tag}
}
func setArrayPropertyValue[T any](properties Properties, tag PropertyName, value []T) []PropertyName {
if len(value) > 0 {
properties.setRaw(tag, value)
} else if properties.getRaw(tag) != nil {
properties.setRaw(tag, nil)
} else {
return []PropertyName{}
}
return []PropertyName{tag}
}
func setSizeProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
var size SizeUnit var size SizeUnit
switch value := value.(type) { switch value := value.(type) {
case string: case string:
@ -544,7 +612,7 @@ func (properties *propertyList) setSizeProperty(tag string, value any) bool {
size.Function = fn size.Function = fn
} else if size, ok = StringToSizeUnit(value); !ok { } else if size, ok = StringToSizeUnit(value); !ok {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return false return nil
} }
case SizeUnit: case SizeUnit:
size = value size = value
@ -567,29 +635,29 @@ func (properties *propertyList) setSizeProperty(tag string, value any) bool {
size.Value = float64(n) size.Value = float64(n)
} else { } else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return nil
} }
} }
if size.Type == Auto { if size.Type == Auto {
delete(properties.properties, tag) properties.setRaw(tag, nil)
} else { } else {
properties.properties[tag] = size properties.setRaw(tag, size)
} }
} }
return true return []PropertyName{tag}
} }
func (properties *propertyList) setAngleProperty(tag string, value any) bool { func setAngleProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !properties.setSimpleProperty(tag, value) { if !setSimpleProperty(properties, tag, value) {
var angle AngleUnit var angle AngleUnit
switch value := value.(type) { switch value := value.(type) {
case string: case string:
var ok bool var ok bool
if angle, ok = StringToAngleUnit(value); !ok { if angle, ok = StringToAngleUnit(value); !ok {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return false return nil
} }
case AngleUnit: case AngleUnit:
angle = value angle = value
@ -605,24 +673,24 @@ func (properties *propertyList) setAngleProperty(tag string, value any) bool {
angle = Rad(float64(n)) angle = Rad(float64(n))
} else { } else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return nil
} }
} }
properties.properties[tag] = angle properties.setRaw(tag, angle)
} }
return true return []PropertyName{tag}
} }
func (properties *propertyList) setColorProperty(tag string, value any) bool { func setColorProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !properties.setSimpleProperty(tag, value) { if !setSimpleProperty(properties, tag, value) {
var result Color var result Color
switch value := value.(type) { switch value := value.(type) {
case string: case string:
var err error var err error
if result, err = stringToColor(value); err != nil { if result, err = stringToColor(value); err != nil {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return false return nil
} }
case Color: case Color:
result = value result = value
@ -632,105 +700,101 @@ func (properties *propertyList) setColorProperty(tag string, value any) bool {
result = Color(color) result = Color(color)
} else { } else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return nil
} }
} }
if result == 0 { properties.setRaw(tag, result)
delete(properties.properties, tag)
} else {
properties.properties[tag] = result
}
} }
return true return []PropertyName{tag}
} }
func (properties *propertyList) setEnumProperty(tag string, value any, values []string) bool { func setEnumProperty(properties Properties, tag PropertyName, value any, values []string) []PropertyName {
if !properties.setSimpleProperty(tag, value) { if !setSimpleProperty(properties, tag, value) {
var n int var n int
if text, ok := value.(string); ok { if text, ok := value.(string); ok {
if n, ok = enumStringToInt(text, values, false); !ok { if n, ok = enumStringToInt(text, values, false); !ok {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return false return nil
} }
} else if i, ok := isInt(value); ok { } else if i, ok := isInt(value); ok {
if i < 0 || i >= len(values) { if i < 0 || i >= len(values) {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return false return nil
} }
n = i n = i
} else { } else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return nil
} }
properties.properties[tag] = n properties.setRaw(tag, n)
} }
return true return []PropertyName{tag}
} }
func (properties *propertyList) setBoolProperty(tag string, value any) bool { func setBoolProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !properties.setSimpleProperty(tag, value) { if !setSimpleProperty(properties, tag, value) {
if text, ok := value.(string); ok { if text, ok := value.(string); ok {
switch strings.ToLower(strings.Trim(text, " \t")) { switch strings.ToLower(strings.Trim(text, " \t")) {
case "true", "yes", "on", "1": case "true", "yes", "on", "1":
properties.properties[tag] = true properties.setRaw(tag, true)
case "false", "no", "off", "0": case "false", "no", "off", "0":
properties.properties[tag] = false properties.setRaw(tag, false)
default: default:
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return false return nil
} }
} else if n, ok := isInt(value); ok { } else if n, ok := isInt(value); ok {
switch n { switch n {
case 1: case 1:
properties.properties[tag] = true properties.setRaw(tag, true)
case 0: case 0:
properties.properties[tag] = false properties.setRaw(tag, false)
default: default:
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
return false return nil
} }
} else if b, ok := value.(bool); ok { } else if b, ok := value.(bool); ok {
properties.properties[tag] = b properties.setRaw(tag, b)
} else { } else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return nil
} }
} }
return true return []PropertyName{tag}
} }
func (properties *propertyList) setIntProperty(tag string, value any) bool { func setIntProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !properties.setSimpleProperty(tag, value) { if !setSimpleProperty(properties, tag, value) {
if text, ok := value.(string); ok { if text, ok := value.(string); ok {
n, err := strconv.Atoi(strings.Trim(text, " \t")) n, err := strconv.Atoi(strings.Trim(text, " \t"))
if err != nil { if err != nil {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
ErrorLog(err.Error()) ErrorLog(err.Error())
return false return nil
} }
properties.properties[tag] = n properties.setRaw(tag, n)
} else if n, ok := isInt(value); ok { } else if n, ok := isInt(value); ok {
properties.properties[tag] = n properties.setRaw(tag, n)
} else { } else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return nil
} }
} }
return true return []PropertyName{tag}
} }
func (properties *propertyList) setFloatProperty(tag string, value any, min, max float64) bool { func setFloatProperty(properties Properties, tag PropertyName, value any, min, max float64) []PropertyName {
if !properties.setSimpleProperty(tag, value) { if !setSimpleProperty(properties, tag, value) {
f := float64(0) f := float64(0)
switch value := value.(type) { switch value := value.(type) {
case string: case string:
@ -738,14 +802,14 @@ func (properties *propertyList) setFloatProperty(tag string, value any, min, max
if f, err = strconv.ParseFloat(strings.Trim(value, " \t"), 64); err != nil { if f, err = strconv.ParseFloat(strings.Trim(value, " \t"), 64); err != nil {
invalidPropertyValue(tag, value) invalidPropertyValue(tag, value)
ErrorLog(err.Error()) ErrorLog(err.Error())
return false return nil
} }
if f < min || f > max { if f < min || f > max {
ErrorLogF(`"%T" out of range of "%s" property`, value, tag) ErrorLogF(`"%T" out of range of "%s" property`, value, tag)
return false return nil
} }
properties.properties[tag] = value properties.setRaw(tag, value)
return true return nil
case float32: case float32:
f = float64(value) f = float64(value)
@ -758,64 +822,70 @@ func (properties *propertyList) setFloatProperty(tag string, value any, min, max
f = float64(n) f = float64(n)
} else { } else {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return nil
} }
} }
if f >= min && f <= max { if f >= min && f <= max {
properties.properties[tag] = f properties.setRaw(tag, f)
} else { } else {
ErrorLogF(`"%T" out of range of "%s" property`, value, tag) ErrorLogF(`"%T" out of range of "%s" property`, value, tag)
return false return nil
} }
} }
return true return []PropertyName{tag}
} }
func (properties *propertyList) Set(tag string, value any) bool { func propertiesSet(properties Properties, tag PropertyName, value any) []PropertyName {
return properties.set(strings.ToLower(tag), value)
}
func (properties *propertyList) set(tag string, value any) bool {
if value == nil {
delete(properties.properties, tag)
return true
}
if _, ok := sizeProperties[tag]; ok { if _, ok := sizeProperties[tag]; ok {
return properties.setSizeProperty(tag, value) return setSizeProperty(properties, tag, value)
} }
if valuesData, ok := enumProperties[tag]; ok { if valuesData, ok := enumProperties[tag]; ok {
return properties.setEnumProperty(tag, value, valuesData.values) return setEnumProperty(properties, tag, value, valuesData.values)
} }
if limits, ok := floatProperties[tag]; ok { if limits, ok := floatProperties[tag]; ok {
return properties.setFloatProperty(tag, value, limits.min, limits.max) return setFloatProperty(properties, tag, value, limits.min, limits.max)
} }
if isPropertyInList(tag, colorProperties) { if slices.Contains(colorProperties, tag) {
return properties.setColorProperty(tag, value) return setColorProperty(properties, tag, value)
} }
if isPropertyInList(tag, angleProperties) { if slices.Contains(angleProperties, tag) {
return properties.setAngleProperty(tag, value) return setAngleProperty(properties, tag, value)
} }
if isPropertyInList(tag, boolProperties) { if slices.Contains(boolProperties, tag) {
return properties.setBoolProperty(tag, value) return setBoolProperty(properties, tag, value)
} }
if isPropertyInList(tag, intProperties) { if slices.Contains(intProperties, tag) {
return properties.setIntProperty(tag, value) return setIntProperty(properties, tag, value)
} }
if text, ok := value.(string); ok { if text, ok := value.(string); ok {
properties.properties[tag] = text properties.setRaw(tag, text)
return true return []PropertyName{tag}
} }
notCompatibleType(tag, value) notCompatibleType(tag, value)
return nil
}
func (data *dataProperty) Set(tag PropertyName, value any) bool {
if value == nil {
data.Remove(tag)
return true
}
tag = data.normalize(tag)
if slices.Contains(data.supportedProperties, tag) {
return data.set(data, tag, value) != nil
}
ErrorLogF(`"%s" property is not supported`, string(tag))
return false return false
} }

View File

@ -1,5 +1,6 @@
package rui package rui
// Constants for various specific properties of a views
const ( const (
// Visible - default value of the view Visibility property: View is visible // Visible - default value of the view Visibility property: View is visible
Visible = 0 Visible = 0
@ -148,9 +149,9 @@ const (
WhiteSpacePreLine = 4 WhiteSpacePreLine = 4
// WhiteSpaceBreakSpaces - the behavior is identical to that of WhiteSpacePreWrap, except that: // WhiteSpaceBreakSpaces - the behavior is identical to that of WhiteSpacePreWrap, except that:
// * Any sequence of preserved white space always takes up space, including at the end of the line. // - Any sequence of preserved white space always takes up space, including at the end of the line.
// * A line breaking opportunity exists after every preserved white space character, including between white space characters. // - A line breaking opportunity exists after every preserved white space character, including between white space characters.
// * Such preserved spaces take up space and do not hang, and thus affect the boxs intrinsic sizes (min-content size and max-content size). // - Such preserved spaces take up space and do not hang, and thus affect the boxs intrinsic sizes (min-content size and max-content size).
WhiteSpaceBreakSpaces = 5 WhiteSpaceBreakSpaces = 5
// WordBreakNormal - use the default line break rule. // WordBreakNormal - use the default line break rule.
@ -307,4 +308,108 @@ const (
// "dense" packing algorithm attempts to fill in holes earlier in the grid, if smaller items come up later. // "dense" packing algorithm attempts to fill in holes earlier in the grid, if smaller items come up later.
// This may cause views to appear out-of-order, when doing so would fill in holes left by larger views. // This may cause views to appear out-of-order, when doing so would fill in holes left by larger views.
ColumnDenseAutoFlow = 3 ColumnDenseAutoFlow = 3
// BlendNormal - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the top color, regardless of what the bottom color is.
// The effect is like two opaque pieces of paper overlapping.
BlendNormal = 0
// BlendMultiply - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of multiplying the top and bottom colors.
// A black layer leads to a black final layer, and a white layer leads to no change.
// The effect is like two images printed on transparent film overlapping.
BlendMultiply = 1
// BlendScreen - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of inverting the colors, multiplying them, and inverting that value.
// A black layer leads to no change, and a white layer leads to a white final layer.
// The effect is like two images shone onto a projection screen.
BlendScreen = 2
// BlendOverlay - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of multiply if the bottom color is darker, or screen if the bottom color is lighter.
// This blend mode is equivalent to hard-light but with the layers swapped.
BlendOverlay = 3
// BlendDarken - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is composed of the darkest values of each color channel.
BlendDarken = 4
// BlendLighten - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is composed of the lightest values of each color channel.
BlendLighten = 5
// BlendColorDodge - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of dividing the bottom color by the inverse of the top color.
// A black foreground leads to no change. A foreground with the inverse color of the backdrop leads to a fully lit color.
// This blend mode is similar to screen, but the foreground need only be as light as the inverse of the backdrop to create a fully lit color.
BlendColorDodge = 6
// BlendColorBurn - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of inverting the bottom color, dividing the value by the top color, and inverting that value.
// A white foreground leads to no change. A foreground with the inverse color of the backdrop leads to a black final image.
// This blend mode is similar to multiply, but the foreground need only be as dark as the inverse of the backdrop to make the final image black.
BlendColorBurn = 7
// BlendHardLight - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of multiply if the top color is darker, or screen if the top color is lighter.
// This blend mode is equivalent to overlay but with the layers swapped. The effect is similar to shining a harsh spotlight on the backdrop.
BlendHardLight = 8
// BlendSoftLight - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is similar to hard-light, but softer. This blend mode behaves similar to hard-light.
// The effect is similar to shining a diffused spotlight on the backdrop*.*
BlendSoftLight = 9
// BlendDifference - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is the result of subtracting the darker of the two colors from the lighter one.
// A black layer has no effect, while a white layer inverts the other layer's color.
BlendDifference = 10
// BlendExclusion - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color is similar to difference, but with less contrast.
// As with difference, a black layer has no effect, while a white layer inverts the other layer's color.
BlendExclusion = 11
// BlendHue - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color has the hue of the top color, while using the saturation and luminosity of the bottom color.
BlendHue = 12
// BlendSaturation - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color has the saturation of the top color, while using the hue and luminosity of the bottom color.
// A pure gray backdrop, having no saturation, will have no effect.
BlendSaturation = 13
// BlendColor - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color has the hue and saturation of the top color, while using the luminosity of the bottom color.
// The effect preserves gray levels and can be used to colorize the foreground.
BlendColor = 14
// BlendLuminosity - value of the "mix-blend-mode" and "background-blend-mode" property:
// The final color has the luminosity of the top color, while using the hue and saturation of the bottom color.
// This blend mode is equivalent to BlendColor, but with the layers swapped.
BlendLuminosity = 15
// ColumnFillBalance - value of the "column-fill" property: content is equally divided between columns.
ColumnFillBalance = 0
// ColumnFillAuto - value of the "column-fill" property:
// Columns are filled sequentially. Content takes up only the room it needs, possibly resulting in some columns remaining empty.
ColumnFillAuto = 1
// TextWrapOn - value of the "text-wrap" property:
// text is wrapped across lines at appropriate characters (for example spaces,
// in languages like English that use space separators) to minimize overflow.
TextWrapOn = 0
// TextWrapOff - value of the "text-wrap" property: text does not wrap across lines.
// It will overflow its containing element rather than breaking onto a new line.
TextWrapOff = 1
// TextWrapBalance - value of the "text-wrap" property: text is wrapped in a way
// that best balances the number of characters on each line, enhancing layout quality
// and legibility. Because counting characters and balancing them across multiple lines
// is computationally expensive, this value is only supported for blocks of text
// spanning a limited number of lines (six or less for Chromium and ten or less for Firefox).
TextWrapBalance = 2
) )

852
radius.go

File diff suppressed because it is too large Load Diff

75
range.go Normal file
View File

@ -0,0 +1,75 @@
package rui
import (
"fmt"
"strconv"
"strings"
)
// Range defines range limits. The First and Last value are included in the range
type Range struct {
First, Last int
}
// String returns a string representation of the Range struct
func (r Range) String() string {
if r.First == r.Last {
return fmt.Sprintf("%d", r.First)
}
return fmt.Sprintf("%d:%d", r.First, r.Last)
}
func (r *Range) setValue(value string) bool {
var err error
if strings.ContainsRune(value, ':') {
values := strings.Split(value, ":")
if len(values) != 2 {
ErrorLog("Invalid range value: " + value)
return false
}
if r.First, err = strconv.Atoi(strings.Trim(values[0], " \t\n\r")); err != nil {
ErrorLog(`Invalid first range value "` + value + `" (` + err.Error() + ")")
return false
}
if r.Last, err = strconv.Atoi(strings.Trim(values[1], " \t\n\r")); err != nil {
ErrorLog(`Invalid last range value "` + value + `" (` + err.Error() + ")")
return false
}
return true
}
if r.First, err = strconv.Atoi(value); err != nil {
ErrorLog(`Invalid range value "` + value + `" (` + err.Error() + ")")
return false
}
r.Last = r.First
return true
}
func setRangeProperty(properties Properties, tag PropertyName, value any) []PropertyName {
switch value := value.(type) {
case string:
if setSimpleProperty(properties, tag, value) {
return []PropertyName{tag}
}
var r Range
if !r.setValue(value) {
invalidPropertyValue(tag, value)
return nil
}
properties.setRaw(tag, r)
case Range:
properties.setRaw(tag, value)
default:
if n, ok := isInt(value); ok {
properties.setRaw(tag, Range{First: n, Last: n})
} else {
notCompatibleType(tag, value)
return nil
}
}
return []PropertyName{tag}
}

View File

@ -6,40 +6,62 @@ import (
"strings" "strings"
) )
// Constants for [Resizable] specific properties and events
const ( const (
// Side is the constant for the "side" property tag. // Side is the constant for "side" property tag.
// The "side" int property determines which side of the container is used to resize. //
// The value of property is or-combination of TopSide (1), RightSide (2), BottomSide (4), and LeftSide (8) // Used by Resizable.
Side = "side" // Determines which side of the container is used to resize. The value of property is an or-combination of values listed.
// ResizeBorderWidth is the constant for the "resize-border-width" property tag. // Default value is "all".
// The "ResizeBorderWidth" SizeUnit property determines the width of the resizing border //
ResizeBorderWidth = "resize-border-width" // Supported types: int, string.
// CellVerticalAlign is the constant for the "cell-vertical-align" property tag. //
CellVerticalAlign = "cell-vertical-align" // Values:
// CellHorizontalAlign is the constant for the "cell-horizontal-align" property tag. // - 1 (TopSide) or "top" - Top frame side.
CellHorizontalAlign = "cell-horizontal-align" // - 2 (RightSide) or "right" - Right frame side.
// - 4 (BottomSide) or "bottom" - Bottom frame side.
// - 8 (LeftSide) or "left" - Left frame side.
// - 15 (AllSides) or "all" - All frame sides.
Side PropertyName = "side"
// ResizeBorderWidth is the constant for "resize-border-width" property tag.
//
// Used by Resizable.
// Specifies the width of the resizing border.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
ResizeBorderWidth PropertyName = "resize-border-width"
)
// Constants for values of [Resizable] "side" property. These constants can be ORed if needed.
const (
// TopSide is value of the "side" property: the top side is used to resize // TopSide is value of the "side" property: the top side is used to resize
TopSide = 1 TopSide = 1
// RightSide is value of the "side" property: the right side is used to resize // RightSide is value of the "side" property: the right side is used to resize
RightSide = 2 RightSide = 2
// BottomSide is value of the "side" property: the bottom side is used to resize // BottomSide is value of the "side" property: the bottom side is used to resize
BottomSide = 4 BottomSide = 4
// LeftSide is value of the "side" property: the left side is used to resize // LeftSide is value of the "side" property: the left side is used to resize
LeftSide = 8 LeftSide = 8
// AllSides is value of the "side" property: all sides is used to resize // AllSides is value of the "side" property: all sides is used to resize
AllSides = TopSide | RightSide | BottomSide | LeftSide AllSides = TopSide | RightSide | BottomSide | LeftSide
) )
// Resizable - grid-container of View // Resizable represents a Resizable view
type Resizable interface { type Resizable interface {
View View
ParanetView ParentView
} }
type resizableData struct { type resizableData struct {
viewData viewData
content []View
} }
// NewResizable create new Resizable object and return it // NewResizable create new Resizable object and return it
@ -51,97 +73,40 @@ func NewResizable(session Session, params Params) Resizable {
} }
func newResizable(session Session) View { func newResizable(session Session) View {
return NewResizable(session, nil) return new(resizableData)
} }
func (resizable *resizableData) init(session Session) { func (resizable *resizableData) init(session Session) {
resizable.viewData.init(session) resizable.viewData.init(session)
resizable.tag = "Resizable" resizable.tag = "Resizable"
resizable.systemClass = "ruiGridLayout" resizable.systemClass = "ruiGridLayout"
resizable.content = []View{} resizable.set = resizable.setFunc
} resizable.changed = resizable.propertyChanged
func (resizable *resizableData) String() string {
return getViewString(resizable)
} }
func (resizable *resizableData) Views() []View { func (resizable *resizableData) Views() []View {
return resizable.content if view := resizable.content(); view != nil {
return []View{view}
}
return []View{}
} }
func (resizable *resizableData) Remove(tag string) { func (resizable *resizableData) content() View {
resizable.remove(strings.ToLower(tag)) if value := resizable.getRaw(Content); value != nil {
if content, ok := value.(View); ok {
return content
}
}
return nil
} }
func (resizable *resizableData) remove(tag string) { func (resizable *resizableData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag { switch tag {
case Side: case Side:
oldSide := resizable.getSide() return resizableSetSide(resizable, value)
delete(resizable.properties, Side)
if oldSide != resizable.getSide() {
if resizable.created {
updateInnerHTML(resizable.htmlID(), resizable.Session())
resizable.updateResizeBorderWidth()
}
resizable.propertyChangedEvent(tag)
}
case ResizeBorderWidth: case ResizeBorderWidth:
w := resizable.resizeBorderWidth() return setSizeProperty(resizable, tag, value)
delete(resizable.properties, ResizeBorderWidth)
if !w.Equal(resizable.resizeBorderWidth()) {
resizable.updateResizeBorderWidth()
resizable.propertyChangedEvent(tag)
}
case Content:
if len(resizable.content) > 0 {
resizable.content = []View{}
if resizable.created {
updateInnerHTML(resizable.htmlID(), resizable.Session())
}
resizable.propertyChangedEvent(tag)
}
default:
resizable.viewData.remove(tag)
}
}
func (resizable *resizableData) Set(tag string, value any) bool {
return resizable.set(strings.ToLower(tag), value)
}
func (resizable *resizableData) set(tag string, value any) bool {
if value == nil {
resizable.remove(tag)
return true
}
switch tag {
case Side:
oldSide := resizable.getSide()
if !resizable.setSide(value) {
notCompatibleType(tag, value)
return false
}
if oldSide != resizable.getSide() {
if resizable.created {
updateInnerHTML(resizable.htmlID(), resizable.Session())
resizable.updateResizeBorderWidth()
}
resizable.propertyChangedEvent(tag)
}
return true
case ResizeBorderWidth:
w := resizable.resizeBorderWidth()
ok := resizable.setSizeProperty(tag, value)
if ok && !w.Equal(resizable.resizeBorderWidth()) {
resizable.updateResizeBorderWidth()
resizable.propertyChangedEvent(tag)
}
return ok
case Content: case Content:
var newContent View = nil var newContent View = nil
@ -153,45 +118,54 @@ func (resizable *resizableData) set(tag string, value any) bool {
newContent = value newContent = value
case DataObject: case DataObject:
if view := CreateViewFromObject(resizable.Session(), value); view != nil { if newContent = CreateViewFromObject(resizable.Session(), value, nil); newContent == nil {
newContent = view return nil
} else {
return false
} }
default: default:
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return nil
} }
if len(resizable.content) == 0 { resizable.setRaw(Content, newContent)
resizable.content = []View{newContent} return []PropertyName{}
} else {
resizable.content[0] = newContent
}
if resizable.created {
updateInnerHTML(resizable.htmlID(), resizable.Session())
}
resizable.propertyChangedEvent(tag)
return true
case CellWidth, CellHeight, GridRowGap, GridColumnGap, CellVerticalAlign, CellHorizontalAlign: case CellWidth, CellHeight, GridRowGap, GridColumnGap, CellVerticalAlign, CellHorizontalAlign:
ErrorLogF(`Not supported "%s" property`, tag) ErrorLogF(`Not supported "%s" property`, string(tag))
return false return nil
} }
return resizable.viewData.set(tag, value) return resizable.viewData.setFunc(tag, value)
} }
func (resizable *resizableData) Get(tag string) any { func (resizable *resizableData) propertyChanged(tag PropertyName) {
return resizable.get(strings.ToLower(tag)) switch tag {
case Side:
updateInnerHTML(resizable.htmlID(), resizable.Session())
fallthrough
case ResizeBorderWidth:
htmlID := resizable.htmlID()
session := resizable.Session()
column, row := resizableCellSizeCSS(resizable)
session.updateCSSProperty(htmlID, "grid-template-columns", column)
session.updateCSSProperty(htmlID, "grid-template-rows", row)
case Content:
updateInnerHTML(resizable.htmlID(), resizable.Session())
default:
resizable.viewData.propertyChanged(tag)
}
} }
func (resizable *resizableData) getSide() int { func resizableSide(view View) int {
if value := resizable.getRaw(Side); value != nil { if value := view.getRaw(Side); value != nil {
switch value := value.(type) { switch value := value.(type) {
case string: case string:
if value, ok := resizable.session.resolveConstants(value); ok { if value, ok := view.Session().resolveConstants(value); ok {
validValues := map[string]int{ validValues := map[string]int{
"top": TopSide, "top": TopSide,
"right": RightSide, "right": RightSide,
@ -200,7 +174,7 @@ func (resizable *resizableData) getSide() int {
"all": AllSides, "all": AllSides,
} }
if strings.Contains(value, "|") { if strings.ContainsRune(value, '|') {
values := strings.Split(value, "|") values := strings.Split(value, "|")
sides := 0 sides := 0
for _, val := range values { for _, val := range values {
@ -235,15 +209,15 @@ func (resizable *resizableData) getSide() int {
return AllSides return AllSides
} }
func (resizable *resizableData) setSide(value any) bool { func resizableSetSide(properties Properties, value any) []PropertyName {
switch value := value.(type) { switch value := value.(type) {
case string: case string:
if n, err := strconv.Atoi(value); err == nil { if n, err := strconv.Atoi(value); err == nil {
if n >= 1 && n <= AllSides { if n >= 1 && n <= AllSides {
resizable.properties[Side] = n properties.setRaw(Side, n)
return true return []PropertyName{Side}
} }
return false return nil
} }
validValues := map[string]int{ validValues := map[string]int{
"top": TopSide, "top": TopSide,
@ -252,7 +226,7 @@ func (resizable *resizableData) setSide(value any) bool {
"left": LeftSide, "left": LeftSide,
"all": AllSides, "all": AllSides,
} }
if strings.Contains(value, "|") { if strings.ContainsRune(value, '|') {
values := strings.Split(value, "|") values := strings.Split(value, "|")
sides := 0 sides := 0
hasConst := false hasConst := false
@ -260,17 +234,17 @@ func (resizable *resizableData) setSide(value any) bool {
val := strings.Trim(val, " \t\r\n") val := strings.Trim(val, " \t\r\n")
values[i] = val values[i] = val
if val[0] == '@' { if ok, _ := isConstantName(val); ok {
hasConst = true hasConst = true
} else if n, err := strconv.Atoi(val); err == nil { } else if n, err := strconv.Atoi(val); err == nil {
if n < 1 || n > AllSides { if n < 1 || n > AllSides {
return false return nil
} }
sides |= n sides |= n
} else if n, ok := validValues[val]; ok { } else if n, ok := validValues[val]; ok {
sides |= n sides |= n
} else { } else {
return false return nil
} }
} }
@ -279,69 +253,58 @@ func (resizable *resizableData) setSide(value any) bool {
for i := 1; i < len(values); i++ { for i := 1; i < len(values); i++ {
value += "|" + values[i] value += "|" + values[i]
} }
resizable.properties[Side] = value properties.setRaw(Side, value)
return true return []PropertyName{Side}
} }
if sides >= 1 && sides <= AllSides { if sides >= 1 && sides <= AllSides {
resizable.properties[Side] = sides properties.setRaw(Side, sides)
return true return []PropertyName{Side}
} }
} else if value[0] == '@' { } else if ok, _ := isConstantName(value); ok {
resizable.properties[Side] = value properties.setRaw(Side, value)
return true return []PropertyName{Side}
} else if n, ok := validValues[value]; ok { } else if n, ok := validValues[value]; ok {
resizable.properties[Side] = n properties.setRaw(Side, n)
return true return []PropertyName{Side}
} }
case int: case int:
if value >= 1 && value <= AllSides { if value >= 1 && value <= AllSides {
resizable.properties[Side] = value properties.setRaw(Side, value)
return true return []PropertyName{Side}
} else { } else {
ErrorLogF(`Invalid value %d of "side" property`, value) ErrorLogF(`Invalid value %d of "side" property`, value)
return false return nil
} }
default: default:
if n, ok := isInt(value); ok { if n, ok := isInt(value); ok {
if n >= 1 && n <= AllSides { if n >= 1 && n <= AllSides {
resizable.properties[Side] = n properties.setRaw(Side, n)
return true return []PropertyName{Side}
} else { } else {
ErrorLogF(`Invalid value %d of "side" property`, n) ErrorLogF(`Invalid value %d of "side" property`, n)
return false return nil
} }
} }
} }
return false return nil
} }
func (resizable *resizableData) resizeBorderWidth() SizeUnit { func resizableBorderWidth(view View) SizeUnit {
result, _ := sizeProperty(resizable, ResizeBorderWidth, resizable.Session()) result, _ := sizeProperty(view, ResizeBorderWidth, view.Session())
if result.Type == Auto || result.Value == 0 { if result.Type == Auto || result.Value == 0 {
return Px(4) return Px(4)
} }
return result return result
} }
func (resizable *resizableData) updateResizeBorderWidth() { func resizableCellSizeCSS(view View) (string, string) {
if resizable.created { w := resizableBorderWidth(view).cssString("4px", view.Session())
htmlID := resizable.htmlID() side := resizableSide(view)
session := resizable.Session()
column, row := resizable.cellSizeCSS()
updateCSSProperty(htmlID, "grid-template-columns", column, session)
updateCSSProperty(htmlID, "grid-template-rows", row, session)
}
}
func (resizable *resizableData) cellSizeCSS() (string, string) {
w := resizable.resizeBorderWidth().cssString("4px", resizable.Session())
side := resizable.getSide()
column := "1fr" column := "1fr"
row := "1fr" row := "1fr"
@ -369,7 +332,7 @@ func (resizable *resizableData) cellSizeCSS() (string, string) {
} }
func (resizable *resizableData) cssStyle(self View, builder cssBuilder) { func (resizable *resizableData) cssStyle(self View, builder cssBuilder) {
column, row := resizable.cellSizeCSS() column, row := resizableCellSizeCSS(resizable)
builder.add("grid-template-columns", column) builder.add("grid-template-columns", column)
builder.add("grid-template-rows", row) builder.add("grid-template-rows", row)
@ -379,19 +342,19 @@ func (resizable *resizableData) cssStyle(self View, builder cssBuilder) {
func (resizable *resizableData) htmlSubviews(self View, buffer *strings.Builder) { func (resizable *resizableData) htmlSubviews(self View, buffer *strings.Builder) {
side := resizable.getSide() side := resizableSide(resizable)
left := 1 left := 1
top := 1 top := 1
leftSide := (side & LeftSide) != 0 leftSide := (side & LeftSide) != 0
rightSide := (side & RightSide) != 0 rightSide := (side & RightSide) != 0
w := resizable.resizeBorderWidth().cssString("4px", resizable.Session()) w := resizableBorderWidth(resizable).cssString("4px", resizable.Session())
if leftSide { if leftSide {
left = 2 left = 2
} }
writePos := func(x1, x2, y1, y2 int) { writePos := func(x1, x2, y1, y2 int) {
buffer.WriteString(fmt.Sprintf(` grid-column-start: %d; grid-column-end: %d; grid-row-start: %d; grid-row-end: %d;"></div>`, x1, x2, y1, y2)) fmt.Fprintf(buffer, ` grid-column-start: %d; grid-column-end: %d; grid-row-start: %d; grid-row-end: %d;"></div>`, x1, x2, y1, y2)
} }
//htmlID := resizable.htmlID() //htmlID := resizable.htmlID()
@ -461,14 +424,13 @@ func (resizable *resizableData) htmlSubviews(self View, buffer *strings.Builder)
} }
} }
if len(resizable.content) > 0 { if view := resizable.content(); view != nil {
view := resizable.content[0]
view.addToCSSStyle(map[string]string{ view.addToCSSStyle(map[string]string{
"grid-column-start": strconv.Itoa(left), "grid-column-start": strconv.Itoa(left),
"grid-column-end": strconv.Itoa(left + 1), "grid-column-end": strconv.Itoa(left + 1),
"grid-row-start": strconv.Itoa(top), "grid-row-start": strconv.Itoa(top),
"grid-row-end": strconv.Itoa(top + 1), "grid-row-end": strconv.Itoa(top + 1),
}) })
viewHTML(view, buffer) viewHTML(view, buffer, "")
} }
} }

View File

@ -1,44 +1,52 @@
package rui package rui
// ResizeEvent is the constant for "resize-event" property tag. // ResizeEvent is the constant for "resize-event" property tag.
// The "resize-event" is fired when the view changes its size.
// The main listener format:
// //
// func(View, Frame). // Used by View.
// Is fired when the view changes its size.
// //
// The additional listener formats: // General listener format:
// //
// func(Frame), func(View), and func(). // func(view rui.View, frame rui.Frame)
const ResizeEvent = "resize-event" //
// where:
// - view - Interface of a view which generated this event,
// - frame - New offset and size of the view's visible area.
//
// Allowed listener formats:
//
// func(frame rui.Frame)
// func(view rui.View)
// func()
const ResizeEvent PropertyName = "resize-event"
func (view *viewData) onResize(self View, x, y, width, height float64) { func (view *viewData) onResize(self View, x, y, width, height float64) {
view.frame.Left = x view.frame.Left = x
view.frame.Top = y view.frame.Top = y
view.frame.Width = width view.frame.Width = width
view.frame.Height = height view.frame.Height = height
for _, listener := range GetResizeListeners(view) { for _, listener := range getOneArgEventListeners[View, Frame](view, nil, ResizeEvent) {
listener(self, view.frame) listener.Run(self, view.frame)
} }
} }
func (view *viewData) onItemResize(self View, index string, x, y, width, height float64) { func (view *viewData) onItemResize(self View, index string, x, y, width, height float64) {
} }
func (view *viewData) setFrameListener(tag string, value any) bool { /*
listeners, ok := valueToEventListeners[View, Frame](value) func setFrameListener(properties Properties, tag PropertyName, value any) bool {
if !ok { if listeners, ok := valueToOneArgEventListeners[View, Frame](value); ok {
notCompatibleType(tag, value) if len(listeners) == 0 {
return false properties.setRaw(tag, nil)
} else {
properties.setRaw(tag, listeners)
}
return true
} }
notCompatibleType(tag, value)
if listeners == nil { return false
delete(view.properties, tag)
} else {
view.properties[tag] = listeners
}
view.propertyChangedEvent(tag)
return true
} }
*/
func (view *viewData) setNoResizeEvent() { func (view *viewData) setNoResizeEvent() {
view.noResizeEvent = true view.noResizeEvent = true
@ -65,11 +73,11 @@ func (view *viewData) Frame() Frame {
} }
// GetViewFrame returns the size and location of view's viewport. // GetViewFrame returns the size and location of view's viewport.
// If the second argument (subviewID) is not specified or it is "" then the value of the first argument (view) is returned //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetViewFrame(view View, subviewID ...string) Frame { func GetViewFrame(view View, subviewID ...string) Frame {
if len(subviewID) > 0 && subviewID[0] != "" { view = getSubview(view, subviewID)
view = ViewByID(view, subviewID[0])
}
if view == nil { if view == nil {
return Frame{} return Frame{}
} }
@ -77,7 +85,16 @@ func GetViewFrame(view View, subviewID ...string) Frame {
} }
// GetResizeListeners returns the list of "resize-event" listeners. If there are no listeners then the empty list is returned // GetResizeListeners returns the list of "resize-event" listeners. If there are no listeners then the empty list is returned
// If the second argument (subviewID) is not specified or it is "" then the listeners list of the first argument (view) is returned //
func GetResizeListeners(view View, subviewID ...string) []func(View, Frame) { // Result elements can be of the following types:
return getEventListeners[View, Frame](view, subviewID, ResizeEvent) // - func(rui.View, rui.Frame),
// - func(rui.View),
// - func(rui.Frame),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetResizeListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, Frame](view, subviewID, ResizeEvent)
} }

View File

@ -3,6 +3,7 @@ package rui
import ( import (
"embed" "embed"
"io" "io"
"io/fs"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -15,6 +16,7 @@ const (
imageDir = "images" imageDir = "images"
themeDir = "themes" themeDir = "themes"
viewDir = "views" viewDir = "views"
popupDir = "popups"
rawDir = "raw" rawDir = "raw"
stringsDir = "strings" stringsDir = "strings"
) )
@ -44,19 +46,20 @@ var resources = resourceManager{
imageSrcSets: map[string][]scaledImage{}, imageSrcSets: map[string][]scaledImage{},
} }
// AddEmbedResources adds embedded resources to the list of application resources
func AddEmbedResources(fs *embed.FS) { func AddEmbedResources(fs *embed.FS) {
resources.embedFS = append(resources.embedFS, fs) resources.embedFS = append(resources.embedFS, fs)
rootDirs := embedRootDirs(fs) rootDirs := resources.embedRootDirs(fs)
for _, dir := range rootDirs { for _, dir := range rootDirs {
switch dir { switch dir {
case imageDir: case imageDir:
scanEmbedImagesDir(fs, dir, "") resources.scanEmbedImagesDir(fs, dir, "")
case themeDir: case themeDir:
scanEmbedThemesDir(fs, dir) resources.scanEmbedThemesDir(fs, dir)
case stringsDir: case stringsDir:
scanEmbedStringsDir(fs, dir) resources.scanEmbedStringsDir(fs, dir)
case viewDir, rawDir: case viewDir, rawDir:
// do nothing // do nothing
@ -67,13 +70,13 @@ func AddEmbedResources(fs *embed.FS) {
if file.IsDir() { if file.IsDir() {
switch file.Name() { switch file.Name() {
case imageDir: case imageDir:
scanEmbedImagesDir(fs, dir+"/"+imageDir, "") resources.scanEmbedImagesDir(fs, dir+"/"+imageDir, "")
case themeDir: case themeDir:
scanEmbedThemesDir(fs, dir+"/"+themeDir) resources.scanEmbedThemesDir(fs, dir+"/"+themeDir)
case stringsDir: case stringsDir:
scanEmbedStringsDir(fs, dir+"/"+stringsDir) resources.scanEmbedStringsDir(fs, dir+"/"+stringsDir)
case viewDir, rawDir: case viewDir, rawDir:
// do nothing // do nothing
@ -85,7 +88,7 @@ func AddEmbedResources(fs *embed.FS) {
} }
} }
func embedRootDirs(fs *embed.FS) []string { func (resources *resourceManager) embedRootDirs(fs *embed.FS) []string {
result := []string{} result := []string{}
if files, err := fs.ReadDir("."); err == nil { if files, err := fs.ReadDir("."); err == nil {
for _, file := range files { for _, file := range files {
@ -97,34 +100,34 @@ func embedRootDirs(fs *embed.FS) []string {
return result return result
} }
func scanEmbedThemesDir(fs *embed.FS, dir string) { func (resources *resourceManager) scanEmbedThemesDir(fs *embed.FS, dir string) {
if files, err := fs.ReadDir(dir); err == nil { if files, err := fs.ReadDir(dir); err == nil {
for _, file := range files { for _, file := range files {
name := file.Name() name := file.Name()
path := dir + "/" + name path := dir + "/" + name
if file.IsDir() { if file.IsDir() {
scanEmbedThemesDir(fs, path) resources.scanEmbedThemesDir(fs, path)
} else if strings.ToLower(filepath.Ext(name)) == ".rui" { } else if strings.ToLower(filepath.Ext(name)) == ".rui" {
if data, err := fs.ReadFile(path); err == nil { if data, err := fs.ReadFile(path); err == nil {
registerThemeText(string(data)) resources.registerThemeText(string(data))
} }
} }
} }
} }
} }
func scanEmbedImagesDir(fs *embed.FS, dir, prefix string) { func (resources *resourceManager) scanEmbedImagesDir(fs *embed.FS, dir, prefix string) {
if files, err := fs.ReadDir(dir); err == nil { if files, err := fs.ReadDir(dir); err == nil {
for _, file := range files { for _, file := range files {
name := file.Name() name := file.Name()
path := dir + "/" + name path := dir + "/" + name
if file.IsDir() { if file.IsDir() {
scanEmbedImagesDir(fs, path, prefix+name+"/") resources.scanEmbedImagesDir(fs, path, prefix+name+"/")
} else { } else {
ext := strings.ToLower(filepath.Ext(name)) ext := strings.ToLower(filepath.Ext(name))
switch ext { switch ext {
case ".png", ".jpg", ".jpeg", ".svg": case ".png", ".jpg", ".jpeg", ".svg", ".gif", ".bmp", ".webp":
registerImage(fs, path, prefix+name) resources.registerImage(fs, path, prefix+name)
} }
} }
} }
@ -136,7 +139,7 @@ func invalidImageFileFormat(filename string) {
`". Image file name format: name[@x-param].ext (examples: icon.png, icon@1.5x.png)`) `". Image file name format: name[@x-param].ext (examples: icon.png, icon@1.5x.png)`)
} }
func registerImage(fs *embed.FS, path, filename string) { func (resources *resourceManager) registerImage(fs *embed.FS, path, filename string) {
resources.images[filename] = imagePath{fs: fs, path: path} resources.images[filename] = imagePath{fs: fs, path: path}
start := strings.LastIndex(filename, "@") start := strings.LastIndex(filename, "@")
@ -169,16 +172,16 @@ func registerImage(fs *embed.FS, path, filename string) {
} }
} }
func scanImagesDirectory(path, filePrefix string) { func (resources *resourceManager) scanImagesDirectory(path, filePrefix string) {
if files, err := os.ReadDir(path); err == nil { if files, err := os.ReadDir(path); err == nil {
for _, file := range files { for _, file := range files {
filename := file.Name() filename := file.Name()
if filename[0] != '.' { if filename[0] != '.' {
newPath := path + `/` + filename newPath := path + `/` + filename
if !file.IsDir() { if !file.IsDir() {
registerImage(nil, newPath, filePrefix+filename) resources.registerImage(nil, newPath, filePrefix+filename)
} else { } else {
scanImagesDirectory(newPath, filePrefix+filename+"/") resources.scanImagesDirectory(newPath, filePrefix+filename+"/")
} }
} }
} }
@ -187,17 +190,17 @@ func scanImagesDirectory(path, filePrefix string) {
} }
} }
func scanThemesDir(path string) { func (resources *resourceManager) scanThemesDir(path string) {
if files, err := os.ReadDir(path); err == nil { if files, err := os.ReadDir(path); err == nil {
for _, file := range files { for _, file := range files {
filename := file.Name() filename := file.Name()
if filename[0] != '.' { if filename[0] != '.' {
newPath := path + `/` + filename newPath := path + `/` + filename
if file.IsDir() { if file.IsDir() {
scanThemesDir(newPath) resources.scanThemesDir(newPath)
} else if strings.ToLower(filepath.Ext(newPath)) == ".rui" { } else if strings.ToLower(filepath.Ext(newPath)) == ".rui" {
if data, err := os.ReadFile(newPath); err == nil { if data, err := os.ReadFile(newPath); err == nil {
registerThemeText(string(data)) resources.registerThemeText(string(data))
} else { } else {
ErrorLog(err.Error()) ErrorLog(err.Error())
} }
@ -217,12 +220,21 @@ func SetResourcePath(path string) {
resources.path += "/" resources.path += "/"
} }
scanImagesDirectory(resources.path+imageDir, "") resources.scanImagesDirectory(resources.path+imageDir, "")
scanThemesDir(resources.path + themeDir) resources.scanThemesDir(resources.path + themeDir)
scanStringsDir(resources.path + stringsDir) resources.scanStringsDir(resources.path + stringsDir)
} }
func registerThemeText(text string) bool { func (resources *resourceManager) scanDefaultResourcePath() {
if exe, err := os.Executable(); err == nil {
path := filepath.Dir(exe) + "/resources/"
resources.scanImagesDirectory(path+imageDir, "")
resources.scanThemesDir(path + themeDir)
resources.scanStringsDir(path + stringsDir)
}
}
func (resources *resourceManager) registerThemeText(text string) bool {
theme, ok := CreateThemeFromText(text) theme, ok := CreateThemeFromText(text)
if !ok { if !ok {
return false return false
@ -268,12 +280,12 @@ func serveResourceFile(filename string, w http.ResponseWriter, r *http.Request)
if serveEmbed(fs, filename) { if serveEmbed(fs, filename) {
return true return true
} }
for _, dir := range embedRootDirs(fs) { for _, dir := range resources.embedRootDirs(fs) {
if serveEmbed(fs, dir+"/"+filename) { if serveEmbed(fs, dir+"/"+filename) {
return true return true
} }
if subdirs, err := fs.ReadDir(dir); err == nil { if subDirs, err := fs.ReadDir(dir); err == nil {
for _, subdir := range subdirs { for _, subdir := range subDirs {
if subdir.IsDir() { if subdir.IsDir() {
if serveEmbed(fs, dir+"/"+subdir.Name()+"/"+filename) { if serveEmbed(fs, dir+"/"+subdir.Name()+"/"+filename) {
return true return true
@ -314,12 +326,13 @@ func serveResourceFile(filename string, w http.ResponseWriter, r *http.Request)
return false return false
} }
// ReadRawResource returns the contents of the raw resource with the specified name
func ReadRawResource(filename string) []byte { func ReadRawResource(filename string) []byte {
for _, fs := range resources.embedFS { for _, fs := range resources.embedFS {
rootDirs := embedRootDirs(fs) rootDirs := resources.embedRootDirs(fs)
for _, dir := range rootDirs { for _, dir := range rootDirs {
switch dir { switch dir {
case imageDir, themeDir, viewDir: case imageDir, themeDir, viewDir, stringsDir:
// do nothing // do nothing
case rawDir: case rawDir:
@ -351,11 +364,50 @@ func ReadRawResource(filename string) []byte {
return nil return nil
} }
// OpenRawResource returns the contents of the raw resource with the specified name
func OpenRawResource(filename string) fs.File {
for _, fs := range resources.embedFS {
rootDirs := resources.embedRootDirs(fs)
for _, dir := range rootDirs {
switch dir {
case imageDir, themeDir, viewDir, stringsDir:
// do nothing
case rawDir:
if file, err := fs.Open(dir + "/" + filename); err == nil {
return file
}
default:
if file, err := fs.Open(dir + "/" + rawDir + "/" + filename); err == nil {
return file
}
}
}
}
if resources.path != "" {
if file, err := os.Open(resources.path + rawDir + "/" + filename); err == nil {
return file
}
}
if exe, err := os.Executable(); err == nil {
if file, err := os.Open(filepath.Dir(exe) + "/resources/" + rawDir + "/" + filename); err == nil {
return file
}
}
ErrorLogF(`The "%s" raw file don't found`, filename)
return nil
}
// AllRawResources returns the list of all raw resources
func AllRawResources() []string { func AllRawResources() []string {
result := []string{} result := []string{}
for _, fs := range resources.embedFS { for _, fs := range resources.embedFS {
rootDirs := embedRootDirs(fs) rootDirs := resources.embedRootDirs(fs)
for _, dir := range rootDirs { for _, dir := range rootDirs {
switch dir { switch dir {
case imageDir, themeDir, viewDir: case imageDir, themeDir, viewDir:
@ -397,6 +449,7 @@ func AllRawResources() []string {
return result return result
} }
// AllImageResources returns the list of all image resources
func AllImageResources() []string { func AllImageResources() []string {
result := make([]string, 0, len(resources.images)) result := make([]string, 0, len(resources.images))
for image := range resources.images { for image := range resources.images {
@ -406,6 +459,7 @@ func AllImageResources() []string {
return result return result
} }
// AddTheme adds theme to application
func AddTheme(theme Theme) { func AddTheme(theme Theme) {
if theme != nil { if theme != nil {
name := theme.Name() name := theme.Name()

View File

@ -59,7 +59,7 @@ func (writer *ruiWriterData) writeString(str string) {
{old: "\"", new: `\"`}, {old: "\"", new: `\"`},
} }
for _, s := range replace { for _, s := range replace {
str = strings.Replace(str, s.old, s.new, -1) str = strings.ReplaceAll(str, s.old, s.new)
} }
writer.buffer.WriteRune('"') writer.buffer.WriteRune('"')
writer.buffer.WriteString(str) writer.buffer.WriteString(str)

View File

@ -1,25 +1,32 @@
package rui package rui
import "fmt"
// ScrollEvent is the constant for "scroll-event" property tag. // ScrollEvent is the constant for "scroll-event" property tag.
// The "scroll-event" is fired when the content of the view is scrolled.
// The main listener format:
// //
// func(View, Frame). // Used by View.
// Is fired when the content of the view is scrolled.
// //
// The additional listener formats: // General listener format:
// //
// func(Frame), func(View), and func(). // func(view rui.View, frame rui.Frame)
const ScrollEvent = "scroll-event" //
// where:
// - view - Interface of a view which generated this event,
// - frame - New offset and size of the view's visible area.
//
// Allowed listener formats:
//
// func(frame rui.Frame)
// func(view rui.View)
// func()
const ScrollEvent PropertyName = "scroll-event"
func (view *viewData) onScroll(self View, x, y, width, height float64) { func (view *viewData) onScroll(self View, x, y, width, height float64) {
view.scroll.Left = x view.scroll.Left = x
view.scroll.Top = y view.scroll.Top = y
view.scroll.Width = width view.scroll.Width = width
view.scroll.Height = height view.scroll.Height = height
for _, listener := range GetScrollListeners(view) { for _, listener := range getOneArgEventListeners[View, Frame](view, nil, ScrollEvent) {
listener(self, view.scroll) listener.Run(self, view.scroll)
} }
} }
@ -35,11 +42,11 @@ func (view *viewData) setScroll(x, y, width, height float64) {
} }
// GetViewScroll returns ... // GetViewScroll returns ...
// If the second argument (subviewID) is not specified or it is "" then a value of the first argument (view) is returned //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetViewScroll(view View, subviewID ...string) Frame { func GetViewScroll(view View, subviewID ...string) Frame {
if len(subviewID) > 0 && subviewID[0] != "" { view = getSubview(view, subviewID)
view = ViewByID(view, subviewID[0])
}
if view == nil { if view == nil {
return Frame{} return Frame{}
} }
@ -47,40 +54,49 @@ func GetViewScroll(view View, subviewID ...string) Frame {
} }
// GetScrollListeners returns the list of "scroll-event" listeners. If there are no listeners then the empty list is returned // GetScrollListeners returns the list of "scroll-event" listeners. If there are no listeners then the empty list is returned
// If the second argument (subviewID) is not specified or it is "" then the listeners list of the first argument (view) is returned //
func GetScrollListeners(view View, subviewID ...string) []func(View, Frame) { // Result elements can be of the following types:
return getEventListeners[View, Frame](view, subviewID, ResizeEvent) // - func(rui.View, rui.Frame),
// - func(rui.View),
// - func(rui.Frame),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetScrollListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[View, Frame](view, subviewID, ScrollEvent)
} }
// ScrollTo scrolls the view's content to the given position. // ScrollTo scrolls the view's content to the given position.
// If the second argument (subviewID) is "" then the first argument (view) is used //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func ScrollViewTo(view View, subviewID string, x, y float64) { func ScrollViewTo(view View, subviewID string, x, y float64) {
if subviewID != "" { if subviewID != "" {
view = ViewByID(view, subviewID) view = ViewByID(view, subviewID)
} }
if view != nil { if view != nil {
view.Session().runScript(fmt.Sprintf(`scrollTo("%s", %g, %g)`, view.htmlID(), x, y)) view.Session().callFunc("scrollTo", view.htmlID(), x, y)
} }
} }
// ScrollViewToEnd scrolls the view's content to the start of view. // ScrollViewToEnd scrolls the view's content to the start of view.
// If the second argument (subviewID) is not specified or it is "" then the first argument (view) is used //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func ScrollViewToStart(view View, subviewID ...string) { func ScrollViewToStart(view View, subviewID ...string) {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0]) view.Session().callFunc("scrollToStart", view.htmlID())
}
if view != nil {
view.Session().runScript(`scrollToStart("` + view.htmlID() + `")`)
} }
} }
// ScrollViewToEnd scrolls the view's content to the end of view. // ScrollViewToEnd scrolls the view's content to the end of view.
// If the second argument (subviewID) is not specified or it is "" then the first argument (view) is used //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func ScrollViewToEnd(view View, subviewID ...string) { func ScrollViewToEnd(view View, subviewID ...string) {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0]) view.Session().callFunc("scrollToEnd", view.htmlID())
}
if view != nil {
view.Session().runScript(`scrollToEnd("` + view.htmlID() + `")`)
} }
} }

View File

@ -7,8 +7,37 @@ import (
"strings" "strings"
) )
type bridge interface {
startUpdateScript(htmlID string) bool
finishUpdateScript(htmlID string)
callFunc(funcName string, args ...any) bool
updateInnerHTML(htmlID, html string)
appendToInnerHTML(htmlID, html string)
updateCSSProperty(htmlID, property, value string)
updateProperty(htmlID, property string, value any)
removeProperty(htmlID, property string)
sendResponse()
setAnimationCSS(css string)
appendAnimationCSS(css string)
canvasStart(htmlID string)
callCanvasFunc(funcName string, args ...any)
callCanvasVarFunc(v any, funcName string, args ...any)
callCanvasImageFunc(url string, property string, funcName string, args ...any)
createCanvasVar(funcName string, args ...any) any
createPath2D(arg string) any
updateCanvasProperty(property string, value any)
canvasFinish()
canvasTextMetrics(htmlID, font, text string) TextMetrics
htmlPropertyValue(htmlID, name string) string
answerReceived(answer DataObject)
close()
remoteAddr() string
}
// SessionContent is the interface of a session content // SessionContent is the interface of a session content
type SessionContent interface { type SessionContent interface {
// CreateRootView will be called by the library to create a root view of the application
CreateRootView(session Session) View CreateRootView(session Session) View
} }
@ -49,7 +78,7 @@ type Session interface {
// Content returns the SessionContent of session // Content returns the SessionContent of session
Content() SessionContent Content() SessionContent
setContent(content SessionContent, self Session) bool setContent(content SessionContent) bool
// SetTitle sets the text of the browser title/tab // SetTitle sets the text of the browser title/tab
SetTitle(title string) SetTitle(title string)
@ -60,11 +89,11 @@ type Session interface {
RootView() View RootView() View
// Get returns a value of the view (with id defined by the first argument) property with name defined by the second argument. // Get returns a value of the view (with id defined by the first argument) property with name defined by the second argument.
// The type of return value depends on the property. If the property is not set then nil is returned. // The type of return value depends on the property. If the property is not set then nil is returned.
Get(viewID, tag string) any Get(viewID string, tag PropertyName) any
// Set sets the value (third argument) of the property (second argument) of the view with id defined by the first argument. // Set sets the value (third argument) of the property (second argument) of the view with id defined by the first argument.
// Return "true" if the value has been set, in the opposite case "false" are returned and // Return "true" if the value has been set, in the opposite case "false" are returned and
// a description of the error is written to the log // a description of the error is written to the log
Set(viewID, tag string, value any) bool Set(viewID string, tag PropertyName, value any) bool
// DownloadFile downloads (saves) on the client side the file located at the specified path on the server. // DownloadFile downloads (saves) on the client side the file located at the specified path on the server.
DownloadFile(path string) DownloadFile(path string)
@ -73,26 +102,70 @@ type Session interface {
// OpenURL opens the url in the new browser tab // OpenURL opens the url in the new browser tab
OpenURL(url string) OpenURL(url string)
// ClientItem reads value by key from the client-side storage
ClientItem(key string) (string, bool)
// SetClientItem stores a key-value pair in the client-side storage
SetClientItem(key, value string)
// RemoveClientItem removes a key-value pair in the client-side storage
RemoveClientItem(key string)
// RemoveAllClientItems removes all key-value pair from the client-side storage
RemoveAllClientItems()
// SetHotKey sets the function that will be called when the given hotkey is pressed.
// Invoke SetHotKey(..., ..., nil) for remove hotkey function.
SetHotKey(keyCode KeyCode, controlKeys ControlKeyMask, fn func(Session))
// StartTimer starts a timer on the client side.
// The first argument specifies the timer period in milliseconds.
// The second argument specifies a function that will be called on each timer event.
// The result is the id of the timer, which is used to stop the timer
StartTimer(ms int, timerFunc func(Session)) int
// StopTimer the timer with the given id
StopTimer(timerID int)
getCurrentTheme() Theme
registerAnimation(props []AnimatedProperty) string registerAnimation(props []AnimatedProperty) string
resolveConstants(value string) (string, bool) resolveConstants(value string) (string, bool)
checkboxOffImage() string checkboxOffImage(accentColor Color) string
checkboxOnImage() string checkboxOnImage(accentColor Color) string
radiobuttonOffImage() string radiobuttonOffImage() string
radiobuttonOnImage() string radiobuttonOnImage(accentColor Color) string
viewByHTMLID(id string) View viewByHTMLID(id string) View
nextViewID() string nextViewID() string
styleProperty(styleTag, property string) any styleProperty(styleTag string, propertyTag PropertyName) any
setBrige(events chan DataObject, brige WebBrige) setBridge(events chan DataObject, bridge bridge)
writeInitScript(writer *strings.Builder) writeInitScript(writer *strings.Builder)
runScript(script string) callFunc(funcName string, args ...any)
runGetterScript(script string) DataObject //, answer chan DataObject) updateInnerHTML(htmlID, html string)
handleAnswer(data DataObject) appendToInnerHTML(htmlID, html string)
updateCSSProperty(htmlID, property, value string)
updateProperty(htmlID, property string, value any)
removeProperty(htmlID, property string)
startUpdateScript(htmlID string) bool
finishUpdateScript(htmlID string)
sendResponse()
addAnimationCSS(css string)
removeAnimation(keyframe string)
htmlPropertyValue(htmlID, name string) string
canvasStart(htmlID string) bool
callCanvasFunc(funcName string, args ...any)
createCanvasVar(funcName string, args ...any) any
createPath(arg string) any
callCanvasVarFunc(v any, funcName string, args ...any)
callCanvasImageFunc(url string, property string, funcName string, args ...any)
updateCanvasProperty(property string, value any)
canvasFinish()
canvasTextMetrics(htmlID, font, text string) TextMetrics
addToEventsQueue(data DataObject)
handleAnswer(command string, data DataObject) bool
handleRootSize(data DataObject) handleRootSize(data DataObject)
handleResize(data DataObject) handleResize(data DataObject)
handleViewEvent(command string, data DataObject) handleEvent(command string, data DataObject)
close() close()
onStart() onStart()
@ -107,10 +180,6 @@ type Session interface {
popupManager() *popupManager popupManager() *popupManager
imageManager() *imageManager imageManager() *imageManager
startUpdateScript(htmlID string)
updateScript(htmlID string) *strings.Builder
finishUpdateScript(htmlID string)
} }
type sessionData struct { type sessionData struct {
@ -137,11 +206,16 @@ type sessionData struct {
ignoreUpdates bool ignoreUpdates bool
popups *popupManager popups *popupManager
images *imageManager images *imageManager
brige WebBrige bridge bridge
events chan DataObject events chan DataObject
animationCounter int animationCounter int
animationCSS string animationCSS string
updateScripts map[string]*strings.Builder updateScripts map[string]*strings.Builder
clientStorage map[string]string
hotkeys map[string]func(Session)
timers map[int]func(Session)
nextTimerID int
pauseTime int64
} }
func newSession(app Application, id int, customTheme string, params DataObject) Session { func newSession(app Application, id int, customTheme string, params DataObject) Session {
@ -158,6 +232,10 @@ func newSession(app Application, id int, customTheme string, params DataObject)
session.animationCounter = 0 session.animationCounter = 0
session.animationCSS = "" session.animationCSS = ""
session.updateScripts = map[string]*strings.Builder{} session.updateScripts = map[string]*strings.Builder{}
session.clientStorage = map[string]string{}
session.hotkeys = map[string]func(Session){}
session.timers = map[int]func(Session){}
session.nextTimerID = 1
if customTheme != "" { if customTheme != "" {
if theme, ok := CreateThemeFromText(customTheme); ok { if theme, ok := CreateThemeFromText(customTheme); ok {
@ -166,38 +244,8 @@ func newSession(app Application, id int, customTheme string, params DataObject)
} }
} }
if value, ok := params.PropertyValue("touch"); ok { if params != nil {
session.touchScreen = (value == "1" || value == "true") session.handleSessionInfo(params)
}
if value, ok := params.PropertyValue("user-agent"); ok {
session.userAgent = value
}
if value, ok := params.PropertyValue("direction"); ok {
if value == "rtl" {
session.textDirection = RightToLeftDirection
}
}
if value, ok := params.PropertyValue("language"); ok {
session.language = value
}
if value, ok := params.PropertyValue("languages"); ok {
session.languages = strings.Split(value, ",")
}
if value, ok := params.PropertyValue("dark"); ok {
session.darkTheme = (value == "1" || value == "true")
}
if value, ok := params.PropertyValue("pixel-ratio"); ok {
if f, err := strconv.ParseFloat(value, 64); err != nil {
ErrorLog(err.Error())
} else {
session.pixelRatio = f
}
} }
return session return session
@ -211,18 +259,19 @@ func (session *sessionData) ID() int {
return session.sessionID return session.sessionID
} }
func (session *sessionData) setBrige(events chan DataObject, brige WebBrige) { func (session *sessionData) setBridge(events chan DataObject, bridge bridge) {
session.events = events session.events = events
session.brige = brige session.bridge = bridge
} }
func (session *sessionData) close() { func (session *sessionData) close() {
if session.events != nil { if session.events != nil {
session.events <- ParseDataText(`session-close{session="` + strconv.Itoa(session.sessionID) + `"}`) obj, _ := ParseDataText(`session-close{session="` + strconv.Itoa(session.sessionID) + `"}`)
session.events <- obj
} }
} }
func (session *sessionData) styleProperty(styleTag, propertyTag string) any { func (session *sessionData) styleProperty(styleTag string, propertyTag PropertyName) any {
if style := session.getCurrentTheme().style(styleTag); style != nil { if style := session.getCurrentTheme().style(styleTag); style != nil {
return style.getRaw(propertyTag) return style.getRaw(propertyTag)
} }
@ -252,10 +301,10 @@ func (session *sessionData) Content() SessionContent {
return session.content return session.content
} }
func (session *sessionData) setContent(content SessionContent, self Session) bool { func (session *sessionData) setContent(content SessionContent) bool {
if content != nil { if content != nil {
session.content = content session.content = content
session.rootView = content.CreateRootView(self) session.rootView = content.CreateRootView(session)
if session.rootView != nil { if session.rootView != nil {
session.rootView.setParentID("ruiRootView") session.rootView.setParentID("ruiRootView")
return true return true
@ -279,47 +328,63 @@ func (session *sessionData) writeInitScript(writer *strings.Builder) {
if session.rootView != nil { if session.rootView != nil {
writer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`) writer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`)
viewHTML(session.rootView, writer) buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
viewHTML(session.rootView, buffer, "")
text := strings.ReplaceAll(buffer.String(), "'", `\'`)
writer.WriteString(text)
writer.WriteString("';\nscanElementsSize();") writer.WriteString("';\nscanElementsSize();")
} }
session.updateTooltipConstants()
}
func (session *sessionData) updateTooltipConstants() {
if color, ok := session.Color("ruiTooltipBackground"); ok {
session.bridge.callFunc("setCssVar", "--tooltip-background", color.cssString())
}
if color, ok := session.Color("ruiTooltipTextColor"); ok {
session.bridge.callFunc("setCssVar", "--tooltip-text-color", color.cssString())
}
if color, ok := session.Color("ruiTooltipShadowColor"); ok {
session.bridge.callFunc("setCssVar", "--tooltip-shadow-color", color.cssString())
}
} }
func (session *sessionData) reload() { func (session *sessionData) reload() {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
css := appStyles + session.getCurrentTheme().cssText(session) + session.animationCSS css := appStyles + session.getCurrentTheme().cssText(session) + session.animationCSS
css = strings.ReplaceAll(css, "\n", `\n`) session.bridge.callFunc("setStyles", css)
css = strings.ReplaceAll(css, "\t", `\t`)
buffer.WriteString(`document.querySelector('style').textContent = "`)
buffer.WriteString(css)
buffer.WriteString("\";\n")
if session.rootView != nil { if session.rootView != nil {
buffer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`) buffer := allocStringBuilder()
viewHTML(session.rootView, buffer) defer freeStringBuilder(buffer)
buffer.WriteString("';\nscanElementsSize();")
viewHTML(session.rootView, buffer, "")
session.bridge.updateInnerHTML("ruiRootView", buffer.String())
session.bridge.callFunc("scanElementsSize")
} }
session.runScript(buffer.String()) session.updateTooltipConstants()
} }
func (session *sessionData) ignoreViewUpdates() bool { func (session *sessionData) ignoreViewUpdates() bool {
return session.brige == nil || session.ignoreUpdates return session.bridge == nil || session.ignoreUpdates
} }
func (session *sessionData) setIgnoreViewUpdates(ignore bool) { func (session *sessionData) setIgnoreViewUpdates(ignore bool) {
session.ignoreUpdates = ignore session.ignoreUpdates = ignore
} }
func (session *sessionData) Get(viewID, tag string) any { func (session *sessionData) Get(viewID string, tag PropertyName) any {
if view := ViewByID(session.RootView(), viewID); view != nil { if view := ViewByID(session.RootView(), viewID); view != nil {
return view.Get(tag) return view.Get(tag)
} }
return nil return nil
} }
func (session *sessionData) Set(viewID, tag string, value any) bool { func (session *sessionData) Set(viewID string, tag PropertyName, value any) bool {
if view := ViewByID(session.RootView(), viewID); view != nil { if view := ViewByID(session.RootView(), viewID); view != nil {
return view.Set(tag, value) return view.Set(tag, value)
} }
@ -342,27 +407,231 @@ func (session *sessionData) imageManager() *imageManager {
return session.images return session.images
} }
func (session *sessionData) runScript(script string) { func (session *sessionData) callFunc(funcName string, args ...any) {
if session.brige != nil { if session.bridge != nil {
session.brige.WriteMessage(script) session.bridge.callFunc(funcName, args...)
} else { } else {
ErrorLog("No connection") ErrorLog("No connection")
} }
} }
func (session *sessionData) runGetterScript(script string) DataObject { //}, answer chan DataObject) { func (session *sessionData) updateInnerHTML(htmlID, html string) {
if session.brige != nil { if !session.ignoreViewUpdates() {
return session.brige.RunGetterScript(script) if session.bridge != nil {
session.bridge.updateInnerHTML(htmlID, html)
} else {
ErrorLog("No connection")
}
}
}
func (session *sessionData) appendToInnerHTML(htmlID, html string) {
if !session.ignoreViewUpdates() {
if session.bridge != nil {
session.bridge.appendToInnerHTML(htmlID, html)
} else {
ErrorLog("No connection")
}
}
}
func (session *sessionData) updateCSSProperty(htmlID, property, value string) {
if !session.ignoreViewUpdates() {
if session.bridge != nil {
session.bridge.updateCSSProperty(htmlID, property, value)
} else {
ErrorLog("No connection")
}
}
}
func (session *sessionData) updateProperty(htmlID, property string, value any) {
if !session.ignoreViewUpdates() {
if session.bridge != nil {
session.bridge.updateProperty(htmlID, property, value)
} else {
ErrorLog("No connection")
}
}
}
func (session *sessionData) removeProperty(htmlID, property string) {
if !session.ignoreViewUpdates() {
if session.bridge != nil {
session.bridge.removeProperty(htmlID, property)
} else {
ErrorLog("No connection")
}
}
}
func (session *sessionData) startUpdateScript(htmlID string) bool {
if session.bridge != nil {
return session.bridge.startUpdateScript(htmlID)
}
ErrorLog("No connection")
return false
}
func (session *sessionData) finishUpdateScript(htmlID string) {
if session.bridge != nil {
session.bridge.finishUpdateScript(htmlID)
} else {
ErrorLog("No connection")
}
}
func (session *sessionData) sendResponse() {
if session.bridge != nil {
session.bridge.sendResponse()
} else {
ErrorLog("No connection")
}
}
func (session *sessionData) addAnimationCSS(css string) {
session.animationCSS += css
if session.bridge != nil {
session.bridge.appendAnimationCSS(css)
} else {
ErrorLog("No connection")
}
}
func (session *sessionData) removeAnimation(keyframe string) {
css := session.animationCSS
index := strings.Index(css, "@keyframes "+keyframe)
if index < 0 {
return
}
start := strings.IndexRune(css[index:], '{')
if start < 0 {
return
}
n := 1
end := -1
for i := start + index + 1; i < len(css); i++ {
if css[i] == '}' {
n--
if n == 0 {
end = i + 1
if end < len(css) && css[end] == '\n' {
end++
}
break
}
} else if css[i] == '{' {
n++
}
}
if end > index {
session.animationCSS = strings.Trim(css[:index]+css[end:], "\n")
if session.bridge != nil {
session.bridge.setAnimationCSS(session.animationCSS)
} else {
ErrorLog("No connection")
}
}
}
func (session *sessionData) canvasStart(htmlID string) bool {
if session.bridge != nil {
session.bridge.canvasStart(htmlID)
return true
} }
ErrorLog("No connection") ErrorLog("No connection")
result := NewDataObject("error") return false
result.SetPropertyValue("text", "No connection")
return result
} }
func (session *sessionData) handleAnswer(data DataObject) { func (session *sessionData) callCanvasFunc(funcName string, args ...any) {
session.brige.AnswerReceived(data) if session.bridge != nil {
session.bridge.callCanvasFunc(funcName, args...)
}
}
func (session *sessionData) updateCanvasProperty(property string, value any) {
if session.bridge != nil {
session.bridge.updateCanvasProperty(property, value)
}
}
func (session *sessionData) createCanvasVar(funcName string, args ...any) any {
if session.bridge != nil {
return session.bridge.createCanvasVar(funcName, args...)
}
return nil
}
func (session *sessionData) createPath(arg string) any {
if session.bridge != nil {
return session.bridge.createPath2D(arg)
}
return nil
}
func (session *sessionData) callCanvasVarFunc(v any, funcName string, args ...any) {
if session.bridge != nil && v != nil {
session.bridge.callCanvasVarFunc(v, funcName, args...)
}
}
func (session *sessionData) callCanvasImageFunc(url string, property string, funcName string, args ...any) {
if session.bridge != nil {
session.bridge.callCanvasImageFunc(url, property, funcName, args...)
}
}
func (session *sessionData) canvasFinish() {
if session.bridge != nil {
session.bridge.canvasFinish()
}
}
func (session *sessionData) canvasTextMetrics(htmlID, font, text string) TextMetrics {
if session.bridge != nil {
return session.bridge.canvasTextMetrics(htmlID, font, text)
}
ErrorLog("No connection")
return TextMetrics{Width: 0}
}
func (session *sessionData) htmlPropertyValue(htmlID, name string) string {
if session.bridge != nil {
return session.bridge.htmlPropertyValue(htmlID, name)
}
ErrorLog("No connection")
return ""
}
func (session *sessionData) handleAnswer(command string, data DataObject) bool {
switch command {
case "answer":
if session.bridge != nil {
session.bridge.answerReceived(data)
}
case "imageLoaded":
session.imageManager().imageLoaded(data)
case "imageError":
session.imageManager().imageLoadError(data)
default:
return false
}
if session.bridge != nil {
session.bridge.sendResponse()
} else {
ErrorLog("No connection")
}
return true
} }
func (session *sessionData) handleRootSize(data DataObject) { func (session *sessionData) handleRootSize(data DataObject) {
@ -388,8 +657,8 @@ func (session *sessionData) handleRootSize(data DataObject) {
} }
func (session *sessionData) handleResize(data DataObject) { func (session *sessionData) handleResize(data DataObject) {
if node := data.PropertyWithTag("views"); node != nil && node.Type() == ArrayNode { if node := data.PropertyByTag("views"); node != nil && node.Type() == ArrayNode {
for _, el := range node.ArrayElements() { for el := range node.ArrayElements() {
if el.IsObject() { if el.IsObject() {
obj := el.Object() obj := el.Object()
getFloat := func(tag string) float64 { getFloat := func(tag string) float64 {
@ -429,27 +698,181 @@ func (session *sessionData) handleResize(data DataObject) {
} }
} }
func (session *sessionData) handleViewEvent(command string, data DataObject) { func (session *sessionData) handleSessionInfo(params DataObject) {
if viewID, ok := data.PropertyValue("id"); ok { if value, ok := params.PropertyValue("touch"); ok {
if view := session.viewByHTMLID(viewID); view != nil { session.touchScreen = (value == "1" || value == "true")
view.handleCommand(view, command, data) }
if value, ok := params.PropertyValue("user-agent"); ok {
session.userAgent = value
}
if value, ok := params.PropertyValue("direction"); ok {
if value == "rtl" {
session.textDirection = RightToLeftDirection
} }
} else if command != "clickOutsidePopup" { }
ErrorLog(`"id" property not found. Event: ` + command)
if value, ok := params.PropertyValue("language"); ok {
session.language = value
}
if value, ok := params.PropertyValue("languages"); ok {
session.languages = strings.Split(value, ",")
}
if value, ok := params.PropertyValue("dark"); ok {
session.darkTheme = (value == "1" || value == "true")
}
if value, ok := params.PropertyValue("pixel-ratio"); ok {
if f, err := strconv.ParseFloat(value, 64); err != nil {
ErrorLog(err.Error())
} else {
session.pixelRatio = f
}
}
if node := params.PropertyByTag("storage"); node != nil && node.Type() == ObjectNode {
if obj := node.Object(); obj != nil {
for element := range obj.Properties() {
if element.Type() == TextNode {
session.clientStorage[element.Tag()] = element.Text()
}
}
}
}
}
func (session *sessionData) handleEvent(command string, data DataObject) {
switch command {
case "session-pause":
session.onPause()
case "session-resume":
session.onResume()
case "timer":
if text, ok := data.PropertyValue("timerID"); ok {
timerID, err := strconv.Atoi(text)
if err == nil {
if fn, ok := session.timers[timerID]; ok {
fn(session)
} else {
ErrorLog(`Timer (id = ` + text + `) not exists`)
}
} else {
ErrorLog(err.Error())
}
} else {
ErrorLog(`"timerID" property not found`)
}
case "root-size":
session.handleRootSize(data)
case "resize":
session.handleResize(data)
case "sessionInfo":
session.handleSessionInfo(data)
case "storageError":
if text, ok := data.PropertyValue("error"); ok {
ErrorLog(text)
}
default:
if viewID, ok := data.PropertyValue("id"); ok {
if viewID != "body" {
if view := session.viewByHTMLID(viewID); view != nil {
view.handleCommand(view, PropertyName(command), data)
}
}
if command == string(KeyDownEvent) {
var event KeyEvent
event.init(data)
session.hotKey(event)
}
} else {
ErrorLog(`"id" property not found. Event: ` + command)
}
}
session.bridge.sendResponse()
}
func (session *sessionData) hotKey(event KeyEvent) {
popups := session.popupManager().popups
if count := len(popups); count > 0 {
if popups[count-1].keyEvent(event) {
return
}
}
var controlKeys ControlKeyMask = 0
if event.AltKey {
controlKeys |= AltKey
}
if event.CtrlKey {
controlKeys |= CtrlKey
}
if event.MetaKey {
controlKeys |= MetaKey
}
if event.ShiftKey {
controlKeys |= ShiftKey
}
key := hotkeyCode(KeyCode(event.Code), controlKeys)
if fn, ok := session.hotkeys[key]; ok && fn != nil {
fn(session)
}
}
func hotkeyCode(keyCode KeyCode, controlKeys ControlKeyMask) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(strings.ToLower(string(keyCode)))
if controlKeys != 0 {
buffer.WriteRune('-')
if controlKeys&AltKey != 0 {
buffer.WriteRune('a')
}
if controlKeys&CtrlKey != 0 {
buffer.WriteRune('c')
}
if controlKeys&MetaKey != 0 {
buffer.WriteRune('m')
}
if controlKeys&ShiftKey != 0 {
buffer.WriteRune('s')
}
}
return buffer.String()
}
func (session *sessionData) SetHotKey(keyCode KeyCode, controlKeys ControlKeyMask, fn func(Session)) {
hotkey := hotkeyCode(keyCode, controlKeys)
if fn == nil {
delete(session.hotkeys, hotkey)
} else {
session.hotkeys[hotkey] = fn
} }
} }
func (session *sessionData) SetTitle(title string) { func (session *sessionData) SetTitle(title string) {
title, _ = session.GetString(title) title, _ = session.GetString(title)
session.runScript(`document.title = "` + title + `";`) session.callFunc("setTitle", title)
} }
func (session *sessionData) SetTitleColor(color Color) { func (session *sessionData) SetTitleColor(color Color) {
session.runScript(`setTitleColor("` + color.cssString() + `");`) session.callFunc("setTitleColor", color.cssString())
} }
func (session *sessionData) RemoteAddr() string { func (session *sessionData) RemoteAddr() string {
return session.brige.remoteAddr() return session.bridge.remoteAddr()
} }
func (session *sessionData) OpenURL(urlStr string) { func (session *sessionData) OpenURL(urlStr string) {
@ -457,5 +880,47 @@ func (session *sessionData) OpenURL(urlStr string) {
ErrorLog(err.Error()) ErrorLog(err.Error())
return return
} }
session.runScript(`window.open("` + urlStr + `", "_blank");`) session.callFunc("openURL", urlStr)
}
func (session *sessionData) ClientItem(key string) (string, bool) {
value, ok := session.clientStorage[key]
return value, ok
}
func (session *sessionData) SetClientItem(key, value string) {
session.clientStorage[key] = value
session.bridge.callFunc("localStorageSet", key, value)
}
func (session *sessionData) RemoveClientItem(key string) {
delete(session.clientStorage, key)
session.bridge.callFunc("localStorageRemove", key)
}
func (session *sessionData) RemoveAllClientItems() {
session.clientStorage = map[string]string{}
session.bridge.callFunc("localStorageClear")
}
func (session *sessionData) addToEventsQueue(data DataObject) {
session.events <- data
}
func (session *sessionData) StartTimer(ms int, timerFunc func(Session)) int {
timerID := 0
if session.bridge != nil {
timerID = session.nextTimerID
session.nextTimerID++
session.timers[timerID] = timerFunc
session.bridge.callFunc("startTimer", ms, timerID)
}
return timerID
}
func (session *sessionData) StopTimer(timerID int) {
if session.bridge != nil {
session.bridge.callFunc("stopTimer", timerID)
delete(session.timers, timerID)
}
} }

View File

@ -1,32 +1,45 @@
package rui package rui
import "time"
// SessionStartListener is the listener interface of a session start event // SessionStartListener is the listener interface of a session start event
type SessionStartListener interface { type SessionStartListener interface {
// OnStart is a function that is called by the library after the creation of the root view of the application
OnStart(session Session) OnStart(session Session)
} }
// SessionFinishListener is the listener interface of a session start event // SessionFinishListener is the listener interface of a session start event
type SessionFinishListener interface { type SessionFinishListener interface {
// OnFinish is a function that is called by the library when the user closes the application page in the browser
OnFinish(session Session) OnFinish(session Session)
} }
// SessionResumeListener is the listener interface of a session resume event // SessionResumeListener is the listener interface of a session resume event
type SessionResumeListener interface { type SessionResumeListener interface {
// OnResume is a function that is called by the library when the application page in the client's browser becomes
// active and is also called immediately after OnStart
OnResume(session Session) OnResume(session Session)
} }
// SessionPauseListener is the listener interface of a session pause event // SessionPauseListener is the listener interface of a session pause event
type SessionPauseListener interface { type SessionPauseListener interface {
// OnPause is a function that is called by the library when the application page in the client's browser becomes
// inactive and is also called when the user switches to a different browser tab/window, minimizes the browser,
// or switches to another application
OnPause(session Session) OnPause(session Session)
} }
// SessionPauseListener is the listener interface of a session disconnect event // SessionPauseListener is the listener interface of a session disconnect event
type SessionDisconnectListener interface { type SessionDisconnectListener interface {
// OnDisconnect is a function that is called by the library if the server loses connection with the client and
// this happens when the connection is broken
OnDisconnect(session Session) OnDisconnect(session Session)
} }
// SessionPauseListener is the listener interface of a session reconnect event // SessionPauseListener is the listener interface of a session reconnect event
type SessionReconnectListener interface { type SessionReconnectListener interface {
// OnReconnect is a function that is called by the library after the server reconnects with the client
// and this happens when the connection is restored
OnReconnect(session Session) OnReconnect(session Session)
} }
@ -50,13 +63,25 @@ func (session *sessionData) onFinish() {
func (session *sessionData) onPause() { func (session *sessionData) onPause() {
if session.content != nil { if session.content != nil {
session.pauseTime = time.Now().Unix()
if listener, ok := session.content.(SessionPauseListener); ok { if listener, ok := session.content.(SessionPauseListener); ok {
listener.OnPause(session) listener.OnPause(session)
} }
if timeout := session.app.Params().SocketAutoClose; timeout > 0 {
go session.autoClose(session.pauseTime, timeout)
}
}
}
func (session *sessionData) autoClose(start int64, timeout int) {
time.Sleep(time.Second * time.Duration(timeout))
if session.pauseTime == start {
session.bridge.callFunc("closeSocket")
} }
} }
func (session *sessionData) onResume() { func (session *sessionData) onResume() {
session.pauseTime = 0
if session.content != nil { if session.content != nil {
if listener, ok := session.content.(SessionResumeListener); ok { if listener, ok := session.content.(SessionResumeListener); ok {
listener.OnResume(session) listener.OnResume(session)

View File

@ -35,10 +35,8 @@ func (session *sessionData) constant(tag string, prevTags []string) (string, boo
return result, true return result, true
} }
for _, separator := range []string{",", " ", ":", ";", "|", "/"} { if strings.ContainsAny(result, ", :;|/") {
if strings.Contains(result, separator) { return session.resolveConstantsNext(result, tags)
return session.resolveConstantsNext(result, tags)
}
} }
if result[0] != '@' { if result[0] != '@' {
@ -61,7 +59,7 @@ func (session *sessionData) resolveConstants(value string) (string, bool) {
} }
func (session *sessionData) resolveConstantsNext(value string, prevTags []string) (string, bool) { func (session *sessionData) resolveConstantsNext(value string, prevTags []string) (string, bool) {
if !strings.Contains(value, "@") { if !strings.ContainsRune(value, '@') {
return value, true return value, true
} }
@ -203,7 +201,7 @@ func (session *sessionData) SetCustomTheme(name string) bool {
const checkImage = `<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m4 8 3 4 5-8" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"/></svg>` const checkImage = `<svg width="16" height="16" version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="m4 8 3 4 5-8" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"/></svg>`
func (session *sessionData) checkboxImage(checked bool) string { func (session *sessionData) checkboxImage(checked bool, accentColor Color) string {
var borderColor, backgroundColor Color var borderColor, backgroundColor Color
var ok bool var ok bool
@ -217,7 +215,9 @@ func (session *sessionData) checkboxImage(checked bool) string {
} }
if checked { if checked {
if backgroundColor, ok = session.Color("ruiHighlightColor"); !ok { if accentColor != 0 {
backgroundColor = accentColor
} else if backgroundColor, ok = session.Color("ruiHighlightColor"); !ok {
backgroundColor = 0xFF1A74E8 backgroundColor = 0xFF1A74E8
} }
} else if backgroundColor, ok = session.Color("ruiBackgroundColor"); !ok { } else if backgroundColor, ok = session.Color("ruiBackgroundColor"); !ok {
@ -244,16 +244,22 @@ func (session *sessionData) checkboxImage(checked bool) string {
return buffer.String() return buffer.String()
} }
func (session *sessionData) checkboxOffImage() string { func (session *sessionData) checkboxOffImage(accentColor Color) string {
if accentColor != 0 {
return session.checkboxImage(false, accentColor)
}
if session.checkboxOff == "" { if session.checkboxOff == "" {
session.checkboxOff = session.checkboxImage(false) session.checkboxOff = session.checkboxImage(false, accentColor)
} }
return session.checkboxOff return session.checkboxOff
} }
func (session *sessionData) checkboxOnImage() string { func (session *sessionData) checkboxOnImage(accentColor Color) string {
if accentColor != 0 {
return session.checkboxImage(true, accentColor)
}
if session.checkboxOn == "" { if session.checkboxOn == "" {
session.checkboxOn = session.checkboxImage(true) session.checkboxOn = session.checkboxImage(true, accentColor)
} }
return session.checkboxOn return session.checkboxOn
} }
@ -285,12 +291,14 @@ func (session *sessionData) radiobuttonOffImage() string {
return session.radiobuttonOff return session.radiobuttonOff
} }
func (session *sessionData) radiobuttonOnImage() string { func (session *sessionData) radiobuttonOnImage(accentColor Color) string {
if session.radiobuttonOn == "" { if session.radiobuttonOn == "" {
var borderColor, backgroundColor Color var borderColor, backgroundColor Color
var ok bool var ok bool
if borderColor, ok = session.Color("ruiHighlightColor"); !ok { if accentColor != 0 {
borderColor = accentColor
} else if borderColor, ok = session.Color("ruiHighlightColor"); !ok {
borderColor = 0xFF1A74E8 borderColor = 0xFF1A74E8
} }
@ -313,7 +321,7 @@ func (session *sessionData) Language() string {
return session.language return session.language
} }
if session.languages != nil && len(session.languages) > 0 { if len(session.languages) > 0 {
return session.languages[0] return session.languages[0]
} }
@ -325,15 +333,12 @@ func (session *sessionData) SetLanguage(lang string) {
if lang != session.language { if lang != session.language {
session.language = lang session.language = lang
if session.rootView != nil { if session.rootView != nil && session.bridge != nil {
buffer := allocStringBuilder() buffer := allocStringBuilder()
defer freeStringBuilder(buffer) defer freeStringBuilder(buffer)
buffer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`) viewHTML(session.rootView, buffer, "")
viewHTML(session.rootView, buffer) session.bridge.updateInnerHTML("ruiRootView", buffer.String())
buffer.WriteString("';\nscanElementsSize();")
session.runScript(buffer.String())
} }
} }
} }

View File

@ -1,34 +1,5 @@
package rui package rui
import (
"fmt"
"strings"
)
func (session *sessionData) startUpdateScript(htmlID string) {
buffer := allocStringBuilder()
session.updateScripts[htmlID] = buffer
buffer.WriteString("var element = document.getElementById('")
buffer.WriteString(htmlID)
buffer.WriteString("');\nif (element) {\n")
}
func (session *sessionData) updateScript(htmlID string) *strings.Builder {
if buffer, ok := session.updateScripts[htmlID]; ok {
return buffer
}
return nil
}
func (session *sessionData) finishUpdateScript(htmlID string) {
if buffer, ok := session.updateScripts[htmlID]; ok {
buffer.WriteString("scanElementsSize();\n}\n")
session.runScript(buffer.String())
freeStringBuilder(buffer)
delete(session.updateScripts, htmlID)
}
}
func sizeConstant(session Session, tag string) (SizeUnit, bool) { func sizeConstant(session Session, tag string) (SizeUnit, bool) {
if text, ok := session.Constant(tag); ok { if text, ok := session.Constant(tag); ok {
return StringToSizeUnit(text) return StringToSizeUnit(text)
@ -39,15 +10,10 @@ func sizeConstant(session Session, tag string) (SizeUnit, bool) {
func updateCSSStyle(htmlID string, session Session) { func updateCSSStyle(htmlID string, session Session) {
if !session.ignoreViewUpdates() { if !session.ignoreViewUpdates() {
if view := session.viewByHTMLID(htmlID); view != nil { if view := session.viewByHTMLID(htmlID); view != nil {
var builder viewCSSBuilder builder := viewCSSBuilder{buffer: allocStringBuilder()}
builder.buffer = allocStringBuilder()
builder.buffer.WriteString(`updateCSSStyle('`)
builder.buffer.WriteString(view.htmlID())
builder.buffer.WriteString(`', '`)
view.cssStyle(view, &builder) view.cssStyle(view, &builder)
builder.buffer.WriteString(`');`) //session.callFunc("updateCSSStyle", view.htmlID(), builder.finish())
view.Session().runScript(builder.finish()) session.updateProperty(view.htmlID(), "style", builder.finish())
} }
} }
} }
@ -61,94 +27,24 @@ func updateInnerHTML(htmlID string, session Session) {
view = session.viewByHTMLID(htmlID) view = session.viewByHTMLID(htmlID)
} }
if view != nil { if view != nil {
session.callFunc("hideTooltip")
script := allocStringBuilder() script := allocStringBuilder()
defer freeStringBuilder(script) defer freeStringBuilder(script)
script.Grow(32 * 1024) script.Grow(32 * 1024)
view.htmlSubviews(view, script) view.htmlSubviews(view, script)
view.Session().runScript(fmt.Sprintf(`updateInnerHTML('%v', '%v');`, view.htmlID(), script.String())) session.updateInnerHTML(view.htmlID(), script.String())
//view.updateEventHandlers()
} }
} }
} }
func appendToInnerHTML(htmlID, content string, session Session) {
if !session.ignoreViewUpdates() {
if view := session.viewByHTMLID(htmlID); view != nil {
view.Session().runScript(fmt.Sprintf(`appendToInnerHTML('%v', '%v');`, view.htmlID(), content))
//view.updateEventHandlers()
}
}
}
func updateProperty(htmlID, property, value string, session Session) {
if !session.ignoreViewUpdates() {
if buffer := session.updateScript(htmlID); buffer != nil {
buffer.WriteString(fmt.Sprintf(`element.setAttribute('%v', '%v');`, property, value))
buffer.WriteRune('\n')
} else {
session.runScript(fmt.Sprintf(`updateProperty('%v', '%v', '%v');`, htmlID, property, value))
}
}
}
func updateCSSProperty(htmlID, property, value string, session Session) {
if !session.ignoreViewUpdates() {
if buffer := session.updateScript(htmlID); buffer != nil {
buffer.WriteString(fmt.Sprintf(`element.style['%v'] = '%v';`, property, value))
buffer.WriteRune('\n')
} else {
session.runScript(fmt.Sprintf(`updateCSSProperty('%v', '%v', '%v');`, htmlID, property, value))
}
}
}
func updateBoolProperty(htmlID, property string, value bool, session Session) {
if !session.ignoreViewUpdates() {
if buffer := session.updateScript(htmlID); buffer != nil {
if value {
buffer.WriteString(fmt.Sprintf(`element.setAttribute('%v', true);`, property))
} else {
buffer.WriteString(fmt.Sprintf(`element.setAttribute('%v', false);`, property))
}
buffer.WriteRune('\n')
} else if value {
session.runScript(fmt.Sprintf(`updateProperty('%v', '%v', true);`, htmlID, property))
} else {
session.runScript(fmt.Sprintf(`updateProperty('%v', '%v', false);`, htmlID, property))
}
}
}
func removeProperty(htmlID, property string, session Session) {
if !session.ignoreViewUpdates() {
if buffer := session.updateScript(htmlID); buffer != nil {
buffer.WriteString(fmt.Sprintf(`if (element.hasAttribute('%v')) { element.removeAttribute('%v');}`, property, property))
buffer.WriteRune('\n')
} else {
session.runScript(fmt.Sprintf(`removeProperty('%v', '%v');`, htmlID, property))
}
}
}
/*
func setDisabled(htmlID string, disabled bool, session Session) {
if !session.ignoreViewUpdates() {
if disabled {
session.runScript(fmt.Sprintf(`setDisabled('%v', true);`, htmlID))
} else {
session.runScript(fmt.Sprintf(`setDisabled('%v', false);`, htmlID))
}
}
}
*/
func viewByHTMLID(id string, startView View) View { func viewByHTMLID(id string, startView View) View {
if startView != nil { if startView != nil {
if startView.htmlID() == id { if startView.htmlID() == id {
return startView return startView
} }
if container, ok := startView.(ParanetView); ok { if container, ok := startView.(ParentView); ok {
for _, view := range container.Views() { for _, view := range container.Views() {
if view != nil { if view != nil {
if v := viewByHTMLID(id, view); v != nil { if v := viewByHTMLID(id, view); v != nil {

296
shadow.go
View File

@ -5,30 +5,110 @@ import (
"strings" "strings"
) )
// Constants for [ShadowProperty] specific properties
const ( const (
// ColorTag is the name of the color property of the shadow. // ColorTag is the constant for "color" property tag.
ColorTag = "color" //
// Inset is the name of bool property of the shadow. If it is set to "false" (default) then the shadow // Used by ColumnSeparatorProperty, BorderProperty, OutlineProperty, ShadowProperty.
// is assumed to be a drop shadow (as if the box were raised above the content). //
// If it is set to "true" then the shadow to one inside the frame (as if the content was depressed inside the box). // # Usage in ColumnSeparatorProperty
// Inset shadows are drawn inside the border (even transparent ones), above the background, but below content. //
Inset = "inset" // Line color.
// XOffset is the name of the SizeUnit property of the shadow that determines the shadow horizontal offset. //
// Negative values place the shadow to the left of the element. // Supported types: Color, string.
XOffset = "x-offset" //
// YOffset is the name of the SizeUnit property of the shadow that determines the shadow vertical offset. // Internal type is Color, other types converted to it during assignment.
// Negative values place the shadow above the element. // See Color description for more details.
YOffset = "y-offset" //
// BlurRadius is the name of the SizeUnit property of the shadow that determines the radius of the blur effect. // # Usage in BorderProperty
// The larger this value, the bigger the blur, so the shadow becomes bigger and lighter. Negative values are not allowed. //
BlurRadius = "blur" // Border line color.
// SpreadRadius is the name of the SizeUnit property of the shadow. Positive values will cause the shadow to expand //
// and grow bigger, negative values will cause the shadow to shrink. // Supported types: Color, string.
SpreadRadius = "spread-radius" //
// Internal type is Color, other types converted to it during assignment.
// See Color description for more details.
//
// # Usage in OutlineProperty
//
// Outline line color.
//
// Supported types: Color, string.
//
// Internal type is Color, other types converted to it during assignment.
// See Color description for more details.
//
// # Usage in ShadowProperty
//
// Color property of the shadow.
//
// Supported types: Color, string.
//
// Internal type is Color, other types converted to it during assignment.
// See Color description for more details.
ColorTag PropertyName = "color"
// Inset is the constant for "inset" property tag.
//
// Used by ShadowProperty.
// Controls whether to draw shadow inside the frame or outside. Inset shadows are drawn inside the border(even transparent
// ones), above the background, but below content.
//
// Supported types: bool, int, string.
//
// Values:
// - true, 1, "true", "yes", "on", "1" - Drop shadow inside the frame(as if the content was depressed inside the box).
// - false, 0, "false", "no", "off", "0" - Shadow is assumed to be a drop shadow(as if the box were raised above the content).
Inset PropertyName = "inset"
// XOffset is the constant for "x-offset" property tag.
//
// Used by ShadowProperty.
// Determines the shadow horizontal offset. Negative values place the shadow to the left of the element.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
XOffset PropertyName = "x-offset"
// YOffset is the constant for "y-offset" property tag.
//
// Used by ShadowProperty.
// Determines the shadow vertical offset. Negative values place the shadow above the element.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
YOffset PropertyName = "y-offset"
// BlurRadius is the constant for "blur" property tag.
//
// Used by ShadowProperty.
// Determines the radius of the blur effect. The larger this value, the bigger the blur, so the shadow becomes bigger and
// lighter. Negative values are not allowed.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
BlurRadius PropertyName = "blur"
// SpreadRadius is the constant for "spread-radius" property tag.
//
// Used by ShadowProperty.
// Positive values will cause the shadow to expand and grow bigger, negative values will cause the shadow to shrink.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See SizeUnit description for more details.
SpreadRadius PropertyName = "spread-radius"
) )
// ViewShadow contains attributes of the view shadow // ShadowProperty contains attributes of the view shadow
type ViewShadow interface { type ShadowProperty interface {
Properties Properties
fmt.Stringer fmt.Stringer
stringWriter stringWriter
@ -37,34 +117,36 @@ type ViewShadow interface {
visible(session Session) bool visible(session Session) bool
} }
type viewShadowData struct { type shadowPropertyData struct {
propertyList dataProperty
} }
// NewViewShadow create the new shadow for a view. Arguments: // NewShadow create the new shadow property for a view. Arguments:
// offsetX, offsetY - x and y offset of the shadow // - offsetX, offsetY is x and y offset of the shadow (if the argument is specified as int or float64, the value is considered to be in pixels);
// blurRadius - the blur radius of the shadow // - blurRadius is the blur radius of the shadow (if the argument is specified as int or float64, the value is considered to be in pixels);
// spreadRadius - the spread radius of the shadow // - spreadRadius is the spread radius of the shadow (if the argument is specified as int or float64, the value is considered to be in pixels);
// color - the color of the shadow // - color is the color of the shadow.
func NewViewShadow(offsetX, offsetY, blurRadius, spreadRadius SizeUnit, color Color) ViewShadow { func NewShadow[xOffsetType SizeUnit | int | float64, yOffsetType SizeUnit | int | float64, blurType SizeUnit | int | float64, spreadType SizeUnit | int | float64](
return NewShadowWithParams(Params{ xOffset xOffsetType, yOffset yOffsetType, blurRadius blurType, spreadRadius spreadType, color Color) ShadowProperty {
XOffset: offsetX, return NewShadowProperty(Params{
YOffset: offsetY, XOffset: xOffset,
YOffset: yOffset,
BlurRadius: blurRadius, BlurRadius: blurRadius,
SpreadRadius: spreadRadius, SpreadRadius: spreadRadius,
ColorTag: color, ColorTag: color,
}) })
} }
// NewInsetViewShadow create the new inset shadow for a view. Arguments: // NewInsetShadow create the new inset shadow property for a view. Arguments:
// offsetX, offsetY - x and y offset of the shadow // - offsetX, offsetY is x and y offset of the shadow (if the argument is specified as int or float64, the value is considered to be in pixels);
// blurRadius - the blur radius of the shadow // - blurRadius is the blur radius of the shadow (if the argument is specified as int or float64, the value is considered to be in pixels);
// spreadRadius - the spread radius of the shadow // - spreadRadius is the spread radius of the shadow (if the argument is specified as int or float64, the value is considered to be in pixels);
// color - the color of the shadow // - color is the color of the shadow.
func NewInsetViewShadow(offsetX, offsetY, blurRadius, spreadRadius SizeUnit, color Color) ViewShadow { func NewInsetShadow[xOffsetType SizeUnit | int | float64, yOffsetType SizeUnit | int | float64, blurType SizeUnit | int | float64, spreadType SizeUnit | int | float64](
return NewShadowWithParams(Params{ xOffset xOffsetType, yOffset yOffsetType, blurRadius blurType, spreadRadius spreadType, color Color) ShadowProperty {
XOffset: offsetX, return NewShadowProperty(Params{
YOffset: offsetY, XOffset: xOffset,
YOffset: yOffset,
BlurRadius: blurRadius, BlurRadius: blurRadius,
SpreadRadius: spreadRadius, SpreadRadius: spreadRadius,
ColorTag: color, ColorTag: color,
@ -72,66 +154,57 @@ func NewInsetViewShadow(offsetX, offsetY, blurRadius, spreadRadius SizeUnit, col
}) })
} }
// NewTextShadow create the new text shadow. Arguments: // NewTextShadow create the new text shadow property. Arguments:
// offsetX, offsetY - x and y offset of the shadow // - offsetX, offsetY is the x- and y-offset of the shadow (if the argument is specified as int or float64, the value is considered to be in pixels);
// blurRadius - the blur radius of the shadow // - blurRadius is the blur radius of the shadow (if the argument is specified as int or float64, the value is considered to be in pixels);
// color - the color of the shadow // - color is the color of the shadow.
func NewTextShadow(offsetX, offsetY, blurRadius SizeUnit, color Color) ViewShadow { func NewTextShadow[xOffsetType SizeUnit | int | float64, yOffsetType SizeUnit | int | float64, blurType SizeUnit | int | float64](
return NewShadowWithParams(Params{ xOffset xOffsetType, yOffset yOffsetType, blurRadius blurType, color Color) ShadowProperty {
XOffset: offsetX, return NewShadowProperty(Params{
YOffset: offsetY, XOffset: xOffset,
YOffset: yOffset,
BlurRadius: blurRadius, BlurRadius: blurRadius,
ColorTag: color, ColorTag: color,
}) })
} }
// NewShadowWithParams create the new shadow for a view. // NewShadowProperty create the new shadow property for a view.
func NewShadowWithParams(params Params) ViewShadow { //
shadow := new(viewShadowData) // The following properties can be used:
shadow.propertyList.init() // - "color" (ColorTag). Determines the color of the shadow (Color);
// - "x-offset" (XOffset). Determines the shadow horizontal offset (SizeUnit);
// - "y-offset" (YOffset). Determines the shadow vertical offset (SizeUnit);
// - "blur" (BlurRadius). Determines the radius of the blur effect (SizeUnit);
// - "spread-radius" (SpreadRadius). Positive values (SizeUnit) will cause the shadow to expand and grow bigger, negative values will cause the shadow to shrink;
// - "inset" (Inset). Controls (bool) whether to draw shadow inside the frame or outside.
func NewShadowProperty(params Params) ShadowProperty {
shadow := new(shadowPropertyData)
shadow.init()
if params != nil { if params != nil {
for _, tag := range []string{ColorTag, Inset, XOffset, YOffset, BlurRadius, SpreadRadius} { for _, tag := range []PropertyName{ColorTag, Inset, XOffset, YOffset, BlurRadius, SpreadRadius} {
if value, ok := params[tag]; ok && value != nil { if value, ok := params[tag]; ok && value != nil {
shadow.Set(tag, value) shadow.set(shadow, tag, value)
} }
} }
} }
return shadow return shadow
} }
// parseViewShadow parse DataObject and create ViewShadow object // parseShadowProperty parse DataObject and create ShadowProperty object
func parseViewShadow(object DataObject) ViewShadow { func parseShadowProperty(object DataObject) ShadowProperty {
shadow := new(viewShadowData) shadow := new(shadowPropertyData)
shadow.propertyList.init() shadow.init()
parseProperties(shadow, object) parseProperties(shadow, object)
return shadow return shadow
} }
func (shadow *viewShadowData) Remove(tag string) { func (shadow *shadowPropertyData) init() {
delete(shadow.properties, strings.ToLower(tag)) shadow.dataProperty.init()
shadow.supportedProperties = []PropertyName{ColorTag, Inset, XOffset, YOffset, BlurRadius, SpreadRadius}
} }
func (shadow *viewShadowData) Set(tag string, value any) bool { func (shadow *shadowPropertyData) cssStyle(buffer *strings.Builder, session Session, lead string) bool {
if value == nil {
shadow.Remove(tag)
return true
}
tag = strings.ToLower(tag)
switch tag {
case ColorTag, Inset, XOffset, YOffset, BlurRadius, SpreadRadius:
return shadow.propertyList.Set(tag, value)
}
ErrorLogF(`"%s" property is not supported by Shadow`, tag)
return false
}
func (shadow *viewShadowData) Get(tag string) any {
return shadow.propertyList.Get(strings.ToLower(tag))
}
func (shadow *viewShadowData) cssStyle(buffer *strings.Builder, session Session, lead string) bool {
color, _ := colorProperty(shadow, ColorTag, session) color, _ := colorProperty(shadow, ColorTag, session)
offsetX, _ := sizeProperty(shadow, XOffset, session) offsetX, _ := sizeProperty(shadow, XOffset, session)
offsetY, _ := sizeProperty(shadow, YOffset, session) offsetY, _ := sizeProperty(shadow, YOffset, session)
@ -163,7 +236,7 @@ func (shadow *viewShadowData) cssStyle(buffer *strings.Builder, session Session,
return true return true
} }
func (shadow *viewShadowData) cssTextStyle(buffer *strings.Builder, session Session, lead string) bool { func (shadow *shadowPropertyData) cssTextStyle(buffer *strings.Builder, session Session, lead string) bool {
color, _ := colorProperty(shadow, ColorTag, session) color, _ := colorProperty(shadow, ColorTag, session)
offsetX, _ := sizeProperty(shadow, XOffset, session) offsetX, _ := sizeProperty(shadow, XOffset, session)
offsetY, _ := sizeProperty(shadow, YOffset, session) offsetY, _ := sizeProperty(shadow, YOffset, session)
@ -187,7 +260,7 @@ func (shadow *viewShadowData) cssTextStyle(buffer *strings.Builder, session Sess
return true return true
} }
func (shadow *viewShadowData) visible(session Session) bool { func (shadow *shadowPropertyData) visible(session Session) bool {
color, _ := colorProperty(shadow, ColorTag, session) color, _ := colorProperty(shadow, ColorTag, session)
offsetX, _ := sizeProperty(shadow, XOffset, session) offsetX, _ := sizeProperty(shadow, XOffset, session)
offsetY, _ := sizeProperty(shadow, YOffset, session) offsetY, _ := sizeProperty(shadow, YOffset, session)
@ -204,62 +277,45 @@ func (shadow *viewShadowData) visible(session Session) bool {
return true return true
} }
func (shadow *viewShadowData) String() string { func (shadow *shadowPropertyData) String() string {
return runStringWriter(shadow) return runStringWriter(shadow)
} }
func (shadow *viewShadowData) writeString(buffer *strings.Builder, indent string) { func setShadowProperty(properties Properties, tag PropertyName, value any) bool {
buffer.WriteString("_{ ")
comma := false
for _, tag := range shadow.AllTags() {
if value, ok := shadow.properties[tag]; ok {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(" = ")
writePropertyValue(buffer, tag, value, indent)
comma = true
}
}
buffer.WriteString(" }")
}
func (properties *propertyList) setShadow(tag string, value any) bool {
if value == nil { if value == nil {
delete(properties.properties, tag) properties.setRaw(tag, nil)
return true return true
} }
switch value := value.(type) { switch value := value.(type) {
case ViewShadow: case ShadowProperty:
properties.properties[tag] = []ViewShadow{value} properties.setRaw(tag, []ShadowProperty{value})
case []ViewShadow: case []ShadowProperty:
if len(value) == 0 { if len(value) == 0 {
delete(properties.properties, tag) properties.setRaw(tag, nil)
} else { } else {
properties.properties[tag] = value properties.setRaw(tag, value)
} }
case DataValue: case DataValue:
if !value.IsObject() { if !value.IsObject() {
return false return false
} }
properties.properties[tag] = []ViewShadow{parseViewShadow(value.Object())} properties.setRaw(tag, []ShadowProperty{parseShadowProperty(value.Object())})
case []DataValue: case []DataValue:
shadows := []ViewShadow{} shadows := []ShadowProperty{}
for _, data := range value { for _, data := range value {
if data.IsObject() { if data.IsObject() {
shadows = append(shadows, parseViewShadow(data.Object())) shadows = append(shadows, parseShadowProperty(data.Object()))
} }
} }
if len(shadows) == 0 { if len(shadows) == 0 {
return false return false
} }
properties.properties[tag] = shadows properties.setRaw(tag, shadows)
case string: case string:
obj := NewDataObject(value) obj := NewDataObject(value)
@ -267,7 +323,7 @@ func (properties *propertyList) setShadow(tag string, value any) bool {
notCompatibleType(tag, value) notCompatibleType(tag, value)
return false return false
} }
properties.properties[tag] = []ViewShadow{parseViewShadow(obj)} properties.setRaw(tag, []ShadowProperty{parseShadowProperty(obj)})
default: default:
notCompatibleType(tag, value) notCompatibleType(tag, value)
@ -277,20 +333,20 @@ func (properties *propertyList) setShadow(tag string, value any) bool {
return true return true
} }
func getShadows(properties Properties, tag string) []ViewShadow { func getShadows(properties Properties, tag PropertyName) []ShadowProperty {
if value := properties.Get(tag); value != nil { if value := properties.Get(tag); value != nil {
switch value := value.(type) { switch value := value.(type) {
case []ViewShadow: case []ShadowProperty:
return value return value
case ViewShadow: case ShadowProperty:
return []ViewShadow{value} return []ShadowProperty{value}
} }
} }
return []ViewShadow{} return []ShadowProperty{}
} }
func shadowCSS(properties Properties, tag string, session Session) string { func shadowCSS(properties Properties, tag PropertyName, session Session) string {
shadows := getShadows(properties, tag) shadows := getShadows(properties, tag)
if len(shadows) == 0 { if len(shadows) == 0 {
return "" return ""

View File

@ -7,11 +7,15 @@ import (
) )
// SizeFunc describes a function that calculates the SizeUnit size. // SizeFunc describes a function that calculates the SizeUnit size.
//
// Used as the value of the SizeUnit properties. // Used as the value of the SizeUnit properties.
// "min", "max", "clamp", "sum", "sub", "mul", and "div" functions are available. //
// "min", "max", "clamp", "sum", "sub", "mul", "div", mod,
// "round", "round-up", "round-down" and "round-to-zero" functions are available.
type SizeFunc interface { type SizeFunc interface {
fmt.Stringer fmt.Stringer
// Name() returns the function name: "min", "max", "clamp", "sum", "sub", "mul", or "div" // Name() returns the function name: "min", "max", "clamp", "sum", "sub", "mul",
// "div", "mod", "rem", "round", "round-up", "round-down" or "round-to-zero"
Name() string Name() string
// Args() returns a list of function arguments // Args() returns a list of function arguments
Args() []any Args() []any
@ -28,7 +32,9 @@ type sizeFuncData struct {
func parseSizeFunc(text string) SizeFunc { func parseSizeFunc(text string) SizeFunc {
text = strings.Trim(text, " ") text = strings.Trim(text, " ")
for _, tag := range []string{"min", "max", "sum", "sub", "mul", "div", "clamp"} { for _, tag := range []string{
"min", "max", "sum", "sub", "mul", "div", "mod", "rem", "clamp",
"round-up", "round-down", "round-to-zero", "round"} {
if strings.HasPrefix(text, tag) { if strings.HasPrefix(text, tag) {
text = strings.Trim(strings.TrimPrefix(text, tag), " ") text = strings.Trim(strings.TrimPrefix(text, tag), " ")
last := len(text) - 1 last := len(text) - 1
@ -59,7 +65,7 @@ func parseSizeFunc(text string) SizeFunc {
args = append(args, text[start:]) args = append(args, text[start:])
switch tag { switch tag {
case "sub", "mul", "div": case "sub", "mul", "div", "mod", "rem", "round-up", "round-down", "round-to-zero", "round":
if len(args) != 2 { if len(args) != 2 {
ErrorLogF(`"%s" function needs 2 arguments`, tag) ErrorLogF(`"%s" function needs 2 arguments`, tag)
return nil return nil
@ -73,7 +79,9 @@ func parseSizeFunc(text string) SizeFunc {
data := new(sizeFuncData) data := new(sizeFuncData)
data.tag = tag data.tag = tag
if data.parseArgs(args, tag == "mul" || tag == "div") { if data.parseArgs(args, tag == "mul" || tag == "div" || tag == "mod" ||
tag == "rem" || tag == "round-up" || tag == "round-down" ||
tag == "round-to-zero" || tag == "round") {
return data return data
} }
} }
@ -92,19 +100,25 @@ func (data *sizeFuncData) parseArgs(args []any, allowNumber bool) bool {
numberArg := func(index int, value float64) bool { numberArg := func(index int, value float64) bool {
if allowNumber { if allowNumber {
if index == 1 { if index == 1 {
if value == 0 && data.tag == "div" { if value == 0 {
ErrorLog(`Division by 0 in div function`) if data.tag == "div" || data.tag == "mod" {
return false ErrorLogF(`Division by 0 in "%s" function`, data.tag)
return false
}
if data.tag == "round" || data.tag == "round-up" ||
data.tag == "round-down" || data.tag == "round-to-zero" {
ErrorLogF(`The rounding interval is 0 in "%s" function`, data.tag)
return false
}
} }
data.args = append(data.args, value) data.args = append(data.args, value)
return true return true
} else { } else {
ErrorLogF(`Only the second %s function argument can be a number`, data.tag) ErrorLogF(`Only the second %s function argument can be a number`, data.tag)
return false
} }
} else {
ErrorLogF(`The %s function argument can't be a number`, data.tag)
} }
ErrorLogF(`The %s function argument cann't be a number`, data.tag)
return false return false
} }
@ -116,7 +130,7 @@ func (data *sizeFuncData) parseArgs(args []any, allowNumber bool) bool {
return false return false
} }
if arg[0] == '@' { if ok, _ := isConstantName(arg); ok {
data.args = append(data.args, arg) data.args = append(data.args, arg)
} else if val, err := strconv.ParseFloat(arg, 64); err == nil { } else if val, err := strconv.ParseFloat(arg, 64); err == nil {
return numberArg(i, val) return numberArg(i, val)
@ -194,7 +208,7 @@ func (data *sizeFuncData) writeString(topFunc string, buffer *strings.Builder) {
buffer.WriteString(arg.String()) buffer.WriteString(arg.String())
case float64: case float64:
buffer.WriteString(fmt.Sprintf("%g", arg)) fmt.Fprintf(buffer, "%g", arg)
} }
} }
@ -219,7 +233,7 @@ func (data *sizeFuncData) writeCSS(topFunc string, buffer *strings.Builder, sess
case "": case "":
buffer.WriteString("calc(") buffer.WriteString("calc(")
case "min", "max", "clamp": case "min", "max", "clamp", "mod", "rem", "round", "round-up", "round-down", "round-to-zero":
bracket = false bracket = false
default: default:
@ -228,10 +242,22 @@ func (data *sizeFuncData) writeCSS(topFunc string, buffer *strings.Builder, sess
} }
switch data.tag { switch data.tag {
case "min", "max", "clamp": case "min", "max", "clamp", "mod", "rem":
buffer.WriteString(data.tag) buffer.WriteString(data.tag)
buffer.WriteRune('(') buffer.WriteRune('(')
case "round":
buffer.WriteString("round(nearest, ")
case "round-up":
buffer.WriteString("round(up, ")
case "round-down":
buffer.WriteString("round(down, ")
case "round-to-zero":
buffer.WriteString("round(to-zero, ")
case "sum": case "sum":
mathFunc(" + ") mathFunc(" + ")
@ -276,7 +302,7 @@ func (data *sizeFuncData) writeCSS(topFunc string, buffer *strings.Builder, sess
buffer.WriteString(arg.String()) buffer.WriteString(arg.String())
case float64: case float64:
buffer.WriteString(fmt.Sprintf("%g", arg)) fmt.Fprintf(buffer, "%g", arg)
} }
} }
@ -287,7 +313,7 @@ func (data *sizeFuncData) writeCSS(topFunc string, buffer *strings.Builder, sess
} }
// MaxSize creates a SizeUnit function that calculates the maximum argument. // MaxSize creates a SizeUnit function that calculates the maximum argument.
// Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc // Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc
func MaxSize(arg0, arg1 any, args ...any) SizeFunc { func MaxSize(arg0, arg1 any, args ...any) SizeFunc {
data := new(sizeFuncData) data := new(sizeFuncData)
data.tag = "max" data.tag = "max"
@ -298,7 +324,7 @@ func MaxSize(arg0, arg1 any, args ...any) SizeFunc {
} }
// MinSize creates a SizeUnit function that calculates the minimum argument. // MinSize creates a SizeUnit function that calculates the minimum argument.
// Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc. // Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
func MinSize(arg0, arg1 any, args ...any) SizeFunc { func MinSize(arg0, arg1 any, args ...any) SizeFunc {
data := new(sizeFuncData) data := new(sizeFuncData)
data.tag = "min" data.tag = "min"
@ -309,7 +335,7 @@ func MinSize(arg0, arg1 any, args ...any) SizeFunc {
} }
// SumSize creates a SizeUnit function that calculates the sum of arguments. // SumSize creates a SizeUnit function that calculates the sum of arguments.
// Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc. // Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
func SumSize(arg0, arg1 any, args ...any) SizeFunc { func SumSize(arg0, arg1 any, args ...any) SizeFunc {
data := new(sizeFuncData) data := new(sizeFuncData)
data.tag = "sum" data.tag = "sum"
@ -320,7 +346,7 @@ func SumSize(arg0, arg1 any, args ...any) SizeFunc {
} }
// SumSize creates a SizeUnit function that calculates the result of subtracting the arguments (arg1 - arg2). // SumSize creates a SizeUnit function that calculates the result of subtracting the arguments (arg1 - arg2).
// Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc. // Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
func SubSize(arg0, arg1 any) SizeFunc { func SubSize(arg0, arg1 any) SizeFunc {
data := new(sizeFuncData) data := new(sizeFuncData)
data.tag = "sub" data.tag = "sub"
@ -331,7 +357,7 @@ func SubSize(arg0, arg1 any) SizeFunc {
} }
// MulSize creates a SizeUnit function that calculates the result of multiplying the arguments (arg1 * arg2). // MulSize creates a SizeUnit function that calculates the result of multiplying the arguments (arg1 * arg2).
// Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc. // Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64) // The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64)
// or a string which is a text representation of a number. // or a string which is a text representation of a number.
func MulSize(arg0, arg1 any) SizeFunc { func MulSize(arg0, arg1 any) SizeFunc {
@ -344,7 +370,7 @@ func MulSize(arg0, arg1 any) SizeFunc {
} }
// DivSize creates a SizeUnit function that calculates the result of dividing the arguments (arg1 / arg2). // DivSize creates a SizeUnit function that calculates the result of dividing the arguments (arg1 / arg2).
// Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc. // Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64) // The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64)
// or a string which is a text representation of a number. // or a string which is a text representation of a number.
func DivSize(arg0, arg1 any) SizeFunc { func DivSize(arg0, arg1 any) SizeFunc {
@ -356,13 +382,103 @@ func DivSize(arg0, arg1 any) SizeFunc {
return data return data
} }
// RemSize creates a SizeUnit function that calculates the remainder of a division operation
// with the same sign as the dividend (arg1 % arg2).
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64)
// or a string which is a text representation of a number.
func RemSize(arg0, arg1 any) SizeFunc {
data := new(sizeFuncData)
data.tag = "rem"
if !data.parseArgs([]any{arg0, arg1}, true) {
return nil
}
return data
}
// ModSize creates a SizeUnit function that calculates the remainder of a division operation
// with the same sign as the divisor (arg1 % arg2).
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64)
// or a string which is a text representation of a number.
func ModSize(arg0, arg1 any) SizeFunc {
data := new(sizeFuncData)
data.tag = "mod"
if !data.parseArgs([]any{arg0, arg1}, true) {
return nil
}
return data
}
// RoundSize creates a SizeUnit function that calculates a rounded number.
// The function rounds valueToRound (first argument) to the nearest integer multiple
// of roundingInterval (second argument), which may be either above or below the value.
// If the valueToRound is half way between the rounding targets above and below (neither is "nearest"), it will be rounded up.
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64)
// or a string which is a text representation of a number.
func RoundSize(valueToRound, roundingInterval any) SizeFunc {
data := new(sizeFuncData)
data.tag = "round"
if !data.parseArgs([]any{valueToRound, roundingInterval}, true) {
return nil
}
return data
}
// RoundUpSize creates a SizeUnit function that calculates a rounded number.
// The function rounds valueToRound (first argument) up to the nearest integer multiple
// of roundingInterval (second argument) (if the value is negative, it will become "more positive").
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64)
// or a string which is a text representation of a number.
func RoundUpSize(valueToRound, roundingInterval any) SizeFunc {
data := new(sizeFuncData)
data.tag = "round-up"
if !data.parseArgs([]any{valueToRound, roundingInterval}, true) {
return nil
}
return data
}
// RoundDownSize creates a SizeUnit function that calculates a rounded number.
// The function rounds valueToRound (first argument) down to the nearest integer multiple
// of roundingInterval (second argument) (if the value is negative, it will become "more negative").
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64)
// or a string which is a text representation of a number.
func RoundDownSize(valueToRound, roundingInterval any) SizeFunc {
data := new(sizeFuncData)
data.tag = "round-down"
if !data.parseArgs([]any{valueToRound, roundingInterval}, true) {
return nil
}
return data
}
// RoundToZeroSize creates a SizeUnit function that calculates a rounded number.
// The function rounds valueToRound (first argument) to the nearest integer multiple
// of roundingInterval (second argument), which may be either above or below the value.
// If the valueToRound is half way between the rounding targets above and below.
// Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
// The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64)
// or a string which is a text representation of a number.
func RoundToZeroSize(valueToRound, roundingInterval any) SizeFunc {
data := new(sizeFuncData)
data.tag = "round-to-zero"
if !data.parseArgs([]any{valueToRound, roundingInterval}, true) {
return nil
}
return data
}
// ClampSize creates a SizeUnit function whose the result is calculated as follows: // ClampSize creates a SizeUnit function whose the result is calculated as follows:
// //
// min ≤ value ≤ max -> value; // min ≤ value ≤ max -> value;
// value < min -> min; // value < min -> min;
// max < value -> max; // max < value -> max;
// //
// Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc. // Valid arguments types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc.
func ClampSize(min, value, max any) SizeFunc { func ClampSize(min, value, max any) SizeFunc {
data := new(sizeFuncData) data := new(sizeFuncData)
data.tag = "clamp" data.tag = "clamp"

View File

@ -13,6 +13,7 @@ import (
// SizeInDIP, SizeInPt, SizeInInch, SizeInMM, SizeInFraction // SizeInDIP, SizeInPt, SizeInInch, SizeInMM, SizeInFraction
type SizeUnitType uint8 type SizeUnitType uint8
// Constants which represent values of a [SizeUnitType]
const ( const (
// Auto is the SizeUnit type: default value. // Auto is the SizeUnit type: default value.
Auto SizeUnitType = 0 Auto SizeUnitType = 0
@ -44,8 +45,14 @@ const (
// SizeUnit describe a size (Value field) and size unit (Type field). // SizeUnit describe a size (Value field) and size unit (Type field).
type SizeUnit struct { type SizeUnit struct {
Type SizeUnitType // Type or dimension of the value
Value float64 Type SizeUnitType
// Value of the size in Type units
Value float64
// Function representation of a size unit.
// When setting this value type should be set to SizeFunction
Function SizeFunc Function SizeFunc
} }
@ -55,53 +62,53 @@ func AutoSize() SizeUnit {
} }
// Px creates SizeUnit with SizeInPixel type // Px creates SizeUnit with SizeInPixel type
func Px(value float64) SizeUnit { func Px[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{SizeInPixel, value, nil} return SizeUnit{Type: SizeInPixel, Value: float64(value), Function: nil}
} }
// Em creates SizeUnit with SizeInEM type // Em creates SizeUnit with SizeInEM type
func Em(value float64) SizeUnit { func Em[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{SizeInEM, value, nil} return SizeUnit{Type: SizeInEM, Value: float64(value), Function: nil}
} }
// Ex creates SizeUnit with SizeInEX type // Ex creates SizeUnit with SizeInEX type
func Ex(value float64) SizeUnit { func Ex[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{SizeInEX, value, nil} return SizeUnit{Type: SizeInEX, Value: float64(value), Function: nil}
} }
// Percent creates SizeUnit with SizeInDIP type // Percent creates SizeUnit with SizeInDIP type
func Percent(value float64) SizeUnit { func Percent[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{SizeInPercent, value, nil} return SizeUnit{Type: SizeInPercent, Value: float64(value), Function: nil}
} }
// Pt creates SizeUnit with SizeInPt type // Pt creates SizeUnit with SizeInPt type
func Pt(value float64) SizeUnit { func Pt[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{SizeInPt, value, nil} return SizeUnit{Type: SizeInPt, Value: float64(value), Function: nil}
} }
// Pc creates SizeUnit with SizeInPc type // Pc creates SizeUnit with SizeInPc type
func Pc(value float64) SizeUnit { func Pc[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{SizeInPc, value, nil} return SizeUnit{Type: SizeInPc, Value: float64(value), Function: nil}
} }
// Mm creates SizeUnit with SizeInMM type // Mm creates SizeUnit with SizeInMM type
func Mm(value float64) SizeUnit { func Mm[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{SizeInMM, value, nil} return SizeUnit{Type: SizeInMM, Value: float64(value), Function: nil}
} }
// Cm creates SizeUnit with SizeInCM type // Cm creates SizeUnit with SizeInCM type
func Cm(value float64) SizeUnit { func Cm[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{SizeInCM, value, nil} return SizeUnit{Type: SizeInCM, Value: float64(value), Function: nil}
} }
// Inch creates SizeUnit with SizeInInch type // Inch creates SizeUnit with SizeInInch type
func Inch(value float64) SizeUnit { func Inch[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{SizeInInch, value, nil} return SizeUnit{Type: SizeInInch, Value: float64(value), Function: nil}
} }
// Fr creates SizeUnit with SizeInFraction type // Fr creates SizeUnit with SizeInFraction type
func Fr(value float64) SizeUnit { func Fr[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{SizeInFraction, value, nil} return SizeUnit{SizeInFraction, float64(value), nil}
} }
// Equal compare two SizeUnit. Return true if SizeUnit are equal // Equal compare two SizeUnit. Return true if SizeUnit are equal

File diff suppressed because it is too large Load Diff

View File

@ -9,13 +9,13 @@ import (
var stringResources = map[string]map[string]string{} var stringResources = map[string]map[string]string{}
func scanEmbedStringsDir(fs *embed.FS, dir string) { func (resources *resourceManager) scanEmbedStringsDir(fs *embed.FS, dir string) {
if files, err := fs.ReadDir(dir); err == nil { if files, err := fs.ReadDir(dir); err == nil {
for _, file := range files { for _, file := range files {
name := file.Name() name := file.Name()
path := dir + "/" + name path := dir + "/" + name
if file.IsDir() { if file.IsDir() {
scanEmbedStringsDir(fs, path) resources.scanEmbedStringsDir(fs, path)
} else if strings.ToLower(filepath.Ext(name)) == ".rui" { } else if strings.ToLower(filepath.Ext(name)) == ".rui" {
if data, err := fs.ReadFile(path); err == nil { if data, err := fs.ReadFile(path); err == nil {
loadStringResources(string(data)) loadStringResources(string(data))
@ -27,14 +27,14 @@ func scanEmbedStringsDir(fs *embed.FS, dir string) {
} }
} }
func scanStringsDir(path string) { func (resources *resourceManager) scanStringsDir(path string) {
if files, err := os.ReadDir(path); err == nil { if files, err := os.ReadDir(path); err == nil {
for _, file := range files { for _, file := range files {
filename := file.Name() filename := file.Name()
if filename[0] != '.' { if filename[0] != '.' {
newPath := path + `/` + filename newPath := path + `/` + filename
if file.IsDir() { if file.IsDir() {
scanStringsDir(newPath) resources.scanStringsDir(newPath)
} else if strings.ToLower(filepath.Ext(newPath)) == ".rui" { } else if strings.ToLower(filepath.Ext(newPath)) == ".rui" {
if data, err := os.ReadFile(newPath); err == nil { if data, err := os.ReadFile(newPath); err == nil {
loadStringResources(string(data)) loadStringResources(string(data))
@ -50,8 +50,9 @@ func scanStringsDir(path string) {
} }
func loadStringResources(text string) { func loadStringResources(text string) {
data := ParseDataText(text) data, err := ParseDataText(text)
if data == nil { if err != nil {
ErrorLog(err.Error())
return return
} }
@ -61,8 +62,8 @@ func loadStringResources(text string) {
table = map[string]string{} table = map[string]string{}
} }
for i := 0; i < obj.PropertyCount(); i++ { for prop := range obj.Properties() {
if prop := obj.Property(i); prop != nil && prop.Type() == TextNode { if prop.Type() == TextNode {
table[prop.Tag()] = prop.Text() table[prop.Tag()] = prop.Text()
} }
} }
@ -72,8 +73,8 @@ func loadStringResources(text string) {
tag := data.Tag() tag := data.Tag()
if tag == "strings" { if tag == "strings" {
for i := 0; i < data.PropertyCount(); i++ { for prop := range data.Properties() {
if prop := data.Property(i); prop != nil && prop.Type() == ObjectNode { if prop.Type() == ObjectNode {
parseStrings(prop.Object(), prop.Tag()) parseStrings(prop.Object(), prop.Tag())
} }
} }

167
svgImageView.go Normal file
View File

@ -0,0 +1,167 @@
package rui
import (
"io"
"net/http"
"os"
"strings"
)
// SvgImageView represents an SvgImageView view
type SvgImageView interface {
View
}
type svgImageViewData struct {
viewData
}
// NewSvgImageView create new SvgImageView object and return it
func NewSvgImageView(session Session, params Params) SvgImageView {
view := new(svgImageViewData)
view.init(session)
setInitParams(view, params)
return view
}
func newSvgImageView(session Session) View {
return new(svgImageViewData) // NewSvgImageView(session, nil)
}
// Init initialize fields of imageView by default values
func (imageView *svgImageViewData) init(session Session) {
imageView.viewData.init(session)
imageView.tag = "SvgImageView"
imageView.systemClass = "ruiSvgImageView"
imageView.normalize = normalizeSvgImageViewTag
imageView.set = imageView.setFunc
imageView.changed = imageView.propertyChanged
}
func normalizeSvgImageViewTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Source, "source":
tag = Content
case VerticalAlign:
tag = CellVerticalAlign
case HorizontalAlign:
tag = CellHorizontalAlign
}
return tag
}
func (imageView *svgImageViewData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case Content:
if text, ok := value.(string); ok {
imageView.setRaw(Content, text)
return []PropertyName{tag}
}
notCompatibleType(Source, value)
return nil
default:
return imageView.viewData.setFunc(tag, value)
}
}
func (imageView *svgImageViewData) propertyChanged(tag PropertyName) {
switch tag {
case Content:
updateInnerHTML(imageView.htmlID(), imageView.Session())
default:
imageView.viewData.propertyChanged(tag)
}
}
func (imageView *svgImageViewData) htmlTag() string {
return "div"
}
func (imageView *svgImageViewData) writeSvg(data []byte, buffer *strings.Builder) {
text := string(data)
index := strings.Index(text, "<svg")
if index > 0 {
text = text[index:]
}
index = strings.Index(text, "\n")
for index >= 0 {
if index > 0 && text[index-1] == '\r' {
buffer.WriteString(text[:index-1])
} else {
buffer.WriteString(text[:index])
}
end := len(text)
index++
for index < end && (text[index] == ' ' || text[index] == '\t' || text[index] == '\r' || text[index] == '\n') {
index++
}
text = text[index:]
index = strings.Index(text, "\n")
}
buffer.WriteString(text)
}
func (imageView *svgImageViewData) htmlSubviews(self View, buffer *strings.Builder) {
if value := imageView.getRaw(Content); value != nil {
if content, ok := value.(string); ok && content != "" {
if strings.HasPrefix(content, "@") {
if name, ok := imageView.session.ImageConstant(content[1:]); ok {
content = name
}
}
if image, ok := resources.images[content]; ok {
if image.fs != nil {
if data, err := image.fs.ReadFile(image.path); err == nil {
imageView.writeSvg(data, buffer)
return
} else {
DebugLog(err.Error())
}
} else if data, err := os.ReadFile(image.path); err == nil {
imageView.writeSvg(data, buffer)
return
} else {
DebugLog(err.Error())
}
}
if strings.HasPrefix(content, "http://") || strings.HasPrefix(content, "https://") {
resp, err := http.Get(content)
if err == nil {
defer resp.Body.Close()
if body, err := io.ReadAll(resp.Body); err == nil {
imageView.writeSvg(body, buffer)
return
}
}
DebugLog(err.Error())
}
buffer.WriteString(content)
}
}
}
// GetSvgImageViewVerticalAlign return the vertical align of an SvgImageView subview: TopAlign (0), BottomAlign (1), CenterAlign (2)
// If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned
func GetSvgImageViewVerticalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, CellVerticalAlign, LeftAlign, false)
}
// GetSvgImageViewHorizontalAlign return the vertical align of an SvgImageView subview: LeftAlign (0), RightAlign (1), CenterAlign (2)
// If the second argument (subviewID) is not specified or it is "" then a left position of the first argument (view) is returned
func GetSvgImageViewHorizontalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, CellHorizontalAlign, LeftAlign, false)
}

View File

@ -1,6 +1,6 @@
package rui package rui
// TableAdapter describes the TableView content // TableAdapter describes the [TableView] content
type TableAdapter interface { type TableAdapter interface {
// RowCount returns number of rows in the table // RowCount returns number of rows in the table
RowCount() int RowCount() int
@ -9,57 +9,70 @@ type TableAdapter interface {
ColumnCount() int ColumnCount() int
// Cell returns the contents of a table cell. The function can return elements of the following types: // Cell returns the contents of a table cell. The function can return elements of the following types:
// * string // - string
// * rune // - rune
// * float32, float64 // - float32, float64
// * integer values: int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64 // - integer values: int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64
// * bool // - bool
// * rui.Color // - rui.Color
// * rui.View // - rui.View
// * fmt.Stringer // - fmt.Stringer
// * rui.VerticalTableJoin, rui.HorizontalTableJoin // - rui.VerticalTableJoin, rui.HorizontalTableJoin
Cell(row, column int) any Cell(row, column int) any
} }
// TableColumnStyle describes the style of TableView columns. // TableColumnStyle describes the style of [TableView] columns.
// To set column styles, you must either implement the TableColumnStyle interface in the table adapter //
// To set column styles, you must either implement the [TableColumnStyle] interface in the table adapter
// or assign its separate implementation to the "column-style" property. // or assign its separate implementation to the "column-style" property.
type TableColumnStyle interface { type TableColumnStyle interface {
// ColumnStyle returns a map of properties which describe the style of the column
ColumnStyle(column int) Params ColumnStyle(column int) Params
} }
// TableRowStyle describes the style of TableView rows. // TableRowStyle describes the style of [TableView] rows.
// To set row styles, you must either implement the TableRowStyle interface in the table adapter //
// To set row styles, you must either implement the [TableRowStyle] interface in the table adapter
// or assign its separate implementation to the "row-style" property. // or assign its separate implementation to the "row-style" property.
type TableRowStyle interface { type TableRowStyle interface {
// RowStyle returns a map of properties which describe the style of the row
RowStyle(row int) Params RowStyle(row int) Params
} }
// TableCellStyle describes the style of TableView cells. // TableCellStyle describes the style of [TableView] cells.
// To set row cells, you must either implement the TableCellStyle interface in the table adapter //
// To set row cells, you must either implement the [TableCellStyle] interface in the table adapter
// or assign its separate implementation to the "cell-style" property. // or assign its separate implementation to the "cell-style" property.
type TableCellStyle interface { type TableCellStyle interface {
// CellStyle returns a map of properties which describe the style of the cell
CellStyle(row, column int) Params CellStyle(row, column int) Params
} }
// TableAllowCellSelection determines whether TableView cell selection is allowed. // TableAllowCellSelection determines whether [TableView] cell selection is allowed.
//
// It is only used if the "selection-mode" property is set to CellSelection (1). // It is only used if the "selection-mode" property is set to CellSelection (1).
//
// To set cell selection allowing, you must either implement the TableAllowCellSelection interface // To set cell selection allowing, you must either implement the TableAllowCellSelection interface
// in the table adapter or assign its separate implementation to the "allow-selection" property. // in the table adapter or assign its separate implementation to the "allow-selection" property.
type TableAllowCellSelection interface { type TableAllowCellSelection interface {
// AllowCellSelection returns "true" if we allow the user to select particular cell at specific rows and columns
AllowCellSelection(row, column int) bool AllowCellSelection(row, column int) bool
} }
// TableAllowRowSelection determines whether TableView row selection is allowed. // TableAllowRowSelection determines whether [TableView] row selection is allowed.
//
// It is only used if the "selection-mode" property is set to RowSelection (2). // It is only used if the "selection-mode" property is set to RowSelection (2).
//
// To set row selection allowing, you must either implement the TableAllowRowSelection interface // To set row selection allowing, you must either implement the TableAllowRowSelection interface
// in the table adapter or assign its separate implementation to the "allow-selection" property. // in the table adapter or assign its separate implementation to the "allow-selection" property.
type TableAllowRowSelection interface { type TableAllowRowSelection interface {
// AllowRowSelection returns "true" if we allow the user to select particular row in the table
AllowRowSelection(row int) bool AllowRowSelection(row int) bool
} }
// SimpleTableAdapter is implementation of TableAdapter where the content // SimpleTableAdapter is implementation of [TableAdapter] where the content
// defines as [][]any. // defines as [][]any.
//
// When you assign [][]any value to the "content" property, it is converted to SimpleTableAdapter // When you assign [][]any value to the "content" property, it is converted to SimpleTableAdapter
type SimpleTableAdapter interface { type SimpleTableAdapter interface {
TableAdapter TableAdapter
@ -71,7 +84,7 @@ type simpleTableAdapter struct {
columnCount int columnCount int
} }
// TextTableAdapter is implementation of TableAdapter where the content // TextTableAdapter is implementation of [TableAdapter] where the content
// defines as [][]string. // defines as [][]string.
// When you assign [][]string value to the "content" property, it is converted to TextTableAdapter // When you assign [][]string value to the "content" property, it is converted to TextTableAdapter
type TextTableAdapter interface { type TextTableAdapter interface {
@ -228,11 +241,21 @@ func (adapter *textTableAdapter) Cell(row, column int) any {
return nil return nil
} }
type simpleTableRowStyle struct { type simpleTableLineStyle struct {
params []Params params []Params
} }
func (style *simpleTableRowStyle) RowStyle(row int) Params { func (style *simpleTableLineStyle) ColumnStyle(column int) Params {
if column < len(style.params) {
params := style.params[column]
if len(params) > 0 {
return params
}
}
return nil
}
func (style *simpleTableLineStyle) RowStyle(row int) Params {
if row < len(style.params) { if row < len(style.params) {
params := style.params[row] params := style.params[row]
if len(params) > 0 { if len(params) > 0 {
@ -241,154 +264,3 @@ func (style *simpleTableRowStyle) RowStyle(row int) Params {
} }
return nil return nil
} }
func (table *tableViewData) setRowStyle(value any) bool {
newSimpleTableRowStyle := func(params []Params) TableRowStyle {
if len(params) == 0 {
return nil
}
result := new(simpleTableRowStyle)
result.params = params
return result
}
switch value := value.(type) {
case TableRowStyle:
table.properties[RowStyle] = value
case []Params:
if style := newSimpleTableRowStyle(value); style != nil {
table.properties[RowStyle] = style
} else {
delete(table.properties, RowStyle)
}
case DataNode:
if value.Type() == ArrayNode {
params := make([]Params, value.ArraySize())
for i, element := range value.ArrayElements() {
params[i] = Params{}
if element.IsObject() {
obj := element.Object()
for k := 0; k < obj.PropertyCount(); k++ {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
params[i][prop.Tag()] = prop.Text()
}
}
} else {
params[i][Style] = element.Value()
}
}
if style := newSimpleTableRowStyle(params); style != nil {
table.properties[RowStyle] = style
} else {
delete(table.properties, RowStyle)
}
} else {
return false
}
default:
return false
}
return true
}
func (table *tableViewData) getRowStyle() TableRowStyle {
for _, tag := range []string{RowStyle, Content} {
if value := table.getRaw(tag); value != nil {
if style, ok := value.(TableRowStyle); ok {
return style
}
}
}
return nil
}
type simpleTableColumnStyle struct {
params []Params
}
func (style *simpleTableColumnStyle) ColumnStyle(row int) Params {
if row < len(style.params) {
params := style.params[row]
if len(params) > 0 {
return params
}
}
return nil
}
func (table *tableViewData) setColumnStyle(value any) bool {
newSimpleTableColumnStyle := func(params []Params) TableColumnStyle {
if len(params) == 0 {
return nil
}
result := new(simpleTableColumnStyle)
result.params = params
return result
}
switch value := value.(type) {
case TableColumnStyle:
table.properties[ColumnStyle] = value
case []Params:
if style := newSimpleTableColumnStyle(value); style != nil {
table.properties[ColumnStyle] = style
} else {
delete(table.properties, ColumnStyle)
}
case DataNode:
if value.Type() == ArrayNode {
params := make([]Params, value.ArraySize())
for i, element := range value.ArrayElements() {
params[i] = Params{}
if element.IsObject() {
obj := element.Object()
for k := 0; k < obj.PropertyCount(); k++ {
if prop := obj.Property(k); prop != nil && prop.Type() == TextNode {
params[i][prop.Tag()] = prop.Text()
}
}
} else {
params[i][Style] = element.Value()
}
}
if style := newSimpleTableColumnStyle(params); style != nil {
table.properties[ColumnStyle] = style
} else {
delete(table.properties, ColumnStyle)
}
} else {
return false
}
default:
return false
}
return true
}
func (table *tableViewData) getColumnStyle() TableColumnStyle {
for _, tag := range []string{ColumnStyle, Content} {
if value := table.getRaw(tag); value != nil {
if style, ok := value.(TableColumnStyle); ok {
return style
}
}
}
return nil
}
func (table *tableViewData) getCellStyle() TableCellStyle {
for _, tag := range []string{CellStyle, Content} {
if value := table.getRaw(tag); value != nil {
if style, ok := value.(TableCellStyle); ok {
return style
}
}
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -1,22 +1,24 @@
package rui package rui
import "strings" func newTableCellView(session Session) *tableCellView {
view := new(tableCellView)
func (cell *tableCellView) Set(tag string, value any) bool { view.init(session)
return cell.set(strings.ToLower(tag), value) return view
} }
func (cell *tableCellView) set(tag string, value any) bool { func (cell *tableCellView) init(session Session) {
switch tag { cell.viewData.init(session)
case VerticalAlign: cell.normalize = func(tag PropertyName) PropertyName {
tag = TableVerticalAlign if tag == VerticalAlign {
return TableVerticalAlign
}
return tag
} }
return cell.viewData.set(tag, value)
} }
func (cell *tableCellView) cssStyle(self View, builder cssBuilder) { func (cell *tableCellView) cssStyle(self View, builder cssBuilder) {
session := cell.Session() session := cell.Session()
cell.viewData.cssViewStyle(builder, session) writeViewStyleCSS(cell, builder, session, false)
if value, ok := enumProperty(cell, TableVerticalAlign, session, 0); ok { if value, ok := enumProperty(cell, TableVerticalAlign, session, 0); ok {
builder.add("vertical-align", enumProperties[TableVerticalAlign].values[value]) builder.add("vertical-align", enumProperties[TableVerticalAlign].values[value])
@ -24,15 +26,20 @@ func (cell *tableCellView) cssStyle(self View, builder cssBuilder) {
} }
// GetTableContent returns a TableAdapter which defines the TableView content. // GetTableContent returns a TableAdapter which defines the TableView content.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableContent(view View, subviewID ...string) TableAdapter { func GetTableContent(view View, subviewID ...string) TableAdapter {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0]) if content := view.getRaw(Content); content != nil {
} if adapter, ok := content.(TableAdapter); ok {
return adapter
if view != nil { }
if tableView, ok := view.(TableView); ok { }
return tableView.content() if obj := view.binding(); obj != nil {
if adapter, ok := obj.(TableAdapter); ok {
return adapter
}
} }
} }
@ -40,15 +47,17 @@ func GetTableContent(view View, subviewID ...string) TableAdapter {
} }
// GetTableRowStyle returns a TableRowStyle which defines styles of TableView rows. // GetTableRowStyle returns a TableRowStyle which defines styles of TableView rows.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableRowStyle(view View, subviewID ...string) TableRowStyle { func GetTableRowStyle(view View, subviewID ...string) TableRowStyle {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0]) for _, tag := range []PropertyName{RowStyle, Content} {
} if value := view.getRaw(tag); value != nil {
if style, ok := value.(TableRowStyle); ok {
if view != nil { return style
if tableView, ok := view.(TableView); ok { }
return tableView.getRowStyle() }
} }
} }
@ -56,15 +65,17 @@ func GetTableRowStyle(view View, subviewID ...string) TableRowStyle {
} }
// GetTableColumnStyle returns a TableColumnStyle which defines styles of TableView columns. // GetTableColumnStyle returns a TableColumnStyle which defines styles of TableView columns.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableColumnStyle(view View, subviewID ...string) TableColumnStyle { func GetTableColumnStyle(view View, subviewID ...string) TableColumnStyle {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0]) for _, tag := range []PropertyName{ColumnStyle, Content} {
} if value := view.getRaw(tag); value != nil {
if style, ok := value.(TableColumnStyle); ok {
if view != nil { return style
if tableView, ok := view.(TableView); ok { }
return tableView.getColumnStyle() }
} }
} }
@ -72,16 +83,19 @@ func GetTableColumnStyle(view View, subviewID ...string) TableColumnStyle {
} }
// GetTableCellStyle returns a TableCellStyle which defines styles of TableView cells. // GetTableCellStyle returns a TableCellStyle which defines styles of TableView cells.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableCellStyle(view View, subviewID ...string) TableCellStyle { func GetTableCellStyle(view View, subviewID ...string) TableCellStyle {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0]) for _, tag := range []PropertyName{CellStyle, Content} {
} if value := view.getRaw(tag); value != nil {
if style, ok := value.(TableCellStyle); ok {
if view != nil { return style
if tableView, ok := view.(TableView); ok { }
return tableView.getCellStyle() }
} }
return nil
} }
return nil return nil
@ -89,26 +103,34 @@ func GetTableCellStyle(view View, subviewID ...string) TableCellStyle {
// GetTableSelectionMode returns the mode of the TableView elements selection. // GetTableSelectionMode returns the mode of the TableView elements selection.
// Valid values are NoneSelection (0), CellSelection (1), and RowSelection (2). // Valid values are NoneSelection (0), CellSelection (1), and RowSelection (2).
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableSelectionMode(view View, subviewID ...string) int { func GetTableSelectionMode(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, SelectionMode, NoneSelection, false) return enumStyledProperty(view, subviewID, SelectionMode, NoneSelection, false)
} }
// GetTableVerticalAlign returns a vertical align in a TavleView cell. Returns one of next values: // GetTableVerticalAlign returns a vertical align in a TableView cell. Returns one of next values:
// TopAlign (0), BottomAlign (1), CenterAlign (2), and BaselineAlign (3) // TopAlign (0), BottomAlign (1), CenterAlign (2), and BaselineAlign (3)
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableVerticalAlign(view View, subviewID ...string) int { func GetTableVerticalAlign(view View, subviewID ...string) int {
return enumStyledProperty(view, subviewID, TableVerticalAlign, TopAlign, false) return enumStyledProperty(view, subviewID, TableVerticalAlign, TopAlign, false)
} }
// GetTableHeadHeight returns the number of rows in the table header. // GetTableHeadHeight returns the number of rows in the table header.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableHeadHeight(view View, subviewID ...string) int { func GetTableHeadHeight(view View, subviewID ...string) int {
return intStyledProperty(view, subviewID, HeadHeight, 0) return intStyledProperty(view, subviewID, HeadHeight, 0)
} }
// GetTableFootHeight returns the number of rows in the table footer. // GetTableFootHeight returns the number of rows in the table footer.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableFootHeight(view View, subviewID ...string) int { func GetTableFootHeight(view View, subviewID ...string) int {
return intStyledProperty(view, subviewID, FootHeight, 0) return intStyledProperty(view, subviewID, FootHeight, 0)
} }
@ -117,17 +139,13 @@ func GetTableFootHeight(view View, subviewID ...string) int {
// If there is no selected cell/row or the selection mode is NoneSelection (0), // If there is no selected cell/row or the selection mode is NoneSelection (0),
// then a value of the row and column index less than 0 is returned. // then a value of the row and column index less than 0 is returned.
// If the selection mode is RowSelection (2) then the returned column index is less than 0. // If the selection mode is RowSelection (2) then the returned column index is less than 0.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableCurrent(view View, subviewID ...string) CellIndex { func GetTableCurrent(view View, subviewID ...string) CellIndex {
if len(subviewID) > 0 && subviewID[0] != "" { if view = getSubview(view, subviewID); view != nil {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if selectionMode := GetTableSelectionMode(view); selectionMode != NoneSelection { if selectionMode := GetTableSelectionMode(view); selectionMode != NoneSelection {
if tableView, ok := view.(TableView); ok { return tableViewCurrent(view)
return tableView.getCurrent()
}
} }
} }
return CellIndex{Row: -1, Column: -1} return CellIndex{Row: -1, Column: -1}
@ -135,66 +153,92 @@ func GetTableCurrent(view View, subviewID ...string) CellIndex {
// GetTableCellClickedListeners returns listeners of event which occurs when the user clicks on a table cell. // GetTableCellClickedListeners returns listeners of event which occurs when the user clicks on a table cell.
// If there are no listeners then the empty list is returned. // If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetTableCellClickedListeners(view View, subviewID ...string) []func(TableView, int, int) { // Result elements can be of the following types:
if len(subviewID) > 0 && subviewID[0] != "" { // - func(rui.TableView, int, int),
view = ViewByID(view, subviewID[0]) // - func(rui.TableView, int),
} // - func(rui.TableView),
if view != nil { // - func(int, int),
if value := view.Get(TableCellClickedEvent); value != nil { // - func(int),
if result, ok := value.([]func(TableView, int, int)); ok { // - func(),
return result // - string.
} //
} // The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
} // If it is not specified then a value from the first argument (view) is returned.
return []func(TableView, int, int){} func GetTableCellClickedListeners(view View, subviewID ...string) []any {
return getTwoArgEventRawListeners[TableView, int](view, subviewID, TableCellClickedEvent)
} }
// GetTableCellSelectedListeners returns listeners of event which occurs when a table cell becomes selected. // GetTableCellSelectedListeners returns listeners of event which occurs when a table cell becomes selected.
// If there are no listeners then the empty list is returned. // If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetTableCellSelectedListeners(view View, subviewID ...string) []func(TableView, int, int) { // Result elements can be of the following types:
if len(subviewID) > 0 && subviewID[0] != "" { // - func(rui.TableView, int, int),
view = ViewByID(view, subviewID[0]) // - func(rui.TableView, int),
} // - func(rui.TableView),
if view != nil { // - func(int, int),
if value := view.Get(TableCellSelectedEvent); value != nil { // - func(int),
if result, ok := value.([]func(TableView, int, int)); ok { // - func(),
return result // - string.
} //
} // The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
} // If it is not specified then a value from the first argument (view) is returned.
return []func(TableView, int, int){} func GetTableCellSelectedListeners(view View, subviewID ...string) []any {
return getTwoArgEventRawListeners[TableView, int](view, subviewID, TableCellSelectedEvent)
} }
// GetTableRowClickedListeners returns listeners of event which occurs when the user clicks on a table row. // GetTableRowClickedListeners returns listeners of event which occurs when the user clicks on a table row.
// If there are no listeners then the empty list is returned. // If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetTableRowClickedListeners(view View, subviewID ...string) []func(TableView, int) { // Result elements can be of the following types:
return getEventListeners[TableView, int](view, subviewID, TableRowClickedEvent) // - func(rui.TableView, int),
// - func(rui.TableView),
// - func(int),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableRowClickedListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[TableView, int](view, subviewID, TableRowClickedEvent)
} }
// GetTableRowSelectedListeners returns listeners of event which occurs when a table row becomes selected. // GetTableRowSelectedListeners returns listeners of event which occurs when a table row becomes selected.
// If there are no listeners then the empty list is returned. // If there are no listeners then the empty list is returned.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned. //
func GetTableRowSelectedListeners(view View, subviewID ...string) []func(TableView, int) { // Result elements can be of the following types:
return getEventListeners[TableView, int](view, subviewID, TableRowSelectedEvent) // - func(rui.TableView, int),
// - func(rui.TableView),
// - func(int),
// - func(),
// - string.
//
// The second argument (subviewID) specifies the path to the child element whose value needs to be returned.
// If it is not specified then a value from the first argument (view) is returned.
func GetTableRowSelectedListeners(view View, subviewID ...string) []any {
return getOneArgEventRawListeners[TableView, int](view, subviewID, TableRowSelectedEvent)
} }
// ReloadTableViewData updates TableView // ReloadTableViewData updates TableView
// If the second argument (subviewID) is not specified or it is "" then updates the first argument (TableView).
func ReloadTableViewData(view View, subviewID ...string) bool { func ReloadTableViewData(view View, subviewID ...string) bool {
var tableView TableView if view = getSubview(view, subviewID); view != nil {
if len(subviewID) > 0 && subviewID[0] != "" { if tableView, ok := view.(TableView); ok {
if tableView = TableViewByID(view, subviewID[0]); tableView == nil { tableView.ReloadTableData()
return false return true
}
} else {
var ok bool
if tableView, ok = view.(TableView); !ok {
return false
} }
} }
return false
tableView.ReloadTableData() }
return true
// ReloadTableViewCell updates the given table cell.
// If the last argument (subviewID) is not specified or it is "" then updates the cell of the first argument (TableView).
func ReloadTableViewCell(row, column int, view View, subviewID ...string) bool {
if view = getSubview(view, subviewID); view != nil {
if tableView, ok := view.(TableView); ok {
tableView.ReloadCell(row, column)
return true
}
}
return false
} }

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More