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
* Requires go 1.18 or higher
@ -68,7 +218,7 @@
# v0.2.0
* Added "animation" and "transition" properties, Animation interface, animation events
* Renamed ColorPropery constant to ColorTag
* Renamed ColorProperty constant to ColorTag
* Updated readme
* Added the Animation example to the demo
* Bug fixing

File diff suppressed because it is too large Load Diff

989
README.md

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@ package rui
import "strings"
// AbsoluteLayout - list-container of View
// AbsoluteLayout represent an AbsoluteLayout view where child views can be arbitrary positioned
type AbsoluteLayout interface {
ViewsContainer
}
@ -20,7 +20,8 @@ func NewAbsoluteLayout(session Session, params Params) AbsoluteLayout {
}
func newAbsoluteLayout(session Session) View {
return NewAbsoluteLayout(session, nil)
//return NewAbsoluteLayout(session, nil)
return new(absoluteLayoutData)
}
// 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 {
for _, view := range layout.views {
view.addToCSSStyle(map[string]string{`position`: `absolute`})
viewHTML(view, buffer)
viewHTML(view, buffer, "")
}
}
}

View File

@ -1,6 +1,7 @@
package rui
import (
"errors"
"fmt"
"math"
"strconv"
@ -11,43 +12,51 @@ import (
// Can take the following values: Radian, Degree, Gradian, and Turn
type AngleUnitType uint8
// Constants which represent values or the [AngleUnitType]
const (
// Radian - angle in radians
Radian AngleUnitType = 0
// Radian - angle in radians * π
PiRadian AngleUnitType = 1
// Degree - angle in degrees
Degree AngleUnitType = 2
// Gradian - angle in gradian (1400 of a full circle)
Gradian AngleUnitType = 3
// Turn - angle in turns (1 turn = 360 degree)
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 of the angle value
Type AngleUnitType
// Value of the angle in Type units
Value float64
}
// Deg creates AngleUnit with Degree type
func Deg(value float64) AngleUnit {
return AngleUnit{Type: Degree, Value: value}
func Deg[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) AngleUnit {
return AngleUnit{Type: Degree, Value: float64(value)}
}
// Rad create AngleUnit with Radian type
func Rad(value float64) AngleUnit {
return AngleUnit{Type: Radian, Value: value}
func Rad[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) AngleUnit {
return AngleUnit{Type: Radian, Value: float64(value)}
}
// PiRad create AngleUnit with PiRadian type
func PiRad(value float64) AngleUnit {
return AngleUnit{Type: PiRadian, Value: value}
func PiRad[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) AngleUnit {
return AngleUnit{Type: PiRadian, Value: float64(value)}
}
// Grad create AngleUnit with Gradian type
func Grad(value float64) AngleUnit {
return AngleUnit{Type: Gradian, Value: value}
func Grad[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) AngleUnit {
return AngleUnit{Type: Gradian, Value: float64(value)}
}
// 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) {
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) {
val, err := strconv.ParseFloat(value[:len(value)-len(suffix)], 64)
if err != nil {

File diff suppressed because it is too large Load Diff

View File

@ -1,170 +1,181 @@
package rui
import "strings"
// Constants which describe values for view's animation events properties
const (
// 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.
TransitionRunEvent = "transition-run-event"
//
// Used by View:
// 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.
// The "transition-start-event" is fired when a transition has actually started,
// i.e., after "delay" has ended.
TransitionStartEvent = "transition-start-event"
// TransitionStartEvent is the constant for "transition-start-event" property tag.
//
// Used by View:
// 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.
// 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.
// The "transition-cancel-event" is fired when a transition is cancelled. The transition is cancelled when:
// * A new property transition has begun.
// * 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.
TransitionCancelEvent = "transition-cancel-event"
//
// Used by View:
// Is fired when a transition is cancelled. The transition is cancelled when:
// - A new property transition has begun.
// - 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.
// 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.
AnimationStartEvent = "animation-start-event"
//
// Used by View:
// 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.
// 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
// or the animation is removed from the element, the "animation-end-event" is not fired.
AnimationEndEvent = "animation-end-event"
//
// Used by View:
// Fired when an animation has completed. If the animation aborts before reaching completion, such as if the element is
// 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.
// 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".
// This might happen when the animation-name is changed such that the animation is removed,
// or when the animating view is hidden. Therefore, either directly or because any of its
// containing views are hidden.
// The event is not supported by all browsers.
AnimationCancelEvent = "animation-cancel-event"
//
// Used by View:
// Fired when an animation unexpectedly aborts. In other words, any time it stops running without sending the
// "animation-end-event". This might happen when the animation-name is changed such that the animation is removed, or when
// the animating view is hidden. Therefore, either directly or because any of its containing views are hidden. The event
// is not supported by all browsers.
//
// 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.
// 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,
// and therefore does not occur for animations with an "iteration-count" of one.
AnimationIterationEvent = "animation-iteration-event"
//
// Used by View:
// Fired when an iteration of an animation ends, and another one begins. This event does not occur at the same time as the
// 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 }{
TransitionRunEvent: {jsEvent: "ontransitionrun", jsFunc: "transitionRunEvent"},
TransitionStartEvent: {jsEvent: "ontransitionstart", jsFunc: "transitionStartEvent"},
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 {
func (view *viewData) handleTransitionEvents(tag PropertyName, data DataObject) {
if propertyName, ok := data.PropertyValue("property"); ok {
property := PropertyName(propertyName)
if tag == TransitionEndEvent || tag == TransitionCancelEvent {
if animation, ok := view.singleTransition[property]; ok {
delete(view.singleTransition, property)
if animation != nil {
view.transitions[property] = animation
} else {
delete(view.transitions, property)
}
view.updateTransitionCSS()
setTransition(view, property, animation)
session := view.session
session.updateCSSProperty(view.htmlID(), "transition", transitionCSS(view, session))
}
}
for _, listener := range getEventListeners[View, string](view, nil, tag) {
listener(view, property)
for _, listener := range getOneArgEventListeners[View, PropertyName](view, nil, tag) {
listener.Run(view, property)
}
}
}
var animationEvents = map[string]struct{ jsEvent, jsFunc string }{
AnimationStartEvent: {jsEvent: "onanimationstart", jsFunc: "animationStartEvent"},
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 {
func (view *viewData) handleAnimationEvents(tag PropertyName, data DataObject) {
if listeners := getOneArgEventListeners[View, string](view, nil, tag); len(listeners) > 0 {
id := ""
if name, ok := data.PropertyValue("name"); ok {
for _, animation := range GetAnimation(view) {
@ -174,63 +185,135 @@ func (view *viewData) handleAnimationEvents(tag string, data DataObject) {
}
}
for _, listener := range listeners {
listener(view, id)
listener.Run(view, id)
}
}
}
// GetTransitionRunListeners returns the "transition-run-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 GetTransitionRunListeners(view View, subviewID ...string) []func(View, string) {
return getEventListeners[View, string](view, subviewID, TransitionRunEvent)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, string](view, subviewID, TransitionStartEvent)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, string](view, subviewID, TransitionEndEvent)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, string](view, subviewID, TransitionCancelEvent)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, string](view, subviewID, AnimationStartEvent)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, string](view, subviewID, AnimationEndEvent)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, string](view, subviewID, AnimationCancelEvent)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, string](view, subviewID, AnimationIterationEvent)
//
// Result elements can be of the following types:
// - 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 (
"fmt"
"log"
"runtime"
)
@ -10,14 +9,8 @@ import (
// clients and the server is displayed in the debug log
var ProtocolInDebugLog = false
var debugLogFunc func(string) = func(text string) {
log.Println("\033[34m" + text)
}
var errorLogFunc = func(text string) {
log.Println("\033[31m" + text)
//println(text)
}
var debugLogFunc func(string) = debugLog
var errorLogFunc func(string) = errorLog
// SetDebugLog sets a function for outputting debug info.
// 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;
}
:root {
--tooltip-arrow-size: 6px;
--tooltip-background: white;
--tooltip-text-color: black;
--tooltip-shadow-color: gray;
}
body {
-webkit-touch-callout: none;
-webkit-user-select: none;
@ -15,6 +22,7 @@ body {
margin: 0 auto;
width: 100%;
height: 100vh;
font-family: system-ui;
}
div {
@ -34,12 +42,14 @@ div:focus {
*/
input {
box-sizing: border-box;
margin: 2px;
padding: 1px;
font-size: inherit;
}
select {
box-sizing: border-box;
margin: 2px;
font-size: inherit;
}
@ -49,10 +59,12 @@ button {
}
textarea {
box-sizing: border-box;
margin: 2px;
padding: 1px;
padding: 4px;
overflow: auto;
font-size: inherit;
resize: none;
}
ul:focus {
@ -68,7 +80,7 @@ ul:focus {
}
.ruiPopupLayer {
background-color: rgba(128,128,128,0.1);
/*background-color: rgba(128,128,128,0.1);*/
position: absolute;
top: 0px;
bottom: 0px;
@ -76,7 +88,54 @@ ul:focus {
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 {
box-sizing: border-box;
}
.ruiAbsoluteLayout {
@ -89,6 +148,19 @@ ul:focus {
.ruiListLayout {
display: flex;
overflow: auto;
}
.ruiButton {
display: flex;
overflow: auto;
justify-content: center;
align-items: center;
flex-flow: row;
}
.ruiColumnLayout {
overflow: auto;
}
.ruiStackLayout {
@ -112,16 +184,25 @@ ul:focus {
}
.ruiImageView {
display: block;
}
.ruiSvgImageView {
display: grid;
}
.ruiListView {
overflow: auto;
/*
display: flex;
align-content: stretch;
*/
}
.hiddenMarker {
list-style: none;
}
.hiddenMarker::-webkit-details-marker {
display: none;
}
/*
@media (prefers-color-scheme: light) {
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
import (
"bytes"
"context"
_ "embed"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
)
//go:embed app_scripts.js
@ -27,62 +14,70 @@ var appStyles string
//go:embed defaultTheme.rui
var defaultThemeText string
// Application - app interface
// Application represent generic application interface, see also [Session]
type Application interface {
// Finish finishes the application
Finish()
nextSessionID() int
removeSession(id int)
}
type application struct {
server *http.Server
params AppParams
createContentFunc func(Session) SessionContent
sessions map[int]Session
// Params returns application parameters set by StartApp function
Params() AppParams
removeSession(id int)
}
// AppParams defines parameters of the app
type AppParams struct {
// Title - title of the app window/tab
Title string
// TitleColor - background color of the app window/tab (applied only for Safari and Chrome for Android)
TitleColor Color
// Icon - the icon file name
Icon string
// CertFile - path of a certificate for the server must be provided
// 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
// of the server's certificate, any intermediates, and the CA's certificate.
CertFile string
AutoCertDomain string
// KeyFile - path of a private key for the server must be provided
// if neither the Server's TLSConfig.Certificates nor TLSConfig.GetCertificate are populated.
KeyFile string
// Redirect80 - if true then the function of redirect from port 80 to 443 is created
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 {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(`<!DOCTYPE html>
<html>
<head>
func getStartPage(buffer *strings.Builder, params AppParams) {
buffer.WriteString(`<head>
<meta charset="utf-8">
<title>`)
buffer.WriteString(app.params.Title)
buffer.WriteString(params.Title)
buffer.WriteString("</title>")
if app.params.Icon != "" {
if params.Icon != "" {
buffer.WriteString(`
<link rel="icon" href="`)
buffer.WriteString(app.params.Icon)
buffer.WriteString(params.Icon)
buffer.WriteString(`">`)
}
if app.params.TitleColor != 0 {
if params.TitleColor != 0 {
buffer.WriteString(`
<meta name="theme-color" content="`)
buffer.WriteString(app.params.TitleColor.cssString())
buffer.WriteString(params.TitleColor.cssString())
buffer.WriteString(`">`)
}
@ -92,356 +87,18 @@ func (app *application) getStartPage() string {
<style>`)
buffer.WriteString(appStyles)
buffer.WriteString(`</style>
<script>`)
buffer.WriteString(defaultScripts)
buffer.WriteString(`</script>
<style id="ruiAnimations"></style>
<script src="/script.js"></script>
</head>
<body>
<body id="body" onkeydown="keyDownEvent(this, event)">
<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>
</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,
})
</body>`)
}

View File

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

View File

@ -1,113 +1,75 @@
package rui
import "strings"
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
import (
"fmt"
)
// 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 {
Properties
fmt.Stringer
stringWriter
cssStyle(session Session) string
// Tag returns type of the background element.
// Possible values are: "image", "conic-gradient", "linear-gradient" and "radial-gradient"
Tag() string
// Clone creates a new copy of BackgroundElement
Clone() BackgroundElement
}
type backgroundElement struct {
propertyList
dataProperty
}
type backgroundImage struct {
backgroundElement
}
// NewBackgroundImage creates the new background image
func createBackground(obj DataObject) BackgroundElement {
var result BackgroundElement = nil
switch obj.Tag() {
case "image":
image := new(backgroundImage)
image.properties = map[string]any{}
result = image
result = NewBackgroundImage(nil)
case "linear-gradient":
gradient := new(backgroundLinearGradient)
gradient.properties = map[string]any{}
result = gradient
result = NewBackgroundLinearGradient(nil)
case "radial-gradient":
gradient := new(backgroundRadialGradient)
gradient.properties = map[string]any{}
result = gradient
result = NewBackgroundRadialGradient(nil)
case "conic-gradient":
gradient := new(backgroundConicGradient)
gradient.properties = map[string]any{}
result = gradient
result = NewBackgroundConicGradient(nil)
default:
return nil
}
count := obj.PropertyCount()
for i := 0; i < count; i++ {
if node := obj.Property(i); node.Type() == TextNode {
for node := range obj.Properties() {
if node.Type() == TextNode {
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
}
// NewBackgroundImage creates the new background image
func NewBackgroundImage(params Params) BackgroundElement {
result := new(backgroundImage)
result.properties = map[string]any{}
for tag, value := range params {
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
func parseBackgroundText(text string) BackgroundElement {
obj, err := ParseDataText(text)
if err != nil {
ErrorLog(err.Error())
return nil
}
return tag
return createBackground(obj)
}
func (image *backgroundImage) Set(tag string, value any) bool {
tag = image.normalizeTag(tag)
switch tag {
case Attachment, Width, Height, Repeat, ImageHorizontalAlign, ImageVerticalAlign,
backgroundFit, Source:
return image.backgroundElement.Set(tag, value)
func parseBackgroundValue(value any) []BackgroundElement {
switch value := value.(type) {
case BackgroundElement:
return []BackgroundElement{value}
case []BackgroundElement:
return value
case []DataValue:
background := []BackgroundElement{}
for _, el := range value {
if el.IsObject() {
if element := createBackground(el.Object()); element != nil {
background = append(background, element)
} else {
return nil
}
} else if element := parseBackgroundText(el.Value()); element != nil {
background = append(background, element)
} else {
return nil
}
}
return background
case DataObject:
if element := createBackground(value); element != nil {
return []BackgroundElement{element}
}
return false
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 (image *backgroundImage) Get(tag string) any {
return image.backgroundElement.Get(image.normalizeTag(tag))
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 (image *backgroundImage) cssStyle(session Session) string {
if src, ok := imageProperty(image, Source, session); ok && src != "" {
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)
buffer.WriteString(`url(`)
buffer.WriteString(src)
buffer.WriteRune(')')
for _, background := range backgrounds {
if value := background.cssStyle(session); value != "" {
if buffer.Len() > 0 {
buffer.WriteString(", ")
}
buffer.WriteString(value)
}
}
attachment, _ := enumProperty(image, Attachment, session, NoRepeat)
values := enumProperties[Attachment].values
if attachment > 0 && attachment < len(values) {
if buffer.Len() > 0 {
backgroundColor, _ := colorProperty(properties, BackgroundColor, session)
if backgroundColor != 0 {
buffer.WriteRune(' ')
buffer.WriteString(values[attachment])
buffer.WriteString(backgroundColor.cssString())
}
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 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
//
// 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 {
result := new(backgroundConicGradient)
result.properties = map[string]any{}
result.init()
for tag, value := range params {
result.Set(tag, value)
}
return result
}
// String convert internal representation of [BackgroundGradientAngle] into a string.
func (point *BackgroundGradientAngle) String() string {
result := "black"
if point.Color != nil {
@ -47,7 +55,6 @@ func (point *BackgroundGradientAngle) String() string {
case AngleUnit:
result += " " + value.String()
}
}
@ -58,20 +65,21 @@ func (point *BackgroundGradientAngle) 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 {
if ok, constName := isConstantName(color); ok {
if clr, ok := session.Color(constName); ok {
return clr, true
}
} else {
if clr, ok := StringToColor(color); ok {
} else if clr, ok := StringToColor(color); ok {
return clr, true
}
}
}
case Color:
return color, true
default:
if n, ok := isInt(color); ok {
return Color(n), true
}
}
}
return 0, false
@ -92,11 +100,8 @@ func (point *BackgroundGradientAngle) cssString(session Session, buffer *strings
if point.Angle != nil {
switch value := point.Angle.(type) {
case string:
if value != "" {
if value[0] == '@' {
if val, ok := session.Constant(value[1:]); ok {
value = val
} else {
if ok, constName := isConstantName(value); ok {
if value, ok = session.Constant(constName); !ok {
return
}
}
@ -105,7 +110,6 @@ func (point *BackgroundGradientAngle) cssString(session Session, buffer *strings
buffer.WriteRune(' ')
buffer.WriteString(angle.cssString())
}
}
case AngleUnit:
buffer.WriteRune(' ')
@ -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 {
return "conic-gradient"
}
@ -126,8 +139,8 @@ func (image *backgroundConicGradient) Clone() BackgroundElement {
return result
}
func (gradient *backgroundConicGradient) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeConicGradientTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case "x-center":
tag = CenterX
@ -139,27 +152,50 @@ func (gradient *backgroundConicGradient) normalizeTag(tag string) string {
return tag
}
func (gradient *backgroundConicGradient) Set(tag string, value any) bool {
tag = gradient.normalizeTag(tag)
func backgroundConicGradientSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case CenterX, CenterY, Repeating, From:
return gradient.propertyList.Set(tag, value)
case Gradient:
return gradient.setGradient(value)
switch value := value.(type) {
case string:
if value == "" {
return propertiesRemove(properties, tag)
}
ErrorLogF(`"%s" property is not supported by BackgroundConicGradient`, tag)
return false
}
func (gradient *backgroundConicGradient) stringToAngle(text string) (any, bool) {
if text == "" {
return nil, false
} else if text[0] == '@' {
return text, true
if strings.ContainsAny(value, ", ") {
if vector := parseGradientText(value); vector != nil {
properties.setRaw(Gradient, vector)
return []PropertyName{tag}
}
return StringToAngleUnit(text)
} 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
}
return propertiesSet(properties, tag, value)
}
func (gradient *backgroundConicGradient) stringToGradientPoint(text string) (BackgroundGradientAngle, bool) {
@ -178,7 +214,7 @@ func (gradient *backgroundConicGradient) stringToGradientPoint(text string) (Bac
return result, false
}
if colorText[0] == '@' {
if ok, _ := isConstantName(colorText); ok {
result.Color = colorText
} else if color, ok := StringToColor(colorText); ok {
result.Color = color
@ -187,7 +223,9 @@ func (gradient *backgroundConicGradient) stringToGradientPoint(text string) (Bac
}
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
} else {
return result, false
@ -209,63 +247,12 @@ func (gradient *backgroundConicGradient) parseGradientText(value string) []Backg
for i, element := range elements {
var ok bool
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 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 {
points := []BackgroundGradientAngle{}
@ -336,3 +323,16 @@ func (gradient *backgroundConicGradient) cssStyle(session Session) 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)
}

596
border.go
View File

@ -5,44 +5,173 @@ import (
"strings"
)
// Constants related to view's border description
const (
// NoneLine constant specifies that there is no border
NoneLine = 0
// SolidLine constant specifies the border/line as a solid line
SolidLine = 1
// DashedLine constant specifies the border/line as a dashed line
DashedLine = 2
// DottedLine constant specifies the border/line as a dotted line
DottedLine = 3
// DoubleLine constant specifies the border/line as a double solid line
DoubleLine = 4
// DoubleLine constant specifies the border/line as a double solid line
WavyLine = 5
// LeftStyle is the constant for "left-style" property tag.
LeftStyle = "left-style"
// RightStyle is the constant for "-right-style" property tag.
RightStyle = "right-style"
//
// Used by BorderProperty.
// 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 = "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 = "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 = "left-width"
// RightWidth is the constant for "-right-width" property tag.
RightWidth = "right-width"
//
// Used by BorderProperty.
// 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 = "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 = "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 = "left-color"
// RightColor is the constant for "-right-color" property tag.
RightColor = "right-color"
//
// Used by BorderProperty.
// 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 = "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 = "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
@ -50,8 +179,11 @@ type BorderProperty interface {
Properties
fmt.Stringer
stringWriter
// ViewBorders returns top, right, bottom and left borders information all together
ViewBorders(session Session) ViewBorders
delete(tag string)
deleteTag(tag PropertyName) bool
cssStyle(builder cssBuilder, session Session)
cssWidth(builder cssBuilder, session Session)
cssColor(builder cssBuilder, session Session)
@ -61,12 +193,12 @@ type BorderProperty interface {
}
type borderProperty struct {
propertyList
dataProperty
}
func newBorderProperty(value any) BorderProperty {
border := new(borderProperty)
border.properties = map[string]any{}
border.init()
if value != nil {
switch value := value.(type) {
@ -128,12 +260,20 @@ func newBorderProperty(value any) BorderProperty {
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 {
border := new(borderProperty)
border.properties = map[string]any{}
border.init()
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,
LeftWidth, RightWidth, TopWidth, BottomWidth,
LeftColor, RightColor, TopColor, BottomColor} {
@ -145,8 +285,37 @@ func NewBorder(params Params) BorderProperty {
return border
}
func (border *borderProperty) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func (border *borderProperty) init() {
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 {
case BorderLeft, CellBorderLeft:
return Left
@ -213,23 +382,26 @@ func (border *borderProperty) writeString(buffer *strings.Builder, indent string
buffer.WriteString("_{ ")
comma := false
write := func(tag string, value any) {
write := func(tag PropertyName, value any) {
text := propertyValueToString(tag, value, indent)
if text != "" {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(string(tag))
buffer.WriteString(" = ")
writePropertyValue(buffer, BorderStyle, value, indent)
buffer.WriteString(text)
comma = true
}
}
for _, tag := range []string{Style, Width, ColorTag} {
for _, tag := range []PropertyName{Style, Width, ColorTag} {
if value, ok := border.properties[tag]; ok {
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]
width, okWidth := border.properties[side+"-"+Width]
color, okColor := border.properties[side+"-"+ColorTag]
@ -239,7 +411,7 @@ func (border *borderProperty) writeString(buffer *strings.Builder, indent string
comma = false
}
buffer.WriteString(side)
buffer.WriteString(string(side))
buffer.WriteString(" = _{ ")
if okStyle {
write(Style, style)
@ -262,164 +434,92 @@ func (border *borderProperty) String() string {
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 {
result := true
for _, side := range []string{Top, Right, Bottom, Left} {
if node := obj.PropertyWithTag(side); node != nil {
if node.Type() == ObjectNode {
if !border.setSingleBorderObject(side, node.Object()) {
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) {
for node := range obj.Properties() {
tag := PropertyName(node.Tag())
switch node.Type() {
case TextNode:
if borderSet(border, tag, node.Text()) == nil {
result = false
}
case 4:
for n, tag := range [4]string{TopStyle, RightStyle, BottomStyle, LeftStyle} {
if !border.setEnumProperty(tag, values[n], styles) {
case ObjectNode:
if borderSet(border, tag, node.Object()) == nil {
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
}
case 4:
for n, tag := range [4]string{TopWidth, RightWidth, BottomWidth, LeftWidth} {
if !border.setSizeProperty(tag, values[n]) {
result = false
}
}
default:
notCompatibleType(Width, text)
result = false
}
}
return result
}
func (border *borderProperty) Remove(tag string) {
tag = border.normalizeTag(tag)
func borderRemove(properties Properties, tag PropertyName) []PropertyName {
result := []PropertyName{}
removeTag := func(t PropertyName) {
if properties.getRaw(t) != nil {
properties.setRaw(t, nil)
result = append(result, t)
}
}
switch tag {
case Style:
for _, t := range []string{tag, TopStyle, RightStyle, BottomStyle, LeftStyle} {
delete(border.properties, t)
for _, t := range []PropertyName{tag, TopStyle, RightStyle, BottomStyle, LeftStyle} {
removeTag(t)
}
case Width:
for _, t := range []string{tag, TopWidth, RightWidth, BottomWidth, LeftWidth} {
delete(border.properties, t)
for _, t := range []PropertyName{tag, TopWidth, RightWidth, BottomWidth, LeftWidth} {
removeTag(t)
}
case ColorTag:
for _, t := range []string{tag, TopColor, RightColor, BottomColor, LeftColor} {
delete(border.properties, t)
for _, t := range []PropertyName{tag, TopColor, RightColor, BottomColor, LeftColor} {
removeTag(t)
}
case Left, Right, Top, Bottom:
border.Remove(tag + "-style")
border.Remove(tag + "-width")
border.Remove(tag + "-color")
removeTag(tag + "-style")
removeTag(tag + "-width")
removeTag(tag + "-color")
case LeftStyle, RightStyle, TopStyle, BottomStyle:
delete(border.properties, tag)
if style, ok := border.properties[Style]; ok && style != nil {
for _, t := range []string{TopStyle, RightStyle, BottomStyle, LeftStyle} {
removeTag(tag)
if style := properties.getRaw(Style); style != nil {
for _, t := range []PropertyName{TopStyle, RightStyle, BottomStyle, LeftStyle} {
if t != tag {
if _, ok := border.properties[t]; !ok {
border.properties[t] = style
if properties.getRaw(t) == nil {
properties.setRaw(t, style)
result = append(result, t)
}
}
}
}
case LeftWidth, RightWidth, TopWidth, BottomWidth:
delete(border.properties, tag)
if width, ok := border.properties[Width]; ok && width != nil {
for _, t := range []string{TopWidth, RightWidth, BottomWidth, LeftWidth} {
removeTag(tag)
if width := properties.getRaw(Width); width != nil {
for _, t := range []PropertyName{TopWidth, RightWidth, BottomWidth, LeftWidth} {
if t != tag {
if _, ok := border.properties[t]; !ok {
border.properties[t] = width
if properties.getRaw(t) == nil {
properties.setRaw(t, width)
result = append(result, t)
}
}
}
}
case LeftColor, RightColor, TopColor, BottomColor:
delete(border.properties, tag)
if color, ok := border.properties[ColorTag]; ok && color != nil {
for _, t := range []string{TopColor, RightColor, BottomColor, LeftColor} {
removeTag(tag)
if color := properties.getRaw(ColorTag); color != nil {
for _, t := range []PropertyName{TopColor, RightColor, BottomColor, LeftColor} {
if t != tag {
if _, ok := border.properties[t]; !ok {
border.properties[t] = color
if properties.getRaw(t) == nil {
properties.setRaw(t, color)
result = append(result, t)
}
}
}
@ -428,80 +528,121 @@ func (border *borderProperty) Remove(tag string) {
default:
ErrorLogF(`"%s" property is not compatible with the BorderProperty`, tag)
}
return result
}
func (border *borderProperty) Set(tag string, value any) bool {
if value == nil {
border.Remove(tag)
return true
}
func borderSet(properties Properties, tag PropertyName, value any) []PropertyName {
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 {
case Style:
if border.setEnumProperty(Style, value, enumProperties[BorderStyle].values) {
for _, side := range []string{TopStyle, RightStyle, BottomStyle, LeftStyle} {
delete(border.properties, side)
if result := setEnumProperty(properties, Style, value, enumProperties[BorderStyle].values); result != nil {
for _, side := range []PropertyName{TopStyle, RightStyle, BottomStyle, LeftStyle} {
if value := properties.getRaw(side); value != nil {
properties.setRaw(side, nil)
result = append(result, side)
}
return true
}
return result
}
case Width:
if border.setSizeProperty(Width, value) {
for _, side := range []string{TopWidth, RightWidth, BottomWidth, LeftWidth} {
delete(border.properties, side)
if result := setSizeProperty(properties, Width, value); result != nil {
for _, side := range []PropertyName{TopWidth, RightWidth, BottomWidth, LeftWidth} {
if value := properties.getRaw(side); value != nil {
properties.setRaw(side, nil)
result = append(result, side)
}
return true
}
return result
}
case ColorTag:
if border.setColorProperty(ColorTag, value) {
for _, side := range []string{TopColor, RightColor, BottomColor, LeftColor} {
delete(border.properties, side)
if result := setColorProperty(properties, ColorTag, value); result != nil {
for _, side := range []PropertyName{TopColor, RightColor, BottomColor, LeftColor} {
if value := properties.getRaw(side); value != nil {
properties.setRaw(side, nil)
result = append(result, side)
}
return true
}
return result
}
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:
return border.setSizeProperty(tag, value)
return setSizeProperty(properties, tag, value)
case LeftColor, RightColor, TopColor, BottomColor:
return border.setColorProperty(tag, value)
return setColorProperty(properties, tag, value)
case Left, Right, Top, Bottom:
switch value := value.(type) {
case string:
if obj := ParseDataText(value); obj != nil {
return border.setSingleBorderObject(tag, obj)
obj, err := ParseDataText(value)
if err != nil {
ErrorLog(err.Error())
} else {
return setSingleBorderObject(tag, obj)
}
case DataObject:
return border.setSingleBorderObject(tag, value)
return setSingleBorderObject(tag, value)
case BorderProperty:
result := []PropertyName{}
styleTag := tag + "-" + Style
if style := value.Get(styleTag); value != nil {
border.properties[styleTag] = style
properties.setRaw(styleTag, style)
result = append(result, styleTag)
}
colorTag := tag + "-" + ColorTag
if color := value.Get(colorTag); value != nil {
border.properties[colorTag] = color
properties.setRaw(colorTag, color)
result = append(result, colorTag)
}
widthTag := tag + "-" + Width
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:
border.properties[tag+"-"+Style] = value.Style
border.properties[tag+"-"+Width] = value.Width
border.properties[tag+"-"+ColorTag] = value.Color
return true
properties.setRaw(tag+"-"+Style, value.Style)
properties.setRaw(tag+"-"+Width, value.Width)
properties.setRaw(tag+"-"+ColorTag, value.Color)
return []PropertyName{tag + "-" + Style, tag + "-" + Width, tag + "-" + ColorTag}
}
fallthrough
@ -509,105 +650,119 @@ func (border *borderProperty) Set(tag string, value any) bool {
ErrorLogF(`"%s" property is not compatible with the BorderProperty`, tag)
}
return false
return nil
}
func (border *borderProperty) Get(tag string) any {
tag = border.normalizeTag(tag)
if result, ok := border.properties[tag]; ok {
func borderGet(properties Properties, tag PropertyName) any {
if result := properties.getRaw(tag); result != nil {
return result
}
switch tag {
case Left, Right, Top, Bottom:
result := newBorderProperty(nil)
if style, ok := border.properties[tag+"-"+Style]; ok {
if style := properties.getRaw(tag + "-" + Style); style != nil {
result.Set(Style, style)
} else if style, ok := border.properties[Style]; ok {
} else if style := properties.getRaw(Style); style != nil {
result.Set(Style, style)
}
if width, ok := border.properties[tag+"-"+Width]; ok {
if width := properties.getRaw(tag + "-" + Width); width != nil {
result.Set(Width, width)
} else if width, ok := border.properties[Width]; ok {
} else if width := properties.getRaw(Width); width != nil {
result.Set(Width, width)
}
if color, ok := border.properties[tag+"-"+ColorTag]; ok {
if color := properties.getRaw(tag + "-" + ColorTag); color != nil {
result.Set(ColorTag, color)
} else if color, ok := border.properties[ColorTag]; ok {
} else if color := properties.getRaw(ColorTag); color != nil {
result.Set(ColorTag, color)
}
return result
case LeftStyle, RightStyle, TopStyle, BottomStyle:
if style, ok := border.properties[tag]; ok {
if style := properties.getRaw(tag); style != nil {
return style
}
return border.properties[Style]
return properties.getRaw(Style)
case LeftWidth, RightWidth, TopWidth, BottomWidth:
if width, ok := border.properties[tag]; ok {
if width := properties.getRaw(tag); width != nil {
return width
}
return border.properties[Width]
return properties.getRaw(Width)
case LeftColor, RightColor, TopColor, BottomColor:
if color, ok := border.properties[tag]; ok {
if color := properties.getRaw(tag); color != nil {
return color
}
return border.properties[ColorTag]
return properties.getRaw(ColorTag)
}
return nil
}
func (border *borderProperty) delete(tag string) {
tag = border.normalizeTag(tag)
remove := []string{}
func (border *borderProperty) deleteTag(tag PropertyName) bool {
result := false
removeTags := func(tags []PropertyName) {
for _, tag := range tags {
if border.getRaw(tag) != nil {
border.setRaw(tag, nil)
result = true
}
}
}
switch tag {
case Style:
remove = []string{Style, LeftStyle, RightStyle, TopStyle, BottomStyle}
removeTags([]PropertyName{Style, LeftStyle, RightStyle, TopStyle, BottomStyle})
case Width:
remove = []string{Width, LeftWidth, RightWidth, TopWidth, BottomWidth}
removeTags([]PropertyName{Width, LeftWidth, RightWidth, TopWidth, BottomWidth})
case ColorTag:
remove = []string{ColorTag, LeftColor, RightColor, TopColor, BottomColor}
removeTags([]PropertyName{ColorTag, LeftColor, RightColor, TopColor, BottomColor})
case Left, Right, Top, Bottom:
if border.Get(Style) != nil {
border.properties[tag+"-"+Style] = 0
remove = []string{tag + "-" + ColorTag, tag + "-" + Width}
result = true
removeTags([]PropertyName{tag + "-" + ColorTag, tag + "-" + Width})
} else {
remove = []string{tag + "-" + Style, tag + "-" + ColorTag, tag + "-" + Width}
removeTags([]PropertyName{tag + "-" + Style, tag + "-" + ColorTag, tag + "-" + Width})
}
case LeftStyle, RightStyle, TopStyle, BottomStyle:
if border.getRaw(tag) != nil {
if border.Get(Style) != nil {
border.properties[tag] = 0
result = true
} else {
remove = []string{tag}
removeTags([]PropertyName{tag})
}
}
case LeftWidth, RightWidth, TopWidth, BottomWidth:
if border.getRaw(tag) != nil {
if border.Get(Width) != nil {
border.properties[tag] = AutoSize()
result = true
} else {
remove = []string{tag}
removeTags([]PropertyName{tag})
}
}
case LeftColor, RightColor, TopColor, BottomColor:
if border.getRaw(tag) != nil {
if border.Get(ColorTag) != nil {
border.properties[tag] = 0
result = true
} else {
remove = []string{tag}
removeTags([]PropertyName{tag})
}
}
}
for _, tag := range remove {
delete(border.properties, tag)
}
return result
}
func (border *borderProperty) ViewBorders(session Session) ViewBorders {
@ -616,7 +771,7 @@ func (border *borderProperty) ViewBorders(session Session) ViewBorders {
defWidth, _ := sizeProperty(border, Width, session)
defColor, _ := colorProperty(border, ColorTag, session)
getBorder := func(prefix string) ViewBorder {
getBorder := func(prefix PropertyName) ViewBorder {
var result ViewBorder
var ok bool
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 &&
borders.Top.Style == borders.Left.Style &&
borders.Top.Style == borders.Bottom.Style {
builder.add(BorderStyle, values[borders.Top.Style])
builder.add(string(BorderStyle), values[borders.Top.Style])
} 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])
}
}
@ -703,8 +858,13 @@ func (border *borderProperty) cssColorValue(session Session) string {
// ViewBorder describes parameters of a view border
type ViewBorder struct {
// Style of the border line
Style int
// Color of the border line
Color Color
// Width of the border line
Width SizeUnit
}
@ -726,11 +886,25 @@ func (border *ViewBorders) AllTheSame() bool {
border.Top.Width.Equal(border.Bottom.Width)
}
func getBorder(style Properties, tag string) BorderProperty {
if value := style.Get(tag); value != nil {
func getBorderProperty(properties Properties, tag PropertyName) BorderProperty {
if value := properties.getRaw(tag); value != nil {
if border, ok := value.(BorderProperty); ok {
return border
}
}
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"
)
// BorderProperty is the interface of a bounds property data
// BorderProperty is an interface of a bounds property data
type BoundsProperty interface {
Properties
fmt.Stringer
stringWriter
// Bounds returns top, right, bottom and left size of the bounds
Bounds(session Session) Bounds
}
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 {
bounds := new(boundsPropertyData)
bounds.properties = map[string]any{}
bounds.init()
if params != nil {
for _, tag := range []string{Top, Right, Bottom, Left} {
if value, ok := params[tag]; ok {
bounds.Set(tag, value)
for _, tag := range bounds.supportedProperties {
if value, ok := params[tag]; ok && value != nil {
bounds.set(bounds, tag, value)
}
}
}
return bounds
}
func (bounds *boundsPropertyData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
// NewBounds creates the new BoundsProperty object.
//
// 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 {
case MarginTop, PaddingTop, CellPaddingTop,
"top-margin", "top-padding", "top-cell-padding":
@ -58,55 +84,6 @@ func (bounds *boundsPropertyData) String() string {
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 {
top, _ := sizeProperty(bounds, Top, session)
right, _ := sizeProperty(bounds, Right, session)
@ -138,7 +115,7 @@ func (bounds *Bounds) SetAll(value SizeUnit) {
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()
if size, ok := sizeProperty(properties, tag, session); ok {
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 {
if bounds.Left.Type == bounds.Top.Type &&
bounds.Left.Type == bounds.Right.Type &&
@ -190,20 +151,6 @@ func (bounds *Bounds) allFieldsEqual() bool {
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
func (bounds *Bounds) String() string {
if bounds.allFieldsEqual() {
@ -213,11 +160,11 @@ func (bounds *Bounds) String() 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() {
builder.add(tag, bounds.Top.cssString("0", session))
builder.add(string(tag), bounds.Top.cssString("0", session))
} else {
builder.addValues(tag, " ",
builder.addValues(string(tag), " ",
bounds.Top.cssString("0", session),
bounds.Right.cssString("0", session),
bounds.Bottom.cssString("0", session),
@ -231,11 +178,11 @@ func (bounds *Bounds) cssString(session Session) string {
return builder.finish()
}
func (properties *propertyList) setBounds(tag string, value any) bool {
if !properties.setSimpleProperty(tag, value) {
func setBoundsProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
switch value := value.(type) {
case string:
if strings.Contains(value, ",") {
if strings.ContainsRune(value, ',') {
values := split4Values(value)
count := len(values)
switch count {
@ -244,88 +191,119 @@ func (properties *propertyList) setBounds(tag string, value any) bool {
case 4:
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]) {
notCompatibleType(tag, value)
return false
return nil
}
}
properties.properties[tag] = bounds
return true
properties.setRaw(tag, bounds)
return []PropertyName{tag}
default:
notCompatibleType(tag, value)
return false
return nil
}
}
return properties.setSizeProperty(tag, value)
return setSizeProperty(properties, tag, value)
case SizeUnit:
properties.properties[tag] = value
properties.setRaw(tag, value)
case float32:
properties.properties[tag] = Px(float64(value))
properties.setRaw(tag, Px(float64(value)))
case float64:
properties.properties[tag] = Px(value)
properties.setRaw(tag, Px(value))
case Bounds:
bounds := NewBoundsProperty(nil)
if value.Top.Type != Auto {
bounds.Set(Top, value.Top)
bounds.setRaw(Top, value.Top)
}
if value.Right.Type != Auto {
bounds.Set(Right, value.Right)
bounds.setRaw(Right, value.Right)
}
if value.Bottom.Type != Auto {
bounds.Set(Bottom, value.Bottom)
bounds.setRaw(Bottom, value.Bottom)
}
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:
properties.properties[tag] = value
properties.setRaw(tag, value)
case DataObject:
bounds := NewBoundsProperty(nil)
for _, tag := range []string{Top, Right, Bottom, Left} {
if text, ok := value.PropertyValue(tag); ok {
for _, tag := range []PropertyName{Top, Right, Bottom, Left} {
if text, ok := value.PropertyValue(string(tag)); ok {
if !bounds.Set(tag, text) {
notCompatibleType(tag, value)
return false
return nil
}
}
}
properties.properties[tag] = bounds
properties.setRaw(tag, bounds)
default:
if n, ok := isInt(value); ok {
properties.properties[tag] = Px(float64(n))
properties.setRaw(tag, Px(float64(n)))
} else {
notCompatibleType(tag, value)
return false
return nil
}
}
}
return true
return []PropertyName{tag}
}
func (properties *propertyList) boundsProperty(tag string) BoundsProperty {
if value, ok := properties.properties[tag]; ok {
func removeBoundsPropertySide(properties Properties, mainTag, sideTag PropertyName) []PropertyName {
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) {
case string:
bounds := NewBoundsProperty(nil)
for _, t := range []string{Top, Right, Bottom, Left} {
for _, t := range []PropertyName{Top, Right, Bottom, Left} {
bounds.Set(t, value)
}
return bounds
case SizeUnit:
bounds := NewBoundsProperty(nil)
for _, t := range []string{Top, Right, Bottom, Left} {
for _, t := range []PropertyName{Top, Right, Bottom, Left} {
bounds.Set(t, value)
}
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) {
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) {
func getBounds(properties Properties, tag PropertyName, session Session) (Bounds, bool) {
if value := properties.Get(tag); value != nil {
switch value := value.(type) {
case string:

View File

@ -1,36 +1,86 @@
package rui
// Button - button view
import "strings"
// Button represent a Button view
type Button interface {
CustomView
ListLayout
}
type buttonData struct {
CustomViewData
listLayoutData
}
// NewButton create new Button object and return it
func NewButton(session Session, params Params) Button {
button := new(buttonData)
InitCustomView(button, "Button", session, params)
button.init(session)
setInitParams(button, params)
return button
}
func newButton(session Session) View {
return NewButton(session, nil)
return new(buttonData)
}
func (button *buttonData) CreateSuperView(session Session) View {
return NewListLayout(session, Params{
Semantics: ButtonSemantics,
Style: "ruiButton",
StyleDisabled: "ruiDisabledButton",
HorizontalAlign: CenterAlign,
VerticalAlign: CenterAlign,
Orientation: StartToEndOrientation,
})
func (button *buttonData) init(session Session) {
button.listLayoutData.init(session)
button.tag = "Button"
button.systemClass = "ruiButton"
button.setRaw(Style, "ruiEnabledButton")
button.setRaw(StyleDisabled, "ruiDisabledButton")
button.setRaw(Semantics, ButtonSemantics)
button.setRaw(TabIndex, 0)
}
func (button *buttonData) Focusable() bool {
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
}

823
canvas.go

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,25 @@
package rui
import "strings"
import "reflect"
// DrawFunction is the constant for the "draw-function" property tag.
// The "draw-function" property sets the draw function of CanvasView.
// The function should have the following format: func(Canvas)
const DrawFunction = "draw-function"
// DrawFunction is the constant for "draw-function" property tag.
//
// Used by `CanvasView`.
// Property sets the draw function of `CanvasView`.
//
// Supported types: `func(Canvas)`.
const DrawFunction PropertyName = "draw-function"
// CanvasView interface of a custom draw view
type CanvasView interface {
View
// Redraw forces CanvasView to redraw its content
Redraw()
}
type canvasViewData struct {
viewData
drawer func(Canvas)
}
// NewCanvasView creates the new custom draw view
@ -27,21 +31,21 @@ func NewCanvasView(session Session, params Params) CanvasView {
}
func newCanvasView(session Session) View {
return NewCanvasView(session, nil)
return new(canvasViewData)
}
// Init initialize fields of ViewsContainer by default values
func (canvasView *canvasViewData) init(session Session) {
canvasView.viewData.init(session)
canvasView.tag = "CanvasView"
canvasView.normalize = normalizeCanvasViewTag
canvasView.set = canvasView.setFunc
canvasView.remove = canvasView.removeFunc
canvasView.changed = canvasView.propertyChanged
}
func (canvasView *canvasViewData) String() string {
return getViewString(canvasView)
}
func (canvasView *canvasViewData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeCanvasViewTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case "draw-func":
tag = DrawFunction
@ -49,51 +53,44 @@ func (canvasView *canvasViewData) normalizeTag(tag string) string {
return tag
}
func (canvasView *canvasViewData) Remove(tag string) {
canvasView.remove(canvasView.normalizeTag(tag))
}
func (canvasView *canvasViewData) remove(tag string) {
func (canvasView *canvasViewData) removeFunc(tag PropertyName) []PropertyName {
if tag == DrawFunction {
canvasView.drawer = nil
canvasView.Redraw()
canvasView.propertyChangedEvent(tag)
} else {
canvasView.viewData.remove(tag)
if canvasView.getRaw(DrawFunction) != nil {
canvasView.setRaw(DrawFunction, nil)
//canvasView.Redraw()
return []PropertyName{DrawFunction}
}
return []PropertyName{}
}
return canvasView.viewData.removeFunc(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 {
func (canvasView *canvasViewData) setFunc(tag PropertyName, value any) []PropertyName {
if tag == DrawFunction {
if value == nil {
canvasView.drawer = nil
} else if fn, ok := value.(func(Canvas)); ok {
canvasView.drawer = fn
} else {
switch value := value.(type) {
case func(Canvas):
canvasView.setRaw(DrawFunction, value)
case string:
canvasView.setRaw(DrawFunction, value)
default:
notCompatibleType(tag, value)
return false
return nil
}
canvasView.Redraw()
canvasView.propertyChangedEvent(tag)
return true
return []PropertyName{DrawFunction}
}
return canvasView.viewData.set(tag, value)
return canvasView.viewData.setFunc(tag, value)
}
func (canvasView *canvasViewData) Get(tag string) any {
return canvasView.get(canvasView.normalizeTag(tag))
}
func (canvasView *canvasViewData) get(tag string) any {
func (canvasView *canvasViewData) propertyChanged(tag PropertyName) {
if tag == DrawFunction {
return canvasView.drawer
canvasView.Redraw()
} else {
canvasView.viewData.propertyChanged(tag)
}
return canvasView.viewData.get(tag)
}
func (canvasView *canvasViewData) htmlTag() string {
@ -101,14 +98,36 @@ func (canvasView *canvasViewData) htmlTag() string {
}
func (canvasView *canvasViewData) Redraw() {
if canvasView.drawer != nil {
canvas := newCanvas(canvasView)
canvas.ClearRect(0, 0, canvasView.frame.Width, canvasView.frame.Height)
if canvasView.drawer != nil {
canvasView.drawer(canvas)
if value := canvasView.getRaw(DrawFunction); value != nil {
switch drawer := value.(type) {
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
}
canvasView.session.runScript(canvas.finishDraw())
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)
}
}
}
canvas.finishDraw()
}
func (canvasView *canvasViewData) onResize(self View, x, y, width, height float64) {

View File

@ -1,199 +1,163 @@
package rui
import (
"fmt"
"strings"
)
// 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.
const CheckboxChangedEvent = "checkbox-event"
//
// Used by `Checkbox`.
// 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 {
ViewsContainer
}
type checkboxData struct {
viewsContainerData
checkedListeners []func(Checkbox, bool)
}
// NewCheckbox create new Checkbox object and return it
func NewCheckbox(session Session, params Params) Checkbox {
view := new(checkboxData)
view.init(session)
setInitParams(view, Params{
ClickEvent: checkboxClickListener,
KeyDownEvent: checkboxKeyListener,
})
setInitParams(view, params)
return view
}
func newCheckbox(session Session) View {
return NewCheckbox(session, nil)
return new(checkboxData)
}
func (button *checkboxData) init(session Session) {
button.viewsContainerData.init(session)
button.tag = "Checkbox"
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 {
return getViewString(button)
button.setRaw(ClickEvent, []oneArgListener[View, MouseEvent]{newOneArgListenerVE(checkboxClickListener)})
button.setRaw(KeyDownEvent, []oneArgListener[View, KeyEvent]{newOneArgListenerVE(checkboxKeyListener)})
}
func (button *checkboxData) Focusable() bool {
return true
}
func (button *checkboxData) Get(tag string) any {
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 {
func (button *checkboxData) propertyChanged(tag PropertyName) {
switch tag {
case CheckboxChangedEvent:
if !button.setChangedListener(value) {
notCompatibleType(tag, value)
return false
}
case Checked:
oldChecked := button.checked()
if !button.setBoolProperty(Checked, value) {
return false
session := button.Session()
checked := IsCheckboxChecked(button)
if listeners := getOneArgEventListeners[Checkbox, bool](button, nil, CheckboxChangedEvent); len(listeners) > 0 {
for _, listener := range listeners {
listener.Run(button, checked)
}
if button.created {
checked := button.checked()
if checked != oldChecked {
button.changedCheckboxState(checked)
}
}
case CheckboxHorizontalAlign, CheckboxVerticalAlign:
if !button.setEnumProperty(tag, value, enumProperties[tag].values) {
return false
}
if button.created {
htmlID := button.htmlID()
updateCSSStyle(htmlID, button.session)
updateInnerHTML(htmlID, button.session)
}
case VerticalAlign:
if !button.setEnumProperty(tag, value, enumProperties[tag].values) {
return false
}
if button.created {
updateCSSProperty(button.htmlID()+"content", "align-items", button.cssVerticalAlign(), button.session)
}
case HorizontalAlign:
if !button.setEnumProperty(tag, value, enumProperties[tag].values) {
return false
}
if button.created {
updateCSSProperty(button.htmlID()+"content", "justify-items", button.cssHorizontalAlign(), button.session)
}
case CellVerticalAlign, CellHorizontalAlign, CellWidth, CellHeight:
return false
default:
return button.viewsContainerData.set(tag, value)
}
button.propertyChangedEvent(tag)
return true
}
func (button *checkboxData) Remove(tag string) {
button.remove(strings.ToLower(tag))
}
func (button *checkboxData) remove(tag string) {
switch tag {
case ClickEvent:
if !button.viewsContainerData.set(ClickEvent, checkboxClickListener) {
delete(button.properties, tag)
}
case KeyDownEvent:
if !button.viewsContainerData.set(KeyDownEvent, checkboxKeyListener) {
delete(button.properties, tag)
}
case CheckboxChangedEvent:
if len(button.checkedListeners) > 0 {
button.checkedListeners = []func(Checkbox, bool){}
}
case Checked:
oldChecked := button.checked()
delete(button.properties, tag)
if button.created && oldChecked {
button.changedCheckboxState(false)
}
case CheckboxHorizontalAlign, CheckboxVerticalAlign:
delete(button.properties, tag)
if button.created {
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()
defer freeStringBuilder(buffer)
button.htmlCheckbox(buffer, state)
button.Session().runScript(fmt.Sprintf(`updateInnerHTML('%v', '%v');`, button.htmlID()+"checkbox", buffer.String()))
checkboxHtml(button, buffer, checked)
session.updateInnerHTML(button.htmlID()+"checkbox", buffer.String())
case CheckboxHorizontalAlign, CheckboxVerticalAlign:
htmlID := button.htmlID()
session := button.Session()
updateCSSStyle(htmlID, session)
updateInnerHTML(htmlID, session)
case VerticalAlign:
button.Session().updateCSSProperty(button.htmlID()+"content", "align-items", checkboxVerticalAlignCSS(button))
case HorizontalAlign:
button.Session().updateCSSProperty(button.htmlID()+"content", "justify-items", checkboxHorizontalAlignCSS(button))
case AccentColor:
updateInnerHTML(button.htmlID(), button.Session())
default:
button.viewsContainerData.propertyChanged(tag)
}
}
func checkboxClickListener(view View) {
func (button *checkboxData) getFunc(tag PropertyName) any {
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) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case ClickEvent:
if listeners, ok := valueToOneArgEventListeners[View, MouseEvent](value); ok && listeners != nil {
listeners = append(listeners, newOneArgListenerVE(checkboxClickListener))
button.setRaw(tag, listeners)
return []PropertyName{tag}
}
return nil
case KeyDownEvent:
if listeners, ok := valueToOneArgEventListeners[View, KeyEvent](value); ok && listeners != nil {
listeners = append(listeners, newOneArgListenerVE(checkboxKeyListener))
button.setRaw(tag, listeners)
return []PropertyName{tag}
}
return nil
case CheckboxChangedEvent:
return setOneArgEventListener[Checkbox, bool](button, tag, value)
case Checked:
return setBoolProperty(button, Checked, value)
case CellVerticalAlign, CellHorizontalAlign, CellWidth, CellHeight:
ErrorLogF(`"%s" property is not compatible with the BoundsProperty`, string(tag))
return nil
}
return button.viewsContainerData.setFunc(tag, value)
}
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))
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) {
session := button.Session()
vAlign := GetCheckboxVerticalAlign(button)
@ -242,10 +195,11 @@ func (button *checkboxData) cssStyle(self View, builder cssBuilder) {
builder.add("align-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)
hAlign := GetCheckboxHorizontalAlign(button)
@ -279,10 +233,16 @@ func (button *checkboxData) htmlCheckbox(buffer *strings.Builder, checked bool)
}
buffer.WriteString(`">`)
accentColor := Color(0)
if color := GetAccentColor(button, ""); color != 0 {
accentColor = color
}
if checked {
buffer.WriteString(button.Session().checkboxOnImage())
buffer.WriteString(button.Session().checkboxOnImage(accentColor))
} else {
buffer.WriteString(button.Session().checkboxOffImage())
buffer.WriteString(button.Session().checkboxOffImage(accentColor))
}
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) {
vCheckboxAlign, hCheckboxAlign := button.htmlCheckbox(buffer, IsCheckboxChecked(button))
vCheckboxAlign, hCheckboxAlign := checkboxHtml(button, buffer, IsCheckboxChecked(button))
buffer.WriteString(`<div id="`)
buffer.WriteString(button.htmlID())
@ -309,11 +269,11 @@ func (button *checkboxData) htmlSubviews(self View, buffer *strings.Builder) {
}
buffer.WriteString(" align-items: ")
buffer.WriteString(button.cssVerticalAlign())
buffer.WriteString(checkboxVerticalAlignCSS(button))
buffer.WriteRune(';')
buffer.WriteString(" justify-items: ")
buffer.WriteString(button.cssHorizontalAlign())
buffer.WriteString(checkboxHorizontalAlignCSS(button))
buffer.WriteRune(';')
buffer.WriteString(`">`)
@ -321,8 +281,8 @@ func (button *checkboxData) htmlSubviews(self View, buffer *strings.Builder) {
buffer.WriteString(`</div>`)
}
func (button *checkboxData) cssHorizontalAlign() string {
align := GetHorizontalAlign(button)
func checkboxHorizontalAlignCSS(view View) string {
align := GetHorizontalAlign(view)
values := enumProperties[CellHorizontalAlign].cssValues
if align >= 0 && align < len(values) {
return values[align]
@ -330,8 +290,8 @@ func (button *checkboxData) cssHorizontalAlign() string {
return values[0]
}
func (button *checkboxData) cssVerticalAlign() string {
align := GetVerticalAlign(button)
func checkboxVerticalAlignCSS(view View) string {
align := GetVerticalAlign(view)
values := enumProperties[CellVerticalAlign].cssValues
if align >= 0 && align < len(values) {
return values[align]
@ -340,19 +300,41 @@ func (button *checkboxData) cssVerticalAlign() string {
}
// 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 {
return boolStyledProperty(view, subviewID, Checked, false)
}
// 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 {
return enumStyledProperty(view, subviewID, CheckboxVerticalAlign, LeftAlign, false)
}
// 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 {
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
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
func (color Color) ARGB() (uint8, uint8, uint8, uint8) {
return uint8(color >> 24),

View File

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

View File

@ -1,23 +1,51 @@
package rui
import (
"fmt"
"strings"
)
// Constants for [ColorPicker] specific properties and events.
const (
ColorChangedEvent = "color-changed"
ColorPickerValue = "color-picker-value"
// ColorChangedEvent is the constant for "color-changed" property tag.
//
// 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 {
View
}
type colorPickerData struct {
viewData
colorChangedListeners []func(ColorPicker, Color)
}
// NewColorPicker create new ColorPicker object and return it
@ -29,118 +57,94 @@ func NewColorPicker(session Session, params Params) ColorPicker {
}
func newColorPicker(session Session) View {
return NewColorPicker(session, nil)
return new(colorPickerData)
}
func (picker *colorPickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "ColorPicker"
picker.colorChangedListeners = []func(ColorPicker, Color){}
picker.hasHtmlDisabled = true
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 {
return getViewString(picker)
}
func (picker *colorPickerData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeColorPickerTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Value, ColorTag:
return ColorPickerValue
}
return tag
return normalizeDataListTag(tag)
}
func (picker *colorPickerData) Remove(tag string) {
picker.remove(picker.normalizeTag(tag))
}
func (picker *colorPickerData) remove(tag string) {
func (picker *colorPickerData) getFunc(tag PropertyName) any {
switch tag {
case ColorChangedEvent:
if len(picker.colorChangedListeners) > 0 {
picker.colorChangedListeners = []func(ColorPicker, Color){}
picker.propertyChangedEvent(tag)
if listeners := getTwoArgEventRawListeners[ColorPicker, Color](picker, nil, tag); len(listeners) > 0 {
return listeners
}
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:
oldColor := GetColorPickerValue(picker)
delete(picker.properties, ColorPickerValue)
picker.colorChanged(oldColor)
default:
picker.viewData.remove(tag)
result := setColorProperty(picker, ColorPickerValue, value)
if result != nil {
picker.setRaw("old-color", oldColor)
}
}
return result
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
case DataList:
return setDataList(picker, value, "")
}
return picker.viewData.setFunc(tag, value)
}
func (picker *colorPickerData) propertyChanged(tag PropertyName) {
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
picker.propertyChangedEvent(tag)
return true
case ColorPickerValue:
oldColor := GetColorPickerValue(picker)
if picker.setColorProperty(ColorPickerValue, value) {
picker.colorChanged(oldColor)
return true
color := GetColorPickerValue(picker)
picker.Session().callFunc("setInputValue", picker.htmlID(), color.rgbString())
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:
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 {
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) {
picker.viewData.htmlProperties(self, buffer)
@ -152,26 +156,22 @@ func (picker *colorPickerData) htmlProperties(self View, buffer *strings.Builder
if picker.getRaw(ClickEvent) == nil {
buffer.WriteString(` onclick="stopEventPropagation(this, event)"`)
}
dataListHtmlProperties(picker, buffer)
}
func (picker *colorPickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
if IsDisabled(self) {
buffer.WriteString(` disabled`)
}
picker.viewData.htmlDisabledProperties(self, buffer)
}
func (picker *colorPickerData) handleCommand(self View, command string, data DataObject) bool {
func (picker *colorPickerData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command {
case "textChanged":
if text, ok := data.PropertyValue("text"); ok {
oldColor := GetColorPickerValue(picker)
if color, ok := StringToColor(text); ok {
oldColor := GetColorPickerValue(picker)
picker.properties[ColorPickerValue] = color
if color != oldColor {
for _, listener := range picker.colorChangedListeners {
listener(picker, color)
for _, listener := range getTwoArgEventListeners[ColorPicker, Color](picker, nil, ColorChangedEvent) {
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.
// 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
if value, ok := colorProperty(view, ColorPickerValue, view.Session()); ok {
return value
}
for _, tag := range []string{ColorPickerValue, Value, ColorTag} {
for _, tag := range []PropertyName{ColorPickerValue, Value, ColorTag} {
if value := valueFromStyle(view, tag); value != nil {
if result, ok := valueToColor(value, view.Session()); ok {
return result
@ -204,7 +203,18 @@ func GetColorPickerValue(view View, subviewID ...string) Color {
// GetColorChangedListeners returns the ColorChangedListener list of an ColorPicker subview.
// 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) {
return getEventListeners[ColorPicker, Color](view, subviewID, ColorChangedEvent)
//
// Result elements can be of the following types:
// - 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 (
"strconv"
"strings"
)
// Constants for [ColumnLayout] specific properties and events
const (
// ColumnCount is the constant for the "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
// the number of columns is calculated based on the "column-width" property
ColumnCount = "column-count"
// ColumnWidth is the constant for the "column-width" property tag.
// The "column-width" SizeUnit property specifies the width of each column.
ColumnWidth = "column-width"
// ColumnGap is the constant for the "column-gap" property tag.
// The "column-width" SizeUnit property sets the size of the gap (gutter) between columns.
ColumnGap = "column-gap"
// ColumnSeparator is the constant for the "column-separator" property tag.
// The "column-separator" property specifies the line drawn between columns in a multi-column layout.
ColumnSeparator = "column-separator"
// 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
// columns in a multi-column layout.
// Valid values are NoneLine (0), SolidLine (1), DashedLine (2), DottedLine (3), and DoubleLine (4).
ColumnSeparatorStyle = "column-separator-style"
// 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
// columns in a multi-column layout.
ColumnSeparatorWidth = "column-separator-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
// columns in a multi-column layout.
ColumnSeparatorColor = "column-separator-color"
// ColumnCount is the constant for "column-count" property tag.
//
// Used by ColumnLayout.
// Specifies number of columns into which the content is break. Values less than zero are not valid. If this property
// value is 0 then the number of columns is calculated based on the "column-width" property.
//
// Supported types: int, string.
//
// Values:
// - 0 or "0" - Use "column-width" to control how many columns will be created.
// - positive value - Тhe number of columns into which the content is divided.
ColumnCount PropertyName = "column-count"
// ColumnWidth is the constant for "column-width" property tag.
//
// Used by ColumnLayout.
// Specifies the width of each column.
//
// Supported types: SizeUnit, SizeFunc, string, float, int.
//
// Internal type is SizeUnit, other types converted to it during assignment.
// See [SizeUnit] description for more details.
ColumnWidth PropertyName = "column-width"
// ColumnGap is the constant for "column-gap" property tag.
//
// 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 {
ViewsContainer
}
@ -53,22 +133,20 @@ func NewColumnLayout(session Session, params Params) ColumnLayout {
}
func newColumnLayout(session Session) View {
return NewColumnLayout(session, nil)
return new(columnLayoutData)
}
// Init initialize fields of ColumnLayout by default values
func (ColumnLayout *columnLayoutData) init(session Session) {
ColumnLayout.viewsContainerData.init(session)
ColumnLayout.tag = "ColumnLayout"
//ColumnLayout.systemClass = "ruiColumnLayout"
func (columnLayout *columnLayoutData) init(session Session) {
columnLayout.viewsContainerData.init(session)
columnLayout.tag = "ColumnLayout"
columnLayout.systemClass = "ruiColumnLayout"
columnLayout.normalize = normalizeColumnLayoutTag
columnLayout.changed = columnLayout.propertyChanged
}
func (columnLayout *columnLayoutData) String() string {
return getViewString(columnLayout)
}
func (columnLayout *columnLayoutData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeColumnLayoutTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Gap:
return ColumnGap
@ -76,62 +154,28 @@ func (columnLayout *columnLayoutData) normalizeTag(tag string) string {
return tag
}
func (columnLayout *columnLayoutData) Get(tag string) any {
return columnLayout.get(columnLayout.normalizeTag(tag))
}
func (columnLayout *columnLayoutData) Remove(tag string) {
columnLayout.remove(columnLayout.normalizeTag(tag))
}
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())
}
}
}
func (columnLayout *columnLayoutData) Set(tag string, value any) bool {
return columnLayout.set(columnLayout.normalizeTag(tag), value)
}
func (columnLayout *columnLayoutData) set(tag string, value any) bool {
if value == nil {
columnLayout.remove(tag)
return true
}
if !columnLayout.viewsContainerData.set(tag, value) {
return false
}
if columnLayout.created {
func (columnLayout *columnLayoutData) propertyChanged(tag PropertyName) {
switch tag {
case ColumnSeparator:
css := ""
session := columnLayout.Session()
if val, ok := columnLayout.properties[ColumnSeparator]; ok {
separator := val.(ColumnSeparatorProperty)
css = separator.cssValue(columnLayout.Session())
if value := columnLayout.getRaw(ColumnSeparator); value != nil {
separator := value.(ColumnSeparatorProperty)
css = separator.cssValue(session)
}
updateCSSProperty(columnLayout.htmlID(), "column-rule", css, session)
session.updateCSSProperty(columnLayout.htmlID(), "column-rule", css)
case ColumnCount:
session := columnLayout.Session()
if count, ok := intProperty(columnLayout, tag, session, 0); ok && count > 0 {
updateCSSProperty(columnLayout.htmlID(), tag, strconv.Itoa(count), session)
if count := GetColumnCount(columnLayout); count > 0 {
session.updateCSSProperty(columnLayout.htmlID(), string(ColumnCount), strconv.Itoa(count))
} else {
updateCSSProperty(columnLayout.htmlID(), tag, "auto", session)
session.updateCSSProperty(columnLayout.htmlID(), string(ColumnCount), "auto")
}
default:
columnLayout.viewsContainerData.propertyChanged(tag)
}
}
return true
}
// 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
value := view.Get(ColumnSeparator)
if value == nil {
value = valueFromStyle(view, ColumnSeparator)
@ -206,3 +246,20 @@ func GetColumnSeparatorColor(view View, subviewID ...string) Color {
border := getColumnSeparator(view, subviewID)
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 (
"fmt"
"strings"
)
// ColumnSeparatorProperty is the interface of a view separator data
@ -10,19 +9,22 @@ type ColumnSeparatorProperty interface {
Properties
fmt.Stringer
stringWriter
// ViewBorder returns column separator description in a form of ViewBorder
ViewBorder(session Session) ViewBorder
cssValue(session Session) string
}
type columnSeparatorProperty struct {
propertyList
dataProperty
}
func newColumnSeparatorProperty(value any) ColumnSeparatorProperty {
if value == nil {
separator := new(columnSeparatorProperty)
separator.properties = map[string]any{}
separator.init()
return separator
}
@ -32,17 +34,18 @@ func newColumnSeparatorProperty(value any) ColumnSeparatorProperty {
case DataObject:
separator := new(columnSeparatorProperty)
separator.properties = map[string]any{}
for _, tag := range []string{Style, Width, ColorTag} {
if val, ok := value.PropertyValue(tag); ok && val != "" {
separator.set(tag, value)
separator.init()
for _, tag := range []PropertyName{Style, Width, ColorTag} {
if val, ok := value.PropertyValue(string(tag)); ok && val != "" {
propertiesSet(separator, tag, value)
}
}
return separator
case ViewBorder:
separator := new(columnSeparatorProperty)
separator.properties = map[string]any{
separator.init()
separator.properties = map[PropertyName]any{
Style: value.Style,
Width: value.Width,
ColorTag: value.Color,
@ -54,12 +57,17 @@ func newColumnSeparatorProperty(value any) ColumnSeparatorProperty {
return nil
}
// NewColumnSeparator creates the new ColumnSeparatorProperty
func NewColumnSeparator(params Params) ColumnSeparatorProperty {
// NewColumnSeparatorProperty creates the new 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.properties = map[string]any{}
separator.init()
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 {
separator.Set(tag, value)
}
@ -68,8 +76,29 @@ func NewColumnSeparator(params Params) ColumnSeparatorProperty {
return separator
}
func (separator *columnSeparatorProperty) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
// NewColumnSeparator creates the new ColumnSeparatorProperty.
//
// 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 {
case ColumnSeparatorStyle, "separator-style":
return Style
@ -84,69 +113,16 @@ func (separator *columnSeparatorProperty) normalizeTag(tag string) string {
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 {
return runStringWriter(separator)
}
func (separator *columnSeparatorProperty) Remove(tag string) {
switch tag = separator.normalizeTag(tag); tag {
case Style, Width, ColorTag:
delete(separator.properties, tag)
default:
ErrorLogF(`"%s" property is not compatible with the ColumnSeparatorProperty`, tag)
func getColumnSeparatorProperty(properties Properties) ColumnSeparatorProperty {
if val := properties.getRaw(ColumnSeparator); val != nil {
if separator, ok := val.(ColumnSeparatorProperty); ok {
return separator
}
}
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
}
@ -189,3 +165,10 @@ func (separator *columnSeparatorProperty) cssValue(session Session) 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.Grow(16 * 1024)
if kbSize > 0 {
builder.buffer.Grow(kbSize * 1024)
}
}
func (builder *cssStyleBuilder) finish() string {
@ -168,7 +170,7 @@ func (builder *cssStyleBuilder) finish() string {
func (builder *cssStyleBuilder) startMedia(rule string) {
if builder.buffer == nil {
builder.init()
builder.init(0)
}
builder.buffer.WriteString(`@media screen`)
builder.buffer.WriteString(rule)
@ -178,7 +180,7 @@ func (builder *cssStyleBuilder) startMedia(rule string) {
func (builder *cssStyleBuilder) endMedia() {
if builder.buffer == nil {
builder.init()
builder.init(0)
}
builder.buffer.WriteString(`}\n`)
builder.media = false
@ -192,7 +194,7 @@ func (builder *cssStyleBuilder) startStyle(name string) {
}
if builder.buffer == nil {
builder.init()
builder.init(0)
}
if builder.media {
builder.buffer.WriteString(`\t`)
@ -210,7 +212,7 @@ func (builder *cssStyleBuilder) startStyle(name string) {
func (builder *cssStyleBuilder) endStyle() {
if builder.buffer == nil {
builder.init()
builder.init(0)
}
if builder.media {
builder.buffer.WriteString(`\t`)
@ -220,7 +222,7 @@ func (builder *cssStyleBuilder) endStyle() {
func (builder *cssStyleBuilder) startAnimation(name string) {
if builder.buffer == nil {
builder.init()
builder.init(0)
}
builder.media = true
@ -231,7 +233,7 @@ func (builder *cssStyleBuilder) startAnimation(name string) {
func (builder *cssStyleBuilder) endAnimation() {
if builder.buffer == nil {
builder.init()
builder.init(0)
}
builder.buffer.WriteString(`}\n`)
builder.media = false
@ -239,7 +241,7 @@ func (builder *cssStyleBuilder) endAnimation() {
func (builder *cssStyleBuilder) startAnimationFrame(name string) {
if builder.buffer == nil {
builder.init()
builder.init(0)
}
builder.buffer.WriteString(`\t`)
@ -249,7 +251,7 @@ func (builder *cssStyleBuilder) startAnimationFrame(name string) {
func (builder *cssStyleBuilder) endAnimationFrame() {
if builder.buffer == nil {
builder.init()
builder.init(0)
}
builder.buffer.WriteString(`\t}\n`)
}
@ -257,7 +259,7 @@ func (builder *cssStyleBuilder) endAnimationFrame() {
func (builder *cssStyleBuilder) add(key, value string) {
if value != "" {
if builder.buffer == nil {
builder.init()
builder.init(0)
}
if builder.media {
builder.buffer.WriteString(`\t`)
@ -276,7 +278,7 @@ func (builder *cssStyleBuilder) addValues(key, separator string, values ...strin
}
if builder.buffer == nil {
builder.init()
builder.init(0)
}
if builder.media {
builder.buffer.WriteString(`\t`)

View File

@ -1,12 +1,20 @@
package rui
import "strings"
import (
"iter"
"strings"
)
// CustomView defines a custom view interface
type CustomView interface {
ViewsContainer
// CreateSuperView must be implemented to create a base view from which custom control has been built
CreateSuperView(session Session) View
// SuperView must be implemented to return a base view from which custom control has been built
SuperView() View
setSuperView(view View)
setTag(tag string)
}
@ -15,6 +23,7 @@ type CustomView interface {
type CustomViewData struct {
tag string
superView View
defaultParams Params
}
// InitCustomView initializes fields of CustomView by default values
@ -30,6 +39,9 @@ func InitCustomView(customView CustomView, tag string, session Session, params P
return true
}
func (customView *CustomViewData) init(session Session) {
}
// SuperView returns a super view
func (customView *CustomViewData) SuperView() View {
return customView.superView
@ -37,6 +49,12 @@ func (customView *CustomViewData) SuperView() View {
func (customView *CustomViewData) setSuperView(view 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) {
@ -45,52 +63,71 @@ func (customView *CustomViewData) setTag(tag string) {
// 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 (customView *CustomViewData) Get(tag string) any {
func (customView *CustomViewData) Get(tag PropertyName) any {
return customView.superView.Get(tag)
}
func (customView *CustomViewData) getRaw(tag string) any {
func (customView *CustomViewData) getRaw(tag PropertyName) any {
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)
}
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.
// 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) Set(tag string, value any) bool {
func (customView *CustomViewData) Set(tag PropertyName, value any) bool {
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)
}
func (customView *CustomViewData) SetChangeListener(tag string, listener func(View, string)) {
customView.superView.SetChangeListener(tag, listener)
func (customView *CustomViewData) SetParams(params Params) bool {
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
func (customView *CustomViewData) Remove(tag string) {
func (customView *CustomViewData) Remove(tag PropertyName) {
customView.superView.Remove(tag)
}
// AllTags returns an array of the set properties
func (customView *CustomViewData) AllTags() []string {
func (customView *CustomViewData) AllTags() []PropertyName {
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
func (customView *CustomViewData) Clear() {
customView.superView.Clear()
}
func (customView *CustomViewData) cssViewStyle(buffer cssBuilder, session Session) {
customView.superView.cssViewStyle(buffer, session)
}
// Session returns a current Session interface
func (customView *CustomViewData) Session() Session {
return customView.superView.Session()
@ -144,10 +181,12 @@ func (customView *CustomViewData) Frame() Frame {
return customView.superView.Frame()
}
// Scroll returns a location and size of a scrollable view in pixels
func (customView *CustomViewData) Scroll() Frame {
return customView.superView.Scroll()
}
// HasFocus returns "true" if the view has focus
func (customView *CustomViewData) HasFocus() bool {
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)
}
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)
}
@ -188,8 +227,8 @@ func (customView *CustomViewData) htmlProperties(self View, buffer *strings.Buil
customView.superView.htmlProperties(customView.superView, buffer)
}
func (customView *CustomViewData) htmlDisabledProperties(self View, buffer *strings.Builder) {
customView.superView.htmlDisabledProperties(customView.superView, buffer)
func (customView *CustomViewData) htmlDisabledProperty() bool {
return customView.superView.htmlDisabledProperty()
}
func (customView *CustomViewData) cssStyle(self View, builder cssBuilder) {
@ -243,13 +282,48 @@ func (customView *CustomViewData) RemoveView(index int) View {
return container.RemoveView(index)
}
}
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 {
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 + " { }"
}
@ -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 {
return customView.superView.Transition(tag)
}
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 {
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 {
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
}

560
data.go
View File

@ -1,48 +1,105 @@
package rui
import (
"errors"
"fmt"
"iter"
"slices"
"strings"
"unicode"
)
// DataValue interface of a data node value
type DataValue interface {
// IsObject returns "true" if data value is an object
IsObject() bool
// Object returns data value as a data object
Object() DataObject
// Value returns value as a string
Value() string
}
// DataObject interface of a data object
type DataObject interface {
DataValue
// Tag returns data object tag
Tag() string
// Properties() returns an iterator to access the properties
Properties() iter.Seq[DataNode]
// PropertyCount returns properties count
PropertyCount() int
// Property returns a data node corresponding to a property with specific index
Property(index int) DataNode
PropertyWithTag(tag string) DataNode
// PropertyByTag returns a data node corresponding to a property tag
PropertyByTag(tag string) DataNode
// PropertyValue returns a string value of a property with a specific tag
PropertyValue(tag string) (string, bool)
// PropertyObject returns an object value of a property with a specific tag
PropertyObject(tag string) DataObject
// SetPropertyValue sets a string value of a property with a specific tag
SetPropertyValue(tag, value string)
// SetPropertyObject sets an object value of a property with a specific tag
SetPropertyObject(tag string, object DataObject)
// ToParams create a params(map) representation of a data object
ToParams() Params
// PropertyByTag removes a data node corresponding to a property tag and returns it
RemovePropertyByTag(tag string) DataNode
}
// DataNodeType defines the type of DataNode
type DataNodeType int
// Constants which are used to describe a node type, see [DataNode]
const (
// TextNode - node is the pair "tag - text value". Syntax: <tag> = <text>
TextNode = 0
TextNode DataNodeType = 0
// ObjectNode - node is the pair "tag - object". Syntax: <tag> = <object name>{...}
ObjectNode = 1
ObjectNode DataNodeType = 1
// ArrayNode - node is the pair "tag - object". Syntax: <tag> = [...]
ArrayNode = 2
ArrayNode DataNodeType = 2
)
// DataNode interface of a data node
type DataNode interface {
// Tag returns a tag name
Tag() string
Type() int
// Type returns a node type. Possible values are TextNode, ObjectNode and ArrayNode
Type() DataNodeType
// Text returns node text
Text() string
// Object returns node as object if that node type is an object
Object() DataObject
// ArraySize returns array size if that node type is an array
ArraySize() int
// ArrayElement returns a value of an array if that node type is an array
ArrayElement(index int) DataValue
ArrayElements() []DataValue
// ArrayElements returns an array of objects if that node is an array
Array() []DataValue
// ArrayElements returns an iterator to access the array elements of objects if that node is an array
ArrayElements() iter.Seq[DataValue]
// ArrayAsParams returns an array of a params(map) if that node is an array
ArrayAsParams() []Params
}
/******************************************************************************/
@ -92,6 +149,16 @@ func (object *dataObject) Tag() string {
return object.tag
}
func (object *dataObject) Properties() iter.Seq[DataNode] {
return func(yield func(DataNode) bool) {
for _, node := range object.property {
if !yield(node) {
return
}
}
}
}
func (object *dataObject) PropertyCount() int {
if object.property != nil {
return len(object.property)
@ -106,7 +173,7 @@ func (object *dataObject) Property(index int) DataNode {
return object.property[index]
}
func (object *dataObject) PropertyWithTag(tag string) DataNode {
func (object *dataObject) PropertyByTag(tag string) DataNode {
if object.property != nil {
for _, node := range object.property {
if node.Tag() == tag {
@ -117,22 +184,44 @@ func (object *dataObject) PropertyWithTag(tag string) DataNode {
return nil
}
func (object *dataObject) RemovePropertyByTag(tag string) DataNode {
if object.property != nil {
for i, node := range object.property {
if node.Tag() == tag {
switch i {
case 0:
object.property = object.property[1:]
case len(object.property) - 1:
object.property = object.property[:len(object.property)-1]
default:
object.property = append(object.property[:i], object.property[i+1:]...)
}
return node
}
}
}
return nil
}
func (object *dataObject) PropertyValue(tag string) (string, bool) {
if node := object.PropertyWithTag(tag); node != nil && node.Type() == TextNode {
if node := object.PropertyByTag(tag); node != nil && node.Type() == TextNode {
return node.Text(), true
}
return "", false
}
func (object *dataObject) PropertyObject(tag string) DataObject {
if node := object.PropertyWithTag(tag); node != nil && node.Type() == ObjectNode {
if node := object.PropertyByTag(tag); node != nil && node.Type() == ObjectNode {
return node.Object()
}
return nil
}
func (object *dataObject) setNode(node DataNode) {
if object.property == nil || len(object.property) == 0 {
if len(object.property) == 0 {
object.property = []DataNode{node}
} else {
tag := node.Tag()
@ -159,10 +248,50 @@ func (object *dataObject) SetPropertyValue(tag, value string) {
// SetPropertyObject - set a property with tag by object
func (object *dataObject) SetPropertyObject(tag string, obj DataObject) {
if obj != nil {
node := new(dataNode)
node.tag = tag
node.value = obj
object.setNode(node)
} else {
object.RemovePropertyByTag(tag)
}
}
func (object *dataObject) ToParams() Params {
params := Params{}
for _, node := range object.property {
switch node.Type() {
case TextNode:
if text := node.Text(); text != "" {
params[PropertyName(node.Tag())] = text
}
case ObjectNode:
if obj := node.Object(); obj != nil {
params[PropertyName(node.Tag())] = node.Object()
}
case ArrayNode:
array := []any{}
for i := range node.ArraySize() {
if data := node.ArrayElement(i); data != nil {
if data.IsObject() {
if obj := data.Object(); obj != nil {
array = append(array, obj)
}
} else if text := data.Value(); text != "" {
array = append(array, text)
}
}
}
if len(array) > 0 {
params[PropertyName(node.Tag())] = array
}
}
}
return params
}
/******************************************************************************/
@ -176,7 +305,7 @@ func (node *dataNode) Tag() string {
return node.tag
}
func (node *dataNode) Type() int {
func (node *dataNode) Type() DataNodeType {
if node.array != nil {
return ArrayNode
}
@ -214,61 +343,86 @@ func (node *dataNode) ArrayElement(index int) DataValue {
return nil
}
func (node *dataNode) ArrayElements() []DataValue {
func (node *dataNode) Array() []DataValue {
if node.array != nil {
return node.array
return slices.Clone(node.array)
}
return []DataValue{}
return []DataValue{node.value}
}
// ParseDataText - parse text and return DataNode
func ParseDataText(text string) DataObject {
if strings.ContainsAny(text, "\r") {
text = strings.Replace(text, "\r\n", "\n", -1)
text = strings.Replace(text, "\r", "\n", -1)
func (node *dataNode) ArrayElements() iter.Seq[DataValue] {
return func(yield func(DataValue) bool) {
if node.array != nil {
for _, element := range node.array {
if !yield(element) {
return
}
data := append([]rune(text), rune(0))
pos := 0
size := len(data) - 1
line := 1
lineStart := 0
}
} else {
yield(node.value)
}
}
}
skipSpaces := func(skipNewLine bool) {
for pos < size {
switch data[pos] {
func (node *dataNode) ArrayAsParams() []Params {
result := []Params{}
if node.array != nil {
for _, data := range node.array {
if data.IsObject() {
if obj := data.Object(); obj != nil {
if params := obj.ToParams(); len(params) > 0 {
result = append(result, params)
}
}
}
}
}
return result
}
type dataParser struct {
data []rune
size int
pos int
line int
lineStart int
}
func (parser *dataParser) skipSpaces(skipNewLine bool) {
for parser.pos < parser.size {
switch parser.data[parser.pos] {
case '\n':
if !skipNewLine {
return
}
line++
lineStart = pos + 1
parser.line++
parser.lineStart = parser.pos + 1
case '/':
if pos+1 < size {
switch data[pos+1] {
if parser.pos+1 < parser.size {
switch parser.data[parser.pos+1] {
case '/':
pos += 2
for pos < size && data[pos] != '\n' {
pos++
parser.pos += 2
for parser.pos < parser.size && parser.data[parser.pos] != '\n' {
parser.pos++
}
pos--
parser.pos--
case '*':
pos += 3
parser.pos += 3
for {
if pos >= size {
if parser.pos >= parser.size {
ErrorLog("Unexpected end of file")
return
}
if data[pos-1] == '*' && data[pos] == '/' {
if parser.data[parser.pos-1] == '*' && parser.data[parser.pos] == '/' {
break
}
if data[pos-1] == '\n' {
line++
lineStart = pos
if parser.data[parser.pos-1] == '\n' {
parser.line++
parser.lineStart = parser.pos
}
pos++
parser.pos++
}
default:
@ -280,75 +434,72 @@ func ParseDataText(text string) DataObject {
// do nothing
default:
if !unicode.IsSpace(data[pos]) {
if !unicode.IsSpace(parser.data[parser.pos]) {
return
}
}
pos++
}
parser.pos++
}
}
parseTag := func() (string, bool) {
skipSpaces(true)
startPos := pos
if data[pos] == '`' {
pos++
func (parser *dataParser) parseTag() (string, error) {
parser.skipSpaces(true)
startPos := parser.pos
switch parser.data[parser.pos] {
case '`':
parser.pos++
startPos++
for data[pos] != '`' {
pos++
if pos >= size {
ErrorLog("Unexpected end of text")
return string(data[startPos:size]), false
for parser.data[parser.pos] != '`' {
parser.pos++
if parser.pos >= parser.size {
return string(parser.data[startPos:parser.size]), errors.New("unexpected end of text")
}
}
str := string(data[startPos:pos])
pos++
return str, true
str := string(parser.data[startPos:parser.pos])
parser.pos++
return str, nil
} else if data[pos] == '\'' || data[pos] == '"' {
stopSymbol := data[pos]
pos++
case '\'', '"':
stopSymbol := parser.data[parser.pos]
parser.pos++
startPos++
slash := false
for stopSymbol != data[pos] {
if data[pos] == '\\' {
pos += 2
for stopSymbol != parser.data[parser.pos] {
if parser.data[parser.pos] == '\\' {
parser.pos += 2
slash = true
} else {
pos++
parser.pos++
}
if pos >= size {
ErrorLog("Unexpected end of text")
return string(data[startPos:size]), false
if parser.pos >= parser.size {
return string(parser.data[startPos:parser.size]), errors.New("unexpected end of text")
}
}
if !slash {
str := string(data[startPos:pos])
pos++
skipSpaces(false)
return str, true
str := string(parser.data[startPos:parser.pos])
parser.pos++
parser.skipSpaces(false)
return str, nil
}
buffer := make([]rune, pos-startPos+1)
buffer := make([]rune, parser.pos-startPos+1)
n1 := 0
n2 := startPos
invalidEscape := func() (string, bool) {
str := string(data[startPos:pos])
pos++
ErrorLogF("Invalid escape sequence in \"%s\" (position %d)", str, n2-2-startPos)
return str, false
invalidEscape := func() (string, error) {
str := string(parser.data[startPos:parser.pos])
parser.pos++
return str, fmt.Errorf(`invalid escape sequence in "%s" (position %d)`, str, n2-2-startPos)
}
for n2 < pos {
if data[n2] != '\\' {
buffer[n1] = data[n2]
for n2 < parser.pos {
if parser.data[n2] != '\\' {
buffer[n1] = parser.data[n2]
n2++
} else {
n2 += 2
switch data[n2-1] {
switch parser.data[n2-1] {
case 'n':
buffer[n1] = '\n'
@ -368,12 +519,12 @@ func ParseDataText(text string) DataObject {
buffer[n1] = '\\'
case 'x', 'X':
if n2+2 > pos {
if n2+2 > parser.pos {
return invalidEscape()
}
x := 0
for i := 0; i < 2; i++ {
ch := data[n2]
for range 2 {
ch := parser.data[n2]
if ch >= '0' && ch <= '9' {
x = x*16 + int(ch-'0')
} else if ch >= 'a' && ch <= 'f' {
@ -388,12 +539,12 @@ func ParseDataText(text string) DataObject {
buffer[n1] = rune(x)
case 'u', 'U':
if n2+4 > pos {
if n2+4 > parser.pos {
return invalidEscape()
}
x := 0
for i := 0; i < 4; i++ {
ch := data[n2]
for range 4 {
ch := parser.data[n2]
if ch >= '0' && ch <= '9' {
x = x*16 + int(ch-'0')
} else if ch >= 'a' && ch <= 'f' {
@ -408,97 +559,84 @@ func ParseDataText(text string) DataObject {
buffer[n1] = rune(x)
default:
str := string(data[startPos:pos])
ErrorLogF("Invalid escape sequence in \"%s\" (position %d)", str, n2-2-startPos)
return str, false
str := string(parser.data[startPos:parser.pos])
return str, fmt.Errorf(`invalid escape sequence in "%s" (position %d)`, str, n2-2-startPos)
}
}
n1++
}
pos++
skipSpaces(false)
return string(buffer[0:n1]), true
parser.pos++
parser.skipSpaces(false)
return string(buffer[0:n1]), nil
}
stopSymbol := func(symbol rune) bool {
if unicode.IsSpace(symbol) {
return true
}
for _, sym := range []rune{'=', '{', '}', '[', ']', ',', ' ', '\t', '\n', '\'', '"', '`', '/'} {
if sym == symbol {
return true
}
}
return false
for parser.pos < parser.size && !parser.stopSymbol(parser.data[parser.pos]) {
parser.pos++
}
for pos < size && !stopSymbol(data[pos]) {
pos++
}
endPos := pos
skipSpaces(false)
endPos := parser.pos
parser.skipSpaces(false)
if startPos == endPos {
ErrorLog("empty tag")
return "", false
}
return string(data[startPos:endPos]), true
//ErrorLog("empty tag")
return "", nil
}
return string(parser.data[startPos:endPos]), nil
}
var parseObject func(tag string) DataObject
var parseArray func() []DataValue
func (parser *dataParser) stopSymbol(symbol rune) bool {
return unicode.IsSpace(symbol) ||
slices.Contains([]rune{'=', '{', '}', '[', ']', ',', ' ', '\t', '\n', '\'', '"', '`', '/'}, symbol)
}
parseNode := func() DataNode {
func (parser *dataParser) parseNode() (DataNode, error) {
var tag string
var ok bool
var err error
if tag, ok = parseTag(); !ok {
return nil
if tag, err = parser.parseTag(); err != nil {
return nil, err
}
skipSpaces(true)
if data[pos] != '=' {
ErrorLogF("expected '=' after a tag name (line: %d, position: %d)", line, pos-lineStart)
return nil
parser.skipSpaces(true)
if parser.data[parser.pos] != '=' {
return nil, fmt.Errorf("expected '=' after a tag name (line: %d, position: %d)", parser.line, parser.pos-parser.lineStart)
}
pos++
skipSpaces(true)
switch data[pos] {
parser.pos++
parser.skipSpaces(true)
switch parser.data[parser.pos] {
case '[':
node := new(dataNode)
node.tag = tag
if node.array = parseArray(); node.array == nil {
return nil
if node.array, err = parser.parseArray(); err != nil {
return nil, err
}
return node
return node, nil
case '{':
node := new(dataNode)
node.tag = tag
if node.value = parseObject("_"); node.value == nil {
return nil
if node.value, err = parser.parseObject("_"); err != nil {
return nil, err
}
return node
return node, nil
case '}', ']', '=':
ErrorLogF("Expected '[', '{' or a tag name after '=' (line: %d, position: %d)", line, pos-lineStart)
return nil
return nil, fmt.Errorf(`expected '[', '{' or a tag name after '=' (line: %d, position: %d)`, parser.line, parser.pos-parser.lineStart)
default:
var str string
if str, ok = parseTag(); !ok {
return nil
if str, err = parser.parseTag(); err != nil {
return nil, err
}
node := new(dataNode)
node.tag = tag
if data[pos] == '{' {
if node.value = parseObject(str); node.value == nil {
return nil
if parser.data[parser.pos] == '{' {
if node.value, err = parser.parseObject(str); err != nil {
return nil, err
}
} else {
val := new(dataStringValue)
@ -506,91 +644,88 @@ func ParseDataText(text string) DataObject {
node.value = val
}
return node
}
return node, nil
}
}
parseObject = func(tag string) DataObject {
if data[pos] != '{' {
ErrorLogF("Expected '{' (line: %d, position: %d)", line, pos-lineStart)
return nil
func (parser *dataParser) parseObject(tag string) (DataObject, error) {
if parser.data[parser.pos] != '{' {
return nil, fmt.Errorf(`expected '{' (line: %d, position: %d)`, parser.line, parser.pos-parser.lineStart)
}
pos++
parser.pos++
obj := new(dataObject)
obj.tag = tag
obj.property = []DataNode{}
for pos < size {
var node DataNode
skipSpaces(true)
if data[pos] == '}' {
pos++
skipSpaces(false)
return obj
for parser.pos < parser.size {
parser.skipSpaces(true)
if parser.data[parser.pos] == '}' {
parser.pos++
parser.skipSpaces(false)
return obj, nil
}
if node = parseNode(); node == nil {
return nil
node, err := parser.parseNode()
if err != nil {
return nil, err
}
obj.property = append(obj.property, node)
if data[pos] == '}' {
pos++
skipSpaces(true)
return obj
} else if data[pos] != ',' && data[pos] != '\n' {
ErrorLogF(`Expected '}', '\n' or ',' (line: %d, position: %d)`, line, pos-lineStart)
return nil
if parser.data[parser.pos] == '}' {
parser.pos++
parser.skipSpaces(true)
return obj, nil
} else if parser.data[parser.pos] != ',' && parser.data[parser.pos] != '\n' {
return nil, fmt.Errorf(`expected '}', '\n' or ',' (line: %d, position: %d)`, parser.line, parser.pos-parser.lineStart)
}
if data[pos] != '\n' {
pos++
if parser.data[parser.pos] != '\n' {
parser.pos++
}
skipSpaces(true)
for data[pos] == ',' {
pos++
skipSpaces(true)
parser.skipSpaces(true)
for parser.data[parser.pos] == ',' {
parser.pos++
parser.skipSpaces(true)
}
}
ErrorLog("Unexpected end of text")
return nil
}
return nil, errors.New("unexpected end of text")
}
parseArray = func() []DataValue {
pos++
skipSpaces(true)
func (parser *dataParser) parseArray() ([]DataValue, error) {
parser.pos++
parser.skipSpaces(true)
array := []DataValue{}
for pos < size {
var tag string
var ok bool
skipSpaces(true)
for data[pos] == ',' && pos < size {
pos++
skipSpaces(true)
for parser.pos < parser.size {
parser.skipSpaces(true)
for parser.data[parser.pos] == ',' && parser.pos < parser.size {
parser.pos++
parser.skipSpaces(true)
}
if pos >= size {
if parser.pos >= parser.size {
break
}
if data[pos] == ']' {
pos++
skipSpaces(true)
return array
if parser.data[parser.pos] == ']' {
parser.pos++
parser.skipSpaces(true)
return array, nil
}
if tag, ok = parseTag(); !ok {
return nil
tag, err := parser.parseTag()
if err != nil {
return nil, err
}
if data[pos] == '{' {
obj := parseObject(tag)
if obj == nil {
return nil
if parser.data[parser.pos] == '{' {
obj, err := parser.parseObject(tag)
if err != nil {
return nil, err
}
array = append(array, obj)
} else {
@ -599,12 +734,11 @@ func ParseDataText(text string) DataObject {
array = append(array, val)
}
switch data[pos] {
switch parser.data[parser.pos] {
case ']', ',', '\n':
default:
ErrorLogF("Expected ']' or ',' (line: %d, position: %d)", line, pos-lineStart)
return nil
return nil, fmt.Errorf(`expected ']' or ',' (line: %d, position: %d)`, parser.line, parser.pos-parser.lineStart)
}
/*
@ -620,12 +754,28 @@ func ParseDataText(text string) DataObject {
*/
}
ErrorLog("Unexpected end of text")
return nil
return nil, errors.New("unexpected end of text")
}
// ParseDataText - parse text and return DataNode
func ParseDataText(text string) (DataObject, error) {
if strings.ContainsAny(text, "\r") {
text = strings.ReplaceAll(text, "\r\n", "\n")
text = strings.ReplaceAll(text, "\r", "\n")
}
if tag, ok := parseTag(); ok {
return parseObject(tag)
parser := dataParser{
data: append([]rune(text), rune(0)),
pos: 0,
line: 1,
lineStart: 0,
}
return nil
parser.size = len(parser.data) - 1
tag, err := parser.parseTag()
if err != nil {
return nil, err
}
return parser.parseObject(tag)
}

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) {
SetErrorLog(func(text string) {
t.Error(text)
})
text := `obj1 {
key1 = val1,
key2=obj2{
@ -27,8 +23,10 @@ func TestParseDataText(t *testing.T) {
key3 = "\n \t \\ \r \" ' \X4F\x4e \U01Ea",` +
"key4=`" + `\n \t \\ \r \" ' \x8F \UF80a` + "`\r}"
obj := ParseDataText(text)
if obj != nil {
obj, err := ParseDataText(text)
if err != nil {
t.Error(err)
} else {
if 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)
}
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)
if key == nil {
t.Errorf(`%s.Property(%d) == nil`, obj.Tag(), index)
@ -118,7 +116,7 @@ func TestParseDataText(t *testing.T) {
type testKeyData struct {
tag string
nodeType int
nodeType DataNodeType
}
data := []testKeyData{
@ -173,9 +171,6 @@ func TestParseDataText(t *testing.T) {
}
}
SetErrorLog(func(text string) {
})
failText := []string{
" ",
"obj[]",
@ -204,7 +199,7 @@ func TestParseDataText(t *testing.T) {
}
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)
}
}

View File

@ -1,29 +1,124 @@
package rui
import (
"fmt"
"strconv"
"strings"
"time"
)
// Constants for [DatePicker] specific properties and events.
const (
DateChangedEvent = "date-changed"
DatePickerMin = "date-picker-min"
DatePickerMax = "date-picker-max"
DatePickerStep = "date-picker-step"
DatePickerValue = "date-picker-value"
// DateChangedEvent is the constant for "date-changed" property tag.
//
// Used by DatePicker.
// Occur when date picker value has been changed.
//
// 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 {
View
}
type datePickerData struct {
viewData
dateChangedListeners []func(DatePicker, time.Time)
}
// NewDatePicker create new DatePicker object and return it
@ -35,106 +130,37 @@ func NewDatePicker(session Session, params Params) DatePicker {
}
func newDatePicker(session Session) View {
return NewDatePicker(session, nil)
return new(datePickerData) // NewDatePicker(session, nil)
}
func (picker *datePickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "DatePicker"
picker.dateChangedListeners = []func(DatePicker, time.Time){}
}
func (picker *datePickerData) String() string {
return getViewString(picker)
picker.hasHtmlDisabled = true
picker.normalize = normalizeDatePickerTag
picker.set = picker.setFunc
picker.get = picker.getFunc
picker.changed = picker.propertyChanged
}
func (picker *datePickerData) Focusable() bool {
return true
}
func (picker *datePickerData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeDatePickerTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Type, Min, Max, Step, Value:
return "date-picker-" + tag
}
return tag
return normalizeDataListTag(tag)
}
func (picker *datePickerData) Remove(tag string) {
picker.remove(picker.normalizeTag(tag))
}
func (picker *datePickerData) remove(tag string) {
switch tag {
case DateChangedEvent:
if len(picker.dateChangedListeners) > 0 {
picker.dateChangedListeners = []func(DatePicker, time.Time){}
picker.propertyChangedEvent(tag)
}
return
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)
}
func (picker *datePickerData) Set(tag string, value any) bool {
return picker.set(picker.normalizeTag(tag), value)
}
func (picker *datePickerData) set(tag string, value any) bool {
if value == nil {
picker.remove(tag)
return true
}
setTimeValue := func(tag string) (time.Time, bool) {
switch value := value.(type) {
case time.Time:
picker.properties[tag] = value
return value, true
case string:
if text, ok := picker.Session().resolveConstants(value); ok {
func stringToDate(value string) (time.Time, bool) {
format := "20060102"
if strings.ContainsRune(text, '-') {
if part := strings.Split(text, "-"); len(part) == 3 {
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"
@ -147,122 +173,133 @@ func (picker *datePickerData) set(tag string, value any) bool {
format = "2006-01-02"
}
}
} else if strings.ContainsRune(text, ' ') {
if part := strings.Split(text, " "); len(part) == 3 {
} 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(text, '/') {
if part := strings.Split(text, "/"); len(part) == 3 {
} 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(text) == 6 {
} else if len(value) == 6 {
format = "010206"
}
if date, err := time.Parse(format, text); err == nil {
picker.properties[tag] = value
if date, err := time.Parse(format, value); err == nil {
return date, true
}
return time.Now(), false
}
func (picker *datePickerData) getFunc(tag PropertyName) any {
switch tag {
case DateChangedEvent:
if listeners := getTwoArgEventRawListeners[DatePicker, time.Time](picker, nil, tag); len(listeners) > 0 {
return listeners
}
return nil
}
return picker.viewData.getFunc(tag)
}
func (picker *datePickerData) setFunc(tag PropertyName, value any) []PropertyName {
setDateValue := func(tag PropertyName) []PropertyName {
switch value := value.(type) {
case time.Time:
picker.setRaw(tag, value)
return []PropertyName{tag}
case string:
if ok, _ := isConstantName(value); ok {
picker.setRaw(tag, value)
return []PropertyName{tag}
}
if date, ok := stringToDate(value); ok {
picker.setRaw(tag, date)
return []PropertyName{tag}
}
}
notCompatibleType(tag, value)
return time.Now(), false
return nil
}
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:
old, oldOK := getDateProperty(picker, DatePickerMin, Min)
if date, ok := setTimeValue(DatePickerMin); ok {
if !oldOK || date != old {
if picker.created {
updateProperty(picker.htmlID(), Min, date.Format(dateFormat), picker.session)
}
picker.propertyChangedEvent(tag)
}
return true
if date, ok := GetDatePickerMin(picker); ok {
session.updateProperty(picker.htmlID(), "min", date.Format(dateFormat))
} else {
session.removeProperty(picker.htmlID(), "min")
}
case DatePickerMax:
old, oldOK := getDateProperty(picker, DatePickerMax, Max)
if date, ok := setTimeValue(DatePickerMax); ok {
if !oldOK || date != old {
if picker.created {
updateProperty(picker.htmlID(), Max, date.Format(dateFormat), picker.session)
}
picker.propertyChangedEvent(tag)
}
return true
if date, ok := GetDatePickerMax(picker); ok {
session.updateProperty(picker.htmlID(), "max", date.Format(dateFormat))
} else {
session.removeProperty(picker.htmlID(), "max")
}
case DatePickerStep:
oldStep := GetDatePickerStep(picker)
if picker.setIntProperty(DatePickerStep, value) {
if step := GetDatePickerStep(picker); oldStep != step {
if picker.created {
if step > 0 {
updateProperty(picker.htmlID(), Step, strconv.Itoa(step), picker.session)
if step := GetDatePickerStep(picker); step > 0 {
session.updateProperty(picker.htmlID(), "step", strconv.Itoa(step))
} else {
removeProperty(picker.htmlID(), Step, picker.session)
}
}
picker.propertyChangedEvent(tag)
}
return true
session.removeProperty(picker.htmlID(), "step")
}
case DatePickerValue:
oldDate := GetDatePickerValue(picker)
if date, ok := setTimeValue(DatePickerValue); ok {
if date != oldDate {
if picker.created {
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%s')`, picker.htmlID(), date.Format(dateFormat)))
}
for _, listener := range picker.dateChangedListeners {
listener(picker, date)
}
picker.propertyChangedEvent(tag)
}
return true
}
date := GetDatePickerValue(picker)
session.callFunc("setInputValue", picker.htmlID(), date.Format(dateFormat))
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){}
if listeners := getTwoArgEventListeners[DatePicker, time.Time](picker, nil, DateChangedEvent); len(listeners) > 0 {
oldDate := time.Now()
if value := picker.getRaw("old-date"); value != nil {
if date, ok := value.(time.Time); ok {
oldDate = date
}
}
for _, listener := range listeners {
listener.Run(picker, date, oldDate)
}
}
picker.dateChangedListeners = listeners
picker.propertyChangedEvent(tag)
return true
default:
return picker.viewData.set(tag, value)
}
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)
picker.viewData.propertyChanged(tag)
}
}
@ -270,6 +307,16 @@ func (picker *datePickerData) htmlTag() string {
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) {
picker.viewData.htmlProperties(self, buffer)
@ -301,16 +348,11 @@ func (picker *datePickerData) htmlProperties(self View, buffer *strings.Builder)
if picker.getRaw(ClickEvent) == nil {
buffer.WriteString(` onclick="stopEventPropagation(this, event)"`)
}
dataListHtmlProperties(picker, buffer)
}
func (picker *datePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
if IsDisabled(self) {
buffer.WriteString(` disabled`)
}
picker.viewData.htmlDisabledProperties(self, buffer)
}
func (picker *datePickerData) handleCommand(self View, command string, data DataObject) bool {
func (picker *datePickerData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command {
case "textChanged":
if text, ok := data.PropertyValue("text"); ok {
@ -318,9 +360,10 @@ func (picker *datePickerData) handleCommand(self View, command string, data Data
oldValue := GetDatePickerValue(picker)
picker.properties[DatePickerValue] = value
if value != oldValue {
for _, listener := range picker.dateChangedListeners {
listener(picker, value)
for _, listener := range getTwoArgEventListeners[DatePicker, time.Time](picker, nil, DateChangedEvent) {
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)
}
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) {
if value != nil {
switch value := value.(type) {
@ -339,7 +382,7 @@ func getDateProperty(view View, mainTag, shortTag string) (time.Time, bool) {
case string:
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
}
}
@ -353,24 +396,25 @@ func getDateProperty(view View, mainTag, shortTag string) (time.Time, bool) {
return result, true
}
if value := valueFromStyle(view, shortTag); value != nil {
for _, tag := range []PropertyName{mainTag, shortTag} {
if value := valueFromStyle(view, tag); value != nil {
if result, ok := valueToTime(value); ok {
return result, true
}
}
}
}
return time.Now(), false
}
// 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.
// 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) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
return getDateProperty(view, DatePickerMin, Min)
}
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,
// "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) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
return getDateProperty(view, DatePickerMax, Max)
}
return time.Now(), false
}
// 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 {
return intStyledProperty(view, subviewID, DatePickerStep, 0)
}
// 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view == nil {
if view = getSubview(view, subviewID); view == nil {
return time.Now()
}
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.
// 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) {
return getEventListeners[DatePicker, time.Time](view, subviewID, DateChangedEvent)
//
// Result elements can be of the following types:
// - 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,
ruiCurrentTabColor = #FFFFFFFF,
ruiCurrentTabTextColor = #FF000000,
ruiTooltipBackground = #FFFFFFFF,
ruiTooltipTextColor = #FF000000,
ruiTooltipShadowColor = #FF808080,
},
colors:dark = _{
ruiTextColor = #FFE0E0E0,
@ -43,6 +46,9 @@ theme {
ruiTabTextColor = #FFE0E0E0,
ruiCurrentTabColor = #FF000000,
ruiCurrentTabTextColor = #FFFFFFFF,
ruiTooltipBackground = #FF303030,
ruiTooltipTextColor = #FFDDDDDD,
ruiTooltipShadowColor = #FFDDDDDD,
},
constants = _{
ruiButtonHorizontalPadding = 16px,
@ -76,8 +82,7 @@ theme {
background-color = @ruiBackgroundColor,
accent-color = @ruiHighlightColor,
},
ruiButton {
align = center,
ruiEnabledButton {
padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding",
margin = @ruiButtonMargin,
radius = @ruiButtonRadius,
@ -86,7 +91,6 @@ theme {
border = _{width = 1px, style = solid, color = @ruiButtonTextColor}
},
ruiDisabledButton {
align = center,
padding = "@ruiButtonVerticalPadding, @ruiButtonHorizontalPadding, @ruiButtonVerticalPadding, @ruiButtonHorizontalPadding",
margin = @ruiButtonMargin,
radius = @ruiButtonRadius,
@ -94,14 +98,34 @@ theme {
text-color = @ruiButtonDisabledTextColor,
border = _{width = 1px, style = solid, color = @ruiButtonDisabledTextColor}
},
ruiButton:hover {
ruiEnabledButton:hover {
text-color = @ruiTextColor,
background-color = @ruiBackgroundColor,
},
ruiButton:focus {
ruiEnabledButton:focus {
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
},
ruiCheckbox {
@ -110,8 +134,8 @@ theme {
margin = 2px,
},
ruiCheckbox:focus {
margin = 0,
border = _{style = solid, color = @ruiHighlightColor, width = 2px },
outline = _{style = solid, color = @ruiHighlightColor, width = 2px },
outline-offset = -1px,
},
ruiListItem {
radius = 4px,

View File

@ -2,17 +2,44 @@ package rui
import "strings"
// Constants for [DetailsView] specific properties and events
const (
// Summary is the constant for the "summary" property tag.
// The contents of the "summary" property are used as the label for the disclosure widget.
Summary = "summary"
// Expanded is the constant for the "expanded" property tag.
// If the "expanded" boolean property is "true", then the content of view is visible.
// If the value is "false" then the content is collapsed.
Expanded = "expanded"
// Summary is the constant for "summary" property tag.
//
// Used by DetailsView.
// The content of this property is used as the label for the disclosure widget.
//
// Supported types:
// - 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 {
ViewsContainer
}
@ -30,19 +57,21 @@ func NewDetailsView(session Session, params Params) DetailsView {
}
func newDetailsView(session Session) View {
return NewDetailsView(session, nil)
return new(detailsViewData)
}
// Init initialize fields of DetailsView by default values
func (detailsView *detailsViewData) init(session Session) {
detailsView.viewsContainerData.init(session)
detailsView.tag = "DetailsView"
detailsView.set = detailsView.setFunc
detailsView.changed = detailsView.propertyChanged
//detailsView.systemClass = "ruiDetailsView"
}
func (detailsView *detailsViewData) Views() []View {
views := detailsView.viewsContainerData.Views()
if summary := detailsView.get(Summary); summary != nil {
if summary := detailsView.Get(Summary); summary != nil {
switch summary := summary.(type) {
case View:
return append([]View{summary}, views...)
@ -51,94 +80,53 @@ func (detailsView *detailsViewData) Views() []View {
return views
}
func (detailsView *detailsViewData) Remove(tag string) {
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
}
func (detailsView *detailsViewData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case Summary:
switch value := value.(type) {
case string:
detailsView.properties[Summary] = value
detailsView.setRaw(Summary, value)
case View:
detailsView.properties[Summary] = value
detailsView.setRaw(Summary, value)
value.setParentID(detailsView.htmlID())
case DataObject:
if view := CreateViewFromObject(detailsView.Session(), value); view != nil {
detailsView.properties[Summary] = view
if view := CreateViewFromObject(detailsView.Session(), value, nil); view != nil {
detailsView.setRaw(Summary, view)
view.setParentID(detailsView.htmlID())
} else {
return false
return nil
}
default:
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:
if !detailsView.setBoolProperty(tag, value) {
notCompatibleType(tag, value)
return false
}
if detailsView.created {
if IsDetailsExpanded(detailsView) {
updateProperty(detailsView.htmlID(), "open", "", detailsView.Session())
detailsView.Session().updateProperty(detailsView.htmlID(), "open", "")
} else {
removeProperty(detailsView.htmlID(), "open", detailsView.Session())
}
detailsView.Session().removeProperty(detailsView.htmlID(), "open")
}
case NotTranslate:
if !detailsView.viewData.set(tag, value) {
return false
}
if detailsView.created {
updateInnerHTML(detailsView.htmlID(), detailsView.Session())
}
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 {
@ -154,31 +142,57 @@ func (detailsView *detailsViewData) htmlProperties(self View, buffer *strings.Bu
}
func (detailsView *detailsViewData) htmlSubviews(self View, buffer *strings.Builder) {
summary := false
hidden := IsSummaryMarkerHidden(detailsView)
if value, ok := detailsView.properties[Summary]; ok {
switch value := value.(type) {
case string:
if !GetNotTranslate(detailsView) {
value, _ = detailsView.session.GetString(value)
}
if hidden {
buffer.WriteString(`<summary class="hiddenMarker">`)
} else {
buffer.WriteString("<summary>")
}
buffer.WriteString(value)
buffer.WriteString("</summary>")
summary = true
case View:
buffer.WriteString("<summary>")
viewHTML(value, buffer)
if hidden {
buffer.WriteString(`<summary class="hiddenMarker">`)
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)
}
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 n, ok := dataIntProperty(data, "open"); ok {
detailsView.properties[Expanded] = (n != 0)
detailsView.propertyChangedEvent(Expanded)
detailsView.runChangeListener(Expanded)
}
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.
// 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
if value := view.Get(Summary); value != nil {
switch value := value.(type) {
case string:
@ -206,7 +219,17 @@ func GetDetailsSummary(view View, subviewID ...string) View {
}
// 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 {
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
import (
"fmt"
"strconv"
"strings"
)
// 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.
const DropDownEvent = "drop-down-event"
//
// Used by DropDownList.
// 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 {
View
getItems() []string
}
type dropDownListData struct {
viewData
items []string
disabledItems []any
dropDownListener []func(DropDownList, int)
}
// NewDropDownList create new DropDownList object and return it
@ -33,301 +43,162 @@ func NewDropDownList(session Session, params Params) DropDownList {
}
func newDropDownList(session Session) View {
return NewDropDownList(session, nil)
return new(dropDownListData)
}
func (list *dropDownListData) init(session Session) {
list.viewData.init(session)
list.tag = "DropDownList"
list.items = []string{}
list.disabledItems = []any{}
list.dropDownListener = []func(DropDownList, int){}
}
func (list *dropDownListData) String() string {
return getViewString(list)
list.hasHtmlDisabled = true
list.normalize = normalizeDropDownListTag
list.get = list.getFunc
list.set = list.setFunc
list.changed = list.propertyChanged
}
func (list *dropDownListData) Focusable() bool {
return true
}
func (list *dropDownListData) Remove(tag string) {
list.remove(strings.ToLower(tag))
func normalizeDropDownListTag(tag PropertyName) PropertyName {
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 {
case Items:
if len(list.items) > 0 {
list.items = []string{}
if list.created {
updateInnerHTML(list.htmlID(), list.session)
if items, ok := anyToStringArray(value, ""); ok {
return setArrayPropertyValue(list, tag, items)
}
list.propertyChangedEvent(tag)
notCompatibleType(Items, value)
return nil
case DisabledItems, ItemSeparators:
if items, ok := parseIndicesArray(value); ok {
return setArrayPropertyValue(list, tag, items)
}
case DisabledItems:
if len(list.disabledItems) > 0 {
list.disabledItems = []any{}
if list.created {
updateInnerHTML(list.htmlID(), list.session)
}
list.propertyChangedEvent(tag)
}
case DropDownEvent:
if len(list.dropDownListener) > 0 {
list.dropDownListener = []func(DropDownList, int){}
list.propertyChangedEvent(tag)
}
case Current:
oldCurrent := GetCurrent(list)
delete(list.properties, Current)
if oldCurrent != 0 {
if list.created {
list.session.runScript(fmt.Sprintf(`selectDropDownListItem('%s', %d)`, list.htmlID(), 0))
}
list.onSelectedItemChanged(0)
}
default:
list.viewData.remove(tag)
return
}
}
func (list *dropDownListData) Set(tag string, value any) bool {
return list.set(strings.ToLower(tag), value)
}
func (list *dropDownListData) set(tag string, value 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
return nil
case DropDownEvent:
return setTwoArgEventListener[DropDownList, int](list, tag, value)
case Current:
oldCurrent := GetCurrent(list)
if !list.setIntProperty(Current, value) {
return false
list.setRaw("old-current", GetCurrent(list))
return setIntProperty(list, Current, value)
}
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)
return list.viewData.setFunc(tag, value)
}
func (list *dropDownListData) setItems(value any) bool {
switch value := value.(type) {
case string:
list.items = []string{value}
func (list *dropDownListData) propertyChanged(tag PropertyName) {
switch tag {
case Items, DisabledItems, ItemSeparators:
updateInnerHTML(list.htmlID(), list.Session())
case []string:
list.items = value
case Current:
current := GetCurrent(list)
list.Session().callFunc("selectDropDownListItem", list.htmlID(), current)
case []DataValue:
list.items = make([]string, 0, len(value))
for _, val := range value {
if !val.IsObject() {
list.items = append(list.items, val.Value())
oldCurrent, _ := intProperty(list, "old-current", list.Session(), -1)
for _, listener := range getTwoArgEventListeners[DropDownList, int](list, nil, DropDownEvent) {
listener.Run(list, current, oldCurrent)
}
}
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 {
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 {
notCompatibleType(Items, value)
return false
list.viewData.propertyChanged(tag)
}
}
}
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 {
func intArrayToStringArray[T int | uint | int8 | uint8 | int16 | uint16 | int32 | uint32 | int64 | uint64](array []T) []string {
items := make([]string, len(array))
for i, val := range array {
items[i] = strconv.Itoa(int(val))
}
return items
}
func parseIndicesArray(value any) ([]any, bool) {
switch value := value.(type) {
case int:
return []any{value}, true
case []int:
list.disabledItems = make([]any, len(value))
items := make([]any, len(value))
for i, n := range value {
list.disabledItems[i] = n
items[i] = n
}
return items, true
case []any:
disabledItems := make([]any, len(value))
for i, val := range value {
if val == nil {
notCompatibleType(DisabledItems, value)
return false
}
items := make([]any, 0, len(value))
for _, val := range value {
if val != nil {
switch val := val.(type) {
case string:
if isConstantName(val) {
disabledItems[i] = val
if ok, _ := isConstantName(val); ok {
items = append(items, val)
} else if n, err := strconv.Atoi(val); err == nil {
items = append(items, n)
} else {
n, err := strconv.Atoi(val)
if err != nil {
notCompatibleType(DisabledItems, value)
return false
}
disabledItems[i] = n
return nil, false
}
default:
if n, ok := isInt(val); ok {
disabledItems[i] = n
items = append(items, n)
} else {
notCompatibleType(DisabledItems, value)
return false
return nil, false
}
}
}
}
return items, true
case []string:
items := make([]any, 0, len(value))
for _, str := range value {
if str = strings.Trim(str, " \t"); str != "" {
if ok, _ := isConstantName(str); ok {
items = append(items, str)
} else if n, err := strconv.Atoi(str); err == nil {
items = append(items, n)
} else {
return nil, false
}
list.disabledItems = disabledItems
}
}
return items, true
case string:
values := 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
return parseIndicesArray(strings.Split(value, ","))
case []DataValue:
disabledItems := make([]string, 0, len(value))
items := make([]string, 0, len(value))
for _, val := range value {
if !val.IsObject() {
disabledItems = append(disabledItems, val.Value())
items = append(items, val.Value())
}
}
return list.setDisabledItems(disabledItems)
default:
notCompatibleType(DisabledItems, value)
return false
return parseIndicesArray(items)
}
if list.created {
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
return nil, false
}
func (list *dropDownListData) htmlTag() string {
@ -335,11 +206,12 @@ func (list *dropDownListData) htmlTag() string {
}
func (list *dropDownListData) htmlSubviews(self View, buffer *strings.Builder) {
if list.items != nil {
if items := GetDropDownItems(list); len(items) > 0 {
current := GetCurrent(list)
notTranslate := GetNotTranslate(list)
disabledItems := GetDropDownDisabledItems(list)
for i, item := range list.items {
separators := GetDropDownItemSeparators(list)
for i, item := range items {
disabled := false
for _, index := range disabledItems {
if i == index {
@ -361,6 +233,12 @@ func (list *dropDownListData) htmlSubviews(self View, buffer *strings.Builder) {
buffer.WriteString(item)
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)"`)
}
func (list *dropDownListData) htmlDisabledProperties(self View, buffer *strings.Builder) {
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 {
func (list *dropDownListData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command {
case "itemSelected":
if text, ok := data.PropertyValue("number"); ok {
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.onSelectedItemChanged(number)
for _, listener := range getTwoArgEventListeners[DropDownList, int](list, nil, DropDownEvent) {
listener.Run(list, number, old)
}
list.runChangeListener(Current)
}
} else {
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.
// 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) {
return getEventListeners[DropDownList, int](view, subviewID, DropDownEvent)
//
// Result elements can be of the following types:
// - 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.
// 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
if view = getSubview(view, subviewID); view != nil {
if value := view.Get(Items); value != nil {
if items, ok := value.([]string); ok {
return items
}
if view != nil {
if list, ok := view.(DropDownList); ok {
return list.getItems()
}
}
return []string{}
}
// GetDropDownDisabledItems return the list of DropDownList disabled item indexes.
// 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])
}
func getIndicesArray(view View, tag PropertyName) []int {
if view != nil {
if value := view.Get(DisabledItems); value != nil {
if value := view.Get(tag); value != nil {
if values, ok := value.([]any); ok {
count := len(values)
if count > 0 {
@ -443,8 +318,8 @@ func GetDropDownDisabledItems(view View, subviewID ...string) []int {
result = append(result, value)
case string:
if value != "" && value[0] == '@' {
if val, ok := view.Session().Constant(value[1:]); ok {
if ok, constName := isConstantName(value); ok {
if val, ok := view.Session().Constant(constName); ok {
if n, err := strconv.Atoi(val); err == nil {
result = append(result, n)
}
@ -459,3 +334,21 @@ func GetDropDownDisabledItems(view View, subviewID ...string) []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
import (
"fmt"
"strconv"
"strings"
)
// Constants for [EditView] specific properties and events
const (
// EditTextChangedEvent is the constant for the "edit-text-changed" property tag.
EditTextChangedEvent = "edit-text-changed"
// EditViewType is the constant for the "edit-view-type" property tag.
EditViewType = "edit-view-type"
// EditViewPattern is the constant for the "edit-view-pattern" property tag.
EditViewPattern = "edit-view-pattern"
// Spellcheck is the constant for the "spellcheck" property tag.
Spellcheck = "spellcheck"
// EditTextChangedEvent is the constant for "edit-text-changed" property tag.
//
// Used by EditView.
// Occur when edit view text has been changed.
//
// General listener format:
// func(editView rui.EditView, newText string, oldText string).
//
// 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 (
// SingleLineText - single-line text type of EditView
SingleLineText = 0
// PasswordText - password type of EditView
PasswordText = 1
// EmailText - e-mail type of EditView. Allows to enter one email
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
// URLText - url type of EditView. Allows to enter one url
URLText = 4
// PhoneText - telephone type of EditView. Allows to enter one phone number
PhoneText = 5
// MultiLineText - multi-line text type of EditView
MultiLineText = 6
)
// EditView - grid-container of View
// EditView represent an EditView view
type EditView interface {
View
// AppendText appends text to the current text of an EditView view
AppendText(text string)
textChanged(newText, oldText string)
}
type editViewData struct {
viewData
textChangeListeners []func(EditView, string)
}
// NewEditView create new EditView object and return it
@ -54,25 +113,25 @@ func NewEditView(session Session, params Params) EditView {
}
func newEditView(session Session) View {
return NewEditView(session, nil)
return new(editViewData) // NewEditView(session, nil)
}
func (edit *editViewData) init(session Session) {
edit.viewData.init(session)
edit.textChangeListeners = []func(EditView, string){}
edit.hasHtmlDisabled = true
edit.tag = "EditView"
}
func (edit *editViewData) String() string {
return getViewString(edit)
edit.normalize = normalizeEditViewTag
edit.get = edit.getFunc
edit.set = edit.setFunc
edit.changed = edit.propertyChanged
}
func (edit *editViewData) Focusable() bool {
return true
}
func (edit *editViewData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeEditViewTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Type, "edit-type":
return EditViewType
@ -87,307 +146,144 @@ func (edit *editViewData) normalizeTag(tag string) string {
return EditWrap
}
return tag
return normalizeDataListTag(tag)
}
func (edit *editViewData) Remove(tag string) {
edit.remove(edit.normalizeTag(tag))
}
func (edit *editViewData) remove(tag string) {
_, exists := edit.properties[tag]
func (edit *editViewData) getFunc(tag PropertyName) any {
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:
if exists {
delete(edit.properties, Hint)
if edit.created {
removeProperty(edit.htmlID(), "placeholder", edit.session)
}
edit.propertyChangedEvent(tag)
if text, ok := value.(string); ok {
return setStringPropertyValue(edit, tag, strings.Trim(text, " \t\n"))
}
notCompatibleType(tag, value)
return nil
case MaxLength:
if exists {
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 DataList:
setDataList(edit, value, "")
case EditTextChangedEvent:
if len(edit.textChangeListeners) > 0 {
edit.textChangeListeners = []func(EditView, string){}
edit.propertyChangedEvent(tag)
return setTwoArgEventListener[EditView, string](edit, tag, value)
}
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 {
return edit.set(edit.normalizeTag(tag), value)
}
func (edit *editViewData) set(tag string, value any) bool {
if value == nil {
edit.remove(tag)
return true
}
func (edit *editViewData) propertyChanged(tag PropertyName) {
session := edit.Session()
switch tag {
case Text:
oldText := GetText(edit)
if text, ok := value.(string); ok {
edit.properties[Text] = text
if text = GetText(edit); oldText != text {
edit.textChanged(text)
if edit.created {
if GetEditViewType(edit) == MultiLineText {
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))
text := GetText(edit)
session.callFunc("setInputValue", edit.htmlID(), text)
old := ""
if val := edit.getRaw("old-text"); val != nil {
if txt, ok := val.(string); ok {
old = txt
}
}
}
return true
}
return false
edit.textChanged(text, old)
case Hint:
oldText := GetHint(edit)
if text, ok := value.(string); ok {
edit.properties[Hint] = text
if text = GetHint(edit); oldText != text {
if edit.created {
if text != "" {
updateProperty(edit.htmlID(), "placeholder", text, edit.session)
if text := GetHint(edit); text != "" {
session.updateProperty(edit.htmlID(), "placeholder", text)
} else {
removeProperty(edit.htmlID(), "placeholder", edit.session)
session.removeProperty(edit.htmlID(), "placeholder")
}
}
edit.propertyChangedEvent(tag)
}
return true
}
return false
case MaxLength:
oldMaxLength := GetMaxLength(edit)
if edit.setIntProperty(MaxLength, value) {
if maxLength := GetMaxLength(edit); maxLength != oldMaxLength {
if edit.created {
if maxLength > 0 {
updateProperty(edit.htmlID(), "maxlength", strconv.Itoa(maxLength), edit.session)
if maxLength := GetMaxLength(edit); maxLength > 0 {
session.updateProperty(edit.htmlID(), "maxlength", strconv.Itoa(maxLength))
} else {
removeProperty(edit.htmlID(), "maxlength", edit.session)
session.removeProperty(edit.htmlID(), "maxlength")
}
}
edit.propertyChangedEvent(tag)
}
return true
}
return false
case ReadOnly:
if edit.setBoolProperty(ReadOnly, value) {
if edit.created {
if IsReadOnly(edit) {
updateProperty(edit.htmlID(), ReadOnly, "", edit.session)
session.updateProperty(edit.htmlID(), "readonly", "")
} else {
removeProperty(edit.htmlID(), ReadOnly, edit.session)
session.removeProperty(edit.htmlID(), "readonly")
}
}
edit.propertyChangedEvent(tag)
return true
}
return false
case Spellcheck:
if edit.setBoolProperty(Spellcheck, value) {
if edit.created {
updateBoolProperty(edit.htmlID(), Spellcheck, IsSpellcheck(edit), edit.session)
}
edit.propertyChangedEvent(tag)
return true
}
return false
session.updateProperty(edit.htmlID(), "spellcheck", IsSpellcheck(edit))
case EditViewPattern:
oldText := GetEditViewPattern(edit)
if text, ok := value.(string); ok {
edit.properties[EditViewPattern] = text
if text = GetEditViewPattern(edit); oldText != text {
if edit.created {
if text != "" {
updateProperty(edit.htmlID(), Pattern, text, edit.session)
if text := GetEditViewPattern(edit); text != "" {
session.updateProperty(edit.htmlID(), "pattern", text)
} else {
removeProperty(edit.htmlID(), Pattern, edit.session)
session.removeProperty(edit.htmlID(), "pattern")
}
}
edit.propertyChangedEvent(tag)
}
return true
}
return false
case EditViewType:
oldType := GetEditViewType(edit)
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
updateInnerHTML(edit.parentHTMLID(), session)
case EditWrap:
oldWrap := IsEditViewWrap(edit)
if edit.setBoolProperty(EditWrap, value) {
if GetEditViewType(edit) == MultiLineText {
if wrap := IsEditViewWrap(edit); wrap != oldWrap {
if edit.created {
if wrap {
updateProperty(edit.htmlID(), "wrap", "soft", edit.session)
if wrap := IsEditViewWrap(edit); wrap {
session.updateProperty(edit.htmlID(), "wrap", "soft")
} else {
updateProperty(edit.htmlID(), "wrap", "off", edit.session)
}
}
edit.propertyChangedEvent(tag)
}
}
return true
}
return false
case EditTextChangedEvent:
listeners, ok := valueToEventListeners[EditView, string](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(EditView, string){}
}
edit.textChangeListeners = listeners
edit.propertyChangedEvent(tag)
return true
session.updateProperty(edit.htmlID(), "wrap", "off")
}
return edit.viewData.set(tag, value)
}
case DataList:
updateInnerHTML(edit.htmlID(), session)
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
default:
edit.viewData.propertyChanged(tag)
}
return edit.viewData.get(tag)
}
func (edit *editViewData) AppendText(text string) {
if GetEditViewType(edit) == MultiLineText {
if value := edit.getRaw(Text); value != nil {
if textValue, ok := value.(string); ok {
oldText := textValue
textValue += text
edit.properties[Text] = textValue
text := strings.ReplaceAll(text, `"`, `\"`)
text = strings.ReplaceAll(text, `'`, `\'`)
text = strings.ReplaceAll(text, "\n", `\n`)
text = strings.ReplaceAll(text, "\r", `\r`)
edit.session.runScript(`appendToInnerHTML("` + edit.htmlID() + `", "` + text + `")`)
edit.textChanged(textValue)
edit.session.callFunc("appendToInnerHTML", edit.htmlID(), text)
edit.session.callFunc("appendToInputValue", edit.htmlID(), text)
edit.textChanged(textValue, oldText)
return
}
}
edit.set(Text, text)
edit.Set(Text, text)
} else {
edit.set(Text, GetText(edit)+text)
edit.Set(Text, GetText(edit)+text)
}
}
func (edit *editViewData) textChanged(newText string) {
for _, listener := range edit.textChangeListeners {
listener(edit, newText)
func (edit *editViewData) textChanged(newText, oldText string) {
for _, listener := range getTwoArgEventListeners[EditView, string](edit, nil, EditTextChangedEvent) {
listener.Run(edit, newText, oldText)
}
edit.propertyChangedEvent(Text)
edit.runChangeListener(Text)
}
func (edit *editViewData) htmlTag() string {
@ -397,6 +293,17 @@ func (edit *editViewData) htmlTag() string {
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) {
edit.viewData.htmlProperties(self, buffer)
@ -452,7 +359,10 @@ func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) {
if strings.ContainsRune(text, '"') {
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 != "" {
@ -475,29 +385,18 @@ func (edit *editViewData) htmlProperties(self View, buffer *strings.Builder) {
buffer.WriteByte('"')
}
}
dataListHtmlProperties(edit, buffer)
}
func (edit *editViewData) htmlDisabledProperties(self View, buffer *strings.Builder) {
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 {
func (edit *editViewData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command {
case "textChanged":
oldText := GetText(edit)
if text, ok := data.PropertyValue("text"); ok {
edit.properties[Text] = text
if text := GetText(edit); text != oldText {
edit.textChanged(text)
edit.setRaw(Text, text)
if text != oldText {
edit.textChanged(text, oldText)
}
}
return true
@ -509,10 +408,7 @@ func (edit *editViewData) handleCommand(self View, command string, data DataObje
// 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.
func GetText(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(Text); value != nil {
if text, ok := value.(string); ok {
return text
@ -525,25 +421,34 @@ func GetText(view View, subviewID ...string) string {
// 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.
func GetHint(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
view = getSubview(view, subviewID)
session := view.Session()
text := ""
if view != nil {
if text, ok := stringProperty(view, Hint, view.Session()); ok {
return text
}
var ok bool
text, ok = stringProperty(view, Hint, view.Session())
if !ok {
if value := valueFromStyle(view, Hint); value != nil {
if text, ok := value.(string); ok {
if text, ok = view.Session().resolveConstants(text); ok {
if text, ok = value.(string); ok {
if text, ok = session.resolveConstants(text); !ok {
text = ""
}
} else {
text = ""
}
}
}
}
if text != "" && !GetNotTranslate(view) {
text, _ = session.GetString(text)
}
return text
}
}
}
}
return ""
}
// 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.
func GetMaxLength(view View, subviewID ...string) int {
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.
// 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 {
return boolStyledProperty(view, subviewID, Spellcheck, false)
}
// GetTextChangedListeners returns the TextChangedListener list of an EditView or MultiLineEditView subview.
// 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) {
return getEventListeners[EditView, string](view, subviewID, EditTextChangedEvent)
//
// Result elements can be of the following types:
// - 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.
// 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 {
return enumStyledProperty(view, subviewID, EditViewType, SingleLineText, false)
}
// 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
if pattern, ok := stringProperty(view, EditViewPattern, view.Session()); ok {
return pattern
}
@ -596,13 +515,17 @@ func GetEditViewPattern(view View, subviewID ...string) string {
}
// 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 {
return boolStyledProperty(view, subviewID, EditWrap, false)
}
// 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) {
if subviewID != "" {
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.
// If the second argument (subviewID) is not specified or it is "" then a value from the first argument (view) is returned.
// GetCaretColor returns the color of the text input caret.
//
// 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 {
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
import (
"encoding/base64"
"fmt"
"strconv"
"strings"
"time"
)
// Constants for [FilePicker] specific properties and events
const (
// 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.
// 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.
// 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
type FileInfo struct {
// Name - the file's name.
Name string
// LastModified specifying the date and time at which the file was last modified
LastModified time.Time
// Size - the size of the file in bytes.
Size int64
// MimeType - the file's MIME type.
MimeType string
data []byte
}
// FilePicker - the control view for the files selecting
// FilePicker represents the FilePicker view
type FilePicker interface {
View
// Files returns the list of selected files.
@ -46,11 +85,11 @@ type FilePicker interface {
type filePickerData struct {
viewData
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 {
file.Name, _ = obj.PropertyValue("name")
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
@ -78,19 +122,19 @@ func NewFilePicker(session Session, params Params) FilePicker {
}
func newFilePicker(session Session) View {
return NewFilePicker(session, nil)
return new(filePickerData) // NewFilePicker(session, nil)
}
func (picker *filePickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "FilePicker"
picker.hasHtmlDisabled = true
picker.files = []FileInfo{}
picker.loader = map[int]func(FileInfo, []byte){}
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){}
}
//picker.loader = map[int]func(FileInfo, []byte){}
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 {
@ -102,75 +146,47 @@ func (picker *filePickerData) Files() []FileInfo {
}
func (picker *filePickerData) LoadFile(file FileInfo, result func(FileInfo, []byte)) {
if result == nil {
return
}
if result != nil {
for i, info := range picker.files {
if info.Name == file.Name && info.Size == file.Size && info.LastModified == file.LastModified {
picker.loader[i] = result
picker.Session().runScript(fmt.Sprintf(`loadSelectedFile("%s", %d)`, picker.htmlID(), i))
if info.Name == file.Name && info.Size == file.Size && info.LastModified.Equal(file.LastModified) {
if info.data != nil {
result(info, info.data)
} else {
picker.fileLoader[info.key()] = func(file FileInfo, data []byte) {
picker.files[i].data = data
result(file, data)
}
picker.Session().callFunc("loadSelectedFile", picker.htmlID(), i)
}
return
}
}
picker.viewData.LoadFile(file, result)
}
}
func (picker *filePickerData) Remove(tag string) {
picker.remove(strings.ToLower(tag))
}
func (picker *filePickerData) remove(tag string) {
func (picker *filePickerData) getFunc(tag PropertyName) any {
switch tag {
case FileSelectedEvent:
if len(picker.fileSelectedListeners) > 0 {
picker.fileSelectedListeners = []func(FilePicker, []FileInfo){}
picker.propertyChangedEvent(tag)
if listeners := getOneArgEventRawListeners[FilePicker, []FileInfo](picker, nil, tag); len(listeners) > 0 {
return listeners
}
case Accept:
delete(picker.properties, tag)
if picker.created {
removeProperty(picker.htmlID(), "accept", picker.Session())
}
picker.propertyChangedEvent(tag)
default:
picker.viewData.remove(tag)
return nil
}
return picker.viewData.getFunc(tag)
}
func (picker *filePickerData) Set(tag string, value any) bool {
return picker.set(strings.ToLower(tag), value)
}
func (picker *filePickerData) set(tag string, value any) bool {
if value == nil {
picker.remove(tag)
return true
}
func (picker *filePickerData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case FileSelectedEvent:
listeners, ok := valueToEventListeners[FilePicker, []FileInfo](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(FilePicker, []FileInfo){}
}
picker.fileSelectedListeners = listeners
picker.propertyChangedEvent(tag)
return true
return setOneArgEventListener[FilePicker, []FileInfo](picker, tag, value)
case Accept:
switch value := value.(type) {
case string:
value = strings.Trim(value, " \t\n")
if value == "" {
picker.remove(Accept)
} else {
picker.properties[Accept] = value
}
return setStringPropertyValue(picker, Accept, strings.Trim(value, " \t\n"))
case []string:
buffer := allocStringBuilder()
@ -184,29 +200,27 @@ func (picker *filePickerData) set(tag string, value any) bool {
buffer.WriteString(val)
}
}
if buffer.Len() == 0 {
picker.remove(Accept)
} else {
picker.properties[Accept] = buffer.String()
return setStringPropertyValue(picker, Accept, buffer.String())
}
default:
notCompatibleType(tag, value)
return false
return nil
}
if picker.created {
if css := picker.acceptCSS(); css != "" {
updateProperty(picker.htmlID(), "accept", css, picker.Session())
return picker.viewData.setFunc(tag, value)
}
func (picker *filePickerData) propertyChanged(tag PropertyName) {
switch tag {
case Accept:
session := picker.Session()
if css := acceptPropertyCSS(picker); css != "" {
session.updateProperty(picker.htmlID(), "accept", css)
} else {
removeProperty(picker.htmlID(), "accept", picker.Session())
session.removeProperty(picker.htmlID(), "accept")
}
}
picker.propertyChangedEvent(tag)
return true
default:
return picker.viewData.set(tag, value)
picker.viewData.propertyChanged(tag)
}
}
@ -214,10 +228,10 @@ func (picker *filePickerData) htmlTag() string {
return "input"
}
func (picker *filePickerData) acceptCSS() string {
accept, ok := stringProperty(picker, Accept, picker.Session())
func acceptPropertyCSS(view View) string {
accept, ok := stringProperty(view, Accept, view.Session())
if !ok {
if value := valueFromStyle(picker, Accept); value != nil {
if value := valueFromStyle(view, Accept); value != nil {
accept, ok = value.(string)
}
}
@ -230,7 +244,7 @@ func (picker *filePickerData) acceptCSS() string {
if buffer.Len() > 0 {
buffer.WriteString(", ")
}
if value[0] != '.' && !strings.Contains(value, "/") {
if value[0] != '.' && !strings.ContainsRune(value, '/') {
buffer.WriteRune('.')
}
buffer.WriteString(value)
@ -244,7 +258,7 @@ func (picker *filePickerData) acceptCSS() string {
func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder) {
picker.viewData.htmlProperties(self, buffer)
if accept := picker.acceptCSS(); accept != "" {
if accept := acceptPropertyCSS(picker); accept != "" {
buffer.WriteString(` accept="`)
buffer.WriteString(accept)
buffer.WriteRune('"')
@ -261,69 +275,27 @@ func (picker *filePickerData) htmlProperties(self View, buffer *strings.Builder)
}
}
func (picker *filePickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
if IsDisabled(self) {
buffer.WriteString(` disabled`)
}
picker.viewData.htmlDisabledProperties(self, buffer)
}
func (picker *filePickerData) handleCommand(self View, command string, data DataObject) bool {
switch command {
case "fileSelected":
if node := data.PropertyWithTag("files"); node != nil && node.Type() == ArrayNode {
func parseFilesTag(data DataObject) []FileInfo {
if node := data.PropertyByTag("files"); node != nil && node.Type() == ArrayNode {
count := node.ArraySize()
files := make([]FileInfo, count)
for i := 0; i < count; i++ {
for i := range count {
if value := node.ArrayElement(i); value != nil {
files[i].initBy(value)
files[i] = dataToFileInfo(value)
}
}
return files
}
return nil
}
func (picker *filePickerData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command {
case "fileSelected":
if files := parseFilesTag(data); files != nil {
picker.files = files
for _, listener := range picker.fileSelectedListeners {
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)
for _, listener := range getOneArgEventListeners[FilePicker, []FileInfo](picker, nil, FileSelectedEvent) {
listener.Run(picker, files)
}
}
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.
// 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 {
return boolStyledProperty(view, subviewID, Multiple, false)
}
// 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
accept, ok := stringProperty(view, Accept, view.Session())
if !ok {
if value := valueFromStyle(view, Accept); value != nil {
@ -377,7 +350,7 @@ func GetFilePickerAccept(view View, subviewID ...string) []string {
}
if ok {
result := strings.Split(accept, ",")
for i := 0; i < len(result); i++ {
for i := range len(result) {
result[i] = strings.Trim(result[i], " \t\n")
}
return result
@ -388,7 +361,16 @@ func GetFilePickerAccept(view View, subviewID ...string) []string {
// GetFileSelectedListeners returns the "file-selected-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 GetFileSelectedListeners(view View, subviewID ...string) []func(FilePicker, []FileInfo) {
return getEventListeners[FilePicker, []FileInfo](view, subviewID, FileSelectedEvent)
//
// Result elements can be of the following types:
// - 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"
// Constants which represent [View] specific focus events properties
const (
// FocusEvent is the constant for "focus-event" property tag.
// The "focus-event" event occurs when the View takes input focus.
// The main listener format:
// func(View).
// The additional listener format:
//
// Used by View.
// Occur when the view takes input focus.
//
// General listener format:
// func(rui.View).
//
// where:
// view - Interface of a view which generated this event.
//
// Allowed listener formats:
// func().
FocusEvent = "focus-event"
FocusEvent PropertyName = "focus-event"
// 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:
// func(View).
// The additional listener format:
// func().
LostFocusEvent = "lost-focus-event"
//
// Used by View.
// Occur when the View lost input focus.
//
// General listener format:
// 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) {
if view.Focusable() {
for _, js := range focusEvents {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
for _, tag := range []PropertyName{FocusEvent, LostFocusEvent} {
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
// 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) {
return getFocusListeners(view, subviewID, FocusEvent)
//
// Result elements can be of the following types:
// - 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
// 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) {
return getFocusListeners(view, subviewID, LostFocusEvent)
//
// Result elements can be of the following types:
// - 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
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.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
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"
)
// 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 {
ViewsContainer
// UpdateContent updates child Views if the "content" property value is set to GridAdapter,
// otherwise does nothing
UpdateGridContent()
}
type gridLayoutData struct {
viewsContainerData
adapter GridAdapter
}
// NewGridLayout create new GridLayout object and return it
@ -23,7 +126,8 @@ func NewGridLayout(session Session, params Params) GridLayout {
}
func newGridLayout(session Session) View {
return NewGridLayout(session, nil)
//return NewGridLayout(session, nil)
return new(gridLayoutData)
}
// Init initialize fields of GridLayout by default values
@ -31,20 +135,22 @@ func (gridLayout *gridLayoutData) init(session Session) {
gridLayout.viewsContainerData.init(session)
gridLayout.tag = "GridLayout"
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 {
return getViewString(gridLayout)
}
func (style *viewStyle) setGridCellSize(tag string, value any) bool {
func setGridCellSize(properties Properties, tag PropertyName, value any) []PropertyName {
setValues := func(values []string) bool {
count := len(values)
if count > 1 {
sizes := make([]any, count)
for i, val := range values {
val = strings.Trim(val, " \t\n\r")
if isConstantName(val) {
if ok, _ := isConstantName(val); ok {
sizes[i] = val
} else if fn := parseSizeFunc(val); fn != nil {
sizes[i] = SizeUnit{Type: SizeFunction, Function: fn}
@ -55,11 +161,11 @@ func (style *viewStyle) setGridCellSize(tag string, value any) bool {
return false
}
}
style.properties[tag] = sizes
} else if isConstantName(values[0]) {
style.properties[tag] = values[0]
properties.setRaw(tag, sizes)
} else if ok, _ := isConstantName(values[0]); ok {
properties.setRaw(tag, values[0])
} else if size, err := stringToSizeUnit(values[0]); err == nil {
style.properties[tag] = size
properties.setRaw(tag, size)
} else {
invalidPropertyValue(tag, value)
return false
@ -71,41 +177,41 @@ func (style *viewStyle) setGridCellSize(tag string, value any) bool {
case CellWidth, CellHeight:
switch value := value.(type) {
case SizeUnit, []SizeUnit:
style.properties[tag] = value
properties.setRaw(tag, value)
case string:
if !setValues(strings.Split(value, ",")) {
return false
return nil
}
case []string:
if !setValues(value) {
return false
return nil
}
case []DataValue:
count := len(value)
if count == 0 {
invalidPropertyValue(tag, value)
return false
return nil
}
values := make([]string, count)
for i, val := range value {
if val.IsObject() {
invalidPropertyValue(tag, value)
return false
return nil
}
values[i] = val.Value()
}
if !setValues(values) {
return false
return nil
}
case []any:
count := len(value)
if count == 0 {
invalidPropertyValue(tag, value)
return false
return nil
}
sizes := make([]any, count)
for i, val := range value {
@ -114,35 +220,35 @@ func (style *viewStyle) setGridCellSize(tag string, value any) bool {
sizes[i] = val
case string:
if isConstantName(val) {
if ok, _ := isConstantName(val); ok {
sizes[i] = val
} else if size, err := stringToSizeUnit(val); err == nil {
sizes[i] = size
} else {
invalidPropertyValue(tag, value)
return false
return nil
}
default:
invalidPropertyValue(tag, value)
return false
return nil
}
}
style.properties[tag] = sizes
properties.setRaw(tag, sizes)
default:
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 {
switch cellSize := gridCellSizes(style, tag, session); len(cellSize) {
func gridCellSizesCSS(properties Properties, tag PropertyName, session Session) string {
switch cellSize := gridCellSizes(properties, tag, session); len(cellSize) {
case 0:
case 1:
@ -179,8 +285,8 @@ func (style *viewStyle) gridCellSizesCSS(tag string, session Session) string {
return ""
}
func (gridLayout *gridLayoutData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeGridLayoutTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case VerticalAlign:
return CellVerticalAlign
@ -197,84 +303,166 @@ func (gridLayout *gridLayoutData) normalizeTag(tag string) string {
return tag
}
func (gridLayout *gridLayoutData) Get(tag string) any {
return gridLayout.get(gridLayout.normalizeTag(tag))
}
func (gridLayout *gridLayoutData) get(tag string) any {
if tag == Gap {
func (gridLayout *gridLayoutData) getFunc(tag PropertyName) any {
switch tag {
case Gap:
rowGap := GetGridRowGap(gridLayout)
columnGap := GetGridColumnGap(gridLayout)
if rowGap.Equal(columnGap) {
return rowGap
}
return AutoSize()
case Content:
if gridLayout.adapter != nil {
return gridLayout.adapter
}
}
return gridLayout.viewsContainerData.get(tag)
return gridLayout.viewsContainerData.getFunc(tag)
}
func (gridLayout *gridLayoutData) Remove(tag string) {
gridLayout.remove(gridLayout.normalizeTag(tag))
}
func (gridLayout *gridLayoutData) removeFunc(tag PropertyName) []PropertyName {
switch tag {
case Gap:
result := []PropertyName{}
for _, tag := range []PropertyName{GridRowGap, GridColumnGap} {
if gridLayout.getRaw(tag) != nil {
gridLayout.setRaw(tag, nil)
result = append(result, tag)
}
}
return result
func (gridLayout *gridLayoutData) remove(tag string) {
if tag == Gap {
gridLayout.remove(GridRowGap)
gridLayout.remove(GridColumnGap)
gridLayout.propertyChangedEvent(Gap)
return
case Content:
if len(gridLayout.views) > 0 || gridLayout.adapter != nil {
gridLayout.views = []View{}
gridLayout.adapter = nil
return []PropertyName{Content}
}
return []PropertyName{}
}
gridLayout.viewsContainerData.remove(tag)
if gridLayout.created {
return gridLayout.viewsContainerData.removeFunc(tag)
}
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:
updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`,
gridLayout.gridCellSizesCSS(CellWidth, gridLayout.session), gridLayout.session)
session := gridLayout.Session()
session.updateCSSProperty(gridLayout.htmlID(), `grid-template-columns`,
gridCellSizesCSS(gridLayout, CellWidth, session))
case CellHeight:
updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`,
gridLayout.gridCellSizesCSS(CellHeight, gridLayout.session), gridLayout.session)
session := gridLayout.Session()
session.updateCSSProperty(gridLayout.htmlID(), `grid-template-rows`,
gridCellSizesCSS(gridLayout, CellHeight, session))
}
default:
gridLayout.viewsContainerData.propertyChanged(tag)
}
}
func (gridLayout *gridLayoutData) Set(tag string, value any) bool {
return gridLayout.set(gridLayout.normalizeTag(tag), value)
}
func (gridLayout *gridLayoutData) set(tag string, value any) bool {
if value == nil {
gridLayout.remove(tag)
return true
}
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
}
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 gridCellSizes(properties Properties, tag string, session Session) []SizeUnit {
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 {
switch value := value.(type) {
case []SizeUnit:
@ -314,38 +502,37 @@ func gridCellSizes(properties Properties, tag string, session Session) []SizeUni
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)
// 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 {
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)
// 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 {
return enumStyledProperty(view, subviewID, CellHorizontalAlign, StretchAlign, false)
}
// 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 {
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.
// 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
return gridCellSizes(view, CellWidth, view.Session())
}
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.
// 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
return gridCellSizes(view, CellHeight, view.Session())
}
return []SizeUnit{}
}
// 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 {
return sizeStyledProperty(view, subviewID, GridRowGap, false)
}
// 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 {
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
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 (
// 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 = 1
ImageReady ImageLoadingStatus = 1
// 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.
type Image interface {
// URL returns the url of the image
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() string
setLoadingError(err string)
// Width returns the width of the image in pixels. While LoadingStatus() != ImageReady returns 0
Width() float64
// Height returns the height of the image in pixels. While LoadingStatus() != ImageReady returns 0
Height() float64
}
type imageData struct {
url string
loadingStatus int
loadingStatus ImageLoadingStatus
loadingError string
width, height float64
listener func(Image)
@ -42,7 +55,7 @@ func (image *imageData) URL() string {
return image.url
}
func (image *imageData) LoadingStatus() int {
func (image *imageData) LoadingStatus() ImageLoadingStatus {
return image.loadingStatus
}
@ -76,11 +89,13 @@ func (manager *imageManager) loadImage(url string, onLoaded func(Image), session
image.listener = onLoaded
image.loadingStatus = ImageLoading
manager.images[url] = image
session.runScript("loadImage('" + url + "');")
session.callFunc("loadImage", url)
session.sendResponse()
return image
}
func (manager *imageManager) imageLoaded(obj DataObject, session Session) {
func (manager *imageManager) imageLoaded(obj DataObject) {
if manager.images == nil {
manager.images = make(map[string]*imageData)
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 {
manager.images = make(map[string]*imageData)
return
@ -128,8 +143,8 @@ func (manager *imageManager) imageLoadError(obj DataObject, session Session) {
// LoadImage starts the async image loading by url
func LoadImage(url string, onLoaded func(Image), session Session) Image {
if url != "" && url[0] == '@' {
if image, ok := session.ImageConstant(url[1:]); ok {
if ok, constName := isConstantName(url); ok {
if image, ok := session.ImageConstant(constName); ok {
url = image
}
}

View File

@ -5,35 +5,63 @@ import (
"strings"
)
// Constants which represent [ImageView] specific properties and events
const (
// LoadedEvent is the constant for the "loaded-event" property tag.
// The "loaded-event" event occurs event occurs when the image has been loaded.
LoadedEvent = "loaded-event"
// ErrorEvent is the constant for the "error-event" property tag.
// The "error-event" event occurs event occurs when the image loading failed.
ErrorEvent = "error-event"
// LoadedEvent is the constant for "loaded-event" property tag.
//
// Used by ImageView.
// Occur when the image has been loaded.
//
// 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 = 0
// 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.
// 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.
ContainFit = 1
// 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.
// 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
// 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.
// 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
// 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.
ScaleDownFit = 4
)
// ImageView - image View
// ImageView represents an ImageView view
type ImageView interface {
View
// 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 {
return NewImageView(session, nil)
return new(imageViewData)
}
// Init initialize fields of imageView by default values
func (imageView *imageViewData) init(session Session) {
imageView.viewData.init(session)
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 {
return getViewString(imageView)
}
func (imageView *imageViewData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeImageViewTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case "source":
tag = Source
case "src-set", "source-set":
tag = SrcSet
case VerticalAlign:
tag = ImageVerticalAlign
@ -93,108 +123,92 @@ func (imageView *imageViewData) normalizeTag(tag string) string {
return tag
}
func (imageView *imageViewData) Remove(tag string) {
imageView.remove(imageView.normalizeTag(tag))
}
func (imageView *imageViewData) remove(tag string) {
imageView.viewData.remove(tag)
if imageView.created {
func (imageView *imageViewData) getFunc(tag PropertyName) any {
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)
}
}
}
func (imageView *imageViewData) Set(tag string, value any) bool {
return imageView.set(imageView.normalizeTag(tag), value)
}
func (imageView *imageViewData) set(tag string, value any) bool {
if value == nil {
imageView.remove(tag)
return true
}
switch tag {
case Source:
if text, ok := value.(string); ok {
imageView.properties[Source] = text
if imageView.created {
src := text
if src != "" && src[0] == '@' {
src, _ = imageProperty(imageView, Source, imageView.session)
}
updateProperty(imageView.htmlID(), "src", src, imageView.session)
if srcset := imageView.srcSet(src); srcset != "" {
updateProperty(imageView.htmlID(), "srcset", srcset, imageView.session)
} else {
removeProperty(imageView.htmlID(), "srcset", imageView.session)
}
}
imageView.propertyChangedEvent(Source)
return true
}
notCompatibleType(Source, value)
case AltText:
if text, ok := value.(string); ok {
imageView.properties[AltText] = text
if imageView.created {
updateInnerHTML(imageView.htmlID(), imageView.session)
}
imageView.propertyChangedEvent(Source)
return true
}
notCompatibleType(tag, value)
case LoadedEvent, ErrorEvent:
if listeners, ok := valueToNoParamListeners[ImageView](value); ok {
if listeners == nil {
delete(imageView.properties, tag)
} else {
imageView.properties[tag] = listeners
}
return true
}
default:
if imageView.viewData.set(tag, value) {
if imageView.created {
switch tag {
case ImageVerticalAlign, ImageHorizontalAlign:
updateCSSStyle(imageView.htmlID(), imageView.session)
}
}
return true
}
}
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 {
if listeners := getNoArgEventRawListeners[ImageView](imageView, nil, tag); len(listeners) > 0 {
return listeners
}
return nil
}
return []func(ImageView){}
return imageView.viewData.getFunc(tag)
}
func (imageView *imageViewData) srcSet(path string) string {
func (imageView *imageViewData) setFunc(tag PropertyName, value any) []PropertyName {
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) propertyChanged(tag PropertyName) {
session := imageView.Session()
htmlID := imageView.htmlID()
switch tag {
case Source:
src, srcset := imageViewSrc(imageView, GetImageViewSource(imageView))
session.updateProperty(htmlID, "src", src)
if srcset != "" {
session.updateProperty(htmlID, "srcset", srcset)
} else {
session.removeProperty(htmlID, "srcset")
}
case SrcSet:
_, srcset := imageViewSrc(imageView, GetImageViewSource(imageView))
if srcset != "" {
session.updateProperty(htmlID, "srcset", srcset)
} else {
session.removeProperty(htmlID, "srcset")
}
case AltText:
updateInnerHTML(htmlID, session)
case ImageVerticalAlign, ImageHorizontalAlign:
updateCSSStyle(htmlID, session)
default:
imageView.viewData.propertyChanged(tag)
}
}
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 buffer.String()
}
}
if srcset, ok := resources.imageSrcSets[path]; ok {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
@ -203,7 +217,7 @@ func (imageView *imageViewData) srcSet(path string) string {
buffer.WriteString(", ")
}
buffer.WriteString(src.path)
buffer.WriteString(fmt.Sprintf(" %gx", src.scale))
fmt.Fprintf(buffer, " %gx", src.scale)
}
return buffer.String()
}
@ -214,29 +228,31 @@ func (imageView *imageViewData) htmlTag() string {
return "img"
}
/*
func (imageView *imageViewData) closeHTMLTag() bool {
return false
func imageViewSrc(view View, src string) (string, string) {
if ok, constName := isConstantName(src); ok {
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) {
imageView.viewData.htmlProperties(self, buffer)
if imageResource, ok := imageProperty(imageView, Source, imageView.Session()); ok && imageResource != "" {
if imageResource[0] == '@' {
if image, ok := imageView.Session().ImageConstant(imageResource[1:]); ok {
imageResource = image
} else {
imageResource = ""
}
}
if imageResource != "" {
if src, srcset := imageViewSrc(imageView, imageResource); src != "" {
buffer.WriteString(` src="`)
buffer.WriteString(imageResource)
buffer.WriteString(src)
buffer.WriteString(`"`)
if srcset := imageView.srcSet(imageResource); srcset != "" {
if srcset != "" {
buffer.WriteString(` srcset="`)
buffer.WriteString(srcset)
buffer.WriteString(`"`)
@ -246,13 +262,13 @@ func (imageView *imageViewData) htmlProperties(self View, buffer *strings.Builde
if text := GetImageViewAltText(imageView); text != "" {
buffer.WriteString(` alt="`)
buffer.WriteString(textToJS(text))
buffer.WriteString(text)
buffer.WriteString(`"`)
}
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)"`)
}
}
@ -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 {
case "imageViewError":
for _, listener := range imageView.imageListeners(ErrorEvent) {
listener(imageView)
for _, listener := range getNoArgEventListeners[ImageView](imageView, nil, ErrorEvent) {
listener.Run(imageView)
}
case "imageViewLoaded":
@ -304,8 +320,8 @@ func (imageView *imageViewData) handleCommand(self View, command string, data Da
imageView.naturalHeight = dataFloatProperty(data, "natural-height")
imageView.currentSrc, _ = data.PropertyValue("current-src")
for _, listener := range imageView.imageListeners(LoadedEvent) {
listener(imageView)
for _, listener := range getNoArgEventListeners[ImageView](imageView, nil, LoadedEvent) {
listener.Run(imageView)
}
default:
@ -325,11 +341,7 @@ func (imageView *imageViewData) CurrentSource() string {
// 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
func GetImageViewSource(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
if image, ok := imageProperty(view, Source, view.Session()); ok {
return image
}
@ -341,11 +353,7 @@ func GetImageViewSource(view View, subviewID ...string) string {
// 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
func GetImageViewAltText(view View, subviewID ...string) string {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
if value := view.getRaw(AltText); value != nil {
if text, ok := value.(string); ok {
text, _ = view.Session().GetString(text)
@ -374,3 +382,31 @@ func GetImageViewVerticalAlign(view View, subviewID ...string) int {
func GetImageViewHorizontalAlign(view View, subviewID ...string) int {
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"
// Constants which represent [View] specific keyboard events properties
const (
// KeyDown 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:
// func(View, KeyEvent).
// The additional listener formats:
// func(KeyEvent), func(View), and func().
KeyDownEvent = "key-down-event"
// KeyDownEvent is the constant for "key-down-event" property tag.
//
// Used by View.
// Is fired when a key is pressed.
//
// General listener format:
//
// 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.
// The "key-up-event" event is fired when a key is released.
// The main listener format:
// func(View, KeyEvent).
// The additional listener formats:
// func(KeyEvent), func(View), and func().
KeyUpEvent = "key-up-event"
// KeyUpEvent is the constant for "key-up-event" property tag.
//
// Used by View.
// Is fired when a key is released.
//
// General listener format:
//
// 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 {
// TimeStamp is the time at which the event was created (in milliseconds).
// 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
// 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 bool
@ -59,7 +423,8 @@ func (event *KeyEvent) init(data DataObject) {
}
event.Key, _ = data.PropertyValue("key")
event.Code, _ = data.PropertyValue("code")
code, _ := data.PropertyValue("code")
event.Code = KeyCode(code)
event.TimeStamp = getTimeStamp(data)
event.Repeat = getBool("repeat")
event.CtrlKey = getBool("ctrlKey")
@ -68,207 +433,87 @@ func (event *KeyEvent) init(data DataObject) {
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) {
for tag, js := range keyEvents {
if listeners := getEventListeners[View, KeyEvent](view, nil, tag); len(listeners) > 0 {
buffer.WriteString(js.jsEvent + `="` + js.jsFunc + `(this, event)" `)
if len(getOneArgEventListeners[View, KeyEvent](view, nil, KeyDownEvent)) > 0 ||
(view.Focusable() && len(getOneArgEventListeners[View, MouseEvent](view, nil, ClickEvent)) > 0) {
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) {
listeners := getEventListeners[View, KeyEvent](view, nil, tag)
if len(listeners) > 0 {
func handleKeyEvents(view View, tag PropertyName, data DataObject) {
var event KeyEvent
event.init(data)
listeners := getOneArgEventListeners[View, KeyEvent](view, nil, tag)
if len(listeners) > 0 {
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.
// 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) {
return getEventListeners[View, KeyEvent](view, subviewID, KeyDownEvent)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, KeyEvent](view, subviewID, KeyUpEvent)
//
// Result elements can be of the following types:
// - 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
type ListAdapter interface {
// ListSize returns the number of elements in the list
ListSize() int
// ListItem creates a View of a list item at the given index
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
}

View File

@ -4,30 +4,44 @@ import (
"strings"
)
// Constants which represent values of the "orientation" property of the [ListLayout]
const (
// TopDownOrientation - subviews are arranged from top to bottom. Synonym of VerticalOrientation
TopDownOrientation = 0
// StartToEndOrientation - subviews are arranged from left to right. Synonym of HorizontalOrientation
StartToEndOrientation = 1
// BottomUpOrientation - subviews are arranged from bottom to top
BottomUpOrientation = 2
// EndToStartOrientation - subviews are arranged from right to left
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 = 0
// ListWrapOn - the new row/column starts at bottom/right
ListWrapOn = 1
// ListWrapReverse - the new row/column starts at top/left
ListWrapReverse = 2
)
// ListLayout - list-container of View
// ListLayout represents a ListLayout view
type ListLayout interface {
ViewsContainer
// UpdateContent updates child Views if the "content" property value is set to ListAdapter,
// otherwise does nothing
UpdateContent()
}
type listLayoutData struct {
viewsContainerData
adapter ListAdapter
}
// NewListLayout create new ListLayout object and return it
@ -39,7 +53,8 @@ func NewListLayout(session Session, params Params) ListLayout {
}
func newListLayout(session Session) View {
return NewListLayout(session, nil)
//return NewListLayout(session, nil)
return new(listLayoutData)
}
// Init initialize fields of ViewsAlignContainer by default values
@ -47,14 +62,15 @@ func (listLayout *listLayoutData) init(session Session) {
listLayout.viewsContainerData.init(session)
listLayout.tag = "ListLayout"
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 {
return getViewString(listLayout)
}
func (listLayout *listLayoutData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeListLayoutTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case "wrap":
tag = ListWrap
@ -68,98 +84,149 @@ func (listLayout *listLayoutData) normalizeTag(tag string) string {
return tag
}
func (listLayout *listLayoutData) Get(tag string) any {
return listLayout.get(listLayout.normalizeTag(tag))
}
func (listLayout *listLayoutData) get(tag string) any {
if tag == Gap {
func (listLayout *listLayoutData) getFunc(tag PropertyName) any {
switch tag {
case Gap:
if rowGap := GetListRowGap(listLayout); rowGap.Equal(GetListColumnGap(listLayout)) {
return rowGap
}
return AutoSize()
case Content:
if listLayout.adapter != nil {
return listLayout.adapter
}
}
return listLayout.viewsContainerData.get(tag)
return listLayout.viewsContainerData.getFunc(tag)
}
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
func (listLayout *listLayoutData) removeFunc(tag PropertyName) []PropertyName {
switch tag {
case Gap:
result := []PropertyName{}
for _, tag := range []PropertyName{ListRowGap, ListColumnGap} {
if listLayout.getRaw(tag) != nil {
listLayout.setRaw(tag, nil)
result = append(result, tag)
}
listLayout.viewsContainerData.remove(tag)
if listLayout.created {
}
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)
}
}
}
updateCSSStyle(listLayout.htmlID(), listLayout.Session())
func (listLayout *listLayoutData) Set(tag string, value any) bool {
return listLayout.set(listLayout.normalizeTag(tag), value)
}
func (listLayout *listLayoutData) set(tag string, value any) bool {
if value == nil {
listLayout.remove(tag)
return true
default:
listLayout.viewsContainerData.propertyChanged(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 false
}
func (listLayout *listLayoutData) htmlSubviews(self View, buffer *strings.Builder) {
if listLayout.views != nil {
for _, view := range listLayout.views {
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)
// 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 {
return enumStyledProperty(view, subviewID, VerticalAlign, TopAlign, false)
}
// GetListHorizontalAlign returns the vertical align of a ListLayout or ListView subview:
// 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 {
return enumStyledProperty(view, subviewID, HorizontalAlign, LeftAlign, false)
}
// GetListOrientation returns the orientation of a ListLayout or ListView subview:
// 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
if orientation, ok := valueToOrientation(view.Get(Orientation), view.Session()); ok {
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:
// 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 {
return enumStyledProperty(view, subviewID, ListWrap, ListWrapOff, false)
}
// 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 {
return sizeStyledProperty(view, subviewID, ListRowGap, false)
}
// 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 {
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"
)
// Constants related to [View] mouse events properties
const (
// 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:
// func(View, MouseEvent).
// The additional listener formats:
// func(MouseEvent), func(View), and func().
ClickEvent = "click-event"
//
// Used by View.
// Occur when the user clicks on the view.
//
// General listener format:
//
// 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.
// The "double-click-event" event occurs when the user double clicks on the View.
// The main listener format:
// func(View, MouseEvent).
// The additional listener formats:
// func(MouseEvent), func(View), and func().
DoubleClickEvent = "double-click-event"
//
// Used by View.
// Occur when the user double clicks on the view.
//
// General listener format:
//
// 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.
// The "mouse-down" event is fired at a View when a pointing device button is pressed
// while the pointer is inside the view.
// The main listener format:
// func(View, MouseEvent).
// The additional listener formats:
// func(MouseEvent), func(View), and func().
MouseDown = "mouse-down"
//
// Used by View.
// Is fired at a View when a pointing device button is pressed while the pointer is inside the view.
//
// General listener format:
//
// 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.
// 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.
// "mouse-up" events are the counterpoint to "mouse-down" events.
// The main listener format:
// func(View, MouseEvent).
// The additional listener formats:
// func(MouseEvent), func(View), and func().
MouseUp = "mouse-up"
//
// Used by View.
// 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. "mouse-up" events are the counterpoint to "mouse-down" events.
//
// General listener format:
//
// 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.
// 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.
// The main listener format:
// func(View, MouseEvent).
// The additional listener formats:
// func(MouseEvent), func(View), and func().
MouseMove = "mouse-move"
//
// Used by View.
// Is fired at a view when a pointing device(usually a mouse) is moved while the cursor's hotspot is inside it.
//
// General listener format:
//
// 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.
// 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.
// "mouse-out" is also delivered to a view if the cursor enters a child view,
// because the child view obscures the visible area of the view.
// The main listener format:
// func(View, MouseEvent).
// The additional listener formats:
// func(MouseEvent), func(View), and func().
// The additional listener formats:
// func(MouseEvent), func(View), and func().
MouseOut = "mouse-out"
//
// Used by 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
// contained within the view or one of its children. "mouse-out" is also delivered to a view if the cursor enters a child
// view, because the child view obscures the visible area of the view.
//
// General listener format:
//
// 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()
MouseOut PropertyName = "mouse-out"
// 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.
// The main listener formats:
// func(View, MouseEvent).
// The additional listener formats:
// func(MouseEvent), func(View), and func().
MouseOver = "mouse-over"
//
// Used by View.
// 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.
//
// General listener format:
//
// 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.
// The "context-menu-event" event occurs when the user calls the context menu by the right mouse clicking.
// The main listener format:
// func(View, MouseEvent).
// The additional listener formats:
// func(MouseEvent), func(View), and func().
ContextMenuEvent = "context-menu-event"
//
// Used by View.
// Occur when the user calls the context menu by the right mouse clicking.
//
// General listener format:
//
// 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 = 0
// AuxiliaryMouseButton is a number of the auxiliary pressed button, usually the wheel button
// or the middle button (if present)
AuxiliaryMouseButton = 1
// SecondaryMouseButton is a number of the secondary pressed button, usually the right button
SecondaryMouseButton = 2
// MouseButton4 is a number of the fourth button, typically the Browser Back button
MouseButton4 = 3
// MouseButton5 is a number of the fifth button, typically the Browser Forward button
MouseButton5 = 4
// PrimaryMouseMask is the mask of the primary button (usually the left button)
PrimaryMouseMask = 1
// SecondaryMouseMask is the mask of the secondary button (usually the right button)
SecondaryMouseMask = 2
// AuxiliaryMouseMask is the mask of the auxiliary button (usually the mouse wheel button or middle button)
AuxiliaryMouseMask = 4
// MouseMask4 is the mask of the 4th button (typically the "Browser Back" button)
MouseMask4 = 8
//MouseMask5 is the mask of the 5th button (typically the "Browser Forward" button)
MouseMask5 = 16
)
// MouseEvent represent a mouse event
type MouseEvent struct {
// TimeStamp is the time at which the event was created (in milliseconds).
// This value is time since epoch—but in reality, browsers' definitions vary.
@ -144,56 +244,6 @@ type MouseEvent struct {
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 {
if value, ok := data.PropertyValue("timeStamp"); ok {
if index := strings.Index(value, "."); index > 0 {
@ -223,63 +273,135 @@ func (event *MouseEvent) init(data DataObject) {
event.MetaKey = dataBoolProperty(data, "metaKey")
}
func handleMouseEvents(view View, tag string, data DataObject) {
listeners := getEventListeners[View, MouseEvent](view, nil, tag)
func handleMouseEvents(view View, tag PropertyName, data DataObject) {
listeners := getOneArgEventListeners[View, MouseEvent](view, nil, tag)
if len(listeners) > 0 {
var event MouseEvent
event.init(data)
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.
// 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) {
return getEventListeners[View, MouseEvent](view, subviewID, ClickEvent)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, MouseEvent](view, subviewID, DoubleClickEvent)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, MouseEvent](view, subviewID, ContextMenuEvent)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, MouseEvent](view, subviewID, MouseDown)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, MouseEvent](view, subviewID, MouseUp)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, MouseEvent](view, subviewID, MouseMove)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, MouseEvent](view, subviewID, MouseOver)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, MouseEvent](view, subviewID, MouseOut)
//
// Result elements can be of the following types:
// - 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"
)
// Constants related to [NumberPicker] specific properties and events
const (
NumberChangedEvent = "number-changed"
NumberPickerType = "number-picker-type"
NumberPickerMin = "number-picker-min"
NumberPickerMax = "number-picker-max"
NumberPickerStep = "number-picker-step"
NumberPickerValue = "number-picker-value"
// NumberChangedEvent is the constant for "number-changed" property tag.
//
// Used by NumberPicker.
// Set listener(s) that track the change in the entered 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 (
// NumberEditor - type of NumberPicker. NumberPicker is presented by editor
NumberEditor = 0
// NumberSlider - type of NumberPicker. NumberPicker is presented by slider
NumberSlider = 1
)
// NumberPicker - NumberPicker view
// NumberPicker represents a NumberPicker view
type NumberPicker interface {
View
}
type numberPickerData struct {
viewData
numberChangedListeners []func(NumberPicker, float64)
}
// NewNumberPicker create new NumberPicker object and return it
@ -42,146 +121,114 @@ func NewNumberPicker(session Session, params Params) NumberPicker {
}
func newNumberPicker(session Session) View {
return NewNumberPicker(session, nil)
return new(numberPickerData)
}
func (picker *numberPickerData) init(session Session) {
picker.viewData.init(session)
picker.tag = "NumberPicker"
picker.numberChangedListeners = []func(NumberPicker, float64){}
}
func (picker *numberPickerData) String() string {
return getViewString(picker)
picker.hasHtmlDisabled = true
picker.normalize = normalizeNumberPickerTag
picker.get = picker.getFunc
picker.set = picker.setFunc
picker.changed = picker.propertyChanged
}
func (picker *numberPickerData) Focusable() bool {
return true
}
func (picker *numberPickerData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeNumberPickerTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Type, Min, Max, Step, Value:
case Type, Min, Max, Step, Value, "precision":
return "number-picker-" + tag
}
return tag
return normalizeDataListTag(tag)
}
func (picker *numberPickerData) Remove(tag string) {
picker.remove(picker.normalizeTag(tag))
}
func (picker *numberPickerData) remove(tag string) {
func (picker *numberPickerData) getFunc(tag PropertyName) any {
switch tag {
case NumberChangedEvent:
if len(picker.numberChangedListeners) > 0 {
picker.numberChangedListeners = []func(NumberPicker, float64){}
picker.propertyChangedEvent(tag)
if listeners := getTwoArgEventRawListeners[NumberPicker, float64](picker, nil, tag); len(listeners) > 0 {
return listeners
}
default:
picker.viewData.remove(tag)
picker.propertyChanged(tag)
return nil
}
return picker.viewData.getFunc(tag)
}
func (picker *numberPickerData) Set(tag string, value any) bool {
return picker.set(picker.normalizeTag(tag), value)
}
func (picker *numberPickerData) set(tag string, value any) bool {
if value == nil {
picker.remove(tag)
return true
}
func (picker *numberPickerData) setFunc(tag PropertyName, value any) []PropertyName {
switch tag {
case NumberChangedEvent:
listeners, ok := valueToEventListeners[NumberPicker, float64](value)
if !ok {
notCompatibleType(tag, value)
return false
} else if listeners == nil {
listeners = []func(NumberPicker, float64){}
}
picker.numberChangedListeners = listeners
picker.propertyChangedEvent(tag)
return true
return setTwoArgEventListener[NumberPicker, float64](picker, tag, value)
case NumberPickerValue:
oldValue := GetNumberPickerValue(picker)
picker.setRaw("old-number", GetNumberPickerValue(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
return setFloatProperty(picker, NumberPickerValue, value, min, max)
case DataList:
return setDataList(picker, value, "")
}
default:
if picker.viewData.set(tag, value) {
picker.propertyChanged(tag)
return true
}
}
return false
return picker.viewData.setFunc(tag, value)
}
func (picker *numberPickerData) propertyChanged(tag string) {
if picker.created {
func (picker *numberPickerData) numberFormat() string {
if precision := GetNumberPickerPrecision(picker); precision > 0 {
return fmt.Sprintf("%%.%df", precision)
}
return "%g"
}
func (picker *numberPickerData) propertyChanged(tag PropertyName) {
switch tag {
case NumberPickerType:
if GetNumberPickerType(picker) == NumberSlider {
updateProperty(picker.htmlID(), "type", "range", picker.session)
picker.Session().updateProperty(picker.htmlID(), "type", "range")
} else {
updateProperty(picker.htmlID(), "type", "number", picker.session)
picker.Session().updateProperty(picker.htmlID(), "type", "number")
}
case NumberPickerMin:
min, _ := GetNumberPickerMinMax(picker)
updateProperty(picker.htmlID(), Min, strconv.FormatFloat(min, 'f', -1, 32), picker.session)
picker.Session().updateProperty(picker.htmlID(), "min", fmt.Sprintf(picker.numberFormat(), min))
case NumberPickerMax:
_, max := GetNumberPickerMinMax(picker)
updateProperty(picker.htmlID(), Max, strconv.FormatFloat(max, 'f', -1, 32), picker.session)
picker.Session().updateProperty(picker.htmlID(), "max", fmt.Sprintf(picker.numberFormat(), max))
case NumberPickerStep:
if step := GetNumberPickerStep(picker); step > 0 {
updateProperty(picker.htmlID(), Step, strconv.FormatFloat(step, 'f', -1, 32), picker.session)
picker.Session().updateProperty(picker.htmlID(), "step", fmt.Sprintf(picker.numberFormat(), step))
} else {
updateProperty(picker.htmlID(), Step, "any", picker.session)
picker.Session().updateProperty(picker.htmlID(), "step", "any")
}
case NumberPickerValue:
value := GetNumberPickerValue(picker)
picker.session.runScript(fmt.Sprintf(`setInputValue('%s', '%f')`, picker.htmlID(), value))
for _, listener := range picker.numberChangedListeners {
listener(picker, value)
}
}
}
}
format := picker.numberFormat()
picker.Session().callFunc("setInputValue", picker.htmlID(), fmt.Sprintf(format, value))
func (picker *numberPickerData) Get(tag string) any {
return picker.get(picker.normalizeTag(tag))
}
func (picker *numberPickerData) get(tag string) any {
switch tag {
case NumberChangedEvent:
return picker.numberChangedListeners
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:
return picker.viewData.get(tag)
picker.viewData.propertyChanged(tag)
}
}
@ -189,6 +236,13 @@ func (picker *numberPickerData) htmlTag() string {
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) {
picker.viewData.htmlProperties(self, buffer)
@ -198,43 +252,39 @@ func (picker *numberPickerData) htmlProperties(self View, buffer *strings.Builde
buffer.WriteString(` type="number"`)
}
format := picker.numberFormat()
min, max := GetNumberPickerMinMax(picker)
if min != math.Inf(-1) {
buffer.WriteString(` min="`)
buffer.WriteString(strconv.FormatFloat(min, 'f', -1, 64))
fmt.Fprintf(buffer, format, min)
buffer.WriteByte('"')
}
if max != math.Inf(1) {
buffer.WriteString(` max="`)
buffer.WriteString(strconv.FormatFloat(max, 'f', -1, 64))
fmt.Fprintf(buffer, format, max)
buffer.WriteByte('"')
}
step := GetNumberPickerStep(picker)
if step != 0 {
buffer.WriteString(` step="`)
buffer.WriteString(strconv.FormatFloat(step, 'f', -1, 64))
fmt.Fprintf(buffer, format, step)
buffer.WriteByte('"')
} else {
buffer.WriteString(` step="any"`)
}
buffer.WriteString(` value="`)
buffer.WriteString(strconv.FormatFloat(GetNumberPickerValue(picker), 'f', -1, 64))
fmt.Fprintf(buffer, format, GetNumberPickerValue(picker))
buffer.WriteByte('"')
buffer.WriteString(` oninput="editViewInputEvent(this)"`)
dataListHtmlProperties(picker, buffer)
}
func (picker *numberPickerData) htmlDisabledProperties(self View, buffer *strings.Builder) {
if IsDisabled(self) {
buffer.WriteString(` disabled`)
}
picker.viewData.htmlDisabledProperties(self, buffer)
}
func (picker *numberPickerData) handleCommand(self View, command string, data DataObject) bool {
func (picker *numberPickerData) handleCommand(self View, command PropertyName, data DataObject) bool {
switch command {
case "textChanged":
if text, ok := data.PropertyValue("text"); ok {
@ -242,9 +292,10 @@ func (picker *numberPickerData) handleCommand(self View, command string, data Da
oldValue := GetNumberPickerValue(picker)
picker.properties[NumberPickerValue] = text
if value != oldValue {
for _, listener := range picker.numberChangedListeners {
listener(picker, value)
for _, listener := range getTwoArgEventListeners[NumberPicker, float64](picker, nil, NumberChangedEvent) {
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:
// NumberEditor (0) - NumberPicker is presented by editor (default type);
// 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 {
return enumStyledProperty(view, subviewID, NumberPickerType, NumberEditor, false)
}
// 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) {
var pickerType int
if len(subviewID) > 0 && subviewID[0] != "" {
pickerType = GetNumberPickerType(view, subviewID[0])
} else {
pickerType = GetNumberPickerType(view)
}
view = getSubview(view, subviewID)
pickerType := GetNumberPickerType(view)
var defMin, defMax float64
if pickerType == NumberSlider {
@ -281,8 +332,8 @@ func GetNumberPickerMinMax(view View, subviewID ...string) (float64, float64) {
defMax = math.Inf(1)
}
min := floatStyledProperty(view, subviewID, NumberPickerMin, defMin)
max := floatStyledProperty(view, subviewID, NumberPickerMax, defMax)
min := floatStyledProperty(view, nil, NumberPickerMin, defMin)
max := floatStyledProperty(view, nil, NumberPickerMax, defMax)
if min > max {
return max, min
@ -291,16 +342,14 @@ func GetNumberPickerMinMax(view View, subviewID ...string) (float64, float64) {
}
// 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 {
var max float64
if len(subviewID) > 0 && subviewID[0] != "" {
_, max = GetNumberPickerMinMax(view, subviewID[0])
} else {
_, max = GetNumberPickerMinMax(view)
}
view = getSubview(view, subviewID)
_, max := GetNumberPickerMinMax(view)
result := floatStyledProperty(view, subviewID, NumberPickerStep, 0)
result := floatStyledProperty(view, nil, NumberPickerStep, 0)
if result > max {
return max
}
@ -308,22 +357,37 @@ func GetNumberPickerStep(view View, subviewID ...string) float64 {
}
// 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 {
var min float64
if len(subviewID) > 0 && subviewID[0] != "" {
min, _ = GetNumberPickerMinMax(view, subviewID[0])
} else {
min, _ = GetNumberPickerMinMax(view)
}
result := floatStyledProperty(view, subviewID, NumberPickerValue, min)
return result
view = getSubview(view, subviewID)
min, _ := GetNumberPickerMinMax(view)
return floatStyledProperty(view, nil, NumberPickerValue, min)
}
// GetNumberChangedListeners returns the NumberChangedListener list of an NumberPicker subview.
// 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) {
return getEventListeners[NumberPicker, float64](view, subviewID, NumberChangedEvent)
//
// Result elements can be of the following types:
// - 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"
)
// OutlineProperty defines a view's outside border
type OutlineProperty interface {
Properties
stringWriter
fmt.Stringer
// ViewOutline returns style color and line width of an outline
ViewOutline(session Session) ViewOutline
}
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 {
outline := new(outlinePropertyData)
outline.properties = map[string]any{}
outline.init()
for tag, value := range params {
outline.Set(tag, value)
}
return outline
}
func (outline *outlinePropertyData) writeString(buffer *strings.Builder, indent string) {
buffer.WriteString("_{ ")
comma := false
for _, tag := range []string{Style, Width, ColorTag} {
if value, ok := outline.properties[tag]; ok {
if comma {
buffer.WriteString(", ")
}
buffer.WriteString(tag)
buffer.WriteString(" = ")
writePropertyValue(buffer, BorderStyle, value, indent)
comma = true
}
}
buffer.WriteString(" }")
func (outline *outlinePropertyData) init() {
outline.dataProperty.init()
outline.normalize = normalizeOutlineTag
outline.set = outlineSet
outline.supportedProperties = []PropertyName{Style, Width, ColorTag}
}
func (outline *outlinePropertyData) String() string {
return runStringWriter(outline)
}
func (outline *outlinePropertyData) normalizeTag(tag string) string {
return strings.TrimPrefix(strings.ToLower(tag), "outline-")
func normalizeOutlineTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
return PropertyName(strings.TrimPrefix(string(tag), "outline-"))
}
func (outline *outlinePropertyData) Remove(tag string) {
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)
func outlineSet(properties Properties, tag PropertyName, value any) []PropertyName {
switch tag {
case Style:
return outline.setEnumProperty(Style, value, enumProperties[BorderStyle].values)
return setEnumProperty(properties, Style, value, enumProperties[BorderStyle].values)
case Width:
if width, ok := value.(SizeUnit); ok {
switch width.Type {
case SizeInFraction, SizeInPercent:
notCompatibleType(tag, value)
return false
return nil
}
}
return outline.setSizeProperty(Width, value)
return setSizeProperty(properties, Width, value)
case ColorTag:
return outline.setColorProperty(ColorTag, value)
return setColorProperty(properties, ColorTag, value)
default:
ErrorLogF(`"%s" property is not compatible with the OutlineProperty`, tag)
}
return false
}
func (outline *outlinePropertyData) Get(tag string) any {
return outline.propertyList.Get(outline.normalizeTag(tag))
return nil
}
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
type ViewOutline struct {
// Style of the outline line
Style int
// Color of the outline line
Color Color
// Width of the outline line
Width SizeUnit
}
@ -118,7 +107,7 @@ func (outline ViewOutline) cssString(session Session) string {
return builder.finish()
}
func getOutline(properties Properties) OutlineProperty {
func getOutlineProperty(properties Properties) OutlineProperty {
if value := properties.Get(Outline); value != nil {
if outline, ok := value.(OutlineProperty); ok {
return outline
@ -128,30 +117,30 @@ func getOutline(properties Properties) OutlineProperty {
return nil
}
func (style *viewStyle) setOutline(value any) bool {
func setOutlineProperty(properties Properties, value any) []PropertyName {
switch value := value.(type) {
case OutlineProperty:
style.properties[Outline] = value
properties.setRaw(Outline, value)
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:
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:
outline := NewOutlineProperty(nil)
for _, tag := range []string{Style, Width, ColorTag} {
if text, ok := value.PropertyValue(tag); ok && text != "" {
for _, tag := range []PropertyName{Style, Width, ColorTag} {
if text, ok := value.PropertyValue(string(tag)); ok && text != "" {
outline.Set(tag, text)
}
}
style.properties[Outline] = outline
properties.setRaw(Outline, outline)
default:
notCompatibleType(Outline, value)
return false
return nil
}
return true
return []PropertyName{Outline}
}

View File

@ -1,27 +1,34 @@
package rui
import "sort"
import (
"iter"
"slices"
)
// 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)
}
func (params Params) getRaw(tag string) any {
func (params Params) getRaw(tag PropertyName) any {
if value, ok := params[tag]; ok {
return value
}
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)
return true
}
func (params Params) setRaw(tag string, value any) {
func (params Params) setRaw(tag PropertyName, value any) {
if value != nil {
params[tag] = value
} 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)
}
// Clear removes all properties from a map.
func (params Params) Clear() {
for tag := range params {
delete(params, tag)
}
}
func (params Params) AllTags() []string {
tags := make([]string, 0, len(params))
func (params Params) All() iter.Seq2[PropertyName, any] {
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 {
tags = append(tags, t)
}
sort.Strings(tags)
slices.Sort(tags)
return tags
}
func (params Params) IsEmpty() bool {
return len(params) == 0
}

153
path.go
View File

@ -1,15 +1,7 @@
package rui
import (
"strconv"
"strings"
)
// Path is a 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(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.
// 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;
// x1, y1 - coordinates of the second control point;
// radius - the arc's radius. Must be non-negative.
// - x0, y0 - coordinates of the first control point;
// - x1, y1 - coordinates of the second control point;
// - radius - the arc's radius. Must be non-negative.
ArcTo(x0, y0, x1, y1, radius float64)
// Arc adds a circular arc to the current sub-path.
// x, y - coordinates of the arc's center;
// radius - the arc's radius. Must be non-negative;
// startAngle - the angle at which the arc starts, measured clockwise from the positive
// - x, y - coordinates of the arc's center;
// - radius - the arc's radius. Must be non-negative;
// - startAngle - the angle at which the arc starts, measured clockwise from the positive
// 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.
// 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
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
// the latest point in the current path.
// cp0x, cp0y - coordinates of the first control point;
// cp1x, cp1y - coordinates of the second control point;
// x, y - coordinates of the end point.
// - cp0x, cp0y - coordinates of the first control point;
// - cp1x, cp1y - coordinates of the second control point;
// - x, y - coordinates of the end point.
BezierCurveTo(cp0x, cp0y, cp1x, cp1y, x, y float64)
// QuadraticCurveTo adds a quadratic Bézier curve to the current sub-path.
// cpx, cpy - coordinates of the control point;
// x, y - coordinates of the end point.
// - cpx, cpy - coordinates of the control point;
// - x, y - coordinates of the end point.
QuadraticCurveTo(cpx, cpy, x, y float64)
// Ellipse adds an elliptical arc to the current sub-path
// x, y - coordinates of the ellipse's center;
// radiusX - the ellipse's major-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;
// startAngle - the angle at which the ellipse starts, measured clockwise
// - x, y - coordinates of the ellipse's center;
// - radiusX - the ellipse's major-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;
// - startAngle - the angle at which the ellipse starts, measured clockwise
// 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.
// 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)
// 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.
Close()
scriptText() string
obj() any
}
type pathData struct {
script strings.Builder
session Session
varName any
}
// NewPath creates a new empty Path
func NewPath() Path {
func (canvas *canvasData) NewPath() Path {
path := new(pathData)
path.script.Grow(4096)
path.script.WriteString("\nctx.beginPath();")
path.session = canvas.session
path.varName = canvas.session.createPath("")
return path
}
func (path *pathData) Reset() {
path.script.Reset()
path.script.WriteString("\nctx.beginPath();")
func (canvas *canvasData) NewPathFromSvg(data string) Path {
path := new(pathData)
path.session = canvas.session
path.varName = canvas.session.createPath(data)
return path
}
func (path *pathData) MoveTo(x, y float64) {
path.script.WriteString("\nctx.moveTo(")
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
path.script.WriteString(");")
path.session.callCanvasVarFunc(path.varName, "moveTo", x, y)
}
func (path *pathData) LineTo(x, y float64) {
path.script.WriteString("\nctx.lineTo(")
path.script.WriteString(strconv.FormatFloat(x, 'g', -1, 64))
path.script.WriteRune(',')
path.script.WriteString(strconv.FormatFloat(y, 'g', -1, 64))
path.script.WriteString(");")
path.session.callCanvasVarFunc(path.varName, "lineTo", x, y)
}
func (path *pathData) ArcTo(x0, y0, x1, y1, radius float64) {
if radius > 0 {
path.script.WriteString("\nctx.arcTo(")
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(");")
path.session.callCanvasVarFunc(path.varName, "arcTo", x0, y0, x1, y1, radius)
}
}
func (path *pathData) Arc(x, y, radius, startAngle, endAngle float64, clockwise bool) {
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 {
path.script.WriteString(",true);")
path.session.callCanvasVarFunc(path.varName, "arc", x, y, radius, startAngle, endAngle, true)
} 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) {
path.script.WriteString("\nctx.bezierCurveTo(")
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(");")
path.session.callCanvasVarFunc(path.varName, "bezierCurveTo", cp0x, cp0y, cp1x, cp1y, x, y)
}
func (path *pathData) QuadraticCurveTo(cpx, cpy, x, y float64) {
path.script.WriteString("\nctx.quadraticCurveTo(")
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(");")
path.session.callCanvasVarFunc(path.varName, "quadraticCurveTo", cpx, cpy, x, y)
}
func (path *pathData) Ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle float64, clockwise bool) {
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 {
path.script.WriteString(",true);")
path.session.callCanvasVarFunc(path.varName, "ellipse", x, y, radiusX, radiusY, rotation, startAngle, endAngle, true)
} else {
path.script.WriteString(");")
path.session.callCanvasVarFunc(path.varName, "ellipse", x, y, radiusX, radiusY, rotation, startAngle, endAngle)
}
}
}
func (path *pathData) Close() {
path.script.WriteString("\nctx.close();")
path.session.callCanvasVarFunc(path.varName, "closePath")
}
func (path *pathData) scriptText() string {
return path.script.String()
func (path *pathData) obj() any {
return path.varName
}

View File

@ -1,54 +1,133 @@
package rui
import (
"strings"
)
// Constants for [View] specific pointer events properties
const (
// 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.
// For touch, it is fired when physical contact is made with the digitizer.
// For pen, it is fired when the stylus makes physical contact with the digitizer.
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerDown = "pointer-down"
//
// Used by View.
// 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. For touch, it is fired when physical contact is made with the digitizer. For pen, it is
// fired when the stylus makes physical contact with the digitizer.
//
// 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.
// The "pointer-up" event is fired when a pointer is no longer active.
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerUp = "pointer-up"
//
// Used by View.
// Is fired when a pointer is no longer active.
//
// 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.
// The "pointer-move" event is fired when a pointer changes coordinates.
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerMove = "pointer-move"
//
// Used by View.
// Is fired when a pointer changes coordinates.
//
// 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.
// The "pointer-cancel" event is fired if the pointer will no longer be able to generate events
// (for example the related device is deactivated).
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerCancel = "pointer-cancel"
//
// Used by View.
// Is fired if the pointer will no longer be able to generate events (for example the related device is deactivated).
//
// 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.
// 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
// that does not support hover (see "pointer-up"); after firing the pointercancel event (see "pointer-cancel");
// when a pen stylus leaves the hover range detectable by the digitizer.
// The main listener format: func(View, PointerEvent).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerOut = "pointer-out"
//
// Used by View.
// Is fired for several reasons including: pointing device is moved out of the hit test boundaries of an element; firing
// the "pointer-up" event for a device that does not support hover (see "pointer-up"); after firing the "pointer-cancel"
// event (see "pointer-cancel"); when a pen stylus leaves the hover range detectable by the digitizer.
//
// 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.
// 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).
// The additional listener formats: func(PointerEvent), func(View), and func().
PointerOver = "pointer-over"
//
// Used by View.
// Is fired when a pointing device is moved into an view's hit test boundaries.
//
// 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 {
MouseEvent
@ -87,54 +166,6 @@ type PointerEvent struct {
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) {
event.MouseEvent.init(data)
@ -151,8 +182,8 @@ func (event *PointerEvent) init(data DataObject) {
event.IsPrimary = dataBoolProperty(data, "isPrimary")
}
func handlePointerEvents(view View, tag string, data DataObject) {
listeners := getEventListeners[View, PointerEvent](view, nil, tag)
func handlePointerEvents(view View, tag PropertyName, data DataObject) {
listeners := getOneArgEventListeners[View, PointerEvent](view, nil, tag)
if len(listeners) == 0 {
return
}
@ -161,42 +192,96 @@ func handlePointerEvents(view View, tag string, data DataObject) {
event.init(data)
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.
// 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) {
return getEventListeners[View, PointerEvent](view, subviewID, PointerDown)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, PointerEvent](view, subviewID, PointerUp)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, PointerEvent](view, subviewID, PointerMove)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, PointerEvent](view, subviewID, PointerCancel)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, PointerEvent](view, subviewID, PointerOver)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[View, PointerEvent](view, subviewID, PointerOut)
//
// Result elements can be of the following types:
// - 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)
}

1784
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".
//
// 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).
func ShowQuestion(title, text string, session Session, onYes func(), onNo func()) {
textView := NewTextView(session, Params{
@ -28,17 +30,9 @@ func ShowQuestion(title, text string, session Session, onYes func(), onNo func()
CloseButton: false,
OutsideClose: false,
Buttons: []PopupButton{
{
Title: "No",
OnClick: func(popup Popup) {
popup.Dismiss()
if onNo != nil {
onNo()
}
},
},
{
Title: "Yes",
Type: DefaultButton,
OnClick: func(popup Popup) {
popup.Dismiss()
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 != "" {
@ -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".
//
// 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.
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,
Buttons: []PopupButton{
{
Title: "Cancel",
Title: "Yes",
Type: DefaultButton,
OnClick: func(popup Popup) {
popup.Dismiss()
if onCancel != nil {
onCancel()
if onYes != nil {
onYes()
}
},
},
@ -86,11 +92,12 @@ func ShowCancellableQuestion(title, text string, session Session, onYes func(),
},
},
{
Title: "Yes",
Title: "Cancel",
Type: CancelButton,
OnClick: func(popup Popup) {
popup.Dismiss()
if onYes != nil {
onYes()
if onCancel != nil {
onCancel()
}
},
},
@ -127,10 +134,14 @@ func (popup *popupMenuData) ListSize() int {
}
func (popup *popupMenuData) ListItem(index int, session Session) View {
return NewTextView(popup.session, Params{
view := NewTextView(popup.session, Params{
Text: popup.items[index],
Style: "ruiPopupMenuItem",
})
if !popup.IsListItemEnabled(index) {
view.Set(TextColor, "@ruiDisabledTextColor")
}
return view
}
func (popup *popupMenuData) IsListItemEnabled(index int) bool {
@ -144,9 +155,12 @@ func (popup *popupMenuData) IsListItemEnabled(index int) bool {
return true
}
// PopupMenuResult is the constant for the "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.
// PopupMenuResult is the constant for "popup-menu-result" property tag.
//
// 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"
// ShowMenu displays the menu. Menu items are set using the Items property.

View File

@ -5,12 +5,30 @@ import (
"strings"
)
// Constants for [ProgressBar] specific properties and events
const (
ProgressBarMax = "progress-max"
ProgressBarValue = "progress-value"
// ProgressBarMax is the constant for "progress-max" property tag.
//
// 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 {
View
}
@ -28,20 +46,18 @@ func NewProgressBar(session Session, params Params) ProgressBar {
}
func newProgressBar(session Session) View {
return NewProgressBar(session, nil)
return new(progressBarData)
}
func (progress *progressBarData) init(session Session) {
progress.viewData.init(session)
progress.tag = "ProgressBar"
progress.normalize = normalizeProgressBarTag
progress.changed = progress.propertyChanged
}
func (progress *progressBarData) String() string {
return getViewString(progress)
}
func (progress *progressBarData) normalizeTag(tag string) string {
tag = strings.ToLower(tag)
func normalizeProgressBarTag(tag PropertyName) PropertyName {
tag = defaultNormalize(tag)
switch tag {
case Max, "progress-bar-max", "progressbar-max":
return ProgressBarMax
@ -52,41 +68,20 @@ func (progress *progressBarData) normalizeTag(tag string) string {
return tag
}
func (progress *progressBarData) Remove(tag string) {
progress.remove(progress.normalizeTag(tag))
}
func (progress *progressBarData) propertyChanged(tag PropertyName) {
func (progress *progressBarData) remove(tag string) {
progress.viewData.remove(tag)
progress.propertyChanged(tag)
}
func (progress *progressBarData) propertyChanged(tag string) {
if progress.created {
switch tag {
case ProgressBarMax:
updateProperty(progress.htmlID(), Max, strconv.FormatFloat(GetProgressBarMax(progress), 'f', -1, 32), progress.session)
progress.Session().updateProperty(progress.htmlID(), "max",
strconv.FormatFloat(GetProgressBarMax(progress), 'f', -1, 32))
case ProgressBarValue:
updateProperty(progress.htmlID(), Value, strconv.FormatFloat(GetProgressBarValue(progress), 'f', -1, 32), progress.session)
}
}
}
progress.Session().updateProperty(progress.htmlID(), "value",
strconv.FormatFloat(GetProgressBarValue(progress), 'f', -1, 32))
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
default:
progress.viewData.propertyChanged(tag)
}
return false
}
func (progress *progressBarData) Get(tag string) any {
return progress.get(progress.normalizeTag(tag))
}
func (progress *progressBarData) htmlTag() string {
@ -106,13 +101,17 @@ func (progress *progressBarData) htmlProperties(self View, buffer *strings.Build
}
// 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 {
return floatStyledProperty(view, subviewID, ProgressBarMax, 1)
}
// 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 {
return floatStyledProperty(view, subviewID, ProgressBarValue, 0)
}

View File

@ -1,7 +1,8 @@
package rui
import (
"sort"
"iter"
"slices"
"strings"
)
@ -9,79 +10,205 @@ import (
type Properties interface {
// 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.
Get(tag string) any
getRaw(tag string) any
Get(tag PropertyName) any
getRaw(tag PropertyName) any
// 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
// a description of the error is written to the log
Set(tag string, value any) bool
setRaw(tag string, value any)
Set(tag PropertyName, value any) bool
setRaw(tag PropertyName, value any)
// Remove removes the property with name defined by the argument
Remove(tag string)
Remove(tag PropertyName)
// Clear removes all properties
Clear()
// All returns an iterator to access the properties
All() iter.Seq2[PropertyName, any]
// AllTags returns an array of the set properties
AllTags() []string
AllTags() []PropertyName
IsEmpty() bool
}
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() {
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 {
return properties.getRaw(strings.ToLower(tag))
func (properties *propertyList) IsEmpty() bool {
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 {
return value
}
return nil
}
func (properties *propertyList) setRaw(tag string, value any) {
properties.properties[tag] = value
}
func (properties *propertyList) Remove(tag string) {
delete(properties.properties, strings.ToLower(tag))
}
func (properties *propertyList) remove(tag string) {
func (properties *propertyList) setRaw(tag PropertyName, value any) {
if value == nil {
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)
} else {
properties.properties[tag] = value
}
sort.Strings(tags)
}
/*
func (properties *propertyList) Remove(tag PropertyName) {
properties.remove(properties, properties.normalize(tag))
}
*/
func (properties *propertyList) Clear() {
properties.properties = map[PropertyName]any{}
}
func (properties *propertyList) All() iter.Seq2[PropertyName, any] {
return func(yield func(PropertyName, any) bool) {
for tag, value := range properties.properties {
if !yield(tag, value) {
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) {
count := object.PropertyCount()
for i := 0; i < count; i++ {
if node := object.Property(i); node != nil {
for node := range object.Properties() {
switch node.Type() {
case TextNode:
properties.Set(node.Tag(), node.Text())
properties.Set(PropertyName(node.Tag()), node.Text())
case ObjectNode:
properties.Set(node.Tag(), node.Object())
properties.Set(PropertyName(node.Tag()), node.Object())
case ArrayNode:
properties.Set(node.Tag(), node.ArrayElements())
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"
)
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 text, ok := value.(string); ok {
return session.resolveConstants(text)
@ -15,11 +15,11 @@ func stringProperty(properties Properties, tag string, session Session) (string,
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 text, ok := value.(string); ok {
if text != "" && text[0] == '@' {
if image, ok := session.ImageConstant(text[1:]); ok {
if ok, constName := isConstantName(text); ok {
if image, ok := session.ImageConstant(constName); ok {
return image, true
} else {
return "", false
@ -61,11 +61,11 @@ func valueToSizeUnit(value any, session Session) (SizeUnit, bool) {
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)
}
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 {
switch value := value.(type) {
case AngleUnit:
@ -88,8 +88,8 @@ func valueToColor(value any, session Session) (Color, bool) {
return value, true
case string:
if len(value) > 1 && value[0] == '@' {
return session.Color(value[1:])
if ok, constName := isConstantName(value); ok {
return session.Color(constName)
}
return StringToColor(value)
}
@ -98,11 +98,11 @@ func valueToColor(value any, session Session) (Color, bool) {
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)
}
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 {
values := enumProperties[tag].values
switch value := value.(type) {
@ -165,7 +165,7 @@ func enumStringToInt(value string, enumValues []string, logError bool) (int, boo
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)
}
@ -194,7 +194,7 @@ func valueToBool(value any, session Session) (bool, bool) {
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)
}
@ -224,7 +224,7 @@ func valueToInt(value any, session Session, defaultValue int) (int, bool) {
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)
}
@ -248,7 +248,7 @@ func valueToFloat(value any, session Session, defaultValue float64) (float64, bo
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)
}
@ -272,7 +272,7 @@ func valueToFloatText(value any, session Session, defaultValue float64) (string,
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)
}
@ -297,6 +297,6 @@ func valueToRange(value any, session Session) (Range, bool) {
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)
}

File diff suppressed because it is too large Load Diff

View File

@ -2,11 +2,12 @@ package rui
import (
"math"
"slices"
"strconv"
"strings"
)
var colorProperties = []string{
var colorProperties = []PropertyName{
ColorTag,
BackgroundColor,
TextColor,
@ -19,25 +20,24 @@ var colorProperties = []string{
OutlineColor,
TextLineColor,
ColorPickerValue,
AccentColor,
}
func isPropertyInList(tag string, list []string) bool {
/*
func isPropertyInList(tag PropertyName, list []PropertyName) bool {
for _, prop := range list {
if prop == tag {
return true
}
}
return false
}
var angleProperties = []string{
Rotate,
SkewX,
SkewY,
}
*/
var angleProperties = []PropertyName{
From,
}
var boolProperties = []string{
var boolProperties = []PropertyName{
Disabled,
Focusable,
Inset,
@ -63,9 +63,12 @@ var boolProperties = []string{
TabCloseButton,
Repeating,
UserSelect,
ColumnSpanAll,
MoveToFrontAnimation,
HideSummaryMarker,
}
var intProperties = []string{
var intProperties = []PropertyName{
ZIndex,
TabSize,
HeadHeight,
@ -73,16 +76,15 @@ var intProperties = []string{
RowSpan,
ColumnSpan,
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},
ScaleX: {min: -math.MaxFloat64, max: math.MaxFloat64},
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},
ShowOpacity: {min: 0, max: 1},
NumberPickerMax: {min: -math.MaxFloat64, max: math.MaxFloat64},
NumberPickerMin: {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},
VideoWidth: {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{
Width: Width,
Height: Height,
MinWidth: MinWidth,
MinHeight: MinHeight,
MaxWidth: MaxWidth,
MaxHeight: MaxHeight,
Left: Left,
Right: Right,
Top: Top,
Bottom: Bottom,
var sizeProperties = map[PropertyName]string{
Width: string(Width),
Height: string(Height),
MinWidth: string(MinWidth),
MinHeight: string(MinHeight),
MaxWidth: string(MaxWidth),
MaxHeight: string(MaxHeight),
Left: string(Left),
Right: string(Right),
Top: string(Top),
Bottom: string(Bottom),
TextSize: "font-size",
TextIndent: TextIndent,
LetterSpacing: LetterSpacing,
WordSpacing: WordSpacing,
LineHeight: LineHeight,
TextIndent: string(TextIndent),
LetterSpacing: string(LetterSpacing),
WordSpacing: string(WordSpacing),
LineHeight: string(LineHeight),
TextLineThickness: "text-decoration-thickness",
ListRowGap: "row-gap",
ListColumnGap: "column-gap",
GridRowGap: GridRowGap,
GridColumnGap: GridColumnGap,
ColumnWidth: ColumnWidth,
ColumnGap: ColumnGap,
Gap: Gap,
Margin: Margin,
MarginLeft: MarginLeft,
MarginRight: MarginRight,
MarginTop: MarginTop,
MarginBottom: MarginBottom,
Padding: Padding,
PaddingLeft: PaddingLeft,
PaddingRight: PaddingRight,
PaddingTop: PaddingTop,
PaddingBottom: PaddingBottom,
BorderWidth: BorderWidth,
BorderLeftWidth: BorderLeftWidth,
BorderRightWidth: BorderRightWidth,
BorderTopWidth: BorderTopWidth,
BorderBottomWidth: BorderBottomWidth,
OutlineWidth: OutlineWidth,
XOffset: XOffset,
YOffset: YOffset,
BlurRadius: BlurRadius,
SpreadRadius: SpreadRadius,
Perspective: Perspective,
PerspectiveOriginX: PerspectiveOriginX,
PerspectiveOriginY: PerspectiveOriginY,
OriginX: OriginX,
OriginY: OriginY,
OriginZ: OriginZ,
TranslateX: TranslateX,
TranslateY: TranslateY,
TranslateZ: TranslateZ,
Radius: Radius,
RadiusX: RadiusX,
RadiusY: RadiusY,
RadiusTopLeft: RadiusTopLeft,
RadiusTopLeftX: RadiusTopLeftX,
RadiusTopLeftY: RadiusTopLeftY,
RadiusTopRight: RadiusTopRight,
RadiusTopRightX: RadiusTopRightX,
RadiusTopRightY: RadiusTopRightY,
RadiusBottomLeft: RadiusBottomLeft,
RadiusBottomLeftX: RadiusBottomLeftX,
RadiusBottomLeftY: RadiusBottomLeftY,
RadiusBottomRight: RadiusBottomRight,
RadiusBottomRightX: RadiusBottomRightX,
RadiusBottomRightY: RadiusBottomRightY,
ItemWidth: ItemWidth,
ItemHeight: ItemHeight,
CenterX: CenterX,
CenterY: CenterX,
GridRowGap: string(GridRowGap),
GridColumnGap: string(GridColumnGap),
ColumnWidth: string(ColumnWidth),
ColumnGap: string(ColumnGap),
Gap: string(Gap),
Margin: string(Margin),
MarginLeft: string(MarginLeft),
MarginRight: string(MarginRight),
MarginTop: string(MarginTop),
MarginBottom: string(MarginBottom),
Padding: string(Padding),
PaddingLeft: string(PaddingLeft),
PaddingRight: string(PaddingRight),
PaddingTop: string(PaddingTop),
PaddingBottom: string(PaddingBottom),
BorderWidth: string(BorderWidth),
BorderLeftWidth: string(BorderLeftWidth),
BorderRightWidth: string(BorderRightWidth),
BorderTopWidth: string(BorderTopWidth),
BorderBottomWidth: string(BorderBottomWidth),
OutlineWidth: string(OutlineWidth),
OutlineOffset: string(OutlineOffset),
XOffset: string(XOffset),
YOffset: string(YOffset),
BlurRadius: string(BlurRadius),
SpreadRadius: string(SpreadRadius),
Perspective: string(Perspective),
PerspectiveOriginX: string(PerspectiveOriginX),
PerspectiveOriginY: string(PerspectiveOriginY),
TransformOriginX: string(TransformOriginX),
TransformOriginY: string(TransformOriginY),
TransformOriginZ: string(TransformOriginZ),
Radius: string(Radius),
RadiusX: string(RadiusX),
RadiusY: string(RadiusY),
RadiusTopLeft: string(RadiusTopLeft),
RadiusTopLeftX: string(RadiusTopLeftX),
RadiusTopLeftY: string(RadiusTopLeftY),
RadiusTopRight: string(RadiusTopRight),
RadiusTopRightX: string(RadiusTopRightX),
RadiusTopRightY: string(RadiusTopRightY),
RadiusBottomLeft: string(RadiusBottomLeft),
RadiusBottomLeftX: string(RadiusBottomLeftX),
RadiusBottomLeftY: string(RadiusBottomLeftY),
RadiusBottomRight: string(RadiusBottomRight),
RadiusBottomRightX: string(RadiusBottomRightX),
RadiusBottomRightY: string(RadiusBottomRightY),
ItemWidth: string(ItemWidth),
ItemHeight: string(ItemHeight),
CenterX: string(CenterX),
CenterY: string(CenterX),
ArrowSize: "",
ArrowWidth: "",
ArrowOffset: "",
}
var enumProperties = map[string]struct {
type enumPropertyData struct {
values []string
cssTag string
cssValues []string
}{
}
var enumProperties = map[PropertyName]enumPropertyData{
Semantics: {
[]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: {
[]string{"hidden", "visible", "scroll", "auto"},
Overflow,
string(Overflow),
[]string{"hidden", "visible", "scroll", "auto"},
},
TextAlign: {
[]string{"left", "right", "center", "justify"},
TextAlign,
string(TextAlign),
[]string{"left", "right", "center", "justify"},
},
TextTransform: {
[]string{"none", "capitalize", "lowercase", "uppercase"},
TextTransform,
string(TextTransform),
[]string{"none", "capitalize", "lowercase", "uppercase"},
},
TextWeight: {
@ -204,22 +213,27 @@ var enumProperties = map[string]struct {
},
WhiteSpace: {
[]string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"},
WhiteSpace,
string(WhiteSpace),
[]string{"normal", "nowrap", "pre", "pre-wrap", "pre-line", "break-spaces"},
},
WordBreak: {
[]string{"normal", "break-all", "keep-all", "break-word"},
WordBreak,
string(WordBreak),
[]string{"normal", "break-all", "keep-all", "break-word"},
},
TextOverflow: {
[]string{"clip", "ellipsis"},
TextOverflow,
string(TextOverflow),
[]string{"clip", "ellipsis"},
},
TextWrap: {
[]string{"wrap", "nowrap", "balance"},
string(TextWrap),
[]string{"wrap", "nowrap", "balance"},
},
WritingMode: {
[]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"},
},
TextDirection: {
@ -239,7 +253,7 @@ var enumProperties = map[string]struct {
},
BorderStyle: {
[]string{"none", "solid", "dashed", "dotted", "double"},
BorderStyle,
string(BorderStyle),
[]string{"none", "solid", "dashed", "dotted", "double"},
},
TopStyle: {
@ -264,7 +278,7 @@ var enumProperties = map[string]struct {
},
OutlineStyle: {
[]string{"none", "solid", "dashed", "dotted", "double"},
OutlineStyle,
string(OutlineStyle),
[]string{"none", "solid", "dashed", "dotted", "double"},
},
Tabs: {
@ -327,9 +341,19 @@ var enumProperties = map[string]struct {
"justify-items",
[]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: {
[]string{"row", "column", "row-dense", "column-dense"},
GridAutoFlow,
string(GridAutoFlow),
[]string{"row", "column", "row dense", "column dense"},
},
ImageVerticalAlign: {
@ -369,7 +393,7 @@ var enumProperties = map[string]struct {
},
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"},
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"},
},
Fit: {
@ -397,6 +421,21 @@ var enumProperties = map[string]struct {
"background-clip",
[]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: {
[]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"},
},
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) {
ErrorLogF(`"%T" type not compatible with "%s" property`, value, tag)
func notCompatibleType(tag PropertyName, value any) {
ErrorLogF(`"%T" type not compatible with "%s" property`, value, string(tag))
}
func invalidPropertyValue(tag string, value any) {
ErrorLogF(`Invalid value "%v" of "%s" property`, value, tag)
func invalidPropertyValue(tag PropertyName, value any) {
ErrorLogF(`Invalid value "%v" of "%s" property`, value, string(tag))
}
func isConstantName(text string) bool {
func isConstantName(text string) (bool, string) {
len := len(text)
if len <= 1 || text[0] != '@' {
return false
if len <= 1 || text[0] != '@' ||
strings.ContainsAny(text, ",;|\"'`+(){}[]<>/\\*&%! \t\n\r") {
return false, ""
}
if len > 2 {
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")
return true, text[1:]
}
func isInt(value any) (int, bool) {
@ -515,26 +561,48 @@ func isInt(value any) (int, bool) {
return n, true
}
func (properties *propertyList) setSimpleProperty(tag string, value any) bool {
func setSimpleProperty(properties Properties, tag PropertyName, value any) bool {
if value == nil {
delete(properties.properties, tag)
properties.setRaw(tag, nil)
return true
} else if text, ok := value.(string); ok {
text = strings.Trim(text, " \t\n\r")
if text == "" {
delete(properties.properties, tag)
properties.setRaw(tag, nil)
return true
}
if isConstantName(text) {
properties.properties[tag] = text
if ok, _ := isConstantName(text); ok {
properties.setRaw(tag, text)
return true
}
}
return false
}
func (properties *propertyList) setSizeProperty(tag string, value any) bool {
if !properties.setSimpleProperty(tag, value) {
func setStringPropertyValue(properties Properties, tag PropertyName, text any) []PropertyName {
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
switch value := value.(type) {
case string:
@ -544,7 +612,7 @@ func (properties *propertyList) setSizeProperty(tag string, value any) bool {
size.Function = fn
} else if size, ok = StringToSizeUnit(value); !ok {
invalidPropertyValue(tag, value)
return false
return nil
}
case SizeUnit:
size = value
@ -567,29 +635,29 @@ func (properties *propertyList) setSizeProperty(tag string, value any) bool {
size.Value = float64(n)
} else {
notCompatibleType(tag, value)
return false
return nil
}
}
if size.Type == Auto {
delete(properties.properties, tag)
properties.setRaw(tag, nil)
} else {
properties.properties[tag] = size
properties.setRaw(tag, size)
}
}
return true
return []PropertyName{tag}
}
func (properties *propertyList) setAngleProperty(tag string, value any) bool {
if !properties.setSimpleProperty(tag, value) {
func setAngleProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
var angle AngleUnit
switch value := value.(type) {
case string:
var ok bool
if angle, ok = StringToAngleUnit(value); !ok {
invalidPropertyValue(tag, value)
return false
return nil
}
case AngleUnit:
angle = value
@ -605,24 +673,24 @@ func (properties *propertyList) setAngleProperty(tag string, value any) bool {
angle = Rad(float64(n))
} else {
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 {
if !properties.setSimpleProperty(tag, value) {
func setColorProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
var result Color
switch value := value.(type) {
case string:
var err error
if result, err = stringToColor(value); err != nil {
invalidPropertyValue(tag, value)
return false
return nil
}
case Color:
result = value
@ -632,105 +700,101 @@ func (properties *propertyList) setColorProperty(tag string, value any) bool {
result = Color(color)
} else {
notCompatibleType(tag, value)
return false
return nil
}
}
if result == 0 {
delete(properties.properties, tag)
} else {
properties.properties[tag] = result
}
properties.setRaw(tag, result)
}
return true
return []PropertyName{tag}
}
func (properties *propertyList) setEnumProperty(tag string, value any, values []string) bool {
if !properties.setSimpleProperty(tag, value) {
func setEnumProperty(properties Properties, tag PropertyName, value any, values []string) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
var n int
if text, ok := value.(string); ok {
if n, ok = enumStringToInt(text, values, false); !ok {
invalidPropertyValue(tag, value)
return false
return nil
}
} else if i, ok := isInt(value); ok {
if i < 0 || i >= len(values) {
invalidPropertyValue(tag, value)
return false
return nil
}
n = i
} else {
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 {
if !properties.setSimpleProperty(tag, value) {
func setBoolProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
if text, ok := value.(string); ok {
switch strings.ToLower(strings.Trim(text, " \t")) {
case "true", "yes", "on", "1":
properties.properties[tag] = true
properties.setRaw(tag, true)
case "false", "no", "off", "0":
properties.properties[tag] = false
properties.setRaw(tag, false)
default:
invalidPropertyValue(tag, value)
return false
return nil
}
} else if n, ok := isInt(value); ok {
switch n {
case 1:
properties.properties[tag] = true
properties.setRaw(tag, true)
case 0:
properties.properties[tag] = false
properties.setRaw(tag, false)
default:
invalidPropertyValue(tag, value)
return false
return nil
}
} else if b, ok := value.(bool); ok {
properties.properties[tag] = b
properties.setRaw(tag, b)
} else {
notCompatibleType(tag, value)
return false
return nil
}
}
return true
return []PropertyName{tag}
}
func (properties *propertyList) setIntProperty(tag string, value any) bool {
if !properties.setSimpleProperty(tag, value) {
func setIntProperty(properties Properties, tag PropertyName, value any) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
if text, ok := value.(string); ok {
n, err := strconv.Atoi(strings.Trim(text, " \t"))
if err != nil {
invalidPropertyValue(tag, value)
ErrorLog(err.Error())
return false
return nil
}
properties.properties[tag] = n
properties.setRaw(tag, n)
} else if n, ok := isInt(value); ok {
properties.properties[tag] = n
properties.setRaw(tag, n)
} else {
notCompatibleType(tag, value)
return false
return nil
}
}
return true
return []PropertyName{tag}
}
func (properties *propertyList) setFloatProperty(tag string, value any, min, max float64) bool {
if !properties.setSimpleProperty(tag, value) {
func setFloatProperty(properties Properties, tag PropertyName, value any, min, max float64) []PropertyName {
if !setSimpleProperty(properties, tag, value) {
f := float64(0)
switch value := value.(type) {
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 {
invalidPropertyValue(tag, value)
ErrorLog(err.Error())
return false
return nil
}
if f < min || f > max {
ErrorLogF(`"%T" out of range of "%s" property`, value, tag)
return false
return nil
}
properties.properties[tag] = value
return true
properties.setRaw(tag, value)
return nil
case float32:
f = float64(value)
@ -758,64 +822,70 @@ func (properties *propertyList) setFloatProperty(tag string, value any, min, max
f = float64(n)
} else {
notCompatibleType(tag, value)
return false
return nil
}
}
if f >= min && f <= max {
properties.properties[tag] = f
properties.setRaw(tag, f)
} else {
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 {
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
}
func propertiesSet(properties Properties, tag PropertyName, value any) []PropertyName {
if _, ok := sizeProperties[tag]; ok {
return properties.setSizeProperty(tag, value)
return setSizeProperty(properties, tag, value)
}
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 {
return properties.setFloatProperty(tag, value, limits.min, limits.max)
return setFloatProperty(properties, tag, value, limits.min, limits.max)
}
if isPropertyInList(tag, colorProperties) {
return properties.setColorProperty(tag, value)
if slices.Contains(colorProperties, tag) {
return setColorProperty(properties, tag, value)
}
if isPropertyInList(tag, angleProperties) {
return properties.setAngleProperty(tag, value)
if slices.Contains(angleProperties, tag) {
return setAngleProperty(properties, tag, value)
}
if isPropertyInList(tag, boolProperties) {
return properties.setBoolProperty(tag, value)
if slices.Contains(boolProperties, tag) {
return setBoolProperty(properties, tag, value)
}
if isPropertyInList(tag, intProperties) {
return properties.setIntProperty(tag, value)
if slices.Contains(intProperties, tag) {
return setIntProperty(properties, tag, value)
}
if text, ok := value.(string); ok {
properties.properties[tag] = text
return true
properties.setRaw(tag, text)
return []PropertyName{tag}
}
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
}

View File

@ -1,5 +1,6 @@
package rui
// Constants for various specific properties of a views
const (
// Visible - default value of the view Visibility property: View is visible
Visible = 0
@ -148,9 +149,9 @@ const (
WhiteSpacePreLine = 4
// 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.
// * 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).
// - 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.
// - 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
// 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.
// This may cause views to appear out-of-order, when doing so would fill in holes left by larger views.
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
)

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

View File

@ -1,44 +1,52 @@
package rui
// 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().
const ResizeEvent = "resize-event"
// func(view rui.View, frame rui.Frame)
//
// 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) {
view.frame.Left = x
view.frame.Top = y
view.frame.Width = width
view.frame.Height = height
for _, listener := range GetResizeListeners(view) {
listener(self, view.frame)
for _, listener := range getOneArgEventListeners[View, Frame](view, nil, ResizeEvent) {
listener.Run(self, view.frame)
}
}
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)
if !ok {
/*
func setFrameListener(properties Properties, tag PropertyName, value any) bool {
if listeners, ok := valueToOneArgEventListeners[View, Frame](value); ok {
if len(listeners) == 0 {
properties.setRaw(tag, nil)
} else {
properties.setRaw(tag, listeners)
}
return true
}
notCompatibleType(tag, value)
return false
}
if listeners == nil {
delete(view.properties, tag)
} else {
view.properties[tag] = listeners
}
view.propertyChangedEvent(tag)
return true
}
*/
func (view *viewData) setNoResizeEvent() {
view.noResizeEvent = true
@ -65,11 +73,11 @@ func (view *viewData) Frame() Frame {
}
// 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
view = getSubview(view, subviewID)
if view == nil {
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
// 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) {
return getEventListeners[View, Frame](view, subviewID, ResizeEvent)
//
// Result elements can be of the following types:
// - 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 (
"embed"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
@ -15,6 +16,7 @@ const (
imageDir = "images"
themeDir = "themes"
viewDir = "views"
popupDir = "popups"
rawDir = "raw"
stringsDir = "strings"
)
@ -44,19 +46,20 @@ var resources = resourceManager{
imageSrcSets: map[string][]scaledImage{},
}
// AddEmbedResources adds embedded resources to the list of application resources
func AddEmbedResources(fs *embed.FS) {
resources.embedFS = append(resources.embedFS, fs)
rootDirs := embedRootDirs(fs)
rootDirs := resources.embedRootDirs(fs)
for _, dir := range rootDirs {
switch dir {
case imageDir:
scanEmbedImagesDir(fs, dir, "")
resources.scanEmbedImagesDir(fs, dir, "")
case themeDir:
scanEmbedThemesDir(fs, dir)
resources.scanEmbedThemesDir(fs, dir)
case stringsDir:
scanEmbedStringsDir(fs, dir)
resources.scanEmbedStringsDir(fs, dir)
case viewDir, rawDir:
// do nothing
@ -67,13 +70,13 @@ func AddEmbedResources(fs *embed.FS) {
if file.IsDir() {
switch file.Name() {
case imageDir:
scanEmbedImagesDir(fs, dir+"/"+imageDir, "")
resources.scanEmbedImagesDir(fs, dir+"/"+imageDir, "")
case themeDir:
scanEmbedThemesDir(fs, dir+"/"+themeDir)
resources.scanEmbedThemesDir(fs, dir+"/"+themeDir)
case stringsDir:
scanEmbedStringsDir(fs, dir+"/"+stringsDir)
resources.scanEmbedStringsDir(fs, dir+"/"+stringsDir)
case viewDir, rawDir:
// 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{}
if files, err := fs.ReadDir("."); err == nil {
for _, file := range files {
@ -97,34 +100,34 @@ func embedRootDirs(fs *embed.FS) []string {
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 {
for _, file := range files {
name := file.Name()
path := dir + "/" + name
if file.IsDir() {
scanEmbedThemesDir(fs, path)
resources.scanEmbedThemesDir(fs, path)
} else if strings.ToLower(filepath.Ext(name)) == ".rui" {
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 {
for _, file := range files {
name := file.Name()
path := dir + "/" + name
if file.IsDir() {
scanEmbedImagesDir(fs, path, prefix+name+"/")
resources.scanEmbedImagesDir(fs, path, prefix+name+"/")
} else {
ext := strings.ToLower(filepath.Ext(name))
switch ext {
case ".png", ".jpg", ".jpeg", ".svg":
registerImage(fs, path, prefix+name)
case ".png", ".jpg", ".jpeg", ".svg", ".gif", ".bmp", ".webp":
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)`)
}
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}
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 {
for _, file := range files {
filename := file.Name()
if filename[0] != '.' {
newPath := path + `/` + filename
if !file.IsDir() {
registerImage(nil, newPath, filePrefix+filename)
resources.registerImage(nil, newPath, filePrefix+filename)
} 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 {
for _, file := range files {
filename := file.Name()
if filename[0] != '.' {
newPath := path + `/` + filename
if file.IsDir() {
scanThemesDir(newPath)
resources.scanThemesDir(newPath)
} else if strings.ToLower(filepath.Ext(newPath)) == ".rui" {
if data, err := os.ReadFile(newPath); err == nil {
registerThemeText(string(data))
resources.registerThemeText(string(data))
} else {
ErrorLog(err.Error())
}
@ -217,12 +220,21 @@ func SetResourcePath(path string) {
resources.path += "/"
}
scanImagesDirectory(resources.path+imageDir, "")
scanThemesDir(resources.path + themeDir)
scanStringsDir(resources.path + stringsDir)
resources.scanImagesDirectory(resources.path+imageDir, "")
resources.scanThemesDir(resources.path + themeDir)
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)
if !ok {
return false
@ -268,12 +280,12 @@ func serveResourceFile(filename string, w http.ResponseWriter, r *http.Request)
if serveEmbed(fs, filename) {
return true
}
for _, dir := range embedRootDirs(fs) {
for _, dir := range resources.embedRootDirs(fs) {
if serveEmbed(fs, dir+"/"+filename) {
return true
}
if subdirs, err := fs.ReadDir(dir); err == nil {
for _, subdir := range subdirs {
if subDirs, err := fs.ReadDir(dir); err == nil {
for _, subdir := range subDirs {
if subdir.IsDir() {
if serveEmbed(fs, dir+"/"+subdir.Name()+"/"+filename) {
return true
@ -314,12 +326,13 @@ func serveResourceFile(filename string, w http.ResponseWriter, r *http.Request)
return false
}
// ReadRawResource returns the contents of the raw resource with the specified name
func ReadRawResource(filename string) []byte {
for _, fs := range resources.embedFS {
rootDirs := embedRootDirs(fs)
rootDirs := resources.embedRootDirs(fs)
for _, dir := range rootDirs {
switch dir {
case imageDir, themeDir, viewDir:
case imageDir, themeDir, viewDir, stringsDir:
// do nothing
case rawDir:
@ -351,11 +364,50 @@ func ReadRawResource(filename string) []byte {
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 {
result := []string{}
for _, fs := range resources.embedFS {
rootDirs := embedRootDirs(fs)
rootDirs := resources.embedRootDirs(fs)
for _, dir := range rootDirs {
switch dir {
case imageDir, themeDir, viewDir:
@ -397,6 +449,7 @@ func AllRawResources() []string {
return result
}
// AllImageResources returns the list of all image resources
func AllImageResources() []string {
result := make([]string, 0, len(resources.images))
for image := range resources.images {
@ -406,6 +459,7 @@ func AllImageResources() []string {
return result
}
// AddTheme adds theme to application
func AddTheme(theme Theme) {
if theme != nil {
name := theme.Name()

View File

@ -59,7 +59,7 @@ func (writer *ruiWriterData) writeString(str string) {
{old: "\"", new: `\"`},
}
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.WriteString(str)

View File

@ -1,25 +1,32 @@
package rui
import "fmt"
// 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().
const ScrollEvent = "scroll-event"
// func(view rui.View, frame rui.Frame)
//
// 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) {
view.scroll.Left = x
view.scroll.Top = y
view.scroll.Width = width
view.scroll.Height = height
for _, listener := range GetScrollListeners(view) {
listener(self, view.scroll)
for _, listener := range getOneArgEventListeners[View, Frame](view, nil, ScrollEvent) {
listener.Run(self, view.scroll)
}
}
@ -35,11 +42,11 @@ func (view *viewData) setScroll(x, y, width, height float64) {
}
// 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
view = getSubview(view, subviewID)
if view == nil {
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
// 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) {
return getEventListeners[View, Frame](view, subviewID, ResizeEvent)
//
// Result elements can be of the following types:
// - 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.
// 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) {
if subviewID != "" {
view = ViewByID(view, subviewID)
}
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.
// 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) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
view.Session().runScript(`scrollToStart("` + view.htmlID() + `")`)
if view = getSubview(view, subviewID); view != nil {
view.Session().callFunc("scrollToStart", view.htmlID())
}
}
// 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) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
view.Session().runScript(`scrollToEnd("` + view.htmlID() + `")`)
if view = getSubview(view, subviewID); view != nil {
view.Session().callFunc("scrollToEnd", view.htmlID())
}
}

View File

@ -7,8 +7,37 @@ import (
"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
type SessionContent interface {
// CreateRootView will be called by the library to create a root view of the application
CreateRootView(session Session) View
}
@ -49,7 +78,7 @@ type Session interface {
// Content returns the SessionContent of session
Content() SessionContent
setContent(content SessionContent, self Session) bool
setContent(content SessionContent) bool
// SetTitle sets the text of the browser title/tab
SetTitle(title string)
@ -60,11 +89,11 @@ type Session interface {
RootView() View
// 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.
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.
// 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
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(path string)
@ -73,26 +102,70 @@ type Session interface {
// OpenURL opens the url in the new browser tab
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
resolveConstants(value string) (string, bool)
checkboxOffImage() string
checkboxOnImage() string
checkboxOffImage(accentColor Color) string
checkboxOnImage(accentColor Color) string
radiobuttonOffImage() string
radiobuttonOnImage() string
radiobuttonOnImage(accentColor Color) string
viewByHTMLID(id string) View
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)
runScript(script string)
runGetterScript(script string) DataObject //, answer chan DataObject)
handleAnswer(data DataObject)
callFunc(funcName string, args ...any)
updateInnerHTML(htmlID, html string)
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)
handleResize(data DataObject)
handleViewEvent(command string, data DataObject)
handleEvent(command string, data DataObject)
close()
onStart()
@ -107,10 +180,6 @@ type Session interface {
popupManager() *popupManager
imageManager() *imageManager
startUpdateScript(htmlID string)
updateScript(htmlID string) *strings.Builder
finishUpdateScript(htmlID string)
}
type sessionData struct {
@ -137,11 +206,16 @@ type sessionData struct {
ignoreUpdates bool
popups *popupManager
images *imageManager
brige WebBrige
bridge bridge
events chan DataObject
animationCounter int
animationCSS string
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 {
@ -158,6 +232,10 @@ func newSession(app Application, id int, customTheme string, params DataObject)
session.animationCounter = 0
session.animationCSS = ""
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 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 {
session.touchScreen = (value == "1" || value == "true")
}
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
}
if params != nil {
session.handleSessionInfo(params)
}
return session
@ -211,18 +259,19 @@ func (session *sessionData) ID() int {
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.brige = brige
session.bridge = bridge
}
func (session *sessionData) close() {
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 {
return style.getRaw(propertyTag)
}
@ -252,10 +301,10 @@ func (session *sessionData) Content() SessionContent {
return session.content
}
func (session *sessionData) setContent(content SessionContent, self Session) bool {
func (session *sessionData) setContent(content SessionContent) bool {
if content != nil {
session.content = content
session.rootView = content.CreateRootView(self)
session.rootView = content.CreateRootView(session)
if session.rootView != nil {
session.rootView.setParentID("ruiRootView")
return true
@ -279,47 +328,63 @@ func (session *sessionData) writeInitScript(writer *strings.Builder) {
if session.rootView != nil {
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();")
}
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() {
css := appStyles + session.getCurrentTheme().cssText(session) + session.animationCSS
session.bridge.callFunc("setStyles", css)
if session.rootView != nil {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
css := appStyles + session.getCurrentTheme().cssText(session) + session.animationCSS
css = strings.ReplaceAll(css, "\n", `\n`)
css = strings.ReplaceAll(css, "\t", `\t`)
buffer.WriteString(`document.querySelector('style').textContent = "`)
buffer.WriteString(css)
buffer.WriteString("\";\n")
if session.rootView != nil {
buffer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`)
viewHTML(session.rootView, 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 {
return session.brige == nil || session.ignoreUpdates
return session.bridge == nil || session.ignoreUpdates
}
func (session *sessionData) setIgnoreViewUpdates(ignore bool) {
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 {
return view.Get(tag)
}
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 {
return view.Set(tag, value)
}
@ -342,27 +407,231 @@ func (session *sessionData) imageManager() *imageManager {
return session.images
}
func (session *sessionData) runScript(script string) {
if session.brige != nil {
session.brige.WriteMessage(script)
func (session *sessionData) callFunc(funcName string, args ...any) {
if session.bridge != nil {
session.bridge.callFunc(funcName, args...)
} else {
ErrorLog("No connection")
}
}
func (session *sessionData) runGetterScript(script string) DataObject { //}, answer chan DataObject) {
if session.brige != nil {
return session.brige.RunGetterScript(script)
func (session *sessionData) updateInnerHTML(htmlID, html string) {
if !session.ignoreViewUpdates() {
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")
result := NewDataObject("error")
result.SetPropertyValue("text", "No connection")
return result
return false
}
func (session *sessionData) handleAnswer(data DataObject) {
session.brige.AnswerReceived(data)
func (session *sessionData) callCanvasFunc(funcName string, args ...any) {
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) {
@ -388,8 +657,8 @@ func (session *sessionData) handleRootSize(data DataObject) {
}
func (session *sessionData) handleResize(data DataObject) {
if node := data.PropertyWithTag("views"); node != nil && node.Type() == ArrayNode {
for _, el := range node.ArrayElements() {
if node := data.PropertyByTag("views"); node != nil && node.Type() == ArrayNode {
for el := range node.ArrayElements() {
if el.IsObject() {
obj := el.Object()
getFloat := func(tag string) float64 {
@ -429,27 +698,181 @@ func (session *sessionData) handleResize(data DataObject) {
}
}
func (session *sessionData) handleViewEvent(command string, data DataObject) {
if viewID, ok := data.PropertyValue("id"); ok {
if view := session.viewByHTMLID(viewID); view != nil {
view.handleCommand(view, command, data)
func (session *sessionData) handleSessionInfo(params DataObject) {
if value, ok := params.PropertyValue("touch"); ok {
session.touchScreen = (value == "1" || value == "true")
}
} else if command != "clickOutsidePopup" {
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
}
}
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) {
title, _ = session.GetString(title)
session.runScript(`document.title = "` + title + `";`)
session.callFunc("setTitle", title)
}
func (session *sessionData) SetTitleColor(color Color) {
session.runScript(`setTitleColor("` + color.cssString() + `");`)
session.callFunc("setTitleColor", color.cssString())
}
func (session *sessionData) RemoteAddr() string {
return session.brige.remoteAddr()
return session.bridge.remoteAddr()
}
func (session *sessionData) OpenURL(urlStr string) {
@ -457,5 +880,47 @@ func (session *sessionData) OpenURL(urlStr string) {
ErrorLog(err.Error())
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
import "time"
// SessionStartListener is the listener interface of a session start event
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)
}
// SessionFinishListener is the listener interface of a session start event
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)
}
// SessionResumeListener is the listener interface of a session resume event
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)
}
// SessionPauseListener is the listener interface of a session pause event
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)
}
// SessionPauseListener is the listener interface of a session disconnect event
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)
}
// SessionPauseListener is the listener interface of a session reconnect event
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)
}
@ -50,13 +63,25 @@ func (session *sessionData) onFinish() {
func (session *sessionData) onPause() {
if session.content != nil {
session.pauseTime = time.Now().Unix()
if listener, ok := session.content.(SessionPauseListener); ok {
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() {
session.pauseTime = 0
if session.content != nil {
if listener, ok := session.content.(SessionResumeListener); ok {
listener.OnResume(session)

View File

@ -35,11 +35,9 @@ func (session *sessionData) constant(tag string, prevTags []string) (string, boo
return result, true
}
for _, separator := range []string{",", " ", ":", ";", "|", "/"} {
if strings.Contains(result, separator) {
if strings.ContainsAny(result, ", :;|/") {
return session.resolveConstantsNext(result, tags)
}
}
if result[0] != '@' {
return result, true
@ -61,7 +59,7 @@ func (session *sessionData) resolveConstants(value string) (string, bool) {
}
func (session *sessionData) resolveConstantsNext(value string, prevTags []string) (string, bool) {
if !strings.Contains(value, "@") {
if !strings.ContainsRune(value, '@') {
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>`
func (session *sessionData) checkboxImage(checked bool) string {
func (session *sessionData) checkboxImage(checked bool, accentColor Color) string {
var borderColor, backgroundColor Color
var ok bool
@ -217,7 +215,9 @@ func (session *sessionData) checkboxImage(checked bool) string {
}
if checked {
if backgroundColor, ok = session.Color("ruiHighlightColor"); !ok {
if accentColor != 0 {
backgroundColor = accentColor
} else if backgroundColor, ok = session.Color("ruiHighlightColor"); !ok {
backgroundColor = 0xFF1A74E8
}
} else if backgroundColor, ok = session.Color("ruiBackgroundColor"); !ok {
@ -244,16 +244,22 @@ func (session *sessionData) checkboxImage(checked bool) 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 == "" {
session.checkboxOff = session.checkboxImage(false)
session.checkboxOff = session.checkboxImage(false, accentColor)
}
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 == "" {
session.checkboxOn = session.checkboxImage(true)
session.checkboxOn = session.checkboxImage(true, accentColor)
}
return session.checkboxOn
}
@ -285,12 +291,14 @@ func (session *sessionData) radiobuttonOffImage() string {
return session.radiobuttonOff
}
func (session *sessionData) radiobuttonOnImage() string {
func (session *sessionData) radiobuttonOnImage(accentColor Color) string {
if session.radiobuttonOn == "" {
var borderColor, backgroundColor Color
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
}
@ -313,7 +321,7 @@ func (session *sessionData) Language() string {
return session.language
}
if session.languages != nil && len(session.languages) > 0 {
if len(session.languages) > 0 {
return session.languages[0]
}
@ -325,15 +333,12 @@ func (session *sessionData) SetLanguage(lang string) {
if lang != session.language {
session.language = lang
if session.rootView != nil {
if session.rootView != nil && session.bridge != nil {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`)
viewHTML(session.rootView, buffer)
buffer.WriteString("';\nscanElementsSize();")
session.runScript(buffer.String())
viewHTML(session.rootView, buffer, "")
session.bridge.updateInnerHTML("ruiRootView", buffer.String())
}
}
}

View File

@ -1,34 +1,5 @@
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) {
if text, ok := session.Constant(tag); ok {
return StringToSizeUnit(text)
@ -39,15 +10,10 @@ func sizeConstant(session Session, tag string) (SizeUnit, bool) {
func updateCSSStyle(htmlID string, session Session) {
if !session.ignoreViewUpdates() {
if view := session.viewByHTMLID(htmlID); view != nil {
var builder viewCSSBuilder
builder.buffer = allocStringBuilder()
builder.buffer.WriteString(`updateCSSStyle('`)
builder.buffer.WriteString(view.htmlID())
builder.buffer.WriteString(`', '`)
builder := viewCSSBuilder{buffer: allocStringBuilder()}
view.cssStyle(view, &builder)
builder.buffer.WriteString(`');`)
view.Session().runScript(builder.finish())
//session.callFunc("updateCSSStyle", view.htmlID(), builder.finish())
session.updateProperty(view.htmlID(), "style", builder.finish())
}
}
}
@ -61,94 +27,24 @@ func updateInnerHTML(htmlID string, session Session) {
view = session.viewByHTMLID(htmlID)
}
if view != nil {
session.callFunc("hideTooltip")
script := allocStringBuilder()
defer freeStringBuilder(script)
script.Grow(32 * 1024)
view.htmlSubviews(view, script)
view.Session().runScript(fmt.Sprintf(`updateInnerHTML('%v', '%v');`, view.htmlID(), script.String()))
//view.updateEventHandlers()
session.updateInnerHTML(view.htmlID(), script.String())
}
}
}
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 {
if startView != nil {
if startView.htmlID() == id {
return startView
}
if container, ok := startView.(ParanetView); ok {
if container, ok := startView.(ParentView); ok {
for _, view := range container.Views() {
if view != nil {
if v := viewByHTMLID(id, view); v != nil {

296
shadow.go
View File

@ -5,30 +5,110 @@ import (
"strings"
)
// Constants for [ShadowProperty] specific properties
const (
// ColorTag is the name of the color property of the shadow.
ColorTag = "color"
// Inset is the name of bool property of the shadow. If it is set to "false" (default) then the shadow
// 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).
// Inset shadows are drawn inside the border (even transparent ones), above the background, but below content.
Inset = "inset"
// 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.
XOffset = "x-offset"
// YOffset is the name of the SizeUnit property of the shadow that determines the shadow vertical offset.
// Negative values place the shadow above the element.
YOffset = "y-offset"
// BlurRadius is the name of the SizeUnit property of the shadow that 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.
BlurRadius = "blur"
// 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.
SpreadRadius = "spread-radius"
// ColorTag is the constant for "color" property tag.
//
// Used by ColumnSeparatorProperty, BorderProperty, OutlineProperty, ShadowProperty.
//
// # Usage in ColumnSeparatorProperty
//
// 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 BorderProperty
//
// Border 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 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
type ViewShadow interface {
// ShadowProperty contains attributes of the view shadow
type ShadowProperty interface {
Properties
fmt.Stringer
stringWriter
@ -37,34 +117,36 @@ type ViewShadow interface {
visible(session Session) bool
}
type viewShadowData struct {
propertyList
type shadowPropertyData struct {
dataProperty
}
// NewViewShadow create the new shadow for a view. Arguments:
// offsetX, offsetY - x and y offset of the shadow
// blurRadius - the blur radius of the shadow
// spreadRadius - the spread radius of the shadow
// color - the color of the shadow
func NewViewShadow(offsetX, offsetY, blurRadius, spreadRadius SizeUnit, color Color) ViewShadow {
return NewShadowWithParams(Params{
XOffset: offsetX,
YOffset: offsetY,
// NewShadow create the new shadow property for a view. Arguments:
// - 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 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 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 is the color of the shadow.
func NewShadow[xOffsetType SizeUnit | int | float64, yOffsetType SizeUnit | int | float64, blurType SizeUnit | int | float64, spreadType SizeUnit | int | float64](
xOffset xOffsetType, yOffset yOffsetType, blurRadius blurType, spreadRadius spreadType, color Color) ShadowProperty {
return NewShadowProperty(Params{
XOffset: xOffset,
YOffset: yOffset,
BlurRadius: blurRadius,
SpreadRadius: spreadRadius,
ColorTag: color,
})
}
// NewInsetViewShadow create the new inset shadow for a view. Arguments:
// offsetX, offsetY - x and y offset of the shadow
// blurRadius - the blur radius of the shadow
// spreadRadius - the spread radius of the shadow
// color - the color of the shadow
func NewInsetViewShadow(offsetX, offsetY, blurRadius, spreadRadius SizeUnit, color Color) ViewShadow {
return NewShadowWithParams(Params{
XOffset: offsetX,
YOffset: offsetY,
// NewInsetShadow create the new inset shadow property for a view. Arguments:
// - 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 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 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 is the color of the shadow.
func NewInsetShadow[xOffsetType SizeUnit | int | float64, yOffsetType SizeUnit | int | float64, blurType SizeUnit | int | float64, spreadType SizeUnit | int | float64](
xOffset xOffsetType, yOffset yOffsetType, blurRadius blurType, spreadRadius spreadType, color Color) ShadowProperty {
return NewShadowProperty(Params{
XOffset: xOffset,
YOffset: yOffset,
BlurRadius: blurRadius,
SpreadRadius: spreadRadius,
ColorTag: color,
@ -72,66 +154,57 @@ func NewInsetViewShadow(offsetX, offsetY, blurRadius, spreadRadius SizeUnit, col
})
}
// NewTextShadow create the new text shadow. Arguments:
// offsetX, offsetY - x and y offset of the shadow
// blurRadius - the blur radius of the shadow
// color - the color of the shadow
func NewTextShadow(offsetX, offsetY, blurRadius SizeUnit, color Color) ViewShadow {
return NewShadowWithParams(Params{
XOffset: offsetX,
YOffset: offsetY,
// NewTextShadow create the new text shadow property. Arguments:
// - 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 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 is the color of the shadow.
func NewTextShadow[xOffsetType SizeUnit | int | float64, yOffsetType SizeUnit | int | float64, blurType SizeUnit | int | float64](
xOffset xOffsetType, yOffset yOffsetType, blurRadius blurType, color Color) ShadowProperty {
return NewShadowProperty(Params{
XOffset: xOffset,
YOffset: yOffset,
BlurRadius: blurRadius,
ColorTag: color,
})
}
// NewShadowWithParams create the new shadow for a view.
func NewShadowWithParams(params Params) ViewShadow {
shadow := new(viewShadowData)
shadow.propertyList.init()
// NewShadowProperty create the new shadow property for a view.
//
// The following properties can be used:
// - "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 {
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 {
shadow.Set(tag, value)
shadow.set(shadow, tag, value)
}
}
}
return shadow
}
// parseViewShadow parse DataObject and create ViewShadow object
func parseViewShadow(object DataObject) ViewShadow {
shadow := new(viewShadowData)
shadow.propertyList.init()
// parseShadowProperty parse DataObject and create ShadowProperty object
func parseShadowProperty(object DataObject) ShadowProperty {
shadow := new(shadowPropertyData)
shadow.init()
parseProperties(shadow, object)
return shadow
}
func (shadow *viewShadowData) Remove(tag string) {
delete(shadow.properties, strings.ToLower(tag))
func (shadow *shadowPropertyData) init() {
shadow.dataProperty.init()
shadow.supportedProperties = []PropertyName{ColorTag, Inset, XOffset, YOffset, BlurRadius, SpreadRadius}
}
func (shadow *viewShadowData) Set(tag string, value any) 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 {
func (shadow *shadowPropertyData) cssStyle(buffer *strings.Builder, session Session, lead string) bool {
color, _ := colorProperty(shadow, ColorTag, session)
offsetX, _ := sizeProperty(shadow, XOffset, session)
offsetY, _ := sizeProperty(shadow, YOffset, session)
@ -163,7 +236,7 @@ func (shadow *viewShadowData) cssStyle(buffer *strings.Builder, session Session,
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)
offsetX, _ := sizeProperty(shadow, XOffset, session)
offsetY, _ := sizeProperty(shadow, YOffset, session)
@ -187,7 +260,7 @@ func (shadow *viewShadowData) cssTextStyle(buffer *strings.Builder, session Sess
return true
}
func (shadow *viewShadowData) visible(session Session) bool {
func (shadow *shadowPropertyData) visible(session Session) bool {
color, _ := colorProperty(shadow, ColorTag, session)
offsetX, _ := sizeProperty(shadow, XOffset, session)
offsetY, _ := sizeProperty(shadow, YOffset, session)
@ -204,62 +277,45 @@ func (shadow *viewShadowData) visible(session Session) bool {
return true
}
func (shadow *viewShadowData) String() string {
func (shadow *shadowPropertyData) String() string {
return runStringWriter(shadow)
}
func (shadow *viewShadowData) writeString(buffer *strings.Builder, indent string) {
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 {
func setShadowProperty(properties Properties, tag PropertyName, value any) bool {
if value == nil {
delete(properties.properties, tag)
properties.setRaw(tag, nil)
return true
}
switch value := value.(type) {
case ViewShadow:
properties.properties[tag] = []ViewShadow{value}
case ShadowProperty:
properties.setRaw(tag, []ShadowProperty{value})
case []ViewShadow:
case []ShadowProperty:
if len(value) == 0 {
delete(properties.properties, tag)
properties.setRaw(tag, nil)
} else {
properties.properties[tag] = value
properties.setRaw(tag, value)
}
case DataValue:
if !value.IsObject() {
return false
}
properties.properties[tag] = []ViewShadow{parseViewShadow(value.Object())}
properties.setRaw(tag, []ShadowProperty{parseShadowProperty(value.Object())})
case []DataValue:
shadows := []ViewShadow{}
shadows := []ShadowProperty{}
for _, data := range value {
if data.IsObject() {
shadows = append(shadows, parseViewShadow(data.Object()))
shadows = append(shadows, parseShadowProperty(data.Object()))
}
}
if len(shadows) == 0 {
return false
}
properties.properties[tag] = shadows
properties.setRaw(tag, shadows)
case string:
obj := NewDataObject(value)
@ -267,7 +323,7 @@ func (properties *propertyList) setShadow(tag string, value any) bool {
notCompatibleType(tag, value)
return false
}
properties.properties[tag] = []ViewShadow{parseViewShadow(obj)}
properties.setRaw(tag, []ShadowProperty{parseShadowProperty(obj)})
default:
notCompatibleType(tag, value)
@ -277,20 +333,20 @@ func (properties *propertyList) setShadow(tag string, value any) bool {
return true
}
func getShadows(properties Properties, tag string) []ViewShadow {
func getShadows(properties Properties, tag PropertyName) []ShadowProperty {
if value := properties.Get(tag); value != nil {
switch value := value.(type) {
case []ViewShadow:
case []ShadowProperty:
return value
case ViewShadow:
return []ViewShadow{value}
case ShadowProperty:
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)
if len(shadows) == 0 {
return ""

View File

@ -7,11 +7,15 @@ import (
)
// SizeFunc describes a function that calculates the SizeUnit size.
//
// 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 {
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
// Args() returns a list of function arguments
Args() []any
@ -28,7 +32,9 @@ type sizeFuncData struct {
func parseSizeFunc(text string) SizeFunc {
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) {
text = strings.Trim(strings.TrimPrefix(text, tag), " ")
last := len(text) - 1
@ -59,7 +65,7 @@ func parseSizeFunc(text string) SizeFunc {
args = append(args, text[start:])
switch tag {
case "sub", "mul", "div":
case "sub", "mul", "div", "mod", "rem", "round-up", "round-down", "round-to-zero", "round":
if len(args) != 2 {
ErrorLogF(`"%s" function needs 2 arguments`, tag)
return nil
@ -73,7 +79,9 @@ func parseSizeFunc(text string) SizeFunc {
data := new(sizeFuncData)
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
}
}
@ -92,19 +100,25 @@ func (data *sizeFuncData) parseArgs(args []any, allowNumber bool) bool {
numberArg := func(index int, value float64) bool {
if allowNumber {
if index == 1 {
if value == 0 && data.tag == "div" {
ErrorLog(`Division by 0 in div function`)
if value == 0 {
if data.tag == "div" || data.tag == "mod" {
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)
return true
} else {
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
}
@ -116,7 +130,7 @@ func (data *sizeFuncData) parseArgs(args []any, allowNumber bool) bool {
return false
}
if arg[0] == '@' {
if ok, _ := isConstantName(arg); ok {
data.args = append(data.args, arg)
} else if val, err := strconv.ParseFloat(arg, 64); err == nil {
return numberArg(i, val)
@ -194,7 +208,7 @@ func (data *sizeFuncData) writeString(topFunc string, buffer *strings.Builder) {
buffer.WriteString(arg.String())
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 "":
buffer.WriteString("calc(")
case "min", "max", "clamp":
case "min", "max", "clamp", "mod", "rem", "round", "round-up", "round-down", "round-to-zero":
bracket = false
default:
@ -228,10 +242,22 @@ func (data *sizeFuncData) writeCSS(topFunc string, buffer *strings.Builder, sess
}
switch data.tag {
case "min", "max", "clamp":
case "min", "max", "clamp", "mod", "rem":
buffer.WriteString(data.tag)
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":
mathFunc(" + ")
@ -276,7 +302,7 @@ func (data *sizeFuncData) writeCSS(topFunc string, buffer *strings.Builder, sess
buffer.WriteString(arg.String())
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.
// 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 {
data := new(sizeFuncData)
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.
// 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 {
data := new(sizeFuncData)
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.
// 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 {
data := new(sizeFuncData)
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).
// 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 {
data := new(sizeFuncData)
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).
// 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)
// or a string which is a text representation of a number.
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).
// 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)
// or a string which is a text representation of a number.
func DivSize(arg0, arg1 any) SizeFunc {
@ -356,13 +382,103 @@ func DivSize(arg0, arg1 any) SizeFunc {
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:
//
// min ≤ value ≤ max -> value;
// value < min -> min;
// 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 {
data := new(sizeFuncData)
data.tag = "clamp"

View File

@ -13,6 +13,7 @@ import (
// SizeInDIP, SizeInPt, SizeInInch, SizeInMM, SizeInFraction
type SizeUnitType uint8
// Constants which represent values of a [SizeUnitType]
const (
// Auto is the SizeUnit type: default value.
Auto SizeUnitType = 0
@ -44,8 +45,14 @@ const (
// SizeUnit describe a size (Value field) and size unit (Type field).
type SizeUnit struct {
// Type or dimension of the value
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
}
@ -55,53 +62,53 @@ func AutoSize() SizeUnit {
}
// Px creates SizeUnit with SizeInPixel type
func Px(value float64) SizeUnit {
return SizeUnit{SizeInPixel, value, nil}
func Px[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInPixel, Value: float64(value), Function: nil}
}
// Em creates SizeUnit with SizeInEM type
func Em(value float64) SizeUnit {
return SizeUnit{SizeInEM, value, nil}
func Em[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInEM, Value: float64(value), Function: nil}
}
// Ex creates SizeUnit with SizeInEX type
func Ex(value float64) SizeUnit {
return SizeUnit{SizeInEX, value, nil}
func Ex[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInEX, Value: float64(value), Function: nil}
}
// Percent creates SizeUnit with SizeInDIP type
func Percent(value float64) SizeUnit {
return SizeUnit{SizeInPercent, value, nil}
func Percent[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInPercent, Value: float64(value), Function: nil}
}
// Pt creates SizeUnit with SizeInPt type
func Pt(value float64) SizeUnit {
return SizeUnit{SizeInPt, value, nil}
func Pt[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInPt, Value: float64(value), Function: nil}
}
// Pc creates SizeUnit with SizeInPc type
func Pc(value float64) SizeUnit {
return SizeUnit{SizeInPc, value, nil}
func Pc[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInPc, Value: float64(value), Function: nil}
}
// Mm creates SizeUnit with SizeInMM type
func Mm(value float64) SizeUnit {
return SizeUnit{SizeInMM, value, nil}
func Mm[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInMM, Value: float64(value), Function: nil}
}
// Cm creates SizeUnit with SizeInCM type
func Cm(value float64) SizeUnit {
return SizeUnit{SizeInCM, value, nil}
func Cm[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInCM, Value: float64(value), Function: nil}
}
// Inch creates SizeUnit with SizeInInch type
func Inch(value float64) SizeUnit {
return SizeUnit{SizeInInch, value, nil}
func Inch[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{Type: SizeInInch, Value: float64(value), Function: nil}
}
// Fr creates SizeUnit with SizeInFraction type
func Fr(value float64) SizeUnit {
return SizeUnit{SizeInFraction, value, nil}
func Fr[T float64 | float32 | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64](value T) SizeUnit {
return SizeUnit{SizeInFraction, float64(value), nil}
}
// 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{}
func scanEmbedStringsDir(fs *embed.FS, dir string) {
func (resources *resourceManager) scanEmbedStringsDir(fs *embed.FS, dir string) {
if files, err := fs.ReadDir(dir); err == nil {
for _, file := range files {
name := file.Name()
path := dir + "/" + name
if file.IsDir() {
scanEmbedStringsDir(fs, path)
resources.scanEmbedStringsDir(fs, path)
} else if strings.ToLower(filepath.Ext(name)) == ".rui" {
if data, err := fs.ReadFile(path); err == nil {
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 {
for _, file := range files {
filename := file.Name()
if filename[0] != '.' {
newPath := path + `/` + filename
if file.IsDir() {
scanStringsDir(newPath)
resources.scanStringsDir(newPath)
} else if strings.ToLower(filepath.Ext(newPath)) == ".rui" {
if data, err := os.ReadFile(newPath); err == nil {
loadStringResources(string(data))
@ -50,8 +50,9 @@ func scanStringsDir(path string) {
}
func loadStringResources(text string) {
data := ParseDataText(text)
if data == nil {
data, err := ParseDataText(text)
if err != nil {
ErrorLog(err.Error())
return
}
@ -61,8 +62,8 @@ func loadStringResources(text string) {
table = map[string]string{}
}
for i := 0; i < obj.PropertyCount(); i++ {
if prop := obj.Property(i); prop != nil && prop.Type() == TextNode {
for prop := range obj.Properties() {
if prop.Type() == TextNode {
table[prop.Tag()] = prop.Text()
}
}
@ -72,8 +73,8 @@ func loadStringResources(text string) {
tag := data.Tag()
if tag == "strings" {
for i := 0; i < data.PropertyCount(); i++ {
if prop := data.Property(i); prop != nil && prop.Type() == ObjectNode {
for prop := range data.Properties() {
if prop.Type() == ObjectNode {
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
// TableAdapter describes the TableView content
// TableAdapter describes the [TableView] content
type TableAdapter interface {
// RowCount returns number of rows in the table
RowCount() int
@ -9,57 +9,70 @@ type TableAdapter interface {
ColumnCount() int
// Cell returns the contents of a table cell. The function can return elements of the following types:
// * string
// * rune
// * float32, float64
// * integer values: int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64
// * bool
// * rui.Color
// * rui.View
// * fmt.Stringer
// * rui.VerticalTableJoin, rui.HorizontalTableJoin
// - string
// - rune
// - float32, float64
// - integer values: int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64
// - bool
// - rui.Color
// - rui.View
// - fmt.Stringer
// - rui.VerticalTableJoin, rui.HorizontalTableJoin
Cell(row, column int) any
}
// TableColumnStyle describes the style of TableView columns.
// To set column styles, you must either implement the TableColumnStyle interface in the table adapter
// TableColumnStyle describes the style of [TableView] columns.
//
// 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.
type TableColumnStyle interface {
// ColumnStyle returns a map of properties which describe the style of the column
ColumnStyle(column int) Params
}
// TableRowStyle describes the style of TableView rows.
// To set row styles, you must either implement the TableRowStyle interface in the table adapter
// TableRowStyle describes the style of [TableView] rows.
//
// 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.
type TableRowStyle interface {
// RowStyle returns a map of properties which describe the style of the row
RowStyle(row int) Params
}
// TableCellStyle describes the style of TableView cells.
// To set row cells, you must either implement the TableCellStyle interface in the table adapter
// TableCellStyle describes the style of [TableView] cells.
//
// 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.
type TableCellStyle interface {
// CellStyle returns a map of properties which describe the style of the cell
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).
//
// 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.
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
}
// 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).
//
// 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.
type TableAllowRowSelection interface {
// AllowRowSelection returns "true" if we allow the user to select particular row in the table
AllowRowSelection(row int) bool
}
// SimpleTableAdapter is implementation of TableAdapter where the content
// SimpleTableAdapter is implementation of [TableAdapter] where the content
// defines as [][]any.
//
// When you assign [][]any value to the "content" property, it is converted to SimpleTableAdapter
type SimpleTableAdapter interface {
TableAdapter
@ -71,7 +84,7 @@ type simpleTableAdapter struct {
columnCount int
}
// TextTableAdapter is implementation of TableAdapter where the content
// TextTableAdapter is implementation of [TableAdapter] where the content
// defines as [][]string.
// When you assign [][]string value to the "content" property, it is converted to TextTableAdapter
type TextTableAdapter interface {
@ -228,11 +241,21 @@ func (adapter *textTableAdapter) Cell(row, column int) any {
return nil
}
type simpleTableRowStyle struct {
type simpleTableLineStyle struct {
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) {
params := style.params[row]
if len(params) > 0 {
@ -241,154 +264,3 @@ func (style *simpleTableRowStyle) RowStyle(row int) Params {
}
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
import "strings"
func (cell *tableCellView) Set(tag string, value any) bool {
return cell.set(strings.ToLower(tag), value)
func newTableCellView(session Session) *tableCellView {
view := new(tableCellView)
view.init(session)
return view
}
func (cell *tableCellView) set(tag string, value any) bool {
switch tag {
case VerticalAlign:
tag = TableVerticalAlign
func (cell *tableCellView) init(session Session) {
cell.viewData.init(session)
cell.normalize = func(tag PropertyName) PropertyName {
if tag == VerticalAlign {
return TableVerticalAlign
}
return tag
}
return cell.viewData.set(tag, value)
}
func (cell *tableCellView) cssStyle(self View, builder cssBuilder) {
session := cell.Session()
cell.viewData.cssViewStyle(builder, session)
writeViewStyleCSS(cell, builder, session, false)
if value, ok := enumProperty(cell, TableVerticalAlign, session, 0); ok {
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.
// 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
if view = getSubview(view, subviewID); view != nil {
if content := view.getRaw(Content); content != nil {
if adapter, ok := content.(TableAdapter); ok {
return adapter
}
}
if obj := view.binding(); obj != nil {
if adapter, ok := obj.(TableAdapter); ok {
return adapter
}
if view != nil {
if tableView, ok := view.(TableView); ok {
return tableView.content()
}
}
@ -40,15 +47,17 @@ func GetTableContent(view View, subviewID ...string) TableAdapter {
}
// 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
if view = getSubview(view, subviewID); view != nil {
for _, tag := range []PropertyName{RowStyle, Content} {
if value := view.getRaw(tag); value != nil {
if style, ok := value.(TableRowStyle); ok {
return style
}
}
if view != nil {
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.
// 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
if view = getSubview(view, subviewID); view != nil {
for _, tag := range []PropertyName{ColumnStyle, Content} {
if value := view.getRaw(tag); value != nil {
if style, ok := value.(TableColumnStyle); ok {
return style
}
}
if view != nil {
if tableView, ok := view.(TableView); ok {
return tableView.getColumnStyle()
}
}
@ -72,43 +83,54 @@ func GetTableColumnStyle(view View, subviewID ...string) TableColumnStyle {
}
// 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
if view = getSubview(view, subviewID); view != nil {
for _, tag := range []PropertyName{CellStyle, Content} {
if value := view.getRaw(tag); value != nil {
if style, ok := value.(TableCellStyle); ok {
return style
}
if view != nil {
if tableView, ok := view.(TableView); ok {
return tableView.getCellStyle()
}
}
return nil
}
return nil
}
// GetTableSelectionMode returns the mode of the TableView elements selection.
// 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 {
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)
// 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 {
return enumStyledProperty(view, subviewID, TableVerticalAlign, TopAlign, false)
}
// 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 {
return intStyledProperty(view, subviewID, HeadHeight, 0)
}
// 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 {
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),
// 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 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 {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if view = getSubview(view, subviewID); view != nil {
if selectionMode := GetTableSelectionMode(view); selectionMode != NoneSelection {
if tableView, ok := view.(TableView); ok {
return tableView.getCurrent()
}
return tableViewCurrent(view)
}
}
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.
// 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) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.Get(TableCellClickedEvent); value != nil {
if result, ok := value.([]func(TableView, int, int)); ok {
return result
}
}
}
return []func(TableView, int, int){}
//
// Result elements can be of the following types:
// - func(rui.TableView, int, int),
// - func(rui.TableView, int),
// - func(rui.TableView),
// - 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 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.
// 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) {
if len(subviewID) > 0 && subviewID[0] != "" {
view = ViewByID(view, subviewID[0])
}
if view != nil {
if value := view.Get(TableCellSelectedEvent); value != nil {
if result, ok := value.([]func(TableView, int, int)); ok {
return result
}
}
}
return []func(TableView, int, int){}
//
// Result elements can be of the following types:
// - func(rui.TableView, int, int),
// - func(rui.TableView, int),
// - func(rui.TableView),
// - 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 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.
// 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) {
return getEventListeners[TableView, int](view, subviewID, TableRowClickedEvent)
//
// Result elements can be of the following types:
// - 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.
// 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) {
return getEventListeners[TableView, int](view, subviewID, TableRowSelectedEvent)
//
// Result elements can be of the following types:
// - 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
// If the second argument (subviewID) is not specified or it is "" then updates the first argument (TableView).
func ReloadTableViewData(view View, subviewID ...string) bool {
var tableView TableView
if len(subviewID) > 0 && subviewID[0] != "" {
if tableView = TableViewByID(view, subviewID[0]); tableView == nil {
return false
}
} else {
var ok bool
if tableView, ok = view.(TableView); !ok {
return false
}
}
if view = getSubview(view, subviewID); view != nil {
if tableView, ok := view.(TableView); ok {
tableView.ReloadTableData()
return true
}
}
return false
}
// 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